colors: add workspaces

This commit is contained in:
Mark Poliakov 2025-03-03 13:05:38 +02:00
parent c4e3128528
commit 8493573721
9 changed files with 1051 additions and 762 deletions

View File

@ -4,7 +4,6 @@ use libcolors::{
};
// use yggdrasil_abi::io::{KeyboardKey, KeyboardKeyEvent};
#[derive(Default)]
pub struct InputState {
lshift: bool,

View File

@ -13,12 +13,12 @@ use input::InputState;
use libcolors::{
event::{EventData, KeyEvent, KeyModifiers, KeyboardKeyEvent, WindowEvent, WindowInfo},
input::Key,
message::{ClientMessage, CreateWindowInfo, WindowType},
message::{ClientMessage, CreateWindowInfo},
};
use sys::{Backend, DisplaySurface, FromClient, Point, ServerSender, WindowServer};
use uipc::PeerAddress;
use window::Window;
use wm::{Direction, Workspace};
use wm::{display::Display, layout::Direction};
pub mod input;
pub mod sys;
@ -36,7 +36,7 @@ pub struct Server<'a> {
last_client_id: u32,
client_map: HashMap<u32, PeerAddress>,
workspace: Workspace<u32>,
display: Display<u32>,
windows: BTreeMap<u32, Window<'a>>,
last_window_id: u32,
@ -55,7 +55,7 @@ impl<'a> Server<'a> {
client_map: HashMap::new(),
windows: BTreeMap::new(),
workspace: Workspace::new(800, 600),
display: Display::new(800, 600),
last_window_id: 1,
_pd: PhantomData,
@ -71,19 +71,11 @@ impl<'a> Server<'a> {
) -> Result<(WindowInfo, RawFd), Error> {
let wid = self.last_window_id;
self.last_window_id += 1;
let need_focus = match info.ty {
WindowType::Default => {
if !self.workspace.create_window(wid) {
todo!()
}
true
}
WindowType::Reservation(height) => {
self.workspace.add_reservation(wid, height);
false
}
};
let frame = self.workspace.window_frame(wid).unwrap();
let need_focus = self
.display
.create_window(wid, &info.ty)
.expect("TODO: handle create window failed");
let frame = self.display.window_frame(wid).unwrap();
let mapping_size = surface.width() * surface.height() * 4;
// let mapping_size = self.display.width() * self.display.height() * 4;
let surface_shm = SharedMemory::new(mapping_size).unwrap();
@ -118,12 +110,13 @@ impl<'a> Server<'a> {
if need_focus {
self.focus_window(tx, wid);
}
self.redraw_all(tx);
Ok((info, fd))
}
fn focus_window(&mut self, tx: &mut ServerSender, wid: u32) {
let (old_wid, new) = self.workspace.focus_window(wid);
let (old_wid, new) = self.display.current_workspace_mut().focus_window(wid);
let old = old_wid.and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
@ -147,33 +140,41 @@ impl<'a> Server<'a> {
}
fn move_focus(&mut self, tx: &mut ServerSender, direction: Direction) {
if let Some(wid) = self.workspace.window_towards_wrap(direction) {
if let Some(wid) = self
.display
.current_workspace_mut()
.window_towards_wrap(direction)
{
self.focus_window(tx, wid);
}
}
fn redraw_all(&mut self, tx: &mut ServerSender) {
self.display.all_windows().for_each(|(wid, _)| {
if let Some(window) = self.windows.get(&wid) {
tx.send_event(
EventData::WindowEvent(wid, WindowEvent::RedrawRequested),
&window.peer,
);
}
});
}
fn move_window(
&mut self,
surface: &mut DisplaySurface,
tx: &mut ServerSender,
direction: Direction,
) {
if self.workspace.move_window(direction) {
if self.display.move_window(direction) {
surface.fill(self.background);
self.flush_dirty_frames(tx);
self.workspace.all_windows().for_each(|(wid, _)| {
if let Some(window) = self.windows.get(&wid) {
tx.send_event(
EventData::WindowEvent(wid, WindowEvent::RedrawRequested),
&window.peer,
);
}
});
self.redraw_all(tx);
}
}
fn flush_dirty_frames(&mut self, tx: &mut ServerSender) {
for (wid, rect) in self.workspace.dirty_windows() {
for (wid, rect) in self.display.dirty_windows() {
let Some(window) = self.windows.get_mut(&wid) else {
continue;
};
@ -193,11 +194,72 @@ impl<'a> Server<'a> {
);
}
}
fn switch_to_workspace(&mut self, tx: &mut ServerSender, next: usize) -> bool {
if next == self.display.current_workspace || next >= Display::<u32>::MAX {
return false;
}
log::info!(
"Switch workspace {} -> {}",
self.display.current_workspace,
next
);
let old_focus_wid = self.display.current_workspace().focused_window();
let old_focus = old_focus_wid.and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
});
if let Some((old_wid, old_window)) = old_focus {
log::info!("Unfocus #{old_wid}");
tx.send_event(
EventData::WindowEvent(old_wid, WindowEvent::FocusChanged(false)),
&old_window.peer,
);
}
self.display.current_workspace = next;
let new_focus_wid = self.display.current_workspace().focused_window();
let new_focus = new_focus_wid.and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
});
if let Some((new_wid, new_window)) = new_focus {
log::info!("Focus #{new_wid}");
tx.send_event(
EventData::WindowEvent(new_wid, WindowEvent::FocusChanged(true)),
&new_window.peer,
);
}
// Send redraw to all frames
self.flush_dirty_frames(tx);
self.redraw_all(tx);
true
}
fn send_focus_to_workspace(&mut self, tx: &mut ServerSender, target: usize) -> bool {
if target == self.display.current_workspace && target >= Display::<u32>::MAX {
return false;
}
if !self.display.send_focused_window(target) {
return false;
}
self.flush_dirty_frames(tx);
self.redraw_all(tx);
true
}
}
impl WindowServer for Server<'_> {
fn handle_initial(&mut self, mut surface: DisplaySurface) {
self.workspace
self.display
.resize(surface.width() as u32, surface.height() as u32);
surface.fill(self.background);
surface.present();
@ -242,7 +304,7 @@ impl WindowServer for Server<'_> {
} => {
log::trace!("{:?}: BlitWindow", peer);
if let Some(window) = self.windows.get(&window_id) {
let Some(frame) = self.workspace.window_frame(window_id) else {
let Some(frame) = self.display.window_frame(window_id) else {
return;
};
@ -274,11 +336,32 @@ impl WindowServer for Server<'_> {
ClientMessage::DestroyWindow(wid) => {
log::info!("{:?}: DestroyWindow", peer);
let window = self.windows.remove(&wid);
let old_focus = self.display.current_workspace().focused_window();
if window.is_some() {
self.workspace.remove_window(wid);
self.flush_dirty_frames(tx);
if self.display.remove_window(wid) {
surface.fill(self.background);
self.flush_dirty_frames(tx);
self.redraw_all(tx);
let focus = self.display.current_workspace().focused_window();
if focus != old_focus {
let focus = focus.and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
});
if let Some((wid, window)) = focus {
tx.send_event(
EventData::WindowEvent(wid, WindowEvent::FocusChanged(true)),
&window.peer,
);
}
}
surface.present();
}
}
surface.present();
}
}
}
@ -306,6 +389,22 @@ impl WindowServer for Server<'_> {
}
match (input.modifiers, input.key) {
(KeyModifiers::ALT, Key::Char(c)) if c.is_ascii_digit() => {
let number = ((c + 9) - b'0') % 10;
if self.switch_to_workspace(tx, number as usize) {
surface.fill(self.background);
surface.present();
}
return;
}
(KeyModifiers::ALT_SHIFT, Key::Char(c)) if c.is_ascii_digit() => {
let number = ((c + 9) - b'0') % 10;
if self.send_focus_to_workspace(tx, number as usize) {
surface.fill(self.background);
surface.present();
}
return;
}
(KeyModifiers::ALT, Key::Char(b'l')) => {
self.move_focus(tx, Direction::Right);
return;
@ -344,22 +443,32 @@ impl WindowServer for Server<'_> {
}
_ => (),
}
// // Window keys
// if let Some((row, col)) = self.focused_frame {
// let row_len = self.rows[row].frames.len();
// if let Some((_, window)) = self.get_focused_window() {
// } else {
// self.focused_frame = None;
// }
}
let focus = self.workspace.focused_window().and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
});
let focus = self
.display
.current_workspace()
.focused_window()
.and_then(|wid| {
let window = self.windows.get(&wid)?;
Some((wid, window))
});
if let Some((wid, window)) = focus {
if event.state {
match (input.modifiers, input.key) {
(KeyModifiers::ALT_SHIFT, Key::Char(b'q')) => {
log::info!("Send close request -> #{wid}");
tx.send_event(
EventData::WindowEvent(wid, WindowEvent::CloseRequested),
&window.peer,
);
return;
}
_ => (),
}
}
if event.state {
tx.send_event(
EventData::WindowEvent(

View File

@ -6,15 +6,15 @@ use libcolors::{
};
use uipc::{PeerAddress, Sender};
#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
pub mod yggdrasil;
#[cfg(any(rust_analyzer, unix))]
pub mod unix;
#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
pub use yggdrasil::{Backend, DisplaySurface, Error};
pub mod yggdrasil;
#[cfg(any(rust_analyzer, unix))]
pub use unix::{Backend, DisplaySurface, Error};
#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
pub use yggdrasil::{Backend, DisplaySurface, Error};
#[derive(Debug)]
pub struct FromClient {

View File

@ -165,7 +165,12 @@ impl<S: WindowServer> Backend<S> {
let mut surface = softbuffer::Surface::new(&context, self.window.clone())?;
let size = self.window.inner_size();
surface.resize(NonZero::new(size.width).unwrap(), NonZero::new(size.height).unwrap()).unwrap();
surface
.resize(
NonZero::new(size.width).unwrap(),
NonZero::new(size.height).unwrap(),
)
.unwrap();
self.server.handle_initial(DisplaySurface {
inner: surface.buffer_mut().unwrap(),
width: size.width as _,

View File

@ -145,7 +145,7 @@ impl<S: WindowServer> Backend<'_, S> {
pub fn run(mut self) -> Result<(), Error> {
self.server.handle_initial(DisplaySurface {
display: &mut self.display
display: &mut self.display,
});
loop {
@ -169,7 +169,8 @@ impl<S: WindowServer> Backend<'_, S> {
let surface = DisplaySurface {
display: &mut self.display,
};
self.server.handle_keyboard_event(surface, &mut self.tx, event);
self.server
.handle_keyboard_event(surface, &mut self.tx, event);
}
}
}
@ -271,8 +272,5 @@ fn convert_key_event(raw: yggdrasil_abi::io::KeyboardKeyEvent) -> Option<Keyboar
KeyboardKey::F(_) => return None,
};
Some(KeyboardKeyEvent {
key,
state
})
Some(KeyboardKeyEvent { key, state })
}

View File

@ -0,0 +1,212 @@
use std::{fmt, hash::Hash, iter};
use libcolors::message::WindowType;
use super::{
layout::{Direction, NodeLayout, Rect},
workspace::Workspace,
};
pub struct Display<T> {
workspaces: Vec<Workspace<T>>,
width: u32,
height: u32,
reservations_top: Vec<Reservation<T>>,
reservation_height: u32,
pub current_workspace: usize,
}
pub struct Reservation<T> {
layout: NodeLayout,
size: u32,
wid: T,
}
impl<T: Eq + Hash + Copy> Display<T> {
pub const MAX: usize = 10;
pub fn new(width: u32, height: u32) -> Self {
let workspaces = (0..Self::MAX)
.map(|_| Workspace::new(width, height))
.collect();
Self {
workspaces,
width,
height,
reservations_top: Vec::new(),
reservation_height: 0,
current_workspace: 0,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
self.width = width;
self.height = height;
// Re-do reservation layout
self.update_reservation_layout();
for workspace in self.workspaces.iter_mut() {
workspace.resize(width, height, self.reservation_height);
}
}
pub fn workspace(&self, index: usize) -> &Workspace<T> {
&self.workspaces[index]
}
pub fn workspace_mut(&mut self, index: usize) -> &mut Workspace<T> {
&mut self.workspaces[index]
}
pub fn current_workspace(&self) -> &Workspace<T> {
&self.workspaces[self.current_workspace]
}
pub fn current_workspace_mut(&mut self) -> &mut Workspace<T> {
&mut self.workspaces[self.current_workspace]
}
pub fn all_windows(&self) -> impl Iterator<Item = (T, &NodeLayout)> + '_ {
let reservation_windows = self.reservation_windows();
let workspace_windows = self.current_workspace().all_windows();
iter::chain(reservation_windows, workspace_windows)
}
pub fn dirty_windows(&self) -> impl Iterator<Item = (T, Rect)> + '_ {
let reservation_windows = self.dirty_reservation_windows();
let workspace_windows = self.current_workspace().dirty_windows();
iter::chain(reservation_windows, workspace_windows)
}
pub fn window_frame(&self, wid: T) -> Option<Rect> {
if let Some(reservation) = self.lookup_reservation(wid) {
return reservation.layout.get();
}
self.current_workspace().window_frame(wid)
}
pub fn create_window(&mut self, wid: T, ty: &WindowType) -> Option<bool> {
match ty {
&WindowType::Reservation(height) => {
self.add_reservation(wid, height);
Some(false)
}
WindowType::Default => {
if !self.current_workspace_mut().create_window(wid) {
return None;
}
self.update_reservation_layout();
Some(true)
}
}
}
pub fn move_window(&mut self, direction: Direction) -> bool
where
T: fmt::Debug,
{
if self.current_workspace_mut().move_window(direction) {
self.update_reservation_layout();
true
} else {
false
}
}
pub fn remove_window(&mut self, wid: T) -> bool {
// TODO check if the window is a reservation
let mut reservation_removed = false;
self.reservations_top.retain(|res| {
if res.wid == wid {
reservation_removed = true;
false
} else {
true
}
});
if reservation_removed {
self.update_layout(true);
return true;
}
for workspace in self.workspaces.iter_mut() {
if workspace.remove_window(wid) {
self.update_reservation_layout();
return true;
}
}
false
}
pub fn send_focused_window(&mut self, target: usize) -> bool {
let Some(focus_wid) = self.current_workspace().focused_window() else {
return false;
};
if !self.workspaces[target].create_window(focus_wid) {
return false;
}
self.workspaces[target].focus_window(focus_wid);
self.current_workspace_mut().remove_window(focus_wid);
true
}
pub fn add_reservation(&mut self, wid: T, height: u32) {
self.reservations_top.push(Reservation {
layout: NodeLayout::empty(),
size: height,
wid,
});
self.update_layout(true);
}
fn dirty_reservation_windows(&self) -> impl Iterator<Item = (T, Rect)> + '_ {
self.reservation_windows().filter_map(|(wid, layout)| {
if !layout.clear_dirty() {
return None;
}
let rect = layout.get()?;
Some((wid, rect))
})
}
fn reservation_windows(&self) -> impl Iterator<Item = (T, &NodeLayout)> + '_ {
self.reservations_top
.iter()
.map(|res| (res.wid, &res.layout))
}
fn lookup_reservation(&self, wid: T) -> Option<&Reservation<T>> {
self.reservations_top.iter().find(|r| r.wid == wid)
}
// Will also update the workspaces' layouts if reservation height changes
fn update_reservation_layout(&mut self) -> bool {
let mut res_y = 0;
for reservation in self.reservations_top.iter() {
reservation.layout.set(Rect {
x: 0,
y: res_y,
w: self.width,
h: reservation.size,
});
res_y += reservation.size;
}
let changed = res_y != self.reservation_height;
self.reservation_height = res_y;
changed
}
fn update_layout(&mut self, force: bool) {
if self.update_reservation_layout() || force {
for workspace in self.workspaces.iter_mut() {
workspace.resize(self.width, self.height, self.reservation_height);
}
}
}
// pub fn add_reservation(&mut self, wid: T, height: u32) {
// }
}

View File

@ -0,0 +1,67 @@
use std::cell::Cell;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Orientation {
Horizontal,
Vertical,
}
pub struct NodeLayout {
pub rect: Cell<(Option<Rect>, bool)>,
}
impl NodeLayout {
pub fn empty() -> Self {
Self {
rect: Cell::new((None, false)),
}
}
pub fn set(&self, new: Rect) {
let (old, _) = self.rect.get();
let dirty = old.map_or(true, |old| old != new);
self.rect.set((Some(new), dirty));
}
pub fn get(&self) -> Option<Rect> {
let (value, _) = self.rect.get();
value
}
pub fn clear(&self) {
self.rect.set((None, false));
}
pub fn clear_dirty(&self) -> bool {
let (value, dirty) = self.rect.get();
self.rect.set((value, false));
dirty && value.is_some()
}
}
impl Direction {
pub fn split(&self) -> (Orientation, isize) {
match self {
Self::Left => (Orientation::Horizontal, -1),
Self::Right => (Orientation::Horizontal, 1),
Self::Up => (Orientation::Vertical, -1),
Self::Down => (Orientation::Vertical, 1),
}
}
}

View File

@ -1,703 +1,3 @@
use std::{cell::Cell, collections::HashMap, hash::Hash, iter};
pub type NodeId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Orientation {
Horizontal,
Vertical,
}
pub enum NodeContent<T> {
Container(ContainerNode),
Window(WindowNode<T>),
}
pub struct NodeLayout {
rect: Cell<(Option<Rect>, bool)>,
}
pub struct Node<T> {
parent: Option<NodeId>,
layout: NodeLayout,
content: NodeContent<T>,
}
pub struct ContainerNode {
children: Vec<NodeId>,
orientation: Orientation,
}
pub struct WindowNode<T> {
pub wid: T,
}
pub struct Reservation<T> {
layout: NodeLayout,
size: u32,
wid: T,
}
pub struct Workspace<T> {
nodes: HashMap<NodeId, Node<T>>,
wid_to_nid: HashMap<T, NodeId>,
last_node_id: NodeId,
root: NodeId,
focus: Option<NodeId>,
margin: u32,
spacing: u32,
reservations_top: Vec<Reservation<T>>,
reservation_top: u32,
width: u32,
height: u32,
}
impl<T> Node<T> {
pub fn as_window(&self) -> Option<&WindowNode<T>> {
match &self.content {
NodeContent::Window(window) => Some(window),
_ => None,
}
}
pub fn as_container(&self) -> Option<&ContainerNode> {
match &self.content {
NodeContent::Container(container) => Some(container),
_ => None,
}
}
pub fn as_container_mut(&mut self) -> Option<&mut ContainerNode> {
match &mut self.content {
NodeContent::Container(container) => Some(container),
_ => None,
}
}
}
impl NodeLayout {
pub fn set(&self, new: Rect) {
let (old, _) = self.rect.get();
let dirty = old.map_or(true, |old| old != new);
self.rect.set((Some(new), dirty));
}
pub fn get(&self) -> Option<Rect> {
let (value, _) = self.rect.get();
value
}
pub fn clear(&self) {
self.rect.set((None, false));
}
pub fn clear_dirty(&self) -> bool {
let (value, dirty) = self.rect.get();
self.rect.set((value, false));
dirty && value.is_some()
}
}
impl<T: Eq + Hash + Copy> Workspace<T> {
pub fn new(width: u32, height: u32) -> Self {
let root = Node {
parent: None,
layout: NodeLayout {
rect: Cell::new((None, false)),
},
content: NodeContent::Container(ContainerNode {
children: vec![],
orientation: Orientation::Horizontal,
}),
};
let nodes = HashMap::from_iter([(0, root)]);
let mut this = Self {
nodes,
wid_to_nid: HashMap::new(),
last_node_id: 0,
margin: 4,
spacing: 4,
root: 0,
focus: None,
reservation_top: 0,
reservations_top: Vec::new(),
width,
height,
};
this.update_layout();
this
}
pub fn add_reservation(&mut self, wid: T, height: u32) {
self.reservations_top.push(Reservation {
layout: NodeLayout {
rect: Cell::new((None, false)),
},
size: height,
wid,
});
self.update_layout();
}
pub fn all_windows(&self) -> impl Iterator<Item = (T, &NodeLayout)> + '_ {
let nodes_windows = self.nodes.iter().filter_map(|(_, node)| {
let window = node.as_window()?;
Some((window.wid, &node.layout))
// Some(node.as_window()?.wid)
});
let reservation_windows = self
.reservations_top
.iter()
.map(|res| (res.wid, &res.layout));
iter::chain(reservation_windows, nodes_windows)
}
pub fn dirty_windows(&self) -> impl Iterator<Item = (T, Rect)> + '_ {
self.all_windows().filter_map(|(wid, layout)| {
if !layout.clear_dirty() {
return None;
}
let rect = layout.get()?;
Some((wid, rect))
})
}
pub fn window_towards_wrap(&self, direction: Direction) -> Option<T> {
let current_nid = self.focus?;
let current_node = self.nodes.get(&current_nid)?;
let parent_nid = current_node.parent?;
let parent_node = self.nodes.get(&parent_nid)?;
let parents_parent_nid = parent_node.parent;
let parent_container = parent_node.as_container()?;
let (orientation, delta) = direction.split();
let nid = if orientation == parent_container.orientation {
log::info!("Within parent {delta}, {:?}", parent_container.orientation);
let position_in_parent = parent_container
.children
.iter()
.position(|&n| n == current_nid)?;
if delta > 0 {
if position_in_parent < parent_container.children.len() - 1 {
parent_container.children[position_in_parent + 1]
} else {
parent_container.children[0]
}
} else {
if position_in_parent > 0 {
parent_container.children[position_in_parent - 1]
} else {
parent_container.children[parent_container.children.len() - 1]
}
}
} else if let Some(parents_parent_nid) = parents_parent_nid {
let parents_parent_node = self.nodes.get(&parents_parent_nid)?;
let parents_parent_container = parents_parent_node.as_container()?;
assert_eq!(parents_parent_container.orientation, orientation);
let position_in_parent = parents_parent_container
.children
.iter()
.position(|&n| n == parent_nid)?;
log::info!(
"Within parent's parent {delta}, {:?}",
parents_parent_container.orientation
);
log::info!("position_in_parent = {position_in_parent}");
if delta > 0 {
if position_in_parent < parent_container.children.len() - 1 {
parents_parent_container.children[position_in_parent + 1]
} else {
parents_parent_container.children[0]
}
} else {
if position_in_parent > 0 {
parents_parent_container.children[position_in_parent - 1]
} else {
parents_parent_container.children[parent_container.children.len() - 1]
}
}
} else {
return None;
};
self.first_window_in(nid)
}
pub fn first_window_in(&self, nid: NodeId) -> Option<T> {
let node = self.nodes.get(&nid)?;
match &node.content {
NodeContent::Window(window) => Some(window.wid),
NodeContent::Container(container) => {
let first = *container.children.first()?;
self.first_window_in(first)
}
}
}
pub fn move_window(&mut self, direction: Direction) -> bool
where
T: core::fmt::Debug,
{
if let Some(container_nid) = self.move_window_inner(direction) {
self.invalidate_layout(container_nid);
self.fixup_containers(self.root);
self.dump(self.root, 0);
true
} else {
false
}
}
fn dump(&self, nid: NodeId, depth: usize)
where
T: core::fmt::Debug,
{
let Some(node) = self.nodes.get(&nid) else {
return;
};
let rect = node.layout.get();
match &node.content {
NodeContent::Window(window) => {
log::info!(
"{:depth$}Window #{:?} {rect:?}",
"",
window.wid,
depth = depth * 2
);
}
NodeContent::Container(container) => {
log::info!("{:depth$}Container {rect:?} {{", "", depth = depth * 2);
for &child_nid in container.children.iter() {
self.dump(child_nid, depth + 1);
}
log::info!("{:depth$}}}", "", depth = depth * 2);
}
}
}
// pub fn walk_windows<F: FnMut(T)>(&self, mut mapper: F) {
// self.walk_windows_inner(&mut mapper, self.root)
// }
// fn walk_windows_inner<F: FnMut(T)>(&self, mapper: &mut F, nid: NodeId) {
// // let Some(node) = self.nodes.get(&nid) else {
// // return;
// // };
// // match &node.content {
// // NodeContent::Container(container) => {
// // for &child_nid in container.children.iter() {
// // self.walk_windows_inner(mapper, child_nid);
// // }
// // }
// // NodeContent::Window(window) => {
// // mapper(window.wid);
// // }
// // }
// }
fn invalidate_layout(&mut self, nid: NodeId) {
for reservation in self.reservations_top.iter() {
reservation.layout.clear();
}
self.invalidate_layout_inner(nid);
self.update_layout();
}
fn invalidate_layout_inner(&self, nid: NodeId) {
let Some(node) = self.nodes.get(&nid) else {
return;
};
node.layout.clear();
match &node.content {
NodeContent::Container(container) => {
for &child_nid in container.children.iter() {
self.invalidate_layout_inner(child_nid);
}
}
_ => (),
}
}
fn fixup_containers(&mut self, nid: NodeId) {
let Some(node) = self.nodes.get_mut(&nid) else {
return;
};
if let NodeContent::Container(container) = &mut node.content {
if container.children.is_empty() {
let Some(parent_nid) = node.parent else {
return;
};
// Remove the empty container
if let Some(parent_container) = self
.nodes
.get_mut(&parent_nid)
.and_then(Node::as_container_mut)
{
parent_container.children.retain(|&n| n != nid);
self.nodes.remove(&nid);
self.invalidate_layout(parent_nid);
}
} else if container.children.len() == 1 {
// Remove the empty container and reparent its only child
if let Some(parent_nid) = node.parent {
let child_nid = container.children.remove(0);
let [Some(child), Some(parent)] =
self.nodes.get_many_mut([&child_nid, &parent_nid])
else {
return;
};
child.parent = Some(parent_nid);
if let Some(parent_container) = parent.as_container_mut() {
parent_container.children.retain(|&n| n != nid);
parent_container.children.push(child_nid);
}
self.invalidate_layout(parent_nid);
}
} else {
// TODO borrowing stuff
let children = container.children.clone();
for child_nid in children {
self.fixup_containers(child_nid);
}
}
}
}
fn move_window_inner(&mut self, direction: Direction) -> Option<NodeId> {
let current_nid = self.focus?;
let current_node = self.nodes.get(&current_nid)?;
let parent_nid = current_node.parent?;
let parent_node = self.nodes.get_mut(&parent_nid)?;
let parents_parent_nid = parent_node.parent;
let parent_container = parent_node.as_container_mut()?;
let (orientation, delta) = direction.split();
if parent_container.orientation == orientation {
let position_in_parent = parent_container
.children
.iter()
.position(|&n| n == current_nid)?;
// TODO check if this item is also a container
if delta < 0 && position_in_parent > 0 {
parent_container
.children
.swap(position_in_parent - 1, position_in_parent);
Some(parent_nid)
} else if delta > 0 && position_in_parent < parent_container.children.len() - 1 {
parent_container
.children
.swap(position_in_parent, position_in_parent + 1);
Some(parent_nid)
} else {
None
}
} else if let Some(parents_parent_nid) = parents_parent_nid {
// Go to parent's parent
let parents_parent_node = self.nodes.get_mut(&parents_parent_nid)?;
let parents_parent_container = parents_parent_node.as_container_mut()?;
assert_eq!(parents_parent_container.orientation, orientation);
todo!()
} else {
self.last_node_id += 1;
let new_root_nid = self.last_node_id;
// Remove child from parent
parent_container.children.retain(|&n| n != current_nid);
self.nodes.get_mut(&current_nid)?.parent = Some(new_root_nid);
self.nodes.get_mut(&self.root)?.parent = Some(new_root_nid);
let children = if delta > 0 {
vec![self.root, current_nid]
} else {
vec![current_nid, self.root]
};
// Create a new root
let new_root = Node {
parent: None,
layout: NodeLayout {
rect: Cell::new((None, false)),
},
content: NodeContent::Container(ContainerNode {
children,
orientation,
}),
};
self.root = new_root_nid;
self.nodes.insert(new_root_nid, new_root);
Some(self.root)
}
}
pub fn focus_window(&mut self, wid: T) -> (Option<T>, Option<NodeId>) {
let old = self.focus.take().and_then(|nid| self.wid(nid));
let new = self.wid_to_nid.get(&wid).copied();
self.focus = new;
(old, new)
}
pub fn focused_window(&self) -> Option<T> {
let nid = self.focus?;
Some(self.nodes.get(&nid)?.as_window()?.wid)
}
pub fn window(&self, wid: T) -> Option<&Node<T>> {
let nid = *self.wid_to_nid.get(&wid)?;
self.nodes.get(&nid)
}
pub fn reservation(&self, wid: T) -> Option<&Reservation<T>> {
self.reservations_top.iter().find(|r| r.wid == wid)
}
pub fn window_frame(&self, wid: T) -> Option<Rect> {
if let Some(reservation) = self.reservation(wid) {
reservation.layout.get()
} else {
let node = self.window(wid)?;
node.layout.get()
}
}
pub fn resize(&mut self, w: u32, h: u32) {
self.width = w;
self.height = h;
self.update_layout();
}
fn update_reservation_layout(&mut self) -> u32 {
let mut res_y = 0;
for reservation in self.reservations_top.iter() {
reservation.layout.set(Rect {
x: 0,
y: res_y,
w: self.width,
h: reservation.size,
});
res_y += reservation.size;
}
self.reservation_top = res_y;
res_y
}
pub fn update_layout(&mut self) {
let res_y = self.update_reservation_layout();
self.update_layout_for(
self.root,
Rect {
x: self.margin,
y: self.margin + res_y,
w: self.width - self.margin * 2,
h: self.height - self.margin * 2 - res_y,
},
);
}
pub fn create_window(&mut self, wid: T) -> bool {
if self.create_window_in(self.root, wid) {
for res in self.reservations_top.iter() {
res.layout.clear();
}
self.update_reservation_layout();
true
} else {
false
}
}
pub fn remove_window(&mut self, wid: T) -> bool {
let Some(nid) = self.wid_to_nid.remove(&wid) else {
return false;
};
if self.remove_inner(nid) {
self.fixup_containers(self.root);
self.update_layout();
true
} else {
false
}
}
fn create_window_in(&mut self, container_nid: NodeId, wid: T) -> bool {
self.last_node_id += 1;
let nid = self.last_node_id;
self.nodes.insert(
nid,
Node {
parent: Some(container_nid),
layout: NodeLayout {
rect: Cell::new((None, false)),
},
content: NodeContent::Window(WindowNode { wid }),
},
);
self.wid_to_nid.insert(wid, nid);
if !self.add_child_to(container_nid, nid) {
self.wid_to_nid.remove(&wid);
self.nodes.remove(&nid);
return false;
}
self.update_layout();
true
}
fn wid(&self, nid: NodeId) -> Option<T> {
self.nodes.get(&nid).and_then(|node| match &node.content {
NodeContent::Window(window) => Some(window.wid),
_ => None,
})
}
fn update_layout_for(&self, node_id: NodeId, rect: Rect) {
let Some(node) = self.nodes.get(&node_id) else {
log::warn!("update_layout_for: no node {node_id}");
return;
};
node.layout.set(rect);
match &node.content {
NodeContent::Container(container) => {
if container.children.is_empty() {
return;
}
match container.orientation {
Orientation::Vertical => {
let hstep = (rect.h - self.spacing * container.children.len() as u32)
/ container.children.len() as u32;
let ystep = hstep + self.spacing;
for (i, &child_nid) in container.children.iter().enumerate() {
let i = i as u32;
let child_rect = Rect {
x: rect.x,
y: rect.y + i * ystep,
w: rect.w,
h: hstep,
};
log::info!("Frame #{child_nid} size: {child_rect:?}");
self.update_layout_for(child_nid, child_rect);
}
}
Orientation::Horizontal => {
let wstep = (rect.w - self.spacing * container.children.len() as u32)
/ container.children.len() as u32;
let xstep = wstep + self.spacing;
for (i, &child_nid) in container.children.iter().enumerate() {
let i = i as u32;
let child_rect = Rect {
x: rect.x + i * xstep,
y: rect.y,
w: wstep,
h: rect.h,
};
log::info!("Frame #{child_nid} size: {child_rect:?}");
self.update_layout_for(child_nid, child_rect);
}
}
}
}
_ => (),
}
}
fn remove_inner(&mut self, node_id: NodeId) -> bool {
if node_id == self.root {
return false;
}
let Some(node) = self.nodes.remove(&node_id) else {
return false;
};
let container_nid = unsafe { node.parent.unwrap_unchecked() };
let Some(container_node) = self.nodes.get_mut(&container_nid) else {
return false;
};
let NodeContent::Container(container) = &mut container_node.content else {
return false;
};
container_node.layout.clear();
container.children.retain(|&c| c != node_id);
// Only root container can be empty
debug_assert!(!(container_node.parent.is_some() && container.children.is_empty()));
if container.children.len() == 1 && container_node.parent.is_some() {
// Lift the remaining node to the parent container
todo!()
}
true
}
fn add_child_to(&mut self, container_nid: NodeId, child_nid: NodeId) -> bool {
let [Some(container_node), Some(child_node)] =
self.nodes.get_many_mut([&container_nid, &child_nid])
else {
return false;
};
let NodeContent::Container(container) = &mut container_node.content else {
return false;
};
container_node.layout.clear();
child_node.layout.clear();
child_node.parent = Some(container_nid);
container.children.push(child_nid);
true
}
}
impl Direction {
pub fn split(&self) -> (Orientation, isize) {
match self {
Self::Left => (Orientation::Horizontal, -1),
Self::Right => (Orientation::Horizontal, 1),
Self::Up => (Orientation::Vertical, -1),
Self::Down => (Orientation::Vertical, 1),
}
}
}
pub mod display;
pub mod layout;
pub mod workspace;

