welcome: Theme preview tile (#29689)

![CleanShot 2025-04-30 at 13 26
44@2x](https://github.com/user-attachments/assets/f68fefe2-84a1-48b7-b9a2-47c2547cd06b)


- Adds the ThemePreviewTile component, used for upcoming onboarding UI
- Adds the CornerSolver utility for resolving correct nested corner
radii

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2025-04-30 13:46:11 -04:00 committed by GitHub
parent 84e4891d54
commit 8c03934b26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 361 additions and 6 deletions

4
Cargo.lock generated
View file

@ -16878,18 +16878,22 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"component",
"db",
"documented",
"editor",
"fuzzy",
"gpui",
"install_cli",
"language",
"linkme",
"picker",
"project",
"schemars",
"serde",
"settings",
"telemetry",
"theme",
"ui",
"util",
"vim_mode_setting",

View file

@ -435,6 +435,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a0
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenv = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
@ -797,5 +798,6 @@ ignored = [
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

View file

@ -15,6 +15,7 @@ path = "src/ui.rs"
[dependencies]
chrono.workspace = true
component.workspace = true
documented.workspace = true
gpui.workspace = true
icons.workspace = true
itertools.workspace = true
@ -28,7 +29,6 @@ strum.workspace = true
theme.workspace = true
ui_macros.workspace = true
util.workspace = true
documented = "0.9.1"
workspace-hack.workspace = true
[target.'cfg(windows)'.dependencies]

View file

@ -4,11 +4,13 @@ use gpui::App;
use theme::ActiveTheme;
mod color_contrast;
mod corner_solver;
mod format_distance;
mod search_input;
mod with_rem_size;
pub use color_contrast::*;
pub use corner_solver::{CornerSolver, inner_corner_radius};
pub use format_distance::*;
pub use search_input::*;
pub use with_rem_size::*;

View file

@ -0,0 +1,61 @@
use gpui::Pixels;
/// Calculates the childs content-corner radius for a single nested level.
///
/// child_content_radius = max(0, parent_radius - parent_border - parent_padding + self_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)
}
/// Solver for arbitrarily deep nested corner radii.
///
/// Each nested levels 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)
}
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(),
}
}
pub fn add_child(mut self, border: Pixels, padding: Pixels) -> Self {
self.children.push((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);
}
if level >= self.children.len() {
return Pixels::ZERO;
}
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);
}
r
}
}

View file

