mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
fix(engine): new text is readable on the frame it is drawn in (#362)
The colour of new text in the app was chosen from the page alone, so on a dark page text drawn in a white frame (a new frame's default) was white and invisible. readable_text_fills now looks at what the text is drawn on: the nearest layer from its parent out whose top visible fill is a mostly opaque solid colour, else the page. The Text tool and text.create / node.create pass the text's parent. The two tests that asserted the page-only rule in a frame now expect black text in a white frame; new cases cover dark, unfilled, nested, faint and stacked fills, and the Text tool.
This commit is contained in:
@@ -242,7 +242,7 @@ pub fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let doc = ds.doc_mut();
|
||||
let nid = doc.alloc_id();
|
||||
let mut node = if base == "Section" { Node::new_section(nid, name, w, h) } else { Node::new(nid, kind, name, w, h) };
|
||||
if app_text && let Some(fills) = crate::tools::readable_text_fills(doc, doc.page_of(parent).unwrap_or(page)) {
|
||||
if app_text && let Some(fills) = crate::tools::readable_text_fills(doc, parent) {
|
||||
node.fills = fills;
|
||||
}
|
||||
node.transform = space.inverse() * figcraft_geom::Affine::translate((x + offset.0, y + offset.1));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Text made by commands in the app (the interface reported its theme) is readable on its page
|
||||
//! like text drawn with the Text tool: white on a dark page unless a fill is named. A session
|
||||
//! without an interface (CLI, MCP, exports) keeps the model's black everywhere.
|
||||
//! Text made by commands in the app (the interface reported its theme) is readable where it is
|
||||
//! drawn, like text drawn with the Text tool: white on a dark page or a dark frame unless a fill is
|
||||
//! named. A session without an interface (CLI, MCP, exports) keeps the model's black everywhere.
|
||||
|
||||
use figcraft_color::{Color, Paint};
|
||||
use figcraft_doc::NodeId;
|
||||
@@ -62,12 +62,51 @@ fn text_made_by_commands_is_readable_on_its_page() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_in_a_frame_follows_the_page_it_is_on() {
|
||||
// As with the Text tool: the page decides, also inside a frame; made in one undo step.
|
||||
fn text_in_a_frame_is_readable_on_the_frame() {
|
||||
// #362: what the text is drawn on decides: the nearest layer around it with a solid fill (a
|
||||
// new frame's white too), else its page; made in one undo step.
|
||||
let (white, black) = (vec![Paint::solid(Color::WHITE)], vec![Paint::solid(Color::BLACK)]);
|
||||
let mut s = session(Some(true));
|
||||
let frame: NodeId = id_of(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100}))["id"]).unwrap();
|
||||
let t = ex(&mut s, "text.create", json!({"text": "Hi", "x": 10, "y": 10, "parent": frame}));
|
||||
assert_eq!(fills(&s, &t), vec![Paint::solid(Color::WHITE)]);
|
||||
let frame = |s: &mut Session, x: i64, parent: Option<NodeId>, fills: Option<Value>| -> NodeId {
|
||||
let mut p = json!({"x": x, "y": 0, "width": 200, "height": 100});
|
||||
if let Some(parent) = parent {
|
||||
p["parent"] = json!(parent);
|
||||
}
|
||||
let f = id_of(&ex(s, "frame.create", p)["id"]).unwrap();
|
||||
if let Some(fills) = fills {
|
||||
ex(s, "node.set", json!({"id": f, "props": {"fills": fills}}));
|
||||
}
|
||||
f
|
||||
};
|
||||
let light = frame(&mut s, 0, None, None);
|
||||
let dark = frame(&mut s, 300, None, Some(json!([{"type": "solid", "color": "#202020"}])));
|
||||
let bare = frame(&mut s, 600, None, Some(json!([])));
|
||||
let inner = frame(&mut s, 0, Some(light), Some(json!([])));
|
||||
let faint = frame(&mut s, 900, None, Some(json!([{"type": "solid", "color": "#FFFFFF", "opacity": 0.2}])));
|
||||
let stacked = frame(&mut s, 1200, None, Some(json!([{"type": "solid", "color": "#202020"}, {"type": "solid", "color": "#FFFFFF"}])));
|
||||
let cases = [
|
||||
(light, &black, "a white frame on a dark page: black text"),
|
||||
(dark, &white, "a dark frame: white"),
|
||||
(bare, &white, "a frame without a fill: the page decides"),
|
||||
(inner, &black, "a frame without a fill in a white one: the white one"),
|
||||
(faint, &white, "a faint fill lets the page show through: the page decides"),
|
||||
(stacked, &black, "the fill on top counts"),
|
||||
];
|
||||
for (parent, want, why) in cases {
|
||||
let t = ex(&mut s, "text.create", json!({"text": "Hi", "x": 10, "y": 10, "parent": parent}));
|
||||
assert_eq!(&fills(&s, &t), want, "{why}");
|
||||
}
|
||||
// The Text tool reads it the same way.
|
||||
use crate::tools::{PointerEvent, PointerKind, ToolId};
|
||||
let at = s.doc().unwrap().world_bounds(light).unwrap().center();
|
||||
s.set_tool(ToolId::Text);
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::at(kind, at.x, at.y, 1.0)).unwrap();
|
||||
}
|
||||
let typed = s.ds().unwrap().text_edit.as_ref().unwrap().id;
|
||||
assert_eq!(s.doc().unwrap().node(typed).unwrap().fills, black, "the Text tool in a white frame");
|
||||
crate::textedit::end(&mut s).unwrap();
|
||||
let t = ex(&mut s, "text.create", json!({"text": "Hi", "x": 10, "y": 10, "parent": light}));
|
||||
ex(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().node(id_of(&t["id"]).unwrap()).is_none());
|
||||
}
|
||||
|
||||
@@ -118,13 +118,14 @@ fn text_drawn_on_a_dark_page_is_white_and_on_a_light_page_black() {
|
||||
let t = draw_text(&mut s, 40.0, 40.0);
|
||||
assert_eq!(s.doc().unwrap().node(t).unwrap().fills, vec![Paint::solid(want)], "text drawn on {page}");
|
||||
}
|
||||
// In a frame on a dark page too; so is text a command makes in the app (`dark_page_text_tests`).
|
||||
// In a (white) frame on a dark page, readable on the frame (#362); so is text a command makes
|
||||
// in the app (`dark_page_text_tests`).
|
||||
let mut s = Session::empty();
|
||||
s.set_ui_dark(true);
|
||||
ex(&mut s, "file.new", json!({}));
|
||||
ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 200}));
|
||||
let t = draw_text(&mut s, 50.0, 50.0);
|
||||
assert_eq!(s.doc().unwrap().node(t).unwrap().fills, vec![Paint::solid(Color::WHITE)]);
|
||||
assert_eq!(s.doc().unwrap().node(t).unwrap().fills, vec![Paint::solid(Color::BLACK)]);
|
||||
let made = id_of(&ex(&mut s, "text.create", json!({"text": "Hi", "x": 0, "y": 300}))["id"]).unwrap();
|
||||
assert_eq!(s.doc().unwrap().node(made).unwrap().fills, vec![Paint::solid(Color::WHITE)]);
|
||||
}
|
||||
|
||||
@@ -530,12 +530,16 @@ pub fn anchor_name(a: (u8, u8)) -> &'static str {
|
||||
ANCHORS.get(usize::from(a.1.min(2)) * 3 + usize::from(a.0.min(2))).copied().unwrap_or("tl")
|
||||
}
|
||||
|
||||
/// The fill new text takes to be readable where it is drawn: white on a dark `page` (None: the
|
||||
/// model's default, black). For text made in the app: by the Text tool, or by a command that
|
||||
/// names no fill while the interface is up.
|
||||
pub(crate) fn readable_text_fills(doc: &Document, page: NodeId) -> Option<Vec<figcraft_color::Paint>> {
|
||||
matches!(doc.node(page).map(|p| &p.kind), Some(NodeKind::Page { background, .. }) if background.is_dark())
|
||||
.then(|| vec![figcraft_color::Paint::solid(figcraft_color::Color::WHITE)])
|
||||
/// The fill new text takes to be readable where it is drawn, in `parent`: white on a dark
|
||||
/// background (None: the model's default, black). The background is the nearest layer from `parent`
|
||||
/// out whose top visible fill is a mostly opaque solid colour, else the page (#362). For text made
|
||||
/// in the app: by the Text tool, or by a command that names no fill while the interface is up.
|
||||
pub(crate) fn readable_text_fills(doc: &Document, parent: NodeId) -> Option<Vec<figcraft_color::Paint>> {
|
||||
let dark = std::iter::once(parent).chain(doc.ancestors(parent)).filter_map(|id| doc.node(id)).find_map(|n| match &n.kind {
|
||||
NodeKind::Page { background, .. } => Some(background.is_dark()),
|
||||
_ => n.fills.iter().rev().find(|p| p.visible).and_then(|p| p.solid_color()).filter(|c| c.a >= 0.5).map(|c| c.is_dark()),
|
||||
});
|
||||
dark.unwrap_or(false).then(|| vec![figcraft_color::Paint::solid(figcraft_color::Color::WHITE)])
|
||||
}
|
||||
|
||||
const ANCHORS: [&str; 9] = ["tl", "t", "tr", "l", "c", "r", "bl", "b", "br"];
|
||||
@@ -1878,7 +1882,7 @@ impl Session {
|
||||
let at = Point::new(ev.x.round(), (ev.y - 7.0).round());
|
||||
n.transform = pw.inverse() * Affine::translate(at.to_vec2());
|
||||
n.name = String::new();
|
||||
if let Some(fills) = readable_text_fills(d, page) {
|
||||
if let Some(fills) = readable_text_fills(d, parent) {
|
||||
n.fills = fills;
|
||||
}
|
||||
if let Some((w, h)) = figcraft_text::auto_size(&n) {
|
||||
|
||||
Reference in New Issue
Block a user