Skip to main content

iced_resizable_split/
split_horizontal.rs

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