Theme · Platform: one platform for buttons and caps, icon themes, frosted panes
theme::Platform moves out of typography and shell's ControlsStyle folds into it; the theme's platform starts as the system's. Window buttons Ely draws, the title bar's layout and every key cap follow it, secondary included, through typography::keys::keystroke; a title bar whose buttons the OS draws follows the OS. primitives::IconTheme lays an owner's rules by file name and extension over Ely's map, for every file icon. shell::Vibrancy fills a pane with the palette's new glass, translucent so a window opened Blurred shows its backdrop, solid under high contrast. gpui 0.2.2 has no Mica; that line stays blocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53578e0525
commit
f3c8cca4c9
30 files changed
+593
-162
No files matched your search
@@ -127,7 +127,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- A file on its way is a `files::Transfer` drawn by one row in `files::transfer`: TransferQueue and DownloadManager share it, and its actions follow its state.
|
||||
- Paths with `/` between folders become a tree through `lists::Folder::of`, for `FileTree` and `files::ArchiveViewer` alike.
|
||||
- A folder's path over its entries is `lists::path_bar`, and its first order is `lists::listed`, folders first by name: DirectoryListing and FileExplorer share both, so every view shows one path and one order.
|
||||
- A file's icon comes from `primitives::file_icon`, one map from name to icon for every chapter; `files::FileIcon` draws it. What fits across a width comes from `layout::columns_for`, and `layout::fit` adds the inset that centers it.
|
||||
- A file's icon comes from `primitives::file_icon`, one map from name to icon for every chapter, under the owner's `primitives::IconTheme`, whose rules by name and extension come first; `files::FileIcon` draws it. What fits across a width comes from `layout::columns_for`, and `layout::fit` adds the inset that centers it.
|
||||
- Badges, tags and avatars live in `data_display`; forms and shell draw theirs from there, and `data_display` imports neither. A hue worn in a row, a label's or an event's, is `data_display::color_mark`, and `Palette::hue` gives the chart color by index. Avatars take square pictures. A tag is never wider than its container; a long label ends in an ellipsis beside its remove mark.
|
||||
- A picture drawn in code reaches gpui as a `RenderImage`. A host that replaces one frees the old through `App::drop_image`, or every frame stays in the sprite atlas (the gallery's model).
|
||||
- `img()` keeps loading state, and moves an animated picture's frames, only with an id. Content masks are rectangles, so rounded corners survive only when the image fills its box without cropping.
|
||||
@@ -185,6 +185,8 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- A choice that names a person leads with their initials through `forms::Choice::avatar`, in the list and in a Select's trigger alike (`project::AssigneePicker`).
|
||||
- `typography::Ellipsis` shapes its line with the text style's own run, so a strike or an underline set on its box shows (`lists::ListItem::struck`, a closed task's title).
|
||||
- Focus that must follow an item to another parent is taken by the owner and keyed by the item: a handle gpui makes for an element dies with its path (`project::KanbanBoard`, whose cards keep focus across columns).
|
||||
- The theme's `platform`, the system's to start, decides the window buttons Ely draws, the title bar's layout and key caps, `secondary` included (`typography::keys::keystroke`). A title bar whose buttons the OS draws follows the OS.
|
||||
- A pane the window's blur shows through is `shell::Vibrancy`, filled with the palette's `glass`, in a window opened `WindowBackgroundAppearance::Blurred`: vibrancy on macOS, acrylic on Windows. gpui 0.2.2 has no Mica.
|
||||
- The chart's hues go by `theme::HUE_NAMES`, for an event's color and a label's alike.
|
||||
- Text whose every character counts, encoded data and keys, goes through `typography::literal`: JetBrains Mono draws `==` and `->` as ligatures (`devtools::Encoder`).
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::IconButton,
|
||||
primitives::IconName,
|
||||
shell::{ControlsStyle, TitleBar, WindowControls, drag_region},
|
||||
theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize},
|
||||
shell::{TitleBar, WindowControls, drag_region},
|
||||
theme::{ActiveTheme, ControlSize, Elevation, Platform, Radius, TextSize},
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{App, Div, IntoElement, ParentElement, Styled, Window, div, px};
|
||||
@@ -25,9 +25,9 @@ pub fn window_frame(width: f32, height: f32, cx: &App) -> Div {
|
||||
.shadow(theme.elevation(Elevation::Raised))
|
||||
}
|
||||
|
||||
fn demo_bar(id: &'static str, style: ControlsStyle) -> TitleBar {
|
||||
fn demo_bar(id: &'static str, style: Platform) -> TitleBar {
|
||||
TitleBar::new(id)
|
||||
.style(style)
|
||||
.platform(style)
|
||||
.title("Ely — Projects")
|
||||
.on_close(|_, _| log::info!("gallery: demo close refused"))
|
||||
.child(IconButton::new((id, 1usize), IconName::PanelLeft).size(ControlSize::Sm))
|
||||
@@ -36,9 +36,9 @@ fn demo_bar(id: &'static str, style: ControlsStyle) -> TitleBar {
|
||||
|
||||
pub fn title_bar(cx: &App) -> impl IntoElement + use<> {
|
||||
let styles = [
|
||||
("tb-mac", ControlsStyle::Mac, "macOS"),
|
||||
("tb-windows", ControlsStyle::Windows, "Windows"),
|
||||
("tb-linux", ControlsStyle::Linux, "Linux"),
|
||||
("tb-mac", Platform::Mac, "macOS"),
|
||||
("tb-windows", Platform::Windows, "Windows"),
|
||||
("tb-linux", Platform::Linux, "Linux"),
|
||||
];
|
||||
section(
|
||||
"TitleBar",
|
||||
@@ -88,7 +88,7 @@ pub fn window_controls(cx: &App) -> impl IntoElement + use<> {
|
||||
"Mac",
|
||||
plate().child(
|
||||
WindowControls::new("wc-mac")
|
||||
.style(ControlsStyle::Mac)
|
||||
.platform(Platform::Mac)
|
||||
.on_close(|_, _| log::info!("gallery: demo close refused")),
|
||||
),
|
||||
cx,
|
||||
@@ -97,7 +97,7 @@ pub fn window_controls(cx: &App) -> impl IntoElement + use<> {
|
||||
"Windows",
|
||||
plate().px_0().child(
|
||||
WindowControls::new("wc-windows")
|
||||
.style(ControlsStyle::Windows)
|
||||
.platform(Platform::Windows)
|
||||
.on_close(|_, _| log::info!("gallery: demo close refused")),
|
||||
),
|
||||
cx,
|
||||
@@ -106,7 +106,7 @@ pub fn window_controls(cx: &App) -> impl IntoElement + use<> {
|
||||
"Linux",
|
||||
plate().child(
|
||||
WindowControls::new("wc-linux")
|
||||
.style(ControlsStyle::Linux)
|
||||
.platform(Platform::Linux)
|
||||
.on_close(|_, _| log::info!("gallery: demo close refused")),
|
||||
),
|
||||
cx,
|
||||
|
||||
@@ -11,8 +11,10 @@ use gpui::{
|
||||
|
||||
mod editing;
|
||||
mod importing;
|
||||
mod platform;
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
use crate::ui::{section, specimen, specimens};
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
@@ -21,7 +23,12 @@ pub const PAGE: Page = Page {
|
||||
title: "Theme",
|
||||
summary: "Tokens every component reads. Light and dark share one grammar.",
|
||||
render,
|
||||
script: &[],
|
||||
script: &[
|
||||
Step::Click("frosted-open"),
|
||||
Step::Wait(700),
|
||||
Step::ShotWindow("frosted", "frosted-window"),
|
||||
Step::CloseWindow("frosted"),
|
||||
],
|
||||
};
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
@@ -149,6 +156,7 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
)
|
||||
.child(editing::editor(window, cx))
|
||||
.child(importing::importing(window, cx))
|
||||
.child(platform::platform(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, SegmentedControl},
|
||||
files::FileIcon,
|
||||
forms::Switch,
|
||||
primitives::{IconName, IconTheme},
|
||||
shell::{Vibrancy, WindowControls},
|
||||
theme::{ActiveTheme, Platform, Theme},
|
||||
typography::{Kbd, KbdCombo},
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, IntoElement, ParentElement, Render, Styled, TitlebarOptions,
|
||||
Window, WindowBackgroundAppearance, WindowBounds, WindowOptions, div, point, px, size,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
probe::{Opened, probe},
|
||||
ui::{change, keep, section, specimen, specimens},
|
||||
};
|
||||
|
||||
const PLATFORMS: [(Platform, &str); 3] = [
|
||||
(Platform::Mac, "macOS"),
|
||||
(Platform::Windows, "Windows"),
|
||||
(Platform::Linux, "Linux"),
|
||||
];
|
||||
|
||||
/// Whether the demo's own icon rules are on.
|
||||
struct Owned(bool);
|
||||
|
||||
pub fn platform(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
section(
|
||||
"Platform Style · IconThemeProvider · VibrancyBackground",
|
||||
"The theme's platform decides window buttons and key caps, and starts as the system's. An icon theme lays an owner's rules over Ely's file icons. A frosted pane lets a blurred window's backdrop through; gpui 0.2.2 blurs as vibrancy on macOS and acrylic on Windows, and has no Mica.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.child(specimen("Platform", platforms(cx), cx))
|
||||
.child(specimen("IconTheme", icons(window, cx), cx))
|
||||
.child(specimen(
|
||||
"Vibrancy",
|
||||
probe(
|
||||
"frosted-open",
|
||||
Button::new("frosted-open", "Open a frosted window")
|
||||
.on_click(|_, _, cx| frosted(cx)),
|
||||
),
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn platforms(cx: &App) -> impl IntoElement + use<> {
|
||||
let shown = cx.theme().platform;
|
||||
let label = PLATFORMS
|
||||
.iter()
|
||||
.find(|(platform, _)| *platform == shown)
|
||||
.map(|(_, label)| *label)
|
||||
.expect("a listed platform");
|
||||
let control = PLATFORMS.iter().fold(
|
||||
SegmentedControl::new("platform", label),
|
||||
|control, (_, label)| control.segment(*label, *label, None),
|
||||
);
|
||||
div()
|
||||
.w(px(320.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(control.on_change(|picked, _, cx| {
|
||||
let (platform, _) = PLATFORMS
|
||||
.iter()
|
||||
.find(|(_, label)| *label == picked.as_ref())
|
||||
.expect("a listed platform");
|
||||
Theme::update(cx, |theme| theme.platform = *platform);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_3()
|
||||
.child(WindowControls::new("platform-controls"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(Kbd::new("secondary-s"))
|
||||
.child(KbdCombo::new("secondary-k secondary-s")),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn icons(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let owned = keep("icon-theme", || Owned(false), window, cx);
|
||||
let on = owned.read(cx).0;
|
||||
let muted = cx.theme().colors.fg_muted;
|
||||
let rows = ["Cargo.toml", "README.md", "main.rs", "logo.svg"].map(|name| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(FileIcon::file(name))
|
||||
.child(div().text_color(muted).child(name))
|
||||
});
|
||||
div()
|
||||
.w(px(240.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Switch::new("icon-theme", on)
|
||||
.label("The owner's icons")
|
||||
.on_change(move |on, _, cx| {
|
||||
let theme = if on {
|
||||
IconTheme::default()
|
||||
.name("Cargo.toml", IconName::Package)
|
||||
.name("README.md", IconName::BookOpen)
|
||||
.extension("rs", IconName::Braces)
|
||||
} else {
|
||||
IconTheme::default()
|
||||
};
|
||||
theme.apply(cx);
|
||||
change(&owned, cx, |owned| owned.0 = on);
|
||||
}),
|
||||
)
|
||||
.children(rows)
|
||||
}
|
||||
|
||||
fn frosted(cx: &mut App) {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||
None,
|
||||
size(px(560.), px(340.)),
|
||||
cx,
|
||||
))),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some("Frosted".into()),
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(point(px(16.0), px(18.0))),
|
||||
}),
|
||||
window_background: WindowBackgroundAppearance::Blurred,
|
||||
..Default::default()
|
||||
};
|
||||
let handle = cx
|
||||
.open_window(options, |_, cx| cx.new(|_| Frosted))
|
||||
.expect("the frosted window opens");
|
||||
Opened::insert("frosted", handle.into(), cx);
|
||||
}
|
||||
|
||||
/// A window whose sidebar lets the desktop through.
|
||||
struct Frosted;
|
||||
|
||||
impl Render for Frosted {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.font_family(theme.font_family.clone())
|
||||
.text_color(colors.fg)
|
||||
.child(
|
||||
Vibrancy::new()
|
||||
.w(px(180.))
|
||||
.h_full()
|
||||
.pt_12()
|
||||
.px_3()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.children(
|
||||
["Library", "Recents", "Shared"]
|
||||
.map(|name| div().px_2().py_1().child(name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.p_6()
|
||||
.pt_12()
|
||||
.bg(colors.bg)
|
||||
.border_l_1()
|
||||
.border_color(colors.border)
|
||||
.min_w_0()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(
|
||||
div().child("The sidebar lets the desktop through; this pane stays solid."),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ impl RenderOnce for FileChangeCard {
|
||||
})
|
||||
.child(Disclosure::new((self.id.clone(), "chevron"), open).size(IconSize::Sm))
|
||||
.child(
|
||||
Icon::new(file_icon(&self.change.path))
|
||||
Icon::new(file_icon(&self.change.path, cx))
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.fg_muted),
|
||||
)
|
||||
|
||||
+16
-12
@@ -1,13 +1,13 @@
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, FocusHandle, FontWeight, Hsla, IntoElement, Keystroke, MouseButton,
|
||||
App, ClickEvent, ElementId, FocusHandle, FontWeight, Hsla, IntoElement, MouseButton,
|
||||
RenderOnce, SharedString, Window, div, prelude::*, transparent_black,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
motion::Spinner,
|
||||
primitives::{FocusRing, Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Mix, Palette, Radius, TextSize},
|
||||
typography::keys::{Platform, keystroke_labels},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Mix, Palette, Platform, Radius, TextSize},
|
||||
typography::keys::{keystroke, keystroke_labels},
|
||||
};
|
||||
|
||||
pub(crate) type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
@@ -112,11 +112,15 @@ pub(crate) enum Slot {
|
||||
Last,
|
||||
}
|
||||
|
||||
/// A keystroke as the platform spells it: `⌘S`, or `Ctrl+S`.
|
||||
pub(crate) fn shortcut_text(stroke: &Keystroke) -> String {
|
||||
let platform = Platform::current();
|
||||
/// Fails on a keystroke gpui cannot read, when a builder takes it.
|
||||
pub(crate) fn keystroke_of(source: &str) {
|
||||
keystroke(source, Platform::current());
|
||||
}
|
||||
|
||||
/// A keystroke in gpui key syntax as `platform` spells it: `⌘S`, or `Ctrl+S`.
|
||||
pub(crate) fn shortcut_text(source: &str, platform: Platform) -> String {
|
||||
let glue = if platform == Platform::Mac { "" } else { "+" };
|
||||
keystroke_labels(stroke, platform).join(glue)
|
||||
keystroke_labels(&keystroke(source, platform), platform).join(glue)
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
@@ -125,7 +129,7 @@ pub struct Button {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
trailing_icon: Option<IconName>,
|
||||
shortcut: Option<Keystroke>,
|
||||
shortcut: Option<SharedString>,
|
||||
variant: ButtonVariant,
|
||||
size: ControlSize,
|
||||
disabled: bool,
|
||||
@@ -181,9 +185,8 @@ impl Button {
|
||||
|
||||
/// Shows the keystroke that also runs it, e.g. `"cmd-s"`. Panics on a bad keystroke.
|
||||
pub fn shortcut(mut self, keystroke: &str) -> Self {
|
||||
let parsed = Keystroke::parse(keystroke)
|
||||
.unwrap_or_else(|error| panic!("button shortcut {keystroke:?}: {error}"));
|
||||
self.shortcut = Some(parsed);
|
||||
keystroke_of(keystroke);
|
||||
self.shortcut = Some(SharedString::from(keystroke.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -226,7 +229,8 @@ impl RenderOnce for Button {
|
||||
let link = self.variant == ButtonVariant::Link;
|
||||
let icon = |name| Icon::new(name).size(icon_size).color(tone.fg);
|
||||
let radius = theme.radius(Radius::Md);
|
||||
let hint = self.shortcut.as_ref().map(shortcut_text);
|
||||
let platform = cx.theme().platform;
|
||||
let hint = self.shortcut.map(|source| shortcut_text(&source, platform));
|
||||
let spinner = (self.id.clone(), "spinner");
|
||||
let parts = div()
|
||||
.flex()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, Keystroke, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
App, ElementId, Entity, FontWeight, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::button::shortcut_text;
|
||||
use super::button::{keystroke_of, shortcut_text};
|
||||
use crate::{
|
||||
primitives::{FocusRing, Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
@@ -19,7 +19,7 @@ pub struct QuickAction {
|
||||
icon: IconName,
|
||||
label: SharedString,
|
||||
note: Option<SharedString>,
|
||||
shortcut: Option<Keystroke>,
|
||||
shortcut: Option<SharedString>,
|
||||
run: Run,
|
||||
}
|
||||
|
||||
@@ -45,9 +45,8 @@ impl QuickAction {
|
||||
|
||||
/// Panics on a bad keystroke.
|
||||
pub fn shortcut(mut self, keystroke: &str) -> Self {
|
||||
let parsed = Keystroke::parse(keystroke)
|
||||
.unwrap_or_else(|error| panic!("quick action shortcut {keystroke:?}: {error}"));
|
||||
self.shortcut = Some(parsed);
|
||||
keystroke_of(keystroke);
|
||||
self.shortcut = Some(SharedString::from(keystroke.to_string()));
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -171,7 +170,7 @@ impl RenderOnce for QuickActions {
|
||||
div()
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(shortcut_text(stroke)),
|
||||
.child(shortcut_text(stroke, theme.platform)),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ impl RenderOnce for AttachmentChip {
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.bg(colors.hover)
|
||||
.child(
|
||||
Icon::new(file_icon(&item.name))
|
||||
Icon::new(file_icon(&item.name, cx))
|
||||
.size(IconSize::Md)
|
||||
.color(colors.fg_muted),
|
||||
),
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ impl RenderOnce for FileMessage {
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.bg(colors.hover)
|
||||
.child(
|
||||
Icon::new(file_icon(&self.name))
|
||||
Icon::new(file_icon(&self.name, cx))
|
||||
.size(IconSize::Md)
|
||||
.color(colors.fg_muted),
|
||||
),
|
||||
|
||||
@@ -211,7 +211,7 @@ impl RenderOnce for ProjectKnowledgePanel {
|
||||
.py_1()
|
||||
.text_size(theme.text_size(TextSize::Sm))
|
||||
.child(
|
||||
Icon::new(file_icon(&name))
|
||||
Icon::new(file_icon(&name, cx))
|
||||
.size(IconSize::Sm)
|
||||
.color(colors.fg_muted),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, Keystroke,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
prelude::*,
|
||||
AnyElement, App, ElementId, FontWeight, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::OnIndex;
|
||||
@@ -68,14 +67,14 @@ impl RenderOnce for DebugToolbar {
|
||||
let theme = cx.theme();
|
||||
let colors = theme.colors.clone();
|
||||
let paused = self.state == DebugState::Paused;
|
||||
let platform = theme.platform;
|
||||
let button =
|
||||
|command: DebugCommand, icon: IconName, words: &str, key: &str, enabled: bool| {
|
||||
let run = self.on_command.clone();
|
||||
let key = Keystroke::parse(key).expect("a debug key parses");
|
||||
IconButton::new((self.id.clone(), format!("{command:?}")), icon)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(format!("{words} {}", shortcut_text(&key)))
|
||||
.tooltip(format!("{words} {}", shortcut_text(key, platform)))
|
||||
.disabled(!enabled)
|
||||
.when_some(run, |button, run| {
|
||||
button.on_click(move |_, window, cx| {
|
||||
|
||||
@@ -98,6 +98,7 @@ impl RenderOnce for RichTextEditor {
|
||||
};
|
||||
let (open_link, close_link) = (set_linking(true), set_linking(false));
|
||||
let float_id: ElementId = (self.id.clone(), "float").into();
|
||||
let platform = cx.theme().platform;
|
||||
let floating = FloatingToolbar::new(float_id.clone(), &self.field).children(
|
||||
INLINE.iter().map(|tool_of| {
|
||||
let on = pressed.contains(&tool_of.0);
|
||||
@@ -107,6 +108,7 @@ impl RenderOnce for RichTextEditor {
|
||||
*tool_of,
|
||||
on,
|
||||
Some(open_link.clone()),
|
||||
platform,
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use gpui::{App, ElementId, Entity, IntoElement, Keystroke, ParentElement, RenderOnce, Window};
|
||||
use gpui::{App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, Window};
|
||||
|
||||
use super::format::{Format, active, format};
|
||||
use crate::{
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
forms::{Run, TextInput},
|
||||
primitives::IconName,
|
||||
shell::{Toolbar, ToolbarGroup, ToolbarSeparator},
|
||||
theme::ControlSize,
|
||||
theme::{ActiveTheme, ControlSize, Platform},
|
||||
};
|
||||
|
||||
/// A tool: the format it applies, its icon, its name and its key.
|
||||
@@ -56,12 +56,10 @@ pub(crate) fn tool(
|
||||
(format_of, icon, name, key): Tool,
|
||||
on: bool,
|
||||
on_link: Option<Run>,
|
||||
platform: Platform,
|
||||
) -> ToggleButton {
|
||||
let tip = match key {
|
||||
Some(key) => {
|
||||
let stroke = Keystroke::parse(key).expect("a tool's key parses");
|
||||
format!("{name} {}", shortcut_text(&stroke))
|
||||
}
|
||||
Some(key) => format!("{name} {}", shortcut_text(key, platform)),
|
||||
None => name.to_string(),
|
||||
};
|
||||
let field = field.clone();
|
||||
@@ -105,10 +103,18 @@ impl RenderOnce for FixedFormatToolbar {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = self.field.read(cx);
|
||||
let pressed = active(input.text(), input.selection());
|
||||
let platform = cx.theme().platform;
|
||||
let group = |tools: &[Tool]| {
|
||||
ToolbarGroup::new().children(tools.iter().map(|tool_of| {
|
||||
let on = pressed.contains(&tool_of.0);
|
||||
tool(&self.id, &self.field, *tool_of, on, self.on_link.clone())
|
||||
tool(
|
||||
&self.id,
|
||||
&self.field,
|
||||
*tool_of,
|
||||
on,
|
||||
self.on_link.clone(),
|
||||
platform,
|
||||
)
|
||||
}))
|
||||
};
|
||||
Toolbar::new()
|
||||
|
||||
+10
-5
@@ -8,7 +8,7 @@ use gpui::{
|
||||
use super::FileIcon;
|
||||
use crate::{
|
||||
lists::{Folder, Tree, TreeNode},
|
||||
primitives::{IconName, file_icon},
|
||||
primitives::{IconName, IconTheme, icon_theme},
|
||||
theme::{ActiveTheme, IconSize, TextSize},
|
||||
typography::{MiddleEllipsis, format},
|
||||
};
|
||||
@@ -32,14 +32,19 @@ pub(crate) fn saving(size: u64, packed: u64) -> u64 {
|
||||
}
|
||||
|
||||
/// Nodes for `folder` under `prefix`, folders first, then files, each by name; and the bytes it holds.
|
||||
fn nodes(folder: &Folder, prefix: &str, entries: &[ArchiveEntry]) -> (Vec<TreeNode>, u64) {
|
||||
fn nodes(
|
||||
folder: &Folder,
|
||||
prefix: &str,
|
||||
entries: &[ArchiveEntry],
|
||||
icons: &IconTheme,
|
||||
) -> (Vec<TreeNode>, u64) {
|
||||
let mut out = Vec::new();
|
||||
let mut held = 0;
|
||||
let mut folders: Vec<_> = folder.folders.iter().collect();
|
||||
folders.sort_by_key(|(name, _)| name.to_lowercase());
|
||||
for (name, inner) in folders {
|
||||
let path = format!("{prefix}{name}");
|
||||
let (children, bytes) = nodes(inner, &format!("{path}/"), entries);
|
||||
let (children, bytes) = nodes(inner, &format!("{path}/"), entries, icons);
|
||||
held += bytes;
|
||||
let node = TreeNode::new(path, name.clone())
|
||||
.icon(IconName::Folder)
|
||||
@@ -63,7 +68,7 @@ fn nodes(folder: &Folder, prefix: &str, entries: &[ArchiveEntry]) -> (Vec<TreeNo
|
||||
);
|
||||
out.push(
|
||||
TreeNode::new(path, name.clone())
|
||||
.icon(file_icon(name))
|
||||
.icon(icons.file(name))
|
||||
.note(note),
|
||||
);
|
||||
}
|
||||
@@ -115,7 +120,7 @@ impl RenderOnce for ArchiveViewer {
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let folders = Folder::of(self.entries.iter().map(|entry| entry.path.as_ref()));
|
||||
let (nodes, size) = nodes(&folders, "", &self.entries);
|
||||
let (nodes, size) = nodes(&folders, "", &self.entries, icon_theme(cx));
|
||||
let packed: u64 = self.entries.iter().map(|entry| entry.packed).sum();
|
||||
let top: Vec<SharedString> = folders
|
||||
.folders
|
||||
|
||||
+17
-7
@@ -1,4 +1,4 @@
|
||||
use gpui::{App, IntoElement, RenderOnce, Window};
|
||||
use gpui::{App, IntoElement, RenderOnce, SharedString, Window};
|
||||
|
||||
use crate::{
|
||||
lists::DirEntry,
|
||||
@@ -6,28 +6,34 @@ use crate::{
|
||||
theme::{ActiveTheme, IconSize},
|
||||
};
|
||||
|
||||
/// A file's icon by its name's extension, or a folder's, open or shut.
|
||||
/// A file's name, its icon found under the icon theme at render, or an icon itself.
|
||||
enum Shown {
|
||||
File(SharedString),
|
||||
Icon(IconName),
|
||||
}
|
||||
|
||||
/// A file's icon by its name under the owner's icon theme, or a folder's, open or shut.
|
||||
#[derive(IntoElement)]
|
||||
pub struct FileIcon {
|
||||
icon: IconName,
|
||||
shown: Shown,
|
||||
size: IconSize,
|
||||
}
|
||||
|
||||
impl FileIcon {
|
||||
pub fn file(name: impl AsRef<str>) -> Self {
|
||||
Self {
|
||||
icon: file_icon(name.as_ref()),
|
||||
shown: Shown::File(name.as_ref().to_string().into()),
|
||||
size: IconSize::Md,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn folder(open: bool) -> Self {
|
||||
Self {
|
||||
icon: if open {
|
||||
shown: Shown::Icon(if open {
|
||||
IconName::FolderOpen
|
||||
} else {
|
||||
IconName::Folder
|
||||
},
|
||||
}),
|
||||
size: IconSize::Md,
|
||||
}
|
||||
}
|
||||
@@ -48,7 +54,11 @@ impl FileIcon {
|
||||
|
||||
impl RenderOnce for FileIcon {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
Icon::new(self.icon)
|
||||
let icon = match self.shown {
|
||||
Shown::File(name) => file_icon(&name, cx),
|
||||
Shown::Icon(icon) => icon,
|
||||
};
|
||||
Icon::new(icon)
|
||||
.size(self.size)
|
||||
.color(cx.theme().colors.fg_muted)
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ impl RenderOnce for DirectoryListing {
|
||||
Icon::new(if entry.is_folder() {
|
||||
IconName::Folder
|
||||
} else {
|
||||
file_icon(&entry.name)
|
||||
file_icon(&entry.name, cx)
|
||||
})
|
||||
.size(IconSize::Md)
|
||||
.color(colors.fg_muted),
|
||||
|
||||
+10
-7
@@ -9,7 +9,7 @@ use super::{Tree, TreeNode};
|
||||
use crate::{
|
||||
data_display::Tone,
|
||||
forms::{SearchInput, TextInput},
|
||||
primitives::{IconName, file_icon},
|
||||
primitives::{IconName, IconTheme, icon_theme},
|
||||
theme::ControlSize,
|
||||
};
|
||||
|
||||
@@ -90,6 +90,7 @@ fn nodes(
|
||||
paths: &[SharedString],
|
||||
status: &[(SharedString, GitStatus)],
|
||||
query: &str,
|
||||
icons: &IconTheme,
|
||||
) -> (Vec<TreeNode>, Vec<SharedString>) {
|
||||
let root = Folder::of(paths.iter().map(|path| path.as_ref()));
|
||||
let state = |path: &str| {
|
||||
@@ -104,6 +105,7 @@ fn nodes(
|
||||
query: &str,
|
||||
state: &dyn Fn(&str) -> Option<GitStatus>,
|
||||
open: &mut Vec<SharedString>,
|
||||
icons: &IconTheme,
|
||||
) -> (Vec<TreeNode>, bool) {
|
||||
let mut out = Vec::new();
|
||||
let mut changed = false;
|
||||
@@ -111,7 +113,8 @@ fn nodes(
|
||||
folders.sort_by_key(|(name, _)| name.to_lowercase());
|
||||
for (name, inner) in folders {
|
||||
let path = format!("{prefix}{name}");
|
||||
let (children, inner_changed) = walk(inner, &format!("{path}/"), query, state, open);
|
||||
let (children, inner_changed) =
|
||||
walk(inner, &format!("{path}/"), query, state, open, icons);
|
||||
if children.is_empty() && !matches(name, query) {
|
||||
continue;
|
||||
}
|
||||
@@ -136,7 +139,7 @@ fn nodes(
|
||||
files.sort_by_key(|name| name.to_lowercase());
|
||||
for name in files {
|
||||
let path = format!("{prefix}{name}");
|
||||
let node = TreeNode::new(path.clone(), name.clone()).icon(file_icon(name));
|
||||
let node = TreeNode::new(path.clone(), name.clone()).icon(icons.file(name));
|
||||
out.push(match state(&path) {
|
||||
Some(state) => {
|
||||
changed = true;
|
||||
@@ -148,7 +151,7 @@ fn nodes(
|
||||
(out, changed)
|
||||
}
|
||||
let mut open = Vec::new();
|
||||
let (nodes, _) = walk(&root, "", query, &state, &mut open);
|
||||
let (nodes, _) = walk(&root, "", query, &state, &mut open, icons);
|
||||
(nodes, open)
|
||||
}
|
||||
|
||||
@@ -208,7 +211,7 @@ impl RenderOnce for FileTree {
|
||||
Vec::<SharedString>::new()
|
||||
});
|
||||
let query = filter.read(cx).text().trim().to_string();
|
||||
let (nodes, open) = nodes(&self.paths, &self.status, &query);
|
||||
let (nodes, open) = nodes(&self.paths, &self.status, &query, icon_theme(cx));
|
||||
let top: Vec<SharedString> = nodes
|
||||
.iter()
|
||||
.filter(|node| node.opens())
|
||||
@@ -245,7 +248,7 @@ impl RenderOnce for FileTree {
|
||||
mod tests {
|
||||
use gpui::SharedString;
|
||||
|
||||
use super::{GitStatus, nodes};
|
||||
use super::{GitStatus, IconTheme, nodes};
|
||||
use crate::lists::tree::{Shown, rows};
|
||||
|
||||
fn paths() -> Vec<SharedString> {
|
||||
@@ -267,7 +270,7 @@ mod tests {
|
||||
("src/ui/Button.rs".into(), GitStatus::Modified),
|
||||
("README.md".into(), GitStatus::Added),
|
||||
];
|
||||
let (nodes, open) = nodes(&paths(), &status, query);
|
||||
let (nodes, open) = nodes(&paths(), &status, query, &IconTheme::default());
|
||||
let every = ["assets", "src", "src/ui"]
|
||||
.map(SharedString::from)
|
||||
.into_iter()
|
||||
|
||||
+87
-12
@@ -1,9 +1,68 @@
|
||||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
use gpui::{App, Global};
|
||||
|
||||
use super::IconName;
|
||||
|
||||
/// The icon a file's name suggests, by its extension.
|
||||
pub(crate) fn file_icon(name: &str) -> IconName {
|
||||
let extension = name.rsplit_once('.').map(|(_, ext)| ext.to_lowercase());
|
||||
match extension.as_deref() {
|
||||
/// Which icon a file shows: an owner's rules by name and by extension, laid over Ely's map. Without one, Ely's map alone.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct IconTheme {
|
||||
names: HashMap<String, IconName>,
|
||||
extensions: HashMap<String, IconName>,
|
||||
}
|
||||
|
||||
impl Global for IconTheme {}
|
||||
|
||||
impl IconTheme {
|
||||
/// A file of this exact name, in any case: `Cargo.toml`, `Dockerfile`.
|
||||
pub fn name(mut self, name: &str, icon: IconName) -> Self {
|
||||
self.names.insert(name.to_lowercase(), icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Files ending in this extension, given without its dot.
|
||||
pub fn extension(mut self, extension: &str, icon: IconName) -> Self {
|
||||
self.extensions.insert(extension.to_lowercase(), icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Serves every window from now on.
|
||||
pub fn apply(self, cx: &mut App) {
|
||||
log::info!(
|
||||
"icon theme: {} names, {} extensions",
|
||||
self.names.len(),
|
||||
self.extensions.len()
|
||||
);
|
||||
cx.set_global(self);
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
pub(crate) fn file(&self, name: &str) -> IconName {
|
||||
let name = name.to_lowercase();
|
||||
let extension = name.rsplit_once('.').map(|(_, extension)| extension);
|
||||
self.names
|
||||
.get(&name)
|
||||
.or_else(|| extension.and_then(|extension| self.extensions.get(extension)))
|
||||
.copied()
|
||||
.unwrap_or_else(|| ely_icon(extension))
|
||||
}
|
||||
}
|
||||
|
||||
static ELY: LazyLock<IconTheme> = LazyLock::new(IconTheme::default);
|
||||
|
||||
/// The owner's icon theme, or Ely's map alone.
|
||||
pub(crate) fn icon_theme(cx: &App) -> &IconTheme {
|
||||
cx.try_global::<IconTheme>().unwrap_or(&ELY)
|
||||
}
|
||||
|
||||
/// The icon a file's name suggests, under the owner's icon theme.
|
||||
pub(crate) fn file_icon(name: &str, cx: &App) -> IconName {
|
||||
icon_theme(cx).file(name)
|
||||
}
|
||||
|
||||
/// Ely's icon for a file by its extension, in lower case.
|
||||
fn ely_icon(extension: Option<&str>) -> IconName {
|
||||
match extension {
|
||||
Some(
|
||||
"rs" | "ts" | "tsx" | "js" | "jsx" | "py" | "go" | "swift" | "c" | "h" | "cpp" | "java"
|
||||
| "kt" | "rb" | "php" | "css" | "html",
|
||||
@@ -26,16 +85,32 @@ pub(crate) fn file_icon(name: &str) -> IconName {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IconName, file_icon};
|
||||
use super::{IconName, IconTheme};
|
||||
|
||||
#[test]
|
||||
fn a_name_picks_its_icon_by_its_last_extension() {
|
||||
assert_eq!(file_icon("main.RS"), IconName::FileCode);
|
||||
assert_eq!(file_icon("lift.PNG"), IconName::FileImage);
|
||||
assert_eq!(file_icon("notes.md"), IconName::FileText);
|
||||
assert_eq!(file_icon("backup.tar.gz"), IconName::FileArchive);
|
||||
assert_eq!(file_icon(".env"), IconName::FileCog);
|
||||
assert_eq!(file_icon("take.mov"), IconName::FileVideoCamera);
|
||||
assert_eq!(file_icon("Makefile"), IconName::File);
|
||||
assert_eq!(IconTheme::default().file("main.RS"), IconName::FileCode);
|
||||
assert_eq!(IconTheme::default().file("lift.PNG"), IconName::FileImage);
|
||||
assert_eq!(IconTheme::default().file("notes.md"), IconName::FileText);
|
||||
assert_eq!(
|
||||
IconTheme::default().file("backup.tar.gz"),
|
||||
IconName::FileArchive
|
||||
);
|
||||
assert_eq!(IconTheme::default().file(".env"), IconName::FileCog);
|
||||
assert_eq!(
|
||||
IconTheme::default().file("take.mov"),
|
||||
IconName::FileVideoCamera
|
||||
);
|
||||
assert_eq!(IconTheme::default().file("Makefile"), IconName::File);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_owners_rules_come_before_elys_map() {
|
||||
let theme = IconTheme::default()
|
||||
.name("Cargo.toml", IconName::Package)
|
||||
.extension("RS", IconName::Braces);
|
||||
assert_eq!(theme.file("cargo.TOML"), IconName::Package);
|
||||
assert_eq!(theme.file("main.rs"), IconName::Braces);
|
||||
assert_eq!(theme.file("deploy.toml"), IconName::FileCog);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ mod tests;
|
||||
pub use backdrop::{Backdrop, Place};
|
||||
pub use disclosure::Disclosure;
|
||||
pub use divider::Divider;
|
||||
pub(crate) use files::file_icon;
|
||||
pub use files::IconTheme;
|
||||
pub(crate) use files::{file_icon, icon_theme};
|
||||
pub use focus::{FocusNext, FocusPrev, FocusRing, FocusScope};
|
||||
pub(crate) use focus::{Takeover, give_back, hand_back, hold_focus, tab_stop, take_focus};
|
||||
pub use ghost::DragGhost;
|
||||
|
||||
+28
-45
@@ -9,30 +9,9 @@ use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, TextSize},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Platform, TextSize},
|
||||
};
|
||||
|
||||
/// Which system's window buttons to draw.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ControlsStyle {
|
||||
Mac,
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
|
||||
impl ControlsStyle {
|
||||
/// The style of the system this build targets.
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Self::Mac
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drags the window, and zooms it on double-click.
|
||||
pub fn drag_region(id: impl Into<ElementId>, window: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
let id = id.into();
|
||||
@@ -91,7 +70,7 @@ fn run(control: Control, on_close: Option<&OnClose>, window: &mut Window, cx: &m
|
||||
#[derive(IntoElement)]
|
||||
pub struct WindowControls {
|
||||
id: ElementId,
|
||||
style: ControlsStyle,
|
||||
platform: Option<Platform>,
|
||||
on_close: Option<OnClose>,
|
||||
}
|
||||
|
||||
@@ -99,13 +78,14 @@ impl WindowControls {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
style: ControlsStyle::current(),
|
||||
platform: None,
|
||||
on_close: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: ControlsStyle) -> Self {
|
||||
self.style = style;
|
||||
/// Draws this platform's buttons instead of the theme's.
|
||||
pub fn platform(mut self, platform: Platform) -> Self {
|
||||
self.platform = Some(platform);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -118,10 +98,10 @@ impl WindowControls {
|
||||
|
||||
impl RenderOnce for WindowControls {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self.style {
|
||||
ControlsStyle::Mac => traffic_lights(self.id, self.on_close, window, cx),
|
||||
ControlsStyle::Windows => caption_buttons(self.id, self.on_close, window, cx),
|
||||
ControlsStyle::Linux => round_buttons(self.id, self.on_close, window, cx),
|
||||
match self.platform.unwrap_or(cx.theme().platform) {
|
||||
Platform::Mac => traffic_lights(self.id, self.on_close, window, cx),
|
||||
Platform::Windows => caption_buttons(self.id, self.on_close, window, cx),
|
||||
Platform::Linux => round_buttons(self.id, self.on_close, window, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +284,7 @@ fn route_close(id: &ElementId, handler: Option<OnClose>, window: &mut Window, cx
|
||||
pub struct TitleBar {
|
||||
id: ElementId,
|
||||
title: Option<SharedString>,
|
||||
style: Option<ControlsStyle>,
|
||||
platform: Option<Platform>,
|
||||
on_close: Option<OnClose>,
|
||||
leading: SmallVec<[AnyElement; 2]>,
|
||||
actions: SmallVec<[AnyElement; 2]>,
|
||||
@@ -316,7 +296,7 @@ impl TitleBar {
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: None,
|
||||
style: None,
|
||||
platform: None,
|
||||
on_close: None,
|
||||
leading: SmallVec::new(),
|
||||
actions: SmallVec::new(),
|
||||
@@ -328,13 +308,13 @@ impl TitleBar {
|
||||
self
|
||||
}
|
||||
|
||||
/// Draws this style's controls instead of the system's.
|
||||
pub fn style(mut self, style: ControlsStyle) -> Self {
|
||||
self.style = Some(style);
|
||||
/// Draws this platform's controls instead of the system's.
|
||||
pub fn platform(mut self, platform: Platform) -> Self {
|
||||
self.platform = Some(platform);
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs instead of closing; without a forced style, system closes run it too.
|
||||
/// Runs instead of closing; without a forced platform, system closes run it too.
|
||||
pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_close = Some(Rc::new(handler));
|
||||
self
|
||||
@@ -355,19 +335,22 @@ impl ParentElement for TitleBar {
|
||||
|
||||
impl RenderOnce for TitleBar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
if self.style.is_none() {
|
||||
if self.platform.is_none() {
|
||||
route_close(&self.id, self.on_close.clone(), window, cx);
|
||||
}
|
||||
let bar = drag_region(self.id, window, cx);
|
||||
let theme = cx.theme();
|
||||
let system = self.style.is_none() && cfg!(target_os = "macos");
|
||||
let style = self.style.unwrap_or_else(ControlsStyle::current);
|
||||
let system = self.platform.is_none() && cfg!(target_os = "macos");
|
||||
let style = match system {
|
||||
true => Platform::Mac,
|
||||
false => self.platform.unwrap_or(theme.platform),
|
||||
};
|
||||
let mut trailing = (!system).then_some(WindowControls {
|
||||
id: "window-controls".into(),
|
||||
style,
|
||||
platform: Some(style),
|
||||
on_close: self.on_close,
|
||||
});
|
||||
let leading = if style == ControlsStyle::Mac {
|
||||
let leading = if style == Platform::Mac {
|
||||
trailing.take()
|
||||
} else {
|
||||
None
|
||||
@@ -379,7 +362,7 @@ impl RenderOnce for TitleBar {
|
||||
.text_color(theme.colors.fg_muted)
|
||||
.child(title)
|
||||
});
|
||||
let centered = style != ControlsStyle::Windows;
|
||||
let centered = style != Platform::Windows;
|
||||
let inset = theme.traffic_light_inset();
|
||||
let fullscreen = window.is_fullscreen();
|
||||
let bar = bar
|
||||
@@ -396,9 +379,9 @@ impl RenderOnce for TitleBar {
|
||||
let bar = match (system, style) {
|
||||
(true, _) if fullscreen => bar.pl_3().pr_2(),
|
||||
(true, _) => bar.pl(inset).pr_2(),
|
||||
(false, ControlsStyle::Mac) => bar.pl_3().pr_2().children(leading),
|
||||
(false, ControlsStyle::Windows) => bar.pl_3(),
|
||||
(false, ControlsStyle::Linux) => bar.pl_3().pr_2(),
|
||||
(false, Platform::Mac) => bar.pl_3().pr_2().children(leading),
|
||||
(false, Platform::Windows) => bar.pl_3(),
|
||||
(false, Platform::Linux) => bar.pl_3().pr_2(),
|
||||
};
|
||||
let bar = bar.children(self.leading);
|
||||
let bar = match (centered, title) {
|
||||
|
||||
+3
-1
@@ -11,11 +11,12 @@ mod tabs;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
mod update;
|
||||
mod vibrancy;
|
||||
mod windows;
|
||||
|
||||
pub use activity::{ActivityBar, ActivityItem};
|
||||
pub use bars::{StatusBar, StatusBarItem, Toolbar, ToolbarGroup, ToolbarSeparator};
|
||||
pub use chrome::{ControlsStyle, TitleBar, WindowControls, drag_region};
|
||||
pub use chrome::{TitleBar, WindowControls, drag_region};
|
||||
pub use crash::CrashReporter;
|
||||
pub use dialogs::{AboutDialog, SplashScreen};
|
||||
pub use frame::ResizeBorder;
|
||||
@@ -25,4 +26,5 @@ pub use status::{Connectivity, OfflineIndicator, ZoomControl};
|
||||
pub use switcher::WindowSwitcher;
|
||||
pub use tabs::{TabBar, WindowTab};
|
||||
pub use update::{UpdateBanner, UpdateDialog, UpdateState};
|
||||
pub use vibrancy::Vibrancy;
|
||||
pub use windows::{Corner, Hosted, MiniWindow, WindowManager, open_hosted};
|
||||
@@ -88,3 +88,45 @@ fn the_switcher_switches_on_release_so_its_opener_stays_shut(cx: &mut TestAppCon
|
||||
assert!(!desk.read_with(cx, |desk, _| desk.open));
|
||||
assert_eq!(desk.read_with(cx, |desk, _| desk.enters_above), above);
|
||||
}
|
||||
|
||||
struct Platformed;
|
||||
|
||||
impl Render for Platformed {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "controls".into())
|
||||
.child(super::WindowControls::new("controls")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "caps".into())
|
||||
.child(crate::typography::Kbd::new("secondary-s")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Window buttons and key caps follow the theme's platform: Windows draws three wide caption buttons and spells Ctrl.
|
||||
#[gpui::test]
|
||||
fn the_themes_platform_draws_the_buttons_and_the_caps(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
cx.update(|cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Mac));
|
||||
let (_, cx) = cx.add_window_view(|_, _| Platformed);
|
||||
settle(cx);
|
||||
let width = |name: &'static str, cx: &mut VisualTestContext| {
|
||||
cx.debug_bounds(name).expect("a measured part").size.width
|
||||
};
|
||||
let (lights, command) = (width("controls", cx), width("caps", cx));
|
||||
cx.update(|_, cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Windows));
|
||||
settle(cx);
|
||||
assert!(
|
||||
width("controls", cx) > lights * 2.0,
|
||||
"caption buttons wider than lights"
|
||||
);
|
||||
assert!(
|
||||
width("caps", cx) > command,
|
||||
"Ctrl wider than the command mark"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use gpui::{
|
||||
AnyElement, App, Div, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window,
|
||||
div,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
/// A pane the window's blur shows through: vibrancy on macOS, acrylic on Windows. Its window opens with `WindowBackgroundAppearance::Blurred` and nothing opaque beneath the pane; high contrast fills it solid.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Vibrancy {
|
||||
base: Div,
|
||||
}
|
||||
|
||||
impl Vibrancy {
|
||||
pub fn new() -> Self {
|
||||
Self { base: div() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vibrancy {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Vibrancy {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Vibrancy {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.base.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Vibrancy {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
self.base.bg(cx.theme().colors.glass)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod media;
|
||||
mod messaging;
|
||||
mod onboarding;
|
||||
mod palette;
|
||||
mod platform;
|
||||
mod project;
|
||||
mod syntax;
|
||||
mod tokens;
|
||||
@@ -32,6 +33,7 @@ pub use media::MediaSizes;
|
||||
pub use messaging::MessagingSizes;
|
||||
pub use onboarding::OnboardingSizes;
|
||||
pub use palette::{HUE_NAMES, Mix, Palette, Syntax};
|
||||
pub use platform::Platform;
|
||||
pub use project::ProjectSizes;
|
||||
pub use syntax::{SyntaxTheme, syntax_themes};
|
||||
pub use tokens::{
|
||||
@@ -69,6 +71,7 @@ pub struct Theme {
|
||||
pub font_family: SharedString,
|
||||
pub mono_family: SharedString,
|
||||
pub reduced_motion: bool,
|
||||
pub platform: Platform,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
@@ -97,6 +100,7 @@ impl Theme {
|
||||
font_family: "Inter".into(),
|
||||
mono_family: "JetBrains Mono".into(),
|
||||
reduced_motion: false,
|
||||
platform: Platform::current(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ mixable!(
|
||||
backdrop,
|
||||
media_backdrop,
|
||||
shimmer,
|
||||
glass,
|
||||
shadow,
|
||||
tooltip_bg,
|
||||
tooltip_fg,
|
||||
@@ -182,6 +183,7 @@ impl Palette {
|
||||
backdrop: rgba(0x18161352).into(),
|
||||
media_backdrop: rgba(0x0e0d0bf8).into(),
|
||||
shimmer: rgba(0xffffffb3).into(),
|
||||
glass: rgba(0xfcfaf7b8).into(),
|
||||
shadow: c(0x181613),
|
||||
tooltip_bg: c(0x181613),
|
||||
tooltip_fg: c(0xfcfaf7),
|
||||
@@ -215,6 +217,7 @@ impl Palette {
|
||||
palette.fg_subtle = c(0x575552);
|
||||
palette.border = c(0xa6a4a2);
|
||||
palette.border_strong = c(0x82807d);
|
||||
palette.glass = palette.bg;
|
||||
}
|
||||
palette
|
||||
}
|
||||
@@ -251,6 +254,7 @@ impl Palette {
|
||||
backdrop: rgba(0x00000080).into(),
|
||||
media_backdrop: rgba(0x000000f8).into(),
|
||||
shimmer: rgba(0xffffff10).into(),
|
||||
glass: rgba(0x131110b8).into(),
|
||||
shadow: c(0x000000),
|
||||
tooltip_bg: c(0x373533),
|
||||
tooltip_fg: c(0xf3f1f0),
|
||||
@@ -284,6 +288,7 @@ impl Palette {
|
||||
palette.fg_subtle = c(0xafadab);
|
||||
palette.border = c(0x6a6966);
|
||||
palette.border_strong = c(0x858380);
|
||||
palette.glass = palette.bg;
|
||||
}
|
||||
palette
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/// Whose conventions the components follow: window buttons, the title bar's layout and key caps.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Platform {
|
||||
Mac,
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
/// The system this build targets, where the theme starts.
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Platform::Mac
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Platform::Windows
|
||||
} else {
|
||||
Platform::Linux
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -26,7 +26,7 @@ fn every_color_answers_to_its_name() {
|
||||
for (ix, name) in Palette::NAMES.iter().enumerate() {
|
||||
*palette.token_mut(name) = hsla(ix as f32 / 64.0, 0.5, 0.5, 1.0);
|
||||
}
|
||||
assert_eq!(Palette::NAMES.len(), 35, "every color of the palette");
|
||||
assert_eq!(Palette::NAMES.len(), 36, "every color of the palette");
|
||||
let at = |name: &str| {
|
||||
Palette::NAMES
|
||||
.iter()
|
||||
@@ -50,3 +50,18 @@ fn every_color_answers_to_its_name() {
|
||||
fn an_unknown_color_fails() {
|
||||
Palette::light(false).token_mut("glow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glass_lets_the_blur_through_until_high_contrast() {
|
||||
for (plain, strong) in [
|
||||
(Palette::light(false), Palette::light(true)),
|
||||
(Palette::dark(false), Palette::dark(true)),
|
||||
] {
|
||||
assert!(
|
||||
plain.glass.a > 0.0 && plain.glass.a < 1.0,
|
||||
"{:?}",
|
||||
plain.glass
|
||||
);
|
||||
assert_eq!(strong.glass, strong.bg);
|
||||
}
|
||||
}
|
||||
+17
-15
@@ -7,8 +7,8 @@ use gpui::{
|
||||
use regex::RegexBuilder;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::keys::{Platform, keystroke_labels};
|
||||
use crate::theme::{ActiveTheme, Radius, TextSize};
|
||||
use super::keys::{keystroke, keystroke_labels};
|
||||
use crate::theme::{ActiveTheme, Platform, Radius, TextSize};
|
||||
|
||||
/// Monospace fragment on a quiet fill.
|
||||
#[derive(IntoElement)]
|
||||
@@ -56,28 +56,25 @@ fn cap(label: String, cx: &App) -> impl IntoElement + use<> {
|
||||
.child(label)
|
||||
}
|
||||
|
||||
fn parse(source: &str) -> Keystroke {
|
||||
Keystroke::parse(source).unwrap_or_else(|error| panic!("bad keystroke {source:?}: {error}"))
|
||||
}
|
||||
|
||||
/// One key, labeled for this platform. Takes gpui key syntax.
|
||||
/// One key, labeled for the theme's platform. Takes gpui key syntax.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Kbd {
|
||||
stroke: Keystroke,
|
||||
source: SharedString,
|
||||
}
|
||||
|
||||
impl Kbd {
|
||||
/// `"secondary-s"` reads ⌘S on macOS and Ctrl S elsewhere.
|
||||
/// `"secondary-s"` reads ⌘S for the Mac and Ctrl S for the others.
|
||||
pub fn new(source: &str) -> Self {
|
||||
Self {
|
||||
stroke: parse(source),
|
||||
source: SharedString::from(source.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Kbd {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let labels = keystroke_labels(&self.stroke, Platform::current());
|
||||
let platform = cx.theme().platform;
|
||||
let labels = keystroke_labels(&keystroke(&self.source, platform), platform);
|
||||
div()
|
||||
.flex()
|
||||
.gap_0p5()
|
||||
@@ -88,27 +85,32 @@ impl RenderOnce for Kbd {
|
||||
/// A chord sequence such as `"cmd-k cmd-s"`.
|
||||
#[derive(IntoElement)]
|
||||
pub struct KbdCombo {
|
||||
strokes: SmallVec<[Keystroke; 2]>,
|
||||
source: SharedString,
|
||||
}
|
||||
|
||||
impl KbdCombo {
|
||||
pub fn new(source: &str) -> Self {
|
||||
Self {
|
||||
strokes: source.split_whitespace().map(parse).collect(),
|
||||
source: SharedString::from(source.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for KbdCombo {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let platform = Platform::current();
|
||||
let platform = cx.theme().platform;
|
||||
let strokes: Vec<Keystroke> = self
|
||||
.source
|
||||
.split_whitespace()
|
||||
.map(|source| keystroke(source, platform))
|
||||
.collect();
|
||||
let subtle = cx.theme().colors.fg_subtle;
|
||||
let spelled = platform != Platform::Mac;
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.children(self.strokes.iter().map(|stroke| {
|
||||
.children(strokes.iter().map(|stroke| {
|
||||
let labels = keystroke_labels(stroke, platform);
|
||||
let last = labels.len() - 1;
|
||||
div().flex().items_center().gap_0p5().children(
|
||||
|
||||
+25
-16
@@ -1,22 +1,20 @@
|
||||
use gpui::Keystroke;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Platform {
|
||||
Mac,
|
||||
Windows,
|
||||
Linux,
|
||||
}
|
||||
use crate::theme::Platform;
|
||||
|
||||
impl Platform {
|
||||
pub fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Platform::Mac
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Platform::Windows
|
||||
} else {
|
||||
Platform::Linux
|
||||
}
|
||||
}
|
||||
/// One keystroke in gpui key syntax, `secondary` read for `platform`: Command on the Mac, Control elsewhere.
|
||||
pub fn keystroke(source: &str, platform: Platform) -> Keystroke {
|
||||
let secondary = if platform == Platform::Mac {
|
||||
"cmd"
|
||||
} else {
|
||||
"ctrl"
|
||||
};
|
||||
let spelled: Vec<&str> = source
|
||||
.split('-')
|
||||
.map(|part| if part == "secondary" { secondary } else { part })
|
||||
.collect();
|
||||
Keystroke::parse(&spelled.join("-"))
|
||||
.unwrap_or_else(|error| panic!("bad keystroke {source:?}: {error}"))
|
||||
}
|
||||
|
||||
/// Caps for one keystroke, modifiers first, in platform order.
|
||||
@@ -94,6 +92,17 @@ mod tests {
|
||||
assert_eq!(labels, ["Super", "Esc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secondary_reads_for_the_platform() {
|
||||
let mac = keystroke_labels(&keystroke("secondary-s", Platform::Mac), Platform::Mac);
|
||||
assert_eq!(mac, ["⌘", "S"]);
|
||||
let windows = Platform::Windows;
|
||||
assert_eq!(
|
||||
keystroke_labels(&keystroke("secondary-s", windows), windows),
|
||||
["Ctrl", "S"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_keep_their_names_or_upper_case() {
|
||||
assert_eq!(key_label("f12", Platform::Mac), "F12");
|
||||
|
||||
+4
-3
@@ -266,9 +266,10 @@
|
||||
|
||||
## T39c Theme · Platform
|
||||
|
||||
- [ ] IconThemeProvider
|
||||
- [ ] Platform Style
|
||||
- [ ] VibrancyBackground / Mica / Acrylic — native `WindowBackgroundAppearance::Blurred`
|
||||
- [x] IconThemeProvider → `primitives::IconTheme`, an owner's rules by name and extension over `file_icon`
|
||||
- [x] Platform Style → `theme::Platform`, the theme's `platform`: the window buttons Ely draws, the title bar, key caps
|
||||
- [x] VibrancyBackground / Acrylic → `shell::Vibrancy` in a window opened `WindowBackgroundAppearance::Blurred`
|
||||
- [ ] Mica — blocked: gpui 0.2.2's one blur, `Blurred`, is acrylic on Windows
|
||||
|
||||
## T40 i18n & a11y — `src/i18n`
|
||||
|
||||
|
||||
Reference in new issue
Block a user