From b2db7119e508c44de1924fd59fc1571978a3103a Mon Sep 17 00:00:00 2001 From: rokokol Date: Sat, 4 Jul 2026 13:29:22 +0300 Subject: [PATCH 1/3] feat: toggle text outline in inverted color with Alt Tapping Alt while editing text toggles an outline drawn in the inverted text color (255 - channel per RGB, alpha kept). The outline is rendered by stroking each line with the inverted color underneath the fill, so it forms a border around the glyphs, and it is preserved when the text is committed. To avoid interfering with Alt-based combos (e.g. Ctrl+Alt+Arrow to move the text) and key auto-repeat, the toggle only fires on a genuine Alt tap: press then release with no other key pressed in between. --- README.md | 1 + src/style.rs | 5 +++++ src/tools/text.rs | 45 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cff5ae2e..c3e6af0a 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Text: - Ctrl+X to cut selected text to clipboard. 0.20.1 - Ctrl+V to paste text from clipboard. 0.20.1 - Alt+Ctrl with Left or Right or Up or Down to move the text. Use Alt+Ctrl+Shift with arrow keys to nudge the text. 0.20.1 +- Press Alt to toggle outlining the text with the inverted text color. NEXTRELEASE Marker: - Hold Alt to get extra ring. NEXTRELEASE diff --git a/src/style.rs b/src/style.rs index 2733bd00..5f2c8615 100644 --- a/src/style.rs +++ b/src/style.rs @@ -109,6 +109,11 @@ impl Color { Self::new(200, 37, 184, 255) } + /// Returns the color with its RGB channels inverted, preserving alpha. + pub fn inverted(self) -> Self { + Self::new(255 - self.r, 255 - self.g, 255 - self.b, self.a) + } + pub fn to_rgba_f64(self) -> (f64, f64, f64, f64) { ( (self.r as f64) / 255.0, diff --git a/src/tools/text.rs b/src/tools/text.rs index 9afe91d4..8a8219d3 100644 --- a/src/tools/text.rs +++ b/src/tools/text.rs @@ -39,6 +39,7 @@ pub struct Text { line_ranges: RefCell>>, cursor_visible: RefCell, draw_rect: RefCell, + outline: bool, font_ids: Vec, } @@ -83,6 +84,7 @@ impl Text { line_ranges: RefCell::new(Vec::new()), cursor_visible: RefCell::new(true), draw_rect: RefCell::new(true), + outline: false, font_ids: femtovg_area::font_stack().to_vec(), } } @@ -352,13 +354,22 @@ impl Drawable for Text { canvas.stroke_path(&rect_paint, &paint); } + // When outlining, stroke each line with the inverted text color first, then + // fill on top so the visible border wraps the glyphs. The stroke is widened + // because it is centered on the glyph outline and half of it is hidden by the fill. + let outline_paint = self.outline.then(|| { + let mut paint = base_paint.clone(); + paint.set_color(self.style.color.inverted().into()); + paint.set_line_width(base_paint.line_width() * 2.0); + paint + }); + for line_range in &lines { - canvas.fill_text( - self.pos.x, - draw_baseline, - &text[line_range.clone()], - &base_paint, - )?; + let line = &text[line_range.clone()]; + if let Some(outline_paint) = &outline_paint { + canvas.stroke_text(self.pos.x, draw_baseline, line, outline_paint)?; + } + canvas.fill_text(self.pos.x, draw_baseline, line, &base_paint)?; draw_baseline += line_height; } @@ -710,6 +721,7 @@ pub struct TextTool { sender: Option>, drag_start_pos: Vec2D, dragged: Rc>, + alt_tap: bool, } impl Tool for TextTool { @@ -794,6 +806,11 @@ impl Tool for TextTool { } fn handle_key_event(&mut self, event: KeyEventMsg) -> ToolUpdateResult { + // Any key other than Alt cancels a pending Alt tap: Alt is being used as a + // modifier (e.g. Ctrl+Alt+Arrow), not tapped on its own to toggle the outline. + if !matches!(event.key, Key::Alt_L | Key::Alt_R) { + self.alt_tap = false; + } let mut tool_update_result = ToolUpdateResult::StopPropagation; if let Some(t) = &mut self.text { match event.key { @@ -829,6 +846,11 @@ impl Tool for TextTool { Key::Escape => { tool_update_result = self.handle_deactivated(); } + Key::Alt_L | Key::Alt_R => { + // Start tracking a potential Alt tap; the outline is toggled on + // release (see handle_key_release_event) if no other key was pressed. + self.alt_tap = true; + } Key::BackSpace | Key::Delete => { let ctrl_mask = match event.key { Key::BackSpace => ActionScope::BackwardWord, @@ -1078,6 +1100,17 @@ impl Tool for TextTool { tool_update_result } + fn handle_key_release_event(&mut self, event: KeyEventMsg) -> ToolUpdateResult { + if (event.key == Key::Alt_L || event.key == Key::Alt_R) && self.alt_tap { + self.alt_tap = false; + if let Some(t) = &mut self.text { + t.outline = !t.outline; + return ToolUpdateResult::RedrawAndStopPropagation; + } + } + ToolUpdateResult::Unmodified + } + fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult { match event.type_ { MouseEventType::Click => { From 808ea33f1a6651de403c927f549fe94e0521ae48 Mon Sep 17 00:00:00 2001 From: rokokol Date: Sat, 4 Jul 2026 14:19:28 +0300 Subject: [PATCH 2/3] docs: flag Alt text outline as experimental The way tool styles, secondary modes and tool-specific actions are accessed may be revamped (ideally with configurable keybindings) after issue #555, so mark this binding as experimental for now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3e6af0a..4bde7027 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Text: - Ctrl+X to cut selected text to clipboard. 0.20.1 - Ctrl+V to paste text from clipboard. 0.20.1 - Alt+Ctrl with Left or Right or Up or Down to move the text. Use Alt+Ctrl+Shift with arrow keys to nudge the text. 0.20.1 -- Press Alt to toggle outlining the text with the inverted text color. NEXTRELEASE +- Press Alt to toggle outlining the text with the inverted text color. experimental NEXTRELEASE Marker: - Hold Alt to get extra ring. NEXTRELEASE From 36ba54e0340d90a07588527b3491bc64b18a4491 Mon Sep 17 00:00:00 2001 From: rokokol Date: Sat, 4 Jul 2026 14:20:35 +0300 Subject: [PATCH 3/3] feat: cycle text outline modes (none/inverted/contrast) with Alt Replace the boolean outline flag with an OutlineMode enum that the Alt tap cycles through: None -> Inverted -> Contrast -> None. Inverted keeps the previous behaviour (per-channel 255 - value); Contrast picks black or white based on the text color's perceived luminance (YIQ). Co-authored idea by the maintainer. --- README.md | 2 +- src/style.rs | 11 +++++++++++ src/tools/text.rs | 47 ++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4bde7027..8434b398 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Text: - Ctrl+X to cut selected text to clipboard. 0.20.1 - Ctrl+V to paste text from clipboard. 0.20.1 - Alt+Ctrl with Left or Right or Up or Down to move the text. Use Alt+Ctrl+Shift with arrow keys to nudge the text. 0.20.1 -- Press Alt to toggle outlining the text with the inverted text color. experimental NEXTRELEASE +- Press Alt to cycle the text outline: none → inverted text color → contrast (black or white, by luminance). experimental NEXTRELEASE Marker: - Hold Alt to get extra ring. NEXTRELEASE diff --git a/src/style.rs b/src/style.rs index 5f2c8615..d2b7babe 100644 --- a/src/style.rs +++ b/src/style.rs @@ -114,6 +114,17 @@ impl Color { Self::new(255 - self.r, 255 - self.g, 255 - self.b, self.a) } + /// Returns black or white, whichever contrasts better with this color, + /// based on its perceived luminance (YIQ), preserving alpha. + pub fn contrast(self) -> Self { + let luminance = (self.r as u32 * 299 + self.g as u32 * 587 + self.b as u32 * 114) / 1000; + if luminance >= 128 { + Self::new(0, 0, 0, self.a) + } else { + Self::new(255, 255, 255, self.a) + } + } + pub fn to_rgba_f64(self) -> (f64, f64, f64, f64) { ( (self.r as f64) / 255.0, diff --git a/src/tools/text.rs b/src/tools/text.rs index 8a8219d3..495894ea 100644 --- a/src/tools/text.rs +++ b/src/tools/text.rs @@ -26,6 +26,35 @@ use relm4::gtk::gdk::DisplayManager; use std::cell::RefCell; use std::rc::Rc; +/// How the text outline is rendered, cycled through with the Alt key. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum OutlineMode { + #[default] + None, + Inverted, + Contrast, +} + +impl OutlineMode { + /// Cycles None -> Inverted -> Contrast -> None. + fn next(self) -> Self { + match self { + Self::None => Self::Inverted, + Self::Inverted => Self::Contrast, + Self::Contrast => Self::None, + } + } + + /// The outline color for the given text color, or None when disabled. + fn outline_color(self, text_color: crate::style::Color) -> Option { + match self { + Self::None => None, + Self::Inverted => Some(text_color.inverted()), + Self::Contrast => Some(text_color.contrast()), + } + } +} + #[derive(Clone, Debug)] pub struct Text { pos: Vec2D, @@ -39,7 +68,7 @@ pub struct Text { line_ranges: RefCell>>, cursor_visible: RefCell, draw_rect: RefCell, - outline: bool, + outline: OutlineMode, font_ids: Vec, } @@ -84,7 +113,7 @@ impl Text { line_ranges: RefCell::new(Vec::new()), cursor_visible: RefCell::new(true), draw_rect: RefCell::new(true), - outline: false, + outline: OutlineMode::default(), font_ids: femtovg_area::font_stack().to_vec(), } } @@ -354,12 +383,12 @@ impl Drawable for Text { canvas.stroke_path(&rect_paint, &paint); } - // When outlining, stroke each line with the inverted text color first, then - // fill on top so the visible border wraps the glyphs. The stroke is widened - // because it is centered on the glyph outline and half of it is hidden by the fill. - let outline_paint = self.outline.then(|| { + // When outlining, stroke each line with the outline color first, then fill on + // top so the visible border wraps the glyphs. The stroke is widened because it + // is centered on the glyph outline and half of it is hidden by the fill. + let outline_paint = self.outline.outline_color(self.style.color).map(|color| { let mut paint = base_paint.clone(); - paint.set_color(self.style.color.inverted().into()); + paint.set_color(color.into()); paint.set_line_width(base_paint.line_width() * 2.0); paint }); @@ -847,7 +876,7 @@ impl Tool for TextTool { tool_update_result = self.handle_deactivated(); } Key::Alt_L | Key::Alt_R => { - // Start tracking a potential Alt tap; the outline is toggled on + // Start tracking a potential Alt tap; the outline mode is cycled on // release (see handle_key_release_event) if no other key was pressed. self.alt_tap = true; } @@ -1104,7 +1133,7 @@ impl Tool for TextTool { if (event.key == Key::Alt_L || event.key == Key::Alt_R) && self.alt_tap { self.alt_tap = false; if let Some(t) = &mut self.text { - t.outline = !t.outline; + t.outline = t.outline.next(); return ToolUpdateResult::RedrawAndStopPropagation; } }