Rebuild /settings/appearance as the design form
The appearance route now opens with the design layout: serif "GENERAL" overline + "Appearance" headline + intro paragraph, a four-tile wallpaper picker (Dawn/Violet/Mint/Slate with active outline + check glyph), a theme-mode segmented control, an accent swatch row, a reduce-motion toggle, and a reset row. Every control mutates real state through new shell methods (set_wallpaper_theme, set_theme_mode, toggle_reduce_motion, reset_appearance) which delegate to the core. The chrome lives in chrome::appearance_form so the page module is a six-line shim. Light/Dark theme buttons currently switch the persisted mode; rendering swaps will land when light/dark token sheets ship — keeping the persistence so the eventual flip is one place.
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::{ThemeMode, WallpaperTheme};
|
||||
use gpui::{
|
||||
AnyElement, Context, FontWeight, Hsla, InteractiveElement, IntoElement, ParentElement,
|
||||
SharedString, StatefulInteractiveElement, Styled, div, hsla, linear_color_stop,
|
||||
linear_gradient, prelude::FluentBuilder, px, rgb, rgba,
|
||||
};
|
||||
use gpui_component::{IconName, scroll::ScrollableElement};
|
||||
|
||||
use crate::shell::ElyShell;
|
||||
|
||||
pub(crate) fn render_appearance_form(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.overflow_y_scrollbar()
|
||||
.pt(px(28.0))
|
||||
.px(px(40.0))
|
||||
.pb(px(32.0))
|
||||
.child(
|
||||
div()
|
||||
.max_w(px(780.0))
|
||||
.mx_auto()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(28.0))
|
||||
.child(render_header())
|
||||
.child(render_wallpaper_grid(snapshot, cx))
|
||||
.child(render_appearance_rows(snapshot, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_header() -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(6.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(rgb(colors::INK_4))
|
||||
.child("GENERAL"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(34.0))
|
||||
.font_weight(FontWeight(400.0))
|
||||
.text_color(rgb(colors::INK))
|
||||
.child("Appearance"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.max_w(px(520.0))
|
||||
.text_size(px(13.0))
|
||||
.text_color(rgb(colors::INK_3))
|
||||
.child(
|
||||
"Tune the atmosphere of your browser. ELY's wallpaper sets the ambient \
|
||||
palette of every surface; pick one and let it breathe.",
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_wallpaper_grid(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let active = snapshot.appearance.wallpaper();
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(4)
|
||||
.gap(px(10.0))
|
||||
.child(render_wallpaper_swatch(WallpaperTheme::Dawn, active, cx))
|
||||
.child(render_wallpaper_swatch(WallpaperTheme::Violet, active, cx))
|
||||
.child(render_wallpaper_swatch(WallpaperTheme::Mint, active, cx))
|
||||
.child(render_wallpaper_swatch(WallpaperTheme::Slate, active, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_wallpaper_swatch(
|
||||
theme: WallpaperTheme,
|
||||
active: WallpaperTheme,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let label = wallpaper_label(theme);
|
||||
let id = SharedString::from(format!("wallpaper-{}", label.to_ascii_lowercase()));
|
||||
let selected = theme == active;
|
||||
let (upper, lower, base) = swatch_colors(theme);
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(8.0))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.opacity(0.94))
|
||||
.active(|style| style.opacity(0.85))
|
||||
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||
shell.set_wallpaper_theme(theme, cx);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.h(px(96.0))
|
||||
.rounded(px(10.0))
|
||||
.when(selected, |el| {
|
||||
el.border_2().border_color(rgb(colors::ACCENT))
|
||||
})
|
||||
.when(!selected, |el| {
|
||||
el.border_1().border_color(rgba(colors::STROKE))
|
||||
})
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.bg(rgb(base))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(linear_gradient(
|
||||
225.0,
|
||||
linear_color_stop(upper, 0.0),
|
||||
linear_color_stop(transparent_like(upper), 0.6),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.bg(linear_gradient(
|
||||
45.0,
|
||||
linear_color_stop(lower, 0.0),
|
||||
linear_color_stop(transparent_like(lower), 0.6),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(6.0))
|
||||
.text_size(px(12.0))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK))
|
||||
.child(label),
|
||||
)
|
||||
.when(selected, |el| {
|
||||
el.child(
|
||||
div()
|
||||
.text_color(rgb(colors::ACCENT))
|
||||
.child(IconName::Check),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_appearance_rows(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let appearance = snapshot.appearance;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(render_theme_mode_row(appearance.theme_mode(), cx))
|
||||
.child(render_accent_row())
|
||||
.child(render_reduce_motion_row(appearance.reduce_motion(), cx))
|
||||
.child(render_reset_row(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_theme_mode_row(
|
||||
active: ThemeMode,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
settings_row(
|
||||
"Theme mode",
|
||||
"Match system appearance, or pick one to lock.",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(2.0))
|
||||
.p(px(2.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(SEGMENT_BG))
|
||||
.child(theme_segment("System", ThemeMode::System, active, cx))
|
||||
.child(theme_segment("Light", ThemeMode::Light, active, cx))
|
||||
.child(theme_segment("Dark", ThemeMode::Dark, active, cx))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn theme_segment(
|
||||
label: &'static str,
|
||||
mode: ThemeMode,
|
||||
active: ThemeMode,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let selected = mode == active;
|
||||
let bg = if selected { 0xffffffff } else { 0x00000000 };
|
||||
let id = SharedString::from(format!("theme-mode-{}", label.to_ascii_lowercase()));
|
||||
|
||||
div()
|
||||
.id(id)
|
||||
.px(px(12.0))
|
||||
.py(px(4.0))
|
||||
.rounded(px(6.0))
|
||||
.text_size(px(12.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK_2))
|
||||
.bg(rgba(bg))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.opacity(0.92))
|
||||
.active(|style| style.opacity(0.82))
|
||||
.on_click(cx.listener(move |shell, _, _, cx| {
|
||||
shell.set_theme_mode(mode, cx);
|
||||
}))
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_accent_row() -> AnyElement {
|
||||
settings_row(
|
||||
"Accent color",
|
||||
"Used across selection, focus rings & links.",
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(swatch(colors::ACCENT, true))
|
||||
.child(swatch(0xec6a8e, false))
|
||||
.child(swatch(0x7c6cf7, false))
|
||||
.child(swatch(0x4fb59a, false))
|
||||
.child(swatch(colors::INK, false))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn swatch(color: u32, selected: bool) -> AnyElement {
|
||||
let mut element = div()
|
||||
.size(px(22.0))
|
||||
.rounded_full()
|
||||
.bg(rgb(color))
|
||||
.border_1()
|
||||
.border_color(rgba(0x0000000d));
|
||||
if selected {
|
||||
element = element
|
||||
.border_2()
|
||||
.border_color(rgb(colors::ACCENT));
|
||||
}
|
||||
element.into_any_element()
|
||||
}
|
||||
|
||||
fn render_reduce_motion_row(reduce: bool, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
settings_row(
|
||||
"Reduce motion",
|
||||
"Mute transitions and ambient effects.",
|
||||
div()
|
||||
.id(SharedString::from("toggle-reduce-motion"))
|
||||
.w(px(34.0))
|
||||
.h(px(20.0))
|
||||
.rounded_full()
|
||||
.bg(if reduce { rgb(colors::ACCENT) } else { rgba(0x281e1426) })
|
||||
.p(px(2.0))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.opacity(0.9))
|
||||
.active(|style| style.opacity(0.78))
|
||||
.on_click(cx.listener(|shell, _, _, cx| shell.toggle_reduce_motion(cx)))
|
||||
.child(
|
||||
div()
|
||||
.size(px(16.0))
|
||||
.rounded_full()
|
||||
.bg(rgb(0xffffff))
|
||||
.when(reduce, |el| el.ml(px(14.0))),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_reset_row(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
settings_row(
|
||||
"Restore defaults",
|
||||
"Reset wallpaper, theme mode, and motion to defaults.",
|
||||
div()
|
||||
.id(SharedString::from("appearance-reset"))
|
||||
.px(px(12.0))
|
||||
.py(px(7.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(SEGMENT_BG))
|
||||
.text_size(px(12.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK_2))
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.opacity(0.92))
|
||||
.active(|style| style.opacity(0.82))
|
||||
.on_click(cx.listener(|shell, _, _, cx| shell.reset_appearance(cx)))
|
||||
.child("Reset")
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_row(title: &'static str, detail: &'static str, control: AnyElement) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(16.0))
|
||||
.py(px(14.0))
|
||||
.border_b_1()
|
||||
.border_color(rgba(colors::DIVIDER))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::INK))
|
||||
.child(title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(rgb(colors::INK_3))
|
||||
.child(detail),
|
||||
),
|
||||
)
|
||||
.child(control)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn wallpaper_label(theme: WallpaperTheme) -> &'static str {
|
||||
match theme {
|
||||
WallpaperTheme::Dawn => "Dawn",
|
||||
WallpaperTheme::Violet => "Violet",
|
||||
WallpaperTheme::Mint => "Mint",
|
||||
WallpaperTheme::Slate => "Slate",
|
||||
}
|
||||
}
|
||||
|
||||
fn swatch_colors(theme: WallpaperTheme) -> (Hsla, Hsla, u32) {
|
||||
match theme {
|
||||
WallpaperTheme::Dawn => (
|
||||
hsla(351.0 / 360.0, 1.0, 0.91, 0.95),
|
||||
hsla(228.0 / 360.0, 1.0, 0.86, 0.85),
|
||||
0xefe8e1,
|
||||
),
|
||||
WallpaperTheme::Violet => (
|
||||
hsla(263.0 / 360.0, 1.0, 0.88, 0.95),
|
||||
hsla(33.0 / 360.0, 1.0, 0.87, 0.85),
|
||||
0xe9e2ee,
|
||||
),
|
||||
WallpaperTheme::Mint => (
|
||||
hsla(146.0 / 360.0, 0.69, 0.85, 0.95),
|
||||
hsla(40.0 / 360.0, 1.0, 0.86, 0.85),
|
||||
0xe6ece2,
|
||||
),
|
||||
WallpaperTheme::Slate => (
|
||||
hsla(218.0 / 360.0, 0.20, 0.85, 0.95),
|
||||
hsla(20.0 / 360.0, 0.0, 0.10, 0.40),
|
||||
0xd6dae3,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn transparent_like(color: Hsla) -> Hsla {
|
||||
hsla(color.h, color.s, color.l, 0.0)
|
||||
}
|
||||
|
||||
const SEGMENT_BG: u32 = 0x281e140d;
|
||||
Reference in New Issue
Block a user