egui_extras 0.32.3

Extra functionality and widgets for the egui GUI library
Documentation
use egui::{Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, emath::GuiRounding as _};

#[derive(Clone, Copy)]
pub(crate) enum CellSize {
    /// Absolute size in points
    Absolute(f32),

    /// Take all available space
    Remainder,
}

/// Cells are positioned in two dimensions, cells go in one direction and form lines.
///
/// In a strip there's only one line which goes in the direction of the strip:
///
/// In a horizontal strip, a [`StripLayout`] with horizontal [`CellDirection`] is used.
/// Its cells go from left to right inside this [`StripLayout`].
///
/// In a table there's a [`StripLayout`] for each table row with a horizontal [`CellDirection`].
/// Its cells go from left to right. And the lines go from top to bottom.
pub(crate) enum CellDirection {
    /// Cells go from left to right.
    Horizontal,

    /// Cells go from top to bottom.
    Vertical,
}

/// Flags used by [`StripLayout::add`].
#[derive(Clone, Copy, Default)]
pub(crate) struct StripLayoutFlags {
    pub(crate) clip: bool,
    pub(crate) striped: bool,
    pub(crate) hovered: bool,
    pub(crate) selected: bool,
    pub(crate) overline: bool,

    /// Used when we want to accruately measure the size of this cell.
    pub(crate) sizing_pass: bool,
}

/// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`]
pub struct StripLayout<'l> {
    pub(crate) ui: &'l mut Ui,
    direction: CellDirection,
    pub(crate) rect: Rect,
    pub(crate) cursor: Pos2,

    /// Keeps track of the max used position,
    /// so we know how much space we used.
    max: Pos2,

    cell_layout: egui::Layout,
    sense: Sense,
}

impl<'l> StripLayout<'l> {
    pub(crate) fn new(
        ui: &'l mut Ui,
        direction: CellDirection,
        cell_layout: egui::Layout,
        sense: Sense,
    ) -> Self {
        let rect = ui.available_rect_before_wrap();
        let pos = rect.left_top();

        Self {
            ui,
            direction,
            rect,
            cursor: pos,
            max: pos,
            cell_layout,
            sense,
        }
    }

    fn cell_rect(&self, width: &CellSize, height: &CellSize) -> Rect {
        Rect {
            min: self.cursor,
            max: Pos2 {
                x: match width {
                    CellSize::Absolute(width) => self.cursor.x + width,
                    CellSize::Remainder => self.rect.right(),
                },
                y: match height {
                    CellSize::Absolute(height) => self.cursor.y + height,
                    CellSize::Remainder => self.rect.bottom(),
                },
            },
        }
    }

    fn set_pos(&mut self, rect: Rect) {
        self.max.x = self.max.x.max(rect.right());
        self.max.y = self.max.y.max(rect.bottom());

        match self.direction {
            CellDirection::Horizontal => {
                self.cursor.x = rect.right() + self.ui.spacing().item_spacing.x;
            }
            CellDirection::Vertical => {
                self.cursor.y = rect.bottom() + self.ui.spacing().item_spacing.y;
            }
        }
    }

    pub(crate) fn empty(&mut self, width: CellSize, height: CellSize) {
        self.set_pos(self.cell_rect(&width, &height));
    }

    /// This is the innermost part of [`crate::Table`] and [`crate::Strip`].
    ///
    /// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
    pub(crate) fn add(
        &mut self,
        flags: StripLayoutFlags,
        width: CellSize,
        height: CellSize,
        child_ui_id_salt: Id,
        add_cell_contents: impl FnOnce(&mut Ui),
    ) -> (Rect, Response) {
        let max_rect = self.cell_rect(&width, &height);

        // Make sure we don't have a gap in the stripe/frame/selection background:
        let item_spacing = self.ui.spacing().item_spacing;
        let gapless_rect = max_rect.expand2(0.5 * item_spacing).round_ui();

        if flags.striped {
            self.ui.painter().rect_filled(
                gapless_rect,
                egui::CornerRadius::ZERO,
                self.ui.visuals().faint_bg_color,
            );
        }

        if flags.selected {
            self.ui.painter().rect_filled(
                gapless_rect,
                egui::CornerRadius::ZERO,
                self.ui.visuals().selection.bg_fill,
            );
        }

        if flags.hovered && !flags.selected && self.sense.interactive() {
            self.ui.painter().rect_filled(
                gapless_rect,
                egui::CornerRadius::ZERO,
                self.ui.visuals().widgets.hovered.bg_fill,
            );
        }

        let mut child_ui = self.cell(flags, max_rect, child_ui_id_salt, add_cell_contents);

        let used_rect = child_ui.min_rect();

        // Make sure we catch clicks etc on the _whole_ cell:
        child_ui.set_min_size(max_rect.size());

        let allocation_rect = if self.ui.is_sizing_pass() {
            used_rect
        } else if flags.clip {
            max_rect
        } else {
            max_rect | used_rect
        };

        self.set_pos(allocation_rect);

        self.ui.advance_cursor_after_rect(allocation_rect);

        let response = child_ui.response();

        (used_rect, response)
    }

    /// only needed for layouts with multiple lines, like [`Table`](crate::Table).
    pub fn end_line(&mut self) {
        match self.direction {
            CellDirection::Horizontal => {
                self.cursor.y = self.max.y + self.ui.spacing().item_spacing.y;
                self.cursor.x = self.rect.left();
            }
            CellDirection::Vertical => {
                self.cursor.x = self.max.x + self.ui.spacing().item_spacing.x;
                self.cursor.y = self.rect.top();
            }
        }
    }

    /// Skip a lot of space.
    pub(crate) fn skip_space(&mut self, delta: egui::Vec2) {
        let before = self.cursor;
        self.cursor += delta;
        let rect = Rect::from_two_pos(before, self.cursor);
        self.ui.allocate_rect(rect, Sense::hover());
    }

    /// Return the Ui to which the contents where added
    fn cell(
        &mut self,
        flags: StripLayoutFlags,
        max_rect: Rect,
        child_ui_id_salt: egui::Id,
        add_cell_contents: impl FnOnce(&mut Ui),
    ) -> Ui {
        let mut ui_builder = UiBuilder::new()
            .id_salt(child_ui_id_salt)
            .ui_stack_info(egui::UiStackInfo::new(egui::UiKind::TableCell))
            .max_rect(max_rect)
            .layout(self.cell_layout)
            .sense(self.sense);
        if flags.sizing_pass {
            ui_builder = ui_builder.sizing_pass();
        }

        let mut child_ui = self.ui.new_child(ui_builder);

        if flags.clip {
            let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
            let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
            let clip_rect = max_rect.expand2(margin);
            child_ui.shrink_clip_rect(clip_rect);

            if !child_ui.is_sizing_pass() {
                // Better to truncate (if we can), rather than hard clipping:
                child_ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
            }
        }

        if flags.selected {
            let stroke_color = child_ui.style().visuals.selection.stroke.color;
            child_ui.style_mut().visuals.override_text_color = Some(stroke_color);
        }

        if flags.overline {
            child_ui.painter().hline(
                max_rect.x_range(),
                max_rect.top(),
                child_ui.visuals().widgets.noninteractive.bg_stroke,
            );
        }

        add_cell_contents(&mut child_ui);

        child_ui
    }

    /// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size
    pub fn allocate_rect(&mut self) -> Response {
        let mut rect = self.rect;
        rect.set_right(self.max.x);
        rect.set_bottom(self.max.y);

        self.ui.allocate_rect(rect, Sense::hover())
    }
}