Преглед изворни кода

app/chatview: apply styling to URLs

darkfi пре 1 недеља
родитељ
комит
eae0ae5b26

+ 21 - 0
bin/app/src/app/node.rs

@@ -527,6 +527,27 @@ pub fn create_chatview(name: &str) -> SceneNode {
     prop.set_range_f32(0., 1.);
     prop.set_range_f32(0., 1.);
     node.add_property(prop).unwrap();
     node.add_property(prop).unwrap();
 
 
+    let mut prop = Property::new("url_text_color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("url_bg_color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    node.add_property(prop).unwrap();
+
+    let mut prop =
+        Property::new("url_bg_border_size", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
+    let mut prop =
+        Property::new("url_bg_border_color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("nick_colors", PropertyType::Float32, PropertySubType::Pixel);
     let mut prop = Property::new("nick_colors", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_unbounded();
     prop.set_unbounded();
     prop.set_range_f32(0., 1.);
     prop.set_range_f32(0., 1.);

+ 17 - 0
bin/app/src/app/schema/chat.rs

@@ -521,6 +521,23 @@ pub async fn make(
         prop.set_f32(atom, Role::App, 3, 1.).unwrap();
         prop.set_f32(atom, Role::App, 3, 1.).unwrap();
     }
     }
 
 
+    let prop = node.get_property("url_text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.94).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = node.get_property("url_bg_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.13).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.08).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    node.set_property_f32(atom, Role::App, "url_bg_border_size", 1.).unwrap();
+    let prop = node.get_property("url_bg_border_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.11).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.6).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.63).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+
     let prop = node.get_property("nick_colors").unwrap();
     let prop = node.get_property("nick_colors").unwrap();
     #[rustfmt::skip]
     #[rustfmt::skip]
     let nick_colors = [
     let nick_colors = [

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

@@ -29,7 +29,7 @@ pub mod atlas;
 mod editor;
 mod editor;
 pub use editor::Editor;
 pub use editor::Editor;
 mod render;
 mod render;
-pub use render::{render_layout, render_layout_with_opts, DebugRenderOptions};
+pub use render::{render_backgrounds, render_layout, render_layout_with_opts, DebugRenderOptions};
 
 
 pub static GLOBAL_FONT_CTX: LazyLock<parley::FontContext> = LazyLock::new(|| {
 pub static GLOBAL_FONT_CTX: LazyLock<parley::FontContext> = LazyLock::new(|| {
     let mut font_ctx = parley::FontContext {
     let mut font_ctx = parley::FontContext {

+ 56 - 0
bin/app/src/text/render.rs

@@ -57,6 +57,62 @@ pub fn render_layout(
     render_layout_with_opts(layout, DebugRenderOptions::OFF, renderer, tag)
     render_layout_with_opts(layout, 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
+/// across lines gets one box per wrapped line.
+///
+/// Matching by brush (rather than by byte range) is required because a parley
+/// `GlyphRun` does not expose its own byte range — only its parent font run does,
+/// and a font run is coarser than the per-color segment (e.g. the nick, body, and
+/// URL of one line all share a font run). Matching `style().brush` pins the box to
+/// exactly the color segment, so only the intended runs (here: the URL runs) are
+/// 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>,
+    match_brush: Color,
+    bg_color: Color,
+    border_color: Color,
+    border_size: f32,
+    renderer: &Renderer,
+    tag: DebugTag,
+) -> Vec<DrawInstruction> {
+    let mut instrs = vec![];
+    let has_fill = bg_color[3] > 0.;
+    let has_border = border_size > 0. && border_color[3] > 0.;
+    if !has_fill && !has_border {
+        return instrs
+    }
+
+    for line in layout.lines() {
+        for item in line.items() {
+            let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else { continue };
+            if glyph_run.style().brush != match_brush {
+                continue
+            }
+
+            let metrics = glyph_run.run().metrics();
+            let x = glyph_run.offset();
+            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 mut mesh = MeshBuilder::new(tag);
+            if has_fill {
+                mesh.draw_filled_box(&rect, bg_color);
+            }
+            if has_border {
+                mesh.draw_outline(&rect, border_color, border_size);
+            }
+            instrs.push(DrawInstruction::Draw(mesh.alloc(renderer).draw_untextured()));
+        }
+    }
+
+    instrs
+}
+
 pub fn render_layout_with_opts(
 pub fn render_layout_with_opts(
     layout: &parley::Layout<Color>,
     layout: &parley::Layout<Color>,
     opts: DebugRenderOptions,
     opts: DebugRenderOptions,

+ 11 - 0
bin/app/src/ui/chatview/mod.rs

@@ -216,6 +216,13 @@ impl ChatView {
         let timestamp_color =
         let timestamp_color =
             PropertyColor::wrap(node_ref, Role::Internal, "timestamp_color").unwrap();
             PropertyColor::wrap(node_ref, Role::Internal, "timestamp_color").unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
+        let url_text_color =
+            PropertyColor::wrap(node_ref, Role::Internal, "url_text_color").unwrap();
+        let url_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "url_bg_color").unwrap();
+        let url_bg_border_size =
+            PropertyFloat32::wrap(node_ref, Role::Internal, "url_bg_border_size", 0).unwrap();
+        let url_bg_border_color =
+            PropertyColor::wrap(node_ref, Role::Internal, "url_bg_border_color").unwrap();
         let nick_colors = node_ref.get_property("nick_colors").expect("ChatView::nick_colors");
         let nick_colors = node_ref.get_property("nick_colors").expect("ChatView::nick_colors");
         let hi_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "hi_bg_color").unwrap();
         let hi_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "hi_bg_color").unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
@@ -251,6 +258,10 @@ impl ChatView {
                 baseline,
                 baseline,
                 timestamp_color,
                 timestamp_color,
                 text_color,
                 text_color,
+                url_text_color,
+                url_bg_color,
+                url_bg_border_size,
+                url_bg_border_color,
                 nick_colors,
                 nick_colors,
                 hi_bg_color,
                 hi_bg_color,
                 window_scale,
                 window_scale,

+ 119 - 9
bin/app/src/ui/chatview/page.rs

@@ -24,14 +24,16 @@ use futures::stream::{Stream, StreamExt};
 use image::{ImageBuffer, ImageReader, Rgba};
 use image::{ImageBuffer, ImageReader, Rgba};
 use miniquad::{MouseButton, TextureFormat, TouchPhase};
 use miniquad::{MouseButton, TextureFormat, TouchPhase};
 use parking_lot::Mutex as SyncMutex;
 use parking_lot::Mutex as SyncMutex;
+use regex::Regex;
 use std::{
 use std::{
     collections::HashMap,
     collections::HashMap,
     hash::{DefaultHasher, Hash, Hasher},
     hash::{DefaultHasher, Hash, Hasher},
     io::Cursor,
     io::Cursor,
+    ops::Range,
     pin::pin,
     pin::pin,
     sync::{
     sync::{
         atomic::{AtomicBool, Ordering},
         atomic::{AtomicBool, Ordering},
-        Arc,
+        Arc, LazyLock,
     },
     },
 };
 };
 use url::Url;
 use url::Url;
@@ -54,6 +56,13 @@ macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatview::message_buffer
 
 
 const UNCONF_COLOR: [f32; 4] = [0.4, 0.4, 0.4, 1.];
 const UNCONF_COLOR: [f32; 4] = [0.4, 0.4, 0.4, 1.];
 
 
+static URL_REGEX: LazyLock<Regex> =
+    LazyLock::new(|| Regex::new(r"https?://[^\s]+|fud://[^\s]+|www\.[^\s]+").unwrap());
+
+fn url_color_ranges(text: &str, offset: usize, color: Color) -> Vec<(Range<usize>, Color)> {
+    URL_REGEX.find_iter(text).map(|m| (m.start() + offset..m.end() + offset, color)).collect()
+}
+
 #[derive(Clone)]
 #[derive(Clone)]
 pub struct PrivMessage {
 pub struct PrivMessage {
     font_size: f32,
     font_size: f32,
@@ -119,6 +128,7 @@ impl PrivMessage {
         timestamp_width: f32,
         timestamp_width: f32,
         nick_colors: &[Color],
         nick_colors: &[Color],
         text_color: Color,
         text_color: Color,
+        url_text_color: Color,
     ) {
     ) {
         if self.txt_layout.is_some() {
         if self.txt_layout.is_some() {
             return
             return
@@ -133,8 +143,14 @@ impl PrivMessage {
 
 
         let nick_color = select_nick_color(&self.nick, nick_colors);
         let nick_color = select_nick_color(&self.nick, nick_colors);
 
 
-        let txt_layout = if self.nick == "NOTICE" {
-            text::make_layout(
+        let is_notice = self.nick == "NOTICE";
+        // Byte offset of the body within linetext. NOTICE has no nick prefix;
+        // normal messages are "<nick> <body>" so the body starts after nick + space.
+        let body_offset = if is_notice { 0 } else { self.nick.len() + 1 };
+        let url_ranges = url_color_ranges(&self.text, body_offset, url_text_color);
+
+        let txt_layout = if is_notice {
+            text::make_layout2(
                 &linetext,
                 &linetext,
                 text_color,
                 text_color,
                 self.font_size,
                 self.font_size,
@@ -142,10 +158,15 @@ impl PrivMessage {
                 self.window_scale,
                 self.window_scale,
                 Some(clip.w - timestamp_width),
                 Some(clip.w - timestamp_width),
                 &[],
                 &[],
+                &url_ranges,
+                "start",
+                "normal",
             )
             )
         } else {
         } else {
             let body_color = if self.confirmed { text_color } else { UNCONF_COLOR };
             let body_color = if self.confirmed { text_color } else { UNCONF_COLOR };
             let nick_end = self.nick.len() + 1;
             let nick_end = self.nick.len() + 1;
+            let mut foreground_colors = vec![(0..nick_end, nick_color)];
+            foreground_colors.extend(url_ranges);
             text::make_layout2(
             text::make_layout2(
                 &linetext,
                 &linetext,
                 body_color,
                 body_color,
@@ -154,7 +175,7 @@ impl PrivMessage {
                 self.window_scale,
                 self.window_scale,
                 Some(clip.w - timestamp_width),
                 Some(clip.w - timestamp_width),
                 &[],
                 &[],
-                &[(0..nick_end, nick_color)],
+                &foreground_colors,
                 "start",
                 "start",
                 "normal",
                 "normal",
             )
             )
@@ -171,6 +192,10 @@ impl PrivMessage {
         nick_colors: &[Color],
         nick_colors: &[Color],
         timestamp_color: Color,
         timestamp_color: Color,
         text_color: Color,
         text_color: Color,
+        url_text_color: Color,
+        url_bg_color: Color,
+        url_bg_border_size: f32,
+        url_bg_border_color: Color,
         hi_bg_color: Color,
         hi_bg_color: Color,
         renderer: &Renderer,
         renderer: &Renderer,
     ) -> Vec<DrawInstruction> {
     ) -> Vec<DrawInstruction> {
@@ -191,7 +216,14 @@ impl PrivMessage {
             &[],
             &[],
         );
         );
 
 
-        self.cache_txt_layout(clip, line_height, timestamp_width, nick_colors, text_color);
+        self.cache_txt_layout(
+            clip,
+            line_height,
+            timestamp_width,
+            nick_colors,
+            text_color,
+            url_text_color,
+        );
 
 
         let mut all_instrs = vec![];
         let mut all_instrs = vec![];
 
 
@@ -213,6 +245,24 @@ impl PrivMessage {
 
 
         // Render message text offset by timestamp_width
         // Render message text offset by timestamp_width
         all_instrs.push(DrawInstruction::Move(Point::new(timestamp_width, 0.)));
         all_instrs.push(DrawInstruction::Move(Point::new(timestamp_width, 0.)));
+
+        // Draw URL background (and optional border) behind the URL runs, under the
+        // glyphs. render_backgrounds matches glyph runs by their style brush, so
+        // only the URL-colored runs (brush == url_text_color) get a box — never the
+        // nick or surrounding body text.
+        if URL_REGEX.is_match(&self.text) {
+            let bg_instrs = text::render_backgrounds(
+                self.txt_layout.as_ref().unwrap(),
+                url_text_color,
+                url_bg_color,
+                url_bg_border_color,
+                url_bg_border_size,
+                renderer,
+                gfxtag!("chatview_privmsg_urlbg"),
+            );
+            all_instrs.extend(bg_instrs);
+        }
+
         let text_instrs = text::render_layout(
         let text_instrs = text::render_layout(
             self.txt_layout.as_ref().unwrap(),
             self.txt_layout.as_ref().unwrap(),
             renderer,
             renderer,
@@ -742,10 +792,18 @@ impl Message {
         timestamp_width: f32,
         timestamp_width: f32,
         nick_colors: &[Color],
         nick_colors: &[Color],
         text_color: Color,
         text_color: Color,
+        url_text_color: Color,
     ) {
     ) {
         match self {
         match self {
             Self::Priv(m) => {
             Self::Priv(m) => {
-                m.cache_txt_layout(clip, line_height, timestamp_width, nick_colors, text_color);
+                m.cache_txt_layout(
+                    clip,
+                    line_height,
+                    timestamp_width,
+                    nick_colors,
+                    text_color,
+                    url_text_color,
+                );
             }
             }
             Self::Date(_) => {}
             Self::Date(_) => {}
             Self::File(_) => {}
             Self::File(_) => {}
@@ -761,6 +819,10 @@ impl Message {
         nick_colors: &[Color],
         nick_colors: &[Color],
         timestamp_color: Color,
         timestamp_color: Color,
         text_color: Color,
         text_color: Color,
+        url_text_color: Color,
+        url_bg_color: Color,
+        url_bg_border_size: f32,
+        url_bg_border_color: Color,
         hi_bg_color: Color,
         hi_bg_color: Color,
         renderer: &Renderer,
         renderer: &Renderer,
     ) -> Vec<DrawInstruction> {
     ) -> Vec<DrawInstruction> {
@@ -774,6 +836,10 @@ impl Message {
                     nick_colors,
                     nick_colors,
                     timestamp_color,
                     timestamp_color,
                     text_color,
                     text_color,
+                    url_text_color,
+                    url_bg_color,
+                    url_bg_border_size,
+                    url_bg_border_color,
                     hi_bg_color,
                     hi_bg_color,
                     renderer,
                     renderer,
                 )
                 )
@@ -866,6 +932,10 @@ pub struct MessageBuffer {
     baseline: PropertyFloat32,
     baseline: PropertyFloat32,
     timestamp_color: PropertyColor,
     timestamp_color: PropertyColor,
     text_color: PropertyColor,
     text_color: PropertyColor,
+    url_text_color: PropertyColor,
+    url_bg_color: PropertyColor,
+    url_bg_border_size: PropertyFloat32,
+    url_bg_border_color: PropertyColor,
     nick_colors: PropertyPtr,
     nick_colors: PropertyPtr,
     hi_bg_color: PropertyColor,
     hi_bg_color: PropertyColor,
 
 
@@ -887,6 +957,10 @@ impl MessageBuffer {
         baseline: PropertyFloat32,
         baseline: PropertyFloat32,
         timestamp_color: PropertyColor,
         timestamp_color: PropertyColor,
         text_color: PropertyColor,
         text_color: PropertyColor,
+        url_text_color: PropertyColor,
+        url_bg_color: PropertyColor,
+        url_bg_border_size: PropertyFloat32,
+        url_bg_border_color: PropertyColor,
         nick_colors: PropertyPtr,
         nick_colors: PropertyPtr,
         hi_bg_color: PropertyColor,
         hi_bg_color: PropertyColor,
         window_scale: PropertyFloat32,
         window_scale: PropertyFloat32,
@@ -905,6 +979,10 @@ impl MessageBuffer {
             baseline,
             baseline,
             timestamp_color,
             timestamp_color,
             text_color,
             text_color,
+            url_text_color,
+            url_bg_color,
+            url_bg_border_size,
+            url_bg_border_color,
             nick_colors,
             nick_colors,
             hi_bg_color,
             hi_bg_color,
 
 
@@ -953,6 +1031,7 @@ impl MessageBuffer {
         let timestamp_width = self.timestamp_width.get();
         let timestamp_width = self.timestamp_width.get();
         let msg_spacing = self.msg_spacing.get();
         let msg_spacing = self.msg_spacing.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
+        let url_text_color = self.url_text_color.get();
         let nick_colors = self.read_nick_colors();
         let nick_colors = self.read_nick_colors();
         let mut height = 0.;
         let mut height = 0.;
 
 
@@ -968,7 +1047,14 @@ impl MessageBuffer {
                 height += msg_spacing;
                 height += msg_spacing;
             }
             }
 
 
-            msg.cache_txt_layout(&rect, line_height, timestamp_width, &nick_colors, text_color);
+            msg.cache_txt_layout(
+                &rect,
+                line_height,
+                timestamp_width,
+                &nick_colors,
+                text_color,
+                url_text_color,
+            );
 
 
             height += msg.height(line_height);
             height += msg.height(line_height);
         }
         }
@@ -1015,6 +1101,7 @@ impl MessageBuffer {
         let timestamp_width = self.timestamp_width.get();
         let timestamp_width = self.timestamp_width.get();
         let window_scale = self.window_scale.get();
         let window_scale = self.window_scale.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
+        let url_text_color = self.url_text_color.get();
         let nick_colors = self.read_nick_colors();
         let nick_colors = self.read_nick_colors();
 
 
         let mut msg = PrivMessage::new(
         let mut msg = PrivMessage::new(
@@ -1027,7 +1114,14 @@ impl MessageBuffer {
             text,
             text,
         );
         );
 
 
-        msg.cache_txt_layout(&rect, line_height, timestamp_width, &nick_colors, text_color);
+        msg.cache_txt_layout(
+            &rect,
+            line_height,
+            timestamp_width,
+            &nick_colors,
+            text_color,
+            url_text_color,
+        );
 
 
         if self.msgs.is_empty() {
         if self.msgs.is_empty() {
             self.msgs.push(msg);
             self.msgs.push(msg);
@@ -1079,6 +1173,7 @@ impl MessageBuffer {
         let timestamp_width = self.timestamp_width.get();
         let timestamp_width = self.timestamp_width.get();
         let window_scale = self.window_scale.get();
         let window_scale = self.window_scale.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
+        let url_text_color = self.url_text_color.get();
         let nick_colors = self.read_nick_colors();
         let nick_colors = self.read_nick_colors();
 
 
         let mut msg = PrivMessage::new(
         let mut msg = PrivMessage::new(
@@ -1091,7 +1186,14 @@ impl MessageBuffer {
             text,
             text,
         );
         );
 
 
-        msg.cache_txt_layout(rect, line_height, timestamp_width, &nick_colors, text_color);
+        msg.cache_txt_layout(
+            rect,
+            line_height,
+            timestamp_width,
+            &nick_colors,
+            text_color,
+            url_text_color,
+        );
 
 
         let msg_height = msg.height(self.line_height.get());
         let msg_height = msg.height(self.line_height.get());
         self.msgs.push(msg);
         self.msgs.push(msg);
@@ -1110,6 +1212,10 @@ impl MessageBuffer {
 
 
         let timest_color = self.timestamp_color.get();
         let timest_color = self.timestamp_color.get();
         let text_color = self.text_color.get();
         let text_color = self.text_color.get();
+        let url_text_color = self.url_text_color.get();
+        let url_bg_color = self.url_bg_color.get();
+        let url_bg_border_size = self.url_bg_border_size.get();
+        let url_bg_border_color = self.url_bg_border_color.get();
         let nick_colors = self.read_nick_colors();
         let nick_colors = self.read_nick_colors();
         let hi_bg_color = self.hi_bg_color.get();
         let hi_bg_color = self.hi_bg_color.get();
 
 
@@ -1130,6 +1236,10 @@ impl MessageBuffer {
                     &nick_colors,
                     &nick_colors,
                     timest_color,
                     timest_color,
                     text_color,
                     text_color,
+                    url_text_color,
+                    url_bg_color,
+                    url_bg_border_size,
+                    url_bg_border_color,
                     hi_bg_color,
                     hi_bg_color,
                     &renderer,
                     &renderer,
                 )
                 )