mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 02:34:33 +00:00
M3: the text caret takes the text's fill colour (#2156)
Figma draws the text caret in the text's solid fill colour (measured in the web app, 2026-11-09). The overlay took the character before the caret, its run's fills or the layer's, and drew the topmost visible solid opaque; gradient, image and hidden fills keep the interface colour. Width, height, selection and collaborators' carets are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove and briefed; Sonnet 5.5 measured in Figma and wrote at medium
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
79c363466a
commit
2beec8b371
@@ -209,6 +209,26 @@ fn layout_grids(app: &FigApp, painter: &egui::Painter, doc: &Document, c: NodeId
|
||||
}
|
||||
}
|
||||
|
||||
/// The solid colour of the fill the next typed character gets (the character before the caret,
|
||||
/// the first one's at 0, the base style for an empty text): the style's own fills when it has
|
||||
/// them, else the layer's. `None` when the topmost visible paint isn't a solid (gradient, image,
|
||||
/// no visible fill), so the caller keeps the interface colour. Opaque on purpose: Figma's caret
|
||||
/// is one colour, and the fill's and paint's opacity weren't measured, so it must stay visible.
|
||||
fn caret_fill(n: &figcraft_doc::Node, caret: usize) -> Option<figcraft_color::Color> {
|
||||
let t = n.kind.text()?;
|
||||
let style = t.style_at(caret.saturating_sub(1));
|
||||
let paints = style.fills.as_ref().unwrap_or(&n.fills);
|
||||
paints.iter().rev().find(|p| p.visible).and_then(|p| match &p.kind {
|
||||
figcraft_color::PaintKind::Solid { color } => Some(color.with_alpha(1.0)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`caret_fill`] as a screen colour.
|
||||
fn caret_color(ctx: &egui::Context, n: &figcraft_doc::Node, caret: usize) -> Option<Color32> {
|
||||
caret_fill(n, caret).map(|c| doc_c32(ctx, c))
|
||||
}
|
||||
|
||||
fn text_edit_overlay(app: &FigApp, painter: &egui::Painter, t: &Tokens, doc: &Document, te: &figcraft_engine::TextEdit, zoom: f64) {
|
||||
let preedit = app.preedit_view();
|
||||
let edoc: &Document = preedit.map(|p| &*p.doc).unwrap_or(doc);
|
||||
@@ -242,7 +262,8 @@ fn text_edit_overlay(app: &FigApp, painter: &egui::Painter, t: &Tokens, doc: &Do
|
||||
let caret = preedit.map(|p| p.cursor).unwrap_or(te.caret);
|
||||
let blink = (painter.ctx().input(|i| i.time) * 2.0) as i64 % 2 == 0;
|
||||
if blink || a != b {
|
||||
painter.line_segment(text.caret(app, caret, zoom), stroke(t.text, 1.0));
|
||||
let col = edoc.node(te.id).and_then(|n| caret_color(painter.ctx(), n, caret)).unwrap_or(t.text);
|
||||
painter.line_segment(text.caret(app, caret, zoom), stroke(col, 1.0));
|
||||
}
|
||||
painter.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
}
|
||||
@@ -782,3 +803,66 @@ fn pick_outline(app: &FigApp, painter: &egui::Painter, doc: &Document) {
|
||||
draw_outline(app, painter, doc, p, stroke(crate::handles::PINK, 2.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod caret_tests {
|
||||
use super::*;
|
||||
use figcraft_color::{Color, Paint};
|
||||
use figcraft_doc::{Node, NodeId, NodeKind, StyleRun, TextData};
|
||||
|
||||
fn text(chars: &str, fills: Vec<Paint>) -> Node {
|
||||
let mut n = Node::new(NodeId(1), NodeKind::Text(TextData { characters: chars.into(), ..TextData::default() }), "Text", 100.0, 20.0);
|
||||
n.fills = fills;
|
||||
n
|
||||
}
|
||||
|
||||
fn hex(s: &str) -> Color {
|
||||
Color::from_hex(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn red_text_has_a_red_caret() {
|
||||
let n = text("Caret", vec![Paint::solid(hex("#FF0000"))]);
|
||||
assert_eq!(caret_fill(&n, 2), Some(hex("#FF0000")));
|
||||
assert_eq!(caret_color(&egui::Context::default(), &n, 2), Some(Color32::from_rgb(255, 0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_grey_text_keeps_its_own_colour_whatever_is_behind_it() {
|
||||
// Inside a #222222 frame: the frame's fill plays no part.
|
||||
let n = text("Caret", vec![Paint::solid(hex("#DDDDDD"))]);
|
||||
assert_eq!(caret_fill(&n, 5), Some(hex("#DDDDDD")));
|
||||
assert_eq!(caret_color(&egui::Context::default(), &n, 0), Some(Color32::from_rgb(0xDD, 0xDD, 0xDD)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_follows_the_run_of_the_character_before_it() {
|
||||
let mut n = text("ab cd", vec![Paint::solid(hex("#000000"))]);
|
||||
if let Some(t) = n.kind.text_mut() {
|
||||
let mut style = t.style.clone();
|
||||
style.fills = Some(vec![Paint::solid(hex("#FF0000"))]);
|
||||
t.runs.push(StyleRun { start: 3, end: 5, style });
|
||||
}
|
||||
assert_eq!(caret_fill(&n, 2), Some(hex("#000000")), "after a black character");
|
||||
assert_eq!(caret_fill(&n, 4), Some(hex("#FF0000")), "after a red character");
|
||||
assert_eq!(caret_fill(&n, 0), Some(hex("#000000")), "at the start: the first character's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_is_opaque_whatever_the_fill_opacity() {
|
||||
let mut p = Paint::solid(hex("#336699"));
|
||||
p.opacity = 0.2;
|
||||
assert_eq!(caret_fill(&text("x", vec![p]), 1).map(|c| c.a), Some(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_or_missing_fills_keep_the_interface_colour() {
|
||||
let mut g = Paint::solid(hex("#FF0000"));
|
||||
g.kind = figcraft_color::PaintKind::Gradient(figcraft_color::Gradient::default_for(figcraft_color::GradientKind::Linear, hex("#FF0000")));
|
||||
assert_eq!(caret_fill(&text("Caret", vec![g]), 2), None);
|
||||
assert_eq!(caret_fill(&text("Caret", vec![]), 2), None);
|
||||
let mut hidden = Paint::solid(hex("#FF0000"));
|
||||
hidden.visible = false;
|
||||
assert_eq!(caret_fill(&text("Caret", vec![hidden]), 2), None);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user