Skip to main content

egui_macview/controls/table_view/
header.rs

1//! `NSTableHeaderView` column definitions and header bar renderer (SPEC-011).
2
3use crate::symbols::MacSymbol;
4use crate::theme::MacTheme;
5use egui::{Align, Align2, FontId, Id, Pos2, Rect, Sense, Stroke, Ui};
6
7/// Sort order indicator for a [`MacTableColumn`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum MacSortDirection {
10    /// Ascending sort order (`ChevronUp`).
11    Ascending,
12    /// Descending sort order (`ChevronDown`).
13    Descending,
14}
15
16impl MacSortDirection {
17    /// Returns the opposite sort direction (`Ascending` <-> `Descending`).
18    pub fn toggled(self) -> Self {
19        match self {
20            Self::Ascending => Self::Descending,
21            Self::Descending => Self::Ascending,
22        }
23    }
24}
25
26/// Column definition for [`super::MacTableView`].
27#[derive(Debug, Clone)]
28pub struct MacTableColumn<'a> {
29    /// Column header title.
30    pub title: &'a str,
31    /// Column width in points.
32    pub width: f32,
33    /// Horizontal cell alignment.
34    pub alignment: Align,
35    /// Optional active sort direction.
36    pub sort_direction: Option<MacSortDirection>,
37}
38
39impl<'a> MacTableColumn<'a> {
40    /// Creates a new column with the given `title` and `width` in points.
41    pub fn new(title: &'a str, width: f32) -> Self {
42        Self {
43            title,
44            width,
45            alignment: Align::Min,
46            sort_direction: None,
47        }
48    }
49
50    /// Sets horizontal alignment for cells in this column.
51    pub fn align(mut self, alignment: Align) -> Self {
52        self.alignment = alignment;
53        self
54    }
55
56    /// Sets the active sort indicator direction for this column.
57    pub fn sort(mut self, direction: Option<MacSortDirection>) -> Self {
58        self.sort_direction = direction;
59        self
60    }
61}
62
63/// Renders the `NSTableHeaderView` bar across `header_rect` and returns `(clicked_col, toggled_sort)`.
64pub(crate) fn render_table_header(
65    ui: &mut Ui,
66    table_id: Id,
67    theme: &MacTheme,
68    header_rect: Rect,
69    columns: &[MacTableColumn<'_>],
70    enabled: bool,
71) -> (Option<usize>, Option<(usize, MacSortDirection)>) {
72    let tm = theme.table_view();
73    let pad_h = tm.cell_padding_horizontal.points();
74    let font_sz = tm.header_font_size.points();
75    let chevron_sz = tm.sort_chevron_size.points();
76    let chevron_gap = tm.sort_chevron_spacing.points();
77    let sep_w = tm.separator_width.points();
78    let div_inset = tm.header_divider_inset.points();
79
80    let painter = ui.painter_at(header_rect);
81    painter.rect_filled(header_rect, 0, theme.control());
82
83    let mut cursor_x = header_rect.left();
84    let mut clicked_header = None;
85    let mut toggled_sort = None;
86
87    for (col_idx, col) in columns.iter().enumerate() {
88        let col_rect = Rect::from_min_max(
89            Pos2::new(cursor_x, header_rect.top()),
90            Pos2::new(
91                (cursor_x + col.width).min(header_rect.right()),
92                header_rect.bottom(),
93            ),
94        );
95        let hdr_id = table_id.with(("hdr_col", col_idx));
96        let sense = if enabled {
97            Sense::click()
98        } else {
99            Sense::hover()
100        };
101        let resp = ui.interact(col_rect, hdr_id, sense);
102
103        if resp.hovered() && enabled {
104            painter.rect_filled(col_rect, 0, theme.control_background());
105        }
106
107        if resp.clicked() && enabled {
108            clicked_header = Some(col_idx);
109            let next_dir = col
110                .sort_direction
111                .map(MacSortDirection::toggled)
112                .unwrap_or(MacSortDirection::Ascending);
113            toggled_sort = Some((col_idx, next_dir));
114        }
115
116        let right_reserve = if col.sort_direction.is_some() {
117            chevron_sz + pad_h
118        } else {
119            pad_h
120        };
121        let title_clip_rect = Rect::from_min_max(
122            col_rect.min,
123            Pos2::new(
124                (col_rect.right() - right_reserve).max(col_rect.left()),
125                col_rect.max.y,
126            ),
127        );
128        let title_pos = Pos2::new(col_rect.left() + pad_h, col_rect.center().y);
129        painter.with_clip_rect(title_clip_rect).text(
130            title_pos,
131            Align2::LEFT_CENTER,
132            col.title,
133            FontId::proportional(font_sz),
134            theme.label(),
135        );
136
137        if let Some(dir) = col.sort_direction {
138            let symbol = match dir {
139                MacSortDirection::Ascending => MacSymbol::ChevronUp,
140                MacSortDirection::Descending => MacSymbol::ChevronDown,
141            };
142            let chev_x = (col_rect.right() - pad_h - chevron_gap).max(col_rect.left() + pad_h);
143            painter.text(
144                Pos2::new(chev_x, col_rect.center().y),
145                Align2::RIGHT_CENTER,
146                symbol.as_str(),
147                FontId::proportional(chevron_sz),
148                theme.secondary_label(),
149            );
150        }
151
152        cursor_x += col.width;
153        if col_idx + 1 < columns.len() && cursor_x < header_rect.right() {
154            painter.line_segment(
155                [
156                    Pos2::new(cursor_x, header_rect.top() + div_inset),
157                    Pos2::new(cursor_x, header_rect.bottom() - div_inset),
158                ],
159                Stroke::new(sep_w, theme.separator()),
160            );
161        }
162    }
163
164    painter.line_segment(
165        [
166            Pos2::new(header_rect.left(), header_rect.bottom()),
167            Pos2::new(header_rect.right(), header_rect.bottom()),
168        ],
169        Stroke::new(sep_w, theme.separator()),
170    );
171
172    (clicked_header, toggled_sort)
173}