Skip to main content

appcui/ui/hsplitter/
hsplitter.rs

1use self::layout::Dimension;
2
3use super::ResizeBehavior;
4use super::SplitterPanel;
5use crate::prelude::*;
6use crate::ui::layout::Coordinate;
7
8#[derive(Eq, PartialEq, Copy, Clone)]
9enum State {
10    None,
11    OverSeparator,
12    OverTopButton,
13    OverBottomButton,
14    ClickedOnTopButton,
15    ClickedOnBottomButton,
16    Dragging,
17}
18
19#[CustomControl(overwrite=OnPaint + OnKeyPressed + OnMouseEvent + OnResize, internal = true)]
20pub struct HSplitter {
21    top: Handle<SplitterPanel>,
22    bottom: Handle<SplitterPanel>,
23    min_left: Dimension,
24    min_right: Dimension,
25    pos: Coordinate,
26    preserve_pos: i32,
27    resize_behavior: ResizeBehavior,
28    state: State,
29}
30impl HSplitter {
31    /// Creates a new Horizontal Splitter control with the specified position, layout and resize behavior
32    /// The position can be a percentage (e.g. a float value) or an absolute value (e.g. an unsigned value)
33    /// The resize behavior can be one of the following values:
34    /// * `ResizeBehavior::PreserveAspectRatio` - the aspect ratio of the panels is preserved when resizing the control
35    /// * `ResizeBehavior::PreserveTopPanelSize` - the size of the top panel is preserved when resizing the control
36    /// * `ResizeBehavior::PreserveBottomPanelSize` - the size of the bottom panel is preserved when resizing the control
37    /// 
38    /// # Example
39    /// ```rust, no_run
40    /// use appcui::prelude::*;
41    /// 
42    /// let mut vs = HSplitter::new(0.5,layout!("d:f"),hsplitter::ResizeBehavior::PreserveTopPanelSize);
43    /// vs.add(hsplitter::Panel::Top,button!("PressMe,x:1,y:1,w:12"));
44    /// vs.add(hsplitter::Panel::Bottom,button!("PressMe,x:1,y:1,w:12"));
45    /// ```
46    pub fn new<T>(pos: T, layout: Layout, resize_behavior: ResizeBehavior) -> Self
47    where
48        Coordinate: From<T>,
49    {
50        let mut obj = Self {
51            base: ControlBase::with_status_flags(layout, StatusFlags::Visible | StatusFlags::Enabled | StatusFlags::AcceptInput),
52            top: Handle::None,
53            bottom: Handle::None,
54            pos: pos.into(),
55            min_left: Dimension::Absolute(0),
56            min_right: Dimension::Absolute(0),
57            state: State::None,
58            resize_behavior,
59            preserve_pos: 0,
60        };
61        obj.set_size_bounds(1, 3, u16::MAX, u16::MAX);
62        obj.top = obj.add_child(SplitterPanel::new());
63        obj.bottom = obj.add_child(SplitterPanel::new());
64        obj
65    }
66
67    /// Adds a new control to the specified panel of the splitter (top or bottom)
68    ///
69    /// # Example
70    /// ```rust, no_run
71    /// use appcui::prelude::*;
72    ///
73    /// let mut vs = HSplitter::new(0.5,layout!("d:f"),hsplitter::ResizeBehavior::PreserveTopPanelSize);
74    /// vs.add(hsplitter::Panel::Top,button!("PressMe,x:1,y:1,w:12"));
75    /// vs.add(hsplitter::Panel::Bottom,button!("PressMe,x:1,y:1,w:12"));   
76    /// ```
77    #[inline(always)]
78    pub fn add<T>(&mut self, panel: hsplitter::Panel, control: T) -> Handle<T>
79    where
80        T: Control + NotWindow + NotDesktop + 'static,
81    {
82        let h = if panel == hsplitter::Panel::Top { self.top } else { self.bottom };
83        let cm = RuntimeManager::get().get_controls_mut();
84        if let Some(panel) = cm.get_mut(h.cast()) {
85            panel.base_mut().add_child(control)
86        } else {
87            Handle::None
88        }
89    }
90
91    /// Sets the minimum height for the top or bottom panel
92    /// The value can be a percentage (e.g. a float value) or an absolute value (e.g. an unsigned value)
93    ///
94    /// # Example
95    /// ```rust, no_run
96    /// use appcui::prelude::*;
97    ///
98    /// let mut vs = HSplitter::new(0.5,layout!("d:f"),hsplitter::ResizeBehavior::PreserveTopPanelSize);
99    /// vs.add(hsplitter::Panel::Top,button!("PressMe,x:1,y:1,w:12"));
100    /// vs.add(hsplitter::Panel::Bottom,button!("PressMe,x:1,y:1,w:12"));
101    /// // minim 2 chars from Top
102    /// vs.set_min_height(hsplitter::Panel::Top,2);
103    /// // minim 20% from Bottom
104    /// vs.set_min_height(hsplitter::Panel::Bottom,0.2);
105    /// ```
106    pub fn set_min_height<T>(&mut self, panel: hsplitter::Panel, min_size: T)
107    where
108        Dimension: From<T>,
109    {
110        match panel {
111            hsplitter::Panel::Top => self.min_left = min_size.into(),
112            hsplitter::Panel::Bottom => self.min_right = min_size.into(),
113        }
114    }
115
116    /// Returns the absolute position of the splitter (in characters)
117    #[inline(always)]
118    pub fn position(&self) -> i32 {
119        self.pos.absolute(self.size().height.saturating_sub(1) as u16)
120    }
121
122    /// Sets the position of the splitter. The value can be a percentage (e.g. a float value) or an absolute value (e.g. an unsigned value)
123    pub fn set_position<T>(&mut self, pos: T)
124    where
125        Coordinate: From<T>,
126    {
127        // force type conversion
128        self.pos = pos.into();
129        // update the position of the splitter
130        self.update_position(self.pos, true);
131    }
132    fn update_position(&mut self, pos: Coordinate, upadate_preserve_position: bool) {
133        let bottom_most = self.size().height.saturating_sub(1) as u16;
134        let mut abs_value = pos.absolute(bottom_most);
135        let min_top_margin = self.min_left.absolute(bottom_most);
136        let min_bottom_margin = self.min_right.absolute(bottom_most);
137        if abs_value > (bottom_most as i32 - min_bottom_margin as i32) {
138            abs_value = bottom_most as i32 - min_bottom_margin as i32;
139        }
140        abs_value = abs_value.max(min_top_margin as i32);
141        match self.resize_behavior {
142            ResizeBehavior::PreserveAspectRatio => {
143                self.pos.update_with_absolute_value(abs_value as i16, bottom_most);
144            }
145            ResizeBehavior::PreserveTopPanelSize | ResizeBehavior::PreserveBottomPanelSize => {
146                // if the position is preserverd, there is no need to keep the percentage
147                self.pos = Coordinate::Absolute(abs_value);
148            }
149        };
150        self.update_panel_sizes(self.size());
151        if upadate_preserve_position {
152            match self.resize_behavior {
153                ResizeBehavior::PreserveTopPanelSize => {
154                    self.preserve_pos = self.pos.absolute(bottom_most);
155                }
156                ResizeBehavior::PreserveBottomPanelSize => {
157                    self.preserve_pos = bottom_most as i32 - self.pos.absolute(bottom_most);
158                }
159                _ => {}
160            }
161        }
162    }
163    fn update_panel_sizes(&mut self, new_size: Size) {
164        let splitter_pos = self.pos.absolute(new_size.height.saturating_sub(1) as u16).max(0) as u16;
165        let w = new_size.width as u16;
166        let h1 = self.top;
167        let h2 = self.bottom;
168        let rm = RuntimeManager::get();
169        if let Some(p1) = rm.get_control_mut(h1) {
170            p1.set_position(0, 0);
171            if splitter_pos > 0 {
172                p1.set_size(w, splitter_pos);
173                p1.set_visible(true);
174            } else {
175                p1.set_size(w, 0);
176                p1.set_visible(false);
177            }
178        }
179        if let Some(p2) = rm.get_control_mut(h2) {
180            p2.set_position(0, splitter_pos as i32 + 1);
181            if (splitter_pos as i32) + 1 < (new_size.height as i32) {
182                p2.set_size(w, new_size.height as u16 - splitter_pos - 1);
183                p2.set_visible(true);
184            } else {
185                p2.set_size(w, 0);
186                p2.set_visible(false);
187            }
188        }
189    }
190    fn mouse_to_state(&self, x: i32, y: i32, clicked: bool) -> State {
191        let sz = self.size();
192        let pos = self.pos.absolute(sz.height.saturating_sub(1) as u16);
193        if y != pos {
194            State::None
195        } else if clicked {
196            match x {
197                1 => State::ClickedOnTopButton,
198                2 => State::ClickedOnBottomButton,
199                _ => State::Dragging,
200            }
201        } else {
202            match x {
203                1 => State::OverTopButton,
204                2 => State::OverBottomButton,
205                _ => State::OverSeparator,
206            }
207        }
208    }
209}
210impl OnPaint for HSplitter {
211    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
212        let (col_line, col_b1, col_b2) = if !self.is_enabled() {
213            (theme.lines.inactive, theme.symbol.inactive, theme.symbol.inactive)
214        } else {
215            match self.state {
216                State::OverSeparator => (theme.lines.hovered, theme.symbol.arrows, theme.symbol.arrows),
217                State::OverTopButton => (theme.lines.normal, theme.symbol.hovered, theme.symbol.arrows),
218                State::OverBottomButton => (theme.lines.normal, theme.symbol.arrows, theme.symbol.hovered),
219                State::ClickedOnTopButton => (theme.lines.normal, theme.symbol.pressed, theme.symbol.arrows),
220                State::ClickedOnBottomButton => (theme.lines.normal, theme.symbol.arrows, theme.symbol.pressed),
221                State::Dragging => (theme.lines.pressed_or_selected, theme.symbol.arrows, theme.symbol.arrows),
222                State::None => (theme.lines.normal, theme.symbol.arrows, theme.symbol.arrows),
223            }
224        };
225        let sz = self.size();
226        let y = self.pos.absolute(sz.height.saturating_sub(1) as u16);
227        surface.draw_horizontal_line_with_size(0, y, sz.width, LineType::Single, col_line);
228        surface.write_char(1, y, Character::with_attributes(SpecialChar::TriangleUp, col_b1));
229        surface.write_char(2, y, Character::with_attributes(SpecialChar::TriangleDown, col_b2));
230    }
231}
232impl OnKeyPressed for HSplitter {
233    fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
234        match key.value() {
235            key!("Ctrl+Alt+UP") => {
236                let sz = self.size();
237                if sz.height > 0 {
238                    self.update_position(Coordinate::Absolute(self.pos.absolute(sz.height.saturating_sub(1) as u16) - 1), true);
239                }
240                EventProcessStatus::Processed
241            }
242            key!("Ctrl+Alt+Down") => {
243                let sz = self.size();
244                if sz.height > 0 {
245                    self.update_position(Coordinate::Absolute(self.pos.absolute(sz.height.saturating_sub(1) as u16) + 1), true);
246                }
247                EventProcessStatus::Processed
248            }
249            key!("Ctrl+Alt+Shift+Up") => {
250                self.update_position(Coordinate::Absolute(0), true);
251                EventProcessStatus::Processed
252            }
253            key!("Ctrl+Alt+Shift+Down") => {
254                self.update_position(Coordinate::Absolute(self.size().height.saturating_sub(1) as i32), true);
255                EventProcessStatus::Processed
256            }
257            _ => EventProcessStatus::Ignored,
258        }
259    }
260}
261impl OnMouseEvent for HSplitter {
262    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
263        match event {
264            MouseEvent::Enter | MouseEvent::Leave => {
265                self.state = State::None;
266                EventProcessStatus::Processed
267            }
268            MouseEvent::Over(point) => {
269                let new_state = self.mouse_to_state(point.x, point.y, false);
270                if new_state != self.state {
271                    self.state = new_state;
272                    EventProcessStatus::Processed
273                } else {
274                    EventProcessStatus::Ignored
275                }
276            }
277            MouseEvent::Pressed(evn) => {
278                let new_state = self.mouse_to_state(evn.x, evn.y, true);
279                if new_state != self.state {
280                    self.state = new_state;
281                    EventProcessStatus::Processed
282                } else {
283                    EventProcessStatus::Ignored
284                }
285            }
286            MouseEvent::Released(evn) => {
287                let processed = match self.state {
288                    State::ClickedOnTopButton => {
289                        self.update_position(Coordinate::Absolute(0), true);
290                        true
291                    }
292                    State::ClickedOnBottomButton => {
293                        self.update_position(Coordinate::Absolute(self.size().height.saturating_sub(1) as i32), true);
294                        true
295                    }
296                    _ => false,
297                };
298                let new_state = self.mouse_to_state(evn.x, evn.y, false);
299                if (new_state != self.state) || processed {
300                    self.state = new_state;
301                    EventProcessStatus::Processed
302                } else {
303                    EventProcessStatus::Ignored
304                }
305            }
306            MouseEvent::Drag(evn) => {
307                if self.state == State::Dragging {
308                    self.update_position(Coordinate::Absolute(evn.y), true);
309                    EventProcessStatus::Processed
310                } else {
311                    EventProcessStatus::Ignored
312                }
313            }
314            MouseEvent::DoubleClick(_) => EventProcessStatus::Ignored,
315            MouseEvent::Wheel(_) => EventProcessStatus::Ignored,
316        }
317    }
318}
319impl OnResize for HSplitter {
320    fn on_resize(&mut self, old_size: Size, new_size: Size) {
321        let previous_width = old_size.height as i32;
322        // recompute the position of the splitter
323        match self.resize_behavior {
324            ResizeBehavior::PreserveAspectRatio => {
325                if (previous_width > 0) && (self.pos.is_absolute()) {
326                    let ratio = self.pos.absolute(old_size.height.saturating_sub(1) as u16) as f32 / previous_width as f32;
327                    let new_pos = (new_size.height as f32 * ratio) as i32;
328                    self.update_position(Coordinate::Absolute(new_pos), false);
329                } else {
330                    // first time (initialization) or already a percentage
331                    self.update_panel_sizes(new_size);
332                }
333            }
334            ResizeBehavior::PreserveTopPanelSize => {
335                if previous_width == 0 {
336                    // first resize (initialize the splitter preserved position)
337                    self.preserve_pos = self.pos.absolute(new_size.height.saturating_sub(1) as u16);
338                    self.set_position(self.preserve_pos);
339                } else {
340                    self.update_position(Coordinate::Absolute(self.preserve_pos), false);
341                }
342            }
343            ResizeBehavior::PreserveBottomPanelSize => {
344                if previous_width == 0 {
345                    // first resize (initialize the splitter preserved position)
346                    self.preserve_pos =
347                        (new_size.height.saturating_sub(1) as i32 - self.pos.absolute(new_size.height.saturating_sub(1) as u16)).max(0);
348                    let new_pos = (new_size.height.saturating_sub(1) as i32 - self.preserve_pos).max(0);
349                    self.set_position(new_pos);
350                } else {
351                    let new_pos = (new_size.height.saturating_sub(1) as i32 - self.preserve_pos).max(0);
352                    self.update_position(Coordinate::Absolute(new_pos), false);
353                }
354            }
355        }
356    }
357}