mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 13:56:54 -06:00
refactor(ui): simplify panels and menus
This commit is contained in:
@@ -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,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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user