Skip to main content

ui/widgets/
mod.rs

1//! Widgets, grouped as catalog traits on `Theme` — import the group, reach
2//! the component: `use ui::widgets::{Content, Controls, Icons,
3//! Layout, Scaffolding, Status};` → `theme.group_box()`, `theme.tab(..)`.
4//!
5//! What stays here is deliberately trait-shaped: state flags and pure math,
6//! neither of which reads the theme as a receiver.
7
8use gpui::{div, prelude::*, px};
9
10mod buttons;
11mod content;
12mod controls;
13mod icon;
14mod layout;
15mod scaffolding;
16mod status;
17
18pub use buttons::{ButtonStyle, Buttons};
19pub use content::Content;
20pub use controls::{Controls, SliderDrag, slider_fraction};
21pub use icon::Icons;
22pub use layout::{Layout, SPLIT_HANDLE_HIT, SplitDrag, SplitStyle};
23pub use scaffolding::{OPTION_CARD_HEIGHT, OPTION_CARD_RADIUS, Scaffolding};
24pub use status::Status;
25
26/// What a control paints in the 1px border it keeps for
27/// [`crate::focus::focusable`]'s ring: nothing, until focus fills it.
28///
29/// Always present, never conditional. gpui sizes border-box, so a border that
30/// appeared only on focus would shift the content under it by a pixel — a
31/// checkbox whose tick jumps as you tab onto it.
32pub(crate) const RING_SLOT: gpui::Hsla = gpui::transparent_black();
33
34/// A flag that follows something else until the user takes it over.
35///
36/// The rule behind a section that opens itself while work streams in and
37/// collapses when it stops: auto-follow is right until the first press, and
38/// wrong immediately after — whatever the flag does next, the person who
39/// clicked has to win. Nothing agent-shaped about it; a build log that unfolds
40/// while it runs and a detail pane that follows the selection both want this.
41///
42/// It is an `Option<bool>` rather than the two flags it reads as (*touched*,
43/// plus the value): "untouched, and here is the manual value" is a state that
44/// cannot mean anything, and this way it cannot be written.
45///
46/// ```ignore
47/// let open = self.details.get(self.running);           // paint this
48/// // …on the header's click:
49/// self.details.toggle(self.running);
50/// ```
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub struct Takeover(Option<bool>);
53
54impl Takeover {
55    /// What to show: `auto` until the first [`Self::toggle`], the user's own
56    /// choice from then on.
57    pub fn get(self, auto: bool) -> bool {
58        self.0.unwrap_or(auto)
59    }
60
61    /// Flip what is currently on screen — which while nobody has touched it is
62    /// `auto`, *not* the stored value — and take over from here.
63    pub fn toggle(&mut self, auto: bool) {
64        self.0 = Some(!self.get(auto));
65    }
66}
67
68/// Where `pointer` falls along `axis` as a fraction of `bounds` — what a
69/// divider dragged there makes the split, and what a slider dragged there makes
70/// the value. `Axis::Horizontal` travels in x.
71///
72/// Clamped to `min..=1-min` — the dead zone a split passes so neither pane can
73/// be squeezed away, and the `0.0` a slider passes because it has none. On a
74/// zero-extent container the answer is `min`: the frame before layout has run
75/// would otherwise divide by zero.
76pub fn axis_fraction(
77    pointer: gpui::Point<gpui::Pixels>,
78    bounds: gpui::Bounds<gpui::Pixels>,
79    axis: gpui::Axis,
80    min: f32,
81) -> f32 {
82    let min = min.clamp(0.0, 0.5);
83    let (offset, extent) = match axis {
84        gpui::Axis::Horizontal => (pointer.x - bounds.left(), bounds.size.width),
85        gpui::Axis::Vertical => (pointer.y - bounds.top(), bounds.size.height),
86    };
87    if extent <= px(0.0) {
88        return min;
89    }
90    (offset / extent).clamp(min, 1.0 - min)
91}
92
93/// A small state dot — the "working / idle / failed" bead on a row. Takes the
94/// tone from the caller so the meaning stays with the caller's domain.
95pub fn status_dot(tone: gpui::Hsla) -> gpui::Div {
96    div().flex_none().size(px(6.0)).rounded_full().bg(tone)
97}