瀏覽代碼

app/chatview: wrap now to start of line not right of timestamp making full use of screen width

darkfi 2 周之前
父節點
當前提交
fdcbc6f42c
共有 1 個文件被更改,包括 79 次插入 和 47 次删除
  1. 79 47
      bin/app/src/ui/chatview/msg/privmsg.rs

+ 79 - 47
bin/app/src/ui/chatview/msg/privmsg.rs

@@ -48,15 +48,17 @@ use crate::{
     },
     scene::{CallArgType, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak},
     text,
-    ui::UIObject,
+    ui::{
+        chatview::{
+            buffer::MsgBuffer, codec, loader::Loader, ChatView, MessageId, MsgRecord, MsgType,
+            Timestamp, Wakeup,
+        },
+        UIObject,
+    },
     ExecutorPtr,
 };
 
 use super::{evict_beyond, filemsg::get_file_url, DrawOutcome, Hit, SharedProps};
-use crate::ui::chatview::{
-    buffer::MsgBuffer, codec, loader::Loader, ChatView, MessageId, MsgRecord, MsgType, Timestamp,
-    Wakeup,
-};
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatview::privmsg", $($arg)*); } }
 
@@ -205,6 +207,7 @@ impl PrivData {
 #[derive(PartialEq)]
 struct LayoutSig {
     width: f32,
+    timestamp_width: f32,
     font_size: f32,
     timestamp_font_size: f32,
     line_height: f32,
@@ -325,7 +328,7 @@ impl PrivMsgNode {
 
     /// Read the current layout signature off the live properties.
     fn current_sig(&self, data: &PrivData) -> LayoutSig {
-        let width = self.shared.rect.get().w - self.shared.timestamp_width.get();
+        let width = self.shared.rect.get().w;
         let font_size = if data.is_notice {
             self.shared.font_size.get() * 0.8
         } else {
@@ -346,6 +349,7 @@ impl PrivMsgNode {
         };
         LayoutSig {
             width,
+            timestamp_width: self.shared.timestamp_width.get(),
             font_size,
             timestamp_font_size: self.shared.timestamp_font_size.get(),
             line_height: self.shared.line_height.get(),
@@ -377,33 +381,19 @@ impl PrivMsgNode {
             self.own.url_text_color.get(),
         ));
 
-        let txt_layout = if data.is_notice {
-            text::make_layout2(
-                &linetext,
-                sig.body_color,
-                sig.font_size,
-                line_height / sig.font_size,
-                window_scale,
-                Some(sig.width),
-                &[],
-                &[],
-                parley::Alignment::Start,
-                parley::OverflowWrap::Normal,
-            )
-        } else {
-            text::make_layout2(
-                &linetext,
-                sig.body_color,
-                sig.font_size,
-                line_height / sig.font_size,
-                window_scale,
-                Some(sig.width),
-                &[],
-                &foreground_colors,
-                parley::Alignment::Start,
-                parley::OverflowWrap::Normal,
-            )
-        };
+        let txt_layout = text::make_layout_with_indent(
+            &linetext,
+            sig.body_color,
+            sig.font_size,
+            line_height / sig.font_size,
+            window_scale,
+            Some(sig.width),
+            &[],
+            if data.is_notice { &[] } else { &foreground_colors },
+            parley::Alignment::Start,
+            parley::OverflowWrap::Normal,
+            sig.timestamp_width,
+        );
 
         let timestr = gen_timestr(data.ts);
         let ts_layout = text::make_layout(
@@ -420,11 +410,9 @@ impl PrivMsgNode {
         // clickable regions in message-local coordinates.
         let url_color = self.own.url_text_color.get();
         let nick_color = sig.nick_color;
-        let timestamp_width = self.shared.timestamp_width.get();
         let url_rects = Self::compute_hit_rects(
             &txt_layout,
             &data,
-            timestamp_width,
             url_color,
             &url_ranges_of(&data, url_color),
             |raw| sanitize_url(raw),
@@ -433,14 +421,9 @@ impl PrivMsgNode {
             vec![]
         } else {
             let nick_range = 0..data.body_offset();
-            Self::compute_hit_rects(
-                &txt_layout,
-                &data,
-                timestamp_width,
-                nick_color,
-                &[nick_range],
-                |raw| Some(raw.trim_end().to_string()),
-            )
+            Self::compute_hit_rects(&txt_layout, &data, nick_color, &[nick_range], |raw| {
+                Some(raw.trim_end().to_string())
+            })
         };
 
         // Cap/expand: long messages collapse to the cap by default.
@@ -607,7 +590,6 @@ impl PrivMsgNode {
         if inst.instrs.is_none() {
             let mut instrs =
                 text::render_layout(&inst.ts_layout, renderer, gfxtag!("chatview_privmsg_ts"));
-            instrs.push(DrawInstruction::Move(Point::new(self.shared.timestamp_width.get(), 0.)));
 
             // URL backgrounds (and optional borders) behind the URL
             // runs, under the glyphs. render_backgrounds matches runs
@@ -731,7 +713,6 @@ impl PrivMsgNode {
     fn compute_hit_rects(
         layout: &text::TextLayout,
         data: &PrivData,
-        timestamp_width: f32,
         match_color: Color,
         ranges: &[std::ops::Range<usize>],
         payload_fn: impl Fn(&str) -> Option<String>,
@@ -758,7 +739,7 @@ impl PrivMsgNode {
                 let Some(payload) = payload_fn(&linetext[hit_range.clone()]) else { continue };
 
                 let metrics = glyph_run.run().metrics();
-                let x = timestamp_width + glyph_run.offset() / scale;
+                let x = 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;
@@ -1163,6 +1144,51 @@ mod tests {
         assert_eq!(node.inner.lock().layout_builds, 2);
     }
 
+    #[test]
+    fn message_variants_indent_only_first_line() {
+        let (chat, node, _buffer) = smol::block_on(make_node("leftwrap"));
+        let atom = &mut PropertyAtomicGuard::none();
+        chat.get_property("rect").unwrap().set_f32(atom, Role::App, 2, 240.).unwrap();
+        let body = "word ".repeat(30);
+        for (nick, text, copied) in [
+            ("alice", body.clone(), format!("alice {body}")),
+            ("alice", format!("\u{1}ACTION {body}\u{1}"), format!("* alice {body}")),
+            ("NOTICE", body.clone(), body.clone()),
+        ] {
+            let mut rec = rec_of(1_000_000, b'a', &text, true);
+            rec.payload = codec::encode_privmsg_payload(nick, &text, true);
+            let height = node.measure(&rec);
+            assert_eq!(node.copy_text(&rec), Some(copied));
+            let inner = node.inner.lock();
+            let inst = &inner.instances[&(rec.ts, rec.id)];
+            assert!(inst.txt_layout.lines().count() > 2);
+            for (i, line) in inst.txt_layout.lines().enumerate() {
+                assert_eq!(line.metrics().offset, if i == 0 { 50. } else { 0. });
+                assert_eq!(line.metrics().inline_max_coord, 240.);
+            }
+            assert!((height - inst.txt_layout.height() - 4.).abs() < 0.01);
+        }
+    }
+
+    #[test]
+    fn timestamp_width_change_rebuilds_first_line() {
+        let (chat, node, _buffer) = smol::block_on(make_node("timestampwidth"));
+        let rec = rec_of(1_000_000, b'a', "hello", true);
+        let height = node.measure(&rec);
+        assert_eq!(node.nick_rects(&rec)[0].1.x, 50.);
+
+        let atom = &mut PropertyAtomicGuard::none();
+        chat.set_property_f32(atom, Role::App, "timestamp_width", 80.).unwrap();
+        assert_eq!(node.measure(&rec), height);
+        assert_eq!(node.inner.lock().layout_builds, 2);
+        assert_eq!(node.nick_rects(&rec)[0].1.x, 80.);
+
+        node.shared.window_scale.set(atom, 1.5);
+        assert!((node.measure(&rec) - height).abs() < 0.01);
+        assert_eq!(node.inner.lock().layout_builds, 3);
+        assert!((node.nick_rects(&rec)[0].1.x - 80.).abs() < 0.01);
+    }
+
     #[test]
     fn styling_and_data_changes_invalidate() {
         let (chat, node, _buffer) = smol::block_on(make_node("styling"));
@@ -1309,6 +1335,7 @@ mod tests {
         assert!(!nicks.is_empty(), "nick prefix is clickable");
         let (nick, nrect) = &nicks[0];
         assert_eq!(nick, "alice");
+        assert_eq!(nrect.x, 50.);
         assert_eq!(
             node.hit_test(&rec, Point::new(nrect.x + nrect.w / 2., nrect.y + nrect.h / 2.)),
             Some(Hit::Nick("alice".to_string()))
@@ -1332,9 +1359,14 @@ mod tests {
 
         let rects = node.url_rects(&rec);
         assert!(rects.len() >= 2, "wrapped URL runs: {}", rects.len());
-        for (url, _rect) in &rects {
+        for (url, rect) in &rects {
             assert!(url.starts_with("https://example.com/"), "{url}");
+            assert_eq!(
+                node.hit_test(&rec, Point::new(rect.x + 1., rect.y + rect.h / 2.)),
+                Some(Hit::Url(url.clone()))
+            );
         }
+        assert!(rects.iter().any(|(_, rect)| rect.x == 0. && rect.y > 20.));
     }
 
     #[test]