bezel-ui 0.2.18

SwiftUI-flavored components for gpui — popovers, menus, buttons, toggles, and loaders
Documentation
//! Shared caret geometry for text fields and document renderers.

use std::ops::Range;

use gpui::{
    App, BorderStyle, Bounds, Font, Global, Hsla, LineLayout, PaintQuad, Pixels, Point, TextRun,
    Window, WrappedLineLayout, fill, outline, point, px,
};

use super::overdraw::Layers;

/// The app-wide caret shape. Terminal cursors follow their terminal instead.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CaretShape {
    #[default]
    Bar,
    Block,
    Underline,
}

impl Global for CaretShape {}

pub(crate) fn caret_shape(cx: &App) -> CaretShape {
    cx.try_global::<CaretShape>().copied().unwrap_or_default()
}

/// Changes the shape and repaints open windows.
pub(crate) fn set_caret_shape(shape: CaretShape, cx: &mut App) {
    cx.set_global(shape);
    cx.refresh_windows();
}

/// How tall the app-wide block caret stands.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CaretHeight {
    /// The line's height, leading included.
    #[default]
    Line,
    /// The bar's height: the text's font size.
    Text,
}

impl Global for CaretHeight {}

pub(crate) fn caret_height(cx: &App) -> CaretHeight {
    cx.try_global::<CaretHeight>().copied().unwrap_or_default()
}

/// Changes the height and repaints open windows.
pub(crate) fn set_caret_height(height: CaretHeight, cx: &mut App) {
    cx.set_global(height);
    cx.refresh_windows();
}

/// What the app-wide block caret paints as while its window is inactive.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InactiveCaret {
    /// Outlined.
    #[default]
    Hollow,
    /// Nothing.
    Hidden,
}

impl Global for InactiveCaret {}

pub(crate) fn inactive_caret(cx: &App) -> InactiveCaret {
    cx.try_global::<InactiveCaret>()
        .copied()
        .unwrap_or_default()
}

/// Changes it and repaints open windows.
pub(crate) fn set_inactive_caret(inactive: InactiveCaret, cx: &mut App) {
    cx.set_global(inactive);
    cx.refresh_windows();
}

impl CaretShape {
    /// Whether a caret of this shape paints at all, given whether its window
    /// is active. Only a block changes with the window.
    pub fn shown(self, window_active: bool, inactive: InactiveCaret) -> bool {
        window_active || self != Self::Block || inactive == InactiveCaret::Hollow
    }

    /// Adapts a renderer's existing bar, centred in a line `line_height` tall,
    /// to `width`, which a bar ignores. A block stands as tall as `height`
    /// says, solid or, when `hollow`, outlined.
    pub fn quad(
        self,
        mut bar: Bounds<Pixels>,
        line_height: Pixels,
        height: CaretHeight,
        width: Pixels,
        color: Hsla,
        hollow: bool,
    ) -> PaintQuad {
        if self != Self::Bar {
            bar.size.width = width;
        }
        match self {
            Self::Bar => fill(bar, color),
            Self::Block => {
                if height == CaretHeight::Line {
                    bar.origin.y -= (line_height - bar.size.height) / 2.0;
                    bar.size.height = line_height;
                }
                match hollow {
                    true => outline(bar, color, BorderStyle::Solid),
                    false => fill(bar, color),
                }
            }
            Self::Underline => {
                let thickness = px(2.0).min(bar.size.height);
                bar.origin.y += bar.size.height - thickness;
                bar.size.height = thickness;
                fill(bar, color)
            }
        }
    }

    /// Whether the glyph under the caret is painted in the background colour,
    /// over the caret, by [`paint_covered`].
    pub fn cuts_out(self, hollow: bool) -> bool {
        self == Self::Block && !hollow
    }
}

/// The shaped width of `0` in `font`: a wide caret's width where no character
/// follows it.
pub fn zero_width(font: Font, font_size: Pixels, window: &Window) -> Pixels {
    let run = TextRun {
        len: 1,
        font,
        color: Hsla::default(),
        background_color: None,
        underline: None,
        strikethrough: None,
    };
    window
        .text_system()
        .shape_line("0".into(), font_size, &[run], None)
        .width
}

/// Paints the glyphs `layout` shaped for `range` again, in `color`: what a
/// solid block covers, painted over the block and the line. `at` is the top
/// left of the first one's slot, on a row `line_height` tall.
///
/// gpui shapes each colour of a line apart, so recolouring the covered glyph
/// in the line's runs moves the glyphs around it.
pub fn paint_covered(
    layout: &LineLayout,
    range: &Range<usize>,
    at: Point<Pixels>,
    line_height: Pixels,
    color: Hsla,
    window: &mut Window,
) {
    let mut glyphs = layout
        .runs
        .iter()
        .flat_map(|run| run.glyphs.iter().map(move |glyph| (run.font_id, glyph)))
        .filter(|(_, glyph)| range.contains(&glyph.index))
        .peekable();
    let Some(left) = glyphs.peek().map(|(_, glyph)| glyph.position.x) else {
        return;
    };
    let baseline = (line_height - layout.ascent - layout.descent) / 2. + layout.ascent;
    for (font_id, glyph) in glyphs {
        let origin = point(
            at.x + glyph.position.x - left,
            at.y + baseline + glyph.position.y,
        );
        let _ = match glyph.is_emoji {
            true => window.paint_emoji(origin, font_id, glyph.id, layout.font_size),
            false => window.paint_glyph(origin, font_id, glyph.id, layout.font_size, color),
        };
    }
}

/// The colour [`paint_covered`] draws the glyph at `offset` of `runs` in, under
/// a solid `caret` over the opaque `page`: [`theme::ink_over`] the caret, from
/// the [`Layers::surface`] there, as a selected glyph is.
pub fn cover<S>(
    runs: &[TextRun],
    offset: usize,
    page: Hsla,
    layers: &Layers<S>,
    caret: Hsla,
) -> Hsla {
    let mut at = 0;
    let text = runs
        .iter()
        .find(|run| {
            at += run.len;
            offset < at
        })
        .or(runs.last())
        .map_or(page, |run| run.color);
    let surface = layers.surface(page, &(offset..offset + 1));
    theme::ink_over(text, page, surface, theme::stack(page, [caret]))
}

/// `runs` cut at `range`'s edges, the pieces inside it in `color`.
pub fn recoloured(runs: Vec<TextRun>, range: &Range<usize>, color: Hsla) -> Vec<TextRun> {
    let mut out = Vec::with_capacity(runs.len() + 2);
    let mut at = 0;
    for run in runs {
        let end = at + run.len;
        for (start, stop, inside) in [
            (at, end.min(range.start), false),
            (at.max(range.start), end.min(range.end), true),
            (at.max(range.end), end, false),
        ] {
            if stop <= start {
                continue;
            }
            out.push(TextRun {
                len: stop - start,
                color: if inside { color } else { run.color },
                ..run.clone()
            });
        }
        at = end;
    }
    out
}

/// The next grapheme's shaped advance, or no character at a hard/soft line end.
pub fn character_advance(line: &WrappedLineLayout, text: &str, offset: usize) -> Option<Pixels> {
    if offset >= text.len() || !text.is_char_boundary(offset) {
        return None;
    }
    if line
        .wrap_boundaries()
        .iter()
        .any(|boundary| line.runs()[boundary.run_ix].glyphs[boundary.glyph_ix].index == offset)
    {
        return None;
    }
    let end = super::next_boundary(text, offset);
    Some((line.unwrapped_layout.x_for_index(end) - line.unwrapped_layout.x_for_index(offset)).abs())
}