View File

@ -0,0 +1,599 @@
use std::{collections::HashMap, hash::Hash};
use super::layout::{Direction, NodeLayout, Orientation, Rect};
pub type NodeId = u32;
pub enum NodeContent<T> {
Container(ContainerNode),
Window(WindowNode<T>),
}
pub struct Node<T> {
parent: Option<NodeId>,
layout: NodeLayout,
content: NodeContent<T>,
}
pub struct ContainerNode {
children: Vec<NodeId>,
orientation: Orientation,
}
pub struct WindowNode<T> {
pub wid: T,
}
pub struct Workspace<T> {
nodes: HashMap<NodeId, Node<T>>,
wid_to_nid: HashMap<T, NodeId>,
last_node_id: NodeId,
root: NodeId,
focus: Option<NodeId>,
margin: u32,
spacing: u32,
// reservations_top: Vec<Reservation<T>>,
reservation_top: u32,
width: u32,
height: u32,
}
impl<T> Node<T> {
pub fn as_window(&self) -> Option<&WindowNode<T>> {
match &self.content {
NodeContent::Window(window) => Some(window),
_ => None,
}
}
pub fn as_container(&self) -> Option<&ContainerNode> {
match &self.content {
NodeContent::Container(container) => Some(container),
_ => None,
}
}
pub fn as_container_mut(&mut self) -> Option<&mut ContainerNode> {
match &mut self.content {
NodeContent::Container(container) => Some(container),
_ => None,
}
}
}
impl<T: Eq + Hash + Copy> Workspace<T> {
pub fn new(width: u32, height: u32) -> Self {
let root = Node {
parent: None,
layout: NodeLayout::empty(),
content: NodeContent::Container(ContainerNode {
children: vec![],
orientation: Orientation::Horizontal,
}),
};
let nodes = HashMap::from_iter([(0, root)]);
let mut this = Self {
nodes,
wid_to_nid: HashMap::new(),
last_node_id: 0,
margin: 4,
spacing: 4,
root: 0,
focus: None,
width,
height,
reservation_top: 0,
};
this.update_layout();
this
}
pub fn all_windows(&self) -> impl Iterator<Item = (T, &NodeLayout)> + '_ {
self.nodes.iter().filter_map(|(_, node)| {
let window = node.as_window()?;
Some((window.wid, &node.layout))
})
}
pub fn dirty_windows(&self) -> impl Iterator<Item = (T, Rect)> + '_ {
self.all_windows().filter_map(|(wid, layout)| {
if !layout.clear_dirty() {
return None;
}
let rect = layout.get()?;
Some((wid, rect))
})
}
pub fn window_towards_wrap(&self, direction: Direction) -> Option<T> {
let current_nid = self.focus?;
let current_node = self.nodes.get(&current_nid)?;
let parent_nid = current_node.parent?;
let parent_node = self.nodes.get(&parent_nid)?;
let parents_parent_nid = parent_node.parent;
let parent_container = parent_node.as_container()?;
let (orientation, delta) = direction.split();
let nid = if orientation == parent_container.orientation {
log::info!("Within parent {delta}, {:?}", parent_container.orientation);
let position_in_parent = parent_container
.children
.iter()
.position(|&n| n == current_nid)?;
if delta > 0 {
if position_in_parent < parent_container.children.len() - 1 {
parent_container.children[position_in_parent + 1]
} else {
parent_container.children[0]
}
} else {
if position_in_parent > 0 {
parent_container.children[position_in_parent - 1]
} else {
parent_container.children[parent_container.children.len() - 1]
}
}
} else if let Some(parents_parent_nid) = parents_parent_nid {
let parents_parent_node = self.nodes.get(&parents_parent_nid)?;
let parents_parent_container = parents_parent_node.as_container()?;
assert_eq!(parents_parent_container.orientation, orientation);
let position_in_parent = parents_parent_container
.children
.iter()
.position(|&n| n == parent_nid)?;
log::info!(
"Within parent's parent {delta}, {:?}",
parents_parent_container.orientation
);
log::info!("position_in_parent = {position_in_parent}");
if delta > 0 {
if position_in_parent < parent_container.children.len() - 1 {
parents_parent_container.children[position_in_parent + 1]
} else {
parents_parent_container.children[0]
}
} else {
if position_in_parent > 0 {
parents_parent_container.children[position_in_parent - 1]
} else {
parents_parent_container.children[parent_container.children.len() - 1]
}
}
} else {
return None;
};
self.first_window_in(nid)
}
pub fn first_window_in(&self, nid: NodeId) -> Option<T> {
let node = self.nodes.get(&nid)?;
match &node.content {
NodeContent::Window(window) => Some(window.wid),
NodeContent::Container(container) => {
let first = *container.children.first()?;
self.first_window_in(first)
}
}
}
pub fn move_window(&mut self, direction: Direction) -> bool
where
T: core::fmt::Debug,
{
if let Some(container_nid) = self.move_window_inner(direction) {
self.invalidate_layout(container_nid);
self.fixup_containers(self.root);
self.dump(self.root, 0);
true
} else {
false
}
}
fn dump(&self, nid: NodeId, depth: usize)
where
T: core::fmt::Debug,
{
let Some(node) = self.nodes.get(&nid) else {
return;
};
let rect = node.layout.get();
match &node.content {
NodeContent::Window(window) => {
log::info!(
"{:depth$}Window #{:?} {rect:?}",
"",
window.wid,
depth = depth * 2
);
}
NodeContent::Container(container) => {
log::info!("{:depth$}Container {rect:?} {{", "", depth = depth * 2);
for &child_nid in container.children.iter() {
self.dump(child_nid, depth + 1);
}
log::info!("{:depth$}}}", "", depth = depth * 2);
}
}
}
// pub fn walk_windows<F: FnMut(T)>(&self, mut mapper: F) {
// self.walk_windows_inner(&mut mapper, self.root)
// }
// fn walk_windows_inner<F: FnMut(T)>(&self, mapper: &mut F, nid: NodeId) {
// // let Some(node) = self.nodes.get(&nid) else {
// // return;
// // };
// // match &node.content {
// // NodeContent::Container(container) => {
// // for &child_nid in container.children.iter() {
// // self.walk_windows_inner(mapper, child_nid);
// // }
// // }
// // NodeContent::Window(window) => {
// // mapper(window.wid);
// // }
// // }
// }
fn invalidate_layout(&mut self, nid: NodeId) {
// for reservation in self.reservations_top.iter() {
// reservation.layout.clear();
// }
self.invalidate_layout_inner(nid);
self.update_layout();
}
fn invalidate_layout_inner(&self, nid: NodeId) {
let Some(node) = self.nodes.get(&nid) else {
return;
};
node.layout.clear();
match &node.content {
NodeContent::Container(container) => {
for &child_nid in container.children.iter() {
self.invalidate_layout_inner(child_nid);
}
}
_ => (),
}
}
fn fixup_containers(&mut self, nid: NodeId) {
let Some(node) = self.nodes.get_mut(&nid) else {
return;
};
if let NodeContent::Container(container) = &mut node.content {
if container.children.is_empty() {
let Some(parent_nid) = node.parent else {
return;
};
// Remove the empty container
if let Some(parent_container) = self
.nodes
.get_mut(&parent_nid)
.and_then(Node::as_container_mut)
{
parent_container.children.retain(|&n| n != nid);
self.nodes.remove(&nid);
self.invalidate_layout(parent_nid);
}
} else if container.children.len() == 1 {
// Remove the empty container and reparent its only child
if let Some(parent_nid) = node.parent {
let child_nid = container.children.remove(0);
let [Some(child), Some(parent)] =
self.nodes.get_many_mut([&child_nid, &parent_nid])
else {
return;
};
child.parent = Some(parent_nid);
if let Some(parent_container) = parent.as_container_mut() {
parent_container.children.retain(|&n| n != nid);
parent_container.children.push(child_nid);
}
self.invalidate_layout(parent_nid);
}
} else {
// TODO borrowing stuff
let children = container.children.clone();
for child_nid in children {
self.fixup_containers(child_nid);
}
}
}
}
fn move_window_inner(&mut self, direction: Direction) -> Option<NodeId> {
let current_nid = self.focus?;
let current_node = self.nodes.get(&current_nid)?;
let parent_nid = current_node.parent?;
let parent_node = self.nodes.get_mut(&parent_nid)?;
let parents_parent_nid = parent_node.parent;
let parent_container = parent_node.as_container_mut()?;
let (orientation, delta) = direction.split();
if parent_container.orientation == orientation {
let position_in_parent = parent_container
.children
.iter()
.position(|&n| n == current_nid)?;
// TODO check if this item is also a container
if delta < 0 && position_in_parent > 0 {
parent_container
.children
.swap(position_in_parent - 1, position_in_parent);
Some(parent_nid)
} else if delta > 0 && position_in_parent < parent_container.children.len() - 1 {
parent_container
.children
.swap(position_in_parent, position_in_parent + 1);
Some(parent_nid)
} else {
None
}
} else if let Some(parents_parent_nid) = parents_parent_nid {
// Go to parent's parent
let parents_parent_node = self.nodes.get_mut(&parents_parent_nid)?;
let parents_parent_container = parents_parent_node.as_container_mut()?;
assert_eq!(parents_parent_container.orientation, orientation);
todo!()
} else {
self.last_node_id += 1;
let new_root_nid = self.last_node_id;
// Remove child from parent
parent_container.children.retain(|&n| n != current_nid);
self.nodes.get_mut(&current_nid)?.parent = Some(new_root_nid);
self.nodes.get_mut(&self.root)?.parent = Some(new_root_nid);
let children = if delta > 0 {
vec![self.root, current_nid]
} else {
vec![current_nid, self.root]
};
// Create a new root
let new_root = Node {
parent: None,
layout: NodeLayout::empty(),
content: NodeContent::Container(ContainerNode {
children,
orientation,
}),
};
self.root = new_root_nid;
self.nodes.insert(new_root_nid, new_root);
Some(self.root)
}
}
pub fn focus_window(&mut self, wid: T) -> (Option<T>, Option<NodeId>) {
let old = self.focus.take().and_then(|nid| self.wid(nid));
let new = self.wid_to_nid.get(&wid).copied();
self.focus = new;
(old, new)
}
pub fn focused_window(&self) -> Option<T> {
let nid = self.focus?;
Some(self.nodes.get(&nid)?.as_window()?.wid)
}
pub(super) fn window(&self, wid: T) -> Option<&Node<T>> {
let nid = *self.wid_to_nid.get(&wid)?;
self.nodes.get(&nid)
}
// pub fn reservation(&self, wid: T) -> Option<&Reservation<T>> {
// self.reservations_top.iter().find(|r| r.wid == wid)
// }
pub(super) fn window_frame(&self, wid: T) -> Option<Rect> {
self.window(wid)?.layout.get()
}
pub(super) fn resize(&mut self, w: u32, h: u32, reservation_top: u32) {
self.width = w;
self.height = h;
self.reservation_top = reservation_top;
self.update_layout();
}
// fn update_reservation_layout(&mut self) -> u32 {
// res_y
// }
pub(super) fn update_layout(&mut self) {
self.update_layout_for(
self.root,
Rect {
x: self.margin,
y: self.margin + self.reservation_top,
w: self.width - self.margin * 2,
h: self.height - self.margin * 2 - self.reservation_top,
},
);
}
pub(super) fn create_window(&mut self, wid: T) -> bool {
self.create_window_in(self.root, wid)
}
pub fn remove_window(&mut self, wid: T) -> bool {
let Some(nid) = self.wid_to_nid.remove(&wid) else {
return false;
};
if self.remove_inner(nid) {
if self.focus == Some(nid) {
let wid = self.first_window_in(self.root);
if let Some(wid) = wid {
self.focus_window(wid);
} else {
self.focus = None;
}
}
self.fixup_containers(self.root);
self.update_layout();
true
} else {
false
}
}
fn create_window_in(&mut self, container_nid: NodeId, wid: T) -> bool {
self.last_node_id += 1;
let nid = self.last_node_id;
self.nodes.insert(
nid,
Node {
parent: Some(container_nid),
layout: NodeLayout::empty(),
content: NodeContent::Window(WindowNode { wid }),
},
);
self.wid_to_nid.insert(wid, nid);
if !self.add_child_to(container_nid, nid) {
self.wid_to_nid.remove(&wid);
self.nodes.remove(&nid);
return false;
}
self.update_layout();
true
}
fn wid(&self, nid: NodeId) -> Option<T> {
self.nodes.get(&nid).and_then(|node| match &node.content {
NodeContent::Window(window) => Some(window.wid),
_ => None,
})
}
fn update_layout_for(&self, node_id: NodeId, rect: Rect) {
let Some(node) = self.nodes.get(&node_id) else {
log::warn!("update_layout_for: no node {node_id}");
return;
};
node.layout.set(rect);
match &node.content {
NodeContent::Container(container) => {
if container.children.is_empty() {
return;
}
match container.orientation {
Orientation::Vertical => {
let hstep = (rect.h - self.spacing * container.children.len() as u32)
/ container.children.len() as u32;
let ystep = hstep + self.spacing;
for (i, &child_nid) in container.children.iter().enumerate() {
let i = i as u32;
let child_rect = Rect {
x: rect.x,
y: rect.y + i * ystep,
w: rect.w,
h: hstep,
};
log::info!("Frame #{child_nid} size: {child_rect:?}");
self.update_layout_for(child_nid, child_rect);
}
}
Orientation::Horizontal => {
let wstep = (rect.w - self.spacing * container.children.len() as u32)
/ container.children.len() as u32;
let xstep = wstep + self.spacing;
for (i, &child_nid) in container.children.iter().enumerate() {
let i = i as u32;
let child_rect = Rect {
x: rect.x + i * xstep,
y: rect.y,
w: wstep,
h: rect.h,
};
log::info!("Frame #{child_nid} size: {child_rect:?}");
self.update_layout_for(child_nid, child_rect);
}
}
}
}
_ => (),
}
}
fn remove_inner(&mut self, node_id: NodeId) -> bool {
if node_id == self.root {
return false;
}
let Some(node) = self.nodes.remove(&node_id) else {
return false;
};
let container_nid = unsafe { node.parent.unwrap_unchecked() };
let Some(container_node) = self.nodes.get_mut(&container_nid) else {
return false;
};
let NodeContent::Container(container) = &mut container_node.content else {
return false;
};
container_node.layout.clear();
container.children.retain(|&c| c != node_id);
// Only root container can be empty
debug_assert!(!(container_node.parent.is_some() && container.children.is_empty()));
if container.children.len() == 1 && container_node.parent.is_some() {
// Lift the remaining node to the parent container
todo!()
}
true
}
fn add_child_to(&mut self, container_nid: NodeId, child_nid: NodeId) -> bool {
let [Some(container_node), Some(child_node)] =
self.nodes.get_many_mut([&container_nid, &child_nid])
else {
return false;
};
let NodeContent::Container(container) = &mut container_node.content else {
return false;
};
container_node.layout.clear();
child_node.layout.clear();
child_node.parent = Some(container_nid);
container.children.push(child_nid);
true
}
}