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

app: composing underline for android

jkds 1 год назад
Родитель
Сommit
00b8fa1ca4

+ 28 - 1
bin/app/src/gfx/linalg.rs

@@ -85,9 +85,28 @@ impl Point {
         Rectangle { x: self.x, y: self.y, w, h }
     }
 
-    pub fn dist_sq(&self, other: &Point) -> f32 {
+    pub fn dist_sq(&self, other: Point) -> f32 {
         (self.x - other.x).powi(2) + (self.y - other.y).powi(2)
     }
+    pub fn dist(&self, other: Point) -> f32 {
+        self.dist_sq(other).sqrt()
+    }
+
+    pub fn normalize(&mut self) {
+        let scale = self.dist(Point::zero());
+        self.x /= scale;
+        self.y /= scale;
+        assert!((self.dist(Point::zero()) - 1.) < f32::EPSILON);
+    }
+
+    /// Counterclockwise perp vector (with -y up)
+    pub fn perp_left(&self) -> Point {
+        Point::new(self.y, -self.x)
+    }
+    /// Clockwise perp vector (with +y down)
+    pub fn perp_right(&self) -> Point {
+        Point::new(-self.y, self.x)
+    }
 }
 
 impl From<[f32; 2]> for Point {
@@ -130,6 +149,14 @@ impl SubAssign for Point {
     }
 }
 
