use ratatui::layout::Rect;
use ratatui::style::Style;
use super::constants::ACTIVE_FOCUS_COLOR;
use super::constants::HOVER_FOCUS_COLOR;
use super::constants::REMEMBERED_FOCUS_COLOR;
use super::interaction::UiHitbox;
#[derive(Default, Clone)]
pub(super) struct ScrollState {
pos: usize,
}
impl ScrollState {
pub const fn pos(&self) -> usize { self.pos }
pub const fn set(&mut self, pos: usize) { self.pos = pos; }
pub const fn up(&mut self) {
if self.pos > 0 {
self.pos -= 1;
}
}
pub const fn down(&mut self, len: usize) {
if len > 0 && self.pos < len - 1 {
self.pos += 1;
}
}
pub const fn jump_home(&mut self) { self.pos = 0; }
pub const fn jump_end(&mut self, len: usize) { self.pos = len.saturating_sub(1); }
pub const fn clamp(&mut self, len: usize) {
if len == 0 {
self.pos = 0;
} else if self.pos >= len {
self.pos = len - 1;
}
}
}
#[derive(Default, Clone)]
pub(super) struct Pane {
cursor: ScrollState,
hovered: Option<usize>,
len: usize,
content_area: Rect,
scroll_offset: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum PaneFocusState {
Active,
Remembered,
Inactive,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum PaneSelectionState {
Active,
Hovered,
Remembered,
Unselected,
}
impl Pane {
pub const fn new() -> Self {
Self {
cursor: ScrollState { pos: 0 },
hovered: None,
len: 0,
content_area: Rect::new(0, 0, 0, 0),
scroll_offset: 0,
}
}
pub const fn up(&mut self) { self.cursor.up(); }
pub const fn down(&mut self) { self.cursor.down(self.len); }
pub const fn home(&mut self) { self.cursor.jump_home(); }
pub const fn end(&mut self) { self.cursor.jump_end(self.len); }
pub const fn pos(&self) -> usize { self.cursor.pos() }
pub const fn set_pos(&mut self, pos: usize) { self.cursor.set(pos); }
pub const fn set_len(&mut self, len: usize) {
self.len = len;
self.cursor.clamp(len);
if let Some(row) = self.hovered
&& row >= len
{
self.hovered = None;
}
}
pub const fn clear_surface(&mut self) {
self.len = 0;
self.hovered = None;
self.content_area = Rect::ZERO;
self.scroll_offset = 0;
self.cursor.clamp(0);
}
pub const fn set_content_area(&mut self, area: Rect) { self.content_area = area; }
pub const fn set_scroll_offset(&mut self, offset: usize) { self.scroll_offset = offset; }
pub const fn set_hovered(&mut self, hovered: Option<usize>) { self.hovered = hovered; }
pub const fn content_area(&self) -> Rect { self.content_area }
pub const fn scroll_offset(&self) -> usize { self.scroll_offset }
pub const fn len(&self) -> usize { self.len }
pub const fn selection_state(&self, row: usize, focus: PaneFocusState) -> PaneSelectionState {
if row == self.pos() && matches!(focus, PaneFocusState::Active) {
PaneSelectionState::Active
} else if matches!(self.hovered, Some(hovered_row) if hovered_row == row) {
PaneSelectionState::Hovered
} else if row == self.pos() && matches!(focus, PaneFocusState::Remembered) {
PaneSelectionState::Remembered
} else {
PaneSelectionState::Unselected
}
}
pub fn selection_style(focus: PaneFocusState) -> Style {
match focus {
PaneFocusState::Active => Style::default().bg(ACTIVE_FOCUS_COLOR),
PaneFocusState::Remembered => Style::default().bg(REMEMBERED_FOCUS_COLOR),
PaneFocusState::Inactive => Style::default(),
}
}
}
impl PaneSelectionState {
pub fn overlay_style(self) -> Style {
match self {
Self::Active => Pane::selection_style(PaneFocusState::Active),
Self::Hovered => Style::default().bg(HOVER_FOCUS_COLOR),
Self::Remembered => Pane::selection_style(PaneFocusState::Remembered),
Self::Unselected => Style::default(),
}
}
pub fn patch(self, style: Style) -> Style { style.patch(self.overlay_style()) }
}
pub(super) fn scroll_indicator(pos: usize, len: usize) -> String { format!("{} of {len}", pos + 1) }
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash)]
pub(super) enum PaneId {
#[default]
ProjectList,
Package,
Lang,
Git,
Targets,
Lints,
CiRuns,
Output,
Toasts,
Settings,
Finder,
Keymap,
}
impl PaneId {
pub const fn index(self) -> usize {
match self {
Self::ProjectList => 0,
Self::Package => 1,
Self::Lang => 2,
Self::Git => 3,
Self::Targets => 4,
Self::Lints => 5,
Self::CiRuns => 6,
Self::Output => 7,
Self::Toasts => 8,
Self::Settings => 9,
Self::Finder => 10,
Self::Keymap => 11,
}
}
pub const fn pane_count() -> usize { Self::Keymap.index() + 1 }
pub const fn is_overlay(self) -> bool { matches!(self, Self::Settings | Self::Finder) }
}
#[derive(Default)]
pub(super) struct LayoutCache {
pub project_list: Rect,
pub pane_regions: Vec<(PaneId, Rect)>,
pub ui_hitboxes: Vec<UiHitbox>,
}
#[cfg(test)]
mod tests {
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use super::PaneFocusState;
use super::PaneSelectionState;
#[test]
fn active_selection_style_only_adds_background_and_emphasis() {
let style = super::Pane::selection_style(PaneFocusState::Active);
assert_eq!(style.fg, None);
assert_eq!(style.bg, Some(super::ACTIVE_FOCUS_COLOR));
assert_eq!(style.add_modifier, Modifier::default());
}
#[test]
fn selection_patch_preserves_existing_foreground() {
let base = Style::default().fg(Color::Red);
let patched = PaneSelectionState::Active.patch(base);
assert_eq!(patched.fg, Some(Color::Red));
assert_eq!(patched.bg, Some(super::ACTIVE_FOCUS_COLOR));
assert_eq!(patched.add_modifier, Modifier::default());
}
#[test]
fn remembered_selection_patch_preserves_existing_foreground() {
let base = Style::default().fg(Color::Green);
let patched = PaneSelectionState::Remembered.patch(base);
assert_eq!(patched.fg, Some(Color::Green));
assert_eq!(patched.bg, Some(super::REMEMBERED_FOCUS_COLOR));
}
#[test]
fn hovered_selection_patch_preserves_existing_foreground() {
let base = Style::default().fg(Color::Blue);
let patched = PaneSelectionState::Hovered.patch(base);
assert_eq!(patched.fg, Some(Color::Blue));
assert_eq!(patched.bg, Some(super::HOVER_FOCUS_COLOR));
}
#[test]
fn selection_state_returns_hovered_for_non_selected_hovered_row() {
let mut pane = super::Pane::new();
pane.set_len(3);
pane.set_hovered(Some(2));
assert_eq!(
pane.selection_state(2, PaneFocusState::Inactive),
PaneSelectionState::Hovered
);
}
#[test]
fn selection_state_prefers_cursor_over_hovered_row() {
let mut pane = super::Pane::new();
pane.set_len(3);
pane.set_pos(1);
pane.set_hovered(Some(1));
assert_eq!(
pane.selection_state(1, PaneFocusState::Active),
PaneSelectionState::Active
);
}
#[test]
fn selection_state_prefers_hover_for_inactive_selected_row() {
let mut pane = super::Pane::new();
pane.set_len(3);
pane.set_pos(0);
pane.set_hovered(Some(0));
assert_eq!(
pane.selection_state(0, PaneFocusState::Inactive),
PaneSelectionState::Hovered
);
}
}