use std::cell::{OnceCell, RefCell};
use std::sync::Arc;
use blinc_core::context_state::BlincContextState;
use blinc_core::reactive::{ReactiveGraph, computed};
use blinc_core::{Signal, State};
use blinc_layout::InstanceKey;
use blinc_layout::div::{Div, ElementBuilder};
use blinc_layout::element::{CursorStyle, RenderProps};
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_theme::{ColorToken, ThemeState};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ResizeDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Clone)]
pub struct ResizeHandleConfig {
pub thickness: f32,
pub hit_area_padding: f32,
pub show_active: bool,
}
impl Default for ResizeHandleConfig {
fn default() -> Self {
Self {
thickness: 4.0,
hit_area_padding: 4.0,
show_active: true,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum PanelSize {
Fixed(f32),
Flex(f32),
}
impl Default for PanelSize {
fn default() -> Self {
PanelSize::Flex(1.0)
}
}
struct ResizablePanelConfig {
default_size: Option<f32>,
min_size: f32,
max_size: Option<f32>,
is_flex: bool,
flex_grow: f32,
content: Option<Box<dyn ElementBuilder>>,
id: String,
}
impl Default for ResizablePanelConfig {
fn default() -> Self {
Self {
default_size: None,
min_size: 50.0,
max_size: None,
is_flex: false,
flex_grow: 1.0,
content: None,
id: String::new(),
}
}
}
pub struct ResizablePanelBuilder {
config: ResizablePanelConfig,
}
impl ResizablePanelBuilder {
pub fn new() -> Self {
Self {
config: ResizablePanelConfig::default(),
}
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.config.id = id.into();
self
}
pub fn default_size(mut self, size: f32) -> Self {
self.config.default_size = Some(size);
self.config.is_flex = false;
self
}
pub fn min_size(mut self, size: f32) -> Self {
self.config.min_size = size;
self
}
pub fn max_size(mut self, size: f32) -> Self {
self.config.max_size = Some(size);
self
}
pub fn flex_grow(mut self) -> Self {
self.config.is_flex = true;
self.config.flex_grow = 1.0;
self
}
pub fn flex(mut self, factor: f32) -> Self {
self.config.is_flex = true;
self.config.flex_grow = factor;
self
}
pub fn child(mut self, content: impl ElementBuilder + 'static) -> Self {
self.config.content = Some(Box::new(content));
self
}
fn build_config(self) -> ResizablePanelConfig {
self.config
}
}
impl Default for ResizablePanelBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy)]
struct PanelConstraints {
min_size: f32,
max_size: Option<f32>,
}
#[allow(clippy::type_complexity)]
struct ResizableGroupConfig {
direction: ResizeDirection,
handle: ResizeHandleConfig,
panels: Vec<ResizablePanelConfig>,
key: String,
on_resize: Option<Arc<dyn Fn(&[f32]) + Send + Sync>>,
classes: Vec<std::sync::Arc<str>>,
user_id: Option<String>,
}
impl Default for ResizableGroupConfig {
fn default() -> Self {
Self {
direction: ResizeDirection::Horizontal,
handle: ResizeHandleConfig::default(),
panels: Vec::new(),
key: String::new(),
on_resize: None,
classes: Vec::new(),
user_id: None,
}
}
}
pub struct ResizableGroup {
inner: Div,
}
impl ResizableGroup {
fn from_config(config: ResizableGroupConfig) -> Self {
let ctx = BlincContextState::get();
let direction = config.direction;
let handle_config = config.handle;
let key = config.key;
let constraints: Vec<PanelConstraints> = config
.panels
.iter()
.map(|p| PanelConstraints {
min_size: p.min_size,
max_size: p.max_size,
})
.collect();
let panel_sizes: Vec<Signal<f32>> = config
.panels
.iter()
.enumerate()
.map(|(i, panel)| {
let panel_key = if panel.id.is_empty() {
format!("{}_panel_{}", key, i)
} else {
format!("{}_{}", key, panel.id)
};
let default = panel.default_size.unwrap_or(200.0);
ctx.use_signal_keyed(&panel_key, || default)
})
.collect();
let drag_index = ctx.use_signal_keyed(&format!("{}_drag_idx", key), || -1i32);
let drag_start_pos = ctx.use_signal_keyed(&format!("{}_drag_start", key), || 0.0f32);
let drag_start_sizes =
ctx.use_signal_keyed(&format!("{}_drag_sizes", key), Vec::<f32>::new);
let mut container = div().w_full().h_full();
match direction {
ResizeDirection::Horizontal => {
container = container.flex_row();
}
ResizeDirection::Vertical => {
container = container.flex_col();
}
}
let panel_count = config.panels.len();
for (i, panel_config) in config.panels.into_iter().enumerate() {
let size_state = panel_sizes[i];
let is_flex = panel_config.is_flex;
let flex_grow_factor = panel_config.flex_grow;
let min_size = panel_config.min_size;
let max_size = panel_config.max_size;
let panel_wrapper: Div = if is_flex {
let mut wrapper = div().overflow_clip().flex_grow_value(flex_grow_factor);
match direction {
ResizeDirection::Horizontal => {
wrapper = wrapper.h_full();
if min_size > 0.0 {
wrapper = wrapper.min_w(min_size);
}
if let Some(max) = max_size {
wrapper = wrapper.max_w(max);
}
}
ResizeDirection::Vertical => {
wrapper = wrapper.w_full();
if min_size > 0.0 {
wrapper = wrapper.min_h(min_size);
}
if let Some(max) = max_size {
wrapper = wrapper.max_h(max);
}
}
}
if let Some(content) = panel_config.content {
wrapper = wrapper.child_box(content);
}
wrapper
} else {
let mut sized = div().overflow_clip().relative();
match direction {
ResizeDirection::Horizontal => {
sized = sized.w(size_state).h_full();
if min_size > 0.0 {
sized = sized.min_w(min_size);
}
if let Some(max) = max_size {
sized = sized.max_w(max);
}
}
ResizeDirection::Vertical => {
sized = sized.h(size_state).w_full();
if min_size > 0.0 {
sized = sized.min_h(min_size);
}
if let Some(max) = max_size {
sized = sized.max_h(max);
}
}
}
if let Some(content) = panel_config.content {
sized = sized.child(
div()
.absolute()
.left(0.0)
.top(0.0)
.right(0.0)
.bottom(0.0)
.overflow_clip()
.child_box(content),
);
}
sized
};
container = container.child(panel_wrapper);
if i < panel_count - 1 {
let left_constraints = constraints[i];
let right_constraints = constraints[i + 1];
let handle = Self::build_handle(
i,
direction,
&handle_config,
&panel_sizes,
left_constraints,
right_constraints,
drag_index,
drag_start_pos,
drag_start_sizes,
config.on_resize.clone(),
);
container = container.child(handle);
}
}
for c in &config.classes {
container = container.class(c);
}
if let Some(ref id) = config.user_id {
container = container.id(id);
}
Self { inner: container }
}
#[allow(clippy::too_many_arguments)]
fn build_handle(
index: usize,
direction: ResizeDirection,
handle_config: &ResizeHandleConfig,
panel_sizes: &[Signal<f32>],
left_constraints: PanelConstraints,
right_constraints: PanelConstraints,
drag_index: Signal<i32>,
drag_start_pos: Signal<f32>,
drag_start_sizes: Signal<Vec<f32>>,
on_resize: Option<Arc<dyn Fn(&[f32]) + Send + Sync>>,
) -> Div {
let thickness = handle_config.thickness;
let hit_padding = handle_config.hit_area_padding;
let total_hit_area = thickness + hit_padding * 2.0;
let idx = index;
let dragging = drag_index;
let dragged = computed(move |g: &ReactiveGraph| g.get(dragging) == Some(idx as i32));
let (cursor, bar, hit_area) = match direction {
ResizeDirection::Horizontal => (
CursorStyle::ResizeEW,
div().w(thickness).h_full(),
div().w(total_hit_area).h_full(),
),
ResizeDirection::Vertical => (
CursorStyle::ResizeNS,
div().w_full().h(thickness),
div().w_full().h(total_hit_area),
),
};
let drag_index_for_down = drag_index;
let drag_index_for_drag = drag_index;
let drag_index_for_end = drag_index;
let drag_start_pos_for_down = drag_start_pos;
let drag_start_pos_for_drag = drag_start_pos;
let drag_start_sizes_for_down = drag_start_sizes;
let drag_start_sizes_for_drag = drag_start_sizes;
let panel_sizes_for_down: Vec<Signal<f32>> = panel_sizes.to_vec();
let panel_sizes_for_drag: Vec<Signal<f32>> = panel_sizes.to_vec();
let left_min = left_constraints.min_size;
let left_max = left_constraints.max_size;
let right_min = right_constraints.min_size;
let right_max = right_constraints.max_size;
hit_area
.class("cn-resizable-handle")
.attr_when("data-state", "dragging", &dragged)
.items_center()
.justify_center()
.cursor(cursor)
.child(bar.class("cn-resizable-line").cursor(cursor))
.on_mouse_down(move |event| {
drag_index_for_down.set(idx as i32);
let pos = match direction {
ResizeDirection::Horizontal => event.mouse_x,
ResizeDirection::Vertical => event.mouse_y,
};
drag_start_pos_for_down.set(pos);
let sizes: Vec<f32> = panel_sizes_for_down.iter().map(|s| s.get()).collect();
drag_start_sizes_for_down.set(sizes);
})
.on_drag(move |event| {
if drag_index_for_drag.get() < 0 {
return;
}
let pos = match direction {
ResizeDirection::Horizontal => event.mouse_x,
ResizeDirection::Vertical => event.mouse_y,
};
let delta = pos - drag_start_pos_for_drag.get();
let start_sizes = drag_start_sizes_for_drag.get();
if start_sizes.len() <= idx + 1 {
return;
}
let left_start = start_sizes[idx];
let right_start = start_sizes[idx + 1];
let mut new_left = (left_start + delta).max(left_min);
if let Some(max) = left_max {
new_left = new_left.min(max);
}
let mut new_right = (right_start - delta).max(right_min);
if let Some(max) = right_max {
new_right = new_right.min(max);
}
let total = left_start + right_start;
if new_left + new_right > total {
let overflow = (new_left + new_right) - total;
if delta > 0.0 {
new_right = (new_right - overflow).max(right_min);
new_left = total - new_right;
} else {
new_left = (new_left - overflow).max(left_min);
new_right = total - new_left;
}
}
let mut changed = false;
for (state, value) in [
(&panel_sizes_for_drag[idx], new_left),
(&panel_sizes_for_drag[idx + 1], new_right),
] {
if state.get() != value {
state.set(value);
changed = true;
}
}
if changed && let Some(ref on_resize) = on_resize {
let sizes: Vec<f32> = panel_sizes_for_drag.iter().map(|s| s.get()).collect();
on_resize(&sizes);
}
})
.on_drag_end(move |_event| {
drag_index_for_end.set(-1);
})
}
}
impl ElementBuilder for ResizableGroup {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
}
pub struct ResizableGroupBuilder {
config: ResizableGroupConfig,
panels: RefCell<Vec<ResizablePanelConfig>>,
key: InstanceKey,
built: OnceCell<ResizableGroup>,
}
impl ResizableGroupBuilder {
#[track_caller]
pub fn new() -> Self {
Self {
config: ResizableGroupConfig::default(),
panels: RefCell::new(Vec::new()),
key: InstanceKey::new("resizable_group"),
built: OnceCell::new(),
}
}
pub fn direction(mut self, direction: ResizeDirection) -> Self {
self.config.direction = direction;
self
}
pub fn horizontal(mut self) -> Self {
self.config.direction = ResizeDirection::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.config.direction = ResizeDirection::Vertical;
self
}
pub fn handle_thickness(mut self, thickness: f32) -> Self {
self.config.handle.thickness = thickness;
self
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.config.key = key.into();
self
}
pub fn panel(mut self, panel: ResizablePanelBuilder) -> Self {
self.panels.get_mut().push(panel.build_config());
self
}
pub fn on_resize<F>(mut self, callback: F) -> Self
where
F: Fn(&[f32]) + Send + Sync + 'static,
{
self.config.on_resize = Some(Arc::new(callback));
self
}
pub fn class(mut self, name: impl AsRef<str>) -> Self {
self.config
.classes
.push(blinc_core::intern::intern(name.as_ref()));
self
}
pub fn id(mut self, id: &str) -> Self {
self.config.user_id = Some(id.to_string());
self
}
fn take_config(&self) -> ResizableGroupConfig {
ResizableGroupConfig {
direction: self.config.direction,
handle: self.config.handle.clone(),
panels: std::mem::take(&mut *self.panels.borrow_mut()),
key: if self.config.key.is_empty() {
self.key.get().to_string()
} else {
self.config.key.clone()
},
on_resize: self.config.on_resize.clone(),
classes: self.config.classes.clone(),
user_id: self.config.user_id.clone(),
}
}
fn get_or_build(&self) -> &ResizableGroup {
::blinc_layout::build_once::build_once(&self.built, || {
ResizableGroup::from_config(self.take_config())
})
}
pub fn build(self) -> ResizableGroup {
ResizableGroup::from_config(self.take_config())
}
}
impl Default for ResizableGroupBuilder {
fn default() -> Self {
Self::new()
}
}
impl ElementBuilder for ResizableGroupBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().inner.children_builders()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().inner.element_id()
}
}
#[track_caller]
pub fn resizable_group() -> ResizableGroupBuilder {
ResizableGroupBuilder::new()
}
pub fn resizable_panel() -> ResizablePanelBuilder {
ResizablePanelBuilder::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resize_direction_default() {
assert_eq!(ResizeDirection::default(), ResizeDirection::Horizontal);
}
#[test]
fn test_panel_builder_defaults() {
let config = resizable_panel().build_config();
assert_eq!(config.min_size, 50.0);
assert_eq!(config.max_size, None);
assert!(!config.is_flex);
}
#[test]
fn test_panel_builder_with_size() {
let config = resizable_panel()
.default_size(300.0)
.min_size(100.0)
.max_size(500.0)
.build_config();
assert_eq!(config.default_size, Some(300.0));
assert_eq!(config.min_size, 100.0);
assert_eq!(config.max_size, Some(500.0));
}
#[test]
fn test_panel_builder_flex() {
let config = resizable_panel().flex_grow().build_config();
assert!(config.is_flex);
assert_eq!(config.flex_grow, 1.0);
}
}