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}