welcome: Theme preview tile (#29689)
 - 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:
parent
84e4891d54
commit
8c03934b26
9 changed files with 361 additions and 6 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -16878,18 +16878,22 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"client",
|
"client",
|
||||||
|
"component",
|
||||||
"db",
|
"db",
|
||||||
|
"documented",
|
||||||
"editor",
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"install_cli",
|
"install_cli",
|
||||||
"language",
|
"language",
|
||||||
|
"linkme",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"settings",
|
"settings",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
"util",
|
||||||
"vim_mode_setting",
|
"vim_mode_setting",
|
||||||
|
|
|
@ -435,6 +435,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a0
|
||||||
dashmap = "6.0"
|
dashmap = "6.0"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
|
documented = "0.9.1"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
ec4rs = "1.1"
|
ec4rs = "1.1"
|
||||||
emojis = "0.6.1"
|
emojis = "0.6.1"
|
||||||
|
@ -797,5 +798,6 @@ ignored = [
|
||||||
"serde",
|
"serde",
|
||||||
"component",
|
"component",
|
||||||
"linkme",
|
"linkme",
|
||||||
|
"documented",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,6 +15,7 @@ path = "src/ui.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
documented.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
icons.workspace = true
|
icons.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
@ -28,7 +29,6 @@ strum.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui_macros.workspace = true
|
ui_macros.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
documented = "0.9.1"
|
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|
|
@ -4,11 +4,13 @@ use gpui::App;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
mod color_contrast;
|
mod color_contrast;
|
||||||
|
mod corner_solver;
|
||||||
mod format_distance;
|
mod format_distance;
|
||||||
mod search_input;
|
mod search_input;
|
||||||
mod with_rem_size;
|
mod with_rem_size;
|
||||||
|
|
||||||
pub use color_contrast::*;
|
pub use color_contrast::*;
|
||||||
|
pub use corner_solver::{CornerSolver, inner_corner_radius};
|
||||||
pub use format_distance::*;
|
pub use format_distance::*;
|
||||||
pub use search_input::*;
|
pub use search_input::*;
|
||||||
pub use with_rem_size::*;
|
pub use with_rem_size::*;
|
||||||
|
|
61
crates/ui/src/utils/corner_solver.rs
Normal file
61
crates/ui/src/utils/corner_solver.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use gpui::Pixels;
|
||||||
|
|
||||||
|
/// Calculates the child’s 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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,23 +17,27 @@ test-support = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
component.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
|
documented.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
install_cli.workspace = true
|
install_cli.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
linkme.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
vim_mode_setting.workspace = true
|
vim_mode_setting.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
workspace-hack.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
mod base_keymap_picker;
|
|
||||||
mod base_keymap_setting;
|
|
||||||
mod multibuffer_hint;
|
|
||||||
|
|
||||||
use client::{TelemetrySettings, telemetry::Telemetry};
|
use client::{TelemetrySettings, telemetry::Telemetry};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -24,6 +20,11 @@ use workspace::{
|
||||||
pub use base_keymap_setting::BaseKeymap;
|
pub use base_keymap_setting::BaseKeymap;
|
||||||
pub use multibuffer_hint::*;
|
pub use multibuffer_hint::*;
|
||||||
|
|
||||||
|
mod base_keymap_picker;
|
||||||
|
mod base_keymap_setting;
|
||||||
|
mod multibuffer_hint;
|
||||||
|
mod welcome_ui;
|
||||||
|
|
||||||
actions!(welcome, [ResetHints]);
|
actions!(welcome, [ResetHints]);
|
||||||
|
|
||||||
pub const FIRST_OPEN: &str = "first_open";
|
pub const FIRST_OPEN: &str = "first_open";
|
||||||
|
|
1
crates/welcome/src/welcome_ui.rs
Normal file
1
crates/welcome/src/welcome_ui.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mod theme_preview;
|
280
crates/welcome/src/welcome_ui/theme_preview.rs
Normal file
280
crates/welcome/src/welcome_ui/theme_preview.rs
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue