Skip to main content

lattice_cells/
headerline.rs

1//! Generic sticky headerline — the one mechanism for a buffer to surface a
2//! status row pinned above line 0.
3//!
4//! ## Roles
5//!
6//! - [`Headerline`] — the trait.  Any type that knows how to produce a row of
7//!   cells (and a version counter) implements it.  Tutor, multibuffer search,
8//!   LSP status, VCS branch, diagnostics summary — all use this same surface.
9//!
10//! - [`SimpleHeaderline`] / [`SimpleHeaderlineHandle`] — the ready-made
11//!   implementation for modes that want **owned dedicated state**.  The handle
12//!   is cheap-clone, updates via a closure, and bumps the version atomically.
13//!   Modes that already carry their state elsewhere (e.g. an LSP session
14//!   struct) implement [`Headerline`] directly.
15//!
16//! - [`HeaderlineProvider`] — wraps any [`Headerline`] impl and registers it
17//!   as a [`VirtualRowProvider`].  Always emits one [`VirtualRowKind::Sticky`]
18//!   row anchored above line 0; returns an empty vec when the impl returns
19//!   `None` (hide the row).
20//!
21//! ## Design anchor
22//!
23//! `docs/dev/architecture/headerline.md`
24
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, RwLock};
27
28use crate::cell::Cell;
29use crate::virtual_rows::{
30    AnchorPosition, ProviderId, VirtualRow, VirtualRowKind, VirtualRowProvider,
31};
32
33// ── Output type ──────────────────────────────────────────────────────────────
34
35/// The row produced by a [`Headerline`] impl when it wants to be visible.
36pub struct HeaderlineRow {
37    /// Cells to paint.  Non-empty (callers return `None` when the row should
38    /// be hidden instead of returning an empty cell slice).
39    pub cells: Arc<[Cell]>,
40    /// Override the renderer's sticky-row background.  `None` → renderer uses
41    /// the theme-defined header background.  `Some(0xRRGGBB)` → hard-coded
42    /// colour (e.g. tutor's retro palette).
43    pub bg: Option<u32>,
44}
45
46// ── Trait ─────────────────────────────────────────────────────────────────────
47
48/// Anything that can supply a sticky headerline row.
49///
50/// The cells worker calls `version()` on every tick.  When the version has
51/// advanced, it calls `render()` to rebuild the displayed row.  `render()`
52/// returns `None` to hide the row entirely (e.g. while idle).
53pub trait Headerline: Send + Sync + 'static {
54    /// Monotonic counter.  Bump whenever the row content changes.  The worker
55    /// skips `render()` when the version is unchanged since the last call.
56    fn version(&self) -> u64;
57
58    /// Build the current row.  Return `None` to hide the header entirely.
59    /// Must not block — cache results; background tasks push updates via the
60    /// owning handle.
61    fn render(&self) -> Option<HeaderlineRow>;
62}
63
64// ── SimpleHeaderline — owned-state convenience ───────────────────────────────
65
66/// Ready-made [`Headerline`] impl for modes with dedicated header state.
67///
68/// Not constructed directly — create via [`SimpleHeaderlineHandle::new`].
69pub struct SimpleHeaderline<S: Send + Sync + 'static> {
70    state: Arc<RwLock<S>>,
71    version: AtomicU64,
72    renderer: Arc<dyn Fn(&S) -> Option<HeaderlineRow> + Send + Sync>,
73}
74
75impl<S: Send + Sync + 'static> std::fmt::Debug for SimpleHeaderline<S> {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("SimpleHeaderline")
78            .field("version", &self.version.load(Ordering::Relaxed))
79            .finish_non_exhaustive()
80    }
81}
82
83impl<S: Send + Sync + 'static> Headerline for SimpleHeaderline<S> {
84    fn version(&self) -> u64 {
85        self.version.load(Ordering::Acquire)
86    }
87
88    fn render(&self) -> Option<HeaderlineRow> {
89        self.state.read().ok().and_then(|s| (self.renderer)(&s))
90    }
91}
92
93// ── SimpleHeaderlineHandle ────────────────────────────────────────────────────
94
95/// Cheap-clone handle to a [`SimpleHeaderline<S>`].
96///
97/// The mode holds the handle; the paired [`HeaderlineProvider`] holds a type-
98/// erased `Arc<dyn Headerline>` pointing to the same allocation.  Updates via
99/// [`update`] are immediately visible to the next `render()` call.
100///
101/// [`update`]: SimpleHeaderlineHandle::update
102pub struct SimpleHeaderlineHandle<S: Send + Sync + 'static>(Arc<SimpleHeaderline<S>>);
103
104// Manual impl so S does not need to be Clone — we only clone the Arc.
105impl<S: Send + Sync + 'static> Clone for SimpleHeaderlineHandle<S> {
106    fn clone(&self) -> Self {
107        Self(Arc::clone(&self.0))
108    }
109}
110
111impl<S: Send + Sync + 'static> std::fmt::Debug for SimpleHeaderlineHandle<S> {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "SimpleHeaderlineHandle(v={})", self.version())
114    }
115}
116
117impl<S: Send + Sync + 'static> SimpleHeaderlineHandle<S> {
118    /// Create a new handle with `initial` state and a `renderer` closure.
119    ///
120    /// The closure receives a shared reference to the state and returns the
121    /// row to display, or `None` to hide the header.
122    pub fn new(
123        initial: S,
124        renderer: impl Fn(&S) -> Option<HeaderlineRow> + Send + Sync + 'static,
125    ) -> Self {
126        Self(Arc::new(SimpleHeaderline {
127            state: Arc::new(RwLock::new(initial)),
128            version: AtomicU64::new(0),
129            renderer: Arc::new(renderer),
130        }))
131    }
132
133    /// Mutate the state and bump the version so the cells worker rebuilds the
134    /// row on the next tick.
135    pub fn update(&self, f: impl FnOnce(&mut S)) {
136        if let Ok(mut s) = self.0.state.write() {
137            f(&mut s);
138        }
139        self.0.version.fetch_add(1, Ordering::Release);
140    }
141
142    /// Current version — useful for diagnostics / `BufferLocal::describe`.
143    pub fn version(&self) -> u64 {
144        self.0.version.load(Ordering::Acquire)
145    }
146
147    /// Construct a [`HeaderlineProvider`] backed by this handle.  Register the
148    /// result with `register_virtual_row_provider`; keep the handle for updates.
149    pub fn provider(&self, provider_id: ProviderId) -> HeaderlineProvider {
150        HeaderlineProvider {
151            provider_id,
152            inner: Arc::clone(&self.0) as Arc<dyn Headerline>,
153        }
154    }
155}
156
157// ── HeaderlineProvider ────────────────────────────────────────────────────────
158
159/// [`VirtualRowProvider`] that emits one sticky row above line 0 from any
160/// [`Headerline`] impl.
161///
162/// Register this the same way as any other provider.  The row is hidden
163/// (empty `collect()` result) when the impl returns `None` from `render()`.
164pub struct HeaderlineProvider {
165    provider_id: ProviderId,
166    inner: Arc<dyn Headerline>,
167}
168
169impl std::fmt::Debug for HeaderlineProvider {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("HeaderlineProvider")
172            .field("provider_id", &self.provider_id)
173            .field("version", &self.inner.version())
174            .finish()
175    }
176}
177
178impl HeaderlineProvider {
179    /// Wrap any [`Headerline`] impl directly (e.g. when the mode implements
180    /// the trait on its own existing state struct).
181    pub fn new(provider_id: ProviderId, inner: Arc<dyn Headerline>) -> Self {
182        Self { provider_id, inner }
183    }
184}
185
186impl VirtualRowProvider for HeaderlineProvider {
187    fn id(&self) -> ProviderId {
188        self.provider_id
189    }
190
191    fn version(&self) -> u64 {
192        self.inner.version()
193    }
194
195    fn collect(&self) -> Vec<VirtualRow> {
196        let Some(row) = self.inner.render() else {
197            return Vec::new();
198        };
199        vec![VirtualRow {
200            media: None,
201            anchor_line: 0,
202            position: AnchorPosition::Above,
203            cells: row.cells,
204            height: 1,
205            kind: VirtualRowKind::Sticky,
206            bg: row.bg,
207            scales: None,
208            gutter_line: None,
209            gutter_fg: None,
210        }]
211    }
212}
213
214// ── Tests ─────────────────────────────────────────────────────────────────────
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn hidden_when_renderer_returns_none() {
222        let handle = SimpleHeaderlineHandle::new(0u32, |_| None);
223        let provider = handle.provider(1);
224        assert!(provider.collect().is_empty());
225    }
226
227    #[test]
228    fn emits_sticky_row_at_line_zero() {
229        let handle = SimpleHeaderlineHandle::<()>::new((), |_| {
230            let cells: Arc<[Cell]> = vec![Cell::new('x' as u32, 0xffffff, 0, 0)].into();
231            Some(HeaderlineRow {
232                cells,
233                bg: Some(0x000000),
234            })
235        });
236        let provider = handle.provider(42);
237        let rows = provider.collect();
238        assert_eq!(rows.len(), 1);
239        assert_eq!(rows[0].anchor_line, 0);
240        assert_eq!(rows[0].kind, VirtualRowKind::Sticky);
241        assert_eq!(rows[0].bg, Some(0x000000));
242    }
243
244    #[test]
245    fn version_advances_on_update() {
246        let handle = SimpleHeaderlineHandle::new(0u32, |_| None);
247        let v0 = handle.version();
248        handle.update(|s| *s = 1);
249        assert!(handle.version() > v0);
250    }
251
252    #[test]
253    fn provider_version_tracks_handle() {
254        let handle = SimpleHeaderlineHandle::new(0u32, |_| None);
255        let provider = handle.provider(99);
256        let v0 = provider.version();
257        handle.update(|s| *s += 1);
258        assert!(provider.version() > v0);
259    }
260
261    #[test]
262    fn direct_headerline_impl_works() {
263        struct Fixed;
264        impl Headerline for Fixed {
265            fn version(&self) -> u64 {
266                1
267            }
268            fn render(&self) -> Option<HeaderlineRow> {
269                let cells: Arc<[Cell]> = vec![Cell::new('!' as u32, 0, 0, 0)].into();
270                Some(HeaderlineRow { cells, bg: None })
271            }
272        }
273        let p = HeaderlineProvider::new(7, Arc::new(Fixed));
274        assert_eq!(p.collect().len(), 1);
275        assert_eq!(p.collect()[0].bg, None);
276    }
277}