From 2efbe7c5cfec7b5e77c626b2e6bf761b54e1b010 Mon Sep 17 00:00:00 2001 From: Kitsu Date: Sun, 28 Sep 2025 13:09:49 -0300 Subject: [PATCH] Implement adaptive height --- CHANGELOG.md | 2 + src/config.rs | 5 ++ src/draw.rs | 48 +++++++++++++----- src/draw/input_text.rs | 32 ++++++++++-- src/draw/list_view.rs | 97 +++++++++++++++++++++++++++++++------ src/state.rs | 7 ++- src/state/filtered_lines.rs | 2 +- src/window.rs | 6 ++- 8 files changed, 165 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e747858..3354055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,13 @@ - Support some actions with pointer (e.g mouse). - Add flag to hide input bar. +- Adaptive height config option. ## Changes - Log to stderr instead of stdout. - Prefer earlier match with same score for input search. +- Empty subitems now hidden. ## Fixes diff --git a/src/config.rs b/src/config.rs index 4cdb46e..45c90d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,7 @@ pub struct Config { #[def = "false"] force_window: bool, window_offsets: Option<(i32, i32)>, + adaptive_height: bool, scale: Option, term: Option, font: Option, @@ -63,6 +64,10 @@ impl Config { pub fn override_password(&mut self) { self.input_text.password = true; } + + pub fn is_height_adaptive(&self) -> bool { + self.adaptive_height + } } #[derive(Defaults, Deserialize)] diff --git a/src/draw.rs b/src/draw.rs index b29cad6..057c2b2 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -54,6 +54,7 @@ impl<'a> Drawables<'a> { self.state.processed_entries(), self.state.skip_offset(), self.state.selected_item(), + self.state.has_subitems(), self.tx.take().unwrap(), &self.list_config, ), @@ -70,24 +71,43 @@ impl<'a> Drawables<'a> { pub fn make_drawables<'c: 'it, 's: 'it, 'it>( config: &'c crate::config::Config, state: &'s mut crate::state::State, -) -> Drawables<'it> { + scale: u16, +) -> (Drawables<'it>, Option) { let background_config = config.param(); - let input_config = config.param(); - let list_config = config.param(); + let input_config: InputTextParams<'_> = config.param(); + let list_config: ListParams = config.param(); state.process_entries(); + let space = if config.is_height_adaptive() { + let input_space = input_config.occupied_space(scale); + let list_space = list_config.space_for_entries( + state.processed_entries().len().max(1), + scale, + state.has_subitems(), + ); + Some(Space { + width: 0., + height: input_space.height + list_space.height, + }) + } else { + None + }; + let (tx, rx) = oneshot::channel(); - Drawables { - counter: 0, - tx: Some(tx), - rx: Some(rx), - state, - - background_config, - input_config, - list_config, - } + ( + Drawables { + counter: 0, + tx: Some(tx), + rx: Some(rx), + state, + + background_config, + input_config, + list_config, + }, + space, + ) } impl<'a, It> Widget<'a, It> { @@ -99,6 +119,7 @@ impl<'a, It> Widget<'a, It> { items: It, skip_offset: usize, selected_item: usize, + has_subitems: bool, tx: Sender, params: &'a ListParams, ) -> Self { @@ -106,6 +127,7 @@ impl<'a, It> Widget<'a, It> { items, skip_offset, selected_item, + has_subitems, tx, params, )) diff --git a/src/draw/input_text.rs b/src/draw/input_text.rs index 527433f..65c6c8f 100644 --- a/src/draw/input_text.rs +++ b/src/draw/input_text.rs @@ -5,6 +5,9 @@ use crate::font::{Font, FontBackend, FontColor}; use crate::style::{Margin, Padding, Radius}; use crate::Color; +const MIN_PADDING_TOP: f32 = 2.0; +const MIN_PADDING_BOTTOM: f32 = 5.0; + pub struct Params<'a> { pub hide: bool, pub font: Font, @@ -25,6 +28,29 @@ pub struct InputText<'a> { rect: RoundedRect, } +impl Params<'_> { + pub fn occupied_space(&self, scale: u16) -> Space { + if self.hide { + return Space { + width: 0., + height: 0., + }; + } + + let mut padding = &self.padding * f32::from(scale); + padding.top += MIN_PADDING_TOP; + padding.bottom += MIN_PADDING_BOTTOM; + let margin = &self.margin * f32::from(scale); + + let font_size = f32::from(self.font_size * scale); + let rect_height = padding.top + font_size + padding.bottom; + Space { + width: 0., + height: margin.top + rect_height + margin.bottom, + } + } +} + impl<'a> InputText<'a> { pub fn new(text: &'a str, params: &'a Params<'a>) -> Self { let color = params.bg_color; @@ -50,10 +76,8 @@ impl<'a> Drawable for InputText<'a> { let font_size = f32::from(self.params.font_size * scale); let mut padding = &self.params.padding * f32::from(scale); - const PADDING_TOP: f32 = 2.0; - const PADDING_BOTTOM: f32 = 5.0; - padding.top += PADDING_TOP; - padding.bottom += PADDING_BOTTOM; + padding.top += MIN_PADDING_TOP; + padding.bottom += MIN_PADDING_BOTTOM; let margin = &self.params.margin * f32::from(scale); let rect_width = space.width - margin.left - margin.right; diff --git a/src/draw/list_view.rs b/src/draw/list_view.rs index 855f693..39415e4 100644 --- a/src/draw/list_view.rs +++ b/src/draw/list_view.rs @@ -36,16 +36,49 @@ pub struct ListView<'a, It> { items: It, skip_offset: usize, selected_item: usize, + has_subitems: bool, new_skip: Sender, params: &'a Params, _tparam: PhantomData<&'a ()>, } +impl Params { + fn common_bounds(&self, scale: u16) -> (Margin, f32, f32) { + let margin = &self.margin * f32::from(scale); + let item_spacing = self.item_spacing * f32::from(scale); + let icon_size = self.icon_size.unwrap_or(0) * scale; + let font_size = f32::from(self.font_size * scale); + let entry_height = font_size.max(f32::from(icon_size)); + (margin, item_spacing, entry_height) + } + + pub fn entries_fitted(&self, scale: u16, has_subname: bool, space: Space) -> usize { + let (margin, item_spacing, entry_height) = self.common_bounds(scale); + + (((space.height - margin.top - margin.bottom + item_spacing) + / (entry_height + item_spacing)) as usize) + .saturating_sub(has_subname as usize) + } + + pub fn space_for_entries(&self, count: usize, scale: u16, has_subname: bool) -> Space { + let (margin, item_spacing, entry_height) = self.common_bounds(scale); + + let has_subname = (has_subname && !self.hide_actions) as usize; + let count = has_subname + count; + let height = count as f32 * entry_height + + (count - 1 + has_subname) as f32 * item_spacing + + margin.top + + margin.bottom; + Space { height, width: 0.0 } + } +} + impl<'a, It> ListView<'a, It> { pub fn new( items: It, skip_offset: usize, selected_item: usize, + has_subitems: bool, new_skip: Sender, params: &'a Params, ) -> Self { @@ -53,6 +86,7 @@ impl<'a, It> ListView<'a, It> { items, skip_offset, selected_item, + has_subitems, new_skip, params, _tparam: PhantomData, @@ -65,28 +99,18 @@ where It: Iterator>, { fn draw(self, dt: &mut DrawTarget<'_>, scale: u16, space: Space, point: Point) -> Space { - let margin = &self.params.margin * f32::from(scale); - let item_spacing = self.params.item_spacing * f32::from(scale); + let (margin, item_spacing, entry_height) = self.params.common_bounds(scale); let icon_size = self.params.icon_size.unwrap_or(0) * scale; let icon_spacing = self.params.icon_spacing * f32::from(scale); let icon_size_f32 = f32::from(icon_size); let font_size = f32::from(self.params.font_size * scale); let top_offset = point.y + margin.top + (icon_size_f32 - font_size).max(0.) / 2.; - let entry_height = font_size.max(icon_size_f32); - - let mut iter = self.items.peekable(); let hide_actions = self.params.hide_actions; - // For now either all items has subname or none. - let has_subname = iter - .peek() - .map(|e| e.subname.is_some() && !hide_actions) - .unwrap_or(false); + let has_subname = self.has_subitems && !hide_actions; - let displayed_items = ((space.height - margin.top - margin.bottom + item_spacing) - / (entry_height + item_spacing)) as usize - - has_subname as usize; + let displayed_items = self.params.entries_fitted(scale, has_subname, space); let max_offset = self.skip_offset + displayed_items; let (selected_item, skip_offset) = if self.selected_item < self.skip_offset { @@ -102,7 +126,12 @@ where self.new_skip.send(skip_offset).unwrap(); - for (i, item) in iter.skip(skip_offset).enumerate().take(displayed_items) { + for (i, item) in self + .items + .skip(skip_offset) + .enumerate() + .take(displayed_items) + { let relative_offset = (i as f32 + (i > selected_item && has_subname) as i32 as f32) * (entry_height + item_spacing); let x_offset = point.x + margin.left; @@ -201,3 +230,43 @@ where space } } + +#[cfg(test)] +mod tests { + use super::*; + + use test_case::test_matrix; + + #[test_matrix( + [0, 1, 2, 10, 100], + [1, 2, 3], + [false, true] + )] + fn test_param_entries_fit(count: usize, scale: u16, has_subname: bool) { + let param = Params { + font: crate::font::InnerFont::default().into(), + font_size: 24, + font_color: Color::from_rgba(15, 15, 15, 255), + selected_font_color: Color::from_rgba(15, 15, 15, 255), + match_color: None, + icon_size: Some(16), + fallback_icon: None, + margin: Margin { + top: 10., + bottom: 5., + left: 2., + right: 3., + }, + hide_actions: false, + action_left_margin: 0., + item_spacing: 1.5, + icon_spacing: 0.5, + }; + + let space = param.space_for_entries(count, scale, true); + let x = param.entries_fitted(scale, true, space); + + dbg!((count, scale, has_subname, space.height, x)); + assert_eq!(x, count); + } +} diff --git a/src/state.rs b/src/state.rs index f0567f1..94e3bdc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -146,7 +146,12 @@ impl State { self.selected_item } - pub fn processed_entries(&self) -> impl Iterator> { + pub fn has_subitems(&self) -> bool { + // For now either all items has subname or none. + self.inner.subentries_len(self.selected_item) > 0 + } + + pub fn processed_entries(&self) -> impl ExactSizeIterator> { self.filtered_lines .list_items(&self.inner, self.selected_item, self.selected_subitem) } diff --git a/src/state/filtered_lines.rs b/src/state/filtered_lines.rs index 9c38ace..e815c79 100644 --- a/src/state/filtered_lines.rs +++ b/src/state/filtered_lines.rs @@ -55,7 +55,7 @@ impl FilteredLines { mode: &'m Mode, item: usize, subitem: usize, - ) -> impl Iterator> + '_ { + ) -> impl ExactSizeIterator> + '_ { match self { Self(Either::Left(x)) => { Either::Left(x.iter().enumerate().map(move |(idx, (item_idx, s_match))| { diff --git a/src/window.rs b/src/window.rs index cab52a4..72919b1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -252,7 +252,11 @@ impl Window { }; let mut point = Point::new(0., 0.); - let mut drawables = crate::draw::make_drawables(&self.config, &mut self.state); + let (mut drawables, dyn_space) = + crate::draw::make_drawables(&self.config, &mut self.state, self.scale); + if let Some(dyn_space) = dyn_space { + space_left.height = space_left.height.min(dyn_space.height); + } while let Some(d) = drawables.borrowed_next() { let occupied = d.draw(&mut dt, self.scale, space_left, point); debug_assert!(