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 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}