From da39d0a9149c4fbfe5f039e62e888ac56213823f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 1 Oct 2026 12:32:06 -0400 Subject: [PATCH] =?UTF-8?q?T48e=20Docs=20=C2=B7=20Theme:=20reading=20token?= =?UTF-8?q?s,=20the=20calls=20that=20change=20them,=20an=20owner's=20palet?= =?UTF-8?q?te,=20chart=20hues;=20the=20theme=20example=20compiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 ++ examples/docs/theme.rs | 114 ++++++++++++++++++++++++++++++++++++ frontend/src/docs/guides.ts | 2 +- frontend/src/docs/theme.md | 46 +++++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 examples/docs/theme.rs create mode 100644 frontend/src/docs/theme.md diff --git a/Cargo.toml b/Cargo.toml index 3f6ffc2..d0f06ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,3 +95,7 @@ path = "examples/docs/hello.rs" [[example]] name = "docs_start" path = "examples/docs/start.rs" + +[[example]] +name = "docs_theme" +path = "examples/docs/theme.rs" diff --git a/examples/docs/theme.rs b/examples/docs/theme.rs new file mode 100644 index 0000000..b362a92 --- /dev/null +++ b/examples/docs/theme.rs @@ -0,0 +1,114 @@ +use ely_gpui_component::{ + Assets, + buttons::Button, + primitives::FocusScope, + theme::{ActiveTheme, Density, Elevation, Mode, Radius, TextSize, Theme}, + typography::Paragraph, +}; +use gpui::{ + App, AppContext, Context, FocusHandle, IntoElement, ParentElement, Render, Styled, Window, + WindowOptions, div, rgb, +}; + +struct Tokens { + focus: FocusHandle, +} + +impl Render for Tokens { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let theme = cx.theme(); + let dark = theme.is_dark(); + let contrast = theme.high_contrast(); + let compact = theme.density == Density::Compact; + let card = div() + .flex() + .flex_col() + .gap_2() + .p_4() + .bg(theme.colors.surface) + .border_1() + .border_color(theme.colors.border) + .rounded(theme.radius(Radius::Lg)) + .shadow(theme.elevation(Elevation::Floating)) + .text_size(theme.text_size(TextSize::Lg)) + .text_color(theme.colors.fg) + .child("A card made of tokens") + .child(Paragraph::new( + "Its colors, radius, shadow and text size come from the theme.", + )); + let controls = div() + .flex() + .flex_wrap() + .gap_2() + .child( + Button::new("mode", if dark { "Light mode" } else { "Dark mode" }).on_click( + move |_, _, cx| { + Theme::set_mode(if dark { Mode::Light } else { Mode::Dark }, cx) + }, + ), + ) + .child( + Button::new( + "contrast", + if contrast { + "Usual contrast" + } else { + "High contrast" + }, + ) + .on_click(move |_, _, cx| Theme::set_high_contrast(!contrast, cx)), + ) + .child( + Button::new( + "density", + if compact { + "Standard density" + } else { + "Compact" + }, + ) + .on_click(move |_, _, cx| { + Theme::update(cx, |theme| { + theme.density = if compact { + Density::Standard + } else { + Density::Compact + }; + }) + }), + ) + .child(Button::new("accent", "Green accent").on_click(|_, _, cx| { + let mode = cx.theme().mode(); + let mut palette = cx.theme().palette(); + *palette.token_mut("accent") = rgb(0x2f7d4f).into(); + Theme::set_palette(mode, Some(palette), cx); + })); + FocusScope::new(&self.focus) + .root() + .size_full() + .flex() + .flex_col() + .gap_6() + .p_8() + .bg(theme.colors.bg) + .child(card) + .child(controls) + } +} + +fn main() { + gpui_platform::application() + .with_assets(Assets) + .run(|cx: &mut App| { + ely_gpui_component::init(cx); + cx.open_window(WindowOptions::default(), |window, cx| { + cx.new(|cx| { + let focus = cx.focus_handle(); + window.focus(&focus, cx); + Tokens { focus } + }) + }) + .expect("window failed to open"); + cx.activate(true); + }); +} diff --git a/frontend/src/docs/guides.ts b/frontend/src/docs/guides.ts index 766d884..64c953b 100644 --- a/frontend/src/docs/guides.ts +++ b/frontend/src/docs/guides.ts @@ -6,7 +6,7 @@ import { Marked, type Tokens } from "marked"; const at = (relative: string) => fileURLToPath(new URL(relative, import.meta.url)); /** The guides in reading order, by file name. */ -const ORDER = ["introduction", "installation", "getting-started"]; +const ORDER = ["introduction", "installation", "getting-started", "theme"]; export interface Guide { /** The path segment; the introduction has none. */ diff --git a/frontend/src/docs/theme.md b/frontend/src/docs/theme.md new file mode 100644 index 0000000..ed6a7d4 --- /dev/null +++ b/frontend/src/docs/theme.md @@ -0,0 +1,46 @@ +# Theme + +One global `Theme` holds the mode, the palette every component paints with, and the tokens for size, radius and shadow. Read it with `cx.theme()`; change it through `Theme`'s functions, and every window repaints. + +## Read the theme + +Bring `ActiveTheme` into scope and call `cx.theme()` on `App`, or on a `Context`, which derefs to `App`. + +| Item | Holds | +| --- | --- | +| `colors` | The `Palette`: 36 named colors (`bg`, `surface`, `fg`, `fg_muted`, `accent`, `border`, `focus`, `danger` and the rest), with `chart`, `ansi` and `syntax`. | +| `text_size(TextSize)` | `Xs` 11px to `Display` 32px, times `font_scale`. | +| `radius(Radius)` | `Sm` 4px, `Md` 6px, `Lg` 8px, `Xl` 12px, times `radius_scale`. | +| `elevation(Elevation)` | Shadows for `Raised`, `Floating` and `Modal`; darker in dark mode. | +| `control_height(ControlSize)` | `Sm` 24px, `Md` 28px, `Lg` 32px; 4px less under `Density::Compact`, 4px more under `Comfortable`. | +| `is_dark()`, `high_contrast()`, `color_blind_safe()` | The current switches. | + +Sizes come back as `Rems`, so they follow the window's rem size. + +## Change the theme + +| Call | Does | +| --- | --- | +| `Theme::set_mode(mode, cx)` | Cross-fades the palette to `Mode::Light` or `Mode::Dark`. | +| `Theme::set_mode_now(mode, cx)` | Switches at once. Call it before a window opens to set its first frame. | +| `Theme::set_high_contrast(on, cx)` | Muted text and borders move further from the page, and glass turns opaque, in Ely's palettes. | +| `Theme::set_color_blind_safe(on, cx)` | Chart hues that stay apart under protanopia, deuteranopia and tritanopia. | +| `Theme::set_palette(mode, palette, cx)` | An owner's palette for one mode; `None` brings Ely's back. | +| `Theme::update(cx, edit)` | Edits the other fields: `density`, `font_scale`, `radius_scale`, `font_family`, `mono_family`, `reduced_motion`. | + +The color calls log at the `info` level, so a theme change shows in the app's log. + +## An owner's palette + +`cx.theme().palette()` returns the palette the shown mode fades to. Change a color by its field's name with `token_mut`, then hand the palette back for that mode. A name the palette lacks panics, naming it. High contrast and color-blind safe charts change Ely's own palettes, never an owner's. + +## Chart hues + +`colors.chart` holds eight hues. `HUE_NAMES` names them in order: Blue, Teal, Ochre, Rose, Violet, Green, Rust, Cyan. `colors.hue(index, owner)` returns one and panics past the eighth, naming `owner`, so an event or a label can wear the same hue a chart does. + +## A card made of tokens + +The example draws a card from tokens alone, with buttons that turn the mode, the contrast, the density and the accent. + +```rust example=theme +```