1use sim_kernel::{Error, Expr, Result, Symbol};
9use sim_lib_intent::{field, intent_kind_of};
10
11use crate::pane::{new_pane, pane_id};
12use crate::value::{get, int, set, with_panes};
13
14#[derive(Clone, Debug)]
16pub enum LayoutOp {
17 Open {
19 id: Symbol,
21 resource: Expr,
23 lens: Symbol,
25 dock: Symbol,
27 },
28 Close {
30 id: Symbol,
32 },
33 Move {
35 id: Symbol,
37 x: i64,
39 y: i64,
41 },
42 Resize {
44 id: Symbol,
46 w: i64,
48 h: i64,
50 },
51 Dock {
53 id: Symbol,
55 dock: Symbol,
57 },
58 Undock {
60 id: Symbol,
62 },
63}
64
65pub fn apply_layout_op(workspace: &Expr, op: &LayoutOp) -> Result<Expr> {
67 match op {
68 LayoutOp::Open {
69 id,
70 resource,
71 lens,
72 dock,
73 } => {
74 if find_pane(workspace, id).is_some() {
75 return Err(Error::HostError(format!("pane '{id}' already exists")));
76 }
77 let pane = new_pane(&id.name, resource.clone(), &lens.name, &dock.name);
78 let mut panes = crate::value::panes(workspace);
79 panes.push(pane);
80 Ok(set(
81 &with_panes(workspace, panes),
82 "focus",
83 Expr::Symbol(id.clone()),
84 ))
85 }
86 LayoutOp::Close { id } => {
87 let panes = crate::value::panes(workspace)
88 .into_iter()
89 .filter(|pane| pane_id(pane).as_ref() != Some(id))
90 .collect();
91 let updated = with_panes(workspace, panes);
92 let focus_cleared = match get(&updated, "focus") {
93 Some(Expr::Symbol(focus)) if focus == id => set(&updated, "focus", Expr::Nil),
94 _ => updated,
95 };
96 Ok(focus_cleared)
97 }
98 LayoutOp::Move { id, x, y } => update_pane(workspace, id, |pane| {
99 let rect = set(get(pane, "rect").unwrap_or(&Expr::Nil), "x", int(*x));
100 let rect = set(&rect, "y", int(*y));
101 set(pane, "rect", rect)
102 }),
103 LayoutOp::Resize { id, w, h } => update_pane(workspace, id, |pane| {
104 let rect = set(get(pane, "rect").unwrap_or(&Expr::Nil), "w", int(*w));
105 let rect = set(&rect, "h", int(*h));
106 set(pane, "rect", rect)
107 }),
108 LayoutOp::Dock { id, dock } => update_pane(workspace, id, |pane| {
109 set(pane, "dock", Expr::Symbol(dock.clone()))
110 }),
111 LayoutOp::Undock { id } => update_pane(workspace, id, |pane| {
112 set(pane, "dock", Expr::Symbol(Symbol::new("float")))
113 }),
114 }
115}
116
117fn find_pane(workspace: &Expr, id: &Symbol) -> Option<Expr> {
118 crate::value::panes(workspace)
119 .into_iter()
120 .find(|pane| pane_id(pane).as_ref() == Some(id))
121}
122
123fn update_pane(workspace: &Expr, id: &Symbol, edit: impl Fn(&Expr) -> Expr) -> Result<Expr> {
124 let mut found = false;
125 let panes = crate::value::panes(workspace)
126 .iter()
127 .map(|pane| {
128 if pane_id(pane).as_ref() == Some(id) {
129 found = true;
130 edit(pane)
131 } else {
132 pane.clone()
133 }
134 })
135 .collect();
136 if !found {
137 return Err(Error::HostError(format!("no pane '{id}' to update")));
138 }
139 Ok(with_panes(workspace, panes))
140}
141
142pub fn layout_op_from_intent(intent: &Expr) -> Result<Option<LayoutOp>> {
148 let Some(kind) = intent_kind_of(intent) else {
149 return Ok(None);
150 };
151 match &*kind.name {
152 "open" => {
153 let resource = field(intent, "value").cloned().unwrap_or(Expr::Nil);
154 let id = require_symbol(intent, "pane")?;
155 Ok(Some(LayoutOp::Open {
156 id,
157 resource,
158 lens: Symbol::new("view:default"),
159 dock: Symbol::new("center"),
160 }))
161 }
162 "invoke" => layout_op_from_invoke(intent),
163 _ => Ok(None),
164 }
165}
166
167fn layout_op_from_invoke(intent: &Expr) -> Result<Option<LayoutOp>> {
168 let op = match field(intent, "op") {
169 Some(Expr::Symbol(symbol)) => symbol.name.to_string(),
170 _ => return Ok(None),
171 };
172 let id = require_symbol(intent, "target")?;
173 let args = field(intent, "args");
174 let arg_int = |name: &str| {
175 args.and_then(|args| get(args, name))
176 .and_then(crate::value::as_int)
177 };
178 match op.as_str() {
179 "close" => Ok(Some(LayoutOp::Close { id })),
180 "undock" => Ok(Some(LayoutOp::Undock { id })),
181 "dock" => {
182 let dock = match args.and_then(|args| get(args, "dock")) {
183 Some(Expr::Symbol(symbol)) => symbol.clone(),
184 _ => Symbol::new("center"),
185 };
186 Ok(Some(LayoutOp::Dock { id, dock }))
187 }
188 "move" => Ok(Some(LayoutOp::Move {
189 id,
190 x: arg_int("x").unwrap_or(0),
191 y: arg_int("y").unwrap_or(0),
192 })),
193 "resize" => Ok(Some(LayoutOp::Resize {
194 id,
195 w: arg_int("w").unwrap_or(0),
196 h: arg_int("h").unwrap_or(0),
197 })),
198 _ => Ok(None),
199 }
200}
201
202fn require_symbol(intent: &Expr, name: &str) -> Result<Symbol> {
203 match field(intent, name) {
204 Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
205 _ => Err(Error::HostError(format!(
206 "layout intent field '{name}' must be a symbol"
207 ))),
208 }
209}