Selection Trait
Summary
Section titled “Summary”Two traits — Selectable and Focusable — on Modality, each with an associated type that varies per modality. Selection is a set (multi-select), focus is singular. Session wraps both with intent variants following the same pattern as undo/export.
Consumed by:
- Clipboard (the Session Clipboard RFC) — copy/cut operates on the current selection
- AI — Agent impl decides whether/how to include selection in context
- Snapshots — selection/focus included in modality snapshots for Dart rendering
- Collaborative Presence (the Collaborative Presence RFC) — selection/focus synced to peers via Loro EphemeralStore
Trait Signatures
Section titled “Trait Signatures”Selectable
Section titled “Selectable”trait Selectable: Modality { type Selection: Clone + Default + Send + Sync + 'static;
fn selection(&self) -> &Self::Selection; fn set_selection(&mut self, sel: Self::Selection); fn clear_selection(&mut self);}Selection is Default so clearing is always possible. Each modality defines what “selected” means:
| Modality | Selection type | Example |
|---|---|---|
| Whiteboard | HashSet<ElementId> | 3 shapes selected |
| Worksheet | HashSet<QuestionId> | 2 questions selected |
| LessonPlan | HashSet<SlideId> | 1 slide selected |
| Document | TextSelection (ranges/blocks) | Paragraph range |
Focusable
Section titled “Focusable”trait Focusable: Modality { type FocusTarget: Clone + PartialEq + Send + Sync + 'static;
fn focused(&self) -> Option<&Self::FocusTarget>; fn set_focus(&mut self, target: Self::FocusTarget); fn blur(&mut self);}Focus is singular — at most one thing is focused at a time. Not every modality needs focus. Examples:
| Modality | FocusTarget | Example |
|---|---|---|
| Whiteboard | ElementId | One text editor active |
| LessonPlan | SlideId | One slide being edited |
| Worksheet | — (not impl) | No focus concept |
Separate traits because not every modality needs both. Focusable can be implemented independently of Selectable.
Focus implies select, but not vice versa. Select = operating on an element (move, resize, delete). Focus = operating inside an element (e.g. editing prose in a text box). You can select a text box and drag it around without focusing it. Focusing it means you’re inside the prose editor. Each modality’s set_focus impl should ensure the target is also added to the selection set.
Storage
Section titled “Storage”Selection and focus live in M::Ephemeral — not persisted, not synced. The traits provide uniform read/write access; storage is an implementation detail.
#[derive(Default)]pub struct WhiteboardEphemeral { pub selected: HashSet<ElementId>, pub focused: Option<ElementId>, // ... other ephemeral state}
impl Selectable for Whiteboard { type Selection = HashSet<ElementId>;
fn selection(&self) -> &Self::Selection { &self.state().ephemeral.selected } fn set_selection(&mut self, sel: Self::Selection) { self.state_mut().ephemeral.selected = sel; } fn clear_selection(&mut self) { self.state_mut().ephemeral.selected.clear(); }}
impl Focusable for Whiteboard { type FocusTarget = ElementId;
fn focused(&self) -> Option<&Self::FocusTarget> { self.state().ephemeral.focused.as_ref() } fn set_focus(&mut self, target: Self::FocusTarget) { self.state_mut().ephemeral.focused = Some(target); } fn blur(&mut self) { self.state_mut().ephemeral.focused = None; }}Session Integration
Section titled “Session Integration”Session provides abstracted intent variants that delegate to the trait methods, following the undo/export pattern.
SessionIntent<M>
Section titled “SessionIntent<M>”SessionIntent becomes generic over M: Modality instead of a separate intent type I. All associated types (M::Intent, M::Selection, M::FocusTarget) are derived from the single generic:
enum SessionIntent<M: Modality + Selectable + Focusable> { Modality(M::Intent), Ai(AiIntent), Export(ExportFormat), Select(M::Selection), ClearSelection, Focus(M::FocusTarget), Blur,}No per-modality type aliases needed — SessionIntent<Whiteboard> gives you WhiteboardIntent, HashSet<ElementId>, and ElementId automatically.
Reduce
Section titled “Reduce”impl<M> ReduceIntent<M> for SessionIntent<M>where M: Modality + Selectable + Focusable,{ fn reduce(self, state: &mut SessionState<M>) -> Effect<...> { match self { SessionIntent::Modality(i) => { /* delegate to M */ } SessionIntent::Select(sel) => { state.modality.set_selection(sel); Effect::none() } SessionIntent::ClearSelection => { state.modality.clear_selection(); Effect::none() } SessionIntent::Focus(target) => { state.modality.set_focus(target); Effect::none() } SessionIntent::Blur => { state.modality.blur(); Effect::none() } // ... Ai, Export } }}Session knows when selection changes, so it can trigger recompile/snapshot/callbacks in the dispatch lifecycle — same as any other intent.
Conditional Bounds
Section titled “Conditional Bounds”Not every modality implements both traits. When a modality only implements Selectable, SessionIntent can be bounded on M: Selectable alone — the Focus/Blur variants would live in a separate FocusableSessionIntent<M> or be gated behind a where clause. Exact conditional ergonomics TBD at implementation time, but the principle holds: one generic M, all types derived from it.
AI Consumption
Section titled “AI Consumption”Decision: Agent impl controls it. No new trait or generic machinery. Each modality’s Agent implementation decides whether and how to include selection/focus in the AI context.
impl Agent for WhiteboardAgentContext { fn preamble(&self) -> String { let mut prompt = "You are editing a whiteboard...".to_string();
// Whiteboard decides: tell AI what's selected let selected = self.modality.selection(); if !selected.is_empty() { prompt.push_str("\n\nCurrently selected elements:\n"); for id in selected { if let Some(el) = self.modality.element(id) { prompt.push_str(&format!("- {}: {}\n", id, el.describe())); } } }
if let Some(id) = self.modality.focused() { prompt.push_str(&format!("\nFocused element: {}\n", id)); }
prompt }}
// Worksheet: no selection in AI contextimpl Agent for WorksheetAgentContext { fn preamble(&self) -> String { "You are editing a worksheet...".to_string() }}No modality is forced to include selection in its AI context. The Selectable/Focusable trait bounds give the Agent impl access to query selection if it wants to, but the decision is purely per-modality.
Clipboard Integration (the Session Clipboard RFC)
Section titled “Clipboard Integration (the Session Clipboard RFC)”Clipboard operates on the current selection. With the trait bound, clipboard logic can be generic:
fn copy<M: Modality + Selectable>(state: &SessionState<M>) -> ClipboardPayload { let selection = state.modality.selection(); // serialize selected items into clipboard payload M::serialize_selection(selection, &state.modality)}This suggests Selectable may also need a serialization method for clipboard. Deferred to the Session Clipboard RFC.
Undo Integration (the Session Undo/Redo RFC)
Section titled “Undo Integration (the Session Undo/Redo RFC)”Selection is ephemeral — not undo-tracked. Selection state stays as-is after undo/redo. This matches user expectations: undo affects content, not cursor/selection state.
Snapshot Integration
Section titled “Snapshot Integration”Selection and focus are included in modality snapshots so Dart can render selection indicators:
impl Modality for Whiteboard { fn snapshot(&self, ai: &AiState, export: &ExportState, version: u64) -> WhiteboardSnapshot { WhiteboardSnapshot { elements: self.compile_elements(), selected: self.selection().clone(), focused: self.focused().cloned(), // ... } }}Per-Modality Summary
Section titled “Per-Modality Summary”| Modality | Selectable | Focusable | AI uses selection |
|---|---|---|---|
| Whiteboard | HashSet<ElementId> | ElementId | Yes |
| Worksheet | HashSet<QuestionId> | — | No |
| LessonPlan | HashSet<SlideId> | SlideId | Slide name only |
| Document | TextSelection | — | TBD (the Document Modality RFC) |
Unresolved Questions
Section titled “Unresolved Questions”- Clipboard serialization — does
Selectableneed aserialize_selection()method, or is that clipboard’s concern? Deferred to the Session Clipboard RFC. - Multi-focus — some future modality might need multiple focused items (e.g. split-view editor). Current design is singular focus. Cross that bridge if we get there.
- Selection changed callback — should Session emit a separate event when selection changes, or is the normal snapshot emission sufficient? Likely sufficient.
- Document selection —
TextSelectionshape depends on the Document Modality RFC (Document Modality). Placeholder until that’s settled.
Dependencies
Section titled “Dependencies”- the Session Clipboard RFC (Clipboard) — depends on this RFC for selection-aware copy/cut
- the Document Modality RFC (Document) — Document’s
Selectiontype depends on document model - the Collaborative Presence RFC (Collaborative Presence) — builds on this RFC to sync selection/focus via Loro EphemeralStore