Layers panel: a new selection opens its layer and scrolls its row into view

Selecting an object on the canvas left the Layers panel as it was: its
layer could be closed and its row out of view. A new selection now opens
the layer it is on and scrolls its row into view once (closing or scrolling
afterwards is left alone); selected rows are highlighted, and an object
selected inside a group shows as its group. Unnamed image frames are listed
by their file name, as InDesign lists placed images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Govinda Roy
2026-10-14 20:11:35 +06:00
co-authored by Claude Opus 5.5
parent 11a51d5b66
commit c7b3ee4f45
+50 -4
View File
@@ -13,7 +13,8 @@ const ROW_H: f32 = 24.0;
/// Width of the eye and lock columns.
const COL_W: f32 = 22.0;
/// Display name of an item: its own name, the first words of a text frame's story, or the kind.
/// Display name of an item: its own name, the first words of a text frame's story, the file a
/// graphic frame shows (as InDesign lists placed images), or the kind.
pub fn item_label(doc: &Document, it: &Item) -> String {
if !it.name.is_empty() {
return it.name.clone();
@@ -41,6 +42,9 @@ pub fn item_label(doc: &Document, it: &Item) -> String {
return format!("<{short}{ell}>");
}
}
if let Some(name) = it.graphic().and_then(|g| doc.assets.get(&g.asset)).map(|a| a.name.trim()).filter(|n| !n.is_empty()) {
return format!("<{name}>");
}
it.default_label().to_string()
}
@@ -111,6 +115,15 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
let active = st.active_layer;
let selected: Vec<ItemId> = st.selection.items.clone();
let sel_layers: Vec<_> = selected.iter().filter_map(|i| doc.item(*i).map(|it| it.layer)).collect();
// The rows stand for top-level objects: an object selected inside a group shows as its group.
let shown: Vec<ItemId> = selected.iter().map(|i| doc.top_level_of(*i).unwrap_or(*i)).collect();
// A new selection (made on the canvas or anywhere else) is revealed: its layer opens and its row
// scrolls into view, once, so the panel can still be scrolled and closed freely afterwards.
let last_id = egui::Id::new("layers_revealed_selection");
let now: Vec<u64> = shown.iter().map(|i| i.0).collect();
let reveal = !now.is_empty() && ui.data(|d| d.get_temp::<Vec<u64>>(last_id)).as_ref() != Some(&now);
ui.data_mut(|d| d.insert_temp(last_id, now));
let mut scrolled = false;
// Objects on the current spread, topmost first.
let cur = crate::canvas::current_page(app).unwrap_or(0);
let spread_items: Vec<std::sync::Arc<Item>> =
@@ -118,7 +131,11 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
ui.spacing_mut().item_spacing.y = 0.0;
for l in doc.layers.iter().rev() {
let open_id = egui::Id::new(("layer_open", l.id.0));
let open: bool = ui.data(|d| d.get_temp(open_id)).unwrap_or(l.id == active);
let mut open: bool = ui.data(|d| d.get_temp(open_id)).unwrap_or(l.id == active);
if reveal && sel_layers.contains(&l.id) && !open {
open = true;
ui.data_mut(|d| d.insert_temp(open_id, true));
}
let lc = c32(l.color);
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::click());
if l.id == active {
@@ -201,8 +218,14 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
}
for it in spread_items.iter().filter(|i| i.layer == l.id) {
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::click());
let is_sel = selected.contains(&it.id);
if resp.hovered() {
let is_sel = shown.contains(&it.id);
if is_sel {
ui.painter().rect_filled(row, 0.0, t.row_selected);
if reveal && !scrolled {
resp.scroll_to_me(Some(egui::Align::Center));
scrolled = true;
}
} else if resp.hovered() {
ui.painter().rect_filled(row, 0.0, t.hover);
}
let (eye, lock) = eye_lock(ui, &app.ui.language, row, !it.hidden, it.locked, !l.visible, ("item", it.id.0));
@@ -267,4 +290,27 @@ mod tests {
let rr = doc.item(ItemId(rect["id"].as_u64().unwrap())).unwrap();
assert_eq!(item_label(doc, rr), "<rectangle>");
}
#[test]
fn selecting_an_object_opens_its_layer_in_the_panel() {
use egui_kittest::Harness;
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let id = app.run("frame.create", json!({"rect": [72, 72, 200, 200]})).unwrap()["id"].as_u64().unwrap();
let first = app.session.active().unwrap().doc.item(ItemId(id)).unwrap().layer;
// A new layer becomes the active one: the first layer's row starts closed.
app.run("layer.new", json!({})).unwrap();
app.run("selection.set", json!({"ids": []})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(260.0, 400.0)).build_ui_state(|ui, app: &mut DesignApp| show(app, ui), app);
h.run_steps(2);
let open = |h: &Harness<'_, DesignApp>| h.ctx.data(|d| d.get_temp::<bool>(egui::Id::new(("layer_open", first.0)))).unwrap_or(false);
assert!(!open(&h), "closed while nothing on it is selected");
h.state_mut().run("selection.set", json!({"ids": [id]})).unwrap();
h.run_steps(2);
assert!(open(&h), "selecting the object opens its layer");
// Closed again by hand, it stays closed while the selection stays the same.
h.ctx.data_mut(|d| d.insert_temp(egui::Id::new(("layer_open", first.0)), false));
h.run_steps(2);
assert!(!open(&h));
}
}