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::InternalState>()
133 }
134
135 fn state(&self) -> iced_core::widget::tree::State {
136 iced_core::widget::tree::State::new(super::state::InternalState::default())
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 internal_state = tree.state.downcast_mut::<super::state::InternalState>();
198
199 let bounds = layout.bounds();
200 let drag_rect = self.create_split_rect(self.drag_area_size, bounds);
201
202 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 internal_state.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 internal_state.is_dragging => {
220 internal_state.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 internal_state.is_dragging {
229 let next_state = self
230 .state
231 .copy_with_new_ratio(self.ratio(*position, bounds));
232
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 let is_over_divider = drag_rect.contains(*position);
242 if is_over_divider != internal_state.is_hovering {
243 internal_state.is_hovering = is_over_divider;
244 shell.request_redraw();
245 }
246 }
247
248 _ => {}
249 }
250
251 let mut layouts = layout.children();
252 self.first.as_widget_mut().update(
253 &mut tree.children[0],
254 event,
255 layouts.next().unwrap(),
256 cursor,
257 renderer,
258 clipboard,
259 shell,
260 viewport,
261 );
262
263 self.second.as_widget_mut().update(
264 &mut tree.children[1],
265 event,
266 layouts.next().unwrap(),
267 cursor,
268 renderer,
269 clipboard,
270 shell,
271 viewport,
272 );
273 }
274
275 fn draw(
276 &self,
277 tree: &iced_core::widget::Tree,
278 renderer: &mut Renderer,
279 theme: &Theme,
280 style: &iced_core::renderer::Style,
281 layout: iced_core::Layout<'_>,
282 cursor: iced_core::mouse::Cursor,
283 viewport: &iced_core::Rectangle,
284 ) {
285 let mut layouts = layout.children();
286 let first_layout = layouts.next().unwrap();
287 let second_layout = layouts.next().unwrap();
288
289 self.first.as_widget().draw(
290 &tree.children[0],
291 renderer,
292 theme,
293 style,
294 first_layout,
295 cursor,
296 viewport,
297 );
298
299 self.second.as_widget().draw(
300 &tree.children[1],
301 renderer,
302 theme,
303 style,
304 second_layout,
305 cursor,
306 viewport,
307 );
308
309 let internal_state = tree.state.downcast_ref::<super::state::InternalState>();
310 let status = if internal_state.is_dragging {
311 super::style::State::Dragging
312 } else if internal_state.is_hovering {
313 super::style::State::Hovering
314 } else {
315 super::style::State::Idle
316 };
317
318 let style = (self.style)(theme, status);
319
320 renderer.fill_quad(
321 iced_core::renderer::Quad {
322 bounds: self.create_split_rect(style.divider_width, layout.bounds()),
323 ..Default::default()
324 },
325 style.divider_color,
326 );
327 }
328
329 fn mouse_interaction(
330 &self,
331 _tree: &iced_core::widget::Tree,
332 layout: iced_core::Layout<'_>,
333 cursor: iced_core::mouse::Cursor,
334 _viewport: &iced_core::Rectangle,
335 _renderer: &Renderer,
336 ) -> iced_core::mouse::Interaction {
337 let drag_rect = self.create_split_rect(self.drag_area_size, layout.bounds());
338 if let Some(position) = cursor.position()
339 && drag_rect.contains(position)
340 {
341 match self.axis {
342 Axis::Vertical => iced_core::mouse::Interaction::ResizingHorizontally,
343 Axis::Horizontal => iced_core::mouse::Interaction::ResizingVertically,
344 }
345 } else {
346 iced_core::mouse::Interaction::None
347 }
348 }
349}
350
351impl<'a, Message, Theme, Renderer> From<Split<'a, Message, Theme, Renderer>>
352 for iced_core::Element<'a, Message, Theme, Renderer>
353where
354 Message: 'a,
355 Theme: 'a,
356 Renderer: iced_core::Renderer + 'a,
357{
358 fn from(value: Split<'a, Message, Theme, Renderer>) -> Self {
359 Self::new(value)
360 }
361}
362
363pub enum Axis {
364 Vertical,
365 Horizontal,
366}