Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Text:
- <kbd>Ctrl+X</kbd> to cut selected text to clipboard. <sup>0.20.1</sup>
- <kbd>Ctrl+V</kbd> to paste text from clipboard. <sup>0.20.1</sup>
- <kbd>Alt+Ctrl</kbd> with <kbd>Left</kbd> or <kbd>Right</kbd> or <kbd>Up</kbd> or <kbd>Down</kbd> to move the text. Use <kbd>Alt+Ctrl+Shift</kbd> with arrow keys to nudge the text. <sup>0.20.1</sup>
- Press <kbd>Alt</kbd> to cycle the text outline: none → inverted text color → contrast (black or white, by luminance). <sup>experimental</sup> <sup>NEXTRELEASE</sup>

Marker:
- Hold <kbd>Alt</kbd> to get extra ring. <sup>NEXTRELEASE</sup>
Expand Down
16 changes: 16 additions & 0 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
74 changes: 68 additions & 6 deletions src/tools/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::style::Color> {
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,
Expand All @@ -39,6 +68,7 @@ pub struct Text {
line_ranges: RefCell<Vec<Range<usize>>>,
cursor_visible: RefCell<bool>,
draw_rect: RefCell<bool>,
outline: OutlineMode,
font_ids: Vec<FontId>,
}

Expand Down Expand Up @@ -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(),
}
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -710,6 +750,7 @@ pub struct TextTool {
sender: Option<Sender<SketchBoardInput>>,
drag_start_pos: Vec2D,
dragged: Rc<RefCell<bool>>,
alt_tap: bool,
}

impl Tool for TextTool {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 => {
Expand Down
Loading