1use crate::{
5 behavior::splitter::{drag_value, key_value, settle, KeyContext, SplitUnit},
6 core::{
7 assets,
8 attrs::component_attributes,
9 measure::{custom_property, CustomProperty},
10 platform::DomBridge,
11 },
12 prelude::*,
13 theme::use_stylesheets,
14};
15use std::rc::Rc;
16const SPLIT: Bem = Bem::new("fc-split");
17const PANE: Bem = Bem::new("fc-split__pane");
18const BAR: Bem = Bem::new("fc-splitter");
19const RESIZABLE: Bem = Bem::new("fc-resizable");
20fc_modifier! { enum SplitFlow for SPLIT { Row => None, Stacked => "stacked" } }
21fc_modifier! { enum PaneRole for PANE { Secondary => None, Primary => "primary" } }
22fc_modifier! { enum BarKind for BAR { Between => None, Edge => "edge" } }
23fc_modifier! { enum ResizeAxis for RESIZABLE { Inline => None, Block => "block" } }
24
25#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Resize {
29 pub value: f64,
31 pub settled: bool,
33}
34
35#[derive(Clone, Copy, PartialEq)]
36struct Drag {
37 start: f64,
38 origin: f64,
39 per_pixel: f64,
40}
41
42#[derive(Clone, Copy)]
43struct Bar {
44 unit: SplitUnit,
45 inline: bool,
46 growth: f64,
47 min: f64,
48 max: f64,
49 step: f64,
50 disabled: bool,
51}
52
53#[allow(clippy::too_many_arguments)]
55fn separator(
56 bar: Bar,
57 label: &AccessibleName,
58 controls: &Id,
59 mut value: Signal<f64>,
60 mut restore: Signal<Option<f64>>,
61 mut drag: Signal<Option<Drag>>,
62 frame: Signal<Option<Rc<MountedData>>>,
63 onresize: Option<EventHandler<Resize>>,
64 handle: &Id,
65 rtl: bool,
66) -> Element {
67 let Bar {
68 unit,
69 inline,
70 growth,
71 min,
72 max,
73 step,
74 disabled,
75 } = bar;
76 let mut set = move |next: f64, settled: bool| {
77 let next = settle(next, min, max, unit);
78 if (*value.peek() - next).abs() > f64::EPSILON || settled {
80 value.set(next);
81 if let Some(onresize) = onresize {
82 onresize.call(Resize {
83 value: next,
84 settled,
85 });
86 }
87 }
88 next
89 };
90 let class = ClassList::new(BAR.block())
91 .with_modifier(
92 BAR,
93 if unit == SplitUnit::Px {
94 BarKind::Edge
95 } else {
96 BarKind::Between
97 },
98 )
99 .to_string();
100 let capture = handle.clone();
101 rsx! {
102 div { class, id: "{handle}", role: "separator", tabindex: "0",
103 aria_orientation: if inline { "vertical" } else { "horizontal" },
104 aria_controls: "{controls}", aria_label: label.to_string(),
105 aria_valuenow: "{value}", aria_valuemin: "{min}", aria_valuemax: "{max}",
106 aria_disabled: disabled.then_some("true"),
107 onkeydown: move |event| {
108 if disabled { return; }
109 if event.key() == Key::Escape {
110 if let Some(start) = drag() {
111 event.prevent_default();
112 event.stop_propagation();
113 drag.set(None);
114 set(start.start, true);
115 }
116 return;
117 }
118 let context = KeyContext { inline, rtl, growth, current: value(), min, max, step, restore: restore() };
119 let Some((next, remember)) = key_value(&event.key(), event.modifiers().shift(), context) else { return };
120 event.prevent_default();
121 if let Some(remember) = remember { restore.set(Some(remember)); }
122 set(next, true);
123 },
124 onpointerdown: move |event| {
125 if disabled || event.trigger_button() != Some(dioxus::html::input_data::MouseButton::Primary) { return; }
126 event.prevent_default();
127 let point = event.client_coordinates();
128 let origin = if inline { point.x } else { point.y };
129 let pointer = event.pointer_id();
130 let start = value();
131 let capture = capture.clone();
132 spawn(async move {
133 let per_pixel = if unit == SplitUnit::Percent {
134 let Some(frame) = frame() else { return };
135 let Ok(rect) = frame.get_client_rect().await else { return };
136 let total = if inline { rect.width() } else { rect.height() };
137 if total <= 0.0 { return; }
138 100.0 / total
139 } else {
140 1.0
141 };
142 let _ = DomBridge.capture_pointer(&capture, pointer).await;
143 let _ = DomBridge.focus(&capture).await;
144 drag.set(Some(Drag { start, origin, per_pixel }));
145 });
146 },
147 onpointermove: move |event| {
148 let Some(start) = drag() else { return };
149 let point = event.client_coordinates();
150 let delta = (if inline { point.x } else { point.y }) - start.origin;
151 set(drag_value(start.start, delta, start.per_pixel, growth, inline && rtl), false);
152 },
153 onpointerup: move |_| {
154 if drag().is_some() {
155 drag.set(None);
156 let settled = set(value(), true);
157 if settled > min { restore.set(Some(settled)); }
158 }
159 },
160 onpointercancel: move |_| {
161 if drag().is_some() {
162 drag.set(None);
163 set(value(), true);
164 }
165 },
166 }
167 }
168}
169
170#[component]
178pub fn Split(
179 label: AccessibleName,
180 primary: Element,
181 secondary: Element,
182 #[props(default)]
184 primary_last: bool,
185 #[props(default)]
187 stacked: bool,
188 #[props(default)]
190 block_size: Option<String>,
191 #[props(default)] value: Option<ReadSignal<f64>>,
192 #[props(default = 50.0)] default_value: f64,
193 #[props(default = 0.0)] min: f64,
194 #[props(default = 100.0)] max: f64,
195 #[props(default = 5.0)] step: f64,
196 #[props(default)] onresize: Option<EventHandler<Resize>>,
197 #[props(default)]
199 primary_label: Option<AccessibleName>,
200 #[props(default)] availability: Availability,
201 #[props(default)] class: Option<String>,
202 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
203) -> Element {
204 let mut current = use_signal(|| settle(default_value, min, max, SplitUnit::Percent));
205 if let Some(value) = value {
206 let outer = settle(*value.read(), min, max, SplitUnit::Percent);
207 if (*current.peek() - outer).abs() > f64::EPSILON {
208 current.set(outer);
209 }
210 }
211 let restore = use_signal(|| None);
212 let drag = use_signal(|| None);
213 let mut frame = use_signal(|| None);
214 let pane = use_id();
215 let handle = use_id();
216 let rtl = use_direction() == Direction::Rtl;
217 let mut attributes = component_attributes(
218 attributes,
219 ClassList::new(SPLIT.block())
220 .with_modifier(
221 SPLIT,
222 if stacked {
223 SplitFlow::Stacked
224 } else {
225 SplitFlow::Row
226 },
227 )
228 .extend(class.as_deref()),
229 &["style"],
230 );
231 if let (true, Some(block_size)) = (stacked, &block_size) {
232 attributes.push(custom_property(CustomProperty::SplitBlockSize, block_size));
233 }
234 let size = current();
235 let primary_view = rsx! {
236 div { class: ClassList::new(PANE.block()).with_modifier(PANE, PaneRole::Primary).to_string(), id: "{pane}",
237 hidden: size <= 0.0,
238 role: primary_label.as_ref().map(|_| "region"), aria_label: primary_label.as_ref().map(ToString::to_string),
239 tabindex: primary_label.as_ref().map(|_| "0"),
240 ..crate::spread![custom_property(CustomProperty::SplitSize, format!("{size}%"))],
241 {primary}
242 }
243 };
244 let bar = Bar {
245 unit: SplitUnit::Percent,
246 inline: !stacked,
247 growth: if primary_last { -1.0 } else { 1.0 },
249 min,
250 max,
251 step,
252 disabled: availability != Availability::Enabled,
253 };
254 let bar = separator(
255 bar, &label, &pane, current, restore, drag, frame, onresize, &handle, rtl,
256 );
257 let secondary_view = rsx! { div { class: PANE.block(), {secondary} } };
258 rsx! {
259 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_SPLITTER_CSS } }
260 div { onmounted: move |event| frame.set(Some(event.data())), ..attributes,
261 if primary_last { {secondary_view} {bar} {primary_view} } else { {primary_view} {bar} {secondary_view} }
262 }
263 }
264}
265
266#[component]
268pub fn Resizable(
269 label: AccessibleName,
270 #[props(default)]
272 block: bool,
273 #[props(default)] value: Option<ReadSignal<f64>>,
274 default_value: f64,
275 #[props(default = 0.0)] min: f64,
276 max: f64,
277 #[props(default = 16.0)] step: f64,
278 #[props(default)] onresize: Option<EventHandler<Resize>>,
279 #[props(default)] availability: Availability,
280 #[props(default)] class: Option<String>,
281 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
282 children: Element,
283) -> Element {
284 let mut current = use_signal(|| settle(default_value, min, max, SplitUnit::Px));
285 if let Some(value) = value {
286 let outer = settle(*value.read(), min, max, SplitUnit::Px);
287 if (*current.peek() - outer).abs() > f64::EPSILON {
288 current.set(outer);
289 }
290 }
291 let restore = use_signal(|| None);
292 let drag = use_signal(|| None);
293 let frame = use_signal(|| None);
294 let id = use_id();
295 let handle = use_id();
296 let rtl = use_direction() == Direction::Rtl;
297 let mut attributes = component_attributes(
298 attributes,
299 ClassList::new(RESIZABLE.block())
300 .with_modifier(
301 RESIZABLE,
302 if block {
303 ResizeAxis::Block
304 } else {
305 ResizeAxis::Inline
306 },
307 )
308 .extend(class.as_deref()),
309 &["id", "style"],
310 );
311 attributes.push(custom_property(
312 CustomProperty::SplitSize,
313 format!("{}px", current()),
314 ));
315 let bar = Bar {
316 unit: SplitUnit::Px,
317 inline: !block,
318 growth: 1.0,
319 min,
320 max,
321 step,
322 disabled: availability != Availability::Enabled,
323 };
324 rsx! {
325 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_SPLITTER_CSS } }
326 div { id: "{id}", ..attributes,
327 {children}
328 {separator(bar, &label, &id, current, restore, drag, frame, onresize, &handle, rtl)}
329 }
330 }
331}