Skip to main content

kimun_notes/components/
panel.rs

1//! The `Panel` seam — the persistent editor-screen surfaces (activity rail,
2//! drawer, editor) behind one interface, the persistent-surface counterpart to
3//! the `Overlay` trait. See CONTEXT.md ("TUI surfaces").
4
5use ratatui::widgets::{Block, Borders};
6
7use crate::settings::themes::Theme;
8
9/// Identifies a persistent panel. Closed set, mirrors `OverlayKind`.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum PanelKind {
12    /// The fixed-width activity rail on the far left.
13    Rail,
14    /// The single drawer panel; renders whichever rail view is active.
15    Drawer,
16    /// The editor; always visible, fills the remaining width.
17    Editor,
18}
19
20impl PanelKind {
21    /// Footer label shown when this panel is focused. The drawer's label
22    /// depends on its active view — `PanelSet::focused_label()` resolves it.
23    pub fn label(&self) -> &'static str {
24        match self {
25            PanelKind::Rail => "RAIL",
26            PanelKind::Drawer => "DRAWER",
27            PanelKind::Editor => "EDITOR",
28        }
29    }
30}
31
32/// Shared panel chrome: a single-line box with the title embedded in the top
33/// border (`┌─ Title ────┐`) and the border colored by focus state — the one
34/// way every panel draws its frame.
35pub fn panel_block(title: &str, theme: &Theme, focused: bool) -> Block<'static> {
36    let block = Block::default()
37        .borders(Borders::ALL)
38        .border_style(theme.border_style(focused))
39        .style(theme.base_style());
40    if title.is_empty() {
41        // No title: keep the top border unbroken (a titled block would punch
42        // a `─  ` gap into it).
43        block
44    } else {
45        block.title(format!("─ {title} "))
46    }
47}
48
49/// The popup background: regular panel bg, or the harder shade spec §6 gives
50/// the telescope-style modals.
51#[derive(Clone, Copy, PartialEq, Eq, Default)]
52pub enum ModalBg {
53    #[default]
54    Panel,
55    Hard,
56    /// The screen's base background (`theme.base_style()`) — panels that
57    /// read as part of the canvas rather than a raised popup.
58    Base,
59}
60
61/// What a popup shell looks like. Pair with [`modal_chrome`].
62#[derive(Default)]
63pub struct ModalSpec<'a> {
64    /// Top-border title, rendered as-is (callers keep their ` Title ` padding).
65    pub title: Option<&'a str>,
66    /// Border style; `None` = the focused-border style (`theme.border_style(true)`).
67    pub border: Option<ratatui::style::Style>,
68    pub bg: ModalBg,
69}
70
71/// The one way every popup draws its shell (spec §6): clear the area behind
72/// it, draw the titled/bordered block, return the inner rect to fill.
73/// Centering stays with the caller — popups center by percent, by fixed size,
74/// or dock (which-key), but the shell is identical.
75pub fn modal_chrome(
76    f: &mut ratatui::Frame,
77    area: ratatui::layout::Rect,
78    theme: &Theme,
79    spec: ModalSpec,
80) -> ratatui::layout::Rect {
81    f.render_widget(ratatui::widgets::Clear, area);
82    let style = match spec.bg {
83        ModalBg::Panel => theme.panel_style(),
84        ModalBg::Hard => ratatui::style::Style::default()
85            .fg(theme.fg.to_ratatui())
86            .bg(theme.bg_hard.to_ratatui()),
87        ModalBg::Base => theme.base_style(),
88    };
89    let mut block = Block::default()
90        .borders(Borders::ALL)
91        .border_style(spec.border.unwrap_or_else(|| theme.border_style(true)))
92        .style(style);
93    if let Some(title) = spec.title {
94        block = block.title(title.to_string());
95    }
96    let inner = block.inner(area);
97    f.render_widget(block, area);
98    inner
99}