Land #643 (fix #638: Layers panel lays out only rows in view, keeps open rows in the file)
This commit is contained in:
commit
c7aeaa637f
File diff suppressed because one or more lines are too long
|
|
@ -610,6 +610,11 @@ pub struct Document {
|
|||
/// save time only, so changing the view never marks the document modified.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_view: Option<SavedView>,
|
||||
/// The layers, sublayers and groups open in the Layers panel when the document was saved; they
|
||||
/// reopen that way. `None`: the default, only the top-level layers open. Written at save time
|
||||
/// only, like [`Document::last_view`].
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub layers_open: Option<Vec<NodeId>>,
|
||||
/// Edit → Assign Profile: the profiles the document is tagged with (files before format v3
|
||||
/// kept them in `unknown`, see [`Document::migrate_color_profiles`]).
|
||||
#[serde(default, skip_serializing_if = "ColorProfiles::is_empty")]
|
||||
|
|
@ -702,6 +707,7 @@ impl Document {
|
|||
metadata: DocMetadata::default(),
|
||||
raster_effects: RasterEffectsSettings::default(),
|
||||
last_view: None,
|
||||
layers_open: None,
|
||||
color_profiles: ColorProfiles::default(),
|
||||
export_settings: Default::default(),
|
||||
extra: Default::default(),
|
||||
|
|
|
|||
|
|
@ -216,15 +216,20 @@ fn is_svg(f: &Format) -> bool {
|
|||
matches!(f.id, "svg" | "svgz")
|
||||
}
|
||||
|
||||
/// The document as written: native files carry the view to reopen at.
|
||||
/// The document as written: native files carry the view to reopen at and the Layers panel's open
|
||||
/// rows.
|
||||
fn doc_to_save(st: &DocState, f: &Format) -> Arc<Document> {
|
||||
if is_native(f) && st.doc.last_view != st.view {
|
||||
let mut d = (*st.doc).clone();
|
||||
d.last_view = st.view.clone();
|
||||
Arc::new(d)
|
||||
} else {
|
||||
st.doc.clone()
|
||||
if !is_native(f) {
|
||||
return st.doc.clone();
|
||||
}
|
||||
let open = st.layers_open.saved(&st.doc);
|
||||
if st.doc.last_view == st.view && st.doc.layers_open == open {
|
||||
return st.doc.clone();
|
||||
}
|
||||
let mut d = (*st.doc).clone();
|
||||
d.last_view = st.view.clone();
|
||||
d.layers_open = open;
|
||||
Arc::new(d)
|
||||
}
|
||||
|
||||
/// What a format loses against a native file (reported whenever a save writes it).
|
||||
|
|
@ -274,6 +279,7 @@ fn artboard_doc(doc: &Document, i: usize) -> Document {
|
|||
d.artboards = vec![a.clone()];
|
||||
// It opens fitted to its artboard, not at the master file's view.
|
||||
d.last_view = None;
|
||||
d.layers_open = None;
|
||||
for layer in &mut d.layers {
|
||||
keep_on(layer, r);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ pub struct DocState {
|
|||
pub recovery: Option<cmd::recovery::RecoveryCopy>,
|
||||
/// View → Show Print Tiling, per document (view state: not saved, not undoable).
|
||||
pub print_tiling: bool,
|
||||
/// The rows open in the Layers panel (view state: not undoable; native files keep it).
|
||||
pub layers_open: OpenRows,
|
||||
/// Transform Again after a perspective move or scale (Perspective Selection tool): the
|
||||
/// `perspective.transform` params it repeats. `None` once an ordinary transform follows.
|
||||
pub last_perspective: Option<Value>,
|
||||
|
|
@ -160,11 +162,61 @@ pub struct DocState {
|
|||
|
||||
static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
/// The rows open in the Layers panel: the layers, sublayers and groups that show what they hold.
|
||||
/// A document opens with the ones it was saved with ([`Document::layers_open`]), else with only
|
||||
/// its top-level layers open.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct OpenRows {
|
||||
ids: std::collections::HashSet<NodeId>,
|
||||
/// Counts the changes, so views can keep what they work out from the open rows.
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl OpenRows {
|
||||
/// The rows `saved` in a file, else `doc`'s top-level layers.
|
||||
fn new(doc: &Document, saved: Option<Vec<NodeId>>) -> Self {
|
||||
let ids = match saved {
|
||||
Some(ids) => ids.into_iter().collect(),
|
||||
None => doc.layers.iter().map(|l| l.id).collect(),
|
||||
};
|
||||
Self { ids, generation: 0 }
|
||||
}
|
||||
pub fn contains(&self, id: NodeId) -> bool {
|
||||
self.ids.contains(&id)
|
||||
}
|
||||
/// Open or close row `id`.
|
||||
pub fn set(&mut self, id: NodeId, open: bool) {
|
||||
let changed = if open { self.ids.insert(id) } else { self.ids.remove(&id) };
|
||||
self.generation += u64::from(changed);
|
||||
}
|
||||
/// Changes so far: the same number means the same open rows.
|
||||
pub fn generation(&self) -> u64 {
|
||||
self.generation
|
||||
}
|
||||
/// What a native file keeps of the open rows: those of `doc` that hold others, in id order;
|
||||
/// `None` when they are the default (only the top-level layers).
|
||||
pub fn saved(&self, doc: &Document) -> Option<Vec<NodeId>> {
|
||||
let mut ids = vec![];
|
||||
doc.walk(|n| {
|
||||
if n.children().is_some() && self.ids.contains(&n.id) {
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
ids.sort_unstable();
|
||||
let mut layers: Vec<NodeId> = doc.layers.iter().map(|l| l.id).collect();
|
||||
layers.sort_unstable();
|
||||
(ids != layers).then_some(ids)
|
||||
}
|
||||
}
|
||||
|
||||
impl DocState {
|
||||
pub fn new(mut doc: Document, path: Option<String>) -> Self {
|
||||
let active_layer = doc.default_layer();
|
||||
// The saved view lives here while the document is open (saves write it back).
|
||||
// The saved view and open Layers rows live here while the document is open (saves write
|
||||
// them back).
|
||||
let view = doc.last_view.take();
|
||||
let saved_open = doc.layers_open.take();
|
||||
let layers_open = OpenRows::new(&doc, saved_open);
|
||||
let doc = Arc::new(doc);
|
||||
Self {
|
||||
saved_doc: doc.clone(),
|
||||
|
|
@ -190,6 +242,7 @@ impl DocState {
|
|||
recovered: false,
|
||||
recovery: None,
|
||||
print_tiling: false,
|
||||
layers_open,
|
||||
last_perspective: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -956,6 +1009,7 @@ impl Session {
|
|||
st.save_options = old.save_options.clone();
|
||||
st.converted = old.converted;
|
||||
st.view = old.view.clone();
|
||||
st.layers_open = old.layers_open.clone();
|
||||
let old = std::mem::replace(&mut self.docs[index], st);
|
||||
if let Some(stash) = &mut self.batch_stash {
|
||||
stash.push(old);
|
||||
|
|
|
|||
|
|
@ -260,6 +260,37 @@ fn the_saved_view_round_trips_without_dirtying() {
|
|||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_layers_rows_round_trip_without_dirtying() {
|
||||
let d = dir("open-rows");
|
||||
let mut s = session();
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let a = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let g = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
|
||||
// A document starts with its top-level layers open, and a file keeps nothing for that.
|
||||
assert!(s.doc().unwrap().layers_open.contains(layer) && !s.doc().unwrap().layers_open.contains(g));
|
||||
let native = path(&d, "rows.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": native})).unwrap();
|
||||
let bytes = std::fs::read(&native).unwrap();
|
||||
assert_eq!(vectorcraft_format::load(&bytes).unwrap().layers_open, None);
|
||||
// Opening the group and closing the layer is view state: not an edit, kept by the file.
|
||||
let st = s.doc_mut().unwrap();
|
||||
st.layers_open.set(g, true);
|
||||
st.layers_open.set(layer, false);
|
||||
assert!(!s.doc().unwrap().is_dirty());
|
||||
s.execute("document.save", &json!({"path": native})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.layers_open, None, "only the written file carries it");
|
||||
let mut again = session();
|
||||
again.execute("document.open", &json!({"path": native})).unwrap();
|
||||
let open = &again.doc().unwrap().layers_open;
|
||||
assert!(open.contains(g) && !open.contains(layer));
|
||||
// Older files (none saved) open with the top-level layers open.
|
||||
let doc = vectorcraft_format::load(&vectorcraft_format::save(&vectorcraft_doc::Document::new(5.0, 5.0), false)).unwrap();
|
||||
assert_eq!(doc.layers_open, None);
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_plans_suggest_names_and_folders() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -24,13 +24,20 @@
|
|||
//! on the trash clears it.
|
||||
//!
|
||||
//! While an opacity mask is edited the panel lists only its art, under an `<Opacity Mask>` entry.
|
||||
//!
|
||||
//! The rows open are the document's ([`OpenRows`], saved in native files): a document opens with
|
||||
//! only its top-level layers open. The rows shown are listed once per document change, and only
|
||||
//! those scrolled into view are laid out, so a document of many thousand objects costs no more
|
||||
//! per frame than a small one.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Color32, Sense, Stroke, StrokeKind, Ui, vec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_engine::OpenRows;
|
||||
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{Live, PanelDrag};
|
||||
|
|
@ -198,9 +205,97 @@ fn opens(n: &Node, opts: &PanelOptions) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether `n` or anything in it has a name containing `q` (lowercase).
|
||||
fn matches(n: &Node, q: &str) -> bool {
|
||||
n.display_name().to_lowercase().contains(q) || n.children().is_some_and(|c| c.iter().any(|c| matches(c, q)))
|
||||
/// One row of the panel.
|
||||
#[derive(Clone)]
|
||||
struct Row {
|
||||
node: Arc<Node>,
|
||||
/// Nesting level (0: a top-level layer).
|
||||
depth: usize,
|
||||
/// A clipping path: its group or layer clips.
|
||||
clip_path: bool,
|
||||
/// It holds rows ([`opens`]), and shows them.
|
||||
opens: bool,
|
||||
open: bool,
|
||||
/// The colour of its layer (its own, for a layer).
|
||||
colour: Color32,
|
||||
/// Drawn dimmed: hidden itself or in a hidden layer or group, or a template layer.
|
||||
dim: bool,
|
||||
}
|
||||
|
||||
/// What the rows were listed from: listed again when any of it changes.
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct RowsKey {
|
||||
uid: u64,
|
||||
revision: u64,
|
||||
open: u64,
|
||||
layers_only: bool,
|
||||
query: String,
|
||||
}
|
||||
|
||||
/// The rows the panel shows, top down: the layers (only the opacity mask's while one is edited)
|
||||
/// with the rows of the open ones inside them. A search shows every row whose name, or the name of
|
||||
/// anything inside it, contains `query` (lowercase).
|
||||
fn list_rows(doc: &Document, open: &OpenRows, opts: &PanelOptions, query: &str) -> Vec<Row> {
|
||||
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);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Add the rows of `n` and those open inside it to `out`; `(colour, shown)` are those of what holds
|
||||
/// it (the layer's colour, and whether it is visible).
|
||||
fn push_rows(
|
||||
n: &Arc<Node>,
|
||||
depth: usize,
|
||||
clip_path: bool,
|
||||
(colour, shown): (Color32, bool),
|
||||
cx: (&OpenRows, &PanelOptions, &str),
|
||||
out: &mut Vec<Row>,
|
||||
) {
|
||||
let (open_rows, opts, query) = cx;
|
||||
if opts.layers_only && !n.is_layer() {
|
||||
return;
|
||||
}
|
||||
let colour = match &n.kind {
|
||||
NodeKind::Layer { color, .. } => {
|
||||
let [r, g, b] = color.rgb();
|
||||
Color32::from_rgb(r, g, b)
|
||||
}
|
||||
_ => colour,
|
||||
};
|
||||
let shown = shown && n.visible;
|
||||
let opens = opens(n, opts);
|
||||
let searching = !query.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() });
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Searching, a row stays for its own name or for a row it keeps inside it.
|
||||
if searching && out.len() == at + 1 && !n.display_name().to_lowercase().contains(query) {
|
||||
out.truncate(at);
|
||||
}
|
||||
}
|
||||
|
||||
/// The rows of the active document as [`list_rows`] lists them, listed again only when the
|
||||
/// document, its open rows, the search or Show Layers Only changed.
|
||||
fn rows_of_doc(ui: &Ui, st: &vectorcraft_engine::DocState, opts: &PanelOptions, query: &str) -> Arc<Vec<Row>> {
|
||||
let now =
|
||||
RowsKey { uid: st.uid, revision: st.revision, open: st.layers_open.generation(), layers_only: opts.layers_only, query: query.to_string() };
|
||||
let id = key("rows");
|
||||
if let Some((was, rows)) = ui.data(|d| d.get_temp::<(RowsKey, Arc<Vec<Row>>)>(id))
|
||||
&& was == now
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
let rows = Arc::new(list_rows(&st.doc, &st.layers_open, opts, query));
|
||||
ui.data_mut(|d| d.insert_temp(id, (now, rows.clone())));
|
||||
rows
|
||||
}
|
||||
|
||||
/// Everything one frame of the panel reads.
|
||||
|
|
@ -213,8 +308,6 @@ struct View<'a> {
|
|||
opts: PanelOptions,
|
||||
t: Tokens,
|
||||
h: f32,
|
||||
/// The search field's text, lowercase (empty: every row).
|
||||
query: String,
|
||||
column: Option<ColumnDrag>,
|
||||
/// The row to scroll into view.
|
||||
reveal: Option<NodeId>,
|
||||
|
|
@ -224,8 +317,6 @@ struct View<'a> {
|
|||
#[derive(Default)]
|
||||
struct Out {
|
||||
actions: Vec<(String, Value)>,
|
||||
/// The rows shown, top down.
|
||||
order: Vec<NodeId>,
|
||||
/// A row clicked with these modifiers (resolved once every row is listed).
|
||||
click: Option<(NodeId, egui::Modifiers)>,
|
||||
/// The column drag after this frame (`Some(None)`: it ended).
|
||||
|
|
@ -249,23 +340,25 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
d.insert_temp(shown_doc, st.uid);
|
||||
});
|
||||
}
|
||||
let uid = st.uid;
|
||||
let doc = st.doc.clone();
|
||||
let rows = st.highlighted_rows();
|
||||
// Rows highlighted anew (a click, Locate Object, a new layer): open the layers around them.
|
||||
let seen: Vec<NodeId> = ui.data(|d| d.get_temp(key("seen"))).unwrap_or_default();
|
||||
let mut reveal = None;
|
||||
let expanded = app.ui.layers_expanded.entry(uid).or_insert_with(|| doc.layers.iter().map(|l| l.id.0).collect());
|
||||
if rows != seen {
|
||||
for r in &rows {
|
||||
for a in doc.ancestry(*r).unwrap_or_default().split_last().map(|(_, a)| a.to_vec()).unwrap_or_default() {
|
||||
expanded.insert(a.0);
|
||||
let around: Vec<NodeId> =
|
||||
rows.iter().flat_map(|r| doc.ancestry(*r).unwrap_or_default().split_last().map(|(_, a)| a.to_vec()).unwrap_or_default()).collect();
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
for a in around {
|
||||
st.layers_open.set(a, true);
|
||||
}
|
||||
}
|
||||
reveal = rows.first().copied();
|
||||
ui.data_mut(|d| d.insert_temp(key("seen"), rows.clone()));
|
||||
}
|
||||
let query = crate::widgets::search_field(ui, egui::Id::new("layers-search"), tl!("Search All")).trim().to_lowercase();
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let list = rows_of_doc(ui, st, &app.ui.layers_panel, &query);
|
||||
let view = View {
|
||||
doc: &doc,
|
||||
sel: st.selection.objects.iter().copied().collect(),
|
||||
|
|
@ -275,30 +368,31 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
opts: app.ui.layers_panel.clone(),
|
||||
t,
|
||||
h: app.ui.layers_panel.row(),
|
||||
query,
|
||||
column: ui.data(|d| d.get_temp(key("column"))),
|
||||
reveal,
|
||||
};
|
||||
// While an opacity mask is edited only its art is listed.
|
||||
let mask_layer = doc.mask_edit.map(|m| m.layer);
|
||||
let mut out = Out::default();
|
||||
ui.add_space(6.0);
|
||||
let list_h = ui.available_height() - 34.0;
|
||||
let list = egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).show(ui, |ui| {
|
||||
// Only the rows in view are laid out; one revealed out of view is scrolled to.
|
||||
let revealed = reveal.and_then(|id| list.iter().position(|r| r.node.id == id));
|
||||
let scrolled = ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
let expanded = app.ui.layers_expanded.entry(uid).or_default();
|
||||
for l in doc.layers.iter().rev().filter(|l| mask_layer.is_none_or(|m| m == l.id)) {
|
||||
row(ui, &view, l, 0, false, expanded, &mut out);
|
||||
}
|
||||
egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).show_rows(ui, view.h, list.len(), |ui, range| {
|
||||
if let Some(i) = revealed.filter(|i| !range.contains(i)) {
|
||||
let top = ui.max_rect().top() + (i as f32 - range.start as f32) * view.h;
|
||||
ui.scroll_to_rect(egui::Rect::from_min_size(egui::pos2(ui.max_rect().left(), top), vec2(ui.max_rect().width(), view.h)), None);
|
||||
}
|
||||
for r in list.get(range).unwrap_or_default() {
|
||||
row(ui, &view, r, &mut out);
|
||||
}
|
||||
})
|
||||
});
|
||||
let list_rect = list.inner_rect;
|
||||
resolve_click(ui, &view, &mut out);
|
||||
let expanded = app.ui.layers_expanded.entry(uid).or_default();
|
||||
for (id, open) in out.toggle.drain(..) {
|
||||
if open {
|
||||
expanded.insert(id.0);
|
||||
} else {
|
||||
expanded.remove(&id.0);
|
||||
let list_rect = scrolled.inner.inner_rect;
|
||||
resolve_click(ui, &view, &list, &mut out);
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
for (id, open) in out.toggle.drain(..) {
|
||||
st.layers_open.set(id, open);
|
||||
}
|
||||
}
|
||||
// The column drag: live while it lasts, one undo step when the button is released.
|
||||
|
|
@ -415,15 +509,16 @@ fn bottom_bar(app: &mut VectorcraftApp, ui: &mut Ui, view: &View, doc: &Document
|
|||
|
||||
/// Resolve the row clicked this frame: plain (highlight it), Ctrl/Cmd (toggle it) or Shift
|
||||
/// (highlight the rows from the last one clicked down to it).
|
||||
fn resolve_click(ui: &Ui, view: &View, out: &mut Out) {
|
||||
fn resolve_click(ui: &Ui, view: &View, list: &[Row], out: &mut Out) {
|
||||
let Some((id, m)) = out.click.take() else { return };
|
||||
let anchor: Option<NodeId> = ui.data(|d| d.get_temp(key("anchor")));
|
||||
ui.data_mut(|d| d.insert_temp(key("focus"), true));
|
||||
let at = |id: NodeId| list.iter().position(|r| r.node.id == id);
|
||||
if m.shift
|
||||
&& let Some(a) = anchor.or(view.rows.last().copied())
|
||||
&& let (Some(i), Some(j)) = (out.order.iter().position(|r| *r == a), out.order.iter().position(|r| *r == id))
|
||||
&& let (Some(i), Some(j)) = (at(a), at(id))
|
||||
{
|
||||
let range: Vec<u64> = out.order.get(i.min(j)..=i.max(j)).unwrap_or_default().iter().map(|r| r.0).collect();
|
||||
let range: Vec<u64> = list.get(i.min(j)..=i.max(j)).unwrap_or_default().iter().map(|r| r.node.id.0).collect();
|
||||
out.actions.push(("layer.highlight".into(), json!({"ids": range, "mode": if m.command { "add" } else { "set" }})));
|
||||
return;
|
||||
}
|
||||
|
|
@ -447,19 +542,14 @@ fn takes(n: &Node, drag: &LayersDrag, doc: &Document) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expanded: &mut HashSet<u64>, out: &mut Out) {
|
||||
/// The widgets of row `item`.
|
||||
fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut 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;
|
||||
let t = &view.t;
|
||||
if !view.query.is_empty() && !matches(n, &view.query) {
|
||||
return;
|
||||
}
|
||||
if view.opts.layers_only && !n.is_layer() {
|
||||
return;
|
||||
}
|
||||
let h = view.h;
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click_and_drag());
|
||||
out.order.push(n.id);
|
||||
if view.reveal == Some(n.id) {
|
||||
ui.scroll_to_rect(r, None);
|
||||
}
|
||||
|
|
@ -470,7 +560,6 @@ fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expand
|
|||
n.walk(&mut |c| any |= c.id != n.id && view.sel.contains(&c.id));
|
||||
any
|
||||
});
|
||||
let color = layer_colour(doc, n.id);
|
||||
// Highlighted rows (else the current layer's).
|
||||
let highlighted = if view.rows.is_empty() { n.is_layer() && Some(n.id) == view.current } else { view.rows.contains(&n.id) };
|
||||
if highlighted {
|
||||
|
|
@ -490,9 +579,6 @@ fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expand
|
|||
colour_bar(ui, n, r, x, color);
|
||||
x += 6.0 + depth as f32 * INDENT;
|
||||
// Disclosure triangle; Alt opens or closes everything inside too.
|
||||
let searching = !view.query.is_empty();
|
||||
let has_children = opens(n, &view.opts);
|
||||
let open = has_children && (searching || expanded.contains(&n.id.0));
|
||||
if has_children {
|
||||
let dr = egui::Rect::from_min_size(egui::pos2(x, r.center().y - 8.0), vec2(14.0, 16.0));
|
||||
let dresp = ui.interact(dr, ui.id().with(("disc", n.id.0)), Sense::click());
|
||||
|
|
@ -548,8 +634,7 @@ fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expand
|
|||
} else {
|
||||
painted_name(n, &name, crate::i18n::current())
|
||||
};
|
||||
let dim = !doc.is_visible(n.id) || n.is_template();
|
||||
let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, if dim { t.text_dim } else { t.text });
|
||||
let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, if item.dim { t.text_dim } else { t.text });
|
||||
name_end = text.right();
|
||||
// A clipping path's name is underlined, a masked object's with a dashed line.
|
||||
if clip_path {
|
||||
|
|
@ -605,11 +690,6 @@ fn row(ui: &mut Ui, view: &View, n: &Node, depth: usize, clip_path: bool, expand
|
|||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
}
|
||||
drop_target(ui, view, n, r, x - 16.0, open, &resp, out);
|
||||
if open && let Some(children) = n.children() {
|
||||
for (i, c) in children.iter().enumerate().rev() {
|
||||
row(ui, view, c, depth + 1, i == 0 && n.clips(), expanded, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Alt-clicking row `n`'s eye (`eye`) or lock: Hide Others or Lock Others, every top-level layer
|
||||
|
|
@ -951,15 +1031,9 @@ pub fn expand(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|||
v
|
||||
}
|
||||
};
|
||||
let uid = st.uid;
|
||||
let layers: Vec<u64> = st.doc.layers.iter().map(|l| l.id.0).collect();
|
||||
let set = app.ui.layers_expanded.entry(uid).or_insert_with(|| layers.into_iter().collect());
|
||||
let st = app.session.active_mut().ok_or("no document open")?;
|
||||
for i in &ids {
|
||||
if open {
|
||||
set.insert(*i);
|
||||
} else {
|
||||
set.remove(i);
|
||||
}
|
||||
st.layers_open.set(NodeId(*i), open);
|
||||
}
|
||||
Ok(json!({ "count": ids.len() }))
|
||||
}
|
||||
|
|
@ -1623,4 +1697,53 @@ mod tests {
|
|||
assert_eq!(rows_of(&app), vec![sub]);
|
||||
assert_eq!(app.session.active().unwrap().current_layer(), Some(sub));
|
||||
}
|
||||
|
||||
/// A layer of 2,000 groups of 10 paths with every row open: one frame lays out only the rows
|
||||
/// in view, and a row highlighted far below them is scrolled to.
|
||||
#[test]
|
||||
fn only_the_rows_in_view_are_laid_out() {
|
||||
use vectorcraft_doc::Appearance;
|
||||
use vectorcraft_geom::{Rect, shapes};
|
||||
let mut d = Document::new(1000.0, 1000.0);
|
||||
let mut groups = vec![];
|
||||
let mut needle = None;
|
||||
for g in 0..2000 {
|
||||
let kids = (0..10)
|
||||
.map(|k| {
|
||||
let r = Rect::new(k as f64, g as f64 * 0.1, k as f64 + 1.0, g as f64 * 0.1 + 1.0);
|
||||
let mut n = Node::path(d.alloc_id(), shapes::rectangle(r), Appearance::default());
|
||||
if g == 0 && k == 0 {
|
||||
n.name = Some("Needle".into());
|
||||
needle = Some(n.id);
|
||||
}
|
||||
Arc::new(n)
|
||||
})
|
||||
.collect();
|
||||
groups.push(Arc::new(Node::group(d.alloc_id(), kids)));
|
||||
}
|
||||
*Arc::make_mut(&mut d.layers[0]).children_mut().unwrap() = groups;
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.add_document(d, None);
|
||||
expand(&mut app, &json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
// (shapes drawn, whether the needle's name is among them); a second passes per frame, so
|
||||
// scrolling ends.
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut VectorcraftApp| {
|
||||
time += 1.0;
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(300.0, 800.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), time: Some(time), ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let found = out.shapes.iter().any(|c| matches!(&c.shape, egui::Shape::Text(t) if t.galley.text() == "Needle"));
|
||||
(out.shapes.len(), found)
|
||||
};
|
||||
let (shapes, found) = frame(&mut app);
|
||||
// 22,001 rows; about 30 fit.
|
||||
assert!(shapes < 1000 && !found, "{shapes} shapes");
|
||||
let needle = needle.unwrap();
|
||||
app.session.execute("layer.highlight", &json!({"ids": [needle.0], "mode": "set"})).unwrap();
|
||||
let found = (0..4).any(|_| frame(&mut app).1);
|
||||
assert_eq!(app.session.active().unwrap().highlighted_rows(), [needle]);
|
||||
assert!(found, "the highlighted row is scrolled into view");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -365,9 +365,6 @@ pub struct UiState {
|
|||
/// Layers panel › Panel Options… (row size, thumbnails, Show Layers Only).
|
||||
#[serde(default)]
|
||||
pub layers_panel: crate::panels::layers::PanelOptions,
|
||||
/// The Layers panel's open rows, per open document (`DocState::uid` → node ids).
|
||||
#[serde(skip)]
|
||||
pub layers_expanded: std::collections::HashMap<u64, std::collections::HashSet<u64>>,
|
||||
/// The desktop window's size, position and maximized state, saved when the app quits and
|
||||
/// restored at the next launch (the desktop host reads and writes it; none on the web).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -521,7 +518,6 @@ impl Default for UiState {
|
|||
dxf_import: Value::Null,
|
||||
home: None,
|
||||
layers_panel: Default::default(),
|
||||
layers_expanded: Default::default(),
|
||||
window: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -882,6 +882,7 @@ and `layerRows`, the rows highlighted in the panel.
|
|||
dimImages, dimPercent; OK is one undo step), `ui.layersPanelOptions` opens Panel Options (`layersPanelOptions`:
|
||||
layersOnly, rowSize small|medium|large|other, otherSize, thumbLayers, thumbGroups, thumbObjects), and
|
||||
`ui.layersExpand {ids?, open?}` opens or closes rows as their triangles do (Alt-click: everything inside).
|
||||
A document opens with only its top-level layers open; the open rows are saved in the native file and reopen that way.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"layer.newSublayer","params":{"name":"Shadows"}}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue