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_galley = theme.layout_tracked_galley(
129            ui,
130            col.title,
131            FontId::proportional(font_sz),
132            theme.label(),
133            f32::INFINITY,
134        );
135        let title_pos = Pos2::new(
136            col_rect.left() + pad_h,
137            col_rect.center().y - title_galley.size().y * 0.5,
138        );
139        painter
140            .with_clip_rect(title_clip_rect)
141            .galley(title_pos, title_galley, theme.label());
142
143        if let Some(dir) = col.sort_direction {
144            let symbol = match dir {
145                MacSortDirection::Ascending => MacSymbol::ChevronUp,
146                MacSortDirection::Descending => MacSymbol::ChevronDown,
147            };
148            let chev_x = (col_rect.right() - pad_h - chevron_gap).max(col_rect.left() + pad_h);
149            painter.text(
150                Pos2::new(chev_x, col_rect.center().y),
151                Align2::RIGHT_CENTER,
152                symbol.as_str(),
153                FontId::proportional(chevron_sz),
154                theme.secondary_label(),
155            );
156        }
157
158        cursor_x += col.width;
159        if col_idx + 1 < columns.len() && cursor_x < header_rect.right() {
160            painter.line_segment(
161                [
162                    Pos2::new(cursor_x, header_rect.top() + div_inset),
163                    Pos2::new(cursor_x, header_rect.bottom() - div_inset),
164                ],
165                Stroke::new(sep_w, theme.separator()),
166            );
167        }
168    }
169
170    painter.line_segment(
171        [
172            Pos2::new(header_rect.left(), header_rect.bottom()),
173            Pos2::new(header_rect.right(), header_rect.bottom()),
174        ],
175        Stroke::new(sep_w, theme.separator()),
176    );
177
178    (clicked_header, toggled_sort)
179}