diff --git a/Cargo.lock b/Cargo.lock index c930b6952..dd753cfaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,7 +73,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#9d2e67fb7bfc054281641228e3543e8ac26d7f08" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#389b31ac5865e72f403d16d81eec958228065af0" dependencies = [ "ahash 0.8.12", "anyhow", @@ -3179,7 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 1.0.2", + "memchr 2.8.3", "nom 8.0.0", ] diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index d5f86cc3f..c0fcfadb4 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -261,11 +261,13 @@ impl iced::widget::canvas::Program for MTextPreview { let collapsed = self.caret_on && self.sel.map(|(a, b)| a == b).unwrap_or(true); if collapsed && self.boxes.is_empty() { // Empty text: show a caret at the top-left so the user can type. + // Its height matches the preview's constant em (see EM_PX) so the + // empty-editor caret isn't tiny. let path = Path::new(|p| { p.move_to(iced::Point::new(MTEXT_PREVIEW_PAD, MTEXT_PREVIEW_PAD)); p.line_to(iced::Point::new( MTEXT_PREVIEW_PAD, - (MTEXT_PREVIEW_PAD + 22.0).min(self.content_h), + (MTEXT_PREVIEW_PAD + 40.0).min(self.content_h), )); }); frame.stroke( @@ -570,15 +572,58 @@ pub(super) fn mtext_editor_overlay<'a>( maxx = maxx.max(b.xmax); maxy = maxy.max(b.ymax); } - let h_unit = ed.height_value() as f32; - // Real text size: fixed pixels per em so more/taller text grows the - // canvas (and scrolls) instead of shrinking to fit. - let scale = (22.0 / h_unit.max(1e-3)).clamp(2.0, 600.0); + // Normalize the display by the DOMINANT rendered text height — the run + // height the most glyphs share (the body) — NOT the entity's base height + // field. That field can be many times the visible text: `\H…x;` runs + // compound, so an entity of height 1 whose body is `\H0.2x` renders at + // 0.2, and normalizing by 1 would shrink everything 5×. The mode lands + // the body at EM_PX while headings / fine print keep their proportions + // (one uniform factor, so no height is distorted). Zero-width boxes — + // the empty-line caret slots, which carry the raw entity height — are + // skipped so they can't skew it. + let h_unit = { + let mut heights: Vec = ed + .glyph_boxes + .iter() + .filter(|b| b.xmax - b.xmin > 1e-6) + .map(|b| b.ymax - b.ymin) + .filter(|h| h.is_finite() && *h > 1e-6) + .collect(); + if heights.is_empty() { + ed.height_value() as f32 + } else { + heights.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + // Largest cluster of near-equal heights (within 5%) = the body. + let (mut best_h, mut best_n, mut i) = (heights[0], 0usize, 0usize); + while i < heights.len() { + let mut j = i; + while j < heights.len() && heights[j] <= heights[i] * 1.05 { + j += 1; + } + if j - i > best_n { + best_n = j - i; + best_h = heights[(i + j - 1) / 2]; + } + i = j; + } + best_h + } + }; + const EM_PX: f32 = 15.0; + let scale = (EM_PX / h_unit.max(1e-6)).clamp(1e-4, 1e6); let content_h = if maxx >= minx { ((maxy - miny) * scale + 2.0 * MTEXT_PREVIEW_PAD).max(40.0) } else { 40.0 }; + // Multi-column MTEXT lays its columns out side by side, so the content + // can be wider than the editor; size the canvas to the real content + // width and let the scroll area pan horizontally to reach later columns. + let content_w = if maxx >= minx { + (maxx - minx) * scale + 2.0 * MTEXT_PREVIEW_PAD + } else { + 40.0 + }; let prog = MTextPreview { segments, boxes: ed.glyph_boxes.clone(), @@ -591,11 +636,21 @@ pub(super) fn mtext_editor_overlay<'a>( content_h, }; let cv = canvas(prog) - .width(Fill) + .width(iced::Length::Fixed(content_w)) .height(iced::Length::Fixed(content_h)); - // The preview fills the space the toolbars leave, so the resizable - // modal's extra height goes to the text area (it scrolls if taller). - container(iced::widget::scrollable(cv).height(Fill)) + // The preview fills the space the toolbars leave; it scrolls in BOTH + // directions so a taller-than-view text scrolls vertically and a + // wider-than-view multi-column text scrolls horizontally. + use iced::widget::scrollable::{Direction, Scrollbar}; + container( + iced::widget::scrollable(cv) + .direction(Direction::Both { + vertical: Scrollbar::default(), + horizontal: Scrollbar::default(), + }) + .width(Fill) + .height(Fill), + ) .style(move |_: &Theme| container::Style { background: Some(Background::Color(FIELD_BG)), border: Border {