Skip to main content

iced_resizable_split/
split.rs

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        // TODO: Implement touch events
232        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}