use js_sys::Promise;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = web_sys::HtmlElement)]
pub type RegularLayout;
#[wasm_bindgen(method, js_name = "insertPanel")]
pub fn insert_panel(this: &RegularLayout, name: &str, path: JsValue, split: JsValue)
-> Promise;
#[wasm_bindgen(method, js_name = "removePanel")]
pub fn remove_panel(this: &RegularLayout, name: &str) -> Promise;
#[wasm_bindgen(method)]
pub fn clear(this: &RegularLayout) -> Promise;
#[wasm_bindgen(method, js_name = "calculatePath")]
pub fn calculate_path(this: &RegularLayout, name: &str) -> JsValue;
#[wasm_bindgen(method, js_name = "calculateIntersect")]
pub fn calculate_intersect(this: &RegularLayout, coordinates: &JsValue) -> JsValue;
#[wasm_bindgen(method, js_name = "setOverlayState")]
pub fn set_overlay_state(
this: &RegularLayout,
coordinates: &JsValue,
target: &JsValue,
) -> Promise;
#[wasm_bindgen(method, js_name = "clearOverlayState")]
pub fn clear_overlay_state(
this: &RegularLayout,
coordinates: &JsValue,
target: &JsValue,
) -> Promise;
#[wasm_bindgen(method, js_name = "diffCoordinates")]
pub fn diff_coordinates(this: &RegularLayout, coordinates: &JsValue, target: &JsValue) -> f64;
#[wasm_bindgen(method, js_name = "realCoordinates")]
pub fn real_coordinates(this: &RegularLayout, window: &JsValue) -> JsValue;
#[wasm_bindgen(method, js_name = "restorePhysics")]
pub fn restore_physics(this: &RegularLayout, physics: &JsValue);
#[wasm_bindgen(method, js_name = "savePhysics")]
pub fn save_physics(this: &RegularLayout) -> JsValue;
#[wasm_bindgen(method, js_name = "resumeResize")]
pub fn resume_resize(this: &RegularLayout);
#[wasm_bindgen(method)]
pub fn save(this: &RegularLayout) -> JsValue;
#[wasm_bindgen(method)]
pub fn restore(this: &RegularLayout, layout: &JsValue) -> Promise;
#[wasm_bindgen(method, js_name = "restoreSync")]
pub fn restore_sync(this: &RegularLayout, layout: &JsValue);
#[wasm_bindgen(method)]
pub fn select(this: &RegularLayout, name: &str) -> Promise;
#[wasm_bindgen(method)]
pub fn maximize(this: &RegularLayout, name: &str);
#[wasm_bindgen(method)]
pub fn minimize(this: &RegularLayout);
pub type PresizeDetail;
#[wasm_bindgen(method, js_name = "calculatePresizePaths")]
pub fn calculate_presize_paths(this: &PresizeDetail) -> JsValue;
#[wasm_bindgen(method, getter)]
pub fn overlay(this: &PresizeDetail) -> JsValue;
}
impl RegularLayout {
pub const BEFORE_RESIZE_EVENT: &'static str = "regular-layout-before-resize";
pub const BEFORE_UPDATE_EVENT: &'static str = "regular-layout-before-update";
pub const FRAME_TAG_NAME: &'static str = "regular-layout-frame";
pub const SELECT_EVENT: &'static str = "regular-layout-select";
pub const TAG_NAME: &'static str = "regular-layout";
pub const UPDATE_EVENT: &'static str = "regular-layout-update";
pub fn contains_panel(&self, name: &str) -> bool {
let path = self.calculate_path(name);
!path.is_null() && !path.is_undefined()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
#[serde(rename_all = "lowercase")]
pub enum Orientation {
Horizontal,
Vertical,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
#[serde(tag = "type")]
pub enum Layout {
#[serde(rename = "split-layout")]
Split(SplitLayout),
#[serde(rename = "tab-layout")]
Tab(TabLayout),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
pub struct SplitLayout {
pub children: Vec<Layout>,
pub sizes: Vec<f64>,
pub orientation: Orientation,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ts_rs::TS)]
pub struct TabLayout {
pub tabs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub selected: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ViewWindow {
pub row_start: f64,
pub row_end: f64,
pub col_start: f64,
pub col_end: f64,
}
impl Layout {
pub fn insert_split(&self, index: usize, name: &str, orientation: Orientation) -> Layout {
let leaf = Layout::Tab(TabLayout {
tabs: vec![name.to_owned()],
selected: None,
});
match self {
Layout::Split(split) if split.orientation == orientation => {
let index = index.min(split.children.len());
let mut children = split.children.clone();
let mut sizes = split.sizes.clone();
children.insert(index, leaf);
sizes.insert(index, 1.0 / (children.len() - 1).max(1) as f64);
let total: f64 = sizes.iter().sum();
let sizes = if total.is_finite() && total > 0.0 {
sizes.iter().map(|size| size / total).collect()
} else {
vec![1.0 / children.len() as f64; children.len()]
};
Layout::Split(SplitLayout {
children,
sizes,
orientation,
})
},
_ => Layout::Split(SplitLayout {
children: if index == 0 {
vec![leaf, self.clone()]
} else {
vec![self.clone(), leaf]
},
sizes: vec![0.5, 0.5],
orientation,
}),
}
}
pub fn view_window(&self, name: &str) -> Option<ViewWindow> {
self.view_window_in(name, ViewWindow {
row_start: 0.0,
row_end: 1.0,
col_start: 0.0,
col_end: 1.0,
})
}
fn view_window_in(&self, name: &str, window: ViewWindow) -> Option<ViewWindow> {
match self {
Layout::Tab(tab) => {
let selected = tab.tabs.get(tab.selected.unwrap_or(0));
(selected.map(String::as_str) == Some(name)).then_some(window)
},
Layout::Split(split) => {
let vertical = split.orientation == Orientation::Vertical;
let (start, end) = if vertical {
(window.row_start, window.row_end)
} else {
(window.col_start, window.col_end)
};
let mut pos = start;
for (child, size) in split.children.iter().zip(&split.sizes) {
let next = pos + (end - start) * size;
let child_window = if vertical {
ViewWindow {
row_start: pos,
row_end: next,
..window
}
} else {
ViewWindow {
col_start: pos,
col_end: next,
..window
}
};
if let Some(found) = child.view_window_in(name, child_window) {
return Some(found);
}
pos = next;
}
None
},
}
}
pub fn validate(&self, ids: &[String]) -> Result<(), String> {
if let Layout::Split(split) = self
&& split.children.is_empty()
&& split.sizes.is_empty()
&& ids.is_empty()
{
return Ok(());
}
let mut seen = Vec::new();
self.validate_node("layout", ids, &mut seen)?;
let missing = ids
.iter()
.filter(|id| !seen.contains(id))
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(format!(
"Invalid layout: panels {missing:?} are missing - a layout must name every panel \
exactly once (panels: {ids:?})"
));
}
Ok(())
}
fn validate_node(
&self,
path: &str,
ids: &[String],
seen: &mut Vec<String>,
) -> Result<(), String> {
match self {
Layout::Split(split) => {
if split.children.is_empty() {
return Err(format!("Invalid layout: {path}.children is empty"));
}
if split.sizes.len() != split.children.len() {
return Err(format!(
"Invalid layout: {path}.sizes has {} entries for {} children - give one \
size per child",
split.sizes.len(),
split.children.len()
));
}
if let Some(size) = split.sizes.iter().find(|x| !x.is_finite() || **x <= 0.0) {
return Err(format!(
"Invalid layout: {path}.sizes contains {size} - sizes must be positive \
fractions"
));
}
let total: f64 = split.sizes.iter().sum();
if (total - 1.0).abs() > 1e-6 {
return Err(format!(
"Invalid layout: {path}.sizes sum to {total} - sizes are fractions of the \
split and must sum to 1"
));
}
for (index, child) in split.children.iter().enumerate() {
child.validate_node(&format!("{path}.children[{index}]"), ids, seen)?;
}
Ok(())
},
Layout::Tab(tab) => {
if tab.tabs.is_empty() {
return Err(format!("Invalid layout: {path}.tabs is empty"));
}
if let Some(selected) = tab.selected
&& selected >= tab.tabs.len()
{
return Err(format!(
"Invalid layout: {path}.selected is {selected} but {path}.tabs has {} \
entries",
tab.tabs.len()
));
}
for name in &tab.tabs {
if !ids.contains(name) {
return Err(format!(
"Invalid layout: {path}.tabs names unknown panel \"{name}\" (panels: \
{ids:?})"
));
}
if seen.contains(name) {
return Err(format!(
"Invalid layout: panel \"{name}\" appears more than once - a layout \
must name every panel exactly once"
));
}
seen.push(name.clone());
}
Ok(())
},
}
}
pub fn slot_names(&self) -> Vec<String> {
match self {
Layout::Split(split) => split.children.iter().flat_map(Layout::slot_names).collect(),
Layout::Tab(tab) => tab.tabs.clone(),
}
}
pub fn hidden_slot_names(&self) -> Vec<String> {
match self {
Layout::Split(split) => split
.children
.iter()
.flat_map(Layout::hidden_slot_names)
.collect(),
Layout::Tab(tab) => {
let selected = tab.selected.unwrap_or(0);
tab.tabs
.iter()
.enumerate()
.filter(|(idx, _)| *idx != selected)
.map(|(_, name)| name.clone())
.collect()
},
}
}
pub fn remap(&self, f: &impl Fn(&str) -> Option<String>) -> Layout {
match self {
Layout::Split(split) => Layout::Split(SplitLayout {
children: split.children.iter().map(|c| c.remap(f)).collect(),
sizes: split.sizes.clone(),
orientation: split.orientation,
}),
Layout::Tab(tab) => Layout::Tab(TabLayout {
tabs: tab
.tabs
.iter()
.map(|name| f(name).unwrap_or_else(|| name.clone()))
.collect(),
selected: tab.selected,
}),
}
}
}