Files
designcraft/crates/doc/src/selection.rs
T
moppeeerandClaude Opus 5.5 cc13e80c9d feat(tools): Page tool selects pages; handles preview the liquid rule, Alt-drag resizes
Chris Cicchino's "Adobe InDesign – Page Tool and Liquid Layout" (0:42–
2:06) selects a page with the Page tool, gets its bounding box with
handles, drags a handle (the page springs back: a liquid-rule preview),
holds Option to really resize it, Shift for proportions, and reads the
size in a tooltip by the cursor. DesignCraft's Page tool only opened a
generic Page Size dialog on click.

- tools: the Page tool selects the page under a click (none on the
  pasteboard); the selected page shows its outline, eight handles and
  its liquid page rule ("Scale", "Object-Based"…) above it. Dragging a
  handle previews layout.pageSize (the liquid rule re-flows the
  objects) and springs back on release; Alt resizes, Shift keeps the
  proportions; a W/H readout follows the pointer; resize cursors on the
  handles. Double-click a page for the Page Size dialog.
- doc: Selection.page (the Page tool's page); engine: layout.selectPage
  (no undo step).
- layout.pageSize `anchor` (0-8): the page grows away from the
  reference point and its objects and guides keep their place relative
  to it (a handle drag anchors the opposite edge); liquid rules work
  from that.
- canvas: PageHandles and Tag overlays; translations for the tags and
  the Resize Page undo label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:29:55 +02:00

105 lines
3.6 KiB
Rust

//! What is selected: page items (containers or their content) or a text range in a story.
use serde::{Deserialize, Serialize};
use crate::ids::{ItemId, StoryId};
use crate::table::CellRange;
/// A table cell: the table (in the selection's story) and the owning cell's grid position.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CellAddr {
pub table: u64,
pub row: usize,
pub col: usize,
}
impl CellAddr {
/// The text of footnote `id` (see [`crate::notes::FOOTNOTE_TABLE`]).
pub fn footnote(id: u64) -> Self {
CellAddr { table: crate::notes::FOOTNOTE_TABLE, row: id as usize, col: 0 }
}
/// The footnote id when this addresses footnote text.
pub fn footnote_id(&self) -> Option<u64> {
(self.table == crate::notes::FOOTNOTE_TABLE).then_some(self.row as u64)
}
}
/// Selected table cells (Table > Select, dragging across cells).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TableSel {
pub story: StoryId,
pub table: u64,
pub range: CellRange,
}
/// A caret or text range in a story. `anchor` stays put while `focus` moves (Shift-arrows).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextSel {
pub story: StoryId,
pub anchor: usize,
pub focus: usize,
/// The frame the caret is shown in (for carets at frame boundaries).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame: Option<ItemId>,
/// Text in a table cell: `anchor`/`focus` index the cell's story instead of `story`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cell: Option<CellAddr>,
}
impl TextSel {
pub fn caret(story: StoryId, pos: usize) -> Self {
TextSel { story, anchor: pos, focus: pos, frame: None, cell: None }
}
pub fn range(&self) -> std::ops::Range<usize> {
self.anchor.min(self.focus)..self.anchor.max(self.focus)
}
pub fn is_caret(&self) -> bool {
self.anchor == self.focus
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Selection {
/// Selected items (top-level or, with the Direct Selection tool, nested items).
pub items: Vec<ItemId>,
/// Content selection (graphic inside its frame) instead of the container.
#[serde(default)]
pub content: bool,
/// Direct selection: selected anchors as (item, subpath, anchor).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub anchors: Vec<(ItemId, usize, usize)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<TextSel>,
/// Selected table cells.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cells: Option<TableSel>,
/// Key object for Align.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<ItemId>,
/// The page selected with the Page tool (absolute index).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub page: Option<usize>,
}
impl Selection {
pub fn is_empty(&self) -> bool {
self.items.is_empty() && self.text.is_none() && self.cells.is_none()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn items(ids: Vec<ItemId>) -> Self {
Selection { items: ids, ..Default::default() }
}
pub fn text(t: TextSel) -> Self {
Selection { text: Some(t), ..Default::default() }
}
pub fn contains(&self, id: ItemId) -> bool {
self.items.contains(&id)
}
}