commit 15eea931def41515c8ef5bf350094b5365237351
parent 0a2935a31064bac26cf9e6da87cea4f4bae41919
Author: kernelkind <kernelkind@gmail.com>
Date: Tue, 16 Dec 2025 14:23:33 -0500
feat(notedeck-ui): HorizontalHeader
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
1 file changed, 150 insertions(+), 1 deletion(-)
diff --git a/crates/notedeck_ui/src/header.rs b/crates/notedeck_ui/src/header.rs
@@ -1,4 +1,5 @@
-use egui::Stroke;
+use egui::{Frame, Layout, Margin, Stroke, UiBuilder};
+use egui_extras::{Size, StripBuilder};
pub fn chevron(
ui: &mut egui::Ui,
@@ -21,3 +22,151 @@ pub fn chevron(
r
}
+
+/// Generic UI Widget to render widgets horizontally where each is aligned vertically
+pub struct HorizontalHeader {
+ height: f32,
+ margin: Margin,
+ layout: Layout,
+}
+
+impl HorizontalHeader {
+ pub fn new(height: f32) -> Self {
+ Self {
+ height,
+ margin: Margin::same(8),
+ layout: Layout::left_to_right(egui::Align::Center),
+ }
+ }
+
+ pub fn with_margin(mut self, margin: Margin) -> Self {
+ self.margin = margin;
+ self
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ pub fn ui(
+ self,
+ ui: &mut egui::Ui,
+ left_priority: i8, // lower the value, higher the priority
+ center_priority: i8,
+ right_priority: i8,
+ left_aligned: impl FnMut(&mut egui::Ui),
+ centered: impl FnMut(&mut egui::Ui),
+ right_aligned: impl FnMut(&mut egui::Ui),
+ ) {
+ let prev_spacing = ui.spacing().item_spacing.y;
+ ui.spacing_mut().item_spacing.y = 0.0;
+ Frame::new().inner_margin(self.margin).show(ui, |ui| {
+ let mut rect = ui.available_rect_before_wrap();
+ rect.set_height(self.height);
+
+ let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
+
+ horizontal_header_inner(
+ &mut child_ui,
+ self.layout,
+ left_priority,
+ center_priority,
+ right_priority,
+ left_aligned,
+ centered,
+ right_aligned,
+ );
+ ui.advance_cursor_after_rect(rect);
+ });
+ ui.spacing_mut().item_spacing.y = prev_spacing;
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn horizontal_header_inner(
+ ui: &mut egui::Ui,
+ layout: Layout,
+ left_priority: i8, // lower the value, higher the priority
+ center_priority: i8,
+ right_priority: i8,
+ left_aligned: impl FnMut(&mut egui::Ui),
+ centered: impl FnMut(&mut egui::Ui),
+ right_aligned: impl FnMut(&mut egui::Ui),
+) {
+ let item_spacing = 6.0 * ui.spacing().item_spacing.x;
+ let max_width = ui.available_width() - item_spacing;
+
+ let (left_width, left_aligned) = measure_width(ui, left_aligned);
+ let (center_width, centered) = measure_width(ui, centered);
+ let (right_width, right_aligned) = measure_width(ui, right_aligned);
+
+ let half_max = max_width / 2.0;
+ let half_center = center_width / 2.0;
+ let left_spacing = half_max - left_width - half_center;
+ let right_spacing = half_max - right_width - half_center;
+
+ let mut left_center = half_center;
+ let left_cell = if left_spacing > 0.0 || left_priority < center_priority {
+ Size::exact(left_width)
+ } else {
+ Size::remainder()
+ };
+ let mut left_gap = Size::exact(left_spacing.max(0.0));
+
+ if left_spacing <= 0.0 {
+ left_gap = Size::exact(0.0);
+ if left_priority < center_priority {
+ left_center = (half_center + left_spacing).max(0.0);
+ }
+ }
+
+ let mut center_cell = Size::exact((left_center + half_center).max(0.0));
+ let mut right_gap = Size::exact(right_spacing.max(0.0));
+ let mut right_cell = Size::exact(right_width);
+
+ if right_spacing <= 0.0 {
+ right_gap = Size::exact(0.0);
+ if center_priority < right_priority {
+ right_cell = Size::remainder();
+ } else {
+ center_cell = Size::remainder();
+ }
+ }
+
+ let sizes = [left_cell, left_gap, center_cell, right_gap, right_cell];
+
+ let mut builder = StripBuilder::new(ui);
+ for size in sizes {
+ builder = builder.size(size);
+ }
+
+ builder.cell_layout(layout).horizontal(|mut strip| {
+ strip.cell(left_aligned);
+ strip.empty();
+ strip.cell(centered);
+ strip.empty();
+ strip.cell(right_aligned);
+ });
+}
+
+/// Inspired by VirtualList::ui_custom_layout
+fn measure_width(
+ ui: &mut egui::Ui,
+ mut render: impl FnMut(&mut egui::Ui),
+) -> (f32, impl FnMut(&mut egui::Ui)) {
+ let mut measure_ui = ui.new_child(
+ UiBuilder::new()
+ .max_rect(ui.max_rect())
+ .layout(Layout::left_to_right(egui::Align::Min)),
+ );
+ measure_ui.set_invisible();
+
+ let start_width = measure_ui.next_widget_position();
+ let res = measure_ui.scope_builder(UiBuilder::new().id_salt(ui.id().with("measure")), |ui| {
+ render(ui);
+ render
+ });
+ let end_width = measure_ui.next_widget_position();
+
+ (
+ (end_width.x - start_width.x + ui.spacing().item_spacing.x).max(0.0),
+ res.inner,
+ )
+}