Detailed changes
@@ -8,8 +8,8 @@ use client::{Client, TelemetrySettings};
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use gpui::{
- Action, Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, UpdateGlobal,
- WeakEntity, actions, prelude::*, svg, transparent_black,
+ Action, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyBinding, Task,
+ UpdateGlobal, WeakEntity, actions, prelude::*, svg, transparent_black,
};
use menu;
use persistence::ONBOARDING_DB;
@@ -20,7 +20,7 @@ use settings::{Settings, SettingsStore};
use settings_ui::SettingsUiFeatureFlag;
use std::sync::Arc;
use theme::{Theme, ThemeRegistry, ThemeSettings};
-use ui::{ListItem, ToggleState, Vector, VectorName, prelude::*};
+use ui::{KeybindingHint, ListItem, Ring, ToggleState, Vector, VectorName, prelude::*};
use util::ResultExt;
use vim_mode_setting::VimModeSetting;
use welcome::BaseKeymap;
@@ -567,7 +567,7 @@ impl OnboardingUI {
v_flex()
.gap_px()
.py(px(16.))
- .gap(px(12.))
+ .gap(px(2.))
.child(self.render_nav_item(
OnboardingPage::Basics,
"The Basics",
@@ -604,12 +604,13 @@ impl OnboardingUI {
shortcut: impl Into<SharedString>,
cx: &mut Context<Self>,
) -> impl gpui::IntoElement {
- let selected = self.current_page == page;
+ let is_selected = self.current_page == page;
let label = label.into();
let shortcut = shortcut.into();
let id = ElementId::Name(label.clone());
+ let corner_radius = px(4.);
- let is_focused = match page {
+ let item_focused = match page {
OnboardingPage::Basics => self.nav_focus == NavigationFocusItem::Basics,
OnboardingPage::Editing => self.nav_focus == NavigationFocusItem::Editing,
OnboardingPage::AiSetup => self.nav_focus == NavigationFocusItem::AiSetup,
@@ -618,35 +619,39 @@ impl OnboardingUI {
let area_focused = self.focus_area == FocusArea::Navigation;
- h_flex()
- .id(id)
- .h(rems(1.5))
- .w_full()
- .when(is_focused, |this| {
- this.bg(if area_focused {
- cx.theme().colors().border_focused.opacity(0.16)
- } else {
- cx.theme().colors().border.opacity(0.24)
- })
- })
- .child(
- div()
- .w(px(3.))
- .h_full()
- .when(selected, |this| this.bg(cx.theme().colors().border_focused)),
- )
+ Ring::new(corner_radius, item_focused)
+ .active(area_focused && item_focused)
.child(
h_flex()
- .pl(px(23.))
- .flex_1()
- .justify_between()
- .items_center()
- .child(Label::new(label).when(is_focused, |this| this.color(Color::Default)))
- .child(Label::new(format!("⌘{}", shortcut.clone())).color(Color::Muted)),
+ .id(id)
+ .h(rems(1.625))
+ .w_full()
+ .rounded(corner_radius)
+ .px_3()
+ .when(is_selected, |this| {
+ this.bg(cx.theme().colors().border_focused.opacity(0.16))
+ })
+ .child(
+ h_flex()
+ .flex_1()
+ .justify_between()
+ .items_center()
+ .child(
+ Label::new(label)
+ .weight(FontWeight::MEDIUM)
+ .color(Color::Muted)
+ .when(item_focused, |this| this.color(Color::Default)),
+ )
+ .child(
+ Label::new(format!("⌘{}", shortcut.clone()))
+ .color(Color::Placeholder)
+ .size(LabelSize::XSmall),
+ ),
+ )
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.jump_to_page(page, window, cx);
+ })),
)
- .on_click(cx.listener(move |this, _, window, cx| {
- this.jump_to_page(page, window, cx);
- }))
}
fn render_bottom_controls(
@@ -654,7 +659,7 @@ impl OnboardingUI {
window: &mut gpui::Window,
cx: &mut Context<Self>,
) -> impl gpui::IntoElement {
- h_flex().w_full().p(px(12.)).pl(px(24.)).child(
+ h_flex().w_full().p(px(12.)).child(
JuicyButton::new(if self.current_page == OnboardingPage::Welcome {
"Get Started"
} else {
@@ -2,7 +2,7 @@
use gpui::{Hsla, Length};
use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
-use ui::{IntoElement, RenderOnce, prelude::*, utils::inner_corner_radius};
+use ui::{IntoElement, RenderOnce, prelude::*, utils::CornerSolver};
/// Shows a preview of a theme as an abstract illustration
/// of a thumbnail-sized editor.
@@ -37,7 +37,7 @@ impl RenderOnce for ThemePreviewTile {
let root_padding = px(0.0);
let child_border = px(1.0);
let inner_radius =
- inner_corner_radius(root_radius, root_border, root_padding, child_border);
+ CornerSolver::child_radius(root_radius, root_border, root_padding, child_border);
let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
@@ -27,6 +27,7 @@ mod popover_menu;
mod progress;
mod radio;
mod right_click_menu;
+mod ring;
mod scrollbar;
mod settings_container;
mod settings_group;
@@ -69,6 +70,7 @@ pub use popover_menu::*;
pub use progress::*;
pub use radio::*;
pub use right_click_menu::*;
+pub use ring::*;
pub use scrollbar::*;
pub use settings_container::*;
pub use settings_group::*;
@@ -0,0 +1,66 @@
+use gpui::{
+ AnyElement, IntoElement, ParentElement, Pixels, RenderOnce, Styled, px, transparent_black,
+};
+use smallvec::SmallVec;
+use theme::ActiveTheme;
+
+use crate::{h_flex, utils::CornerSolver};
+
+/// A ring is a stylistic focus indicator that draws a ring around
+/// an element with some space between the element and ring.
+#[derive(IntoElement)]
+pub struct Ring {
+ corner_radius: Pixels,
+ border_width: Pixels,
+ padding: Pixels,
+ focused: bool,
+ active: bool,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl Ring {
+ pub fn new(child_corner_radius: Pixels, focused: bool) -> Self {
+ let border_width = px(1.);
+ let padding = px(2.);
+ let corner_radius =
+ CornerSolver::parent_radius(child_corner_radius, border_width, padding, px(0.));
+ Self {
+ corner_radius,
+ border_width,
+ padding,
+ focused,
+ active: false,
+ children: SmallVec::new(),
+ }
+ }
+
+ pub fn active(mut self, active: bool) -> Self {
+ self.active = active;
+ self
+ }
+}
+
+impl RenderOnce for Ring {
+ fn render(self, window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
+ let border_color = if self.focused && self.active {
+ cx.theme().colors().border_focused.opacity(0.48)
+ } else if self.focused {
+ cx.theme().colors().border_variant
+ } else {
+ transparent_black()
+ };
+
+ h_flex()
+ .border(self.border_width)
+ .border_color(border_color)
+ .rounded(self.corner_radius)
+ .p(self.padding)
+ .children(self.children)
+ }
+}
+
+impl ParentElement for Ring {
+ fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+ self.children.extend(elements)
+ }
+}
@@ -10,7 +10,7 @@ mod search_input;
mod with_rem_size;
pub use color_contrast::*;
-pub use corner_solver::{CornerSolver, inner_corner_radius};
+pub use corner_solver::{CornerSolver, NestedCornerSolver};
pub use format_distance::*;
pub use search_input::*;
pub use with_rem_size::*;
@@ -1,61 +1,196 @@
use gpui::Pixels;
-/// Calculates the child’s content-corner radius for a single nested level.
+/// Calculates corner radii for nested elements in both directions.
///
-/// child_content_radius = max(0, parent_radius - parent_border - parent_padding + self_border)
+/// ## Forward calculation (parent → child)
+/// Given a parent's corner radius, calculates the child's corner radius:
+/// ```
+/// child_radius = max(0, parent_radius - parent_border - parent_padding + child_border)
+/// ```
///
-/// - parent_radius: outer corner radius of the parent element
-/// - parent_border: border width of the parent element
-/// - parent_padding: padding of the parent element
-/// - self_border: border width of this child element (for content inset)
-pub fn inner_corner_radius(
- parent_radius: Pixels,
- parent_border: Pixels,
- parent_padding: Pixels,
- self_border: Pixels,
-) -> Pixels {
- (parent_radius - parent_border - parent_padding + self_border).max(Pixels::ZERO)
+/// ## Inverse calculation (child → parent)
+/// Given a child's desired corner radius, calculates the required parent radius:
+/// ```
+/// parent_radius = child_radius + parent_border + parent_padding - child_border
+/// ```
+pub struct CornerSolver;
+
+impl CornerSolver {
+ /// Calculates the child's corner radius given the parent's properties.
+ ///
+ /// # Arguments
+ /// - `parent_radius`: Outer corner radius of the parent element
+ /// - `parent_border`: Border width of the parent element
+ /// - `parent_padding`: Padding of the parent element
+ /// - `child_border`: Border width of the child element
+ pub fn child_radius(
+ parent_radius: Pixels,
+ parent_border: Pixels,
+ parent_padding: Pixels,
+ child_border: Pixels,
+ ) -> Pixels {
+ (parent_radius - parent_border - parent_padding + child_border).max(Pixels::ZERO)
+ }
+
+ /// Calculates the required parent radius to achieve a desired child radius.
+ ///
+ /// # Arguments
+ /// - `child_radius`: Desired corner radius for the child element
+ /// - `parent_border`: Border width of the parent element
+ /// - `parent_padding`: Padding of the parent element
+ /// - `child_border`: Border width of the child element
+ pub fn parent_radius(
+ child_radius: Pixels,
+ parent_border: Pixels,
+ parent_padding: Pixels,
+ child_border: Pixels,
+ ) -> Pixels {
+ child_radius + parent_border + parent_padding - child_border
+ }
}
-/// Solver for arbitrarily deep nested corner radii.
-///
-/// Each nested level’s outer border-box radius is:
-/// R₀ = max(0, root_radius - root_border - root_padding)
-/// Rᵢ = max(0, Rᵢ₋₁ - childᵢ₋₁_border - childᵢ₋₁_padding) for i > 0
-pub struct CornerSolver {
- root_radius: Pixels,
- root_border: Pixels,
- root_padding: Pixels,
- children: Vec<(Pixels, Pixels)>, // (border, padding)
+/// Builder for calculating corner radii across multiple nested levels.
+pub struct NestedCornerSolver {
+ levels: Vec<Level>,
}
-impl CornerSolver {
- pub fn new(root_radius: Pixels, root_border: Pixels, root_padding: Pixels) -> Self {
- Self {
- root_radius,
- root_border,
- root_padding,
- children: Vec::new(),
- }
+#[derive(Debug, Clone, Copy)]
+struct Level {
+ border: Pixels,
+ padding: Pixels,
+}
+
+impl NestedCornerSolver {
+ /// Creates a new nested corner solver.
+ pub fn new() -> Self {
+ Self { levels: Vec::new() }
}
- pub fn add_child(mut self, border: Pixels, padding: Pixels) -> Self {
- self.children.push((border, padding));
+ /// Adds a level to the nesting hierarchy.
+ ///
+ /// Levels should be added from outermost to innermost.
+ pub fn add_level(mut self, border: Pixels, padding: Pixels) -> Self {
+ self.levels.push(Level { border, padding });
self
}
- pub fn corner_radius(&self, level: usize) -> Pixels {
- if level == 0 {
- return (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO);
+ /// Calculates the corner radius at a specific nesting level given the root radius.
+ ///
+ /// # Arguments
+ /// - `root_radius`: The outermost corner radius
+ /// - `level`: The nesting level (0 = first child of root, 1 = grandchild, etc.)
+ pub fn radius_at_level(&self, root_radius: Pixels, level: usize) -> Pixels {
+ let mut radius = root_radius;
+
+ for i in 0..=level.min(self.levels.len().saturating_sub(1)) {
+ let current_level = &self.levels[i];
+ let next_border = if i < self.levels.len() - 1 {
+ self.levels[i + 1].border
+ } else {
+ Pixels::ZERO
+ };
+
+ radius = CornerSolver::child_radius(
+ radius,
+ current_level.border,
+ current_level.padding,
+ next_border,
+ );
}
- if level >= self.children.len() {
- return Pixels::ZERO;
+
+ radius
+ }
+
+ /// Calculates the required root radius to achieve a desired radius at a specific level.
+ ///
+ /// # Arguments
+ /// - `target_radius`: The desired corner radius at the target level
+ /// - `target_level`: The nesting level where the target radius should be achieved
+ pub fn root_radius_for_level(&self, target_radius: Pixels, target_level: usize) -> Pixels {
+ if target_level >= self.levels.len() {
+ return target_radius;
}
- let mut r = (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO);
- for i in 0..level {
- let (b, p) = self.children[i];
- r = (r - b - p).max(Pixels::ZERO);
+
+ let mut radius = target_radius;
+
+ // Work backwards from the target level to the root
+ for i in (0..=target_level).rev() {
+ let current_level = &self.levels[i];
+ let child_border = if i < self.levels.len() - 1 {
+ self.levels[i + 1].border
+ } else {
+ Pixels::ZERO
+ };
+
+ radius = CornerSolver::parent_radius(
+ radius,
+ current_level.border,
+ current_level.padding,
+ child_border,
+ );
}
- r
+
+ radius
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_forward_calculation() {
+ let parent_radius = Pixels(20.0);
+ let parent_border = Pixels(2.0);
+ let parent_padding = Pixels(8.0);
+ let child_border = Pixels(1.0);
+
+ let child_radius =
+ CornerSolver::child_radius(parent_radius, parent_border, parent_padding, child_border);
+
+ assert_eq!(child_radius, Pixels(11.0)); // 20 - 2 - 8 + 1 = 11
+ }
+
+ #[test]
+ fn test_inverse_calculation() {
+ let child_radius = Pixels(11.0);
+ let parent_border = Pixels(2.0);
+ let parent_padding = Pixels(8.0);
+ let child_border = Pixels(1.0);
+
+ let parent_radius =
+ CornerSolver::parent_radius(child_radius, parent_border, parent_padding, child_border);
+
+ assert_eq!(parent_radius, Pixels(20.0)); // 11 + 2 + 8 - 1 = 20
+ }
+
+ #[test]
+ fn test_nested_forward() {
+ let solver = NestedCornerSolver::new()
+ .add_level(Pixels(2.0), Pixels(8.0)) // Root level
+ .add_level(Pixels(1.0), Pixels(4.0)) // First child
+ .add_level(Pixels(1.0), Pixels(2.0)); // Second child
+
+ let root_radius = Pixels(20.0);
+
+ assert_eq!(solver.radius_at_level(root_radius, 0), Pixels(11.0)); // 20 - 2 - 8 + 1
+ assert_eq!(solver.radius_at_level(root_radius, 1), Pixels(7.0)); // 11 - 1 - 4 + 1
+ assert_eq!(solver.radius_at_level(root_radius, 2), Pixels(4.0)); // 7 - 1 - 2 + 0
+ }
+
+ #[test]
+ fn test_nested_inverse() {
+ let solver = NestedCornerSolver::new()
+ .add_level(Pixels(2.0), Pixels(8.0)) // Root level
+ .add_level(Pixels(1.0), Pixels(4.0)) // First child
+ .add_level(Pixels(1.0), Pixels(2.0)); // Second child
+
+ let target_radius = Pixels(4.0);
+ let root_radius = solver.root_radius_for_level(target_radius, 2);
+
+ assert_eq!(root_radius, Pixels(20.0));
+
+ // Verify by calculating forward
+ assert_eq!(solver.radius_at_level(root_radius, 2), target_radius);
}
}