mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
* #133: Selection pane engine: restack inside groups, Show/Hide All, Lock like PowerPoint - arrange.bringForward/sendBackward/bringToFront/sendToBack and arrange.reorder work on a group's children, which stay in their group (as PowerPoint's pane arrows and drag do); steps that change nothing leave no undo step. - selection.select (pane click: Ctrl toggles, children and top-level objects don't mix, picking a hidden object shows it), selection.showAll (everything, children too) and selection.hideAll (top level). - shape.lock cascades through groups; locked objects can be selected, formatted and typed in, but not moved, sized, rotated, flipped, reshaped or cropped (commands and canvas). - PPTX: Lock reads and writes PowerPoint's full locking attribute set per element kind (observed by saving locked objects from PowerPoint); partial locks aren't Lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Front-most first with groups folding; drag a row to restack among its siblings (accent insertion line); Bring Forward / Send Backward arrows; Show All / Hide All; per-row eye and padlock; double-click or F2 renames in place; Ctrl+click toggles, Shift+click selects a run; arrows, Home, End and Esc in the list; right-click menu. Lock/Unlock on the canvas menu, greyed handles on locked objects. egui_kittest tests drive the pane. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
127 lines
4.4 KiB
Rust
127 lines
4.4 KiB
Rust
//! Selection pane: picking objects from the list, Show All and Hide All. Its other actions are
|
|
//! ordinary commands: `shape.visible` (eye), `shape.lock` (padlock), `shape.rename`,
|
|
//! `arrange.bringForward` / `arrange.sendBackward` (arrows) and `arrange.reorder` (drag and drop).
|
|
|
|
use deckcraft_model::{Shape, ShapeId};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::*;
|
|
use crate::{Result, Session};
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!(
|
|
"selection.select",
|
|
"Select",
|
|
["Selection Pane"],
|
|
None,
|
|
"{ids | id, toggle?: bool (Ctrl+click), add?: bool} → {selected}: objects in groups too; a hidden object picked from the list is shown",
|
|
has_slide,
|
|
select
|
|
),
|
|
cmd!("selection.showAll", "Show All", ["Selection Pane"], None, "{} shows every object on the slide, inside groups too", has_slide, show_all),
|
|
cmd!("selection.hideAll", "Hide All", ["Selection Pane"], None, "{} hides every top-level object on the slide", has_slide, hide_all),
|
|
]
|
|
}
|
|
|
|
/// The Selection pane's click. Objects inside groups and top-level objects can't be selected
|
|
/// together (Ctrl+click on the other kind is ignored, as in PowerPoint), and picking a hidden
|
|
/// object from the list shows it again.
|
|
fn select(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let ids = ids_param(p, "ids").or_else(|| id_param(p, "id").map(|i| vec![i])).unwrap_or_default();
|
|
let (toggle, add) = (bool_or(p, "toggle", false), bool_or(p, "add", false));
|
|
let st = s.doc()?;
|
|
let in_group = |id: &ShapeId| deckcraft_model::parent_of(st.shapes(), *id).map(|p| p.is_some());
|
|
let mut valid: Vec<ShapeId> = ids.iter().copied().filter(|i| in_group(i).is_some()).collect();
|
|
if valid.is_empty() {
|
|
return Err(bad("selection.select", "no such shape"));
|
|
}
|
|
let kind = valid.first().and_then(in_group);
|
|
valid.retain(|i| in_group(i) == kind);
|
|
let keep: Vec<ShapeId> = if toggle || add {
|
|
let current = st.selection.shapes.clone();
|
|
if current.first().is_some_and(|c| in_group(c) != kind) {
|
|
// Can't mix objects in groups with top-level ones: nothing changes.
|
|
return Ok(json!({"selected": current}));
|
|
}
|
|
current
|
|
} else {
|
|
vec![]
|
|
};
|
|
let mut next = keep;
|
|
for i in &valid {
|
|
match next.iter().position(|x| x == i) {
|
|
Some(k) if toggle => {
|
|
next.remove(k);
|
|
}
|
|
Some(_) => {}
|
|
None => next.push(*i),
|
|
}
|
|
}
|
|
let reveal: Vec<ShapeId> = next.iter().copied().filter(|i| valid.contains(i) && st.shape(*i).is_some_and(|x| x.hidden)).collect();
|
|
if reveal.is_empty() {
|
|
s.select(|_, sel| {
|
|
sel.text = None;
|
|
sel.cells = None;
|
|
sel.shapes = next.clone();
|
|
})?;
|
|
} else {
|
|
s.edit(|doc, sel| {
|
|
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad("selection.select", "no slide"))?;
|
|
for id in &reveal {
|
|
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
|
|
sh.hidden = false;
|
|
}
|
|
}
|
|
sel.text = None;
|
|
sel.cells = None;
|
|
sel.shapes = next.clone();
|
|
Ok(())
|
|
})?;
|
|
}
|
|
Ok(json!({"selected": s.doc()?.selection.shapes}))
|
|
}
|
|
|
|
fn show_all(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
fn any_hidden(v: &[Shape]) -> bool {
|
|
let mut found = false;
|
|
deckcraft_model::walk(v, &mut |sh, _| found |= sh.hidden);
|
|
found
|
|
}
|
|
if !any_hidden(s.doc()?.shapes()) {
|
|
return ok();
|
|
}
|
|
s.edit(|doc, sel| {
|
|
fn rec(v: &mut [Shape], depth: usize) {
|
|
for sh in v {
|
|
sh.hidden = false;
|
|
if depth < 64
|
|
&& let Some(c) = sh.children_mut()
|
|
{
|
|
rec(c, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
rec(crate::shapes_mut(doc, sel).ok_or_else(|| bad("selection.showAll", "no slide"))?, 0);
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
fn hide_all(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
if s.doc()?.shapes().iter().all(|x| x.hidden) {
|
|
return ok();
|
|
}
|
|
s.edit(|doc, sel| {
|
|
for sh in crate::shapes_mut(doc, sel).ok_or_else(|| bad("selection.hideAll", "no slide"))? {
|
|
sh.hidden = true;
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "selection_tests.rs"]
|
|
mod tests;
|