Guides: ruler guides as layer objects

A ruler guide goes on the current layer (guide.add {layer}), shows,
hides, locks and is deleted with its layer, is listed in the Layers panel
as a row that selects it, and moves to another layer (guide.setLayer).
Guides on a hidden layer neither show nor snap; those on a locked layer
can't be picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
redrun6767 2026-10-09 19:22:52 +00:00 committed by bflatastic
parent d7339804e6
commit 02e93902b3
22 changed files with 230 additions and 18 deletions

View File

@ -402,12 +402,17 @@ pub struct Guide {
/// and moves, is copied and is deleted with it. None: a canvas guide, across the whole canvas.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artboard: Option<u32>,
/// The layer (or sublayer) it is on: it shows, hides and locks with that layer and is listed
/// in it in the Layers panel, and goes when the layer is deleted. None: on no layer (guides
/// from before guides had layers, and from files that don't say), always there.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub layer: Option<NodeId>,
}
impl Guide {
/// A canvas guide.
pub fn new(vertical: bool, pos: f64) -> Self {
Self { vertical, pos, artboard: None }
Self { vertical, pos, artboard: None, layer: None }
}
/// Moved by `d` (a vertical guide across, a horizontal one down).
@ -990,6 +995,19 @@ impl Document {
let r = self.artboards.iter().find(|a| Some(a.id) == g.artboard)?.rect;
Some(if g.vertical { (r.y0, r.y1) } else { (r.x0, r.x1) })
}
/// Is ruler guide `g` shown: on no layer, or on a layer that (with the layers around it) is
/// visible? A guide whose layer is gone shows as one on no layer.
pub fn guide_shown(&self, g: &Guide) -> bool {
g.layer.is_none_or(|l| self.node(l).is_none() || self.is_visible(l))
}
/// Can ruler guide `g` be picked, moved and deleted: shown, and its layer unlocked?
pub fn guide_editable(&self, g: &Guide) -> bool {
g.layer.is_none_or(|l| self.node(l).is_none() || self.is_editable(l))
}
/// The ruler guides on layer `layer` (not its sublayers'), by index.
pub fn guides_on(&self, layer: NodeId) -> impl Iterator<Item = (usize, &Guide)> + '_ {
self.guides.iter().enumerate().filter(move |(_, g)| g.layer == Some(layer))
}
/// Does ruler guide `g` run past `p` (up to `tol` beyond its ends)?
pub fn guide_passes(&self, g: &Guide, p: Point, tol: f64) -> bool {
let along = if g.vertical { p.y } else { p.x };

View File

@ -206,6 +206,24 @@ mod tests {
assert_eq!(s.guides, vec![0]);
}
#[test]
fn guides_show_and_lock_with_their_layer() {
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
let g = crate::Guide { layer: Some(layer), ..crate::Guide::new(true, 10.0) };
assert!(d.guide_shown(&g) && d.guide_editable(&g));
d.node_mut(layer).unwrap().locked = true;
assert!(d.guide_shown(&g) && !d.guide_editable(&g), "locked: shown, not editable");
d.node_mut(layer).unwrap().visible = false;
assert!(!d.guide_shown(&g) && !d.guide_editable(&g), "hidden");
// On no layer, or a layer that's gone: always there.
assert!(d.guide_shown(&crate::Guide::new(false, 5.0)));
let gone = crate::Guide { layer: Some(NodeId(9999)), ..crate::Guide::new(true, 1.0) };
assert!(d.guide_shown(&gone) && d.guide_editable(&gone));
d.guides.push(g);
assert_eq!(d.guides_on(layer).count(), 1);
}
#[test]
fn selection_changes_clear_the_target() {
let mut s = Selection::default();

View File

@ -160,7 +160,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Add Guide",
[],
None,
"{vertical: bool, pos: pt (x for vertical, y for horizontal), artboard?: index (an artboard guide: it runs across that artboard only and moves, is copied and is deleted with it; default a canvas guide, across the whole canvas)} → {index}",
"{vertical: bool, pos: pt (x for vertical, y for horizontal), artboard?: index (an artboard guide: it runs across that artboard only and moves, is copied and is deleted with it; default a canvas guide, across the whole canvas), layer?: id (the layer it is on, shown, hidden, locked and deleted with it; default the current layer)} → {index}",
has_doc,
guide_add
),
@ -169,7 +169,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Guides",
[],
None,
"{} → [{index, vertical, pos, selected, artboard?: index (an artboard guide's)}…] the ruler guides",
"{} → [{index, vertical, pos, selected, artboard?: index (an artboard guide's), layer?: id (the layer it is on), shown, editable (its layer shown and unlocked)}…] the ruler guides",
has_doc,
guide_list
),
@ -200,6 +200,15 @@ pub fn specs() -> Vec<CommandSpec> {
guides_unlocked,
guide_move
),
cmd!(
"guide.setLayer",
"Move Guide to Layer",
[],
None,
"{index?, layer: id} put ruler guide `index`, else the selected guides, on layer `layer` (a layer or sublayer), as for art moved to another layer → {count}",
guides_unlocked,
guide_set_layer
),
cmd!("file.closeAll", "Close All", ["File"], Some("Cmd+Alt+W"), "{} → {closed}", has_doc, close_all),
cmd!(
"file.documentColorMode",
@ -507,8 +516,13 @@ fn guide_add(s: &mut Session, p: &Value) -> Result<Value> {
}
None => None,
};
let layer = match id_param(p, "layer") {
Some(l) if s.doc()?.doc.node(l).is_some_and(Node::is_layer) => Some(l),
Some(_) => return Err(bad("guide.add", "layer must name a layer or sublayer")),
None => s.doc()?.current_layer(),
};
let i = s.edit("New Guide", |d, _| {
d.guides.push(Guide { artboard, ..Guide::new(vertical, pos) });
d.guides.push(Guide { artboard, layer, ..Guide::new(vertical, pos) });
Ok(d.guides.len() - 1)
})?;
Ok(json!({ "index": i }))
@ -521,6 +535,11 @@ fn guide_list(s: &mut Session, _: &Value) -> Result<Value> {
if let Some(ab) = st.doc.artboards.iter().position(|a| Some(a.id) == g.artboard) {
r["artboard"] = json!(ab);
}
if let Some(l) = g.layer {
r["layer"] = json!(l.0);
}
r["shown"] = json!(st.doc.guide_shown(g));
r["editable"] = json!(st.doc.guide_editable(g));
r
};
Ok(Value::Array(st.doc.guides.iter().enumerate().map(row).collect()))
@ -568,6 +587,28 @@ pub(crate) fn guide_remove(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "count": gone.len() }))
}
/// Put ruler guide `index`, else the selected guides, on layer `layer`, in one undo step.
fn guide_set_layer(s: &mut Session, p: &Value) -> Result<Value> {
let layer = id_param(p, "layer").filter(|l| s.doc().is_ok_and(|st| st.doc.node(*l).is_some_and(Node::is_layer)));
let layer = layer.ok_or_else(|| bad("guide.setLayer", "layer must name a layer or sublayer"))?;
let moving: Vec<usize> = match p.get("index") {
Some(v) => vec![guide_index(s, Some(v), "guide.setLayer")?],
None => s.doc()?.selection.guides.clone(),
};
if moving.is_empty() {
return Err(bad("guide.setLayer", "no guide selected (or give an index)"));
}
s.edit("Move Guide to Layer", |d, _| {
for i in &moving {
if let Some(g) = d.guides.get_mut(*i) {
g.layer = Some(layer);
}
}
Ok(())
})?;
Ok(json!({ "count": moving.len() }))
}
/// Put ruler guide `index` at `pos`, else move (or copy) the selected guides, in one undo step.
pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = p.get("index") {

View File

@ -462,6 +462,9 @@ fn delete_rows(s: &mut Session, p: &Value) -> Result<Value> {
for id in &ids {
d.remove(*id)?;
}
// The guides on the deleted layers go with them.
let orphans: Vec<usize> = d.guides.iter().enumerate().filter(|(_, g)| g.layer.is_some_and(|l| d.node(l).is_none())).map(|(i, _)| i).collect();
d.retain_guides(sel, |i, _| !orphans.contains(&i));
sel.prune(d);
Ok(())
})?;

