Skip to main content

iced_resizable_split/
split.rs

1pub struct Split<'a, Message, Theme, Renderer> {
2    axis: Axis,
3    first: iced_core::Element<'a, Message, Theme, Renderer>,
4    second: iced_core::Element<'a, Message, Theme, Renderer>,
5
6    state: super::state::State,
7    on_drag: Box<dyn Fn(super::state::State) -> Message + 'a>,
8
9    drag_area_size: f32,
10    style: super::style::StyleFn<'a, Theme>,
11}
12
13impl<'a, Message, Theme, Renderer> Split<'a, Message, Theme, Renderer>
14where
15    Theme: 'a,
16{
17    pub fn new(
18        axis: Axis,
19        first: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
20        second: impl Into<iced_core::Element<'a, Message, Theme, Renderer>>,
21        state: super::state::State,
22        message: impl Fn(super::state::State) -> Message + 'a,
23    ) -> Self {
24        Self {
25            axis,
26            first: first.into(),
27            second: second.into(),
28            state,
29            on_drag: Box::new(message),
30            drag_area_size: super::DEFAULT_DRAG_AREA_SIZE,
31            style: Box::new(|_, _| super::style::Style::default()),
32        }
33    }
34
35    #[must_use]
36    pub fn style(
37        mut self,
38        style: impl Fn(&Theme, super::style::State) -> super::style::Style + 'a,
39    ) -> Self {
40        self.style = Box::new(style);
41        self
42    }
43
44    #[must_use]
45    pub const fn drag_area_size(mut self, size: f32) -> Self {
46        self.drag_area_size = size;
47        self
48    }
49}
50
51impl<Message, Theme, Renderer> Split<'_, Message, Theme, Renderer> {
52    fn split_pos(&self, size: iced_core::Size) -> f32 {
53        match self.axis {
54            Axis::Vertical => size.width * self.state.ratio(),
55            Axis::Horizontal => size.height * self.state.ratio(),
56        }
57    }
58
59    const fn split_relative_position(&self, bounds: iced_core::Rectangle) -> f32 {
60        match self.axis {
61            Axis::Vertical => bounds.width.mul_add(self.state.ratio(), bounds.x),
62            Axis::Horizontal => bounds.height.mul_add(self.state.ratio(), bounds.y),
63        }
64    }
65
66    fn create_split_rect(&self, size: f32, bounds: iced_core::Rectangle) -> iced_core::Rectangle {
67        let divider_pos = self.split_relative_position(bounds);
68
69        match self.axis {
70            Axis::Vertical => iced_core::Rectangle {
71                x: divider_pos - (size / 2.0),
72                y: bounds.y,
73                width: size,
74                height: bounds.height,
75            },
76            Axis::Horizontal => iced_core::Rectangle {
77                x: bounds.x,
78                y: divider_pos - (size / 2.0),
79                width: bounds.width,
80                height: size,
81            },
82        }
83    }
84
85    fn limit_nodes_size(
86        &self,
87        size: iced_core::Size,
88        limits: iced_core::layout::Limits,
89        split_pos: f32,
90    ) -> (iced_core::layout::Limits, iced_core::layout::Limits) {
91        match self.axis {
92            Axis::Vertical => (
93                limits.max_width(split_pos),
94                limits.max_width(size.width - split_pos),
95            ),
96            Axis::Horizontal => (
97                limits.max_height(split_pos),
98                limits.max_height(size.height - split_pos),
99            ),
100        }
101    }
102
103    const fn second_node_position(&self, split_pos: f32) -> iced_core::Point {
104        match self.axis {
105            Axis::Vertical => iced_core::Point::new(split_pos, 0.0),
106            Axis::Horizontal => iced_core::Point::new(0.0, split_pos),
107        }
108    }
109
110    fn relative_position(&self, mouse_pos: iced_core::Point, bounds: iced_core::Rectangle) -> f32 {
111        match self.axis {
112            Axis::Vertical => mouse_pos.x - bounds.x,
113            Axis::Horizontal => mouse_pos.y - bounds.y,
114        }
115    }
116
117    fn ratio(&self, mouse_pos: iced_core::Point, bounds: iced_core::Rectangle) -> f32 {
118        let relative_mouse_position = self.relative_position(mouse_pos, bounds);
119        match self.axis {
120            Axis::Vertical => (relative_mouse_position / bounds.width).clamp(0.0, 1.0),
121            Axis::Horizontal => (relative_mouse_position / bounds.height).clamp(0.0, 1.0),
122        }
123    }
124}
125
126impl<Message, Theme, Renderer> iced_core::Widget<Message, Theme, Renderer>
127    for Split<'_, Message, Theme, Renderer>
128where
129    Renderer: iced_core::renderer::Renderer,
130{
131    fn tag(&self) -> iced_core::widget::tree::Tag {
132        iced_core::widget::tree::Tag::of::<super::state::IsDragging>()
133    }
134
135    fn state(&self) -> iced_core::widget::tree::State {
136        iced_core::widget::tree::State::new(false)
137    }
138
139    fn children(&self) -> Vec<iced_core::widget::Tree> {
140        vec![
141            iced_core::widget::Tree::new(&self.first),
142            iced_core::widget::Tree::new(&self.second),
143        ]
144    }
145
146    fn diff(&self, tree: &mut iced_core::widget::Tree) {
147        tree.diff_children(&[&self.first, &self.second]);
148    }
149
150    fn size(&self) -> iced_core::Size<iced_core::Length> {
151        iced_core::Size::new(iced_core::Length::Fill, iced_core::Length::Fill)
152    }
153
154    fn layout(
155        &mut self,
156        tree: &mut iced_core::widget::Tree,
157        renderer: &Renderer,
158        limits: &iced_core::layout::Limits,
159    ) -> iced_core::layout::Node {
160        let limits = limits
161            .width(iced_core::Length::Fill)
162            .height(iced_core::Length::Fill);
163        let size = limits.resolve(
164            iced_core::Length::Fill,
165            iced_core::Length::Fill,
166            iced_core::Size::ZERO,
167        );
168
169        let split_pos = self.split_pos(size);
170        let (first_limits, second_limits) = self.limit_nodes_size(size, limits, split_pos);
171
172        let first_node =
173            self.first
174                .as_widget_mut()
175                .layout(&mut tree.children[0], renderer, &first_limits);
176
177        let second_node = self
178            .second
179            .as_widget_mut()
180            .layout(&mut tree.children[1], renderer, &second_limits)
181            .move_to(self.second_node_position(split_pos));
182
183        iced_core::layout::Node::with_children(size, vec![first_node, second_node])
184    }
185
186    fn update(
187        &mut self,
188        tree: &mut iced_core::widget::Tree,
189        event: &iced_core::Event,
190        layout: iced_core::Layout<'_>,
191        cursor: iced_core::mouse::Cursor,
192        renderer: &Renderer,
193        clipboard: &mut dyn iced_core::Clipboard,
194        shell: &mut iced_core::Shell<'_, Message>,
195        viewport: &iced_core::Rectangle,
196    ) {
197        let is_dragging = tree.state.downcast_mut::<super::state::IsDragging>();
198
199        let bounds = layout.bounds();
200        let drag_rect = self.create_split_rect(self.drag_area_size, bounds);
201
202        // TODO: Implement touch events
203        match event {
204            iced_core::Event::Mouse(iced_core::mouse::Event::ButtonPressed(
205                iced_core::mouse::Button::Left,
206            )) => {
207                if let Some(cursor_pos) = cursor.position()
208                    && drag_rect.contains(cursor_pos)
209                {
210                    *is_dragging = true;
211                    shell.capture_event();
212                    return;
213                }
214            }
215
216            iced_core::Event::Mouse(
217                iced_core::mouse::Event::CursorLeft
218                | iced_core::mouse::Event::ButtonReleased(iced_core::mouse::Button::Left),
219            ) if *is_dragging => {
220                *is_dragging = false;
221
222                shell.request_redraw();
223                shell.capture_event();
224                return;
225            }
226
227            iced_core::Event::Mouse(iced_core::mouse::Event::CursorMoved { position })
228                if *is_dragging =>
229            {
230                let new_ratio = self.ratio(*position, bounds);
231
232                let next_state = self.state.copy_with_new_ratio(new_ratio);
233                if next_state != self.state {
234                    shell.publish((self.on_drag)(next_state));
235                }
236
237                shell.capture_event();
238                return;
239            }
240
241            _ => {}
242        }
243
244        let mut layouts = layout.children();
245        self.first.as_widget_mut().update(
246            &mut tree.children[0],
247            event,
248            layouts.next().unwrap(),
249            cursor,
250            renderer,
251            clipboard,
252            shell,
253            viewport,
254        );
255
256        self.second.as_widget_mut().update(
257            &mut tree.children[1],
258            event,
259            layouts.next().unwrap(),
260            cursor,
261            renderer,
262            clipboard,
263            shell,
264            viewport,
265        );
266    }
267
268    fn draw(
269        &self,
270        tree: &iced_core::widget::Tree,
271        renderer: &mut Renderer,
272        theme: &Theme,
273        style: &iced_core::renderer::Style,
274        layout: iced_core::Layout<'_>,
275        cursor: iced_core::mouse::Cursor,
276        viewport: &iced_core::Rectangle,
277    ) {
278        let mut layouts = layout.children();
279        let first_layout = layouts.next().unwrap();
280        let second_layout = layouts.next().unwrap();
281
282        self.first.as_widget().draw(
283            &tree.children[0],
284            renderer,
285            theme,
286            style,
287            first_layout,
288            cursor,
289            viewport,
290        );
291
292        self.second.as_widget().draw(
293            &tree.children[1],
294            renderer,
295            theme,
296            style,
297            second_layout,
298            cursor,
299            viewport,
300        );
301
302        let bounds = layout.bounds();
303        let is_hovering = cursor.position().is_some_and(|position| {
304            let hover_rect = self.create_split_rect(self.drag_area_size, bounds);
305            hover_rect.contains(position)
306        });
307
308        let status = if *tree.state.downcast_ref::<super::state::IsDragging>() {
309            super::style::State::Dragging
310        } else if is_hovering {
311            super::style::State::Hovering
312        } else {
313            super::style::State::Idle
314        };
315
316        let style = (self.style)(theme, status);
317
318        renderer.fill_quad(
319            iced_core::renderer::Quad {
320                bounds: self.create_split_rect(style.divider_width, bounds),
321                ..Default::default()
322            },
323            style.divider_color,
324        );
325    }
326}
327
328impl<'a, Message, Theme, Renderer> From<Split<'a, Message, Theme, Renderer>>
329    for iced_core::Element<'a, Message, Theme, Renderer>
330where
331    Message: 'a,
332    Theme: 'a,
333    Renderer: iced_core::Renderer + 'a,
334{
335    fn from(value: Split<'a, Message, Theme, Renderer>) -> Self {
336        Self::new(value)
337    }
338}
339
340pub enum Axis {
341    Vertical,
342    Horizontal,
343}