mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge pull request #880 from YarilaFox/feat/figcraft-brand
M7: refresh FigCraft brand colors and wordmark
This commit is contained in:
@@ -25,6 +25,7 @@ Release builds embed fonts from the optional craft-fonts build input
|
||||
| Family | Licence | Upstream |
|
||||
|---|---|---|
|
||||
| Inter | SIL Open Font License 1.1 | https://github.com/rsms/inter |
|
||||
| Archivo | SIL Open Font License 1.1 | [Google Fonts / Archivo](https://github.com/google/fonts/tree/6c70c829f09ea345d3590406693220ea35c6553f/ofl/archivo) |
|
||||
| Roboto, Montserrat, Lato, Open Sans, Poppins, Nunito Sans, Merriweather, Playfair Display, Source Sans 3, Source Serif 4, Carlito, Caladea | SIL Open Font License 1.1 | Google Fonts / upstream repositories (see craft-fonts) |
|
||||
| Liberation Sans, Serif, Mono | SIL Open Font License 1.1 | https://github.com/liberationfonts |
|
||||
| BIZ UDPGothic, BIZ UDMincho, Shippori Mincho, Noto Sans Arabic | SIL Open Font License 1.1 | Google Fonts (see craft-fonts) |
|
||||
|
||||
@@ -65,9 +65,8 @@ fn craft_fonts(dir: &std::path::Path, manifest_rel: &str) -> Result<String, Stri
|
||||
if cjk && std::env::var_os("FIGCRAFT_EMBED_CJK").is_none() {
|
||||
continue;
|
||||
}
|
||||
// Web builds have a size budget: Inter, plus Noto Sans CJK (every CJK script, for CJK UI
|
||||
// text) when asked.
|
||||
if wasm && *family != "Inter" && !cjk {
|
||||
// Web builds have a size budget: UI fonts, only the SemiBold wordmark, and optionally CJK.
|
||||
if wasm && *family != "Inter" && !(*family == "Archivo" && *style == "SemiBold") && !cjk {
|
||||
continue;
|
||||
}
|
||||
let path = dir.join(file).canonicalize().map_err(|e| format!("{file}: {e}"))?;
|
||||
|
||||
@@ -316,7 +316,7 @@ fn search_row(app: &mut FigApp, ui: &mut Ui, w: f32) -> String {
|
||||
);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &filter);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
filter.clear();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -805,11 +806,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.
|
||||
@@ -942,20 +943,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
|
||||
@@ -971,20 +972,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.
|
||||
@@ -1007,13 +1008,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));
|
||||
}
|
||||
@@ -1082,12 +1083,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 {
|
||||
@@ -1118,8 +1119,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() {
|
||||
@@ -1134,7 +1135,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));
|
||||
@@ -1146,11 +1147,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1161,7 +1162,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 {
|
||||
@@ -1169,7 +1170,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);
|
||||
@@ -1203,7 +1204,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
|
||||
}
|
||||
@@ -1260,7 +1261,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)] {
|
||||
@@ -1272,7 +1273,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) {
|
||||
@@ -1288,9 +1289,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
|
||||
@@ -1309,7 +1310,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);
|
||||
@@ -1327,7 +1328,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;
|
||||
@@ -1335,7 +1336,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);
|
||||
@@ -1343,14 +1344,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1367,7 +1368,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1382,10 +1383,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);
|
||||
}
|
||||
@@ -1495,8 +1496,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 {
|
||||
@@ -1524,3 +1533,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())
|
||||
}
|
||||
|
||||
@@ -1013,7 +1013,7 @@ fn saved_preset_row(ui: &mut Ui, name: &str) -> Option<PresetAction> {
|
||||
let del_resp = ui.interact(del, ui.id().with(("cp_preset_del", name)), Sense::click()).tip(tr("Delete preset"));
|
||||
let hot = resp.hovered() || del_resp.hovered();
|
||||
if hot {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), crate::theme::ACCENT);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.menu_hover);
|
||||
}
|
||||
let col = if hot { Color32::WHITE } else { t.text };
|
||||
ui.painter().text(pos2(r.min.x + 8.0, r.center().y), Align2::LEFT_CENTER, name, regular(11.5), col);
|
||||
|
||||
@@ -189,10 +189,10 @@ pub fn cluster(points: &[Pos2], r: f32) -> Vec<Vec<usize>> {
|
||||
}
|
||||
|
||||
/// A message's text with its @mentions of `people` picked out, wrapped at `width`.
|
||||
pub fn rich_text(text: &str, people: &[&str], color: Color32, width: f32) -> LayoutJob {
|
||||
pub fn rich_text(text: &str, people: &[&str], color: Color32, mention_color: Color32, width: f32) -> LayoutJob {
|
||||
let mut job = LayoutJob::default();
|
||||
let plain = TextFormat { font_id: regular(12.0), color, ..Default::default() };
|
||||
let mention = TextFormat { font_id: semibold(12.0), color: ACCENT, ..Default::default() };
|
||||
let mention = TextFormat { font_id: semibold(12.0), color: mention_color, ..Default::default() };
|
||||
let mut at = 0;
|
||||
for r in mention_ranges(text, people) {
|
||||
job.append(text.get(at..r.start).unwrap_or(""), 0.0, plain.clone());
|
||||
@@ -400,7 +400,7 @@ pub fn draw(app: &FigApp, painter: &egui::Painter) {
|
||||
if let Some((x, y, _)) = &app.ui.comments.draft {
|
||||
let r = pin_rect(app.to_screen(*x, *y));
|
||||
painter.rect_filled(r, BUBBLE, ACCENT);
|
||||
painter.circle_filled(r.center(), 3.0, Color32::WHITE);
|
||||
painter.circle_filled(r.center(), 3.0, crate::theme::ACCENT_ON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,7 +729,7 @@ fn message(
|
||||
ui.add_space(indent);
|
||||
let mut names: Vec<&str> = cx.people.iter().map(String::as_str).collect();
|
||||
names.push(cx.me);
|
||||
ui.add(egui::Label::new(rich_text(&m.text, &names, t.text, ui.available_width())).wrap());
|
||||
ui.add(egui::Label::new(rich_text(&m.text, &names, t.text, t.accent_text, ui.available_width())).wrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1062,7 +1062,8 @@ pub fn panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
ui.add_space(16.0);
|
||||
ui.label(egui::RichText::new(tr(app.ui.comments.filter.label())).font(regular(11.0)).color(t.text_secondary));
|
||||
ui.add_space(4.0);
|
||||
if ui.add(egui::Label::new(egui::RichText::new(tr("Show open")).font(regular(11.0)).color(ACCENT)).sense(Sense::click())).clicked() {
|
||||
if ui.add(egui::Label::new(egui::RichText::new(tr("Show open")).font(regular(11.0)).color(t.accent_text)).sense(Sense::click())).clicked()
|
||||
{
|
||||
app.ui.comments.filter = Filter::Open;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -628,7 +628,7 @@ pub fn dev_resources_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, id:
|
||||
}
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + PAD + 7.0, r.center().y), vec2(14.0, 14.0)), Icon::Link, t.icon_secondary);
|
||||
let x = r.min.x + PAD + 22.0;
|
||||
ui.painter().text(pos2(x, r.center().y - 7.0), Align2::LEFT_CENTER, widgets::elide(ui, name, text_w), semibold(11.0), ACCENT);
|
||||
ui.painter().text(pos2(x, r.center().y - 7.0), Align2::LEFT_CENTER, widgets::elide(ui, name, text_w), semibold(11.0), t.accent_text);
|
||||
let below = match item.from.and_then(|f| doc.node(f)) {
|
||||
Some(m) => trf("From {name}", &[("name", &m.name)]),
|
||||
None => url.clone(),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use egui::{Align2, Color32, Context, CornerRadius, RichText, Sense, Stroke, vec2};
|
||||
use egui::{Align2, Context, CornerRadius, RichText, Sense, Stroke, vec2};
|
||||
use figcraft_color::ColorProfile;
|
||||
use figcraft_doc::{ExportFormat, ExportSetting, ExportSize, NodeId};
|
||||
use figcraft_engine::cmd::file::{export_jobs, exportable_layers};
|
||||
@@ -12,8 +12,7 @@ use serde_json::{Value, json};
|
||||
use crate::FigApp;
|
||||
use crate::export_panel::{export_layers, save_one, suggested_name};
|
||||
use crate::i18n::{self, tr, tr_noop, trf, trn};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
use crate::theme::{Tokens, regular, semibold};
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -122,10 +121,10 @@ pub(crate) fn tab_strip<T: Copy + PartialEq>(ui: &mut egui::Ui, t: &Tokens, tabs
|
||||
fn about_body(ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(48.0, 48.0), Sense::hover());
|
||||
ui.painter().rect_filled(r, CornerRadius::same(10), ACCENT);
|
||||
icons::paint(ui.painter(), r.shrink(10.0), Icon::Logo, Color32::WHITE);
|
||||
crate::theme::paint_app_icon(ui, r);
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new("FigCraft").font(semibold(18.0)));
|
||||
let (wordmark, _) = ui.allocate_exact_size(vec2(130.0, 23.0), Sense::hover());
|
||||
crate::theme::paint_wordmark(ui.painter(), egui::pos2(wordmark.min.x, wordmark.center().y), 18.0, t);
|
||||
ui.label(RichText::new(trf("Version {version}", &[("version", &VERSION)])).color(t.text_secondary));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -198,9 +198,9 @@ fn dropped(app: &mut FigApp, ctx: &egui::Context) {
|
||||
|
||||
fn sidebar(app: &mut FigApp, ui: &mut Ui, t: &Tokens, left: Rect) {
|
||||
let mut y = left.min.y + 20.0;
|
||||
icons::paint(ui.painter(), Rect::from_min_size(pos2(left.min.x + 16.0, y), vec2(20.0, 20.0)), Icon::Logo, ACCENT);
|
||||
ui.painter().text(pos2(left.min.x + 44.0, y + 10.0), Align2::LEFT_CENTER, "FigCraft", semibold(14.0), t.text);
|
||||
y += 48.0;
|
||||
crate::theme::paint_app_icon(ui, Rect::from_min_size(pos2(left.min.x + 12.0, y - 4.0), vec2(32.0, 32.0)));
|
||||
crate::theme::paint_wordmark(ui.painter(), pos2(left.min.x + 52.0, y + 12.0), 16.0, t);
|
||||
y += 52.0;
|
||||
let search = Rect::from_min_size(pos2(left.min.x + 8.0, y), vec2(SIDEBAR - 16.0, widgets::FIELD_H));
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(search), |ui| {
|
||||
let response =
|
||||
@@ -320,8 +320,8 @@ fn action_card(ui: &mut Ui, t: &Tokens, icon: Icon, title: &str, sub: &str) -> b
|
||||
}
|
||||
card_frame(ui, t, r, &resp);
|
||||
let ir = Rect::from_min_size(pos2(r.min.x + 16.0, r.center().y - 16.0), vec2(32.0, 32.0));
|
||||
ui.painter().rect_filled(ir, CornerRadius::same(6), ACCENT);
|
||||
icons::paint(ui.painter(), ir.shrink(8.0), icon, Color32::WHITE);
|
||||
ui.painter().rect_filled(ir, CornerRadius::same(6), t.accent);
|
||||
icons::paint(ui.painter(), ir.shrink(8.0), icon, t.accent_on);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y - 8.0), Align2::LEFT_CENTER, title_shown, semibold(12.0), t.text);
|
||||
ui.painter().text(pos2(r.min.x + 60.0, r.center().y + 9.0), Align2::LEFT_CENTER, sub_shown, regular(11.0), t.text_secondary);
|
||||
resp.clicked()
|
||||
|
||||
@@ -738,8 +738,8 @@ fn file_icon(ui: &Ui, t: &Tokens, center: egui::Pos2, path: &str) {
|
||||
let r = Rect::from_center_size(center, vec2(16.0, 16.0));
|
||||
match FileKind::of(Path::new(path)) {
|
||||
FileKind::FigCraft | FileKind::Fig => {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), ACCENT);
|
||||
icons::paint(ui.painter(), r.shrink(3.0), Icon::Logo, Color32::WHITE);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), t.accent);
|
||||
icons::paint(ui.painter(), r.shrink(3.0), Icon::Logo, t.accent_on);
|
||||
}
|
||||
kind => {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), t.field);
|
||||
|
||||
@@ -256,9 +256,11 @@ fn rects_of(s: &[Shape], c: egui::Color32) -> usize {
|
||||
s.iter().filter(|s| matches!(s, Shape::Rect(r) if r.fill == c)).count()
|
||||
}
|
||||
|
||||
/// Images drawn (meshes with a texture of their own).
|
||||
/// Preview images in the main content area, excluding the branded sidebar icon.
|
||||
fn images(s: &[Shape]) -> usize {
|
||||
s.iter().filter(|s| matches!(s, Shape::Mesh(m) if m.texture_id != egui::TextureId::default())).count()
|
||||
s.iter()
|
||||
.filter(|s| matches!(s, Shape::Mesh(m) if m.texture_id != egui::TextureId::default() && m.vertices.iter().any(|v| v.pos.x > 240.0)))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -332,7 +332,7 @@ fn pages_edge(app: &mut FigApp, ui: &mut Ui, shown: f32) {
|
||||
if active {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
|
||||
}
|
||||
let (width, colour) = if active { (2.0, t.accent) } else { (1.0, t.border) };
|
||||
let (width, colour) = if active { (2.0, ACCENT) } else { (1.0, t.border) };
|
||||
ui.painter().line_segment([line.left_center(), line.right_center()], Stroke::new(width, colour));
|
||||
let asked = if resp.double_clicked() {
|
||||
Some(None)
|
||||
@@ -642,7 +642,7 @@ fn search_field(app: &mut FigApp, ui: &mut Ui, t: &Tokens, w: f32, id: Id) {
|
||||
);
|
||||
crate::widgets::select_all_on_focus(ui, &r, &app.ui.layer_filter);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(field, CornerRadius::same(5), Stroke::new(1.0, ACCENT), egui::StrokeKind::Inside);
|
||||
}
|
||||
if (r.has_focus() || r.lost_focus()) && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.ui.layer_search = false;
|
||||
|
||||
@@ -1089,7 +1089,7 @@ impl FigApp {
|
||||
let resp = ui.interact(rect, id, egui::Sense::click_and_drag());
|
||||
if resp.hovered() || resp.dragged() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
|
||||
ui.painter().vline(x, rows, egui::Stroke::new(2.0, theme::Tokens::get(ui.ctx()).accent));
|
||||
ui.painter().vline(x, rows, egui::Stroke::new(2.0, theme::ACCENT));
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
return Some(PANEL_DEFAULT);
|
||||
@@ -1812,11 +1812,11 @@ pub(crate) fn drop_overlay(ctx: &egui::Context, rect: egui::Rect, label: &str) {
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("drop_overlay")));
|
||||
let area = rect.shrink(4.0);
|
||||
painter.rect_filled(area, egui::CornerRadius::same(8), t.accent.gamma_multiply(0.12));
|
||||
painter.rect_stroke(area, egui::CornerRadius::same(8), egui::Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
painter.rect_stroke(area, egui::CornerRadius::same(8), egui::Stroke::new(2.0, theme::ACCENT), egui::StrokeKind::Inside);
|
||||
let galley = painter.layout_no_wrap(label.to_owned(), theme::semibold(14.0), t.text);
|
||||
let pill = egui::Rect::from_center_size(area.center(), galley.size() + egui::vec2(32.0, 16.0));
|
||||
painter.rect_filled(pill, egui::CornerRadius::same(8), t.panel);
|
||||
painter.rect_stroke(pill, egui::CornerRadius::same(8), egui::Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
painter.rect_stroke(pill, egui::CornerRadius::same(8), egui::Stroke::new(1.0, theme::ACCENT), egui::StrokeKind::Inside);
|
||||
painter.galley(pill.center() - galley.size() / 2.0, galley, t.text);
|
||||
// Hovering sends no pointer events on every platform: keep the overlay current.
|
||||
ctx.request_repaint();
|
||||
|
||||
@@ -2223,7 +2223,7 @@ fn top_bar(p: &mut Presenter, ui: &mut Ui, bar: Rect, now: f64, chrome: Chrome)
|
||||
}
|
||||
let on = (*act == BarAction::Comments && p.view.comments) || (*act == BarAction::Inspect && p.view.inspect);
|
||||
if on {
|
||||
ui.painter().rect_filled(br, CornerRadius::same(5), ACCENT);
|
||||
ui.painter().rect_filled(br, CornerRadius::same(5), t.accent);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(br, CornerRadius::same(5), hover);
|
||||
}
|
||||
@@ -2234,9 +2234,9 @@ fn top_bar(p: &mut Presenter, ui: &mut Ui, bar: Rect, now: f64, chrome: Chrome)
|
||||
resp = resp.tip(tr("Inspect the layer under the pointer: size, colours, typography"));
|
||||
}
|
||||
if chrome.pane {
|
||||
icons::paint(ui.painter(), br.shrink(5.0), *icon, t.icon);
|
||||
icons::paint(ui.painter(), br.shrink(5.0), *icon, if on { t.accent_on } else { t.icon });
|
||||
} else {
|
||||
ui.painter().text(br.center(), Align2::CENTER_CENTER, shown, regular(11.5), text);
|
||||
ui.painter().text(br.center(), Align2::CENTER_CENTER, shown, regular(11.5), if on { t.accent_on } else { text });
|
||||
}
|
||||
if *act == BarAction::Options {
|
||||
let menu = crate::right::menu_popup(&resp).width(220.0).show(|ui| options_menu(p, ui, device, settings.responsive, now));
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -763,7 +763,7 @@ fn trigger_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &Inte
|
||||
let id = capture_id().with("field");
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.field);
|
||||
if cap {
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(5), Stroke::new(1.0, ACCENT), StrokeKind::Inside);
|
||||
ui.memory_mut(|m| m.request_focus(id));
|
||||
let _ = ui.interact(r, id, Sense::focusable_noninteractive());
|
||||
let pressed = ui.input(|i| {
|
||||
@@ -1048,7 +1048,7 @@ fn animation_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &In
|
||||
let w = ui.available_width().max(200.0);
|
||||
let (r, rr) = ui.allocate_exact_size(vec2(w, 26.0), Sense::click());
|
||||
if rr.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), ACCENT);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.menu_hover);
|
||||
}
|
||||
let col = if rr.hovered() { Color32::WHITE } else { t.text };
|
||||
curve(ui.painter(), Rect::from_min_size(pos2(r.min.x + 6.0, r.min.y + 4.0), vec2(28.0, 18.0)), *e, col, None);
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::i18n::{tr, trf, trn};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::list_drag::{ListDrag, move_flags, remove_flags};
|
||||
use crate::style_picker::{self, StyleSlot};
|
||||
use crate::theme::{ACCENT, Tokens, regular, semibold};
|
||||
use crate::theme::{Tokens, regular, semibold};
|
||||
use crate::widgets::{self, Edit, FIELD_H, FieldEdit, Lead, PAD};
|
||||
use crate::{FigApp, RightTab};
|
||||
|
||||
@@ -84,12 +84,12 @@ pub fn right_panel(app: &mut FigApp, ui: &mut Ui) {
|
||||
// Top bar: people (you and the others in a live session), present, share.
|
||||
let (top, _) = ui.allocate_exact_size(vec2(w, 48.0), Sense::hover());
|
||||
// As wide as its label needs (longer in some languages).
|
||||
let label = ui.painter().layout_no_wrap(tr("Share").to_string(), semibold(11.5), Color32::WHITE);
|
||||
let label = ui.painter().layout_no_wrap(tr("Share").to_string(), semibold(11.5), t.accent_on);
|
||||
let sw = (label.size().x + 24.0).max(56.0);
|
||||
let share = Rect::from_min_size(pos2(top.max.x - 14.0 - sw, top.center().y - 16.0), vec2(sw, 32.0));
|
||||
let sresp = ui.interact(share, ui.id().with("share"), Sense::click()).tip(tr("Share: live session, export"));
|
||||
ui.painter().rect_filled(share, CornerRadius::same(6), if sresp.hovered() { t.accent_hover } else { ACCENT });
|
||||
ui.painter().galley(share.center() - label.size() / 2.0, label, Color32::WHITE);
|
||||
ui.painter().rect_filled(share, CornerRadius::same(6), if sresp.hovered() { t.accent_hover } else { t.accent });
|
||||
ui.painter().galley(share.center() - label.size() / 2.0, label, t.accent_on);
|
||||
crate::live::share_popover(app, &sresp);
|
||||
// Play presents; its chevron opens the play menu (present, or preview in a pane).
|
||||
let play = Rect::from_min_size(pos2(share.min.x - 50.0, top.center().y - 14.0), vec2(42.0, 28.0));
|
||||
@@ -284,13 +284,13 @@ pub fn menu_row(ui: &mut Ui, label: &str, shortcut: &str, art_w: f32, art: impl
|
||||
a11y::own_focus(&resp);
|
||||
let hovered = resp.hovered() || resp.has_focus();
|
||||
if hovered {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), ACCENT);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.menu_hover);
|
||||
}
|
||||
let col = if hovered { Color32::WHITE } else { t.text };
|
||||
let col = t.menu_text;
|
||||
ui.painter().galley(pos2(r.min.x + MENU_PAD, r.center().y - text.size().y / 2.0), text, col);
|
||||
if let Some(k) = keys {
|
||||
let at = pos2(r.max.x - MENU_PAD - k.size().x, r.center().y - k.size().y / 2.0);
|
||||
ui.painter().galley(at, k, if hovered { Color32::WHITE } else { t.text_secondary });
|
||||
ui.painter().galley(at, k, if hovered { t.menu_text } else { t.menu_dim });
|
||||
}
|
||||
art(ui.painter(), r, col);
|
||||
resp
|
||||
|
||||
@@ -482,7 +482,7 @@ fn style_grid(ui: &mut Ui, list: &[(&Style, Key)], applied: Option<&Key>, editab
|
||||
None => preview(ui, r, s, StyleSlot::Fill),
|
||||
}
|
||||
if applied == Some(key) {
|
||||
ui.painter().rect_stroke(r.expand(2.0), CornerRadius::same(5), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(r.expand(2.0), CornerRadius::same(5), Stroke::new(2.0, crate::theme::ACCENT), egui::StrokeKind::Inside);
|
||||
} else if resp.hovered() && !text {
|
||||
ui.painter().rect_stroke(r.expand(2.0), CornerRadius::same(4), Stroke::new(1.0, t.menu_text), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
+115
-23
@@ -1,6 +1,6 @@
|
||||
//! Design tokens (UI3 light and dark) and fonts.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use egui::{Color32, Context, FontData, FontDefinitions, FontFamily, FontId, Stroke, TextStyle, Visuals};
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct Tokens {
|
||||
pub field_hover_border: Color32,
|
||||
pub accent: Color32,
|
||||
pub accent_hover: Color32,
|
||||
pub accent_on: Color32,
|
||||
pub accent_text: Color32,
|
||||
pub selected_row: Color32,
|
||||
pub selected_row_child: Color32,
|
||||
@@ -44,11 +45,38 @@ pub struct Tokens {
|
||||
pub focus: Color32,
|
||||
}
|
||||
|
||||
pub const ACCENT: Color32 = Color32::from_rgb(0x0D, 0x99, 0xFF);
|
||||
/// FigCraft's brand green for solid surfaces.
|
||||
pub const BRAND: Color32 = Color32::from_rgb(0x02, 0xDA, 0x96);
|
||||
/// Contrast-safe green for thin selection outlines and white-on-green labels.
|
||||
/// It reaches 4.5:1 on white and 3:1 on the dark canvas.
|
||||
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(0x14, 0xAE, 0x5C);
|
||||
pub const DEV_CHANGED: Color32 = Color32::from_rgb(0xD9, 0x7A, 0x06);
|
||||
pub const DEV_READY: Color32 = Color32::from_rgb(0x08, 0x7D, 0x4F);
|
||||
pub const DEV_CHANGED: Color32 = Color32::from_rgb(0xA7, 0x5B, 0x00);
|
||||
pub const DEV_COMPLETED: Color32 = Color32::from_rgb(0x6E, 0x6E, 0x73);
|
||||
|
||||
impl Tokens {
|
||||
@@ -70,16 +98,17 @@ impl Tokens {
|
||||
pressed: Color32::from_rgb(0xE6, 0xE6, 0xE6),
|
||||
field: Color32::from_rgb(0xF5, 0xF5, 0xF5),
|
||||
field_hover_border: Color32::from_rgb(0xE6, 0xE6, 0xE6),
|
||||
accent: ACCENT,
|
||||
accent_hover: Color32::from_rgb(0x00, 0x7B, 0xE5),
|
||||
// 5.2:1 on white, 4.6:1 on selected rows: WCAG AA for text.
|
||||
accent_text: Color32::from_rgb(0x00, 0x6B, 0xD1),
|
||||
selected_row: Color32::from_rgb(0xE5, 0xF4, 0xFF),
|
||||
selected_row_child: Color32::from_rgb(0xF2, 0xF9, 0xFF),
|
||||
accent: BRAND,
|
||||
accent_hover: Color32::from_rgb(0x00, 0xAD, 0x78),
|
||||
accent_on: ACCENT_ON,
|
||||
// Darker green keeps links and labels legible on white and selected rows.
|
||||
accent_text: Color32::from_rgb(0x00, 0x7A, 0x56),
|
||||
selected_row: Color32::from_rgb(0xE0, 0xFB, 0xF1),
|
||||
selected_row_child: Color32::from_rgb(0xF1, 0xFC, 0xF7),
|
||||
component: COMPONENT,
|
||||
component_row: Color32::from_rgb(0xF1, 0xE9, 0xFF),
|
||||
danger: Color32::from_rgb(0xF2, 0x48, 0x22),
|
||||
success: Color32::from_rgb(0x14, 0xAE, 0x5C),
|
||||
success: DEV_READY,
|
||||
warning: Color32::from_rgb(0xFF, 0xCD, 0x29),
|
||||
guide: Color32::from_rgb(0xF2, 0x48, 0x22),
|
||||
tab_bar: Color32::from_rgb(0xE8, 0xE8, 0xE8),
|
||||
@@ -89,7 +118,7 @@ impl Tokens {
|
||||
menu_text: Color32::from_rgb(0xFF, 0xFF, 0xFF),
|
||||
menu_hover: ACCENT,
|
||||
menu_dim: Color32::from_rgb(0x9A, 0x9A, 0x9A),
|
||||
focus: Color32::from_rgb(0x00, 0x6B, 0xD1),
|
||||
focus: Color32::from_rgb(0x00, 0x7A, 0x56),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,15 +139,16 @@ impl Tokens {
|
||||
pressed: Color32::from_rgb(0x44, 0x44, 0x44),
|
||||
field: Color32::from_rgb(0x38, 0x38, 0x38),
|
||||
field_hover_border: Color32::from_rgb(0x4D, 0x4D, 0x4D),
|
||||
accent: ACCENT,
|
||||
accent_hover: Color32::from_rgb(0x3D, 0xAD, 0xFF),
|
||||
accent_text: Color32::from_rgb(0x7C, 0xC4, 0xF8),
|
||||
selected_row: Color32::from_rgb(0x2A, 0x45, 0x5C),
|
||||
selected_row_child: Color32::from_rgb(0x2D, 0x37, 0x41),
|
||||
accent: BRAND,
|
||||
accent_hover: Color32::from_rgb(0x52, 0xE9, 0xB7),
|
||||
accent_on: ACCENT_ON,
|
||||
accent_text: Color32::from_rgb(0x52, 0xE9, 0xB7),
|
||||
selected_row: Color32::from_rgb(0x16, 0x42, 0x36),
|
||||
selected_row_child: Color32::from_rgb(0x12, 0x37, 0x2D),
|
||||
component: Color32::from_rgb(0xD2, 0x7C, 0xFF),
|
||||
component_row: Color32::from_rgb(0x3E, 0x30, 0x52),
|
||||
danger: Color32::from_rgb(0xF2, 0x48, 0x22),
|
||||
success: Color32::from_rgb(0x14, 0xAE, 0x5C),
|
||||
success: DEV_READY,
|
||||
warning: Color32::from_rgb(0xFF, 0xCD, 0x29),
|
||||
guide: Color32::from_rgb(0xF2, 0x48, 0x22),
|
||||
tab_bar: Color32::from_rgb(0x1E, 0x1E, 0x1E),
|
||||
@@ -128,7 +158,7 @@ impl Tokens {
|
||||
menu_text: Color32::from_rgb(0xFF, 0xFF, 0xFF),
|
||||
menu_hover: ACCENT,
|
||||
menu_dim: Color32::from_rgb(0x9A, 0x9A, 0x9A),
|
||||
focus: ACCENT,
|
||||
focus: BRAND,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +216,7 @@ fn visuals(t: Tokens) -> Visuals {
|
||||
v.window_corner_radius = egui::CornerRadius::same(13);
|
||||
v.menu_corner_radius = egui::CornerRadius::same(13);
|
||||
v.selection.bg_fill = t.accent.gamma_multiply(0.3);
|
||||
v.selection.stroke = Stroke::new(1.0, t.accent);
|
||||
v.selection.stroke = Stroke::new(1.0, ACCENT);
|
||||
v.hyperlink_color = t.accent_text;
|
||||
for w in [&mut v.widgets.inactive, &mut v.widgets.hovered, &mut v.widgets.active, &mut v.widgets.open, &mut v.widgets.noninteractive] {
|
||||
w.corner_radius = egui::CornerRadius::same(5);
|
||||
@@ -202,10 +232,10 @@ fn visuals(t: Tokens) -> Visuals {
|
||||
v.widgets.hovered.bg_stroke = Stroke::new(1.0, t.field_hover_border);
|
||||
v.widgets.active.bg_fill = t.pressed;
|
||||
v.widgets.active.weak_bg_fill = t.pressed;
|
||||
v.widgets.active.bg_stroke = Stroke::new(1.0, t.accent);
|
||||
v.widgets.active.bg_stroke = Stroke::new(1.0, ACCENT);
|
||||
v.popup_shadow = egui::Shadow { offset: [0, 4], blur: 16, spread: 0, color: t.shadow };
|
||||
v.window_shadow = egui::Shadow { offset: [0, 8], blur: 24, spread: 0, color: t.shadow };
|
||||
v.text_cursor.stroke = Stroke::new(1.0, t.accent);
|
||||
v.text_cursor.stroke = Stroke::new(1.0, ACCENT);
|
||||
v
|
||||
}
|
||||
|
||||
@@ -226,7 +256,8 @@ pub fn on_dark<R>(ui: &mut egui::Ui, f: impl FnOnce(&mut egui::Ui) -> R) -> R {
|
||||
r
|
||||
}
|
||||
|
||||
/// UI fonts: Inter (from the craft-fonts build input) when present, else egui's defaults; with
|
||||
/// UI fonts: Inter and the Archivo wordmark (from the craft-fonts build input) when present;
|
||||
/// otherwise egui's defaults. With
|
||||
/// `system`, also system faces for scripts those lack (Hebrew, joined Arabic) so names in them show,
|
||||
/// and faces for the CJK scripts in `cjk` ([`crate::cjk`]).
|
||||
pub fn install_fonts(ctx: &Context, system: bool, cjk: u8) {
|
||||
@@ -252,6 +283,18 @@ pub fn install_fonts(ctx: &Context, system: bool, cjk: u8) {
|
||||
}
|
||||
semi.extend(fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default());
|
||||
fonts.families.insert(FontFamily::Name("semibold".into()), semi);
|
||||
// Keep the logo's typeface independent of UI text. System Archivo is used when available;
|
||||
// releases with craft-fonts embed it so the wordmark does not depend on installation.
|
||||
let archivo = find("Archivo", "SemiBold").map(FontData::from_static).or_else(|| {
|
||||
system.then(|| figcraft_text::find_face("Archivo", "SemiBold")).flatten().filter(|face| face.family == "Archivo").map(|face| font_data(&face))
|
||||
});
|
||||
let mut brand = Vec::new();
|
||||
if let Some(data) = archivo {
|
||||
fonts.font_data.insert("archivo".into(), Arc::new(data));
|
||||
brand.push("archivo".into());
|
||||
}
|
||||
brand.extend(fonts.families.get(&FontFamily::Name("semibold".into())).cloned().unwrap_or_default());
|
||||
fonts.families.insert(FontFamily::Name("brand".into()), brand);
|
||||
// CJK and other scripts from craft-fonts as fallbacks (Noto Sans CJK only with FIGCRAFT_EMBED_CJK).
|
||||
const BUILT_IN: [&str; 4] = ["Inter", "BIZ UDPGothic", "Noto Sans CJK SC", "Noto Sans Arabic"];
|
||||
let lang = crate::i18n::current().map_or("en", |l| l.code);
|
||||
@@ -339,6 +382,40 @@ pub fn semibold(size: f32) -> FontId {
|
||||
FontId::new(size, FontFamily::Name("semibold".into()))
|
||||
}
|
||||
|
||||
pub fn brand(size: f32) -> FontId {
|
||||
FontId::new(size, FontFamily::Name("brand".into()))
|
||||
}
|
||||
|
||||
/// The two-tone Archivo wordmark used in the home sidebar and About window.
|
||||
pub fn paint_wordmark(painter: &egui::Painter, left_center: egui::Pos2, size: f32, t: &Tokens) {
|
||||
let font = brand(size);
|
||||
let fig = painter.layout_no_wrap("Fig".into(), font.clone(), t.text);
|
||||
let fig_width = fig.size().x;
|
||||
painter.galley(egui::pos2(left_center.x, left_center.y - fig.size().y / 2.0), fig, t.text);
|
||||
painter.text(egui::pos2(left_center.x + fig_width, left_center.y), egui::Align2::LEFT_CENTER, "Craft", font, t.accent_text);
|
||||
}
|
||||
|
||||
/// Paint the owner's original chameleon icon from the existing app asset.
|
||||
pub fn paint_app_icon(ui: &egui::Ui, rect: egui::Rect) {
|
||||
static ICON: OnceLock<Option<egui::ColorImage>> = OnceLock::new();
|
||||
let image = ICON.get_or_init(|| {
|
||||
let rgba = image::load_from_memory(include_bytes!("../../../assets/app-icon/figcraft-64.png")).ok()?.to_rgba8();
|
||||
Some(egui::ColorImage::from_rgba_unmultiplied([rgba.width() as usize, rgba.height() as usize], rgba.as_raw()))
|
||||
});
|
||||
let Some(image) = image else {
|
||||
ui.painter().rect_filled(rect, egui::CornerRadius::same(5), BRAND);
|
||||
crate::icons::paint(ui.painter(), rect.shrink(rect.width() * 0.22), crate::icons::Icon::Logo, ACCENT_ON);
|
||||
return;
|
||||
};
|
||||
let id = egui::Id::new("figcraft_original_app_icon");
|
||||
let texture = ui.ctx().data(|data| data.get_temp::<egui::TextureHandle>(id)).unwrap_or_else(|| {
|
||||
let texture = ui.ctx().load_texture("figcraft-original-app-icon", image.clone(), egui::TextureOptions::LINEAR);
|
||||
ui.ctx().data_mut(|data| data.insert_temp(id, texture.clone()));
|
||||
texture
|
||||
});
|
||||
ui.painter().image(texture.id(), rect, egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
|
||||
pub fn regular(size: f32) -> FontId {
|
||||
FontId::new(size, FontFamily::Proportional)
|
||||
}
|
||||
@@ -370,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();
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::FigApp;
|
||||
use crate::a11y::{self, Nav, Region, Tip};
|
||||
use crate::i18n::{tr, tr_noop, trf};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::{ACCENT, Tokens, regular};
|
||||
use crate::theme::{Tokens, regular};
|
||||
|
||||
/// Every mark in the toolbar row is painted in a box this size, so they share one optical size
|
||||
/// and one stroke width (#737, #738).
|
||||
@@ -159,11 +159,11 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens) {
|
||||
// The Actions button reads on while its panel is open; clicking it again closes it.
|
||||
let actions_open = id == "actions" && app.ui.palette.is_some();
|
||||
if actions_open {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), ACCENT);
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.accent);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if actions_open { Color32::WHITE } else { t.icon });
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if actions_open { t.accent_on } else { t.icon });
|
||||
if resp.clicked() {
|
||||
if id == "actions" {
|
||||
crate::menus::toggle_palette(&mut app.ui);
|
||||
@@ -386,14 +386,14 @@ fn tool_cell(ui: &mut Ui, t: &Tokens, r: Rect, id: impl egui::AsIdSalt, icon: Ic
|
||||
});
|
||||
a11y::toggle(&resp, tip, active);
|
||||
let bg = if active {
|
||||
ACCENT
|
||||
t.accent
|
||||
} else if resp.hovered() {
|
||||
t.hover
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
ui.painter().rect_filled(r, CornerRadius::same(5), bg);
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if active { Color32::WHITE } else { t.icon });
|
||||
icons::paint(ui.painter(), glyph_rect(r), icon, if active { t.accent_on } else { t.icon });
|
||||
resp
|
||||
}
|
||||
|
||||
@@ -409,8 +409,8 @@ fn done_button(ui: &mut Ui, t: &Tokens, x: f32, cy: f32, tip: &str) -> bool {
|
||||
divider(ui, t, x, cy);
|
||||
let done = Rect::from_min_size(pos2(x + 12.0, cy - 14.0), vec2(DONE_W, 28.0));
|
||||
let resp = ui.interact(done, ui.id().with("tool_done"), Sense::click()).tip(tip);
|
||||
ui.painter().rect_filled(done, CornerRadius::same(6), if resp.hovered() { t.accent_hover } else { ACCENT });
|
||||
ui.painter().text(done.center(), Align2::CENTER_CENTER, tr("Done"), crate::theme::semibold(11.5), Color32::WHITE);
|
||||
ui.painter().rect_filled(done, CornerRadius::same(6), if resp.hovered() { t.accent_hover } else { t.accent });
|
||||
ui.painter().text(done.center(), Align2::CENTER_CENTER, tr("Done"), crate::theme::semibold(11.5), t.accent_on);
|
||||
resp.clicked()
|
||||
}
|
||||
|
||||
|
||||
@@ -587,7 +587,7 @@ fn decoration_details(app: &mut FigApp, ui: &mut Ui, styles: &[TextStyle], apply
|
||||
if !auto
|
||||
&& ui
|
||||
.add(
|
||||
egui::Label::new(egui::RichText::new(tr("Auto")).font(regular(11.0)).color(Tokens::get(ui.ctx()).accent))
|
||||
egui::Label::new(egui::RichText::new(tr("Auto")).font(regular(11.0)).color(Tokens::get(ui.ctx()).accent_text))
|
||||
.sense(Sense::click()),
|
||||
)
|
||||
.on_hover_text(tip)
|
||||
@@ -713,7 +713,7 @@ fn variable_axes(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], styles: &[TextSt
|
||||
}
|
||||
if styles.iter().any(|s| !s.variations.is_empty()) {
|
||||
ui.add_space(8.0);
|
||||
let r = ui.add(egui::Label::new(egui::RichText::new(tr("Reset to style")).font(regular(11.0)).color(t.accent)).sense(Sense::click()));
|
||||
let r = ui.add(egui::Label::new(egui::RichText::new(tr("Reset to style")).font(regular(11.0)).color(t.accent_text)).sense(Sense::click()));
|
||||
if r.on_hover_text(tr("Back to the values the font style implies")).clicked() {
|
||||
app.run("text.setVariations", json!({"ids": ids, "variations": {}, "replace": true}));
|
||||
}
|
||||
@@ -789,7 +789,7 @@ fn axis_slider(ui: &mut Ui, id: Id, axis: &figcraft_text::Axis, value: Option<f6
|
||||
let x = x_of(v);
|
||||
p.rect_filled(Rect::from_min_max(track.min, pos2(x, track.max.y)), CornerRadius::same(2), t.accent);
|
||||
p.circle_filled(pos2(x, track.center().y), 6.0, Color32::WHITE);
|
||||
let ring = if resp.hovered() || resp.dragged() { t.accent } else { t.border_strong };
|
||||
let ring = if resp.hovered() || resp.dragged() { crate::theme::ACCENT } else { t.border_strong };
|
||||
p.circle_stroke(pos2(x, track.center().y), 6.0, Stroke::new(1.0, ring));
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
@@ -899,14 +899,14 @@ fn feature_row(ui: &mut Ui, tag: &str, label: &str, on: Option<bool>, supported:
|
||||
match on {
|
||||
Some(true) => {
|
||||
ui.painter().rect_filled(b, CornerRadius::same(3), t.accent);
|
||||
icons::paint(ui.painter(), b.shrink(1.5), Icon::Check, Color32::WHITE);
|
||||
icons::paint(ui.painter(), b.shrink(1.5), Icon::Check, t.accent_on);
|
||||
}
|
||||
Some(false) => {
|
||||
ui.painter().rect_stroke(b, CornerRadius::same(3), Stroke::new(1.0, t.border_strong), StrokeKind::Inside);
|
||||
}
|
||||
None => {
|
||||
ui.painter().rect_filled(b, CornerRadius::same(3), t.accent);
|
||||
ui.painter().line_segment([pos2(b.min.x + 3.5, b.center().y), pos2(b.max.x - 3.5, b.center().y)], Stroke::new(1.5, Color32::WHITE));
|
||||
ui.painter().line_segment([pos2(b.min.x + 3.5, b.center().y), pos2(b.max.x - 3.5, b.center().y)], Stroke::new(1.5, t.accent_on));
|
||||
}
|
||||
}
|
||||
let col = if supported { t.text } else { t.text_tertiary };
|
||||
@@ -1025,7 +1025,7 @@ fn lead_field(ui: &mut Ui, id: Id, glyph: LeadGlyph, value: &str, width: f32, ti
|
||||
crate::a11y::text(&resp, tip, value);
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
if editing.is_some() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.field_hover_border), StrokeKind::Inside);
|
||||
}
|
||||
@@ -1274,7 +1274,7 @@ pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool)
|
||||
if hovered {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.accent);
|
||||
}
|
||||
let col = if hovered { Color32::WHITE } else { t.text };
|
||||
let col = if hovered { t.accent_on } else { t.text };
|
||||
if current.is_some_and(|c| c.eq_ignore_ascii_case(f)) {
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
|
||||
@@ -217,7 +217,7 @@ fn number_field_impl(
|
||||
let hovered = field_resp.hovered() || lead_resp.hovered() || editing.is_some();
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
if editing.is_some() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
} else if hovered {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.field_hover_border), StrokeKind::Inside);
|
||||
}
|
||||
@@ -507,7 +507,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
a11y::text(&resp, name, value);
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
if editing.is_some() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.field_hover_border), StrokeKind::Inside);
|
||||
}
|
||||
@@ -570,7 +570,7 @@ pub fn text_input(ui: &mut Ui, id: Id, text: &mut String, width: f32, hint: &str
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
let r = field_editor(ui, rect, id, text, hint);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
} else if ui.rect_contains_pointer(rect) {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.field_hover_border), StrokeKind::Inside);
|
||||
}
|
||||
@@ -974,7 +974,7 @@ pub fn checkbox(ui: &mut Ui, on: bool, label: &str) -> bool {
|
||||
let b = Rect::from_min_size(pos2(rect.min.x, rect.center().y - 7.0), vec2(14.0, 14.0));
|
||||
if on {
|
||||
ui.painter().rect_filled(b, CornerRadius::same(3), t.accent);
|
||||
icons::paint(ui.painter(), b.shrink(1.5), Icon::Check, Color32::WHITE);
|
||||
icons::paint(ui.painter(), b.shrink(1.5), Icon::Check, t.accent_on);
|
||||
} else {
|
||||
ui.painter().rect_stroke(b, CornerRadius::same(3), Stroke::new(1.0, t.border_strong), StrokeKind::Inside);
|
||||
}
|
||||
@@ -990,9 +990,9 @@ pub fn radio(ui: &mut Ui, on: bool, label: &str) -> bool {
|
||||
let c = pos2(rect.min.x + 7.0, rect.center().y);
|
||||
if on {
|
||||
ui.painter().circle_filled(c, 7.0, t.accent);
|
||||
ui.painter().circle_filled(c, 2.5, Color32::WHITE);
|
||||
ui.painter().circle_filled(c, 2.5, t.accent_on);
|
||||
} else {
|
||||
ui.painter().circle_stroke(c, 6.5, Stroke::new(1.0, if resp.hovered() { t.accent } else { t.border_strong }));
|
||||
ui.painter().circle_stroke(c, 6.5, Stroke::new(1.0, if resp.hovered() { crate::theme::ACCENT } else { t.border_strong }));
|
||||
}
|
||||
ui.painter().text(pos2(rect.min.x + 22.0, rect.center().y), Align2::LEFT_CENTER, label, regular(11.0), t.text);
|
||||
resp.clicked()
|
||||
@@ -1008,8 +1008,8 @@ pub fn button(ui: &mut Ui, label: &str, primary: bool, enabled: bool) -> Respons
|
||||
a11y::button(&resp, label);
|
||||
let hot = enabled && resp.hovered();
|
||||
let (fill, text) = match (primary, hot) {
|
||||
(true, false) => (t.accent, Color32::WHITE),
|
||||
(true, true) => (t.accent_hover, Color32::WHITE),
|
||||
(true, false) => (t.accent, t.accent_on),
|
||||
(true, true) => (t.accent_hover, t.accent_on),
|
||||
(false, false) => (Color32::TRANSPARENT, t.text),
|
||||
(false, true) => (t.hover, t.text),
|
||||
};
|
||||
@@ -1046,7 +1046,7 @@ pub fn text_field_inline(ui: &mut Ui, id: Id, value: &str, rect: Rect, font: egu
|
||||
match buf {
|
||||
Some(mut text) => {
|
||||
ui.painter().rect_filled(rect.expand(2.0), CornerRadius::same(4), t.panel);
|
||||
ui.painter().rect_stroke(rect.expand(2.0), CornerRadius::same(4), Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(rect.expand(2.0), CornerRadius::same(4), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect));
|
||||
let r = crate::bidi::single_line(&mut child, te_id, &mut text, font, rect.width());
|
||||
a11y::own_focus(&r);
|
||||
|
||||
Reference in New Issue
Block a user