View File

@ -785,7 +785,11 @@ fn ruler_guides_select_move_and_delete() {
assert_eq!(s.execute("guide.select", &json!({"indexes": [0]})).unwrap()["selected"], json!([0]));
assert!(selected(&s).is_empty());
assert_eq!(s.execute("guide.select", &json!({"indexes": [1], "toggle": true})).unwrap()["selected"], json!([0, 1]));
assert_eq!(s.execute("guide.list", &json!({})).unwrap()[1], json!({"index": 1, "vertical": false, "pos": 50.0, "selected": true}));
let layer = s.doc().unwrap().current_layer().unwrap().0;
assert_eq!(
s.execute("guide.list", &json!({})).unwrap()[1],
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": true, "layer": layer, "shown": true, "editable": true})
);
assert!(s.execute("guide.select", &json!({"indexes": [7]})).is_err());
// A move takes each selected guide along its own axis, in one undo step.
let n = undo_len(&s);
@ -866,11 +870,12 @@ fn artboard_guides_go_with_their_artboard() {
s.execute("guide.add", &json!({"vertical": true, "pos": 1000, "artboard": 1})).unwrap();
s.execute("guide.add", &json!({"vertical": false, "pos": 100})).unwrap();
let list = |s: &mut Session| s.execute("guide.list", &json!({})).unwrap();
let layer = s.doc().unwrap().current_layer().unwrap().0;
assert_eq!(
list(&mut s),
json!([
{"index": 0, "vertical": true, "pos": 1000.0, "selected": false, "artboard": 1},
{"index": 1, "vertical": false, "pos": 100.0, "selected": false},
{"index": 0, "vertical": true, "pos": 1000.0, "selected": false, "artboard": 1, "layer": layer, "shown": true, "editable": true},
{"index": 1, "vertical": false, "pos": 100.0, "selected": false, "layer": layer, "shown": true, "editable": true},
])
);
let spans = |s: &Session| {
@ -951,12 +956,44 @@ fn guides_dragged_out_of_a_ruler_snap_and_take_the_artboard_tool_s_artboard() {
s.set_tool_option("active", &json!(1));
s.ruler_guide(false, &ev(PointerKind::Drag, 1000.0, 50.0), true, v).unwrap();
s.ruler_guide(false, &ev(PointerKind::Up, 1000.0, 50.0), true, v).unwrap();
let layer = s.doc().unwrap().current_layer().unwrap().0;
assert_eq!(
s.execute("guide.list", &json!({})).unwrap()[1],
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": false, "artboard": 1})
json!({"index": 1, "vertical": false, "pos": 50.0, "selected": false, "artboard": 1, "layer": layer, "shown": true, "editable": true})
);
}
/// Ruler guides are layer objects: made on the current layer, hidden, locked and deleted with it,
/// and moved to another layer.
#[test]
fn ruler_guides_live_on_layers() {
let mut s = session();
let first = s.doc().unwrap().current_layer().unwrap();
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
assert_eq!(s.doc().unwrap().doc.guides[0].layer, Some(first), "on the current layer");
let second = id_of(&s.execute("layer.new", &json!({"name": "Guides"})).unwrap());
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(second), "the new layer is current");
assert!(s.execute("guide.add", &json!({"vertical": true, "pos": 1, "layer": 99999})).is_err());
// Hidden or locked with its layer.
s.execute("layer.setProps", &json!({"ids": [second.0], "visible": false})).unwrap();
assert_eq!(s.execute("guide.list", &json!({})).unwrap()[1]["shown"], false);
s.execute("layer.setProps", &json!({"ids": [second.0], "visible": true, "locked": true})).unwrap();
let row = s.execute("guide.list", &json!({})).unwrap()[1].clone();
assert_eq!((row["shown"].clone(), row["editable"].clone()), (json!(true), json!(false)));
s.execute("layer.setProps", &json!({"ids": [second.0], "locked": false})).unwrap();
// Moved to the first layer, then back.
s.execute("guide.setLayer", &json!({"index": 1, "layer": first.0})).unwrap();
assert_eq!(s.doc().unwrap().doc.guides[1].layer, Some(first));
s.execute("guide.setLayer", &json!({"index": 1, "layer": second.0})).unwrap();
assert!(s.execute("guide.setLayer", &json!({"index": 1, "layer": 99999})).is_err());
// Deleting a layer deletes its guides; undo brings them back.
s.execute("layer.delete", &json!({"ids": [second.0]})).unwrap();
assert_eq!(guides(&s), [(true, 200.0)]);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(guides(&s).len(), 2);
}
/// #451: art moved with the Selection tool lands flush on the artboard's edges and centre (Smart
/// Guides).
#[test]

View File

@ -372,7 +372,13 @@ impl Targets {
/// tools): they pull a point into line where they run.
fn with_rulers(mut self, cx: &ToolContext) -> Self {
if cx.guides {
self.rulers = cx.doc.guides.iter().map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) }).collect();
self.rulers = cx
.doc
.guides
.iter()
.filter(|g| cx.doc.guide_shown(g))
.map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) })
.collect();
}
self
}

