#880: canvas overlays take the green that stands out on the page; white text stays on the darker green

This commit is contained in:
bflatastic
2026-10-20 00:45:56 +07:00
parent 9ea757736f
commit 088693cfd3
5 changed files with 124 additions and 56 deletions
+55 -41
View File
@@ -12,7 +12,7 @@ use serde_json::json;
use crate::FigApp;
use crate::i18n::{tr, trf, trn};
use crate::render_worker::{Done, Job, Worker};
use crate::theme::{ACCENT, COMPONENT, Tokens, doc_c32, regular, semibold};
use crate::theme::{COMPONENT, Tokens, doc_c32, regular, semibold};
pub struct CanvasState {
pub rect: Rect,
@@ -233,6 +233,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
})
.unwrap_or(t.canvas);
painter.rect_filled(rect, CornerRadius::ZERO, bg);
crate::theme::set_canvas_page(&ctx, bg);
if let (Some(tex), Some(v), true) = (&app.canvas.texture, &app.canvas.tex_view, shown.is_some() && app.canvas.tex_of == shown) {
// Re-project the last render onto the current view.
let tl = app.to_screen(v.origin.0, v.origin.1);
@@ -247,8 +248,8 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
if let (Some(start), Some(p)) = (app.canvas.zoom_drag, ctx.input(|i| i.pointer.hover_pos())) {
let area = Rect::from_two_pos(start, p);
if area.width() > 4.0 || area.height() > 4.0 {
painter.rect_filled(area, CornerRadius::ZERO, ACCENT.gamma_multiply(0.1));
painter.rect_stroke(area, CornerRadius::ZERO, Stroke::new(1.0, ACCENT), StrokeKind::Inside);
painter.rect_filled(area, CornerRadius::ZERO, accent(&painter).gamma_multiply(0.1));
painter.rect_stroke(area, CornerRadius::ZERO, Stroke::new(1.0, accent(&painter)), StrokeKind::Inside);
}
}
crate::dev_mode::draw(app, &painter);
@@ -809,11 +810,11 @@ fn swap_highlight(app: &FigApp, painter: &egui::Painter, doc: &Document, id: Nod
}
/// The outline colour for a layer: purple for components, sets and instances and every layer in
/// them, blue otherwise.
fn outline_colour(doc: &Document, id: NodeId) -> Color32 {
/// them, the page's overlay `green` otherwise.
fn outline_colour(doc: &Document, id: NodeId, green: Color32) -> Color32 {
use figcraft_engine::components::{owning_component, owning_instance};
let set = matches!(doc.node(id).map(|n| &n.kind), Some(NodeKind::ComponentSet { .. }));
if set || owning_component(doc, id).is_some() || owning_instance(doc, id).is_some() { COMPONENT } else { ACCENT }
if set || owning_component(doc, id).is_some() || owning_instance(doc, id).is_some() { COMPONENT } else { green }
}
/// The outline path of shapes (ellipses, vectors) for hover/selection outlines.
@@ -946,20 +947,20 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
&& app.session.prefs.highlight_on_hover
&& app.session.tool.overlay.measure_box.is_none()
{
draw_outline(app, painter, &doc, h, stroke(outline_colour(&doc, h), 1.5));
draw_outline(app, painter, &doc, h, stroke(outline_colour(&doc, h, accent(painter)), 1.5));
}
// Inside a boolean group (an operand selected or in vector edit mode): every operand's outline,
// so the shapes making the live result stay visible while one of them changes.
let operands = figcraft_engine::booleans::outlined_operands(&doc, &sel, ds.vector_edit.as_ref().map(|ve| ve.id));
for id in operands.iter().filter(|o| !sel.contains(o)) {
draw_outline(app, painter, &doc, *id, stroke(outline_colour(&doc, *id).gamma_multiply(0.6), 1.0));
draw_outline(app, painter, &doc, *id, stroke(outline_colour(&doc, *id, accent(painter)).gamma_multiply(0.6), 1.0));
}
// Mask outlines (a mask doesn't draw itself), frame outlines and slices, as the View menu shows.
if view.mask_outlines || view.frame_outlines || view.show_slices {
for id in doc.subtree(page) {
let Some(n) = doc.node(id).filter(|n| n.visible) else { continue };
if view.mask_outlines && n.is_mask {
draw_outline(app, painter, &doc, id, stroke(ACCENT, 1.0));
draw_outline(app, painter, &doc, id, stroke(accent(painter), 1.0));
} else if view.frame_outlines && n.kind.is_frame_like() {
draw_outline(app, painter, &doc, id, stroke(t.text_secondary, 1.0));
} else if view.show_slices
@@ -975,20 +976,20 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
if let Some(d) = ov.drop_target
&& let Some(q) = node_quad(app, &doc, d)
{
quad(painter, q, stroke(if figcraft_engine::components::is_slot(&doc, d) { COMPONENT } else { ACCENT }, 1.0));
quad(painter, q, stroke(if figcraft_engine::components::is_slot(&doc, d) { COMPONENT } else { accent(painter) }, 1.0));
}
slot_badges(app, painter, &doc, &sel, ov.drop_target);
if let Some(t) = ov.swap_target {
swap_highlight(app, painter, &doc, t);
}
if let Some([x0, y0, x1, y1]) = ov.insert_line {
painter.line_segment([app.to_screen(x0, y0), app.to_screen(x1, y1)], stroke(ACCENT, 2.0));
painter.line_segment([app.to_screen(x0, y0), app.to_screen(x1, y1)], stroke(accent(painter), 2.0));
}
if let Some([x0, y0, x1, y1]) = ov.drop_cell {
let r = Rect::from_two_pos(app.to_screen(x0, y0), app.to_screen(x1, y1));
painter.rect_filled(r, CornerRadius::same(2), ACCENT.gamma_multiply(0.12));
painter.rect_filled(r, CornerRadius::same(2), accent(painter).gamma_multiply(0.12));
let pts = [r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom(), r.left_top()];
painter.add(Shape::dashed_line(&pts, stroke(ACCENT, 1.5), 4.0, 3.0));
painter.add(Shape::dashed_line(&pts, stroke(accent(painter), 1.5), 4.0, 3.0));
}
// Vector edit mode, with the transform box around several selected points.
// The selection's size label, which measurement labels keep clear of.
@@ -1011,13 +1012,13 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
width_overlay(app, painter, &doc, &ve, zoom);
} else if let Some(c) = app.session.vector_edit_box() {
let label = format!("{} × {}", fmt((c[1] - c[0]).hypot()), fmt((c[3] - c[0]).hypot()));
size_label = transform_box(app, painter, c.map(|p| app.to_screen(p.x, p.y)), ACCENT, true, 4, label);
size_label = transform_box(app, painter, c.map(|p| app.to_screen(p.x, p.y)), accent(painter), true, 4, label);
}
}
// Selection.
let editing = ds.text_edit.clone();
if !sel.is_empty() && editing.is_none() && ds.vector_edit.is_none() {
let col = if sel.iter().any(|i| outline_colour(&doc, *i) == COMPONENT) { COMPONENT } else { ACCENT };
let col = if sel.iter().any(|i| outline_colour(&doc, *i, accent(painter)) == COMPONENT) { COMPONENT } else { accent(painter) };
for id in &sel {
draw_outline(app, painter, &doc, *id, stroke(col, 1.0));
}
@@ -1086,12 +1087,12 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
{
let layout = &text.layout;
if let Some(q) = node_quad(app, edoc, te.id) {
quad(painter, q, stroke(ACCENT, 1.0));
quad(painter, q, stroke(accent(painter), 1.0));
}
let (a, b) = (te.caret.min(te.anchor), te.caret.max(te.anchor));
if a != b && preedit.is_none() {
for q in text.selection(app, a, b) {
painter.add(Shape::convex_polygon(q.to_vec(), ACCENT.gamma_multiply(0.3), Stroke::NONE));
painter.add(Shape::convex_polygon(q.to_vec(), accent(painter).gamma_multiply(0.3), Stroke::NONE));
}
}
if let Some(p) = preedit {
@@ -1122,8 +1123,8 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
// Marquee.
if let Some([x0, y0, x1, y1]) = ov.marquee.or(app.ui.key_box) {
let r = Rect::from_two_pos(app.to_screen(x0, y0), app.to_screen(x1, y1));
painter.rect_filled(r, CornerRadius::ZERO, ACCENT.gamma_multiply(0.1));
painter.rect_stroke(r, CornerRadius::ZERO, stroke(ACCENT, 1.0), StrokeKind::Inside);
painter.rect_filled(r, CornerRadius::ZERO, accent(painter).gamma_multiply(0.1));
painter.rect_stroke(r, CornerRadius::ZERO, stroke(accent(painter), 1.0), StrokeKind::Inside);
}
// Pen path.
if !ov.pen.is_empty() {
@@ -1138,7 +1139,7 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
}
for p in &pts {
painter.circle_filled(*p, 3.5, Color32::WHITE);
painter.circle_stroke(*p, 3.5, stroke(ACCENT, 1.0));
painter.circle_stroke(*p, 3.5, stroke(accent(painter), 1.0));
}
if let Some([x0, y0, x1, y1]) = ov.rubber {
let (a, b) = (app.to_screen(x0, y0), app.to_screen(x1, y1));
@@ -1150,11 +1151,11 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
[a, h, b, b],
false,
Color32::TRANSPARENT,
stroke(ACCENT, 1.0),
stroke(accent(painter), 1.0),
));
}
None => {
painter.line_segment([a, b], stroke(ACCENT, 1.0));
painter.line_segment([a, b], stroke(accent(painter), 1.0));
}
}
}
@@ -1165,7 +1166,7 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
let c = app.to_screen(x, y);
let rs = (r * app.session.ds().map(|d| d.view.zoom).unwrap_or(1.0)) as f32;
painter.circle_stroke(c, rs, stroke(Color32::WHITE, 3.0));
painter.circle_stroke(c, rs, stroke(ACCENT, 1.5));
painter.circle_stroke(c, rs, stroke(accent(painter), 1.5));
}
// Lasso (vector edit mode).
if ov.lasso.len() > 1 {
@@ -1173,7 +1174,7 @@ fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec<(NodeId, [
if let Some(first) = pts.first().copied() {
pts.push(first);
}
painter.add(Shape::dashed_line(&pts, stroke(ACCENT, 1.0), 4.0, 3.0));
painter.add(Shape::dashed_line(&pts, stroke(accent(painter), 1.0), 4.0, 3.0));
}
// Guides (under the rulers).
crate::guides::draw(app, painter);
@@ -1207,7 +1208,7 @@ fn transform_box(app: &FigApp, painter: &egui::Painter, q: [Pos2; 4], col: Color
fn pill(painter: &egui::Painter, at: Pos2, align: Align2, text: String, col: Color32) -> Rect {
let galley = painter.layout_no_wrap(text, semibold(11.0), Color32::WHITE);
let r = align.anchor_size(at, galley.size() + vec2(8.0, 4.0));
painter.rect_filled(r, CornerRadius::same(3), col);
painter.rect_filled(r, CornerRadius::same(3), crate::theme::under_white(col));
painter.galley(r.min + vec2(4.0, 2.0), galley, Color32::WHITE);
r
}
@@ -1264,7 +1265,7 @@ fn width_overlay(app: &FigApp, painter: &egui::Painter, doc: &Document, ve: &fig
let sp = |p: &[f64; 2]| app.to_screen(p[0], p[1]);
let draw = |h: &figcraft_engine::vectoredit::WidthHandle, ghost: bool| {
let (c, a, b) = (sp(&h.at), sp(&h.sides[0]), sp(&h.sides[1]));
let col = if ghost { ACCENT.gamma_multiply(0.5) } else { ACCENT };
let col = if ghost { accent(painter).gamma_multiply(0.5) } else { accent(painter) };
painter.line_segment([a, b], stroke(Color32::from_white_alpha(160), 3.0));
painter.line_segment([a, b], stroke(col, 1.0));
for (e, side) in [(a, WidthSide::Right), (b, WidthSide::Left)] {
@@ -1276,7 +1277,7 @@ fn width_overlay(app: &FigApp, painter: &egui::Painter, doc: &Document, ve: &fig
let on = !ghost && ve.width_point == Some(h.index);
let hovered = !ghost && hover == Some(WHit::Point { index: h.index });
let r = if hovered { 5.0 } else { 4.0 };
painter.circle_filled(c, r, if on { ACCENT } else { Color32::WHITE });
painter.circle_filled(c, r, if on { accent(painter) } else { Color32::WHITE });
painter.circle_stroke(c, r, stroke(col, 1.0));
};
for h in width_handles(doc, ve, zoom) {
@@ -1292,9 +1293,9 @@ fn width_overlay(app: &FigApp, painter: &egui::Painter, doc: &Document, ve: &fig
/// A bezier handle from its anchor `v` to its end `e` (screen points): the line, and a dot of
/// radius `r` filled with `fill` (vector edit mode and the Pen draw them alike).
fn bezier_handle(painter: &egui::Painter, v: Pos2, e: Pos2, r: f32, fill: Color32) {
painter.line_segment([v, e], stroke(ACCENT, 1.0));
painter.line_segment([v, e], stroke(accent(painter), 1.0));
painter.circle_filled(e, r, fill);
painter.circle_stroke(e, r, stroke(ACCENT, 1.0));
painter.circle_stroke(e, r, stroke(accent(painter), 1.0));
}
/// Vector edit mode: a layer's segments, handles and points (`points`) with its `hover`ed part, and
@@ -1313,7 +1314,7 @@ fn vector_overlay(
// Paint bucket target: hatched like a fill to come.
if active && !ov.face.is_empty() {
let polys: Vec<Vec<Pos2>> = ov.face.iter().map(|poly| poly.iter().map(sp).collect()).collect();
hatched(painter, &polys, ACCENT);
hatched(painter, &polys, accent(painter));
}
if active {
region_hatch(app, painter);
@@ -1331,7 +1332,7 @@ fn vector_overlay(
} else {
1.5
};
painter.add(egui::epaint::CubicBezierShape::from_points_stroke([a, b, c, d], false, Color32::TRANSPARENT, stroke(ACCENT, w)));
painter.add(egui::epaint::CubicBezierShape::from_points_stroke([a, b, c, d], false, Color32::TRANSPARENT, stroke(accent(painter), w)));
}
if !points {
return;
@@ -1339,7 +1340,7 @@ fn vector_overlay(
for h in &o.handles {
let hovered = hover == Some(VHit::Handle { seg: h.seg, start: h.start });
let on = o.selected_handles.contains(&(h.seg, h.start));
bezier_handle(painter, sp(&h.vertex), sp(&h.handle), if hovered { 4.5 } else { 3.5 }, if on { ACCENT } else { Color32::WHITE });
bezier_handle(painter, sp(&h.vertex), sp(&h.handle), if hovered { 4.5 } else { 3.5 }, if on { accent(painter) } else { Color32::WHITE });
}
for (i, v) in o.vertices.iter().enumerate() {
let p = sp(v);
@@ -1347,14 +1348,14 @@ fn vector_overlay(
let size = if hovered { 9.0 } else { 7.0 };
let r = Rect::from_center_size(p, vec2(size, size));
let on = o.selected_vertices.contains(&i);
painter.rect_filled(r, CornerRadius::same(5), if on { ACCENT } else { Color32::WHITE });
painter.rect_stroke(r, CornerRadius::same(5), stroke(ACCENT, 1.0), StrokeKind::Inside);
painter.rect_filled(r, CornerRadius::same(5), if on { accent(painter) } else { Color32::WHITE });
painter.rect_stroke(r, CornerRadius::same(5), stroke(accent(painter), 1.0), StrokeKind::Inside);
if o.anchor == Some(i) {
painter.circle_stroke(p, 7.5, stroke(ACCENT, 1.0));
painter.circle_stroke(p, 7.5, stroke(accent(painter), 1.0));
}
}
if let Some([x0, y0, x1, y1]) = ov.rubber.filter(|_| active) {
painter.add(Shape::dashed_line(&[app.to_screen(x0, y0), app.to_screen(x1, y1)], stroke(ACCENT, 1.0), 4.0, 3.0));
painter.add(Shape::dashed_line(&[app.to_screen(x0, y0), app.to_screen(x1, y1)], stroke(accent(painter), 1.0), 4.0, 3.0));
}
}
@@ -1371,7 +1372,7 @@ fn region_hatch(app: &FigApp, painter: &egui::Painter) {
let r = painter.clip_rect();
let clip = figcraft_geom::Rect::new(f64::from(r.min.x), f64::from(r.min.y), f64::from(r.max.x), f64::from(r.max.y));
for [a, b] in app.session.vector_region_hatch(view, Some(clip)) {
painter.line_segment([pos2(a.x as f32, a.y as f32), pos2(b.x as f32, b.y as f32)], stroke(ACCENT, REGION_HATCH_WIDTH));
painter.line_segment([pos2(a.x as f32, a.y as f32), pos2(b.x as f32, b.y as f32)], stroke(accent(painter), REGION_HATCH_WIDTH));
}
}
@@ -1386,10 +1387,10 @@ fn builder_overlay(app: &FigApp, painter: &egui::Painter, t: &Tokens, arr: &figc
for r in &arr.regions {
for poly in screen(r) {
painter.add(Shape::closed_line(poly.clone(), stroke(Color32::from_white_alpha(150), 2.5)));
painter.add(Shape::closed_line(poly, stroke(ACCENT, 1.0)));
painter.add(Shape::closed_line(poly, stroke(accent(painter), 1.0)));
}
}
let col = if ov.builder_remove { t.danger } else { ACCENT };
let col = if ov.builder_remove { t.danger } else { accent(painter) };
for r in ov.builder_regions.iter().filter_map(|i| arr.regions.get(*i)) {
hatched(painter, &screen(r), col);
}
@@ -1499,8 +1500,16 @@ fn rulers(app: &FigApp, painter: &egui::Painter, t: &Tokens, doc: &Document, sel
// Highlight the selection's span.
if let Some(b) = doc.bounds_of(sel) {
let (a, bb) = (app.to_screen(b.x0, b.y0), app.to_screen(b.x1, b.y1));
painter.rect_filled(Rect::from_min_max(pos2(a.x, r.min.y), pos2(bb.x, r.min.y + size)), CornerRadius::ZERO, ACCENT.gamma_multiply(0.15));
painter.rect_filled(Rect::from_min_max(pos2(r.min.x, a.y), pos2(r.min.x + size, bb.y)), CornerRadius::ZERO, ACCENT.gamma_multiply(0.15));
painter.rect_filled(
Rect::from_min_max(pos2(a.x, r.min.y), pos2(bb.x, r.min.y + size)),
CornerRadius::ZERO,
accent(painter).gamma_multiply(0.15),
);
painter.rect_filled(
Rect::from_min_max(pos2(r.min.x, a.y), pos2(r.min.x + size, bb.y)),
CornerRadius::ZERO,
accent(painter).gamma_multiply(0.15),
);
}
let mut x = (p0.x / step).floor() * step;
while x <= p1.x {
@@ -1528,3 +1537,8 @@ pub fn fmt(v: f64) -> String {
#[cfg(test)]
#[path = "zoom_preview_tests.rs"]
mod zoom_preview_tests;
/// The overlay green for the page this painter draws on ([`crate::theme::canvas_accent`]).
fn accent(p: &egui::Painter) -> Color32 {
crate::theme::canvas_accent(p.ctx())
}
+1 -1
View File
@@ -103,7 +103,7 @@ pub fn draw(app: &FigApp, painter: &Painter, t: &Tokens, doc: &Document, id: Nod
let col = if is_comp {
COMPONENT
} else if selected || hovered {
ACCENT
crate::theme::canvas_accent(painter.ctx())
} else {
t.text_secondary
};
+12 -7
View File
@@ -11,7 +11,7 @@ use figcraft_geom::Point;
use serde_json::json;
use crate::colorpicker::PickerTarget;
use crate::theme::{ACCENT, semibold};
use crate::theme::semibold;
use crate::{FigApp, RightTab};
pub(crate) const PINK: Color32 = Color32::from_rgb(0xF2, 0x48, 0x8C);
@@ -293,7 +293,12 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
painter.rect_filled(r, CornerRadius::same(2), crate::theme::doc_c32(painter.ctx(), s.color.with_alpha(1.0)));
painter.rect_stroke(r, CornerRadius::same(2), Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside);
if i == selected {
painter.rect_stroke(r.expand(2.0), CornerRadius::same(3), Stroke::new(1.5, ACCENT), StrokeKind::Outside);
painter.rect_stroke(
r.expand(2.0),
CornerRadius::same(3),
Stroke::new(1.5, crate::theme::canvas_accent(painter.ctx())),
StrokeKind::Outside,
);
}
}
for (i, p) in [a, b, c].iter().enumerate() {
@@ -301,7 +306,7 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
continue;
}
painter.circle_filled(*p, 5.0, Color32::WHITE);
painter.circle_stroke(*p, 5.0, Stroke::new(1.5, ACCENT));
painter.circle_stroke(*p, 5.0, Stroke::new(1.5, crate::theme::canvas_accent(painter.ctx())));
}
return;
}
@@ -311,8 +316,8 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
painter.line_segment([a, b], Stroke::new(1.0, Color32::from_black_alpha(120)));
// The start is hollow (least blur), the end filled (most blur by default).
painter.circle_filled(a, 5.0, Color32::WHITE);
painter.circle_stroke(a, 5.0, Stroke::new(1.5, ACCENT));
painter.circle_filled(b, 5.0, ACCENT);
painter.circle_stroke(a, 5.0, Stroke::new(1.5, crate::theme::canvas_accent(painter.ctx())));
painter.circle_filled(b, 5.0, crate::theme::canvas_accent(painter.ctx()));
painter.circle_stroke(b, 5.0, Stroke::new(1.5, Color32::WHITE));
return;
}
@@ -320,11 +325,11 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
let Some(c) = crop_corners(app, id, r) else { return };
let outline = vec![c[0], c[1], c[2], c[3], c[0]];
painter.add(Shape::line(outline.clone(), Stroke::new(2.0, Color32::WHITE)));
painter.add(Shape::line(outline, Stroke::new(1.0, ACCENT)));
painter.add(Shape::line(outline, Stroke::new(1.0, crate::theme::canvas_accent(painter.ctx()))));
for p in c {
let hr = Rect::from_center_size(p, vec2(8.0, 8.0));
painter.rect_filled(hr, CornerRadius::ZERO, Color32::WHITE);
painter.rect_stroke(hr, CornerRadius::ZERO, Stroke::new(1.0, ACCENT), StrokeKind::Inside);
painter.rect_stroke(hr, CornerRadius::ZERO, Stroke::new(1.0, crate::theme::canvas_accent(painter.ctx())), StrokeKind::Inside);
}
return;
}
+20 -7
View File
@@ -8,7 +8,6 @@ use egui::{Color32, CornerRadius, Id, Pos2, Rect, Shape, Stroke, StrokeKind, pos
use figcraft_doc::{Document, NodeId};
use serde_json::{Value, json};
use crate::theme::ACCENT;
use crate::{FigApp, RightTab};
const HANDLE_R: f32 = 7.0;
@@ -140,7 +139,7 @@ fn arrow(painter: &egui::Painter, pts: &[Pos2; 4], col: Color32) {
}
fn plus_handle(painter: &egui::Painter, c: Pos2, hot: bool) {
painter.circle_filled(c, HANDLE_R + if hot { 1.0 } else { 0.0 }, ACCENT);
painter.circle_filled(c, HANDLE_R + if hot { 1.0 } else { 0.0 }, crate::theme::canvas_accent(painter.ctx()));
painter.circle_stroke(c, HANDLE_R + if hot { 1.0 } else { 0.0 }, Stroke::new(1.5, Color32::WHITE));
painter.line_segment([c - vec2(3.5, 0.0), c + vec2(3.5, 0.0)], Stroke::new(1.5, Color32::WHITE));
painter.line_segment([c - vec2(0.0, 3.5), c + vec2(0.0, 3.5)], Stroke::new(1.5, Color32::WHITE));
@@ -161,7 +160,11 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
let selected = sel.contains(&nd.source);
let is_open = open == Some((nd.source, nd.index));
let near = dragging.is_none() && hover.is_some_and(|q| bezier_distance(&nd.pts, q) < 5.0);
let col = if selected || is_open || near { ACCENT } else { ACCENT.gamma_multiply(0.55) };
let col = if selected || is_open || near {
crate::theme::canvas_accent(painter.ctx())
} else {
crate::theme::canvas_accent(painter.ctx()).gamma_multiply(0.55)
};
let w = if is_open || near {
2.5
} else if selected {
@@ -177,7 +180,12 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
.and_then(|it| figcraft_doc::proto::walk(&it.actions).find_map(|a| a.dest()))
&& let Some(r) = screen_rect(app, doc, dest)
{
painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.0, ACCENT.gamma_multiply(0.7)), StrokeKind::Outside);
painter.rect_stroke(
r,
CornerRadius::ZERO,
Stroke::new(1.0, crate::theme::canvas_accent(painter.ctx()).gamma_multiply(0.7)),
StrokeKind::Outside,
);
}
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(nd.pts, false, Color32::TRANSPARENT, Stroke::new(w, col)));
painter.circle_filled(nd.pts[0], 3.5, col);
@@ -190,12 +198,17 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
&& doc.top_level_frame(d.source) != Some(f)
&& let Some(r) = screen_rect(app, doc, f)
{
painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(2.0, ACCENT), StrokeKind::Outside);
painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(2.0, crate::theme::canvas_accent(painter.ctx())), StrokeKind::Outside);
}
let dx = ((d.pos.x - d.from.x).abs() * 0.5).max(30.0) * if d.pos.x >= d.from.x { 1.0 } else { -1.0 };
let pts = [d.from, d.from + vec2(dx, 0.0), d.pos - vec2(dx, 0.0), d.pos];
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(pts, false, Color32::TRANSPARENT, Stroke::new(2.0, ACCENT)));
arrow(painter, &pts, ACCENT);
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(
pts,
false,
Color32::TRANSPARENT,
Stroke::new(2.0, crate::theme::canvas_accent(painter.ctx())),
));
arrow(painter, &pts, crate::theme::canvas_accent(painter.ctx()));
plus_handle(painter, d.from, true);
}
None => {
+36
View File
@@ -52,6 +52,27 @@ pub const BRAND: Color32 = Color32::from_rgb(0x02, 0xDA, 0x96);
pub const ACCENT: Color32 = Color32::from_rgb(0x00, 0x87, 0x5F);
/// Ink on the bright green brand surface (white would not have enough contrast).
pub const ACCENT_ON: Color32 = Color32::from_rgb(0x07, 0x35, 0x2A);
/// Id of the canvas overlays' green for this frame ([`set_canvas_page`]).
const CANVAS_ACCENT: &str = "figcraft-canvas-accent";
/// Remember the page colour the canvas shows. Its overlays (outlines, handles, arrows) take
/// whichever green contrasts more with it: the bright one on a dark page, the darker on a light one.
pub fn set_canvas_page(ctx: &Context, page: Color32) {
let c = |x: Color32| figcraft_color::Color::new(f32::from(x.r()) / 255.0, f32::from(x.g()) / 255.0, f32::from(x.b()) / 255.0, 1.0);
let pick = if c(BRAND).contrast(c(page)) > c(ACCENT).contrast(c(page)) { BRAND } else { ACCENT };
ctx.data_mut(|d| d.insert_temp(egui::Id::new(CANVAS_ACCENT), pick));
}
/// The green for overlays drawn on the page ([`set_canvas_page`]); [`ACCENT`] before a page shows.
pub fn canvas_accent(ctx: &Context) -> Color32 {
ctx.data(|d| d.get_temp(egui::Id::new(CANVAS_ACCENT))).unwrap_or(ACCENT)
}
/// A fill under white text: the darker green stands in for the bright one.
pub fn under_white(c: Color32) -> Color32 {
if c == BRAND { ACCENT } else { c }
}
pub const COMPONENT: Color32 = Color32::from_rgb(0x97, 0x47, 0xFF);
/// Handoff status badges (white text on these, in both themes).
pub const DEV_READY: Color32 = Color32::from_rgb(0x08, 0x7D, 0x4F);
@@ -426,6 +447,21 @@ pub fn doc_c32(ctx: &Context, c: figcraft_color::Color) -> Color32 {
mod tests {
use super::*;
#[test]
fn canvas_overlays_take_the_green_that_stands_out_on_the_page() {
let ctx = Context::default();
assert_eq!(canvas_accent(&ctx), ACCENT, "before any page shows");
set_canvas_page(&ctx, Color32::from_rgb(0x1E, 0x1E, 0x1E));
assert_eq!(canvas_accent(&ctx), BRAND, "the bright green on a dark page");
set_canvas_page(&ctx, Color32::WHITE);
assert_eq!(canvas_accent(&ctx), ACCENT, "the darker green on a light page");
set_canvas_page(&ctx, Color32::from_rgb(0xF5, 0xF5, 0xF5));
assert_eq!(canvas_accent(&ctx), ACCENT);
// White text never sits on the bright green.
assert_eq!(under_white(BRAND), ACCENT);
assert_eq!(under_white(COMPONENT), COMPONENT);
}
#[test]
fn ui_font_draws_every_mac_shortcut_glyph() {
figcraft_text::scan_system_fonts();