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