1pub struct Split<'a, Message, Renderer, Theme = iced_core::Theme>
2where
3 Theme: super::style::Catalog,
4{
5 axis: Axis,
6 first: iced_core::Element<'a, Message, Theme, Renderer>,
7 second: iced_core::Element<'a, Message, Theme, Renderer>,
8
9 state: super::state::State,
10 on_drag: Box<dyn Fn(super::state::State) -> Message + 'a>,
11
12 drag_area_size: f32,
13 class: Theme::Class<'a>,
14}
15
16impl<'a, Message, Theme, Renderer> Split<'a, Message, Renderer, Theme>
17where
18 Theme: super::style::Catalog,
19{
20 pub fn new(
21 axis: Axis,
22 first: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
23 second: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
24 state: super::state::State,
25 message: impl Fn(super::state::State) -> Message + 'a,
26 ) -> Self {
27 Self {
28 axis,
29 first: first.into(),
30 second: second.into(),
31 state,
32 on_drag: Box::new(message),
33 drag_area_size: super::DEFAULT_DRAG_AREA_SIZE,
34 class: Theme::default(),
35 }
36 }
37
38 #[must_use]
39 pub fn style(
40 mut self,
41 style: impl Fn(&Theme, super::style::Status) -> super::style::Style + 'a,
42 ) -> Self
43 where
44 Theme::Class<'a>: From<super::style::StyleFn<'a, Theme>>,
45 {
46 self.class = (Box::new(style) as super::style::StyleFn<'a, Theme>).into();
47 self
48 }
49
50 #[cfg(feature = "advanced")]
51 #[must_use]
52 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
53 self.class = class.into();
54 self
55 }
56
57 #[must_use]
58 pub const fn drag_area_size(mut self, size: f32) -> Self {
59 self.drag_area_size = size;
60 self
61 }
62}
63
64impl<Message, Theme, Renderer> Split<'_, Message, Renderer, Theme>
65where
66 Theme: super::style::Catalog,
67{
68 fn split_pos(&self, size: iced_core::Size) -> f32 {
69 match self.axis {
70 Axis::Vertical => size.width * self.state.ratio(),
71 Axis::Horizontal => size.height * self.state.ratio(),
72 }
73 }
74
75 const fn split_relative_position(&self, bounds: iced_core::Rectangle) -> f32 {
76 match self.axis {
77 Axis::Vertical => bounds.width.mul_add(self.state.ratio(), bounds.x),
78 Axis::Horizontal => bounds.height.mul_add(self.state.ratio(), bounds.y),
79 }
80 }
81
82 fn create_split_rect(&self, size: f32, bounds: iced_core::Rectangle) -> iced_core::Rectangle {
83 let divider_pos = self.split_relative_position(bounds);
84
85 match self.axis {
86 Axis::Vertical => iced_core::Rectangle {
87 x: divider_pos - (size / 2.0),
88 y: bounds.y,
89 width: size,
90 height: bounds.height,
91 },
92 Axis::Horizontal => iced_core::Rectangle {
93 x: bounds.x,
94 y: divider_pos - (size / 2.0),
95 width: bounds.width,
96 height: size,
97 },
98 }
99 }
100
101 fn limit_nodes_size(
102 &self,
103 size: iced_core::Size,
104 limits: iced_core::layout::Limits,
105 split_pos: f32,
106 ) -> (iced_core::layout::Limits, iced_core::layout::Limits) {
107 match self.axis {
108 Axis::Vertical => (
109 limits.min_width(0.0).max_width(split_pos),
110 limits.min_width(0.0).max_width(size.width - split_pos),
111 ),
112 Axis::Horizontal => (
113 limits.min_height(0.0).max_height(split_pos),
114 limits.min_height(0.0).max_height(size.height - split_pos),
115 ),
116 }
117 }
118
119 const fn second_node_position(&self, split_pos: f32) -> iced_core::Point {
120 match self.axis {
121 Axis::Vertical => iced_core::Point::new(split_pos, 0.0),
122 Axis::Horizontal => iced_core::Point::new(0.0, split_pos),
123 }
124 }
125
126 fn relative_position(&self, mouse_pos: iced_core::Point, bounds: iced_core::Rectangle) -> f32 {
127 match self.axis {
128 Axis::Vertical => mouse_pos.x - bounds.x,
129 Axis::Horizontal => mouse_pos.y - bounds.y,
130 }
131 }
132
133 fn ratio(&self, mouse_pos: iced_core::Point, bounds: iced_core::Rectangle) -> f32 {
134 let relative_mouse_position = self.relative_position(mouse_pos, bounds);
135 match self.axis {
136 Axis::Vertical => {
137 if bounds.width <= 0.0 {
138 0.0
139 } else {
140 (relative_mouse_position / bounds.width).clamp(0.0, 1.0)
141 }
142 }
143 Axis::Horizontal => {
144 if bounds.height <= 0.0 {
145 0.0
146 } else {
147 (relative_mouse_position / bounds.height).clamp(0.0, 1.0)
148 }
149 }
150 }
151 }
152}
153
154impl<Message, Theme, Renderer> iced_core::Widget<Message, Theme, Renderer>
155 for Split<'_, Message, Renderer, Theme>
156where
157 Theme: super::style::Catalog,
158 Renderer: iced_core::renderer::Renderer,
159{
160 fn tag(&self) -> iced_core::widget::tree::Tag {
161 iced_core::widget::tree::Tag::of::<super::state::InternalState>()
162 }
163
164 fn state(&self) -> iced_core::widget::tree::State {
165 iced_core::widget::tree::State::new(super::state::InternalState::default())
166 }
167
168 fn children(&self) -> Vec<iced_core::widget::Tree> {
169 vec![
170 iced_core::widget::Tree::new(&self.first),
171 iced_core::widget::Tree::new(&self.second),
172 ]
173 }
174
175 fn diff(&self, tree: &mut iced_core::widget::Tree) {
176 tree.diff_children(&[&self.first, &self.second]);
177 }
178
179 fn size(&self) -> iced_core::Size<iced_core::Length> {
180 iced_core::Size::new(iced_core::Length::Fill, iced_core::Length::Fill)
181 }
182
183 fn layout(
184 &mut self,
185 tree: &mut iced_core::widget::Tree,
186 renderer: &Renderer,
187 limits: &iced_core::layout::Limits,
188 ) -> iced_core::layout::Node {
189 let limits = limits
190 .width(iced_core::Length::Fill)
191 .height(iced_core::Length::Fill);
192 let size = limits.resolve(
193 iced_core::Length::Fill,
194 iced_core::Length::Fill,
195 iced_core::Size::ZERO,
196 );
197
198 let split_pos = self.split_pos(size);
199 let (first_limits, second_limits) = self.limit_nodes_size(size, limits, split_pos);
200
201 let first_node =
202 self.first
203 .as_widget_mut()
204 .layout(&mut tree.children[0], renderer, &first_limits);
205
206 let second_node = self
207 .second
208 .as_widget_mut()
209 .layout(&mut tree.children[1], renderer, &second_limits)
210 .move_to(self.second_node_position(split_pos));
211
212 iced_core::layout::Node::with_children(size, vec![first_node, second_node])
213 }
214
215 fn update(
216 &mut self,
217 tree: &mut iced_core::widget::Tree,
218 event: &iced_core::Event,
219 layout: iced_core::Layout<'_>,
220 cursor: iced_core::mouse::Cursor,
221 renderer: &Renderer,
222 clipboard: &mut dyn iced_core::Clipboard,
223 shell: &mut iced_core::Shell<'_, Message>,
224 viewport: &iced_core::Rectangle,
225 ) {
226 let internal_state = tree.state.downcast_mut::<super::state::InternalState>();
227
228 let bounds = layout.bounds();
229 let drag_rect = self.create_split_rect(self.drag_area_size, bounds);
230
231 match event {
233 iced_core::Event::Mouse(iced_core::mouse::Event::ButtonPressed(
234 iced_core::mouse::Button::Left,
235 )) => {
236 if let Some(cursor_pos) = cursor.position()
237 && drag_rect.contains(cursor_pos)
238 {
239 internal_state.is_dragging = true;
240
241 shell.request_redraw();
242 shell.capture_event();
243 return;
244 }
245 }
246
247 iced_core::Event::Mouse(
248 iced_core::mouse::Event::CursorLeft
249 | iced_core::mouse::Event::ButtonReleased(iced_core::mouse::Button::Left),
250 ) if internal_state.is_dragging => {
251 internal_state.is_dragging = false;
252
253 shell.request_redraw();
254 shell.capture_event();
255 return;
256 }
257
258 iced_core::Event::Mouse(iced_core::mouse::Event::CursorMoved { position }) => {
259 if internal_state.is_dragging {
260 let next_state = self
261 .state
262 .copy_with_new_ratio(self.ratio(*position, bounds));
263
264 if next_state != self.state {
265 shell.publish((self.on_drag)(next_state));
266 }
267
268 shell.capture_event();
269 return;
270 }
271
272 let is_over_divider = drag_rect.contains(*position);
273 if is_over_divider != internal_state.is_hovering {
274 internal_state.is_hovering = is_over_divider;
275 shell.request_redraw();
276 }
277 }
278
279 _ => {}
280 }
281
282 let children_layouts: Vec<_> = layout.children().collect();
283 assert_eq!(
284 children_layouts.len(),
285 2,
286 "Resizable Split Widget must have exactly 2 children"
287 );
288
289 self.first.as_widget_mut().update(
290 &mut tree.children[0],
291 event,
292 children_layouts[0],
293 cursor,
294 renderer,
295 clipboard,
296 shell,
297 viewport,
298 );
299 self.second.as_widget_mut().update(
300 &mut tree.children[1],
301 event,
302 children_layouts[1],
303 cursor,
304 renderer,
305 clipboard,
306 shell,
307 viewport,
308 );
309 }
310
311 fn draw(
312 &self,
313 tree: &iced_core::widget::Tree,
314 renderer: &mut Renderer,
315 theme: &Theme,
316 style: &iced_core::renderer::Style,
317 layout: iced_core::Layout<'_>,
318 cursor: iced_core::mouse::Cursor,
319 viewport: &iced_core::Rectangle,
320 ) {
321 let children_layouts: Vec<_> = layout.children().collect();
322 assert_eq!(
323 children_layouts.len(),
324 2,
325 "Resizable Split Widget must have exactly 2 children"
326 );
327
328 self.first.as_widget().draw(
329 &tree.children[0],
330 renderer,
331 theme,
332 style,
333 children_layouts[0],
334 cursor,
335 viewport,
336 );
337
338 self.second.as_widget().draw(
339 &tree.children[1],
340 renderer,
341 theme,
342 style,
343 children_layouts[1],
344 cursor,
345 viewport,
346 );
347
348 let internal_state = tree.state.downcast_ref::<super::state::InternalState>();
349 let status = if internal_state.is_dragging {
350 super::style::Status::Dragging
351 } else if internal_state.is_hovering {
352 super::style::Status::Hovering
353 } else {
354 super::style::Status::Idle
355 };
356
357 let resolved = theme.style(&self.class, status);
358
359 renderer.fill_quad(
360 iced_core::renderer::Quad {
361 bounds: self.create_split_rect(resolved.divider_width, layout.bounds()),
362 snap: resolved.snap,
363 ..Default::default()
364 },
365 resolved.divider_color,
366 );
367 }
368
369 fn mouse_interaction(
370 &self,
371 tree: &iced_core::widget::Tree,
372 layout: iced_core::Layout<'_>,
373 cursor: iced_core::mouse::Cursor,
374 viewport: &iced_core::Rectangle,
375 renderer: &Renderer,
376 ) -> iced_core::mouse::Interaction {
377 let internal_state = tree.state.downcast_ref::<super::state::InternalState>();
378
379 if internal_state.is_dragging || internal_state.is_hovering {
380 match self.axis {
381 Axis::Vertical => iced_core::mouse::Interaction::ResizingHorizontally,
382 Axis::Horizontal => iced_core::mouse::Interaction::ResizingVertically,
383 }
384 } else {
385 let mut children = layout.children();
386
387 let first_interaction =
388 children
389 .next()
390 .map_or(iced_core::mouse::Interaction::None, |layout| {
391 self.first.as_widget().mouse_interaction(
392 &tree.children[0],
393 layout,
394 cursor,
395 viewport,
396 renderer,
397 )
398 });
399
400 let second_interaction =
401 children
402 .next()
403 .map_or(iced_core::mouse::Interaction::None, |layout| {
404 self.second.as_widget().mouse_interaction(
405 &tree.children[1],
406 layout,
407 cursor,
408 viewport,
409 renderer,
410 )
411 });
412
413 first_interaction.max(second_interaction)
414 }
415 }
416}
417
418impl<'a, Message, Theme, Renderer> From<Split<'a, Message, Renderer, Theme>>
419 for iced_core::Element<'a, Message, Theme, Renderer>
420where
421 Message: 'a,
422 Theme: super::style::Catalog + 'a,
423 Renderer: iced_core::Renderer + 'a,
424{
425 fn from(value: Split<'a, Message, Renderer, Theme>) -> Self {
426 Self::new(value)
427 }
428}
429
430pub enum Axis {
431 Vertical,
432 Horizontal,
433}