use std::cmp::max;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlElement;
use yew::html::Scope;
use yew::prelude::*;
use crate::components::style::LocalStyle;
use crate::utils::*;
use crate::*;
struct ResizingState {
mousemove: Closure<dyn Fn(MouseEvent)>,
mouseup: Closure<dyn Fn(MouseEvent)>,
cursor: String,
index: usize,
start: i32,
total: i32,
alt: i32,
orientation: Orientation,
reverse: bool,
body_style: web_sys::CssStyleDeclaration,
pointer_id: i32,
pointer_elem: HtmlElement,
}
impl Drop for ResizingState {
fn drop(&mut self) {
let result: ApiResult<()> = maybe! {
let document = web_sys::window().unwrap().document().unwrap();
let body = document.body().unwrap();
let mousemove = self.mousemove.as_ref().unchecked_ref();
body.remove_event_listener_with_callback("mousemove", mousemove)?;
let mouseup = self.mouseup.as_ref().unchecked_ref();
body.remove_event_listener_with_callback("mouseup", mouseup)?;
self.release_cursor()?;
Ok(())
};
result.expect("Drop failed")
}
}
impl ResizingState {
pub fn new(
index: usize,
client_offset: i32,
ctx: &Context<SplitPanel>,
first_elem: &HtmlElement,
pointer_id: i32,
pointer_elem: HtmlElement,
) -> ApiResult<Self> {
let orientation = ctx.props().orientation;
let reverse = ctx.props().reverse;
let split_panel = ctx.link();
let document = web_sys::window().unwrap().document().unwrap();
let body = document.body().unwrap();
let total = match orientation {
Orientation::Horizontal => first_elem.offset_width(),
Orientation::Vertical => first_elem.offset_height(),
};
let alt = match orientation {
Orientation::Horizontal => first_elem.offset_height(),
Orientation::Vertical => first_elem.offset_width(),
};
let mouseup = split_panel
.callback(|_| SplitPanelMsg::StopResizing)
.into_closure();
let mousemove = split_panel
.callback(move |event: MouseEvent| {
SplitPanelMsg::MoveResizing(match orientation {
Orientation::Horizontal => event.client_x(),
Orientation::Vertical => event.client_y(),
})
})
.into_closure();
let mut state = Self {
index,
cursor: "".to_owned(),
start: client_offset,
orientation,
reverse,
total,
alt,
body_style: body.style(),
mouseup,
mousemove,
pointer_id,
pointer_elem,
};
state.capture_cursor()?;
state.register_listeners()?;
Ok(state)
}
fn get_offset(&self, client_offset: i32) -> i32 {
let delta = if self.reverse {
self.start - client_offset
} else {
client_offset - self.start
};
max(0, self.total + delta)
}
pub fn get_style(&self, client_offset: i32) -> Option<String> {
let offset = self.get_offset(client_offset);
Some(match self.orientation {
Orientation::Horizontal => format!(
"max-width:{}px;min-width:{}px;width:{}px",
offset, offset, offset
),
Orientation::Vertical => format!(
"max-height:{}px;min-height:{}px;height:{}px",
offset, offset, offset
),
})
}
pub fn get_dimensions(&self, client_offset: i32) -> (i32, i32) {
let offset = self.get_offset(client_offset);
match self.orientation {
Orientation::Horizontal => (std::cmp::max(0, offset), self.alt),
Orientation::Vertical => (self.alt, std::cmp::max(0, offset)),
}
}
fn register_listeners(&self) -> ApiResult<()> {
let document = web_sys::window().unwrap().document().unwrap();
let body = document.body().unwrap();
let mousemove = self.mousemove.as_ref().unchecked_ref();
body.add_event_listener_with_callback("mousemove", mousemove)?;
let mouseup = self.mouseup.as_ref().unchecked_ref();
Ok(body.add_event_listener_with_callback("mouseup", mouseup)?)
}
fn capture_cursor(&mut self) -> ApiResult<()> {
self.pointer_elem.set_pointer_capture(self.pointer_id)?;
self.cursor = self.body_style.get_property_value("cursor")?;
self.body_style
.set_property("cursor", match self.orientation {
Orientation::Horizontal => "col-resize",
Orientation::Vertical => "row-resize",
})?;
Ok(())
}
fn release_cursor(&self) -> ApiResult<()> {
self.pointer_elem.release_pointer_capture(self.pointer_id)?;
Ok(self.body_style.set_property("cursor", &self.cursor)?)
}
}
#[derive(Clone, Copy, Default, Eq, PartialEq)]
pub enum Orientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Properties, Default)]
pub struct SplitPanelProps {
pub children: Children,
#[prop_or_default]
pub id: Option<String>,
#[prop_or_default]
pub orientation: Orientation,
#[prop_or_default]
pub no_wrap: bool,
#[prop_or_default]
pub reverse: bool,
#[prop_or_default]
pub on_reset: Option<Callback<()>>,
#[prop_or_default]
pub on_resize: Option<Callback<(i32, i32)>>,
#[prop_or_default]
pub on_resize_finished: Option<Callback<()>>,
#[cfg(test)]
#[prop_or_default]
pub weak_link: WeakScope<SplitPanel>,
}
impl SplitPanelProps {
fn validate(&self) -> bool {
!self.children.is_empty()
}
}
impl PartialEq for SplitPanelProps {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.children == other.children
&& self.orientation == other.orientation
&& self.reverse == other.reverse
}
}
pub enum SplitPanelMsg {
StartResizing(usize, i32, i32, HtmlElement),
MoveResizing(i32),
StopResizing,
Reset(usize),
}
pub struct SplitPanel {
resize_state: Option<ResizingState>,
refs: Vec<NodeRef>,
styles: Vec<Option<String>>,
on_reset: Option<Callback<()>>,
}
impl Component for SplitPanel {
type Message = SplitPanelMsg;
type Properties = SplitPanelProps;
fn create(ctx: &Context<Self>) -> Self {
assert!(ctx.props().validate());
enable_weak_link_test!(ctx.props(), ctx.link());
let len = ctx.props().children.len();
Self {
resize_state: None,
refs: vec![Default::default(); len],
styles: vec![Default::default(); len],
on_reset: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
SplitPanelMsg::Reset(index) => {
self.styles[index] = None;
self.on_reset = ctx.props().on_reset.clone();
}
SplitPanelMsg::StartResizing(index, client_offset, pointer_id, pointer_elem) => {
let elem = self.refs[index].cast::<HtmlElement>().unwrap();
let state =
ResizingState::new(index, client_offset, ctx, &elem, pointer_id, pointer_elem);
self.resize_state = state.ok();
}
SplitPanelMsg::StopResizing => {
self.resize_state = None;
if let Some(cb) = &ctx.props().on_resize_finished {
cb.emit(());
}
}
SplitPanelMsg::MoveResizing(client_offset) => {
if let Some(state) = self.resize_state.as_ref() {
if let Some(ref cb) = ctx.props().on_resize {
cb.emit(state.get_dimensions(client_offset));
}
self.styles[state.index] = state.get_style(client_offset);
}
}
};
true
}
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
if let Some(on_reset) = self.on_reset.take() {
on_reset.emit(());
}
}
fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
assert!(ctx.props().validate());
let new_len = ctx.props().children.len();
self.refs.resize(new_len, Default::default());
self.styles.resize(new_len, Default::default());
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let mut iter = ctx.props().children.iter();
let orientation = ctx.props().orientation;
let mut classes = classes!("split-panel");
if orientation == Orientation::Vertical {
classes.push("orient-vertical");
}
if ctx.props().reverse {
classes.push("orient-reverse");
}
let contents = html_template! {
<LocalStyle href={ css!("containers/split-panel") } />
<SplitPanelChild
style={ self.styles[0].clone() }
ref_={ self.refs[0].clone() }>
{ iter.next().unwrap() }
</SplitPanelChild>
{
for iter.enumerate().map(|(i, x)| {
html_template! {
<SplitPanelDivider
i={ i }
orientation={ ctx.props().orientation }
link={ ctx.link().clone() }>
</SplitPanelDivider>
if i == ctx.props().children.len() - 2 {
{ x }
} else {
<SplitPanelChild
style={ self.styles[i + 1].clone() }
ref_={ self.refs[i + 1].clone() }>
{ x }
</SplitPanelChild>
}
}
})
}
};
if ctx.props().no_wrap {
html! {{ contents }}
} else {
html! {
<div id={ ctx.props().id.clone() } class={ classes }>
{ contents }
</div>
}
}
}
}
#[derive(Properties)]
struct SplitPanelDividerProps {
i: usize,
orientation: Orientation,
link: Scope<SplitPanel>,
}
impl PartialEq for SplitPanelDividerProps {
fn eq(&self, rhs: &Self) -> bool {
self.i == rhs.i && self.orientation == rhs.orientation
}
}
#[function_component(SplitPanelDivider)]
fn split_panel_divider(props: &SplitPanelDividerProps) -> Html {
let orientation = props.orientation;
let i = props.i;
let link = props.link.clone();
let onmousedown = link.callback(move |event: PointerEvent| {
let target = event.target().unwrap().unchecked_into::<HtmlElement>();
let pointer_id = event.pointer_id();
let size = match orientation {
Orientation::Horizontal => event.client_x(),
Orientation::Vertical => event.client_y(),
};
SplitPanelMsg::StartResizing(i, size, pointer_id, target)
});
let ondblclick = props.link.callback(move |event: MouseEvent| {
event.prevent_default();
event.stop_propagation();
SplitPanelMsg::Reset(i)
});
let ondragstart = Callback::from(|event: DragEvent| event.prevent_default());
html_template! {
<div
class="split-panel-divider"
ondragstart={ ondragstart }
onpointerdown={ onmousedown }
ondblclick={ ondblclick }>
</div>
}
}
#[derive(Properties, PartialEq)]
struct SplitPanelChildProps {
style: Option<String>,
ref_: NodeRef,
children: Children,
}
#[function_component(SplitPanelChild)]
fn split_panel_child(props: &SplitPanelChildProps) -> Html {
let class = if props.style.is_some() {
classes!("split-panel-child", "is-width-override")
} else {
classes!("split-panel-child")
};
html! {
<div
class={ class }
ref={ props.ref_.clone() }
style={ props.style.clone() }>
{ props.children.iter().next().unwrap() }
</div>
}
}