View File

@ -5,7 +5,8 @@
//! selected guides (Alt copies them). A dragged guide snaps ([`GuideSnap`]): with Shift to the
//! ruler's ticks, else to whole pixels, the grid, with Smart Guides the art's and the artboards'
//! edges, centres and anchors, or with Snap to Point the anchors. Dropped off the canvas onto its
//! ruler, a moved guide is deleted. Hidden or locked guides (View › Guides) can't be picked.
//! ruler, a moved guide is deleted. Hidden or locked guides (View › Guides), and guides on a hidden
//! or locked layer, can't be picked.
use serde_json::json;
use vectorcraft_geom::Point;
@ -24,7 +25,7 @@ pub fn guide_at(cx: &ToolContext, p: Point) -> Option<usize> {
.guides
.iter()
.enumerate()
.filter(|(_, g)| cx.doc.guide_passes(g, p, tol))
.filter(|(_, g)| cx.doc.guide_editable(g) && cx.doc.guide_passes(g, p, tol))
.map(|(i, g)| (i, off(g.pos, g.vertical)))
.filter(|(_, d)| *d <= tol)
.min_by(|a, b| a.1.total_cmp(&b.1).then(b.0.cmp(&a.0)))

View File

@ -43,7 +43,7 @@ fn snap(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
cx.doc
.guides
.iter()
.filter(|g| g.vertical == vertical && (g.pos - v).abs() <= tol && cx.doc.guide_passes(g, p, tol))
.filter(|g| g.vertical == vertical && (g.pos - v).abs() <= tol && cx.doc.guide_shown(g) && cx.doc.guide_passes(g, p, tol))
.min_by(|a, b| (a.pos - v).abs().total_cmp(&(b.pos - v).abs()))
};
if let Some(g) = nearest(true, p.x) {

View File

@ -378,7 +378,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
// Selected guides in the selection colour.
let picked = LineLook { color: t.selection, ..look };
let selected: &[usize] = app.session.active().map_or(&[], |st| &st.selection.guides);
for (i, g) in doc.guides.iter().enumerate() {
// Guides on a hidden layer hide with it.
for (i, g) in doc.guides.iter().enumerate().filter(|(_, g)| doc.guide_shown(g)) {
let at = |along: f64| xf.to_screen(if g.vertical { Point::new(g.pos, along) } else { Point::new(along, g.pos) });
// An artboard guide runs across its artboard, a canvas guide across the window.
let (a, b) = match doc.guide_span(g) {

View File

@ -732,3 +732,5 @@ lighting Light Světlo
Spatter… Rozstřik…
Sprayed Strokes… Stříkané tahy…
Sumi-e… Sumi-e…
Vertical Guide Svislé vodítko
Horizontal Guide Vodorovné vodítko

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -362,6 +362,9 @@
Position Panel Above Text Bedienfeld über dem Text positionieren
Clear Appearance Aussehen löschen
Clear Guides Hilfslinien löschen
Vertical Guide Vertikale Hilfslinie
Horizontal Guide Horizontale Hilfslinie
Move Guide to Layer Hilfslinie auf Ebene verschieben
Clear Overrides Abweichungen löschen
Clear Recent Files Liste der zuletzt geöffneten Dateien leeren
Click Klicken

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4097,3 +4097,6 @@ lighting Light Luz
Stroke Pressure Presión de pincelada
Right Diagonal Diagonal derecha
Left Diagonal Diagonal izquierda
Vertical Guide Guía vertical
Horizontal Guide Guía horizontal
Move Guide to Layer Mover guía a capa

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4107,3 +4107,6 @@ lighting Light Lumière
Stroke Pressure Pression des traits
Right Diagonal Diagonale droite
Left Diagonal Diagonale gauche
Vertical Guide Repère vertical
Horizontal Guide Repère horizontal
Move Guide to Layer Déplacer le repère vers le calque

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4099,3 +4099,6 @@ lighting Light Luce
Stroke Pressure Pressione pennellata
Right Diagonal Diagonale destra
Left Diagonal Diagonale sinistra
Vertical Guide Guida verticale
Horizontal Guide Guida orizzontale
Move Guide to Layer Sposta guida nel livello

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4092,3 +4092,6 @@ lighting Light ライト
Stroke Pressure ストロークの筆圧
Right Diagonal 右斜め
Left Diagonal 左斜め
Vertical Guide 垂直ガイド
Horizontal Guide 水平ガイド
Move Guide to Layer ガイドをレイヤーへ移動

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -2790,3 +2790,5 @@ lighting Light Luz
Spatter… Respingo…
Sprayed Strokes… Traços Borrifados…
Sumi-e… Sumi-e…
Vertical Guide Guia vertical
Horizontal Guide Guia horizontal

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -4095,3 +4095,6 @@ lighting Light Свет
Stroke Pressure Нажим мазка
Right Diagonal Диагональ вправо
Left Diagonal Диагональ влево
Vertical Guide Вертикальная направляющая
Horizontal Guide Горизонтальная направляющая
Move Guide to Layer Переместить направляющую на слой

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4078,3 +4078,6 @@ lighting Light Світло
Stroke Pressure Натиск мазка
Right Diagonal Діагональ праворуч
Left Diagonal Діагональ ліворуч
Vertical Guide Вертикальна напрямна
Horizontal Guide Горизонтальна напрямна
Move Guide to Layer Перемістити напрямну на шар

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -3724,3 +3724,6 @@ lighting Light 光源
Stroke Pressure 描边压力
Right Diagonal 右对角线
Left Diagonal 左对角线
Vertical Guide 垂直参考线
Horizontal Guide 水平参考线
Move Guide to Layer 将参考线移至图层

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3505,3 +3505,6 @@ lighting Light 光源
Stroke Pressure 筆觸壓力
Right Diagonal 右對角線
Left Diagonal 左對角線
Vertical Guide 垂直參考線
Horizontal Guide 水平參考線
Move Guide to Layer 將參考線移至圖層

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -220,6 +220,8 @@ struct Row {
colour: Color32,
/// Drawn dimmed: hidden itself or in a hidden layer or group, or a template layer.
dim: bool,
/// A ruler guide on the layer `node` (its index in the document's guides): the guide's row.
guide: Option<usize>,
}
/// What the rows were listed from: listed again when any of it changes.
@ -239,7 +241,7 @@ fn list_rows(doc: &Document, open: &OpenRows, opts: &PanelOptions, query: &str)
let mask_layer = doc.mask_edit.map(|m| m.layer);
let mut out = vec![];
for l in doc.layers.iter().rev().filter(|l| mask_layer.is_none_or(|m| m == l.id)) {
push_rows(l, 0, false, (Color32::PLACEHOLDER, true), (open, opts, query), &mut out);
push_rows(l, 0, false, (Color32::PLACEHOLDER, true), (open, opts, query, doc), &mut out);
}
out
}
@ -251,10 +253,10 @@ fn push_rows(
depth: usize,
clip_path: bool,
(colour, shown): (Color32, bool),
cx: (&OpenRows, &PanelOptions, &str),
cx: (&OpenRows, &PanelOptions, &str, &Document),
out: &mut Vec<Row>,
) {
let (open_rows, opts, query) = cx;
let (open_rows, opts, query, doc) = cx;
if opts.layers_only && !n.is_layer() {
return;
}
@ -266,11 +268,18 @@ fn push_rows(
_ => colour,
};
let shown = shown && n.visible;
let opens = opens(n, opts);
let searching = !query.is_empty();
// A layer's ruler guides are rows in it, above its art (not while searching).
let guides: Vec<usize> = if n.is_layer() && !opts.layers_only && !searching { doc.guides_on(n.id).map(|(i, _)| i).collect() } else { vec![] };
let opens = opens(n, opts) || !guides.is_empty();
let open = opens && (searching || open_rows.contains(n.id));
let at = out.len();
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template() });
out.push(Row { node: n.clone(), depth, clip_path, opens, open, colour, dim: !shown || n.is_template(), guide: None });
if open {
for i in guides.into_iter().rev() {
out.push(Row { node: n.clone(), depth: depth + 1, clip_path: false, opens: false, open: false, colour, dim: !shown, guide: Some(i) });
}
}
if open && let Some(children) = n.children() {
for (i, c) in children.iter().enumerate().rev() {
push_rows(c, depth + 1, i == 0 && n.clips(), (colour, shown), cx, out);
@ -302,6 +311,8 @@ fn rows_of_doc(ui: &Ui, st: &vectorcraft_engine::DocState, opts: &PanelOptions,
struct View<'a> {
doc: &'a Document,
sel: HashSet<NodeId>,
/// The selected ruler guides.
guides: Vec<usize>,
target: Option<NodeId>,
current: Option<NodeId>,
rows: Vec<NodeId>,
@ -362,6 +373,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let view = View {
doc: &doc,
sel: st.selection.objects.iter().copied().collect(),
guides: st.selection.guides.clone(),
target: st.selection.target,
current: st.current_layer(),
rows,
@ -542,8 +554,47 @@ fn takes(n: &Node, drag: &LayersDrag, doc: &Document) -> bool {
}
}
/// The row of ruler guide `i` on the layer of `item`: a guide mark and its name, the selection
/// square when it is selected. A click selects the guide; Shift or Cmd adds it to (or takes it
/// from) the selected guides.
fn guide_row(ui: &mut Ui, view: &View, item: &Row, i: usize, out: &mut Out) {
let (t, h) = (&view.t, view.h);
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
let Some(g) = view.doc.guides.get(i) else { return };
let selected = view.guides.contains(&i);
if resp.hovered() {
ui.painter().rect_filled(r, 0.0, t.hover.gamma_multiply(0.6));
}
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
let mut x = r.left() + 2.0 * COLUMN + 2.0;
colour_bar(ui, &item.node, r, x, item.colour);
x += 6.0 + item.depth as f32 * INDENT + 16.0;
// The guide mark: a short line across or down, in the guides' colour.
let c = egui::pos2(x + 8.0, r.center().y);
let half = if g.vertical { vec2(0.0, 6.0) } else { vec2(6.0, 0.0) };
ui.painter().line_segment([c - half, c + half], Stroke::new(1.5, t.accent));
x += 22.0;
let name = format!("<{}>", if g.vertical { tl!("Vertical Guide") } else { tl!("Horizontal Guide") });
let color = if item.dim { t.text_dim } else { t.text };
ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(13.0), color);
let col_x = r.right() - 43.5;
ui.painter().line_segment([egui::pos2(col_x, r.top()), egui::pos2(col_x, r.bottom())], Stroke::new(1.0, t.input_border));
if selected {
let q = egui::Rect::from_center_size(egui::pos2(r.right() - SQUARE_X, r.center().y), vec2(7.0, 7.0));
ui.painter().rect_filled(q, 0.0, item.colour);
ui.painter().rect_stroke(q, 0.0, Stroke::new(1.0, t.text), StrokeKind::Inside);
}
if resp.clicked() {
let m = ui.input(|i| i.modifiers);
out.actions.push(("guide.select".into(), json!({"indexes": [i], "toggle": m.shift || m.command})));
}
}
/// The widgets of row `item`.
fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
if let Some(i) = item.guide {
return guide_row(ui, view, item, i, out);
}
let n = &*item.node;
let (depth, clip_path, has_children, open, color) = (item.depth, item.clip_path, item.opens, item.open, item.colour);
let doc = view.doc;

View File

@ -256,6 +256,11 @@ Constrain acts as Shift held (proportional scaling, moves and rotations by 45°)
where it is greyed. The modifier keys still work while dragging (Cmd on a corner distorts it freely, Cmd+Alt+Shift in
perspective, Cmd on a side shears).
Ruler guides are layer objects: `guide.add` puts a guide on the current layer (or `layer`), where it shows, hides,
locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it);
`guide.setLayer {index?, layer}` moves guides to another layer, and `guide.list` gives each
guide's `layer`, `shown` and `editable`.
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;