+impl Mul<f32> for Point {
+    type Output = Self;
+
+    fn mul(self, scale: f32) -> Self {
+        Point::new(scale * self.x, scale * self.y)
+    }
+}
+
 #[derive(Clone, Copy, SerialEncodable, SerialDecodable)]
 pub struct Rectangle {
     pub x: f32,

+ 34 - 2
bin/app/src/mesh.rs

@@ -18,7 +18,7 @@
 
 use crate::{
     error::Result,
-    gfx::{GfxDrawMesh, ManagedBufferPtr, ManagedTexturePtr, Rectangle, RenderApi, Vertex},
+    gfx::{GfxDrawMesh, ManagedBufferPtr, ManagedTexturePtr, Point, Rectangle, RenderApi, Vertex},
 };
 
 pub type Color = [f32; 4];
@@ -134,7 +134,7 @@ impl MeshBuilder {
     }
 
     pub fn draw_filled_box(&mut self, obj: &Rectangle, color: Color) {
-        let uv = Rectangle { x: 0., y: 0., w: 0., h: 0. };
+        let uv = Rectangle::zero();
         self.draw_box(obj, color, &uv);
     }
 
@@ -153,6 +153,38 @@ impl MeshBuilder {
         self.draw_filled_box(&Rectangle::new(x1, y2 - thickness, dist_x, thickness), color);
     }
 
+    pub fn draw_line(&mut self, start: Point, end: Point, color: Color, thickness: f32) {
+        trace!(target: "mesh", "draw_line({start:?}, {end:?}, {color:?}, {thickness})");
+        let mut dir = end - start;
+        dir.normalize();
+        let left = dir.perp_left() * (thickness / 2.);
+        let right = dir.perp_right() * (thickness / 2.);
+        trace!(target: "mesh", " -> dir={dir:?} left={left:?} right={right:?}");
+
+        let p1 = start + left;
+        let p2 = end + left;
+        let p3 = start + right;
+        let p4 = end + right;
+
+        let uv = [0., 0.];
+
+        let verts = vec![
+            // top left
+            Vertex { pos: [p1.x, p1.y], color, uv },
+            // top right
+            Vertex { pos: [p2.x, p2.y], color, uv },
+            // bottom left
+            Vertex { pos: [p3.x, p3.y], color, uv },
+            // bottom right
+            Vertex { pos: [p4.x, p4.y], color, uv },
+        ];
+        let indices = vec![0, 2, 1, 1, 2, 3];
+        trace!(target: "mesh", " -> {p1:?}, {p2:?}, {p3:?}, {p4:?}");
+        trace!(target: "mesh", " -> verts={verts:?} indices={indices:?}");
+
+        self.append(verts, indices);
+    }
+
     pub fn alloc(self, render_api: &RenderApi) -> MeshInfo {
         //debug!(target: "mesh", "allocating {} verts:", self.verts.len());
         //for vert in &self.verts {

+ 4 - 4
bin/app/src/text2/atlas.rs

@@ -80,12 +80,12 @@ impl<'a> Atlas<'a> {
         }
     }
 
-    pub fn push_glyph(&mut self, glyph: parley::Glyph) {
-        if self.glyph_ids.contains(&glyph.id) {
+    pub fn push_glyph(&mut self, glyph_id: swash::GlyphId) {
+        if self.glyph_ids.contains(&glyph_id) {
             return
         }
 
-        self.glyph_ids.push(glyph.id);
+        self.glyph_ids.push(glyph_id);
 
         let rendered_glyph = swash::scale::Render::new(
             // Select our source order
@@ -97,7 +97,7 @@ impl<'a> Atlas<'a> {
         )
         // Select the simple alpha (non-subpixel) format
         .format(zeno::Format::Alpha)
-        .render(&mut self.scaler, glyph.id)
+        .render(&mut self.scaler, glyph_id)
         .unwrap();
 
         let glyph_width = rendered_glyph.placement.width as usize;

+ 15 - 1
bin/app/src/text2/editor/android.rs

@@ -25,6 +25,7 @@ use crate::{
 };
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor::android", $($arg)*); } }
+macro_rules! w { ($($arg:tt)*) => { warn!(target: "text::editor::android", $($arg)*) } }
 
 // You must be careful working with string indexes in Java. They are UTF16 string indexs, not UTF8
 fn char16_to_byte_index(s: &str, char_idx: usize) -> Option<usize> {
@@ -90,9 +91,21 @@ impl Editor {
         let window_scale = self.window_scale.get();
         let lineheight = self.lineheight.get();
 
-        let edit = android::get_editable(self.composer_id).unwrap();
+        let Some(edit) = android::get_editable(self.composer_id) else {
+            w!("refresh(): editable composer_id={} not initialized yet", self.composer_id);
+            return
+        };
         t!("refesh buffer = {}", edit.buffer);
 
+        let mut underlines = vec![];
+        if let Some(compose_start) = edit.compose_start {
+            let compose_end = edit.compose_end.unwrap();
+
+            let compose_start = char16_to_byte_index(&edit.buffer, compose_start).unwrap();
+            let compose_end = char16_to_byte_index(&edit.buffer, compose_end).unwrap();
+            underlines.push((compose_start..compose_end));
+        }
+
         let mut txt_ctx = TEXT_CTX.get().await;
         self.layout = txt_ctx.make_layout(
             &edit.buffer,
@@ -101,6 +114,7 @@ impl Editor {
             lineheight,
             window_scale,
             self.width,
+            &underlines,
         );
     }
 

+ 1 - 1
bin/app/src/text2/editor/parley.rs

@@ -96,6 +96,6 @@ impl Editor {
     }
 
     pub fn selected_text(&self) -> Option<String> {
-        self.editor.selected_text().to_owned()
+        self.editor.selected_text().map(str::to_string)
     }
 }

+ 6 - 0
bin/app/src/text2/mod.rs

@@ -21,6 +21,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
 use std::{
     cell::RefCell,
     fmt::Debug,
+    ops::Range,
     sync::{atomic::AtomicBool, Arc, OnceLock},
 };
 
@@ -105,6 +106,7 @@ impl TextContext {
         lineheight: f32,
         window_scale: f32,
         width: Option<f32>,
+        underlines: &[Range<usize>],
     ) -> parley::Layout<Color> {
         let mut builder = self.layout_ctx.ranged_builder(&mut self.font_ctx, &text, window_scale);
         builder.push_default(parley::StyleProperty::LineHeight(lineheight));
@@ -114,6 +116,10 @@ impl TextContext {
         )));
         builder.push_default(parley::StyleProperty::Brush(text_color));
 
+        for underline in underlines {
+            builder.push(parley::StyleProperty::Underline(true), underline.clone());
+        }
+
         let mut layout: parley::Layout<Color> = builder.build(&text);
         layout.break_all_lines(width);
         layout.align(width, parley::Alignment::Start, parley::AlignmentOptions::default());

+ 63 - 20
bin/app/src/text2/render.rs

@@ -17,11 +17,11 @@
  */
 
 use crate::{
-    gfx::{GfxDrawInstruction, GfxDrawMesh, Rectangle, RenderApi},
+    gfx::{GfxDrawInstruction, GfxDrawMesh, Point, Rectangle, RenderApi},
     mesh::{Color, MeshBuilder, COLOR_WHITE},
 };
 
-use super::atlas::Atlas;
+use super::atlas::{Atlas, RenderedAtlas};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct DebugRenderOptions(u32);
@@ -86,30 +86,16 @@ fn render_glyph_run(
     let run_y = glyph_run.baseline();
     let style = glyph_run.style();
     let color = style.brush;
-
-    let run = glyph_run.run();
     trace!(target: "text::render", "render_glyph_run run_idx={run_idx}");
 
-    let font = run.font();
-    let font_size = run.font_size();
-    let normalized_coords = run.normalized_coords();
-    let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
+    let atlas = create_atlas(scale_ctx, glyph_run, render_api);
 
-    let mut scaler = scale_ctx
-        .builder(font_ref)
-        .size(font_size)
-        .hint(true)
-        .normalized_coords(normalized_coords)
-        .build();
+    let mut mesh = MeshBuilder::new();
 
-    let mut atlas = Atlas::new(scaler, render_api);
-    for glyph in glyph_run.glyphs() {
-        atlas.push_glyph(glyph);
+    if let Some(underline) = &style.underline {
+        render_underline(underline, glyph_run, &mut mesh);
     }
-    //atlas.dump(&format!("/tmp/atlas_{run_idx}.png"));
-    let atlas = atlas.make();
 
-    let mut mesh = MeshBuilder::new();
     for glyph in glyph_run.glyphs() {
         let glyph_inf = atlas.fetch_uv(glyph.id).expect("missing glyph UV rect");
 
@@ -141,3 +127,60 @@ fn render_glyph_run(
 
     mesh.alloc(render_api).draw_with_texture(atlas.texture)
 }
+
+fn render_underline(
+    underline: &parley::layout::Decoration<Color>,
+    glyph_run: &parley::GlyphRun<'_, Color>,
+    mesh: &mut MeshBuilder,
+) {
+    let color = underline.brush;
+    let run_metrics = glyph_run.run().metrics();
+    let offset = match underline.offset {
+        Some(offset) => offset,
+        None => run_metrics.underline_offset,
+    };
+    let width = match underline.size {
+        Some(size) => size,
+        None => run_metrics.underline_size,
+    };
+    // The `offset` is the distance from the baseline to the top of the underline
+    // so we move the line down by half the width
+    // 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 start_x = glyph_run.offset();
+    let end_x = start_x + glyph_run.advance();
+
+    let start = Point::new(start_x, y);
+    let end = Point::new(end_x, y);
+
+    mesh.draw_line(start, end, color, width);
+}
+
+fn create_atlas(
+    scale_ctx: &mut swash::scale::ScaleContext,
+    glyph_run: &parley::GlyphRun<'_, Color>,
+    render_api: &RenderApi,
+) -> RenderedAtlas {
+    let run = glyph_run.run();
+    let font = run.font();
+    let font_size = run.font_size();
+    let normalized_coords = run.normalized_coords();
+    let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
+
+    let mut scaler = scale_ctx
+        .builder(font_ref)
+        .size(font_size)
+        .hint(true)
+        .normalized_coords(normalized_coords)
+        .build();
+
+    let mut atlas = Atlas::new(scaler, render_api);
+    for glyph in glyph_run.glyphs() {
+        atlas.push_glyph(glyph.id);
+    }
+    //atlas.dump(&format!("/tmp/atlas_{run_idx}.png"));
+    atlas.make()
+}

+ 1 - 1
bin/app/src/ui/chatedit.rs

@@ -131,7 +131,7 @@ impl TouchInfo {
     fn update(&mut self, pos: &Point) {
         match &self.state {
             TouchStateAction::Started { pos: start_pos, instant } => {
-                let travel_dist = pos.dist_sq(&start_pos);
+                let travel_dist = pos.dist_sq(*start_pos);
                 let grad = (pos.y - start_pos.y) / (pos.x - start_pos.x);
                 let elapsed = instant.elapsed().as_millis();
                 //debug!(target: "ui::chatedit::touch", "TouchInfo::update() [travel_dist={travel_dist}, grad={grad}]");

+ 3 - 3
bin/app/src/ui/editbox/mod.rs

@@ -119,7 +119,7 @@ impl TouchInfo {
     fn update(&mut self, pos: &Point) {
         match &self.state {
             TouchStateAction::Started { pos: start_pos, instant } => {
-                let travel_dist = pos.dist_sq(&start_pos);
+                let travel_dist = pos.dist_sq(*start_pos);
                 let x_dist = pos.x - start_pos.x;
                 let elapsed = instant.elapsed().as_millis();
 
@@ -961,13 +961,13 @@ impl EditBox {
             // Make pos relative to the rect
             let pos_rel = pos - self.rect.get().pos();
 
-            if p1.dist_sq(&pos_rel) <= TOUCH_RADIUS_SQ {
+            if p1.dist_sq(pos_rel) <= TOUCH_RADIUS_SQ {
                 d!("TouchStateAction::DragSelectHandle [side=-1]");
                 // Set touch_state status to enable begin dragging them
                 touch_info.state = TouchStateAction::DragSelectHandle { side: -1 };
                 return true
             }
-            if p2.dist_sq(&pos_rel) <= TOUCH_RADIUS_SQ {
+            if p2.dist_sq(pos_rel) <= TOUCH_RADIUS_SQ {
                 d!("TouchStateAction::DragSelectHandle [side=1]");
                 // Set touch_state status to enable begin dragging them
                 touch_info.state = TouchStateAction::DragSelectHandle { side: 1 };

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

@@ -196,7 +196,7 @@ impl EmojiPicker {
                 let node = self.node.upgrade().unwrap();
                 node.trigger("emoji_select", param_data).await.unwrap();
             }
-            None => d!("Index out of bounds: {idx}")
+            None => d!("Index out of bounds: {idx}"),
         }
     }
 

+ 2 - 2
bin/app/src/ui/gesture.rs

@@ -74,8 +74,8 @@ impl Gesture {
         let Some(start_2) = state.start[1] else { return None };
         let curr_2 = state.curr[1].unwrap();
 
-        let start_dist_sq = start_1.dist_sq(&start_2);
-        let curr_dist_sq = curr_1.dist_sq(&curr_2);
+        let start_dist_sq = start_1.dist_sq(start_2);
+        let curr_dist_sq = curr_1.dist_sq(curr_2);
         let r = (curr_dist_sq / start_dist_sq).sqrt();
 
         Some(r)

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

@@ -125,7 +125,7 @@ impl Text {
 
         let layout = {
             let mut txt_ctx = TEXT_CTX.get().await;
-            txt_ctx.make_layout(&text, text_color, font_size, 0., window_scale, None)
+            txt_ctx.make_layout(&text, text_color, font_size, 0., window_scale, None, &[])
         };
 
         text2::render_layout(&layout, &self.render_api)