diff --git a/README.md b/README.md
index cff5ae2e..8434b398 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 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 2733bd00..d2b7babe 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -109,6 +109,22 @@ 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)
+ }
+
+ /// 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 9afe91d4..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,6 +68,7 @@ pub struct Text {
line_ranges: RefCell>>,
cursor_visible: RefCell,
draw_rect: RefCell,
+ outline: OutlineMode,
font_ids: Vec,
}
@@ -83,6 +113,7 @@ impl Text {
line_ranges: RefCell::new(Vec::new()),
cursor_visible: RefCell::new(true),
draw_rect: RefCell::new(true),
+ outline: OutlineMode::default(),
font_ids: femtovg_area::font_stack().to_vec(),
}
}
@@ -352,13 +383,22 @@ impl Drawable for Text {
canvas.stroke_path(&rect_paint, &paint);
}
+ // 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(color.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 +750,7 @@ pub struct TextTool {
sender: Option>,
drag_start_pos: Vec2D,
dragged: Rc>,
+ alt_tap: bool,
}
impl Tool for TextTool {
@@ -794,6 +835,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 +875,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 mode is cycled 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 +1129,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.next();
+ return ToolUpdateResult::RedrawAndStopPropagation;
+ }
+ }
+ ToolUpdateResult::Unmodified
+ }
+
fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult {
match event.type_ {
MouseEventType::Click => {