use sim_kernel::{Error, Expr, Result, Symbol};
use sim_lib_intent::{field, intent_kind_of};
use crate::pane::{new_pane, pane_id};
use crate::value::{get, int, set, with_panes};
#[derive(Clone, Debug)]
pub enum LayoutOp {
Open {
id: Symbol,
resource: Expr,
lens: Symbol,
dock: Symbol,
},
Close {
id: Symbol,
},
Move {
id: Symbol,
x: i64,
y: i64,
},
Resize {
id: Symbol,
w: i64,
h: i64,
},
Dock {
id: Symbol,
dock: Symbol,
},
Undock {
id: Symbol,
},
}
pub fn apply_layout_op(workspace: &Expr, op: &LayoutOp) -> Result<Expr> {
match op {
LayoutOp::Open {
id,
resource,
lens,
dock,
} => {
if find_pane(workspace, id).is_some() {
return Err(Error::HostError(format!("pane '{id}' already exists")));
}
let pane = new_pane(&id.name, resource.clone(), &lens.name, &dock.name);
let mut panes = crate::value::panes(workspace);
panes.push(pane);
Ok(set(
&with_panes(workspace, panes),
"focus",
Expr::Symbol(id.clone()),
))
}
LayoutOp::Close { id } => {
let panes = crate::value::panes(workspace)
.into_iter()
.filter(|pane| pane_id(pane).as_ref() != Some(id))
.collect();
let updated = with_panes(workspace, panes);
let focus_cleared = match get(&updated, "focus") {
Some(Expr::Symbol(focus)) if focus == id => set(&updated, "focus", Expr::Nil),
_ => updated,
};
Ok(focus_cleared)
}
LayoutOp::Move { id, x, y } => update_pane(workspace, id, |pane| {
let rect = set(get(pane, "rect").unwrap_or(&Expr::Nil), "x", int(*x));
let rect = set(&rect, "y", int(*y));
set(pane, "rect", rect)
}),
LayoutOp::Resize { id, w, h } => update_pane(workspace, id, |pane| {
let rect = set(get(pane, "rect").unwrap_or(&Expr::Nil), "w", int(*w));
let rect = set(&rect, "h", int(*h));
set(pane, "rect", rect)
}),
LayoutOp::Dock { id, dock } => update_pane(workspace, id, |pane| {
set(pane, "dock", Expr::Symbol(dock.clone()))
}),
LayoutOp::Undock { id } => update_pane(workspace, id, |pane| {
set(pane, "dock", Expr::Symbol(Symbol::new("float")))
}),
}
}
fn find_pane(workspace: &Expr, id: &Symbol) -> Option<Expr> {
crate::value::panes(workspace)
.into_iter()
.find(|pane| pane_id(pane).as_ref() == Some(id))
}
fn update_pane(workspace: &Expr, id: &Symbol, edit: impl Fn(&Expr) -> Expr) -> Result<Expr> {
let mut found = false;
let panes = crate::value::panes(workspace)
.iter()
.map(|pane| {
if pane_id(pane).as_ref() == Some(id) {
found = true;
edit(pane)
} else {
pane.clone()
}
})
.collect();
if !found {
return Err(Error::HostError(format!("no pane '{id}' to update")));
}
Ok(with_panes(workspace, panes))
}
pub fn layout_op_from_intent(intent: &Expr) -> Result<Option<LayoutOp>> {
let Some(kind) = intent_kind_of(intent) else {
return Ok(None);
};
match &*kind.name {
"open" => {
let resource = field(intent, "value").cloned().unwrap_or(Expr::Nil);
let id = require_symbol(intent, "pane")?;
Ok(Some(LayoutOp::Open {
id,
resource,
lens: Symbol::new("view:default"),
dock: Symbol::new("center"),
}))
}
"invoke" => layout_op_from_invoke(intent),
_ => Ok(None),
}
}
fn layout_op_from_invoke(intent: &Expr) -> Result<Option<LayoutOp>> {
let op = match field(intent, "op") {
Some(Expr::Symbol(symbol)) => symbol.name.to_string(),
_ => return Ok(None),
};
let id = require_symbol(intent, "target")?;
let args = field(intent, "args");
let arg_int = |name: &str| {
args.and_then(|args| get(args, name))
.and_then(crate::value::as_int)
};
match op.as_str() {
"close" => Ok(Some(LayoutOp::Close { id })),
"undock" => Ok(Some(LayoutOp::Undock { id })),
"dock" => {
let dock = match args.and_then(|args| get(args, "dock")) {
Some(Expr::Symbol(symbol)) => symbol.clone(),
_ => Symbol::new("center"),
};
Ok(Some(LayoutOp::Dock { id, dock }))
}
"move" => Ok(Some(LayoutOp::Move {
id,
x: arg_int("x").unwrap_or(0),
y: arg_int("y").unwrap_or(0),
})),
"resize" => Ok(Some(LayoutOp::Resize {
id,
w: arg_int("w").unwrap_or(0),
h: arg_int("h").unwrap_or(0),
})),
_ => Ok(None),
}
}
fn require_symbol(intent: &Expr, name: &str) -> Result<Symbol> {
match field(intent, name) {
Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
_ => Err(Error::HostError(format!(
"layout intent field '{name}' must be a symbol"
))),
}
}