Complete MText editor controls and persistence
This commit is contained in:
parent
96ca741f35
commit
af4401ce67
4 changed files with 773 additions and 34 deletions
|
|
@ -2655,6 +2655,31 @@ pub enum Message {
|
||||||
MTextWidth(String),
|
MTextWidth(String),
|
||||||
/// Toolbar character-spacing field changed.
|
/// Toolbar character-spacing field changed.
|
||||||
MTextCharSpace(String),
|
MTextCharSpace(String),
|
||||||
|
/// Undo / redo editor text and inline-format operations.
|
||||||
|
MTextUndo,
|
||||||
|
MTextRedo,
|
||||||
|
/// Convert the selected numerator/separator/denominator to a stacked run.
|
||||||
|
MTextStack,
|
||||||
|
/// Remove inline character formatting from the selection (or all text).
|
||||||
|
MTextClearFormatting,
|
||||||
|
/// Insert a predefined symbol or field token at the caret.
|
||||||
|
MTextInsert(String),
|
||||||
|
/// Per-object annotation flag edited from the text toolbar.
|
||||||
|
MTextAnnotative(bool),
|
||||||
|
/// Column layout controls.
|
||||||
|
MTextColumnMode(String),
|
||||||
|
MTextColumnCount(String),
|
||||||
|
MTextColumnWidth(String),
|
||||||
|
MTextColumnGutter(String),
|
||||||
|
MTextColumnHeight(String),
|
||||||
|
MTextColumnFlowReversed(bool),
|
||||||
|
/// Paragraph indent/spacing controls.
|
||||||
|
MTextParagraphNumber(mtext_editor::ParaNumber, String),
|
||||||
|
MTextFindText(String),
|
||||||
|
MTextReplaceText(String),
|
||||||
|
MTextFindNext,
|
||||||
|
MTextReplaceNext,
|
||||||
|
MTextReplaceAll,
|
||||||
/// Toolbar colour picker (same widget as Properties) — applies to the
|
/// Toolbar colour picker (same widget as Properties) — applies to the
|
||||||
/// selection, or the whole text when nothing is selected.
|
/// selection, or the whole text when nothing is selected.
|
||||||
MTextColorChanged(AcadColor),
|
MTextColorChanged(AcadColor),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
use acadrust::entities::mtext::AttachmentPoint;
|
use acadrust::entities::mtext::AttachmentPoint;
|
||||||
use acadrust::entities::mtext_format::{
|
use acadrust::entities::mtext_format::{
|
||||||
parse_mtext, MTextColor, MTextDocument, MTextFont, MTextParagraph, MTextParagraphAlignment,
|
parse_mtext, MTextColor, MTextDocument, MTextFont, MTextParagraph, MTextParagraphAlignment,
|
||||||
MTextSpan, ParagraphProperties, SpanProperties, StackingData, StackingType,
|
MTextScalar, MTextSpan, ParagraphProperties, SpanProperties, StackingData, StackingType,
|
||||||
};
|
};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Handle, MText};
|
use acadrust::{EntityType, Handle, MText};
|
||||||
|
|
@ -38,6 +38,15 @@ pub enum ParaAlign {
|
||||||
Justify,
|
Justify,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ParaNumber {
|
||||||
|
FirstIndent,
|
||||||
|
LeftIndent,
|
||||||
|
RightIndent,
|
||||||
|
SpaceBefore,
|
||||||
|
SpaceAfter,
|
||||||
|
}
|
||||||
|
|
||||||
/// `pick_list`-friendly wrapper for the 9 attachment points.
|
/// `pick_list`-friendly wrapper for the 9 attachment points.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct JustifyChoice(pub AttachmentPoint);
|
pub struct JustifyChoice(pub AttachmentPoint);
|
||||||
|
|
@ -107,6 +116,23 @@ pub struct MTextEditorState {
|
||||||
/// it is NOT derived from the typed content, so adding characters wraps
|
/// it is NOT derived from the typed content, so adding characters wraps
|
||||||
/// to the next line instead of stretching the box into one long line.
|
/// to the next line instead of stretching the box into one long line.
|
||||||
pub rect_width: f64,
|
pub rect_width: f64,
|
||||||
|
pub rect_height: String,
|
||||||
|
pub annotative: bool,
|
||||||
|
pub column_type: i16,
|
||||||
|
pub column_count: String,
|
||||||
|
pub column_auto_height: bool,
|
||||||
|
pub column_flow_reversed: bool,
|
||||||
|
pub column_width: String,
|
||||||
|
pub column_gutter: String,
|
||||||
|
pub paragraph_first_indent: String,
|
||||||
|
pub paragraph_left_indent: String,
|
||||||
|
pub paragraph_right_indent: String,
|
||||||
|
pub paragraph_space_before: String,
|
||||||
|
pub paragraph_space_after: String,
|
||||||
|
pub find_text: String,
|
||||||
|
pub replace_text: String,
|
||||||
|
undo: Vec<String>,
|
||||||
|
redo: Vec<String>,
|
||||||
/// `Some` when editing an existing entity; `None` for a fresh MText.
|
/// `Some` when editing an existing entity; `None` for a fresh MText.
|
||||||
pub editing: Option<Handle>,
|
pub editing: Option<Handle>,
|
||||||
/// When true the panel shows the rendered preview; when false the raw
|
/// When true the panel shows the rendered preview; when false the raw
|
||||||
|
|
@ -167,6 +193,23 @@ impl MTextEditorState {
|
||||||
// Default box ~20 characters wide; overwritten with the entity's
|
// Default box ~20 characters wide; overwritten with the entity's
|
||||||
// own width when editing an existing MText.
|
// own width when editing an existing MText.
|
||||||
rect_width: (height * 20.0).max(1.0),
|
rect_width: (height * 20.0).max(1.0),
|
||||||
|
rect_height: "0".to_string(),
|
||||||
|
annotative: false,
|
||||||
|
column_type: 0,
|
||||||
|
column_count: "2".to_string(),
|
||||||
|
column_auto_height: false,
|
||||||
|
column_flow_reversed: false,
|
||||||
|
column_width: (height * 10.0).to_string(),
|
||||||
|
column_gutter: (height * 5.0).to_string(),
|
||||||
|
paragraph_first_indent: "0".to_string(),
|
||||||
|
paragraph_left_indent: "0".to_string(),
|
||||||
|
paragraph_right_indent: "0".to_string(),
|
||||||
|
paragraph_space_before: "0".to_string(),
|
||||||
|
paragraph_space_after: "0".to_string(),
|
||||||
|
find_text: String::new(),
|
||||||
|
replace_text: String::new(),
|
||||||
|
undo: Vec::new(),
|
||||||
|
redo: Vec::new(),
|
||||||
editing,
|
editing,
|
||||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||||
original: None,
|
original: None,
|
||||||
|
|
@ -274,8 +317,53 @@ impl MTextEditorState {
|
||||||
mt.attachment_point = self.attachment;
|
mt.attachment_point = self.attachment;
|
||||||
mt.line_spacing_factor = self.line_spacing as f64;
|
mt.line_spacing_factor = self.line_spacing as f64;
|
||||||
mt.style = self.style.clone();
|
mt.style = self.style.clone();
|
||||||
|
mt.is_annotative = self.annotative;
|
||||||
|
let rectangle_height = self.rect_height.trim().parse::<f64>().unwrap_or(0.0);
|
||||||
|
mt.rectangle_height = (rectangle_height > 0.0).then_some(rectangle_height);
|
||||||
|
mt.column_data.column_type = self.column_type;
|
||||||
|
if self.column_type == 0 {
|
||||||
|
mt.column_data.column_count = 0;
|
||||||
|
mt.column_data.heights.clear();
|
||||||
|
} else {
|
||||||
|
mt.column_data.column_count = self
|
||||||
|
.column_count
|
||||||
|
.trim()
|
||||||
|
.parse::<i32>()
|
||||||
|
.ok()
|
||||||
|
.filter(|count| *count > 0)
|
||||||
|
.unwrap_or(1);
|
||||||
|
mt.column_data.auto_height = self.column_auto_height;
|
||||||
|
mt.column_data.flow_reversed = self.column_flow_reversed;
|
||||||
|
if let Ok(width) = self.column_width.trim().parse::<f64>() {
|
||||||
|
if width > 0.0 {
|
||||||
|
mt.column_data.width = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(gutter) = self.column_gutter.trim().parse::<f64>() {
|
||||||
|
if gutter >= 0.0 {
|
||||||
|
mt.column_data.gutter = gutter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.column_auto_height && rectangle_height > 0.0 {
|
||||||
|
mt.column_data.heights = vec![
|
||||||
|
rectangle_height;
|
||||||
|
mt.column_data.column_count.max(1) as usize
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
mt
|
mt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_undo(&mut self) {
|
||||||
|
let value = self.content.text();
|
||||||
|
if self.undo.last().is_none_or(|last| last != &value) {
|
||||||
|
self.undo.push(value);
|
||||||
|
if self.undo.len() > 100 {
|
||||||
|
self.undo.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.redo.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a numeric field, returning `Some(v)` only when it differs from the
|
/// Parse a numeric field, returning `Some(v)` only when it differs from the
|
||||||
|
|
@ -595,6 +683,24 @@ fn vertical_caret_target(
|
||||||
best.1
|
best.1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn seed_mtext_state(state: &mut MTextEditorState, m: &MText) {
|
||||||
|
state.attachment = m.attachment_point;
|
||||||
|
state.line_spacing = m.line_spacing_factor as f32;
|
||||||
|
state.rect_width = m.rectangle_width.max(0.0);
|
||||||
|
state.rect_height = m.rectangle_height.unwrap_or(0.0).max(0.0).to_string();
|
||||||
|
state.annotative = m.is_annotative;
|
||||||
|
state.column_type = m.column_data.column_type;
|
||||||
|
state.column_count = m.column_data.column_count.max(1).to_string();
|
||||||
|
state.column_auto_height = m.column_data.auto_height;
|
||||||
|
state.column_flow_reversed = m.column_data.flow_reversed;
|
||||||
|
state.column_width = m.column_data.width.max(0.0).to_string();
|
||||||
|
state.column_gutter = m.column_data.gutter.max(0.0).to_string();
|
||||||
|
if !m.style.trim().is_empty() {
|
||||||
|
state.style = m.style.clone();
|
||||||
|
}
|
||||||
|
state.original = Some(m.clone());
|
||||||
|
}
|
||||||
|
|
||||||
impl super::OpenCADStudio {
|
impl super::OpenCADStudio {
|
||||||
/// Open the in-place editor for a new (`handle = None`) or existing MText.
|
/// Open the in-place editor for a new (`handle = None`) or existing MText.
|
||||||
/// Open the rich MText editor for a new or existing MText / MultiLeader.
|
/// Open the rich MText editor for a new or existing MText / MultiLeader.
|
||||||
|
|
@ -605,6 +711,7 @@ impl super::OpenCADStudio {
|
||||||
handle: Option<Handle>,
|
handle: Option<Handle>,
|
||||||
initial: &str,
|
initial: &str,
|
||||||
height: f64,
|
height: f64,
|
||||||
|
template: Option<MText>,
|
||||||
) {
|
) {
|
||||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -614,20 +721,11 @@ impl super::OpenCADStudio {
|
||||||
state.screen_anchor = p;
|
state.screen_anchor = p;
|
||||||
}
|
}
|
||||||
// Seed attachment / line-spacing / box width from the entity being edited.
|
// Seed attachment / line-spacing / box width from the entity being edited.
|
||||||
|
let has_template = template.is_some();
|
||||||
if let Some(h) = handle {
|
if let Some(h) = handle {
|
||||||
match self.tabs[self.active_tab].scene.document.get_entity(h) {
|
match self.tabs[self.active_tab].scene.document.get_entity(h) {
|
||||||
Some(EntityType::MText(m)) => {
|
Some(EntityType::MText(m)) => {
|
||||||
state.attachment = m.attachment_point;
|
seed_mtext_state(&mut state, m);
|
||||||
state.line_spacing = m.line_spacing_factor as f32;
|
|
||||||
if !m.style.trim().is_empty() {
|
|
||||||
state.style = m.style.clone();
|
|
||||||
}
|
|
||||||
if m.rectangle_width > 0.0 {
|
|
||||||
state.rect_width = m.rectangle_width;
|
|
||||||
}
|
|
||||||
// Keep the whole entity so `build_mtext` preserves the fields
|
|
||||||
// the toolbar can't edit (columns, rotation, background…).
|
|
||||||
state.original = Some(m.clone());
|
|
||||||
}
|
}
|
||||||
Some(EntityType::MultiLeader(ml)) => {
|
Some(EntityType::MultiLeader(ml)) => {
|
||||||
state.line_spacing = ml.context.line_spacing_factor as f32;
|
state.line_spacing = ml.context.line_spacing_factor as f32;
|
||||||
|
|
@ -637,6 +735,8 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
} else if let Some(m) = template {
|
||||||
|
seed_mtext_state(&mut state, &m);
|
||||||
} else {
|
} else {
|
||||||
// New MText inherits the document's current text style (STYLE),
|
// New MText inherits the document's current text style (STYLE),
|
||||||
// not the "Standard" default. See #92.
|
// not the "Standard" default. See #92.
|
||||||
|
|
@ -659,7 +759,7 @@ impl super::OpenCADStudio {
|
||||||
// The preview uses a fixed on-screen text size. Therefore the initial
|
// The preview uses a fixed on-screen text size. Therefore the initial
|
||||||
// wrap width is the drawing-unit span that exactly reaches the right
|
// wrap width is the drawing-unit span that exactly reaches the right
|
||||||
// edge of the editor, placing both ruler and slider at their maximum.
|
// edge of the editor, placing both ruler and slider at their maximum.
|
||||||
if handle.is_none() {
|
if handle.is_none() && !has_template {
|
||||||
let initial_width = self.mtext_editor.as_ref().map(|ed| {
|
let initial_width = self.mtext_editor.as_ref().map(|ed| {
|
||||||
super::view::overlay::MTEXT_EDITOR_WRITING_WIDTH / ed.preview_scale()
|
super::view::overlay::MTEXT_EDITOR_WRITING_WIDTH / ed.preview_scale()
|
||||||
});
|
});
|
||||||
|
|
@ -746,6 +846,318 @@ impl super::OpenCADStudio {
|
||||||
/// Splice text around the preview selection (visible-char range) in the
|
/// Splice text around the preview selection (visible-char range) in the
|
||||||
/// raw value. `case` optionally transforms the selected slice. Returns
|
/// raw value. `case` optionally transforms the selected slice. Returns
|
||||||
/// true when a preview selection was present and applied.
|
/// true when a preview selection was present and applied.
|
||||||
|
pub(super) fn mtext_undo(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
if let Some(previous) = ed.undo.pop() {
|
||||||
|
ed.redo.push(ed.content.text());
|
||||||
|
ed.content = text_editor::Content::with_text(&previous);
|
||||||
|
ed.caret = previous.chars().count();
|
||||||
|
ed.sel = Some((ed.caret, ed.caret));
|
||||||
|
ed.sel_anchor = ed.caret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_redo(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
if let Some(next) = ed.redo.pop() {
|
||||||
|
ed.undo.push(ed.content.text());
|
||||||
|
ed.content = text_editor::Content::with_text(&next);
|
||||||
|
ed.caret = next.chars().count();
|
||||||
|
ed.sel = Some((ed.caret, ed.caret));
|
||||||
|
ed.sel_anchor = ed.caret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_apply_span_number(&mut self, field: &'static str, value: String) {
|
||||||
|
let Some(number) = value.trim().replace(',', ".").parse::<f64>().ok() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let valid = match field {
|
||||||
|
"height" => number > 0.0,
|
||||||
|
"oblique" => (-85.0..=85.0).contains(&number),
|
||||||
|
"width" => number > 0.0,
|
||||||
|
"tracking" => number.is_finite(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !valid {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let has_selection = self
|
||||||
|
.mtext_editor
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|editor| editor.sel)
|
||||||
|
.is_some_and(|(start, end)| start < end);
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
if has_selection {
|
||||||
|
let (start, end) = ed.sel.unwrap();
|
||||||
|
ed.record_undo();
|
||||||
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
|
let end = end.min(cells.len());
|
||||||
|
for cell in &mut cells[start.min(end)..end] {
|
||||||
|
if let Cell::Char(_, props) | Cell::Stack { props, .. } = cell {
|
||||||
|
match field {
|
||||||
|
"height" => props.height = Some(MTextScalar::Absolute(number)),
|
||||||
|
"oblique" => props.oblique_angle = (number != 0.0).then_some(number),
|
||||||
|
"width" => props.width_factor = (number != 1.0).then_some(number),
|
||||||
|
"tracking" => props.tracking = (number != 0.0).then_some(number),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ed.content = text_editor::Content::with_text(
|
||||||
|
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||||
|
);
|
||||||
|
ed.sel = Some((start, end));
|
||||||
|
ed.caret = end;
|
||||||
|
} else {
|
||||||
|
match field {
|
||||||
|
"height" => ed.height = value,
|
||||||
|
"oblique" => ed.oblique = value,
|
||||||
|
"width" => ed.width = value,
|
||||||
|
"tracking" => ed.char_space = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_apply_paragraph_number(&mut self, field: ParaNumber, value: String) {
|
||||||
|
let Some(number) = value.trim().replace(',', ".").parse::<f64>().ok() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !number.is_finite() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.record_undo();
|
||||||
|
match field {
|
||||||
|
ParaNumber::FirstIndent => ed.paragraph_first_indent = value,
|
||||||
|
ParaNumber::LeftIndent => ed.paragraph_left_indent = value,
|
||||||
|
ParaNumber::RightIndent => ed.paragraph_right_indent = value,
|
||||||
|
ParaNumber::SpaceBefore => ed.paragraph_space_before = value,
|
||||||
|
ParaNumber::SpaceAfter => ed.paragraph_space_after = value,
|
||||||
|
}
|
||||||
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
let cells = doc_to_cells(&ed.doc);
|
||||||
|
let (start, end) = ed
|
||||||
|
.sel
|
||||||
|
.filter(|(start, end)| start < end)
|
||||||
|
.unwrap_or((ed.caret, ed.caret));
|
||||||
|
let paragraph_of = |index: usize| {
|
||||||
|
cells
|
||||||
|
.iter()
|
||||||
|
.take(index.min(cells.len()))
|
||||||
|
.filter(|cell| matches!(cell, Cell::Break(..)))
|
||||||
|
.count()
|
||||||
|
};
|
||||||
|
let first = paragraph_of(start);
|
||||||
|
let last = paragraph_of(end.saturating_sub((end > start) as usize));
|
||||||
|
let mut doc = cells_to_doc(¶0, &cells);
|
||||||
|
for paragraph in doc
|
||||||
|
.paragraphs
|
||||||
|
.iter_mut()
|
||||||
|
.skip(first)
|
||||||
|
.take(last.saturating_sub(first) + 1)
|
||||||
|
{
|
||||||
|
let optional = (number != 0.0).then_some(number);
|
||||||
|
match field {
|
||||||
|
ParaNumber::FirstIndent => paragraph.properties.first_line_indent = optional,
|
||||||
|
ParaNumber::LeftIndent => paragraph.properties.left_margin = optional,
|
||||||
|
ParaNumber::RightIndent => paragraph.properties.right_margin = optional,
|
||||||
|
ParaNumber::SpaceBefore => paragraph.properties.spacing_before = optional,
|
||||||
|
ParaNumber::SpaceAfter => paragraph.properties.spacing_after = optional,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ed.content = text_editor::Content::with_text(&doc.to_mtext_string());
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_stack_selection(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
let Some((start, end)) = ed.sel.filter(|(start, end)| start < end) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
|
let end = end.min(cells.len());
|
||||||
|
let plain = cells_to_doc(&ParagraphProperties::default(), &cells[start..end])
|
||||||
|
.to_plain_text();
|
||||||
|
let Some((split, separator)) = plain
|
||||||
|
.char_indices()
|
||||||
|
.find(|(_, ch)| matches!(ch, '/' | '#' | '^'))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let separator_len = separator.len_utf8();
|
||||||
|
let data = StackingData {
|
||||||
|
numerator: plain[..split].to_string(),
|
||||||
|
denominator: plain[split + separator_len..].to_string(),
|
||||||
|
stacking_type: StackingType::from_char(separator),
|
||||||
|
};
|
||||||
|
if data.numerator.is_empty() || data.denominator.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let props = cells[start..end]
|
||||||
|
.iter()
|
||||||
|
.find_map(|cell| match cell {
|
||||||
|
Cell::Char(_, props) | Cell::Stack { props, .. } => Some(props.clone()),
|
||||||
|
Cell::Break(..) => None,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
ed.record_undo();
|
||||||
|
let replacement = flatten_stack(&data)
|
||||||
|
.chars()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, _)| Cell::Stack {
|
||||||
|
data: data.clone(),
|
||||||
|
props: props.clone(),
|
||||||
|
head: index == 0,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let replacement_len = replacement.len();
|
||||||
|
cells.splice(start..end, replacement);
|
||||||
|
let new_end = start + replacement_len;
|
||||||
|
ed.content = text_editor::Content::with_text(
|
||||||
|
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||||
|
);
|
||||||
|
ed.sel = Some((start, new_end));
|
||||||
|
ed.caret = new_end;
|
||||||
|
ed.sel_anchor = start;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_clear_formatting(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
|
let (start, end) = ed
|
||||||
|
.sel
|
||||||
|
.filter(|(start, end)| start < end)
|
||||||
|
.unwrap_or((0, cells.len()));
|
||||||
|
let end = end.min(cells.len());
|
||||||
|
ed.record_undo();
|
||||||
|
for cell in &mut cells[start.min(end)..end] {
|
||||||
|
if let Cell::Char(_, props) | Cell::Stack { props, .. } = cell {
|
||||||
|
*props = SpanProperties::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ed.content = text_editor::Content::with_text(
|
||||||
|
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||||
|
);
|
||||||
|
ed.sel = Some((start, end));
|
||||||
|
ed.caret = end;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_find_next(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
let needle: Vec<char> = ed.find_text.chars().collect();
|
||||||
|
if needle.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cells = doc_to_cells(&ed.doc);
|
||||||
|
let chars: Vec<Option<char>> = cells
|
||||||
|
.iter()
|
||||||
|
.map(|cell| match cell {
|
||||||
|
Cell::Char(ch, _) => Some(*ch),
|
||||||
|
Cell::Break(..) => Some('\n'),
|
||||||
|
Cell::Stack { .. } => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let start = ed.caret.min(chars.len());
|
||||||
|
let found = (start..=chars.len().saturating_sub(needle.len()))
|
||||||
|
.chain(0..start.min(chars.len().saturating_sub(needle.len()) + 1))
|
||||||
|
.find(|index| {
|
||||||
|
chars[*index..*index + needle.len()]
|
||||||
|
.iter()
|
||||||
|
.zip(&needle)
|
||||||
|
.all(|(actual, expected)| actual == &Some(*expected))
|
||||||
|
});
|
||||||
|
if let Some(found) = found {
|
||||||
|
ed.sel_anchor = found;
|
||||||
|
ed.sel = Some((found, found + needle.len()));
|
||||||
|
ed.caret = found + needle.len();
|
||||||
|
ed.caret_blink_on = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_replace_next(&mut self) {
|
||||||
|
let replacement = self
|
||||||
|
.mtext_editor
|
||||||
|
.as_ref()
|
||||||
|
.map(|editor| editor.replace_text.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let matches = self.mtext_editor.as_ref().is_some_and(|editor| {
|
||||||
|
let Some((start, end)) = editor.sel.filter(|(start, end)| start < end) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let cells = doc_to_cells(&editor.doc);
|
||||||
|
cells_to_doc(
|
||||||
|
&ParagraphProperties::default(),
|
||||||
|
&cells[start.min(cells.len())..end.min(cells.len())],
|
||||||
|
)
|
||||||
|
.to_plain_text()
|
||||||
|
== editor.find_text
|
||||||
|
});
|
||||||
|
if matches {
|
||||||
|
self.mtext_type(&replacement);
|
||||||
|
}
|
||||||
|
self.mtext_find_next();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn mtext_replace_all(&mut self) {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
let needle: Vec<char> = ed.find_text.chars().collect();
|
||||||
|
if needle.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
|
let replacement = ed.replace_text.clone();
|
||||||
|
let mut matches = Vec::new();
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index + needle.len() <= cells.len() {
|
||||||
|
let found = cells[index..index + needle.len()]
|
||||||
|
.iter()
|
||||||
|
.zip(&needle)
|
||||||
|
.all(|(cell, expected)| matches!(cell, Cell::Char(actual, _) if actual == expected));
|
||||||
|
if found {
|
||||||
|
matches.push(index);
|
||||||
|
index += needle.len();
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ed.record_undo();
|
||||||
|
for start in matches.into_iter().rev() {
|
||||||
|
let props = insert_props(&cells, start);
|
||||||
|
let paragraph = para_props_at(¶0, &cells, start);
|
||||||
|
let replacement_cells = str_to_cells(&replacement, &props, ¶graph);
|
||||||
|
cells.splice(start..start + needle.len(), replacement_cells);
|
||||||
|
}
|
||||||
|
ed.content = text_editor::Content::with_text(
|
||||||
|
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||||
|
);
|
||||||
|
ed.caret = cells.len();
|
||||||
|
ed.sel = Some((ed.caret, ed.caret));
|
||||||
|
ed.sel_anchor = ed.caret;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle a character format over the preview selection, on the structured
|
/// Toggle a character format over the preview selection, on the structured
|
||||||
/// span properties. The stroke-font renderer has no true bold, so Bold
|
/// span properties. The stroke-font renderer has no true bold, so Bold
|
||||||
/// switches the run to the heavier "Gothic" face; Italic applies a 15°
|
/// switches the run to the heavier "Gothic" face; Italic applies a 15°
|
||||||
|
|
@ -798,6 +1210,7 @@ impl super::OpenCADStudio {
|
||||||
if a >= b {
|
if a >= b {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ed.record_undo();
|
||||||
match kind {
|
match kind {
|
||||||
MTextFmt::Underline => {
|
MTextFmt::Underline => {
|
||||||
let on = !all_have(&cells[a..b], |p| p.underline());
|
let on = !all_have(&cells[a..b], |p| p.underline());
|
||||||
|
|
@ -882,6 +1295,7 @@ impl super::OpenCADStudio {
|
||||||
/// Set the alignment of every paragraph the selection (or caret) touches.
|
/// Set the alignment of every paragraph the selection (or caret) touches.
|
||||||
pub(super) fn mtext_apply_align(&mut self, align: ParaAlign) {
|
pub(super) fn mtext_apply_align(&mut self, align: ParaAlign) {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.record_undo();
|
||||||
let para0 = doc_para0(&ed.doc);
|
let para0 = doc_para0(&ed.doc);
|
||||||
let cells = doc_to_cells(&ed.doc);
|
let cells = doc_to_cells(&ed.doc);
|
||||||
let (a, b) = ed
|
let (a, b) = ed
|
||||||
|
|
@ -933,6 +1347,7 @@ impl super::OpenCADStudio {
|
||||||
s
|
s
|
||||||
};
|
};
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.record_undo();
|
||||||
// Edit the structured document as a flat cell list, then serialize
|
// Edit the structured document as a flat cell list, then serialize
|
||||||
// it back into the raw content the preview/commit still read.
|
// it back into the raw content the preview/commit still read.
|
||||||
let para0 = doc_para0(&ed.doc);
|
let para0 = doc_para0(&ed.doc);
|
||||||
|
|
@ -962,6 +1377,7 @@ impl super::OpenCADStudio {
|
||||||
/// Delete the selection, or the visible character before the caret.
|
/// Delete the selection, or the visible character before the caret.
|
||||||
pub(super) fn mtext_backspace(&mut self) {
|
pub(super) fn mtext_backspace(&mut self) {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.record_undo();
|
||||||
let para0 = doc_para0(&ed.doc);
|
let para0 = doc_para0(&ed.doc);
|
||||||
let mut cells = doc_to_cells(&ed.doc);
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
let count = cells.len();
|
let count = cells.len();
|
||||||
|
|
@ -985,6 +1401,7 @@ impl super::OpenCADStudio {
|
||||||
/// Delete the selection, or the visible character at the caret.
|
/// Delete the selection, or the visible character at the caret.
|
||||||
pub(super) fn mtext_delete(&mut self) {
|
pub(super) fn mtext_delete(&mut self) {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.record_undo();
|
||||||
let para0 = doc_para0(&ed.doc);
|
let para0 = doc_para0(&ed.doc);
|
||||||
let mut cells = doc_to_cells(&ed.doc);
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
let count = cells.len();
|
let count = cells.len();
|
||||||
|
|
@ -1104,6 +1521,7 @@ impl super::OpenCADStudio {
|
||||||
let mut cells = doc_to_cells(&ed.doc);
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
let b = b.min(cells.len());
|
let b = b.min(cells.len());
|
||||||
if a < b {
|
if a < b {
|
||||||
|
ed.record_undo();
|
||||||
for c in &mut cells[a..b] {
|
for c in &mut cells[a..b] {
|
||||||
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
||||||
let (bold, italic) = p
|
let (bold, italic) = p
|
||||||
|
|
@ -1155,6 +1573,7 @@ impl super::OpenCADStudio {
|
||||||
let mut cells = doc_to_cells(&ed.doc);
|
let mut cells = doc_to_cells(&ed.doc);
|
||||||
let b = b.min(cells.len());
|
let b = b.min(cells.len());
|
||||||
if a < b {
|
if a < b {
|
||||||
|
ed.record_undo();
|
||||||
for c in &mut cells[a..b] {
|
for c in &mut cells[a..b] {
|
||||||
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
||||||
p.color = mcolor.clone();
|
p.color = mcolor.clone();
|
||||||
|
|
@ -1217,7 +1636,8 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
let body_empty = ed.content.text().trim().is_empty();
|
let body_empty = ed.content.text().trim().is_empty();
|
||||||
let mut mt = ed.build_mtext();
|
let mut mt = ed.build_mtext();
|
||||||
let annotative = ed.editing.is_none()
|
let annotative = mt.is_annotative
|
||||||
|
|| ed.editing.is_none()
|
||||||
&& crate::scene::annotative::text_style_is_annotative(
|
&& crate::scene::annotative::text_style_is_annotative(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&mt.style,
|
&mt.style,
|
||||||
|
|
@ -1240,9 +1660,12 @@ impl super::OpenCADStudio {
|
||||||
Some(EntityType::MText(t)) => {
|
Some(EntityType::MText(t)) => {
|
||||||
t.value = mt.value;
|
t.value = mt.value;
|
||||||
t.height = mt.height;
|
t.height = mt.height;
|
||||||
|
t.style = mt.style;
|
||||||
t.attachment_point = mt.attachment_point;
|
t.attachment_point = mt.attachment_point;
|
||||||
t.line_spacing_factor = mt.line_spacing_factor;
|
t.line_spacing_factor = mt.line_spacing_factor;
|
||||||
t.rectangle_width = mt.rectangle_width;
|
t.rectangle_width = mt.rectangle_width;
|
||||||
|
t.rectangle_height = mt.rectangle_height;
|
||||||
|
t.column_data = mt.column_data;
|
||||||
}
|
}
|
||||||
Some(EntityType::MultiLeader(ml)) => {
|
Some(EntityType::MultiLeader(ml)) => {
|
||||||
ml.context.text_string = mt.value;
|
ml.context.text_string = mt.value;
|
||||||
|
|
@ -1255,9 +1678,25 @@ impl super::OpenCADStudio {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Keep the displayed annotation context in sync.
|
// Keep the displayed annotation context in sync.
|
||||||
|
if matches!(self.tabs[i].scene.document.get_entity(h), Some(EntityType::MText(_))) {
|
||||||
|
crate::scene::annotative::set_entity_annotative(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
h,
|
||||||
|
annotative,
|
||||||
|
);
|
||||||
|
if annotative {
|
||||||
|
if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() {
|
||||||
|
crate::scene::annotative::create_annotation_context(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
h,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
self.tabs[i].scene.document.get_entity(h),
|
self.tabs[i].scene.document.get_entity(h),
|
||||||
Some(EntityType::MultiLeader(_))
|
Some(EntityType::MText(_) | EntityType::MultiLeader(_))
|
||||||
) {
|
) {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -1283,7 +1722,6 @@ impl super::OpenCADStudio {
|
||||||
position.y,
|
position.y,
|
||||||
position.z,
|
position.z,
|
||||||
);
|
);
|
||||||
mt.rotation = 0.0;
|
|
||||||
self.push_undo_snapshot(i, "MTEXT");
|
self.push_undo_snapshot(i, "MTEXT");
|
||||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
||||||
if annotative {
|
if annotative {
|
||||||
|
|
@ -1321,7 +1759,8 @@ impl super::OpenCADStudio {
|
||||||
),
|
),
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let annotative = editing.is_none()
|
let annotative = mt.is_annotative
|
||||||
|
|| editing.is_none()
|
||||||
&& crate::scene::annotative::text_style_is_annotative(
|
&& crate::scene::annotative::text_style_is_annotative(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&mt.style,
|
&mt.style,
|
||||||
|
|
@ -1341,9 +1780,12 @@ impl super::OpenCADStudio {
|
||||||
Some(EntityType::MText(t)) => {
|
Some(EntityType::MText(t)) => {
|
||||||
t.value = mt.value;
|
t.value = mt.value;
|
||||||
t.height = mt.height;
|
t.height = mt.height;
|
||||||
|
t.style = mt.style;
|
||||||
t.attachment_point = mt.attachment_point;
|
t.attachment_point = mt.attachment_point;
|
||||||
t.line_spacing_factor = mt.line_spacing_factor;
|
t.line_spacing_factor = mt.line_spacing_factor;
|
||||||
t.rectangle_width = mt.rectangle_width;
|
t.rectangle_width = mt.rectangle_width;
|
||||||
|
t.rectangle_height = mt.rectangle_height;
|
||||||
|
t.column_data = mt.column_data;
|
||||||
}
|
}
|
||||||
Some(EntityType::MultiLeader(ml)) => {
|
Some(EntityType::MultiLeader(ml)) => {
|
||||||
ml.context.text_string = mt.value;
|
ml.context.text_string = mt.value;
|
||||||
|
|
@ -1355,9 +1797,25 @@ impl super::OpenCADStudio {
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
if matches!(self.tabs[i].scene.document.get_entity(h), Some(EntityType::MText(_))) {
|
||||||
|
crate::scene::annotative::set_entity_annotative(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
h,
|
||||||
|
annotative,
|
||||||
|
);
|
||||||
|
if annotative {
|
||||||
|
if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() {
|
||||||
|
crate::scene::annotative::create_annotation_context(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
h,
|
||||||
|
scale,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
self.tabs[i].scene.document.get_entity(h),
|
self.tabs[i].scene.document.get_entity(h),
|
||||||
Some(EntityType::MultiLeader(_))
|
Some(EntityType::MText(_) | EntityType::MultiLeader(_))
|
||||||
) {
|
) {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -1383,7 +1841,6 @@ impl super::OpenCADStudio {
|
||||||
position.y,
|
position.y,
|
||||||
position.z,
|
position.z,
|
||||||
);
|
);
|
||||||
mt.rotation = 0.0;
|
|
||||||
self.push_undo_snapshot(i, "MTEXT");
|
self.push_undo_snapshot(i, "MTEXT");
|
||||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
|
@ -1400,6 +1857,11 @@ impl super::OpenCADStudio {
|
||||||
// Bind the editor to the fresh entity so the next Apply updates it.
|
// Bind the editor to the fresh entity so the next Apply updates it.
|
||||||
if let (Some(h), Some(ed)) = (handle, self.mtext_editor.as_mut()) {
|
if let (Some(h), Some(ed)) = (handle, self.mtext_editor.as_mut()) {
|
||||||
ed.editing = Some(h);
|
ed.editing = Some(h);
|
||||||
|
if let Some(EntityType::MText(mtext)) =
|
||||||
|
self.tabs[i].scene.document.get_entity(h)
|
||||||
|
{
|
||||||
|
ed.original = Some(mtext.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
|
|
|
||||||
|
|
@ -3880,10 +3880,7 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextHeight(s) => {
|
Message::MTextHeight(s) => {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
self.mtext_apply_span_number("height", s);
|
||||||
ed.height = s;
|
|
||||||
}
|
|
||||||
self.rebuild_mtext_preview();
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextRectWidth(width) => {
|
Message::MTextRectWidth(width) => {
|
||||||
|
|
@ -3915,26 +3912,128 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextOblique(s) => {
|
Message::MTextOblique(s) => {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
self.mtext_apply_span_number("oblique", s);
|
||||||
ed.oblique = s;
|
|
||||||
}
|
|
||||||
self.rebuild_mtext_preview();
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextWidth(s) => {
|
Message::MTextWidth(s) => {
|
||||||
|
self.mtext_apply_span_number("width", s);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextCharSpace(s) => {
|
||||||
|
self.mtext_apply_span_number("tracking", s);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextUndo => {
|
||||||
|
self.mtext_undo();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextRedo => {
|
||||||
|
self.mtext_redo();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextStack => {
|
||||||
|
self.mtext_stack_selection();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextClearFormatting => {
|
||||||
|
self.mtext_clear_formatting();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextInsert(value) => {
|
||||||
|
self.mtext_type(&value);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextAnnotative(value) => {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
ed.width = s;
|
ed.annotative = value;
|
||||||
}
|
}
|
||||||
self.rebuild_mtext_preview();
|
self.rebuild_mtext_preview();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MTextCharSpace(s) => {
|
Message::MTextColumnMode(mode) => {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
ed.char_space = s;
|
match mode.as_str() {
|
||||||
|
"Static" => {
|
||||||
|
ed.column_type = 1;
|
||||||
|
ed.column_auto_height = false;
|
||||||
|
}
|
||||||
|
"Dynamic auto" => {
|
||||||
|
ed.column_type = 2;
|
||||||
|
ed.column_auto_height = true;
|
||||||
|
}
|
||||||
|
"Dynamic manual" => {
|
||||||
|
ed.column_type = 2;
|
||||||
|
ed.column_auto_height = false;
|
||||||
|
}
|
||||||
|
_ => ed.column_type = 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.rebuild_mtext_preview();
|
self.rebuild_mtext_preview();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::MTextColumnCount(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.column_count = value;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextColumnWidth(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.column_width = value;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextColumnGutter(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.column_gutter = value;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextColumnHeight(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.rect_height = value;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextColumnFlowReversed(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.column_flow_reversed = value;
|
||||||
|
}
|
||||||
|
self.rebuild_mtext_preview();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextParagraphNumber(field, value) => {
|
||||||
|
self.mtext_apply_paragraph_number(field, value);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextFindText(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.find_text = value;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextReplaceText(value) => {
|
||||||
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
|
ed.replace_text = value;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextFindNext => {
|
||||||
|
self.mtext_find_next();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextReplaceNext => {
|
||||||
|
self.mtext_replace_next();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::MTextReplaceAll => {
|
||||||
|
self.mtext_replace_all();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::MTextJustify(ap) => {
|
Message::MTextJustify(ap) => {
|
||||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||||
ed.attachment = ap;
|
ed.attachment = ap;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ use iced::advanced::renderer;
|
||||||
use iced::advanced::widget;
|
use iced::advanced::widget;
|
||||||
use iced::advanced::{Layout, Shell, Widget};
|
use iced::advanced::{Layout, Shell, Widget};
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, column, container, mouse_area, row, scrollable, stack, text, text_input, Space,
|
button, checkbox, column, container, mouse_area, row, scrollable, stack, text, text_input,
|
||||||
|
Space,
|
||||||
};
|
};
|
||||||
use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector};
|
use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
@ -374,7 +375,7 @@ pub(super) fn mtext_editor_overlay<'a>(
|
||||||
iced::widget::Space::new().width(Fill).height(Fill),
|
iced::widget::Space::new().width(Fill).height(Fill),
|
||||||
t!("Text Editor"),
|
t!("Text Editor"),
|
||||||
content,
|
content,
|
||||||
Message::MTextCancel,
|
Message::MTextOk,
|
||||||
modal_offset,
|
modal_offset,
|
||||||
crate::ui::modal::ModalOptions::STANDARD,
|
crate::ui::modal::ModalOptions::STANDARD,
|
||||||
)
|
)
|
||||||
|
|
@ -450,6 +451,9 @@ fn mtext_editor_content<'a>(
|
||||||
.on_select(Message::MTextStyle)
|
.on_select(Message::MTextStyle)
|
||||||
.text_size(11)
|
.text_size(11)
|
||||||
.width(iced::Length::Fixed(96.0));
|
.width(iced::Length::Fixed(96.0));
|
||||||
|
let annotative = checkbox(ed.annotative)
|
||||||
|
.on_toggle(Message::MTextAnnotative)
|
||||||
|
.size(14);
|
||||||
let font_sel = if ed.font.trim().is_empty() {
|
let font_sel = if ed.font.trim().is_empty() {
|
||||||
"[Style default]".to_string()
|
"[Style default]".to_string()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -487,6 +491,9 @@ fn mtext_editor_content<'a>(
|
||||||
|
|
||||||
let row1 = row![
|
let row1 = row![
|
||||||
style_pl,
|
style_pl,
|
||||||
|
row![annotative, text(t!("Annotative")).size(11)]
|
||||||
|
.spacing(3)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
font_pl,
|
font_pl,
|
||||||
small_input("2.5", &ed.height, Message::MTextHeight, 64.0),
|
small_input("2.5", &ed.height, Message::MTextHeight, 64.0),
|
||||||
iced::widget::Space::new().width(6),
|
iced::widget::Space::new().width(6),
|
||||||
|
|
@ -535,6 +542,14 @@ fn mtext_editor_content<'a>(
|
||||||
.text_size(11)
|
.text_size(11)
|
||||||
.width(iced::Length::Fixed(112.0));
|
.width(iced::Length::Fixed(112.0));
|
||||||
let row2 = row![
|
let row2 = row![
|
||||||
|
button(lbl("↶"))
|
||||||
|
.on_press(Message::MTextUndo)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("↷"))
|
||||||
|
.on_press(Message::MTextRedo)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
lbl("O"),
|
lbl("O"),
|
||||||
small_input("0", &ed.oblique, Message::MTextOblique, 48.0),
|
small_input("0", &ed.oblique, Message::MTextOblique, 48.0),
|
||||||
lbl("W"),
|
lbl("W"),
|
||||||
|
|
@ -573,6 +588,144 @@ fn mtext_editor_content<'a>(
|
||||||
.on_press(Message::MTextLineSpacing(2.0))
|
.on_press(Message::MTextLineSpacing(2.0))
|
||||||
.padding(3)
|
.padding(3)
|
||||||
.style(btn_style),
|
.style(btn_style),
|
||||||
|
button(lbl("Stack"))
|
||||||
|
.on_press(Message::MTextStack)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("Clear"))
|
||||||
|
.on_press(Message::MTextClearFormatting)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("°"))
|
||||||
|
.on_press(Message::MTextInsert("°".to_string()))
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("±"))
|
||||||
|
.on_press(Message::MTextInsert("±".to_string()))
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("⌀"))
|
||||||
|
.on_press(Message::MTextInsert("⌀".to_string()))
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center)
|
||||||
|
.width(width);
|
||||||
|
|
||||||
|
let column_mode = match (ed.column_type, ed.column_auto_height) {
|
||||||
|
(1, _) => "Static",
|
||||||
|
(2, true) => "Dynamic auto",
|
||||||
|
(2, false) => "Dynamic manual",
|
||||||
|
_ => "No columns",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
let column_picker = iced::widget::pick_list(
|
||||||
|
Some(column_mode),
|
||||||
|
["No columns", "Static", "Dynamic auto", "Dynamic manual"]
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_string)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
|value| value.to_string(),
|
||||||
|
)
|
||||||
|
.on_select(Message::MTextColumnMode)
|
||||||
|
.text_size(11)
|
||||||
|
.width(iced::Length::Fixed(120.0));
|
||||||
|
let reverse = checkbox(ed.column_flow_reversed)
|
||||||
|
.on_toggle(Message::MTextColumnFlowReversed)
|
||||||
|
.size(14);
|
||||||
|
let row3 = row![
|
||||||
|
lbl("Columns"),
|
||||||
|
column_picker,
|
||||||
|
lbl("Count"),
|
||||||
|
small_input("2", &ed.column_count, Message::MTextColumnCount, 42.0),
|
||||||
|
lbl("Height"),
|
||||||
|
text_input("0", &ed.rect_height)
|
||||||
|
.on_input(Message::MTextColumnHeight)
|
||||||
|
.width(iced::Length::Fixed(58.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
lbl("Width"),
|
||||||
|
small_input("0", &ed.column_width, Message::MTextColumnWidth, 58.0),
|
||||||
|
lbl("Gutter"),
|
||||||
|
small_input("0", &ed.column_gutter, Message::MTextColumnGutter, 58.0),
|
||||||
|
reverse,
|
||||||
|
lbl("Reverse"),
|
||||||
|
iced::widget::Space::new().width(8),
|
||||||
|
lbl("First"),
|
||||||
|
text_input("0", &ed.paragraph_first_indent)
|
||||||
|
.on_input(|value| Message::MTextParagraphNumber(
|
||||||
|
super::super::mtext_editor::ParaNumber::FirstIndent,
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
.width(iced::Length::Fixed(48.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
lbl("Left"),
|
||||||
|
text_input("0", &ed.paragraph_left_indent)
|
||||||
|
.on_input(|value| Message::MTextParagraphNumber(
|
||||||
|
super::super::mtext_editor::ParaNumber::LeftIndent,
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
.width(iced::Length::Fixed(48.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
lbl("Right"),
|
||||||
|
text_input("0", &ed.paragraph_right_indent)
|
||||||
|
.on_input(|value| Message::MTextParagraphNumber(
|
||||||
|
super::super::mtext_editor::ParaNumber::RightIndent,
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
.width(iced::Length::Fixed(48.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
lbl("Before"),
|
||||||
|
text_input("0", &ed.paragraph_space_before)
|
||||||
|
.on_input(|value| Message::MTextParagraphNumber(
|
||||||
|
super::super::mtext_editor::ParaNumber::SpaceBefore,
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
.width(iced::Length::Fixed(48.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
lbl("After"),
|
||||||
|
text_input("0", &ed.paragraph_space_after)
|
||||||
|
.on_input(|value| Message::MTextParagraphNumber(
|
||||||
|
super::super::mtext_editor::ParaNumber::SpaceAfter,
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
.width(iced::Length::Fixed(48.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.align_y(iced::Alignment::Center)
|
||||||
|
.width(width);
|
||||||
|
let row4 = row![
|
||||||
|
lbl("Find"),
|
||||||
|
text_input("Find", &ed.find_text)
|
||||||
|
.on_input(Message::MTextFindText)
|
||||||
|
.width(iced::Length::Fixed(140.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
button(lbl("Next"))
|
||||||
|
.on_press(Message::MTextFindNext)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
lbl("Replace"),
|
||||||
|
text_input("Replace", &ed.replace_text)
|
||||||
|
.on_input(Message::MTextReplaceText)
|
||||||
|
.width(iced::Length::Fixed(140.0))
|
||||||
|
.padding(3)
|
||||||
|
.size(12),
|
||||||
|
button(lbl("Replace"))
|
||||||
|
.on_press(Message::MTextReplaceNext)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
|
button(lbl("Replace All"))
|
||||||
|
.on_press(Message::MTextReplaceAll)
|
||||||
|
.padding(3)
|
||||||
|
.style(btn_style),
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
|
|
@ -710,13 +863,13 @@ fn mtext_editor_content<'a>(
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Top action bar: Apply on the right, exactly like the style managers'
|
// ── Top action bar: Apply keeps editing; Close saves and exits. Escape
|
||||||
// toolbar strip. Closing (the modal ✕) without applying discards the
|
// remains the explicit discard path.
|
||||||
// buffer, so there is no separate Cancel.
|
|
||||||
let action_bar = container(
|
let action_bar = container(
|
||||||
row![
|
row![
|
||||||
iced::widget::Space::new().width(width),
|
iced::widget::Space::new().width(width),
|
||||||
crate::ui::style::style_manager::tb_button(t!("Apply"), Message::MTextApply, true),
|
crate::ui::style::style_manager::tb_button(t!("Apply"), Message::MTextApply, true),
|
||||||
|
crate::ui::style::style_manager::tb_button(t!("Close Text Editor"), Message::MTextOk, true),
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center),
|
.align_y(iced::Alignment::Center),
|
||||||
)
|
)
|
||||||
|
|
@ -730,7 +883,7 @@ fn mtext_editor_content<'a>(
|
||||||
.padding([5, 8]);
|
.padding([5, 8]);
|
||||||
|
|
||||||
container(
|
container(
|
||||||
column![action_bar, row1, row2, width_slider, body]
|
column![action_bar, row1, row2, row3, row4, width_slider, body]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
.width(width)
|
.width(width)
|
||||||
.height(height),
|
.height(height),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue