Просмотр исходного кода

app/text: bugfix incorrect text scaling calcs. we actually render text at pixel scale to achieve crisp glyphs before applying window_scale to avoid upsampling so we werent performing the correct scaling division before doing this. more info below.

Summary
Text was being scaled twice: parley bakes window_scale into the layout, and the renderer multiplies every vertex by the same factor again via SetScale — so text rendered ∝ scale² while all other UI rendered ∝ scale. The fix keeps parley's scale (glyphs still rasterize at physical resolution, so they stay crisp) and removes the scale exactly once when converting physical layout coordinates to the virtual units the rest of the UI uses:
- src/text/mod.rs — make_layout/make_layout2 now return a TextLayout wrapper that remembers the scale; height()/width() return virtual units; the line-wrap width is scaled up (this was also silently wrong before: virtual widths were applied to physical layouts, wrapping lines too early by ×scale).
- src/text/render.rs — glyph quads, underlines, and run background boxes are divided by the scale at mesh emission; the renderer's SetScale then applies it exactly once.
- Editors (text/editor/{android,parley}.rs) — height(), width(), cursor position, selection rects and drag-handle endpoints now return virtual units; touch hit-testing scales incoming positions up; new render_instrs(), selection_rects(), selection_endpoints() encapsulate the conversion.
- Consumers (ui/text.rs, ui/chatview/{mod,page}.rs, ui/edit/{mod,action}.rs) — field types updated; URL click-rects divided by scale; edit widget uses the new editor methods.
Sites that already passed scale 1.0 (emoji picker, token table) are unaffected since dividing by 1 is a no-op. On kilian (scale 0.625) text will now match the surrounding UI instead of rendering at 0.625× its size.
darkfi 1 день назад
Родитель
Сommit
18d2706ae8

+ 44 - 6
bin/app/src/text/editor/android.rs

@@ -21,7 +21,7 @@ use std::cmp::{max, min};
 use super::driver::ParleyDriverWrapper;
 use crate::{
     android::textinput::{AndroidTextInput, AndroidTextInputState},
-    gfx::Point,
+    gfx::{DebugTag, DrawInstruction, Point, Rectangle, Renderer},
     mesh::Color,
     prop::{PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyStr},
     text,
@@ -34,7 +34,7 @@ pub struct Editor {
     pub state: AndroidTextInputState,
     pub recvr: async_channel::Receiver<AndroidTextInputState>,
 
-    layout: parley::Layout<Color>,
+    layout: text::TextLayout,
     width: Option<f32>,
 
     text: PropertyStr,
@@ -126,7 +126,43 @@ impl Editor {
         &self.layout
     }
 
+    /// Render the editor text. Meshes are emitted in virtual units.
+    pub fn render_instrs(&self, renderer: &Renderer, tag: DebugTag) -> Vec<DrawInstruction> {
+        text::render_layout(&self.layout, renderer, tag)
+    }
+
+    /// Selection highlight rectangles in virtual units.
+    pub fn selection_rects(&self) -> Vec<Rectangle> {
+        let mut rects = vec![];
+        let sel = self.selection(1);
+        if sel.is_collapsed() {
+            return rects
+        }
+
+        let scale = self.window_scale.get();
+        sel.geometry_with(&self.layout, |rect: parley::BoundingBox, _| {
+            rects.push(Rectangle::from(rect) / scale);
+        });
+        rects
+    }
+
+    /// Selection anchor and focus endpoint positions in virtual units.
+    pub fn selection_endpoints(&self) -> Option<(Point, Point)> {
+        let sel = self.selection(1);
+        if sel.is_collapsed() {
+            return None
+        }
+
+        let scale = self.window_scale.get();
+        let first = Rectangle::from(sel.anchor().geometry(&self.layout, 0.)).pos() / scale;
+        let last = Rectangle::from(sel.focus().geometry(&self.layout, 0.)).pos() / scale;
+        Some((first, last))
+    }
+
     pub fn move_to_pos(&mut self, pos: Point) {
+        // The layout coordinates are physical so scale the virtual
+        // position up before hit testing.
+        let pos = pos * self.window_scale.get();
         let cursor = parley::Cursor::from_point(&self.layout, pos.x, pos.y);
         let cursor_idx = cursor.index();
         t!("  move_to_pos: {cursor_idx}");
@@ -138,6 +174,7 @@ impl Editor {
     }
 
     pub fn select_word_at_point(&mut self, pos: Point) {
+        let pos = pos * self.window_scale.get();
         let select = parley::Selection::word_from_point(&self.layout, pos.x, pos.y);
         assert!(!select.is_collapsed());
         let select = select.text_range();
@@ -146,6 +183,7 @@ impl Editor {
 
     pub fn get_cursor_pos(&self) -> Point {
         let lineheight = self.lineheight.get();
+        let scale = self.window_scale.get();
         let cursor_idx = self.state.select.0;
 
         let cursor = if cursor_idx >= self.state.text.len() {
@@ -157,8 +195,8 @@ impl Editor {
         } else {
             parley::Cursor::from_byte_index(&self.layout, cursor_idx, parley::Affinity::Downstream)
         };
-        let cursor_rect = cursor.geometry(&self.layout, lineheight);
-        Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32)
+        let cursor_rect = cursor.geometry(&self.layout, lineheight * scale);
+        Point::new(cursor_rect.x0 as f32 / scale, cursor_rect.y0 as f32 / scale)
     }
 
     pub fn insert(&mut self, txt: &str, atom: &mut PropertyAtomicGuard) {
@@ -180,10 +218,10 @@ impl Editor {
         self.width = Some(w);
     }
     pub fn width(&self) -> f32 {
-        self.layout().full_width()
+        self.layout().full_width() / self.window_scale.get()
     }
     pub fn height(&self) -> f32 {
-        self.layout().height()
+        self.layout().height() / self.window_scale.get()
     }
 
     pub fn selected_text(&self) -> Option<String> {

+ 42 - 5
bin/app/src/text/editor/parley.rs

@@ -17,7 +17,7 @@
  */
 
 use crate::{
-    gfx::Point,
+    gfx::{DebugTag, DrawInstruction, Point, Rectangle, Renderer},
     mesh::Color,
     prop::{PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyStr},
     text::{self, FONT_STACK},
@@ -96,6 +96,41 @@ impl Editor {
         self.editor.try_layout().unwrap()
     }
 
+    /// Render the editor text. Meshes are emitted in virtual units.
+    pub fn render_instrs(&self, renderer: &Renderer, tag: DebugTag) -> Vec<DrawInstruction> {
+        text::render_raw_layout(self.layout(), self.window_scale.get(), renderer, tag)
+    }
+
+    /// Selection highlight rectangles in virtual units.
+    pub fn selection_rects(&self) -> Vec<Rectangle> {
+        let mut rects = vec![];
+        let sel = self.selection(1);
+        if sel.is_collapsed() {
+            return rects
+        }
+
+        let scale = self.window_scale.get();
+        let layout = self.layout();
+        sel.geometry_with(layout, |rect: parley::BoundingBox, _| {
+            rects.push(Rectangle::from(rect) / scale);
+        });
+        rects
+    }
+
+    /// Selection anchor and focus endpoint positions in virtual units.
+    pub fn selection_endpoints(&self) -> Option<(Point, Point)> {
+        let sel = self.selection(1);
+        if sel.is_collapsed() {
+            return None
+        }
+
+        let scale = self.window_scale.get();
+        let layout = self.layout();
+        let first = Rectangle::from(sel.anchor().geometry(layout, 0.)).pos() / scale;
+        let last = Rectangle::from(sel.focus().geometry(layout, 0.)).pos() / scale;
+        Some((first, last))
+    }
+
     pub fn move_to_pos(&mut self, _: Point) {
         unimplemented!()
     }
@@ -104,8 +139,9 @@ impl Editor {
     }
 
     pub fn get_cursor_pos(&self) -> Point {
+        let scale = self.window_scale.get();
         let cursor_rect = self.editor.cursor_geometry(0.).unwrap();
-        let cursor_pos = Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32);
+        let cursor_pos = Point::new(cursor_rect.x0 as f32 / scale, cursor_rect.y0 as f32 / scale);
         cursor_pos
     }
 
@@ -119,13 +155,14 @@ impl Editor {
     }
 
     pub fn set_width(&mut self, w: f32) {
-        self.editor.set_width(Some(w));
+        // The editor layout is physical so scale the virtual width up.
+        self.editor.set_width(Some(w * self.window_scale.get()));
     }
     pub fn width(&self) -> f32 {
-        self.layout().full_width()
+        self.layout().full_width() / self.window_scale.get()
     }
     pub fn height(&self) -> f32 {
-        self.layout().height()
+        self.layout().height() / self.window_scale.get()
     }
 
     pub fn selected_text(&self) -> Option<String> {

+ 61 - 4
bin/app/src/text/mod.rs

@@ -29,6 +29,8 @@ pub mod atlas;
 mod editor;
 pub use editor::Editor;
 mod render;
+#[cfg(not(target_os = "android"))]
+pub use render::render_raw_layout;
 pub use render::{render_backgrounds, render_layout, render_layout_with_opts, DebugRenderOptions};
 
 pub static GLOBAL_FONT_CTX: LazyLock<parley::FontContext> = LazyLock::new(|| {
@@ -56,6 +58,59 @@ const FONT_STACK: &[parley::FontFamilyName<'_>] = &[
     parley::FontFamilyName::named("Noto Color Emoji"),
 ];
 
+/// A parley layout paired with the window scale it was built with.
+///
+/// Parley bakes the builder scale into every coordinate (font size,
+/// advances, glyph positions), so the underlying layout is in physical
+/// pixels. The renderer applies the window scale again via `SetScale`,
+/// so geometry consumed in virtual units must be divided by the scale
+/// exactly once. The accessors below do that division. Rendering code
+/// in `render.rs` divides when emitting meshes. Glyph rasterization
+/// still happens at physical resolution so text stays crisp.
+#[derive(Clone)]
+pub struct TextLayout {
+    layout: parley::Layout<Color>,
+    /// Scale parley baked into `layout`. Divide physical coords by this
+    /// to get virtual units.
+    scale: f32,
+}
+
+impl std::ops::Deref for TextLayout {
+    type Target = parley::Layout<Color>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.layout
+    }
+}
+
+impl std::ops::DerefMut for TextLayout {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.layout
+    }
+}
+
+impl Default for TextLayout {
+    fn default() -> Self {
+        Self { layout: parley::Layout::default(), scale: 1. }
+    }
+}
+
+impl TextLayout {
+    pub fn scale(&self) -> f32 {
+        self.scale
+    }
+
+    /// Height in virtual units
+    pub fn height(&self) -> f32 {
+        self.layout.height() / self.scale
+    }
+
+    /// Width in virtual units
+    pub fn width(&self) -> f32 {
+        self.layout.width() / self.scale
+    }
+}
+
 pub fn make_layout(
     text: &str,
     text_color: Color,
@@ -64,7 +119,7 @@ pub fn make_layout(
     window_scale: f32,
     width: Option<f32>,
     underlines: &[Range<usize>],
-) -> parley::Layout<Color> {
+) -> TextLayout {
     make_layout2(
         text,
         text_color,
@@ -90,7 +145,7 @@ pub fn make_layout2(
     foreground_colors: &[(Range<usize>, Color)],
     text_align: &str,
     overflow_wrap: &str,
-) -> parley::Layout<Color> {
+) -> TextLayout {
     THREAD_LAYOUT_CTX.with(|layout_ctx| {
         let text_align = match text_align {
             "start" => parley::Alignment::Start,
@@ -127,8 +182,10 @@ pub fn make_layout2(
         }
 
         let mut layout: parley::Layout<Color> = builder.build(text);
-        layout.break_all_lines(width);
+        // The wrap width is given in virtual units while the layout
+        // coordinates are physical, so scale it up before breaking.
+        layout.break_all_lines(width.map(|w| w * window_scale));
         layout.align(text_align, parley::AlignmentOptions::default());
-        layout
+        TextLayout { layout, scale: window_scale }
     })
 }

+ 52 - 18
bin/app/src/text/render.rs

@@ -21,7 +21,10 @@ use crate::{
     mesh::{Color, MeshBuilder, COLOR_WHITE},
 };
 
-use super::atlas::{Atlas, RenderedAtlas, RunIdx};
+use super::{
+    atlas::{Atlas, RenderedAtlas, RunIdx},
+    TextLayout,
+};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct DebugRenderOptions(u32);
@@ -50,13 +53,25 @@ impl std::ops::BitOrAssign for DebugRenderOptions {
 }
 
 pub fn render_layout(
-    layout: &parley::Layout<Color>,
+    layout: &TextLayout,
     renderer: &Renderer,
     tag: DebugTag,
 ) -> Vec<DrawInstruction> {
     render_layout_with_opts(layout, DebugRenderOptions::OFF, renderer, tag)
 }
 
+/// Render a raw parley layout that was built with the given scale. Used
+/// by editors that own their layout internally (e.g. `PlainEditor`).
+#[cfg(not(target_os = "android"))]
+pub fn render_raw_layout(
+    layout: &parley::Layout<Color>,
+    scale: f32,
+    renderer: &Renderer,
+    tag: DebugTag,
+) -> Vec<DrawInstruction> {
+    render_raw_layout_with_opts(layout, scale, DebugRenderOptions::OFF, renderer, tag)
+}
+
 /// Draw a filled (and optionally outlined) background box behind every glyph run
 /// whose style brush equals `match_brush`. The box tracks the run's horizontal
 /// advance and the font-metric ascent/descent vertically, so a run that wraps
@@ -70,7 +85,7 @@ pub fn render_layout(
 /// highlighted. The fill is skipped when `bg_color` alpha is ~0; the outline is
 /// skipped when `border_size` is ~0 or `border_color` alpha is ~0.
 pub fn render_backgrounds(
-    layout: &parley::Layout<Color>,
+    layout: &TextLayout,
     match_brush: Color,
     bg_color: Color,
     border_color: Color,
@@ -85,6 +100,7 @@ pub fn render_backgrounds(
         return instrs
     }
 
+    let scale = layout.scale();
     for line in layout.lines() {
         for item in line.items() {
             let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue };
@@ -97,7 +113,7 @@ pub fn render_backgrounds(
             let y = glyph_run.baseline() - metrics.ascent;
             let w = glyph_run.advance();
             let h = metrics.ascent + metrics.descent;
-            let rect = Rectangle::new(x, y, w, h);
+            let rect = Rectangle::new(x, y, w, h) / scale;
 
             let mut mesh = MeshBuilder::new(tag);
             if has_fill {
@@ -114,7 +130,22 @@ pub fn render_backgrounds(
 }
 
 pub fn render_layout_with_opts(
+    layout: &TextLayout,
+    opts: DebugRenderOptions,
+    renderer: &Renderer,
+    tag: DebugTag,
+) -> Vec<DrawInstruction> {
+    render_raw_layout_with_opts(layout, layout.scale(), opts, renderer, tag)
+}
+
+/// Layout coordinates are physical (scale is baked in by parley) while
+/// meshes are consumed in virtual units and scaled up again by the
+/// renderer's `SetScale`. So every emitted coordinate is divided by
+/// `scale` here. Glyphs are still rasterized at physical resolution so
+/// the final on-screen texel mapping stays crisp.
+fn render_raw_layout_with_opts(
     layout: &parley::Layout<Color>,
+    scale: f32,
     opts: DebugRenderOptions,
     renderer: &Renderer,
     tag: DebugTag,
@@ -145,7 +176,8 @@ pub fn render_layout_with_opts(
         for item in line.items() {
             match item {
                 parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
-                    let mesh = render_glyph_run(&glyph_run, run_idx, opts, &atlas, renderer, tag);
+                    let mesh =
+                        render_glyph_run(&glyph_run, run_idx, opts, &atlas, scale, renderer, tag);
                     instrs.push(DrawInstruction::Draw(mesh));
                     run_idx += 1;
                 }
@@ -185,6 +217,7 @@ fn render_glyph_run(
     run_idx: usize,
     opts: DebugRenderOptions,
     atlas: &RenderedAtlas,
+    scale: f32,
     renderer: &Renderer,
     tag: DebugTag,
 ) -> DrawMesh {
@@ -197,7 +230,7 @@ fn render_glyph_run(
     let mut mesh = MeshBuilder::new(tag);
 
     if let Some(underline) = &style.underline {
-        render_underline(underline, glyph_run, &mut mesh);
+        render_underline(underline, glyph_run, scale, &mut mesh);
     }
 
     for glyph in glyph_run.glyphs() {
@@ -208,10 +241,10 @@ fn render_glyph_run(
         run_x += glyph.advance;
 
         let glyph_rect = Rectangle::new(
-            glyph_x + glyph_inf.place.left as f32,
-            glyph_y - glyph_inf.place.top as f32,
-            glyph_inf.place.width as f32,
-            glyph_inf.place.height as f32,
+            (glyph_x + glyph_inf.place.left as f32) / scale,
+            (glyph_y - glyph_inf.place.top as f32) / scale,
+            glyph_inf.place.width as f32 / scale,
+            glyph_inf.place.height as f32 / scale,
         );
 
         if opts.has(DebugRenderOptions::GLYPH) {
@@ -223,10 +256,10 @@ fn render_glyph_run(
     }
 
     if opts.has(DebugRenderOptions::BASELINE) {
-        mesh.draw_filled_box(
-            &Rectangle::new(glyph_run.offset(), glyph_run.baseline(), glyph_run.advance(), 1.),
-            [0., 0., 1., 0.7],
-        );
+        let rect =
+            Rectangle::new(glyph_run.offset(), glyph_run.baseline(), glyph_run.advance(), 1.) /
+                scale;
+        mesh.draw_filled_box(&rect, [0., 0., 1., 0.7]);
     }
 
     mesh.alloc(renderer).draw_with_textures(vec![atlas.texture.clone()])
@@ -235,6 +268,7 @@ fn render_glyph_run(
 fn render_underline(
     underline: &parley::layout::Decoration<Color>,
     glyph_run: &parley::GlyphRun<'_, Color>,
+    scale: f32,
     mesh: &mut MeshBuilder,
 ) {
     let color = underline.brush;
@@ -252,13 +286,13 @@ fn render_underline(
     // Remember that we are using a y-down coordinate system
     // If there's a custom width, because this is an underline, we want the custom
     // width to go down from the default expectation
-    let y = glyph_run.baseline() - offset + width / 2.;
+    let y = (glyph_run.baseline() - offset + width / 2.) / scale;
 
-    let start_x = glyph_run.offset();
-    let end_x = start_x + glyph_run.advance();
+    let start_x = glyph_run.offset() / scale;
+    let end_x = start_x + glyph_run.advance() / scale;
 
     let start = Point::new(start_x, y);
     let end = Point::new(end_x, y);
 
-    mesh.draw_line(start, end, color, width);
+    mesh.draw_line(start, end, color, width / scale);
 }

+ 1 - 1
bin/app/src/ui/chatview/mod.rs

@@ -172,7 +172,7 @@ pub type ChatViewPtr = Arc<ChatView>;
 /// Transient "Copied link" overlay state. Rendered as a `DrawInstruction::Overlay`
 /// in its own draw call so it floats above the link and escapes the message clip rect.
 struct LinkToast {
-    text_layout: parley::Layout<Color>,
+    text_layout: text::TextLayout,
     anchor: Point,
     offset: f32,
     fg_color: Color,

+ 15 - 11
bin/app/src/ui/chatview/page.rs

@@ -94,7 +94,7 @@ pub struct PrivMessage {
     is_selected: bool,
 
     mesh_cache: Option<Vec<DrawInstruction>>,
-    txt_layout: Option<parley::Layout<Color>>,
+    txt_layout: Option<text::TextLayout>,
 
     /// Bounding rects of this message's URL runs in message-local coordinates,
     /// each tagged with its URL string. Populated in `gen_mesh`, used by
@@ -358,12 +358,13 @@ impl PrivMessage {
         self.is_selected
     }
 
-    /// Build the URL hit-rectangles for this message, in message-local coordinates.
-    /// Each URL-colored glyph run (`style().brush == url_text_color`) becomes a rect
-    /// `(timestamp_width + run.offset, run.baseline - ascent, run.advance,
-    /// ascent + descent)`. The run is tagged with its URL string by intersecting its
-    /// (coarse) font-run `text_range()` with the message's URL byte ranges in
-    /// `linetext`, so wrapped URLs and multiple URLs are handled correctly.
+    /// Build the URL hit-rectangles for this message, in message-local
+    /// virtual coordinates. Each URL-colored glyph run (`style().brush ==
+    /// url_text_color`) becomes a rect `(timestamp_width + run.offset,
+    /// run.baseline - ascent, run.advance, ascent + descent)`. The run is
+    /// tagged with its URL string by intersecting its (coarse) font-run
+    /// `text_range()` with the message's URL byte ranges in `linetext`,
+    /// so wrapped URLs and multiple URLs are handled correctly.
     fn compute_url_click_rects(
         &self,
         timestamp_width: f32,
@@ -385,6 +386,9 @@ impl PrivMessage {
             return rects
         }
 
+        // Layout coordinates are physical so divide by the scale to get
+        // the virtual units the hit test positions use.
+        let scale = layout.scale();
         for line in layout.lines() {
             for item in line.items() {
                 let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue };
@@ -404,10 +408,10 @@ impl PrivMessage {
                 let url_str = linetext[url_range.clone()].to_string();
 
                 let metrics = glyph_run.run().metrics();
-                let x = timestamp_width + glyph_run.offset();
-                let y = glyph_run.baseline() - metrics.ascent;
-                let w = glyph_run.advance();
-                let h = metrics.ascent + metrics.descent;
+                let x = timestamp_width + glyph_run.offset() / scale;
+                let y = (glyph_run.baseline() - metrics.ascent) / scale;
+                let w = glyph_run.advance() / scale;
+                let h = (metrics.ascent + metrics.descent) / scale;
                 rects.push((url_str, Rectangle::new(x, y, w, h)));
             }
         }

+ 1 - 1
bin/app/src/ui/edit/action.rs

@@ -28,7 +28,7 @@ use crate::{
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::edit::action", $($arg)*); } }
 
 struct MenuItem {
-    layout: parley::Layout<Color>,
+    layout: text::TextLayout,
     action_id: u32,
     rect: Rectangle,
 }

+ 9 - 18
bin/app/src/ui/edit/mod.rs

@@ -798,17 +798,11 @@ impl BaseEdit {
     }
 
     fn get_select_handles(&self, editor: &Editor) -> Option<(Point, Point)> {
-        let layout = editor.layout();
-
-        let sel = editor.selection(1);
-        if sel.is_collapsed() {
+        let endpoints = editor.selection_endpoints();
+        if endpoints.is_none() {
             assert!(!self.is_phone_select.load(Ordering::Relaxed));
-            return None
         }
-
-        let first = Rectangle::from(sel.anchor().geometry(layout, 0.)).pos();
-        let last = Rectangle::from(sel.focus().geometry(layout, 0.)).pos();
-        Some((first, last))
+        endpoints
     }
 
     fn try_handle_drag(&self, mut touch_pos: Point) -> bool {
@@ -1215,10 +1209,8 @@ impl BaseEdit {
 
         // Render text
         let editor = self.editor.lock();
-        let layout = editor.layout();
 
-        let mut render_instrs =
-            text::render_layout(layout, &self.renderer, gfxtag!("chatedit_txt_mesh"));
+        let mut render_instrs = editor.render_instrs(&self.renderer, gfxtag!("chatedit_txt_mesh"));
         instrs.append(&mut render_instrs);
 
         instrs
@@ -1228,15 +1220,14 @@ impl BaseEdit {
         let mut instrs = vec![DrawInstruction::Move(self.behave.inner_pos())];
 
         let editor = self.editor.lock();
-        let layout = editor.layout();
 
-        let sel = editor.selection(1);
         let sel_color = self.hi_bg_color.get();
-        if !sel.is_collapsed() {
+        let sel_rects = editor.selection_rects();
+        if !sel_rects.is_empty() {
             let mut mesh = MeshBuilder::new(gfxtag!("chatedit_select_mesh"));
-            sel.geometry_with(layout, |rect: parley::BoundingBox, _| {
-                mesh.draw_filled_box(&rect.into(), sel_color);
-            });
+            for rect in sel_rects {
+                mesh.draw_filled_box(&rect, sel_color);
+            }
 
             instrs.push(DrawInstruction::Draw(mesh.alloc(renderer).draw_untextured()));
         }

+ 4 - 4
bin/app/src/ui/text.rs

@@ -24,7 +24,7 @@ use tracing::instrument;
 
 use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer},
-    mesh::{Color, MeshBuilder},
+    mesh::MeshBuilder,
     prop::{
         PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyEnum, PropertyFloat32,
         PropertyRect, PropertyStr, PropertyUint32, Role,
@@ -64,7 +64,7 @@ pub struct Text {
     window_scale: PropertyFloat32,
     /// Cached layout + rendered instrs. `None` means stale: recompute in
     /// the draw pass. Layout is the expensive part (shaping, line breaks).
-    draw_cache: SyncMutex<Option<(parley::Layout<Color>, Vec<DrawInstruction>)>>,
+    draw_cache: SyncMutex<Option<(text::TextLayout, Vec<DrawInstruction>)>>,
 }
 
 impl Text {
@@ -118,7 +118,7 @@ impl Text {
         Pimpl::Text(self_)
     }
 
-    fn make_layout(&self) -> parley::Layout<Color> {
+    fn make_layout(&self) -> text::TextLayout {
         let text = self.text.get();
         let font_size = self.font_size.get();
         let lineheight = self.lineheight.get();
@@ -153,7 +153,7 @@ impl Text {
         )
     }
 
-    fn regen_mesh(&self, layout: &parley::Layout<Color>) -> Vec<DrawInstruction> {
+    fn regen_mesh(&self, layout: &text::TextLayout) -> Vec<DrawInstruction> {
         let mut debug_opts = text::DebugRenderOptions::OFF;
         if self.debug.get() {
             debug_opts |= text::DebugRenderOptions::BASELINE;