T04 window and shell: chrome, bars, rail, tabs, windows, dialogs, tray, badge, zoom

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 03:21:03 -04:00
1 parent 2bdf902bf4
commit 77bcd5be07
37 files changed
+4516 -111

No files matched your search

+287
View File
@@ -0,0 +1,287 @@
use gpui::{
Animation, AnimationExt, AnyElement, App, AppContext, Bounds, IntoElement, ParentElement,
RenderOnce, SharedString, Styled, Window, WindowBounds, WindowHandle, WindowOptions, div,
prelude::*, relative,
};
use super::{Hosted, open_hosted};
use crate::{
motion,
primitives::{Icon, IconName},
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
typography::{Caption, CopyableText, ExternalLink, Paragraph, Title},
};
fn mark(name: &SharedString, icon: Option<IconName>, cx: &App) -> impl IntoElement + use<> {
let theme = cx.theme();
let initial: SharedString = name.chars().take(1).collect::<String>().into();
div()
.flex()
.items_center()
.justify_center()
.size(theme.control_height(ControlSize::Lg) * 2.0)
.rounded(theme.radius(Radius::Xl))
.bg(theme.colors.sunken)
.border_1()
.border_color(theme.colors.border)
.text_size(theme.text_size(TextSize::Xxl))
.text_color(theme.colors.fg)
.map(|tile| match icon {
Some(icon) => tile.child(Icon::new(icon).size(IconSize::Xxl)),
None => tile.child(initial),
})
}
/// A thin track: `Some` fills to the value, `None` sweeps.
pub(super) fn progress_track(value: Option<f32>, cx: &App) -> AnyElement {
let theme = cx.theme();
let colors = &theme.colors;
let fill = div()
.absolute()
.top_0()
.h_full()
.rounded_full()
.bg(colors.fg);
let track = div()
.relative()
.w_full()
.h(theme.progress_thickness())
.rounded_full()
.overflow_hidden()
.bg(colors.border);
match value {
Some(value) => {
assert!(
(0.0..=1.0).contains(&value),
"progress {value} is not 0..=1"
);
track
.child(fill.left_0().w(relative(value)))
.into_any_element()
}
None if theme.reduced_motion => track
.child(fill.left(relative(0.35)).w(relative(0.3)))
.into_any_element(),
None => track
.child(
fill.w(relative(0.3)).with_animation(
"progress-sweep",
Animation::new(motion::duration(motion::SLOW, cx) * 4)
.repeat()
.with_easing(motion::ease_in_out_cubic),
|bar, t| bar.left(relative(-0.3 + 1.3 * t)),
),
)
.into_any_element(),
}
}
/// Name, version, credits: the About window's content.
#[derive(IntoElement, Clone)]
pub struct AboutDialog {
name: SharedString,
version: SharedString,
icon: Option<IconName>,
description: Option<SharedString>,
links: Vec<(SharedString, SharedString)>,
copyright: Option<SharedString>,
}
impl AboutDialog {
pub fn new(name: impl Into<SharedString>, version: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
version: version.into(),
icon: None,
description: None,
links: Vec::new(),
copyright: None,
}
}
/// App glyph; the name's initial stands in without one.
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
pub fn description(mut self, text: impl Into<SharedString>) -> Self {
self.description = Some(text.into());
self
}
pub fn link(mut self, label: impl Into<SharedString>, url: impl Into<SharedString>) -> Self {
self.links.push((label.into(), url.into()));
self
}
pub fn copyright(mut self, text: impl Into<SharedString>) -> Self {
self.copyright = Some(text.into());
self
}
/// Shows it in its own window; Escape closes it.
pub fn open(self, cx: &mut App) -> anyhow::Result<WindowHandle<Hosted<AboutDialog>>> {
let size = cx.theme().about_window();
open_hosted(format!("About {}", self.name), size, self, cx)
}
}
impl RenderOnce for AboutDialog {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
div()
.flex()
.flex_col()
.items_center()
.gap_2()
.px_8()
.pt_4()
.pb_8()
.w_full()
.child(mark(&self.name, self.icon, cx))
.child(div().pt_3().child(Title::new(self.name.clone())))
.child(CopyableText::new(
"about-version",
format!("Version {}", self.version),
))
.when_some(self.description, |about, text| {
about.child(div().pt_2().text_center().child(Paragraph::new(text)))
})
.when(!self.links.is_empty(), |about| {
about.child(
div()
.flex()
.gap_4()
.pt_2()
.children(
self.links
.into_iter()
.enumerate()
.map(|(ix, (label, url))| {
ExternalLink::new(("about-link", ix), label, url)
}),
),
)
})
.when_some(self.copyright, |about, text| {
about.child(div().pt_4().child(Caption::new(text)))
})
}
}
/// First screen while an app loads: mark, name, progress.
#[derive(IntoElement, Clone)]
pub struct SplashScreen {
name: SharedString,
icon: Option<IconName>,
tagline: Option<SharedString>,
status: Option<SharedString>,
progress: Option<f32>,
}
impl SplashScreen {
pub fn new(name: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
icon: None,
tagline: None,
status: None,
progress: None,
}
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
pub fn tagline(mut self, text: impl Into<SharedString>) -> Self {
self.tagline = Some(text.into());
self
}
pub fn status(mut self, text: impl Into<SharedString>) -> Self {
self.status = Some(text.into());
self
}
/// 0 to 1; `None` shows a moving bar.
pub fn progress(mut self, progress: Option<f32>) -> Self {
if let Some(value) = progress {
assert!(
(0.0..=1.0).contains(&value),
"splash progress {value} is not 0..=1"
);
}
self.progress = progress;
self
}
/// Shows it borderless and centered. Close it with `window.remove_window()`.
pub fn open(self, cx: &mut App) -> anyhow::Result<WindowHandle<Hosted<SplashScreen>>> {
let size = cx.theme().splash_window();
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(None, size, cx))),
titlebar: None,
is_movable: false,
is_resizable: false,
is_minimizable: false,
..Default::default()
};
let handle = cx.open_window(options, |window, cx| {
let focus = cx.focus_handle();
window.focus(&focus);
cx.new(|_| Hosted::new(self, focus))
})?;
log::info!("splash: shown");
Ok(handle)
}
}
impl RenderOnce for SplashScreen {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = &theme.colors;
let enter = motion::duration(motion::SLOW, cx);
let track = progress_track(self.progress, cx);
div()
.size_full()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap_2()
.px_10()
.child(
div()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(mark(&self.name, self.icon, cx))
.child(div().pt_2().child(Title::new(self.name.clone())))
.when_some(self.tagline, |block, text| block.child(Caption::new(text)))
.with_animation(
"splash-in",
Animation::new(enter).with_easing(motion::ease_out_cubic),
|block, t| block.opacity(t).mt(motion::NUDGE * 2.0 * (1.0 - t)),
),
)
.child(
div()
.w(relative(0.6))
.pt_8()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(track)
.child(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.child(self.status.unwrap_or_default()),
),
)
}
}