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