@ -17,23 +17,27 @@ test-support = []
[dependencies]
anyhow.workspace = true
client.workspace = true
component.workspace = true
db.workspace = true
documented.workspace = true
fuzzy.workspace = true
gpui.workspace = true
install_cli.workspace = true
language.workspace = true
linkme.workspace = true
picker.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
telemetry.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
vim_mode_setting.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -1,7 +1,3 @@
mod base_keymap_picker;
mod base_keymap_setting;
mod multibuffer_hint;
use client::{TelemetrySettings, telemetry::Telemetry};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
@ -24,6 +20,11 @@ use workspace::{
pub use base_keymap_setting::BaseKeymap;
pub use multibuffer_hint::*;
mod base_keymap_picker;
mod base_keymap_setting;
mod multibuffer_hint;
mod welcome_ui;
actions!(welcome, [ResetHints]);
pub const FIRST_OPEN: &str = "first_open";

View file

@ -0,0 +1 @@
mod theme_preview;

View file

@ -0,0 +1,280 @@
#![allow(unused, dead_code)]
use gpui::{Hsla, Length};
use std::sync::Arc;
use theme::{Theme, ThemeRegistry};
use ui::{
IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius,
};
/// Shows a preview of a theme as an abstract illustration
/// of a thumbnail-sized editor.
#[derive(IntoElement, RegisterComponent, Documented)]
pub struct ThemePreviewTile {
theme: Arc<Theme>,
selected: bool,
seed: f32,
}
impl ThemePreviewTile {
pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self {
Self {
theme,
selected,
seed,
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ThemePreviewTile {
fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement {
let color = self.theme.colors();
let root_radius = px(8.0);
let root_border = px(2.0);
let root_padding = px(2.0);
let child_border = px(1.0);
let inner_radius =
inner_corner_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);
let skeleton_height = px(4.);
let sidebar_seeded_width = |seed: f32, index: usize| {
let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
0.5 + value * 0.45
};
let sidebar_skeleton_items = 8;
let sidebar_skeleton = (0..sidebar_skeleton_items)
.map(|i| {
let width = sidebar_seeded_width(self.seed, i);
item_skeleton(
relative(width).into(),
skeleton_height,
color.text.alpha(0.45),
)
})
.collect::<Vec<_>>();
let sidebar = div()
.h_full()
.w(relative(0.25))
.border_r(px(1.))
.border_color(color.border_transparent)
.bg(color.panel_background)
.child(
div()
.p_2()
.flex()
.flex_col()
.size_full()
.gap(px(4.))
.children(sidebar_skeleton),
);
let pseudo_code_skeleton = |theme: Arc<Theme>, seed: f32| -> AnyElement {
let colors = theme.colors();
let syntax = theme.syntax();
let keyword_color = syntax.get("keyword").color;
let function_color = syntax.get("function").color;
let string_color = syntax.get("string").color;
let comment_color = syntax.get("comment").color;
let variable_color = syntax.get("variable").color;
let type_color = syntax.get("type").color;
let punctuation_color = syntax.get("punctuation").color;
let syntax_colors = [
keyword_color,
function_color,
string_color,
variable_color,
type_color,
punctuation_color,
comment_color,
];
let line_width = |line_idx: usize, block_idx: usize| -> f32 {
let val = (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin()
* 0.5
+ 0.5;
0.05 + val * 0.2
};
let indentation = |line_idx: usize| -> f32 {
let step = line_idx % 6;
if step < 3 {
step as f32 * 0.1
} else {
(5 - step) as f32 * 0.1
}
};
let pick_color = |line_idx: usize, block_idx: usize| -> Hsla {
let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin()
* 3.5)
.abs() as usize
% syntax_colors.len();
syntax_colors[idx].unwrap_or(colors.text)
};
let line_count = 13;
let lines = (0..line_count)
.map(|line_idx| {
let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5)
* 3.0)
.round() as usize
+ 2;
let indent = indentation(line_idx);
let blocks = (0..block_count)
.map(|block_idx| {
let width = line_width(line_idx, block_idx);
let color = pick_color(line_idx, block_idx);
item_skeleton(relative(width).into(), skeleton_height, color)
})
.collect::<Vec<_>>();
h_flex().gap(px(2.)).ml(relative(indent)).children(blocks)
})
.collect::<Vec<_>>();
v_flex()
.size_full()
.p_1()
.gap(px(6.))
.children(lines)
.into_any_element()
};
let pane = div()
.h_full()
.flex_grow()
.flex()
.flex_col()
// .child(
// div()
// .w_full()
// .border_color(color.border)
// .border_b(px(1.))
// .h(relative(0.1))
// .bg(color.tab_bar_background),
// )
.child(
div()
.size_full()
.overflow_hidden()
.bg(color.editor_background)
.p_2()
.child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
);
let content = div().size_full().flex().child(sidebar).child(pane);
div()
.size_full()
.rounded(root_radius)
.p(root_padding)
.border(root_border)
.border_color(color.border_transparent)
.when(self.selected, |this| {
this.border_color(color.border_selected)
})
.child(
div()
.size_full()
.rounded(inner_radius)
.border(child_border)
.border_color(color.border)
.bg(color.background)
.child(content),
)
}
}
impl Component for ThemePreviewTile {
fn description() -> Option<&'static str> {
Some(Self::DOCS)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let theme_registry = ThemeRegistry::global(cx);
let one_dark = theme_registry.get("One Dark");
let one_light = theme_registry.get("One Light");
let gruvbox_dark = theme_registry.get("Gruvbox Dark");
let gruvbox_light = theme_registry.get("Gruvbox Light");
let themes_to_preview = vec![
one_dark.clone().ok(),
one_light.clone().ok(),
gruvbox_dark.clone().ok(),
gruvbox_light.clone().ok(),
]
.into_iter()
.flatten()
.collect::<Vec<_>>();
Some(
v_flex()
.gap_6()
.p_4()
.children({
if let Some(one_dark) = one_dark.ok() {
vec![example_group(vec![
single_example(
"Default",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark.clone(), false, 0.42))
.into_any_element(),
),
single_example(
"Selected",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark, true, 0.42))
.into_any_element(),
),
])]
} else {
vec![]
}
})
.child(
example_group(vec![single_example(
"Default Themes",
h_flex()
.gap_4()
.children(
themes_to_preview
.iter()
.enumerate()
.map(|(i, theme)| {
div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new(
theme.clone(),
false,
0.42,
))
})
.collect::<Vec<_>>(),
)
.into_any_element(),
)])
.grow(),
)
.into_any_element(),
)
}
}