//! [`LoadLayout`] and [`MutateLayout`] (ARCHITECTURE §6, §7; L4 + #4). //! //! A project owns **several named layouts** (see [`super::store`]). These two use //! cases operate on **one** layout — the one identified by `layout_id`, or the //! active layout when `layout_id` is `None`. They stay thin orchestrators: the //! mutating operations are the domain's pure `LayoutTree` functions. use std::sync::Arc; use domain::ports::{EventBus, FileSystem, ProjectStore}; use domain::{AgentId, Direction, DomainEvent, LayoutError, LayoutId, LayoutTree, LeafCell, NodeId, ProjectId, SessionId}; use crate::error::AppError; use super::store::{persist_doc, resolve_doc}; /// Maps a [`LayoutError`] to the application error type. fn map_layout_err(e: LayoutError) -> AppError { match e { LayoutError::NodeNotFound(id) => AppError::NotFound(format!("layout node {id}")), other => AppError::Invalid(other.to_string()), } } /// A layout mutation expressed in terms of the pure domain operations. /// /// Each variant maps 1:1 to a pure `LayoutTree` method /// (`split`/`merge`/`resize`/`move`/`set_session`). Decoupling the *operation* /// from the *tree* keeps the use case a thin orchestrator and lets the /// presentation layer (and undo/redo) speak in intentions. #[derive(Debug, Clone, PartialEq)] pub enum LayoutOperation { /// Split a leaf into a two-child split (original + a new empty leaf). Split { /// The leaf to split. target: NodeId, /// Row (columns) or Column (rows). direction: Direction, /// Id for the new sibling leaf. new_leaf: NodeId, /// Id for the wrapping split container. container: NodeId, }, /// Collapse a split container back to one of its children. Merge { /// The split container to collapse. container: NodeId, /// Index of the child to keep. keep_index: usize, }, /// Reassign the relative weights of a split's children. Resize { /// The split container to resize. container: NodeId, /// New weights (one per child, all `> 0`). weights: Vec, }, /// Move the session hosted by one leaf to another (empty) leaf. Move { /// Source leaf (left empty). from: NodeId, /// Target leaf (must be empty). to: NodeId, }, /// Attach or detach a session to/from a leaf (cell ↔ terminal binding). SetSession { /// The hosting leaf. target: NodeId, /// Session to host, or `None` to clear. session: Option, }, /// Attach or detach an agent to/from a leaf (per-cell agent, feature #3). SetCellAgent { /// The hosting leaf. target: NodeId, /// Agent to associate, or `None` to clear. agent: Option, }, } impl LayoutOperation { /// Applies this operation to `tree`, returning the new validated tree. fn apply(&self, tree: &LayoutTree) -> Result { let result = match self { Self::Split { target, direction, new_leaf, container, } => tree.split( *target, *direction, LeafCell { id: *new_leaf, session: None, agent: None, }, *container, ), Self::Merge { container, keep_index, } => tree.merge(*container, *keep_index), Self::Resize { container, weights } => tree.resize(*container, weights), Self::Move { from, to } => tree.move_session(*from, *to), Self::SetSession { target, session } => tree.set_session(*target, *session), Self::SetCellAgent { target, agent } => tree.set_cell_agent(*target, *agent), }; result.map_err(map_layout_err) } } /// Input for [`LoadLayout::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LoadLayoutInput { /// Project whose layout to load. pub project_id: ProjectId, /// Which named layout to load; `None` loads the active one. pub layout_id: Option, } /// Output of [`LoadLayout::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct LoadLayoutOutput { /// The id of the layout that was loaded (resolved from active when omitted). pub layout_id: LayoutId, /// The loaded layout tree. pub layout: LayoutTree, } /// Loads one named layout (the active one by default), self-healing / migrating /// the layouts store as needed (see [`super::store::resolve_doc`]). pub struct LoadLayout { store: Arc, fs: Arc, } impl LoadLayout { /// Builds the use case from its injected ports. #[must_use] pub fn new(store: Arc, fs: Arc) -> Self { Self { store, fs } } /// Executes the load. /// /// # Errors /// - [`AppError::NotFound`] if the project or the requested layout is unknown, /// - [`AppError::FileSystem`] if seeding the default layouts fails to persist, /// - [`AppError::Store`] on registry I/O failure. pub async fn execute(&self, input: LoadLayoutInput) -> Result { let project = self.store.load_project(input.project_id).await?; let doc = resolve_doc(self.fs.as_ref(), &project).await?; let id = doc.resolve_id(input.layout_id); let named = doc .find(id) .ok_or_else(|| AppError::NotFound(format!("layout {id}")))?; Ok(LoadLayoutOutput { layout_id: id, layout: named.tree.clone(), }) } } /// Input for [`MutateLayout::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct MutateLayoutInput { /// Project whose layout to mutate. pub project_id: ProjectId, /// Which named layout to mutate; `None` mutates the active one. pub layout_id: Option, /// The operation to apply. pub operation: LayoutOperation, } /// Output of [`MutateLayout::execute`]. #[derive(Debug, Clone, PartialEq)] pub struct MutateLayoutOutput { /// The id of the mutated layout. pub layout_id: LayoutId, /// The new, persisted layout tree. pub layout: LayoutTree, } /// Applies a pure layout operation to one named layout, persists the whole /// layouts store and publishes [`DomainEvent::LayoutChanged`]. pub struct MutateLayout { store: Arc, fs: Arc, events: Arc, } impl MutateLayout { /// Builds the use case from its injected ports. #[must_use] pub fn new( store: Arc, fs: Arc, events: Arc, ) -> Self { Self { store, fs, events } } /// Executes the mutation. /// /// # Errors /// - [`AppError::NotFound`] if the project, layout or a referenced node is unknown, /// - [`AppError::Invalid`] if the operation violates a layout invariant, /// - [`AppError::FileSystem`] on persistence failure, /// - [`AppError::Store`] on registry I/O failure. pub async fn execute(&self, input: MutateLayoutInput) -> Result { let project = self.store.load_project(input.project_id).await?; let mut doc = resolve_doc(self.fs.as_ref(), &project).await?; let id = doc.resolve_id(input.layout_id); let named = doc .find_mut(id) .ok_or_else(|| AppError::NotFound(format!("layout {id}")))?; let next = input.operation.apply(&named.tree)?; named.tree = next.clone(); persist_doc(self.fs.as_ref(), &project, &doc).await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project_id, }); Ok(MutateLayoutOutput { layout_id: id, layout: next, }) } }