feat(tui): resize panes by dragging pane borders
Press and drag the border under Results or the seam at a row pane's right edge; the panes resize live and release ends the drag. Dragged sizes are cell counts on PaneLayout, so W resets panes, zoom and sizes in one key, and the reset-layout hint shows once anything is dragged. Undragged panes keep their default Fill proportions.
This commit is contained in:
@@ -57,6 +57,14 @@ impl FocusedPane {
|
|||||||
pub struct PaneLayout {
|
pub struct PaneLayout {
|
||||||
hidden: [bool; FocusedPane::ALL.len()],
|
hidden: [bool; FocusedPane::ALL.len()],
|
||||||
pub zoomed: bool,
|
pub zoomed: bool,
|
||||||
|
/// Cell height dragged onto the divider between Results and the row
|
||||||
|
/// underneath. `None` keeps the default proportions.
|
||||||
|
pub top_height: Option<u16>,
|
||||||
|
/// Cell widths dragged onto the dividers in the bottom row. `None` keeps
|
||||||
|
/// the default proportions; Info, which has no divider of its own, takes
|
||||||
|
/// whatever the row has left over.
|
||||||
|
pub searches_width: Option<u16>,
|
||||||
|
pub downloads_width: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PaneLayout {
|
impl PaneLayout {
|
||||||
@@ -322,6 +330,15 @@ pub struct AppState {
|
|||||||
pub searches_pane_area: Option<Rect>,
|
pub searches_pane_area: Option<Rect>,
|
||||||
pub results_pane_area: Option<Rect>,
|
pub results_pane_area: Option<Rect>,
|
||||||
pub downloads_pane_area: Option<Rect>,
|
pub downloads_pane_area: Option<Rect>,
|
||||||
|
/// The whole area the panes share, and the bottom row's part of it,
|
||||||
|
/// recorded at each draw so a divider drag maps to cells.
|
||||||
|
pub content_area: Option<Rect>,
|
||||||
|
pub row_area: Option<Rect>,
|
||||||
|
pub info_pane_area: Option<Rect>,
|
||||||
|
/// The border rows a mouse can grab to resize: the one Results sits on
|
||||||
|
/// and the seam at the right of each row pane.
|
||||||
|
pub hsplit_divider: Option<Rect>,
|
||||||
|
pub vsplit_dividers: Vec<(FocusedPane, Rect)>,
|
||||||
/// Where the open popup was last drawn, for its page size. Only one is
|
/// Where the open popup was last drawn, for its page size. Only one is
|
||||||
/// ever open, so one record serves them all.
|
/// ever open, so one record serves them all.
|
||||||
pub popup_area: Option<Rect>,
|
pub popup_area: Option<Rect>,
|
||||||
@@ -397,6 +414,11 @@ impl AppState {
|
|||||||
searches_pane_area: None,
|
searches_pane_area: None,
|
||||||
results_pane_area: None,
|
results_pane_area: None,
|
||||||
downloads_pane_area: None,
|
downloads_pane_area: None,
|
||||||
|
content_area: None,
|
||||||
|
row_area: None,
|
||||||
|
info_pane_area: None,
|
||||||
|
hsplit_divider: None,
|
||||||
|
vsplit_dividers: Vec::new(),
|
||||||
popup_area: None,
|
popup_area: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::MainTui;
|
use super::MainTui;
|
||||||
|
use super::render::{MIN_PANE, shrink};
|
||||||
use crate::models::{CommandBarMode, FocusedPane, LogView, PaneLayout};
|
use crate::models::{CommandBarMode, FocusedPane, LogView, PaneLayout};
|
||||||
use crate::ui::page_of;
|
use crate::ui::page_of;
|
||||||
use crate::ui::panes::{
|
use crate::ui::panes::{
|
||||||
@@ -102,6 +103,9 @@ impl MainTui {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('W') => {
|
KeyCode::Char('W') => {
|
||||||
|
// The universal undo for the window: every pane back, zoom
|
||||||
|
// off, and any mouse-dragged pane sizes back to the default
|
||||||
|
// proportions — they all live in the layout.
|
||||||
self.state.layout = PaneLayout::default();
|
self.state.layout = PaneLayout::default();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -566,24 +570,131 @@ impl MainTui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn handle_mouse_event(&mut self, mouse: MouseEvent) {
|
pub(super) fn handle_mouse_event(&mut self, mouse: MouseEvent) {
|
||||||
if mouse.kind != MouseEventKind::Down(MouseButton::Left) {
|
match mouse.kind {
|
||||||
return;
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
}
|
let at = Position::new(mouse.column, mouse.row);
|
||||||
|
// A divider wins over the panes it separates; grabbing one
|
||||||
let clicked = Position::new(mouse.column, mouse.row);
|
// starts a drag, and the click alone already resizes.
|
||||||
// The areas are from the last draw. A pane hidden since then, in the
|
if let Some(drag) = self.grabbed_divider(at) {
|
||||||
// same batch of events, still has one, and is not there to click.
|
self.resize_drag = Some(drag);
|
||||||
let hit = FocusedPane::ALL.into_iter().find(|pane| {
|
self.apply_resize(drag, at);
|
||||||
self.state.layout.is_visible(*pane)
|
return;
|
||||||
&& self
|
}
|
||||||
.state
|
// The areas are from the last draw. A pane hidden since then, in the
|
||||||
.pane_area(*pane)
|
// same batch of events, still has one, and is not there to click.
|
||||||
.is_some_and(|area| area.contains(clicked))
|
let hit = FocusedPane::ALL.into_iter().find(|pane| {
|
||||||
});
|
self.state.layout.is_visible(*pane)
|
||||||
if let Some(pane) = hit {
|
&& self
|
||||||
self.state.focused_pane = pane;
|
.state
|
||||||
|
.pane_area(*pane)
|
||||||
|
.is_some_and(|area| area.contains(at))
|
||||||
|
});
|
||||||
|
if let Some(pane) = hit {
|
||||||
|
self.state.focused_pane = pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEventKind::Drag(MouseButton::Left) => {
|
||||||
|
if let Some(drag) = self.resize_drag {
|
||||||
|
self.apply_resize(
|
||||||
|
drag,
|
||||||
|
Position::new(mouse.column, mouse.row),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEventKind::Up(MouseButton::Left) => {
|
||||||
|
self.resize_drag = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which dragged divider, if any, the pointer is on. Nothing is grabbable
|
||||||
|
/// while a popup or a zoom covers the dividers.
|
||||||
|
fn grabbed_divider(&self, at: Position) -> Option<ResizeDrag> {
|
||||||
|
if self.state.layout.zoomed
|
||||||
|
|| self.state.show_help
|
||||||
|
|| self.state.show_messages
|
||||||
|
|| self.state.show_browse
|
||||||
|
|| self.state.show_rooms
|
||||||
|
|| self.state.settings.is_some()
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.state
|
||||||
|
.hsplit_divider
|
||||||
|
.is_some_and(|rect| rect.contains(at))
|
||||||
|
{
|
||||||
|
return Some(ResizeDrag::Rows);
|
||||||
|
}
|
||||||
|
self.state
|
||||||
|
.vsplit_dividers
|
||||||
|
.iter()
|
||||||
|
.find(|(_, rect)| rect.contains(at))
|
||||||
|
.map(|(pane, _)| ResizeDrag::Columns(*pane))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the divider under the pointer to where the pointer is, keeping
|
||||||
|
/// every pane at least [`MIN_PANE`] cells.
|
||||||
|
fn apply_resize(&mut self, drag: ResizeDrag, at: Position) {
|
||||||
|
match drag {
|
||||||
|
ResizeDrag::Rows => {
|
||||||
|
let Some(area) = self.state.content_area else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// The dragged row is the pane's last row, so the height is
|
||||||
|
// everything from the top of the content down to it.
|
||||||
|
let height = at.y.saturating_sub(area.y).saturating_add(1);
|
||||||
|
self.state.layout.top_height =
|
||||||
|
Some(shrink(height, area.height));
|
||||||
|
}
|
||||||
|
ResizeDrag::Columns(pane) => {
|
||||||
|
let Some(row) = self.state.row_area else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let left = if pane == FocusedPane::Downloads
|
||||||
|
&& self.state.layout.is_visible(FocusedPane::Searches)
|
||||||
|
{
|
||||||
|
// Downloads starts where Searches ends.
|
||||||
|
self.state
|
||||||
|
.searches_pane_area
|
||||||
|
.map_or(row.x, |area| area.x.saturating_add(area.width))
|
||||||
|
} else {
|
||||||
|
row.x
|
||||||
|
};
|
||||||
|
let width = at.x.saturating_sub(left).saturating_add(1);
|
||||||
|
// Info shares the row, so the drag may not crowd it (and any
|
||||||
|
// other visible pane) out of `MIN_PANE` cells.
|
||||||
|
let others = MIN_PANE
|
||||||
|
* u16::from(
|
||||||
|
self.state.layout.is_visible(FocusedPane::Searches)
|
||||||
|
&& pane == FocusedPane::Downloads,
|
||||||
|
)
|
||||||
|
+ MIN_PANE;
|
||||||
|
let max = row.width.saturating_sub(others).max(MIN_PANE);
|
||||||
|
let width = width.max(MIN_PANE).min(max);
|
||||||
|
match pane {
|
||||||
|
FocusedPane::Searches => {
|
||||||
|
self.state.layout.searches_width = Some(width);
|
||||||
|
}
|
||||||
|
FocusedPane::Downloads => {
|
||||||
|
self.state.layout.downloads_width = Some(width);
|
||||||
|
}
|
||||||
|
FocusedPane::Results => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a divider does while the left button is held on it.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(super) enum ResizeDrag {
|
||||||
|
/// The border between Results and the row underneath: sets that row's height.
|
||||||
|
Rows,
|
||||||
|
/// The seam at the right of the named row pane: gives it that width and
|
||||||
|
/// takes it from the panes to its right.
|
||||||
|
Columns(FocusedPane),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a key did to a filter being typed.
|
/// What a key did to a filter being typed.
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ pub struct MainTui {
|
|||||||
config_path: Option<std::path::PathBuf>,
|
config_path: Option<std::path::PathBuf>,
|
||||||
/// Last snapshot written to disk, to skip no-op saves.
|
/// Last snapshot written to disk, to skip no-op saves.
|
||||||
saved_snapshot: Snapshot,
|
saved_snapshot: Snapshot,
|
||||||
|
/// Which divider the left mouse button is holding, if any.
|
||||||
|
resize_drag: Option<input::ResizeDrag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MainTui {
|
impl MainTui {
|
||||||
@@ -50,6 +52,7 @@ impl MainTui {
|
|||||||
store,
|
store,
|
||||||
config_path,
|
config_path,
|
||||||
saved_snapshot: Snapshot::default(),
|
saved_snapshot: Snapshot::default(),
|
||||||
|
resize_drag: None,
|
||||||
};
|
};
|
||||||
tui.restore_persisted_state();
|
tui.restore_persisted_state();
|
||||||
tui
|
tui
|
||||||
@@ -1360,6 +1363,27 @@ mod tests {
|
|||||||
screen_sized(tui, 160, 40)
|
screen_sized(tui, 160, 40)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mouse(
|
||||||
|
tui: &mut MainTui,
|
||||||
|
kind: ratatui::crossterm::event::MouseEventKind,
|
||||||
|
column: u16,
|
||||||
|
row: u16,
|
||||||
|
) {
|
||||||
|
tui.handle_mouse_event(ratatui::crossterm::event::MouseEvent {
|
||||||
|
kind,
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_drag_row(tui: &mut MainTui, from: (u16, u16), to: (u16, u16)) {
|
||||||
|
use ratatui::crossterm::event::{MouseButton, MouseEventKind};
|
||||||
|
mouse(tui, MouseEventKind::Down(MouseButton::Left), from.0, from.1);
|
||||||
|
mouse(tui, MouseEventKind::Drag(MouseButton::Left), to.0, to.1);
|
||||||
|
mouse(tui, MouseEventKind::Up(MouseButton::Left), to.0, to.1);
|
||||||
|
}
|
||||||
|
|
||||||
/// The screen row carrying a pane's legend, `[2] Results` say, without
|
/// The screen row carrying a pane's legend, `[2] Results` say, without
|
||||||
/// the quotes the test backend wraps each row in.
|
/// the quotes the test backend wraps each row in.
|
||||||
fn row_with<'a>(screen: &'a str, needle: &str) -> &'a str {
|
fn row_with<'a>(screen: &'a str, needle: &str) -> &'a str {
|
||||||
@@ -2044,6 +2068,88 @@ mod tests {
|
|||||||
assert_eq!(tui.state.focused_pane, FocusedPane::Downloads);
|
assert_eq!(tui.state.focused_pane, FocusedPane::Downloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dragging_the_border_under_results_resizes_its_row() {
|
||||||
|
let mut tui = furnished_tui();
|
||||||
|
let _ = screen_of(&mut tui);
|
||||||
|
let results = tui.state.results_pane_area.expect("laid out");
|
||||||
|
let divider = tui.state.hsplit_divider.expect("a divider");
|
||||||
|
let before = results.height;
|
||||||
|
let focused = tui.state.focused_pane;
|
||||||
|
|
||||||
|
// Two drags: the row shrinks by the rows the pointer moved up.
|
||||||
|
mouse_drag_row(
|
||||||
|
&mut tui,
|
||||||
|
(results.x + 1, divider.y),
|
||||||
|
(results.x + 1, divider.y - 2),
|
||||||
|
);
|
||||||
|
assert_eq!(tui.state.layout.top_height, Some(before - 2));
|
||||||
|
assert_eq!(
|
||||||
|
tui.state.focused_pane, focused,
|
||||||
|
"a grab is not a focus click"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = screen_of(&mut tui);
|
||||||
|
assert_eq!(
|
||||||
|
tui.state.results_pane_area.expect("laid out").height,
|
||||||
|
before - 2
|
||||||
|
);
|
||||||
|
|
||||||
|
// A drag far past the row stops at the smallest pane, not zero.
|
||||||
|
mouse_drag_row(
|
||||||
|
&mut tui,
|
||||||
|
(results.x + 1, divider.y - 2),
|
||||||
|
(results.x + 1, 0),
|
||||||
|
);
|
||||||
|
assert!(tui.state.layout.top_height.expect("resized") >= 3);
|
||||||
|
|
||||||
|
press(&mut tui, KeyCode::Char('W'));
|
||||||
|
assert_eq!(tui.state.layout.top_height, None, "W resets the layout");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dragging_a_row_panes_right_edge_gives_it_width() {
|
||||||
|
use ratatui::crossterm::event::{MouseButton, MouseEventKind};
|
||||||
|
|
||||||
|
let mut tui = furnished_tui();
|
||||||
|
let _ = screen_of(&mut tui);
|
||||||
|
let searches = tui.state.searches_pane_area.expect("laid out");
|
||||||
|
let seam = tui
|
||||||
|
.state
|
||||||
|
.vsplit_dividers
|
||||||
|
.iter()
|
||||||
|
.find(|(pane, _)| *pane == FocusedPane::Searches)
|
||||||
|
.map(|(_, rect)| *rect)
|
||||||
|
.expect("a seam beside Searches");
|
||||||
|
tui.state.focused_pane = FocusedPane::Downloads;
|
||||||
|
|
||||||
|
mouse_drag_row(
|
||||||
|
&mut tui,
|
||||||
|
(seam.x, searches.y + 1),
|
||||||
|
(seam.x + 4, searches.y + 1),
|
||||||
|
);
|
||||||
|
assert_eq!(tui.state.layout.searches_width, Some(searches.width + 4));
|
||||||
|
assert_eq!(tui.state.focused_pane, FocusedPane::Downloads);
|
||||||
|
|
||||||
|
// The widths are applied, and Info narrows to whatever is left.
|
||||||
|
let _ = screen_of(&mut tui);
|
||||||
|
assert_eq!(
|
||||||
|
tui.state.searches_pane_area.expect("laid out").width,
|
||||||
|
searches.width + 4
|
||||||
|
);
|
||||||
|
assert!(tui.state.info_pane_area.expect("laid out").width >= 3);
|
||||||
|
|
||||||
|
// Releasing the button ends the drag: later motions just move the
|
||||||
|
// pointer.
|
||||||
|
mouse(
|
||||||
|
&mut tui,
|
||||||
|
MouseEventKind::Drag(MouseButton::Left),
|
||||||
|
seam.x + 40,
|
||||||
|
searches.y + 1,
|
||||||
|
);
|
||||||
|
assert_eq!(tui.state.layout.searches_width, Some(searches.width + 4));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_info_pane_describes_the_highlighted_result() {
|
fn the_info_pane_describes_the_highlighted_result() {
|
||||||
let mut tui = results_tui(3);
|
let mut tui = results_tui(3);
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ const HELP_LEFT: &[(&str, &[(&str, &str)])] = &[
|
|||||||
("z", "zoom the focused pane"),
|
("z", "zoom the focused pane"),
|
||||||
("w", "hide the focused pane"),
|
("w", "hide the focused pane"),
|
||||||
("Esc", "leave zoom"),
|
("Esc", "leave zoom"),
|
||||||
("W", "every pane back, zoom off"),
|
("W", "every pane back, zoom off, sizes reset"),
|
||||||
("click", "focus a pane"),
|
("click", "focus a pane"),
|
||||||
|
("drag", "resize panes by their borders"),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@@ -170,11 +171,16 @@ impl MainTui {
|
|||||||
self.state.searches_pane_area = None;
|
self.state.searches_pane_area = None;
|
||||||
self.state.results_pane_area = None;
|
self.state.results_pane_area = None;
|
||||||
self.state.downloads_pane_area = None;
|
self.state.downloads_pane_area = None;
|
||||||
|
self.state.info_pane_area = None;
|
||||||
|
self.state.hsplit_divider = None;
|
||||||
|
self.state.vsplit_dividers.clear();
|
||||||
|
self.state.row_area = None;
|
||||||
|
|
||||||
if self.state.layout.zoomed {
|
if self.state.layout.zoomed {
|
||||||
self.render_pane(frame, area, self.state.focused_pane);
|
self.render_pane(frame, area, self.state.focused_pane);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.state.content_area = Some(area);
|
||||||
|
|
||||||
// Transfers carry the most columns, so they get the most width.
|
// Transfers carry the most columns, so they get the most width.
|
||||||
let row_panes: Vec<(FocusedPane, u16)> =
|
let row_panes: Vec<(FocusedPane, u16)> =
|
||||||
@@ -187,26 +193,61 @@ impl MainTui {
|
|||||||
// With only Info left in the row, the results deserve more of
|
// With only Info left in the row, the results deserve more of
|
||||||
// the height.
|
// the height.
|
||||||
let row_weight = if row_panes.is_empty() { 1 } else { 2 };
|
let row_weight = if row_panes.is_empty() { 1 } else { 2 };
|
||||||
let [top, bottom] = Layout::vertical([
|
let top = match self.state.layout.top_height {
|
||||||
Constraint::Fill(3),
|
Some(rows) => Constraint::Length(shrink(rows, area.height)),
|
||||||
Constraint::Fill(row_weight),
|
None => Constraint::Fill(3),
|
||||||
])
|
};
|
||||||
.areas(area);
|
let [top, bottom] =
|
||||||
|
Layout::vertical([top, Constraint::Fill(row_weight)])
|
||||||
|
.areas(area);
|
||||||
|
// The two touching border rows between the halves; either can
|
||||||
|
// be dragged.
|
||||||
|
self.state.hsplit_divider = Some(Rect::new(
|
||||||
|
area.x,
|
||||||
|
top.y.saturating_add(top.height).saturating_sub(1),
|
||||||
|
area.width,
|
||||||
|
2,
|
||||||
|
));
|
||||||
self.render_pane(frame, top, FocusedPane::Results);
|
self.render_pane(frame, top, FocusedPane::Results);
|
||||||
bottom
|
bottom
|
||||||
} else {
|
} else {
|
||||||
area
|
area
|
||||||
};
|
};
|
||||||
|
self.state.row_area = Some(row_area);
|
||||||
|
|
||||||
let mut constraints: Vec<Constraint> = row_panes
|
let mut constraints: Vec<Constraint> = row_panes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, weight)| Constraint::Fill(*weight))
|
.map(|(pane, weight)| {
|
||||||
|
let dragged = match *pane {
|
||||||
|
FocusedPane::Searches => self.state.layout.searches_width,
|
||||||
|
FocusedPane::Downloads => self.state.layout.downloads_width,
|
||||||
|
FocusedPane::Results => None,
|
||||||
|
};
|
||||||
|
match dragged {
|
||||||
|
Some(cols) => {
|
||||||
|
Constraint::Length(shrink(cols, row_area.width))
|
||||||
|
}
|
||||||
|
None => Constraint::Fill(*weight),
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
constraints.push(Constraint::Fill(6)); // Info
|
constraints.push(Constraint::Fill(6)); // Info
|
||||||
let chunks = Layout::horizontal(constraints).split(row_area);
|
let chunks = Layout::horizontal(constraints).split(row_area);
|
||||||
for (chunk, (pane, _)) in chunks.iter().zip(&row_panes) {
|
for (chunk, (pane, _)) in chunks.iter().zip(&row_panes) {
|
||||||
|
// The seam at this pane's right edge: its border column and the
|
||||||
|
// next pane's, both draggable.
|
||||||
|
self.state.vsplit_dividers.push((
|
||||||
|
*pane,
|
||||||
|
Rect::new(
|
||||||
|
chunk.x.saturating_add(chunk.width).saturating_sub(1),
|
||||||
|
row_area.y,
|
||||||
|
2,
|
||||||
|
row_area.height,
|
||||||
|
),
|
||||||
|
));
|
||||||
self.render_pane(frame, *chunk, *pane);
|
self.render_pane(frame, *chunk, *pane);
|
||||||
}
|
}
|
||||||
|
self.state.info_pane_area = Some(chunks[row_panes.len()]);
|
||||||
self.render_info_pane(frame, chunks[row_panes.len()]);
|
self.render_info_pane(frame, chunks[row_panes.len()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,6 +809,18 @@ impl MainTui {
|
|||||||
/// Columns between the two halves of the keys list.
|
/// Columns between the two halves of the keys list.
|
||||||
const HELP_GAP: u16 = 2;
|
const HELP_GAP: u16 = 2;
|
||||||
|
|
||||||
|
/// A dragged pane size kept inside the space the terminal now offers: never
|
||||||
|
/// below [`MIN_PANE`], and never so big that what shares the split with it
|
||||||
|
/// drops below `MIN_PANE` too.
|
||||||
|
pub(super) fn shrink(dragged: u16, total: u16) -> u16 {
|
||||||
|
let max = total.saturating_sub(MIN_PANE).max(MIN_PANE);
|
||||||
|
dragged.max(MIN_PANE).min(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The smallest pane a drag can leave anyone: a border and a row or column
|
||||||
|
/// of content each side.
|
||||||
|
pub(super) const MIN_PANE: u16 = 3;
|
||||||
|
|
||||||
/// The widest key in a column, so the descriptions line up.
|
/// The widest key in a column, so the descriptions line up.
|
||||||
fn key_column_width(sections: &[(&str, &[(&str, &str)])]) -> usize {
|
fn key_column_width(sections: &[(&str, &[(&str, &str)])]) -> usize {
|
||||||
sections
|
sections
|
||||||
|
|||||||
Reference in New Issue
Block a user