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