refactor(ui): simplify panels and menus

This commit is contained in:
Dylan M. Taylor
2026-10-28 16:00:36 +00:00
parent e98b97de2b
commit 927ceb0638
23 changed files with 463 additions and 296 deletions
+7 -3
View File
@@ -27,7 +27,7 @@ pub use select_layer::open_select_layer;
use select_layer::{SELECT_LAYER, select_layer_ask_id, select_layer_menu};
#[cfg(test)]
pub(crate) use tree::{AIM_REST, HOVER_DELAY, heading_for, in_triangle};
use tree::{At, menu_frame, plain_row_hover, set_open, submenu_at};
use tree::{At, SubmenuOptions, menu_frame, plain_row_hover, set_open, submenu_at};
pub(crate) use tree::{forget_tree, item, separator, submenu};
/// Commands the UI layer implements itself (dialogs and platform actions).
@@ -403,8 +403,12 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
}
ui.add_space(6.0);
for top in TOP_MENUS {
if submenu_at(ui, Id::unique(("menu", *top)), top, tr(top), 12.0, false, &t, |ui| menu_level(app, ui, top).then_some(()))
.is_some()
if submenu_at(
ui,
SubmenuOptions { area: Id::unique(("menu", *top)), key: top, label: tr(top), gap: 12.0, constrain: false, tokens: &t },
|ui| menu_level(app, ui, top).then_some(()),
)
.is_some()
{
close = true;
}
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{FigApp, dev_mode_refuses, json, trn};
/// Run a menu item: some open dialogs or use platform services, the rest go to the engine.
/// Paste: the system clipboard wins when it holds something newer than our own copy (layers copied
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Align2, Color32, CornerRadius, FigApp, Id, MENU, Rect, SEP, Sense, Stroke, StrokeKind, Tokens, Ui, available, i18n, invoke, json, pos2,
pretty_shortcut, record_recent, regular, tr, vec2,
};
/// The Actions palette's tab.
#[derive(Clone, Copy, PartialEq, Eq, Default)]
+14 -10
View File
@@ -1,5 +1,5 @@
use super::tree::{indented_item, separator, submenu_at};
use super::*;
use super::tree::{SubmenuOptions, indented_item, separator, submenu_at};
use super::{FigApp, Id, Tokens, Ui, json, tr};
/// The context menu's Select layer submenu (its key in the context menu's tree).
pub(super) const SELECT_LAYER: &str = "Select layer";
@@ -35,15 +35,19 @@ pub(super) fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2,
return false;
}
let sel = ds.selection.clone();
let picked = submenu_at(ui, Id::unique("ctx_select_layer"), SELECT_LAYER, tr("Select layer"), 4.0, true, t, |ui| {
let mut picked = None;
for (id, depth, name) in &layers {
if indented_item(ui, name, "", true, sel.contains(id), 12.0 * (*depth).min(8) as f32, t) {
picked = Some(*id);
let picked = submenu_at(
ui,
SubmenuOptions { area: Id::unique("ctx_select_layer"), key: SELECT_LAYER, label: tr("Select layer"), gap: 4.0, constrain: true, tokens: t },
|ui| {
let mut picked = None;
for (id, depth, name) in &layers {
if indented_item(ui, name, "", true, sel.contains(id), 12.0 * (*depth).min(8) as f32, t) {
picked = Some(*id);
}
}
}
picked
});
picked
},
);
separator(ui);
match picked {
Some(id) => {
+13 -13
View File
@@ -1,10 +1,10 @@
use super::*;
use super::{Color32, CornerRadius, Icon, Id, Rect, Sense, Stroke, Tokens, Ui, a11y, i18n, icons, pos2, pretty_shortcut, regular, vec2};
/// A submenu row `label` in the menu being drawn; while it's open, `add` draws its items beside
/// it. `key` names the submenu (its menu path, unique in its menu tree). Returns what `add`
/// returned.
pub(crate) fn submenu<R>(ui: &mut Ui, key: &str, label: &str, t: &Tokens, add: impl FnOnce(&mut Ui) -> Option<R>) -> Option<R> {
submenu_at(ui, Id::unique(("submenu", key)), key, label, 4.0, false, t, add)
submenu_at(ui, SubmenuOptions { area: Id::unique(("submenu", key)), key, label, gap: 4.0, constrain: false, tokens: t }, add)
}
// ---------------------------------------------------------------------------------------------
@@ -240,17 +240,17 @@ pub(super) fn plain_row_hover(ui: &Ui, resp: &egui::Response) -> bool {
}
/// A submenu row whose panel (area `area`, `gap` right of the row) shows while it's open.
#[allow(clippy::too_many_arguments)]
pub(super) fn submenu_at<R>(
ui: &mut Ui,
area: Id,
key: &str,
label: &str,
gap: f32,
constrain: bool,
t: &Tokens,
add: impl FnOnce(&mut Ui) -> Option<R>,
) -> Option<R> {
pub(super) struct SubmenuOptions<'a> {
pub(super) area: Id,
pub(super) key: &'a str,
pub(super) label: &'a str,
pub(super) gap: f32,
pub(super) constrain: bool,
pub(super) tokens: &'a Tokens,
}
pub(super) fn submenu_at<R>(ui: &mut Ui, options: SubmenuOptions<'_>, add: impl FnOnce(&mut Ui) -> Option<R>) -> Option<R> {
let SubmenuOptions { area, key, label, gap, constrain, tokens: t } = options;
let at = level_at(ui);
let aim = aiming(ui);
let w = ui.available_width();
+1 -1
View File
@@ -11,7 +11,7 @@
//! (preferred ones first), select it, reset it to the main's default content.
use egui::{Id, Sense, Ui, vec2};
use figcraft_doc::*;
use figcraft_doc::{Document, Node, NodeId, NodeKind, PropDef, VariableType};
use figcraft_engine::cmd::component::{can_add_variant, can_combine_variants};
use figcraft_engine::components::{
PROP_BINDING, default_variant, inside_instance, owning_component, owning_instance, slot_def, slot_edited, slots_of, variant_conflicts,
+1 -1
View File
@@ -6,7 +6,7 @@
use std::collections::BTreeSet;
use egui::{Align2, Color32, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_doc::*;
use figcraft_doc::{Document, ExportFormat, ExportSetting, ExportSize, NodeId, NodeKind, PrintSettings, VdCompat};
use figcraft_engine::cmd::file::export_jobs;
use serde_json::{Value, json};
+273 -212
View File
@@ -728,6 +728,259 @@ fn search_field(ui: &mut Ui, t: &Tokens, w: f32, id: Id, hint: &str, text: &mut
}
}
/// Dragging near the list's top or bottom edge scrolls it, faster closer to the edge.
fn drag_autoscroll(ui: &mut Ui, dragging: bool) {
if dragging && let Some(p) = ui.input(|i| i.pointer.hover_pos()) {
const EDGE: f32 = 28.0;
let view = ui.clip_rect();
let dy = if p.y < view.min.y + EDGE {
(view.min.y + EDGE - p.y).min(EDGE)
} else if p.y > view.max.y - EDGE {
-(p.y - (view.max.y - EDGE)).min(EDGE)
} else {
0.0
};
if dy != 0.0 && view.x_range().contains(p.x) {
ui.scroll_with_delta(vec2(0.0, dy * 0.5));
ui.ctx().request_repaint();
}
}
}
/// A row's icon or name colour: the component tint (faded when hidden), else `normal` or `faded`.
fn row_color(component: bool, shown: bool, normal: Color32, faded: Color32) -> Color32 {
if component {
COMPONENT.gamma_multiply(if shown { 1.0 } else { 0.5 })
} else if shown {
normal
} else {
faded
}
}
/// The pointer `p` over row `rects` = [row, padded row]: the drop zone it shows (-1 above, 0 into,
/// 1 below), whether holding there should open the collapsed row, and whether the button was released.
fn drop_zone(ui: &mut Ui, p: egui::Pos2, rects: [Rect; 2], x0: f32, id: NodeId, collapsed: bool, n: &figcraft_doc::Node) -> (i8, bool, bool) {
let [r, rr] = rects;
let mut open = false;
let rel = (p.y - r.min.y) / r.height();
let into = n.kind.is_container() && !matches!(n.kind, NodeKind::Instance { .. }) && (0.25..0.75).contains(&rel);
let zone: i8 = if into {
0
} else if rel < 0.5 {
-1
} else {
1
};
let y = match zone {
-1 => r.min.y,
1 => r.max.y,
_ => r.center().y,
};
if zone == 0 {
ui.painter().rect_stroke(rr, CornerRadius::same(5), Stroke::new(2.0, ACCENT), egui::StrokeKind::Inside);
// Holding over a collapsed group or frame opens it, to drop deeper inside.
if collapsed {
open = hold_opens(ui, id);
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
}
} else {
crate::widgets::insertion_line(ui.painter(), x0, rr.max.x, y);
}
(zone, open, ui.input(|i| i.pointer.any_released()))
}
/// True once the pointer has stayed over row `id` for 0.6 s (the timer then restarts).
fn hold_opens(ui: &mut Ui, id: NodeId) -> bool {
let hold_id = Id::unique("layer_drag_hold");
let now = ui.input(|i| i.time);
match ui.data(|d| d.get_temp::<(NodeId, f64)>(hold_id)) {
Some((held, since)) if held == id => {
if now - since > 0.6 {
ui.data_mut(|d| d.remove::<(NodeId, f64)>(hold_id));
return true;
}
}
_ => {
ui.data_mut(|d| d.insert_temp(hold_id, (id, now)));
}
}
false
}
/// Applies a finished sweep across the eyes or padlocks, or keeps it going while the button is down.
fn finish_sweep(app: &mut FigApp, ui: &mut Ui, sw: Sweep) {
let (released, down) = ui.input(|i| (i.pointer.any_released(), i.pointer.primary_down()));
if released {
let (cmd, key) = if sw.lock { ("object.toggleLock", "locked") } else { ("object.toggleVisible", "visible") };
// A sweep started in the design can end after a switch to Dev Mode.
if !crate::menus::dev_mode_refuses(app, cmd) {
app.run(cmd, json!({"ids": sw.ids, key: if sw.lock { sw.on } else { !sw.on }}));
}
}
// A sweep whose release the panel never saw (it wasn't shown) is dropped.
if released || !down {
ui.data_mut(|d| d.remove::<Sweep>(sweep_id()));
} else {
ui.data_mut(|d| d.insert_temp(sweep_id(), sw));
}
}
/// A click on row `id`: plain selects it, Shift extends from the anchor, Cmd (or Shift without an
/// anchor) toggles it.
fn select_clicked(app: &mut FigApp, rows: &[Row], sel: &[NodeId], id: NodeId, m: egui::Modifiers) {
app.ui.layers_focus = true;
app.ui.pages_focus = None;
let order: Vec<NodeId> = rows.iter().map(|r| r.id).collect();
let pos = |x: NodeId| order.iter().position(|o| *o == x);
let anchor = app.ui.layer_anchor.filter(|a| sel.contains(a)).and_then(pos);
if m.shift
&& let (Some(ai), Some(bi)) = (anchor, pos(id))
{
// Shift: the visible rows from the anchor to here (added to the selection with Cmd).
let (lo, hi) = (ai.min(bi), ai.max(bi));
let mut s: Vec<NodeId> = if m.command { sel.to_vec() } else { vec![] };
for x in order.get(lo..=hi).unwrap_or_default() {
if !s.contains(x) {
s.push(*x);
}
}
app.run("select.set", json!({"ids": s}));
} else if m.shift || m.command {
let mut s = sel.to_vec();
if let Some(pos) = s.iter().position(|x| *x == id) {
s.remove(pos);
} else {
s.push(id);
}
app.ui.layer_anchor = Some(id);
app.run("select.set", json!({"ids": s}));
} else {
app.ui.layer_anchor = Some(id);
app.run("select.set", json!({"ids": [id]}));
}
// A click in the panel needs no reveal scroll.
app.ui.layers_seen_sel = app.session.selection();
if let Ok(ds) = app.session.ds_mut()
&& ds.text_edit.is_some()
{
let _ = figcraft_engine::textedit::end(&mut app.session);
}
}
/// Drops `moving` on `target` (`zone`: 0 into it, -1 above, 1 below), unless Dev Mode refuses
/// (a drag started in the design can end after a switch to it).
fn drop_layers(app: &mut FigApp, doc: &Document, moving: &[NodeId], target: NodeId, zone: i8) {
if crate::menus::dev_mode_refuses(app, "node.reparent") {
return;
}
let ids = moving.to_vec();
match zone {
0 => {
app.run("node.reparent", json!({"ids": ids, "parent": target}));
}
z => {
// Above in the list = higher z = after the target among siblings.
if let Some(parent) = doc.node(target).and_then(|n| n.parent) {
let idx = doc.index_in_parent(target).unwrap_or(0);
let gap = if z < 0 { idx + 1 } else { idx };
// `index` is the final position: the dragged layers below the gap leave it.
let below = doc.node(parent).map_or(0, |p| p.children.iter().take(gap).filter(|c| ids.contains(c)).count());
app.run("node.reparent", json!({"ids": ids, "parent": parent, "index": gap - below}));
}
}
}
}
/// The eye and padlock of a row: painted when hovered (`over`, false in Dev Mode) or set, and, outside
/// Dev Mode, clickable; a drag over them starts a sweep. Returns the layers whose eye / padlock was clicked.
fn eye_lock(
ui: &mut Ui,
t: &Tokens,
rects: [Rect; 2],
row: &Row,
n: &figcraft_doc::Node,
over: bool,
dev: bool,
sweep: &mut Option<Sweep>,
) -> (Option<NodeId>, Option<NodeId>) {
let [r, rr] = rects;
let (mut vis, mut lock) = (None, None);
// A sweep across the eyes or padlocks takes in the rows the pointer passes.
if let Some(sw) = sweep.as_mut()
&& !sw.ids.contains(&row.id)
&& ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| r.y_range().contains(p.y))
{
sw.ids.push(row.id);
}
let swept = |lock: bool| sweep.as_ref().filter(|sw| sw.lock == lock && sw.ids.contains(&row.id)).map(|sw| sw.on);
// Lock / eye on hover (or when set); a layer in a locked one shows a faint padlock.
let hidden = swept(false).unwrap_or(!n.visible);
let locked = swept(true).unwrap_or(n.locked);
// In Dev Mode they only show what is set.
let show_lock = over || locked || row.locked_above;
let show_eye = over || hidden;
let er = Rect::from_center_size(pos2(rr.max.x - 14.0, r.center().y), vec2(20.0, 20.0));
let lr = Rect::from_center_size(pos2(rr.max.x - 36.0, r.center().y), vec2(20.0, 20.0));
if show_eye {
icons::paint(ui.painter(), er.shrink(3.0), if hidden { Icon::EyeOff } else { Icon::Eye }, t.icon_secondary);
}
if show_eye && !dev {
let eresp = ui.interact(er, ui.scope_id().with(("eye", row.id)), Sense::click_and_drag());
a11y::toggle(&eresp, tr("Hidden"), hidden);
if eresp.clicked() {
vis = Some(row.id);
}
if eresp.drag_started() {
*sweep = Some(Sweep { lock: false, on: n.visible, ids: vec![row.id] });
}
}
if show_lock {
let col = if locked || !row.locked_above { t.icon_secondary } else { t.text_tertiary };
icons::paint(ui.painter(), lr.shrink(3.0), if locked || row.locked_above { Icon::Lock } else { Icon::Unlock }, col);
}
if show_lock && !dev {
let lresp = ui.interact(lr, ui.scope_id().with(("lock", row.id)), Sense::click_and_drag());
a11y::toggle(&lresp, tr("Locked"), locked || row.locked_above);
if lresp.clicked() {
lock = Some(row.id);
}
if lresp.drag_started() {
*sweep = Some(Sweep { lock: true, on: !n.locked, ids: vec![row.id] });
}
}
(vis, lock)
}
/// Expands the ancestors of a selection not seen yet (once, so manual collapsing sticks); whether it was new.
fn reveal_selection(app: &mut FigApp, doc: &Document, page: NodeId, sel: &[NodeId]) -> bool {
let reveal = sel != app.ui.layers_seen_sel;
if reveal {
for s in sel {
for a in doc.ancestors(*s) {
if a != page {
app.ui.expanded.insert(a);
}
}
}
app.ui.layers_seen_sel = sel.to_vec();
}
reveal
}
/// A row's background: selected, inside a selected layer, or outlined when hovered.
/// `state` is [selected, in_selected, component].
fn paint_row_bg(ui: &Ui, t: &Tokens, rr: Rect, state: [bool; 3], hovered: bool) {
let [selected, in_selected, is_comp] = state;
if selected {
ui.painter().rect_filled(rr, CornerRadius::same(5), if is_comp { t.component_row } else { t.selected_row });
} else if in_selected {
ui.painter().rect_filled(rr, CornerRadius::ZERO, t.selected_row_child);
} else if hovered {
ui.painter().rect_stroke(rr, CornerRadius::same(5), Stroke::new(1.0, if is_comp { COMPONENT } else { ACCENT }), egui::StrokeKind::Inside);
}
}
/// Draw the layer tree with search, cached rows, reveal-selection expansion and scrolling,
/// rename/Tab navigation, selection and drag/drop, sweep toggles and Dev Mode restrictions.
fn layers(app: &mut FigApp, ui: &mut Ui) {
@@ -761,17 +1014,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
let hover = ds.hover;
// Reveal a new selection: expand its ancestors and scroll its row into view, once, so
// "Collapse layers" and manual collapsing stick.
let reveal = sel != app.ui.layers_seen_sel;
if reveal {
for s in &sel {
for a in doc.ancestors(*s) {
if a != page {
app.ui.expanded.insert(a);
}
}
}
app.ui.layers_seen_sel = sel.clone();
}
let reveal = reveal_selection(app, &doc, page, &sel);
let filter = app.ui.layer_filter.trim().to_lowercase();
let rows = rows_for(ui, &doc, page, &app.ui.expanded, &filter);
let mut scroll_to = if reveal { sel.first().and_then(|s| rows.iter().position(|r| r.id == *s)) } else { None };
@@ -831,24 +1074,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
ui.add(egui::Label::new(egui::RichText::new(hint).font(regular(11.0)).color(t.text_secondary)).wrap());
});
}
// Dragging near the list's top or bottom edge scrolls it, faster closer to the edge.
if dragging.is_some()
&& let Some(p) = ui.input(|i| i.pointer.hover_pos())
{
const EDGE: f32 = 28.0;
let view = ui.clip_rect();
let dy = if p.y < view.min.y + EDGE {
(view.min.y + EDGE - p.y).min(EDGE)
} else if p.y > view.max.y - EDGE {
-(p.y - (view.max.y - EDGE)).min(EDGE)
} else {
0.0
};
if dy != 0.0 && view.x_range().contains(p.x) {
ui.scroll_with_delta(vec2(0.0, dy * 0.5));
ui.ctx().request_repaint();
}
}
drag_autoscroll(ui, dragging.is_some());
let first = ((viewport.min.y / pitch).floor().max(0.0) as usize).min(rows.len());
let last = ((viewport.max.y / pitch).ceil().max(0.0) as usize).saturating_add(1).min(rows.len());
let band = Rect::from_x_y_ranges(ui.max_rect().x_range(), (top + first as f32 * pitch)..=(top + last as f32 * pitch));
@@ -879,18 +1105,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
a11y::ring_around(ui, rr, &resp);
// Over the row, its eye and padlock included (they sit on top of it).
let over = resp.contains_pointer();
if selected {
ui.painter().rect_filled(rr, CornerRadius::same(5), if is_comp { t.component_row } else { t.selected_row });
} else if in_selected {
ui.painter().rect_filled(rr, CornerRadius::ZERO, t.selected_row_child);
} else if over || hover == Some(row.id) {
ui.painter().rect_stroke(
rr,
CornerRadius::same(5),
Stroke::new(1.0, if is_comp { COMPONENT } else { ACCENT }),
egui::StrokeKind::Inside,
);
}
paint_row_bg(ui, &t, rr, [selected, in_selected, is_comp], over || hover == Some(row.id));
if over {
hovered_row = Some(row.id);
}
@@ -912,13 +1127,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
}
let icon = node_icon(&doc, row.id);
let shown = n.visible && !row.dimmed;
let icon_col = if is_comp || icon == Icon::Slot {
COMPONENT.gamma_multiply(if shown { 1.0 } else { 0.5 })
} else if shown {
t.icon
} else {
t.text_tertiary
};
let icon_col = row_color(is_comp || icon == Icon::Slot, shown, t.icon, t.text_tertiary);
icons::paint(ui.painter(), Rect::from_center_size(pos2(x0 + 22.0, r.center().y), vec2(14.0, 14.0)), icon, icon_col);
let name_x = x0 + 36.0;
if app.ui.rename.is_some_and(|rn| rn.id == row.id) {
@@ -926,68 +1135,18 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
rename_done = Some((row.id, new));
}
} else {
let col = if is_comp {
COMPONENT.gamma_multiply(if shown { 1.0 } else { 0.5 })
} else if shown {
t.text
} else {
t.text_tertiary
};
let font = if doc.node(n.parent.unwrap_or(page)).is_some_and(|p| p.is_page())
&& (n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }))
{
semibold(11.0)
} else {
regular(11.0)
};
let col = row_color(is_comp, shown, t.text, t.text_tertiary);
let top_level = doc.node(n.parent.unwrap_or(page)).is_some_and(|p| p.is_page());
let font =
if top_level && (n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. })) { semibold(11.0) } else { regular(11.0) };
let name = if n.name.is_empty() { tr(n.kind.label()).to_string() } else { n.name.clone() };
// Room left of the lock and eye buttons.
let max_w = (rr.max.x - 56.0 - name_x).max(20.0);
crate::bidi::paint_name(ui.painter(), pos2(name_x, r.center().y), &name, font, col, max_w);
}
// A sweep across the eyes or padlocks takes in the rows the pointer passes.
if let Some(sw) = sweep.as_mut()
&& !sw.ids.contains(&row.id)
&& ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| r.y_range().contains(p.y))
{
sw.ids.push(row.id);
}
let swept = |lock: bool| sweep.as_ref().filter(|sw| sw.lock == lock && sw.ids.contains(&row.id)).map(|sw| sw.on);
// Lock / eye on hover (or when set); a layer in a locked one shows a faint padlock.
let hidden = swept(false).unwrap_or(!n.visible);
let locked = swept(true).unwrap_or(n.locked);
// In Dev Mode they only show what is set.
let show_lock = (over && !dev) || locked || row.locked_above;
let show_eye = (over && !dev) || hidden;
let er = Rect::from_center_size(pos2(rr.max.x - 14.0, r.center().y), vec2(20.0, 20.0));
let lr = Rect::from_center_size(pos2(rr.max.x - 36.0, r.center().y), vec2(20.0, 20.0));
if show_eye {
icons::paint(ui.painter(), er.shrink(3.0), if hidden { Icon::EyeOff } else { Icon::Eye }, t.icon_secondary);
}
if show_eye && !dev {
let eresp = ui.interact(er, ui.scope_id().with(("eye", row.id)), Sense::click_and_drag());
a11y::toggle(&eresp, tr("Hidden"), hidden);
if eresp.clicked() {
vis = Some(row.id);
}
if eresp.drag_started() {
sweep = Some(Sweep { lock: false, on: n.visible, ids: vec![row.id] });
}
}
if show_lock {
let col = if locked || !row.locked_above { t.icon_secondary } else { t.text_tertiary };
icons::paint(ui.painter(), lr.shrink(3.0), if locked || row.locked_above { Icon::Lock } else { Icon::Unlock }, col);
}
if show_lock && !dev {
let lresp = ui.interact(lr, ui.scope_id().with(("lock", row.id)), Sense::click_and_drag());
a11y::toggle(&lresp, tr("Locked"), locked || row.locked_above);
if lresp.clicked() {
lock = Some(row.id);
}
if lresp.drag_started() {
sweep = Some(Sweep { lock: true, on: !n.locked, ids: vec![row.id] });
}
}
let (v, l) = eye_lock(ui, &t, [r, rr], row, n, over && !dev, dev, &mut sweep);
vis = v.or(vis);
lock = l.or(lock);
if resp.clicked() {
clicked = Some((row.id, ui.input(|i| i.modifiers)));
}
@@ -1016,43 +1175,12 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
&& let Some(p) = ui.input(|i| i.pointer.hover_pos())
&& r.contains(p)
{
let rel = (p.y - r.min.y) / r.height();
let into = n.kind.is_container() && !matches!(n.kind, NodeKind::Instance { .. }) && (0.25..0.75).contains(&rel);
let zone: i8 = if into {
0
} else if rel < 0.5 {
-1
} else {
1
};
let y = match zone {
-1 => r.min.y,
1 => r.max.y,
_ => r.center().y,
};
if zone == 0 {
ui.painter().rect_stroke(rr, CornerRadius::same(5), Stroke::new(2.0, ACCENT), egui::StrokeKind::Inside);
// Holding over a collapsed group or frame opens it, to drop deeper inside.
if row.has_kids && !app.ui.expanded.contains(&row.id) {
let hold_id = Id::unique("layer_drag_hold");
let now = ui.input(|i| i.time);
match ui.data(|d| d.get_temp::<(NodeId, f64)>(hold_id)) {
Some((id, since)) if id == row.id => {
if now - since > 0.6 {
toggle = Some(row.id);
ui.data_mut(|d| d.remove::<(NodeId, f64)>(hold_id));
}
}
_ => {
ui.data_mut(|d| d.insert_temp(hold_id, (row.id, now)));
}
}
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
}
} else {
crate::widgets::insertion_line(ui.painter(), x0, rr.max.x, y);
let collapsed = row.has_kids && !app.ui.expanded.contains(&row.id);
let (zone, open, released) = drop_zone(ui, p, [r, rr], x0, row.id, collapsed, n);
if open {
toggle = Some(row.id);
}
if ui.input(|i| i.pointer.any_released()) {
if released {
drop = Some((row.id, zone));
}
}
@@ -1087,46 +1215,15 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
}
}
if let Some(sw) = sweep {
let (released, down) = ui.input(|i| (i.pointer.any_released(), i.pointer.primary_down()));
if released {
let (cmd, key) = if sw.lock { ("object.toggleLock", "locked") } else { ("object.toggleVisible", "visible") };
// A sweep started in the design can end after a switch to Dev Mode.
if !crate::menus::dev_mode_refuses(app, cmd) {
app.run(cmd, json!({"ids": sw.ids, key: if sw.lock { sw.on } else { !sw.on }}));
}
}
// A sweep whose release the panel never saw (it wasn't shown) is dropped.
if released || !down {
ui.data_mut(|d| d.remove::<Sweep>(sweep_id()));
} else {
ui.data_mut(|d| d.insert_temp(sweep_id(), sw));
}
finish_sweep(app, ui, sw);
}
if ui.input(|i| i.pointer.any_released()) {
ui.data_mut(|d| {
d.remove::<NodeId>(drag_id);
d.remove::<(NodeId, f64)>(Id::unique("layer_drag_hold"));
});
// A drag started in the design can end after a switch to Dev Mode.
if let (Some(_), Some((target, zone))) = (dragging, drop)
&& !crate::menus::dev_mode_refuses(app, "node.reparent")
{
let ids = moving.clone();
match zone {
0 => {
app.run("node.reparent", json!({"ids": ids, "parent": target}));
}
z => {
// Above in the list = higher z = after the target among siblings.
if let Some(parent) = doc.node(target).and_then(|n| n.parent) {
let idx = doc.index_in_parent(target).unwrap_or(0);
let gap = if z < 0 { idx + 1 } else { idx };
// `index` is the final position: the dragged layers below the gap leave it.
let below = doc.node(parent).map_or(0, |p| p.children.iter().take(gap).filter(|c| ids.contains(c)).count());
app.run("node.reparent", json!({"ids": ids, "parent": parent, "index": gap - below}));
}
}
}
if let (Some(_), Some((target, zone))) = (dragging, drop) {
drop_layers(app, &doc, &moving, target, zone);
}
}
if let Ok(ds) = app.session.ds_mut() {
@@ -1159,43 +1256,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
a11y::refocus_row(&mut app.ui.nav);
}
if let Some((id, m)) = clicked {
app.ui.layers_focus = true;
app.ui.pages_focus = None;
let order: Vec<NodeId> = rows.iter().map(|r| r.id).collect();
let pos = |x: NodeId| order.iter().position(|o| *o == x);
let anchor = app.ui.layer_anchor.filter(|a| sel.contains(a)).and_then(pos);
if m.shift
&& let (Some(ai), Some(bi)) = (anchor, pos(id))
{
// Shift: the visible rows from the anchor to here (added to the selection with Cmd).
let (lo, hi) = (ai.min(bi), ai.max(bi));
let mut s: Vec<NodeId> = if m.command { sel.clone() } else { vec![] };
for x in order.get(lo..=hi).unwrap_or_default() {
if !s.contains(x) {
s.push(*x);
}
}
app.run("select.set", json!({"ids": s}));
} else if m.shift || m.command {
let mut s = sel.clone();
if let Some(pos) = s.iter().position(|x| *x == id) {
s.remove(pos);
} else {
s.push(id);
}
app.ui.layer_anchor = Some(id);
app.run("select.set", json!({"ids": s}));
} else {
app.ui.layer_anchor = Some(id);
app.run("select.set", json!({"ids": [id]}));
}
// A click in the panel needs no reveal scroll.
app.ui.layers_seen_sel = app.session.selection();
if let Ok(ds) = app.session.ds_mut()
&& ds.text_edit.is_some()
{
let _ = figcraft_engine::textedit::end(&mut app.session);
}
select_clicked(app, &rows, &sel, id, m);
}
let _ = Color32::TRANSPARENT;
}
+1 -1
View File
@@ -5,7 +5,7 @@
//! commands.
use egui::{Align2, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_doc::*;
use figcraft_doc::{AutoLayout, CellAlign, ContentAlign, CrossAlign, Document, LayoutMode, MainAlign, Node, NodeId, NodeKind, Sizing};
use serde_json::json;
use crate::FigApp;
+47 -21
View File
@@ -165,7 +165,7 @@ pub fn action_list(app: &mut FigApp, ui: &mut Ui, cx: &Cx, list: &[Action], pref
for (i, a) in list.iter().enumerate() {
let path = [prefix, &[i]].concat();
let is_open = single || open.starts_with(&path);
let (header, extent) = action_item(app, ui, cx, a, path, list.len(), is_open, single);
let (header, extent) = action_item(app, ui, ActionItem { context: cx, action: a, path, count: list.len(), open: is_open, single });
if let Some(d) = &mut drag {
d.row(ui, i, header, extent, || action_summary(cx.doc, a));
}
@@ -222,8 +222,17 @@ fn add_action_row(app: &mut FigApp, ui: &mut Ui, cx: &Cx, path: Vec<usize>) {
/// One action: its kind (open) or a summary (closed) with a menu to move or remove it, and when
/// open its settings below. Returns its row and the area it covers with its settings.
#[allow(clippy::too_many_arguments)]
fn action_item(app: &mut FigApp, ui: &mut Ui, cx: &Cx, a: &Action, path: Vec<usize>, count: usize, open: bool, single: bool) -> (Rect, Rect) {
struct ActionItem<'context, 'document> {
context: &'context Cx<'document>,
action: &'context Action,
path: Vec<usize>,
count: usize,
open: bool,
single: bool,
}
fn action_item(app: &mut FigApp, ui: &mut Ui, item: ActionItem<'_, '_>) -> (Rect, Rect) {
let ActionItem { context: cx, action: a, path, count, open, single } = item;
let t = Tokens::get(ui.ctx());
let i = path.last().copied().unwrap_or(0);
let removable = path.len() > 1 || count > 1;
@@ -451,7 +460,17 @@ fn condition_editor(app: &mut FigApp, ui: &mut Ui, cx: &Cx, slot: &Slot, b: usiz
Some(s) => builder_row(app, ui, cx, slot, b, &s),
None => {
let id = slot.id("proto_cond_expr").with(b);
if let Some(e) = expr_field(ui, id, vars, Some(c), tr("e.g. count >= 3 and not done"), |e| e.check_condition(vars), None::<fn(&mut Ui)>) {
if let Some(e) = expr_field(
ui,
ExprInput {
id,
variables: vars,
current: Some(c),
placeholder: tr("e.g. count >= 3 and not done"),
check: |e: &Expr| e.check_condition(vars),
trailing: None::<fn(&mut Ui)>,
},
) {
set_condition(app, slot, b, &e);
}
}
@@ -575,14 +594,16 @@ fn set_variable_fields(app: &mut FigApp, ui: &mut Ui, cx: &Cx, slot: &Slot, vari
Some(e) => {
let edited = expr_field(
ui,
slot.id("proto_set_expr"),
vars,
Some(e),
tr("e.g. count + 1"),
|e| e.check_value(vars, var.kind),
Some(|ui: &mut Ui| {
toggled = toggle(ui);
}),
ExprInput {
id: slot.id("proto_set_expr"),
variables: vars,
current: Some(e),
placeholder: tr("e.g. count + 1"),
check: |e: &Expr| e.check_value(vars, var.kind),
trailing: Some(|ui: &mut Ui| {
toggled = toggle(ui);
}),
},
);
if let Some(e) = edited {
set(app, value.clone(), Some(e));
@@ -616,16 +637,21 @@ fn set_variable_fields(app: &mut FigApp, ui: &mut Ui, cx: &Cx, slot: &Slot, vari
/// An expression typed as text in a row (with a `trailing` button after the field): what `check`
/// accepts is returned when committed; otherwise the text stays and why shows under it.
#[allow(clippy::too_many_arguments)]
fn expr_field(
ui: &mut Ui,
struct ExprInput<'a, Check, Trailing> {
id: Id,
vars: &Variables,
cur: Option<&Expr>,
placeholder: &str,
check: impl Fn(&Expr) -> Result<(), String>,
trailing: Option<impl FnOnce(&mut Ui)>,
) -> Option<Expr> {
variables: &'a Variables,
current: Option<&'a Expr>,
placeholder: &'a str,
check: Check,
trailing: Option<Trailing>,
}
fn expr_field<Check, Trailing>(ui: &mut Ui, input: ExprInput<'_, Check, Trailing>) -> Option<Expr>
where
Check: Fn(&Expr) -> Result<(), String>,
Trailing: FnOnce(&mut Ui),
{
let ExprInput { id, variables: vars, current: cur, placeholder, check, trailing } = input;
let t = Tokens::get(ui.ctx());
let shown = cur.map(|e| e.display(vars)).unwrap_or_default();
let key = id.with("draft");
+55 -20
View File
@@ -8,7 +8,7 @@ use figcraft_doc::proto::{
Action, ConditionalBlock, Direction, Easing, GAMEPAD_BUTTONS, Interaction, KeyMods, Overflow, OverlayAnchor, OverlayPosition, ScrollPosition,
Transition, Trigger, is_gamepad_button,
};
use figcraft_doc::*;
use figcraft_doc::{Document, Node, NodeId, NodeKind, VariableValue, proto};
use serde_json::{Value, json};
use crate::FigApp;
@@ -640,15 +640,24 @@ fn interactions(app: &mut FigApp, ui: &mut Ui, doc: &Document, page: NodeId, nod
{
let panel_left = ui.clip_rect().min.x;
let y = anchor.map(|a| a.min.y).unwrap_or(ui.clip_rect().min.y + 80.0);
editor_popover(app, ui.ctx(), doc, page, id, k, it, pos2(panel_left - 8.0, y));
editor_popover(app, ui.ctx(), InteractionPopover { doc, page, node: id, index: k, interaction: it, anchor: pos2(panel_left - 8.0, y) });
} else if open.is_some_and(|(n, _)| n == id) {
open_editor(ui.ctx(), None);
}
}
/// The interaction editor, floating left of the panel (`at` is its top-right corner).
#[allow(clippy::too_many_arguments)]
fn editor_popover(app: &mut FigApp, ctx: &egui::Context, doc: &Document, page: NodeId, node: NodeId, k: usize, it: &Interaction, at: Pos2) {
struct InteractionPopover<'a> {
doc: &'a Document,
page: NodeId,
node: NodeId,
index: usize,
interaction: &'a Interaction,
anchor: Pos2,
}
fn editor_popover(app: &mut FigApp, ctx: &egui::Context, popover: InteractionPopover<'_>) {
let InteractionPopover { doc, page, node, index: k, interaction: it, anchor: at } = popover;
let t = Tokens::get(ctx);
let width = 280.0;
let screen = ctx.content_rect();
@@ -832,17 +841,20 @@ fn trigger_rows(app: &mut FigApp, ui: &mut Ui, node: NodeId, k: usize, it: &Inte
const PAD_MENU_W: f32 = 92.0;
/// A destination picker for the action at `slot`: `make` builds the action for the destination.
#[allow(clippy::too_many_arguments)]
fn dest_dropdown(
app: &mut FigApp,
ui: &mut Ui,
doc: &Document,
options: &[NodeId],
cur: Option<NodeId>,
label: &str,
make: impl Fn(NodeId) -> Action,
slot: &Slot,
) {
struct DestinationPicker<'a, Make> {
doc: &'a Document,
options: &'a [NodeId],
current: Option<NodeId>,
label: &'a str,
make: Make,
slot: &'a Slot,
}
fn dest_dropdown<Make>(app: &mut FigApp, ui: &mut Ui, picker: DestinationPicker<'_, Make>)
where
Make: Fn(NodeId) -> Action,
{
let DestinationPicker { doc, options, current: cur, label, make, slot } = picker;
widgets::sublabels(ui, label, None);
row(ui, 32.0, |ui| {
let text = cur.map(|d| name_of(doc, d)).unwrap_or_else(|| tr("None").into());
@@ -871,22 +883,45 @@ pub(crate) fn action_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, page:
Action::Navigate { dest, preserve_scroll } => {
let (dest, preserve_scroll) = (*dest, *preserve_scroll);
let mk = |d| Action::Navigate { dest: d, preserve_scroll };
dest_dropdown(app, ui, doc, &nav_targets(doc, page), Some(dest), tr("Destination"), mk, slot);
dest_dropdown(
app,
ui,
DestinationPicker { doc, options: &nav_targets(doc, page), current: Some(dest), label: tr("Destination"), make: mk, slot },
);
row(ui, 28.0, |ui| {
if widgets::checkbox(ui, preserve_scroll, tr("Preserve scroll position")) {
set_action(app, slot, Action::Navigate { dest, preserve_scroll: !preserve_scroll });
}
});
}
Action::SwapOverlay { dest } => dest_dropdown(app, ui, doc, &frames, Some(*dest), tr("Overlay"), |d| Action::SwapOverlay { dest: d }, slot),
Action::SwapOverlay { dest } => dest_dropdown(
app,
ui,
DestinationPicker { doc, options: &frames, current: Some(*dest), label: tr("Overlay"), make: |d| Action::SwapOverlay { dest: d }, slot },
),
Action::ChangeTo { dest } => {
let vs = variants_for(doc, node);
dest_dropdown(app, ui, doc, &vs, Some(*dest), tr("Variant"), |d| Action::ChangeTo { dest: d }, slot);
dest_dropdown(
app,
ui,
DestinationPicker { doc, options: &vs, current: Some(*dest), label: tr("Variant"), make: |d| Action::ChangeTo { dest: d }, slot },
);
}
Action::ScrollTo { dest, offset } => {
let (dest, offset) = (*dest, *offset);
let layers: Vec<NodeId> = doc.top_level_frame(node).map(|f| doc.subtree(f).into_iter().skip(1).take(400).collect()).unwrap_or_default();
dest_dropdown(app, ui, doc, &layers, Some(dest), tr("Scroll to layer"), move |d| Action::ScrollTo { dest: d, offset }, slot);
dest_dropdown(
app,
ui,
DestinationPicker {
doc,
options: &layers,
current: Some(dest),
label: tr("Scroll to layer"),
make: move |d| Action::ScrollTo { dest: d, offset },
slot,
},
);
// Where the layer ends up from the top-left of what scrolls (room for a fixed header).
widgets::sublabels(ui, tr("Offset"), None);
row(ui, 32.0, |ui| {
@@ -920,7 +955,7 @@ pub(crate) fn action_fields(app: &mut FigApp, ui: &mut Ui, doc: &Document, page:
Action::OpenOverlay { dest, position, close_on_outside, background, offset, anchor } => {
let (dest, position, close_on_outside, background, offset, anchor) = (*dest, *position, *close_on_outside, *background, *offset, *anchor);
let mk = move |d: NodeId| Action::OpenOverlay { dest: d, position, close_on_outside, background, offset, anchor };
dest_dropdown(app, ui, doc, &frames, Some(dest), tr("Overlay"), mk, slot);
dest_dropdown(app, ui, DestinationPicker { doc, options: &frames, current: Some(dest), label: tr("Overlay"), make: mk, slot });
widgets::sublabels(ui, tr("Overlay position"), None);
row(ui, 32.0, |ui| {
let resp = widgets::dropdown(ui, tr(position.label()), ui.available_width(), None);
+5 -1
View File
@@ -2,7 +2,11 @@
use egui::{Align2, Color32, CornerRadius, Id, Pos2, Rect, Sense, Stroke, Ui, pos2, vec2};
use figcraft_color::{BlendMode, Paint, PaintKind};
use figcraft_doc::*;
use figcraft_doc::{
Blur, BlurType, BoolOp, Brush, Document, Effect, EffectKind, ExportFormat, ExportSetting, Glass, GridAlign, GridKind, LayoutGrid,
MAX_BLUR_RADIUS, MaskType, Node, NodeId, NodeKind, Noise, NoiseMode, Shadow, StrokeAlign, StrokeCap, StrokeJoin, StrokeProps, Texture,
VariableType, WidthPoint, WidthPreset, WidthProfile,
};
use figcraft_engine::cmd::lists::{moved_index, removed_index};
use serde_json::{Value, json};
+5 -1
View File
@@ -1,5 +1,9 @@
use super::menu::{context_popup, menu_check, menu_item, menu_popup, row, row_context_menu, row_rect};
use super::*;
use super::{
Blur, BlurType, Document, Effect, EffectKind, FieldEdit, FigApp, Glass, GridAlign, GridKind, Icon, Id, LayoutGrid, Lead, ListDrag,
MAX_BLUR_RADIUS, NodeId, Noise, NoiseMode, PAD, Paint, Sense, Shadow, StyleSlot, Texture, Ui, VariableType, common, icons, json, move_flags,
moved_index, remove_flags, removed_index, style_picker, tr, trf, trn, vec2, widgets,
};
/// Draw the effect list with add/style/type/toggle/remove/reorder, open settings, and keep an
/// open picker and settings flags aligned when rows move or are removed.
+4 -1
View File
@@ -1,5 +1,8 @@
use super::menu::{menu_item, menu_popup, row};
use super::*;
use super::{
Align2, Color32, CornerRadius, Document, ExportFormat, ExportSetting, FigApp, Icon, Id, NodeId, PAD, Paint, Rect, Sense, Stroke, Tip, Tokens, Ui,
a11y, icons, pos2, regular, semibold, tr, trf, vec2, widgets,
};
/// One group of inspected properties: label, value (with a swatch for colours) and the style or
/// variable it comes from. Clicking a row copies its value.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{BlendMode, Color32, CornerRadius, FIELD_H, Id, PAD, Rect, Sense, Tokens, Ui, a11y, pos2, regular, tr, vec2};
/// The smallest width a popup menu is measured in (see [`menu_popup`]).
const MENU_MIN_W: f32 = 48.0;
+5 -1
View File
@@ -1,6 +1,10 @@
use super::menu::{blend_menu, context_popup, menu_check, menu_item, menu_popup, row, row_context_menu, row_rect};
use super::stroke::stroke_settings;
use super::*;
use super::{
Align2, BlendMode, CornerRadius, Document, FIELD_H, FieldEdit, FigApp, Icon, Id, Lead, ListDrag, MaskType, Node, NodeId, NodeKind, PAD, Paint,
PaintKind, Rect, Sense, Stroke, StrokeAlign, StyleSlot, Tip, Tokens, Ui, Value, VariableType, common, icons, json, moved_index, pos2, regular,
removed_index, style_picker, tr, trf, vec2, widgets,
};
/// Draw visibility/blend/mask, opacity, uniform or per-corner radius, corner smoothing and
/// the blend-mode row.
+4 -1
View File
@@ -1,5 +1,8 @@
use super::menu::{menu_item, menu_item_with, menu_popup, row};
use super::*;
use super::{
Brush, Color32, Document, FigApp, Icon, Id, Lead, NodeId, NodeKind, PAD, Pos2, Rect, Stroke, StrokeAlign, StrokeCap, StrokeJoin, StrokeProps,
Tokens, Ui, Value, WidthPoint, WidthPreset, WidthProfile, a11y, common, icons, json, pos2, tr, widgets,
};
/// A width profile drawn as a filled band across `rect` (thickest point = full height); `None`
/// draws a uniform stroke.
+4 -1
View File
@@ -8,7 +8,10 @@
use std::sync::Arc;
use egui::{Align2, Color32, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_doc::*;
use figcraft_doc::{
DecorationLength, Document, LeadingTrim, LetterSpacing, LineHeight, ListStyle, NodeId, NodeKind, TextAlignH, TextAlignV, TextAutoResize,
TextCase, TextData, TextDecoration, TextStyle, VariableType, trim_num,
};
use serde_json::{Value, json};
use crate::FigApp;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::fields::menu_item_hover;
use super::*;
use super::{
Color32, CornerRadius, FIELD_H, FieldEdit, FigApp, Id, Lead, MIXED, NodeId, Rect, Sense, Stroke, TextStyle, Tokens, Ui, Value, commit_text, json,
pos2, regular, text_style_params, tr, trf, trim_num, uniform, vec2, widgets,
};
/// Type settings ▸ Variable: the font's named instances, then a slider and a field per visible
/// axis (values the style name implies show until changed; "Mixed" when the shown styles differ).
@@ -2,7 +2,11 @@ use super::axes::variable_axes;
use super::glyphs::{LeadGlyph, caption, feature_row, text_seg};
use super::picker::{font_picker, remember_family};
use super::spacing::{SpacingField, commit_spacing, parse_letter_spacing, parse_line_height, spacing_field, spacing_input};
use super::*;
use super::{
Arc, DecorationLength, Document, FEATURES, FIELD_H, FieldEdit, FigApp, Icon, Id, Lead, LeadingTrim, ListStyle, MIXED, NodeId, PAD, SIZES, Sense,
TABS, TextAlignH, TextAlignV, TextAutoResize, TextCase, TextData, TextDecoration, TextStyle, Tokens, Ui, Value, VariableType, common, field_row,
icons, json, live, menu_item, menu_popup, regular, row, semibold, text_style_params, tr, tr_noop, trf, trim_num, uniform, vec2, widgets,
};
/// The font rows: family, style and size, line height and letter spacing.
pub(super) fn font_rows(
@@ -4,7 +4,7 @@ use super::axes::round_axis;
use super::picker::{FontPick, font_picker};
#[cfg(test)]
use super::spacing::{parse_letter_spacing, parse_line_height};
use super::*;
use super::{Align2, Arc, Color32, CornerRadius, FIELD_H, Icon, Id, Rect, Sense, Stroke, StrokeKind, Tokens, Ui, icons, pos2, regular, tr, vec2};
pub(super) fn caption(ui: &mut Ui, s: &str) {
let t = Tokens::get(ui.ctx());
@@ -147,6 +147,9 @@ pub(super) fn paint_lead(p: &egui::Painter, r: Rect, g: LeadGlyph, col: Color32)
#[cfg(test)]
mod tests {
use super::*;
use crate::panels::typography::{target_styles, uniform};
use figcraft_doc::{Document, LetterSpacing, LineHeight, Node, NodeKind, TextData, TextStyle};
use serde_json::json;
#[test]
fn small_caps_sample_draws_only_letters_the_ui_font_has() {
@@ -1,4 +1,4 @@
use super::*;
use super::{Align2, CornerRadius, Icon, Id, Rect, Sense, Tokens, Ui, icons, pos2, regular, semibold, tr, vec2, widgets};
// ---- Font picker ----
@@ -1,5 +1,8 @@
use super::glyphs::{LeadGlyph, paint_lead};
use super::*;
use super::{
Align2, CornerRadius, FIELD_H, FieldEdit, FigApp, Id, LetterSpacing, LineHeight, MIXED, NodeId, Rect, Sense, Stroke, StrokeKind, Tokens, Ui,
Value, VariableType, commit_text, json, live, pos2, regular, tr, trim_num, vec2, widgets,
};
/// Line height, letter spacing or paragraph spacing: property, field id, glyph and tooltip.
pub(super) struct SpacingField {