mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #11218 from oxi-does-dev/feat/launcher-nord
Launcher: Nord theme and the theme picker in the Blocks look
This commit is contained in:
15 files changed
+305
-116
No files matched your search
@@ -28,13 +28,11 @@ pub const CAPTION: Step = step("night-caption", 12., Web::Fixed, 1.4, 0., 400, f
|
||||
/// A screen uses two or three of these, plus [`crate::MICRO`] for paths and counts.
|
||||
pub const TYPE: [Step; 4] = [DISPLAY, TITLE, UI, CAPTION];
|
||||
|
||||
/// Corners. A plate cuts each corner in two [`PIXEL_CORNER`] steps, like the sky's art, and its
|
||||
/// 1 px stroke follows the steps. The main action (an accent fill: Play, Create, Install) cuts one
|
||||
/// step. Other controls (buttons, fields, focus rings) round at [`RADIUS_CONTROL`]: at 1x a step
|
||||
/// vanishes on a pale fill and reads as notches on a thin ring. Pills are [`crate::RADIUS_PILL`],
|
||||
/// for chips only.
|
||||
pub const PIXEL_CORNER: f32 = 4.;
|
||||
pub const RADIUS_CONTROL: f32 = 8.;
|
||||
/// Corners. The night look is square: plates, buttons, fields and focus rings have no rounded
|
||||
/// corners and no stepped cuts. [`PIXEL_CORNER`] stays for the cut helpers' tests and callers;
|
||||
/// zero draws a plain rectangle.
|
||||
pub const PIXEL_CORNER: f32 = 0.;
|
||||
pub const RADIUS_CONTROL: f32 = 0.;
|
||||
/// The main action (Play) is taller than the other controls.
|
||||
pub const PLAY: f32 = 56.;
|
||||
|
||||
@@ -42,6 +40,22 @@ pub const PLAY: f32 = 56.;
|
||||
/// lower stops (the sunset) are too bright for light text, so text there always has a plate.
|
||||
pub const UNCARDED_STOPS: usize = 3;
|
||||
|
||||
/// The sky's hand-drawn art (the clouds, the sun and the two skylines): the colours a launcher
|
||||
/// theme paints them with, beside the sky's own bands.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SkyArt {
|
||||
/// The sun's three bands, from its rim to its middle.
|
||||
pub sun: [u32; 3],
|
||||
/// The far and the near skyline.
|
||||
pub far: u32,
|
||||
pub near: u32,
|
||||
/// The clouds far to near, and each layer's lit band.
|
||||
pub cloud: [u32; 3],
|
||||
pub lit: [u32; 3],
|
||||
pub shade: u32,
|
||||
pub star: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Night {
|
||||
/// The sky from the top of the window to the horizon, drawn in bands.
|
||||
@@ -71,6 +85,8 @@ pub struct Night {
|
||||
pub warn: u32,
|
||||
pub danger: u32,
|
||||
pub dark: bool,
|
||||
/// The sky's clouds, sun and skylines.
|
||||
pub art: SkyArt,
|
||||
}
|
||||
|
||||
pub const NIGHT: Night = Night {
|
||||
@@ -94,6 +110,15 @@ pub const NIGHT: Night = Night {
|
||||
warn: 0xf5b45a,
|
||||
danger: 0xff8a78,
|
||||
dark: true,
|
||||
art: SkyArt {
|
||||
sun: [0xffe28c, 0xffbf66, 0xff944f],
|
||||
far: 0x3a1f5e,
|
||||
near: 0x150f2e,
|
||||
cloud: [0x3b2869, 0x6e3a7e, 0xc96a86],
|
||||
lit: [0x5a3f8a, 0x9c5a92, 0xf7a98a],
|
||||
shade: 0x2a1a4c,
|
||||
star: 0xf4f2ff,
|
||||
},
|
||||
};
|
||||
|
||||
/// The light appearance: the same sky at dawn, pale blue to peach, with the night's violet.
|
||||
@@ -118,6 +143,15 @@ pub const DAWN: Night = Night {
|
||||
warn: 0x7a4f00,
|
||||
danger: 0xa3301d,
|
||||
dark: false,
|
||||
art: SkyArt {
|
||||
sun: [0xfffbea, 0xffeac2, 0xffd3a1],
|
||||
far: 0xdcd3ee,
|
||||
near: 0xbaafdd,
|
||||
cloud: [0xeef0fb, 0xfbeef3, 0xfff1e4],
|
||||
lit: [0xffffff; 3],
|
||||
shade: 0xd9cbe8,
|
||||
star: 0xffffff,
|
||||
},
|
||||
};
|
||||
|
||||
/// `top` at `alpha` over `under`, as the GPU blends them.
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
--index: 40px;
|
||||
--plate-bleed: 16px;
|
||||
--rise: 8px;
|
||||
--night-pixel-corner: 4px;
|
||||
--night-radius-control: 8px;
|
||||
--night-pixel-corner: 0px;
|
||||
--night-radius-control: 0px;
|
||||
--night-play: 56px;
|
||||
--measure: 56ch;
|
||||
--quick: 150ms;
|
||||
|
||||
@@ -772,7 +772,7 @@ impl Launcher {
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(12.))
|
||||
.child(div().flex_none().size(px(6.)).rounded_full().bg(t.success))
|
||||
.child(div().flex_none().size(px(6.)).bg(t.success))
|
||||
.child(
|
||||
text(theme::UI)
|
||||
.flex_1()
|
||||
|
||||
@@ -146,9 +146,7 @@ impl Launcher {
|
||||
.text_color(t.ink)
|
||||
.child(choice_name(choice)),
|
||||
)
|
||||
.when(chosen, |d| {
|
||||
d.child(div().size(px(6.)).rounded_full().bg(t.accent))
|
||||
})
|
||||
.when(chosen, |d| d.child(div().size(px(6.)).bg(t.accent)))
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.pick_language(choice, cx)
|
||||
})),
|
||||
|
||||
@@ -558,7 +558,7 @@ pub(super) fn content_with(
|
||||
.when(!selected.is_empty(), |d| {
|
||||
d.child(
|
||||
text(selected)
|
||||
.rounded(px(2.))
|
||||
.rounded(px(if crate::glass_ui::on() { 2. } else { 0. }))
|
||||
.bg(t.accent.opacity(theme::SELECTION)),
|
||||
)
|
||||
})
|
||||
|
||||
+8
-16
@@ -579,8 +579,7 @@ impl Launcher {
|
||||
// `window.appearance()`, so both draw the same look.
|
||||
titlebar::apply_appearance(window, settings.appearance);
|
||||
let mut this = Self {
|
||||
theme: Theme::for_appearance(settings.appearance, settings.accent, window.appearance())
|
||||
.with_glass(settings.glass_tint(), settings.glass_blur()),
|
||||
theme: crate::theme::from_settings(&settings, window.appearance()),
|
||||
page: Page::from_env(),
|
||||
switches: 0,
|
||||
content_scroll: Scrollbar::default(),
|
||||
@@ -3305,9 +3304,7 @@ impl Launcher {
|
||||
.when(latest, |d| {
|
||||
d.child(caps(tr("auto-update")).flex_none().text_color(t.muted))
|
||||
})
|
||||
.when(selected, |d| {
|
||||
d.child(div().size(px(6.)).rounded_full().bg(t.accent))
|
||||
}),
|
||||
.when(selected, |d| d.child(div().size(px(6.)).bg(t.accent))),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.pin_version((!latest).then(|| tag.clone()));
|
||||
@@ -3790,14 +3787,7 @@ impl Launcher {
|
||||
.text_size(px(theme::UI.px))
|
||||
.line_height(px(21.))
|
||||
.text_color(t.ink)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.mt(px(11.))
|
||||
.size(px(6.))
|
||||
.rounded_full()
|
||||
.bg(t.danger),
|
||||
)
|
||||
.child(div().flex_none().mt(px(11.)).size(px(6.)).bg(t.danger))
|
||||
.child(body)
|
||||
}
|
||||
|
||||
@@ -4045,6 +4035,7 @@ impl Launcher {
|
||||
Some(appearance.into_any_element()),
|
||||
&t,
|
||||
))
|
||||
.child(self.launcher_theme_row(cx))
|
||||
.child(self.accent_row(cx))
|
||||
.child(self.home_background_row(cx))
|
||||
.child(self.language_row(cx))
|
||||
@@ -5287,10 +5278,11 @@ impl Render for Launcher {
|
||||
if self.opening.is_none() && window.focused(cx).is_none() {
|
||||
window.focus(&self.root_focus, cx);
|
||||
}
|
||||
// Dark: the new design's scene; Light keeps the dawn sky until the design has a light
|
||||
// look.
|
||||
// The glass look draws the new design's scene in the dark appearance; the light look and
|
||||
// the blocky looks keep the baked sky. The sky is tiles that don't depend on the
|
||||
// window's size, so a resize costs no rebake.
|
||||
glass::begin_build(window);
|
||||
let sky = if t.dark {
|
||||
let sky = if t.glass && t.dark {
|
||||
self.scene.clone().into_any_element()
|
||||
} else {
|
||||
let (sky, next) = self.sky.render(
|
||||
|
||||
@@ -413,7 +413,7 @@ impl Launcher {
|
||||
|| (current.is_some()
|
||||
&& value.is_none()
|
||||
&& !sorts.iter().any(|(v, _)| *v == current)),
|
||||
|d| d.child(div().size(px(6.)).rounded_full().bg(t.accent)),
|
||||
|d| d.child(div().size(px(6.)).bg(t.accent)),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.mods.browse.sort_menu = false;
|
||||
|
||||
+11
-6
@@ -151,7 +151,7 @@ fn headline() -> StyledText {
|
||||
}
|
||||
|
||||
/// "HORIZON15 · 15% off" in a dashed cyan frame, as on Sculk's banner.
|
||||
fn code_chip(size: f32, height: f32) -> Div {
|
||||
fn code_chip(size: f32, height: f32, glass: bool) -> Div {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -162,7 +162,7 @@ fn code_chip(size: f32, height: f32) -> Div {
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(rgba(CYAN, 0.7))
|
||||
.rounded(px(6.))
|
||||
.rounded(px(if glass { 6. } else { 0. }))
|
||||
.font_family(crate::theme::MONO)
|
||||
.text_size(px(size))
|
||||
.whitespace_nowrap()
|
||||
@@ -203,7 +203,7 @@ pub fn card(id: &'static str, width: f32, compact: bool, t: &Theme) -> Stateful<
|
||||
.flex_none()
|
||||
.w(px(width))
|
||||
.bg(rgb(CARD))
|
||||
.rounded(px(10.))
|
||||
.rounded(px(if t.glass { 10. } else { 0. }))
|
||||
.group_hover(id, |s| s.top(px(-2.)));
|
||||
let logo = |height: f32| {
|
||||
img(LOGO.clone())
|
||||
@@ -251,7 +251,7 @@ pub fn card(id: &'static str, width: f32, compact: bool, t: &Theme) -> Stateful<
|
||||
.px(px(12.))
|
||||
.pt(px(6.))
|
||||
.pb(px(12.))
|
||||
.child(code_chip(11., 26.).flex_1()),
|
||||
.child(code_chip(11., 26., t.glass).flex_1()),
|
||||
)
|
||||
} else {
|
||||
let art_h = width * ART_HEIGHT / ART_WIDTH;
|
||||
@@ -260,7 +260,7 @@ pub fn card(id: &'static str, width: f32, compact: bool, t: &Theme) -> Stateful<
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h(px(36.))
|
||||
.rounded(px(8.))
|
||||
.rounded(px(if t.glass { 8. } else { 0. }))
|
||||
.bg(rgb(CYAN))
|
||||
.group_hover(id, |s| s.bg(rgb(CYAN_HOVER)))
|
||||
.text_color(rgb(ON_CYAN))
|
||||
@@ -304,7 +304,12 @@ pub fn card(id: &'static str, width: f32, compact: bool, t: &Theme) -> Stateful<
|
||||
.child(horizon_launcher::i18n::tr("Ready in under a minute")),
|
||||
)
|
||||
.child(div().mt(px(16.)).child(button))
|
||||
.child(div().mt(px(8.)).flex().child(code_chip(12., 30.).flex_1())),
|
||||
.child(
|
||||
div()
|
||||
.mt(px(8.))
|
||||
.flex()
|
||||
.child(code_chip(12., 30., t.glass).flex_1()),
|
||||
),
|
||||
)
|
||||
};
|
||||
let el = div()
|
||||
|
||||
@@ -1570,7 +1570,7 @@ impl Launcher {
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(10.))
|
||||
.child(div().size(px(8.)).rounded_full().bg(dot))
|
||||
.child(div().size(px(8.)).bg(dot))
|
||||
.child(caps(phase_label(&status)).text_color(t.muted))
|
||||
.when_some(status.uptime.filter(|_| running), |d, up| {
|
||||
d.child(caps(format!("· up {}", uptime(up))).text_color(t.muted))
|
||||
@@ -1691,7 +1691,7 @@ impl Launcher {
|
||||
.items_center()
|
||||
.gap(px(12.))
|
||||
.p(px(16.))
|
||||
.child(div().flex_none().size(px(6.)).rounded_full().bg(t.success))
|
||||
.child(div().flex_none().size(px(6.)).bg(t.success))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -1714,7 +1714,7 @@ impl Launcher {
|
||||
.items_center()
|
||||
.gap(px(12.))
|
||||
.p(px(16.))
|
||||
.child(div().flex_none().size(px(6.)).rounded_full().bg(t.accent))
|
||||
.child(div().flex_none().size(px(6.)).bg(t.accent))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -3054,7 +3054,6 @@ impl Launcher {
|
||||
.child(
|
||||
div()
|
||||
.size(px(12.))
|
||||
.rounded_full()
|
||||
.border_1()
|
||||
.border_color(if chosen { t.accent } else { t.muted })
|
||||
.when(chosen, |d| d.bg(t.accent)),
|
||||
@@ -3128,7 +3127,6 @@ impl Launcher {
|
||||
.id(id)
|
||||
.flex_none()
|
||||
.size(px(18.))
|
||||
.rounded(px(4.))
|
||||
.border_1()
|
||||
.border_color(if on { t.accent } else { t.muted })
|
||||
.bg(if on { t.accent } else { t.surface })
|
||||
|
||||
+9
-41
@@ -9,7 +9,7 @@
|
||||
//! motion, and stops 10 s after the window loses focus ([`motion`]).
|
||||
use crate::art::bake;
|
||||
use gpui::{div, img, prelude::*, px, rgb, Div, RenderImage};
|
||||
use horizon_theme::night::Night;
|
||||
use horizon_theme::night::{Night, SkyArt};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -152,38 +152,6 @@ const SUN_RADIUS: usize = 15;
|
||||
const SUN_RISE: usize = 8;
|
||||
const SUN_X: f32 = 0.72;
|
||||
|
||||
/// The art's colours in one appearance, beside the night tokens' sky.
|
||||
struct Colors {
|
||||
sun: [u32; 3],
|
||||
far: u32,
|
||||
near: u32,
|
||||
/// Clouds far to near, and each layer's lit band.
|
||||
cloud: [u32; 3],
|
||||
lit: [u32; 3],
|
||||
shade: u32,
|
||||
star: u32,
|
||||
}
|
||||
|
||||
const NIGHT_ART: Colors = Colors {
|
||||
sun: [0xffe28c, 0xffbf66, 0xff944f],
|
||||
far: 0x3a1f5e,
|
||||
near: 0x150f2e,
|
||||
cloud: [0x3b2869, 0x6e3a7e, 0xc96a86],
|
||||
lit: [0x5a3f8a, 0x9c5a92, 0xf7a98a],
|
||||
shade: 0x2a1a4c,
|
||||
star: 0xf4f2ff,
|
||||
};
|
||||
|
||||
const DAWN_ART: Colors = Colors {
|
||||
sun: [0xfffbea, 0xffeac2, 0xffd3a1],
|
||||
far: 0xdcd3ee,
|
||||
near: 0xbaafdd,
|
||||
cloud: [0xeef0fb, 0xfbeef3, 0xfff1e4],
|
||||
lit: [0xffffff; 3],
|
||||
shade: 0xd9cbe8,
|
||||
star: 0xffffff,
|
||||
};
|
||||
|
||||
/// How much white the light look lays over the dawn's colours.
|
||||
const MORNING: f32 = 0.55;
|
||||
|
||||
@@ -255,7 +223,7 @@ struct Layers {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sky {
|
||||
baked: Option<((bool, u32), Layers)>,
|
||||
baked: Option<((SkyArt, u32), Layers)>,
|
||||
/// Images replaced by a new appearance or scale, still in the window's sprite atlas.
|
||||
replaced: Vec<Arc<RenderImage>>,
|
||||
clock: Clock,
|
||||
@@ -285,9 +253,9 @@ impl Sky {
|
||||
};
|
||||
self.clock.set(run, now);
|
||||
let t = self.clock.at(now).as_secs_f32();
|
||||
let layers = self.layers(n.dark, scale);
|
||||
let layers = self.layers(n.art, scale);
|
||||
let snap = |v: f32| (v * scale).round() / scale;
|
||||
let art = if n.dark { &NIGHT_ART } else { &DAWN_ART };
|
||||
let art = &n.art;
|
||||
|
||||
let mut sky = div()
|
||||
.absolute()
|
||||
@@ -398,10 +366,10 @@ impl Sky {
|
||||
(sky, next)
|
||||
}
|
||||
|
||||
fn layers(&mut self, dark: bool, scale: f32) -> &Layers {
|
||||
let key = (dark, (scale * 100.).round() as u32);
|
||||
fn layers(&mut self, art: SkyArt, scale: f32) -> &Layers {
|
||||
let key = (art, (scale * 100.).round() as u32);
|
||||
if !matches!(&self.baked, Some((k, _)) if *k == key) {
|
||||
let art = if dark { &NIGHT_ART } else { &DAWN_ART };
|
||||
let art = &art;
|
||||
let layers = Layers {
|
||||
clouds: std::array::from_fn(|i| {
|
||||
let l = &CLOUDS[i];
|
||||
@@ -532,7 +500,7 @@ fn stamp(
|
||||
}
|
||||
}
|
||||
|
||||
fn clouds(layer: &CloudLayer, art: &Colors, depth: usize) -> Vec<[u8; 4]> {
|
||||
fn clouds(layer: &CloudLayer, art: &SkyArt, depth: usize) -> Vec<[u8; 4]> {
|
||||
let mut out = vec![[0; 4]; layer.period * CLOUD_ROWS];
|
||||
for &(x, y, sprite) in layer.clouds {
|
||||
stamp(&mut out, layer.period, (x, y), sprite, |m| match m {
|
||||
@@ -578,7 +546,7 @@ fn skyline(line: &Skyline, color: u32, scale: f32) -> Arc<RenderImage> {
|
||||
|
||||
/// The upper half of a sun cut by the horizon, in three bands of colour, its lower half sliced by
|
||||
/// rows of sky like Horizon's mark.
|
||||
fn sun(art: &Colors) -> Vec<[u8; 4]> {
|
||||
fn sun(art: &SkyArt) -> Vec<[u8; 4]> {
|
||||
let (r, w) = (SUN_RADIUS as f32, SUN_RADIUS * 2);
|
||||
let mut out = vec![[0; 4]; w * SUN_RADIUS];
|
||||
for y in 0..SUN_RADIUS {
|
||||
|
||||
@@ -119,9 +119,7 @@ impl Launcher {
|
||||
.child(text(theme::LABEL).truncate().text_color(t.ink).child(name))
|
||||
.child(text(theme::CAPTION).text_color(t.muted).child(how)),
|
||||
)
|
||||
.when(chosen, |d| {
|
||||
d.child(div().size(px(6.)).rounded_full().bg(t.accent))
|
||||
})
|
||||
.when(chosen, |d| d.child(div().size(px(6.)).bg(t.accent)))
|
||||
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
|
||||
this.play_as(who.clone());
|
||||
cx.notify();
|
||||
|
||||
+57
-9
@@ -3,8 +3,8 @@
|
||||
//! Horizon's violet) in Bricolage Grotesque, with Geist Mono for paths and codes (all bundled, SIL
|
||||
//! Open Font License). Screens not rebuilt yet draw the Blocks parts in the night's colours.
|
||||
use gpui::{rgb, Hsla, WindowAppearance};
|
||||
use horizon_launcher::backend::Appearance;
|
||||
use horizon_launcher::themes::Accent;
|
||||
use horizon_launcher::backend::{Appearance, Settings};
|
||||
use horizon_launcher::themes::{self, Accent};
|
||||
use horizon_theme as tokens;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -74,6 +74,8 @@ pub struct Theme {
|
||||
pub on_face: Hsla,
|
||||
/// The night look's own tokens (the sky, plates with their opacity), for rebuilt screens.
|
||||
pub night: Night,
|
||||
/// The accent this look was built from, before the glass look's mixes.
|
||||
pub accent_choice: Accent,
|
||||
pub dark: bool,
|
||||
/// Whether the pages draw the redesign's glass look: both appearances do (light with ink
|
||||
/// for white and light plates, `glass_ui::light`).
|
||||
@@ -106,13 +108,36 @@ pub fn pinned_dark(choice: Appearance) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The look Settings ask for: the appearance, accent and launcher theme, at the player's Glass
|
||||
/// and Blur.
|
||||
pub fn from_settings(settings: &Settings, system: WindowAppearance) -> Theme {
|
||||
let mut theme = Theme::for_appearance(settings.appearance, settings.accent, system);
|
||||
if let Some(picked) = themes::theme(settings.theme.as_deref()) {
|
||||
theme = theme.with_launcher_theme(picked);
|
||||
}
|
||||
theme.with_glass(settings.glass_tint(), settings.glass_blur())
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
/// The pinned look (`pinned_dark`: dark unless the player picked another), the system's when
|
||||
/// they asked to follow it, in the player's accent.
|
||||
/// This look under another sky (a launcher theme from Settings), top to bottom.
|
||||
pub fn with_sky(mut self, sky: [u32; 6]) -> Self {
|
||||
self.night.sky = sky;
|
||||
self
|
||||
/// A launcher theme from Settings: its sky, or its whole palette (dark and light) when it
|
||||
/// brings one, keeping the player's Glass and Blur.
|
||||
pub fn with_launcher_theme(mut self, picked: &themes::Theme) -> Self {
|
||||
let Some((dark, light)) = picked.palette else {
|
||||
self.night.sky = picked.sky;
|
||||
return self;
|
||||
};
|
||||
let night = if self.dark { *dark } else { *light };
|
||||
let accent = self.accent_choice;
|
||||
// A theme with its own palette draws the square, solid Blocks look: no glass plates, no
|
||||
// rounded corners. The glass look's own colours stay the mockup's violet ones.
|
||||
let mut themed = Self::from(accent.apply(night.blocks()), night, accent);
|
||||
themed.look = tokens::glass::Glass::new(accent.rgb(), self.look.tint, self.look.blur);
|
||||
if !themed.dark {
|
||||
themed.look = light_look(themed.look);
|
||||
}
|
||||
themed
|
||||
}
|
||||
|
||||
/// The glass look with the player's Glass (how much white the plates take, 0 to 1) and Blur
|
||||
@@ -132,7 +157,7 @@ impl Theme {
|
||||
);
|
||||
let dark = pinned_dark(choice).unwrap_or(system_dark);
|
||||
let night = if dark { night::NIGHT } else { night::DAWN };
|
||||
Self::from(accent.apply(night.blocks()), night).glass(accent)
|
||||
Self::from(accent.apply(night.blocks()), night, accent).glass(accent)
|
||||
}
|
||||
|
||||
/// The dark appearance draws the glass look ("Glass" in `docs/design-system.md`): what the
|
||||
@@ -178,12 +203,13 @@ impl Theme {
|
||||
face_lip: none,
|
||||
on_face: c(look.on_accent),
|
||||
look,
|
||||
accent_choice: accent,
|
||||
glass: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
fn from(p: tokens::Blocks, night: Night) -> Self {
|
||||
fn from(p: tokens::Blocks, night: Night, accent: Accent) -> Self {
|
||||
Self {
|
||||
look: tokens::glass::Glass::default_look(),
|
||||
page: c(p.page),
|
||||
@@ -213,6 +239,7 @@ impl Theme {
|
||||
face_lip: c(p.face_lip),
|
||||
on_face: c(p.on_face),
|
||||
night,
|
||||
accent_choice: accent,
|
||||
dark: p.dark,
|
||||
glass: false,
|
||||
}
|
||||
@@ -224,7 +251,28 @@ pub const SELECTION: f32 = 0.22;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{tokens, SELECTION};
|
||||
use super::{from_settings, tokens, SELECTION};
|
||||
use gpui::WindowAppearance;
|
||||
use horizon_launcher::backend::{Appearance, Settings};
|
||||
use horizon_launcher::themes::Accent;
|
||||
|
||||
/// Nord asks for the solid, square Blocks look: no glass, and its own polar-night palette
|
||||
/// with the frost accent rather than Horizon's violet glass colours.
|
||||
#[test]
|
||||
fn a_palette_theme_is_solid_and_square() {
|
||||
let settings = Settings {
|
||||
appearance: Appearance::Dark,
|
||||
accent: Accent::from_rgb(0x88c0d0),
|
||||
theme: Some("nord".into()),
|
||||
..Settings::default()
|
||||
};
|
||||
let theme = from_settings(&settings, WindowAppearance::Dark);
|
||||
assert!(!theme.glass, "Nord draws the blocks look");
|
||||
assert!(theme.dark);
|
||||
assert_eq!(theme.night.page, 0x2e3440);
|
||||
assert_eq!(theme.night.accent, 0x88c0d0);
|
||||
assert_eq!(theme.accent_choice.rgb(), 0x88c0d0);
|
||||
}
|
||||
|
||||
/// `top` at `alpha` over `under`, as the GPU blends them.
|
||||
fn blend(top: u32, under: u32, alpha: f32) -> u32 {
|
||||
|
||||
+108
-5
@@ -6,6 +6,7 @@
|
||||
//! accent and the main action's face, bevel and lip) to it, keeping their saturation and
|
||||
//! lightness, then darkens or lightens each just enough to read as well as the violet does: white
|
||||
//! on the face at 4.5:1, the accent at 3:1 on every background.
|
||||
use horizon_theme::night::{Night, SkyArt};
|
||||
use horizon_theme::{contrast, Blocks};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -19,27 +20,123 @@ pub struct Accent {
|
||||
rgb: Option<u32>,
|
||||
}
|
||||
|
||||
/// The accent a launcher theme brings: a hue in degrees (First Light) or one exact colour (Nord's
|
||||
/// frost).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ThemeAccent {
|
||||
Hue(f32),
|
||||
Colour(u32),
|
||||
}
|
||||
|
||||
impl ThemeAccent {
|
||||
pub fn accent(self) -> Accent {
|
||||
match self {
|
||||
Self::Hue(hue) => Accent::custom(hue),
|
||||
Self::Colour(rgb) => Accent::from_rgb(rgb),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A launcher theme: free for everyone in Settings › Appearance, never sold or claimed. Each
|
||||
/// has a sky (six stops, top to bottom, behind the light appearance) and the accent it brings.
|
||||
/// A theme that brings a whole palette carries its dark and light [`Night`] and paints every
|
||||
/// surface with it; the other themes only change the sky.
|
||||
pub struct Theme {
|
||||
/// Its name in `Settings::theme`.
|
||||
pub id: &'static str,
|
||||
/// Its name in Settings, in English (through `tr`).
|
||||
pub name: &'static str,
|
||||
pub sky: [u32; 6],
|
||||
pub accent_hue: f32,
|
||||
pub accent: ThemeAccent,
|
||||
/// The theme's own palette (dark, light), for a theme that is more than a sky.
|
||||
pub palette: Option<(&'static Night, &'static Night)>,
|
||||
}
|
||||
|
||||
/// Nord's polar night and snow storm, with the frost accent.
|
||||
pub const NORD_DARK: Night = Night {
|
||||
sky: [0x2e3440, 0x2e3440, 0x313845, 0x353d4b, 0x3b4252, 0x434c5e],
|
||||
page: 0x2e3440,
|
||||
side: 0x292e39,
|
||||
card: 0x3b4252,
|
||||
card_alpha: 0.88,
|
||||
card_hi: 0x434c5e,
|
||||
card_hi_alpha: 0.92,
|
||||
stroke: 0xd8dee9,
|
||||
stroke_alpha: 0.10,
|
||||
rule_alpha: 0.10,
|
||||
ink: 0xeceff4,
|
||||
ink_soft: 0xd8dee9,
|
||||
muted: 0xb6bfca,
|
||||
accent: 0x88c0d0,
|
||||
accent_hi: 0x9fd3d3,
|
||||
on_accent: 0x2e3440,
|
||||
success: 0xafc99a,
|
||||
warn: 0xebcb8b,
|
||||
danger: 0xeeb3b8,
|
||||
dark: true,
|
||||
art: SkyArt {
|
||||
sun: [0x8fbcbb, 0x88c0d0, 0x5e81ac],
|
||||
far: 0x3b4252,
|
||||
near: 0x272c36,
|
||||
cloud: [0x3b4252, 0x434c5e, 0x4c566a],
|
||||
lit: [0x4c566a, 0x5e6a7d, 0x81a1c1],
|
||||
shade: 0x272c36,
|
||||
star: 0xeceff4,
|
||||
},
|
||||
};
|
||||
|
||||
pub const NORD_LIGHT: Night = Night {
|
||||
sky: [0xeceff4, 0xeceff4, 0xe5e9f0, 0xe5e9f0, 0xd8dee9, 0xc8d2e0],
|
||||
page: 0xeceff4,
|
||||
side: 0xe5e9f0,
|
||||
card: 0xf8f9fb,
|
||||
card_alpha: 0.90,
|
||||
card_hi: 0xffffff,
|
||||
card_hi_alpha: 0.95,
|
||||
stroke: 0x4c566a,
|
||||
stroke_alpha: 0.14,
|
||||
rule_alpha: 0.10,
|
||||
ink: 0x2e3440,
|
||||
ink_soft: 0x434c5e,
|
||||
muted: 0x536070,
|
||||
accent: 0x4c6a92,
|
||||
accent_hi: 0x4c6a92,
|
||||
on_accent: 0xffffff,
|
||||
success: 0x3f6b2e,
|
||||
warn: 0x7a5d00,
|
||||
danger: 0xa8323f,
|
||||
dark: false,
|
||||
art: SkyArt {
|
||||
sun: [0xffffff, 0xeceff4, 0xd8dee9],
|
||||
far: 0xd8dee9,
|
||||
near: 0xc8d2e0,
|
||||
cloud: [0xf8f9fb, 0xeceff4, 0xe5e9f0],
|
||||
lit: [0xffffff; 3],
|
||||
shade: 0xd8dee9,
|
||||
star: 0xffffff,
|
||||
},
|
||||
};
|
||||
|
||||
/// First Light: the sky just before sunrise, with a harbour skyline.
|
||||
pub const FIRST_LIGHT: Theme = Theme {
|
||||
id: "first_light",
|
||||
name: "First Light",
|
||||
sky: [0x0b1030, 0x1d2a63, 0x3d4c94, 0x7f78c0, 0xe2a0b0, 0xffd6a0],
|
||||
accent_hue: 246.,
|
||||
accent: ThemeAccent::Hue(246.),
|
||||
palette: None,
|
||||
};
|
||||
|
||||
/// Nord: the Nord palette's polar night, snow storm and frost.
|
||||
pub const NORD: Theme = Theme {
|
||||
id: "nord",
|
||||
name: "Nord",
|
||||
sky: NORD_DARK.sky,
|
||||
accent: ThemeAccent::Colour(0x88c0d0),
|
||||
palette: Some((&NORD_DARK, &NORD_LIGHT)),
|
||||
};
|
||||
|
||||
/// The launcher themes past Horizon's own sky.
|
||||
pub const THEMES: [Theme; 1] = [FIRST_LIGHT];
|
||||
pub const THEMES: [Theme; 2] = [FIRST_LIGHT, NORD];
|
||||
|
||||
/// The theme named `id` (`Settings::theme`); None for Horizon's own sky or a name this launcher
|
||||
/// doesn't know.
|
||||
@@ -189,8 +286,14 @@ impl Accent {
|
||||
let (_, s, l) = hsl(c);
|
||||
(hue, s, l)
|
||||
};
|
||||
// The face holds white text.
|
||||
let face = fit(turn(p.face), |c| contrast(p.on_face, c) >= 4.5, -1.);
|
||||
// The face holds its label: white on a dark face, Nord's ink on a light one. Move the
|
||||
// face away from the label until the two read.
|
||||
let step = if contrast(p.on_face, 0x000000) < contrast(p.on_face, 0xffffff) {
|
||||
1.
|
||||
} else {
|
||||
-1.
|
||||
};
|
||||
let face = fit(turn(p.face), |c| contrast(p.on_face, c) >= 4.5, step);
|
||||
let (_, _, face_l) = hsl(face);
|
||||
let (h, s, l) = turn(p.face_bevel);
|
||||
p.face_bevel = from_hsl(h, s, l.max(face_l + 0.08).min(0.92));
|
||||
|
||||
@@ -11,7 +11,12 @@ fn blend(top: u32, under: u32, alpha: f32) -> u32 {
|
||||
|
||||
#[test]
|
||||
fn every_accent_reads_as_well_as_the_violet() {
|
||||
for base in [NIGHT.blocks(), DAWN.blocks()] {
|
||||
for base in [
|
||||
NIGHT.blocks(),
|
||||
DAWN.blocks(),
|
||||
NORD_DARK.blocks(),
|
||||
NORD_LIGHT.blocks(),
|
||||
] {
|
||||
for accent in Accent::ALL
|
||||
.into_iter()
|
||||
.chain((0..720).map(|i| Accent::custom(i as f32 * 0.5)))
|
||||
@@ -61,6 +66,53 @@ fn colours_go_to_hsl_and_back() {
|
||||
assert_eq!(serde_json::to_string(&Accent::AMBER).unwrap(), "\"amber\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nord_text_reads_over_its_sky() {
|
||||
use horizon_theme::night::{over, UNCARDED_STOPS};
|
||||
|
||||
for p in [NORD_DARK, NORD_LIGHT] {
|
||||
let look = if p.dark { "dark" } else { "light" };
|
||||
let texts = [
|
||||
("ink", p.ink),
|
||||
("ink_soft", p.ink_soft),
|
||||
("muted", p.muted),
|
||||
("accent_hi", p.accent_hi),
|
||||
("success", p.success),
|
||||
("warn", p.warn),
|
||||
("danger", p.danger),
|
||||
];
|
||||
for (i, &sky) in p.sky.iter().enumerate() {
|
||||
for (plate, top, alpha) in [
|
||||
("card", p.card, p.card_alpha),
|
||||
("card_hi", p.card_hi, p.card_hi_alpha),
|
||||
] {
|
||||
let bg = over(top, alpha, sky);
|
||||
for (tn, fg) in texts {
|
||||
let ratio = contrast(fg, bg);
|
||||
assert!(
|
||||
ratio >= 4.5,
|
||||
"{look} {tn} on {plate} over sky {i}: {ratio:.2}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i, &sky) in p.sky[..UNCARDED_STOPS].iter().enumerate() {
|
||||
for (tn, fg) in &texts[..4] {
|
||||
let ratio = contrast(*fg, sky);
|
||||
assert!(ratio >= 4.5, "{look} {tn} on sky {i}: {ratio:.2}");
|
||||
}
|
||||
}
|
||||
for (bn, bg) in [("page", p.page), ("side", p.side)] {
|
||||
for (tn, fg) in &texts[..4] {
|
||||
let ratio = contrast(*fg, bg);
|
||||
assert!(ratio >= 4.5, "{look} {tn} on {bn}: {ratio:.2}");
|
||||
}
|
||||
}
|
||||
let label = contrast(p.on_accent, p.accent);
|
||||
assert!(label >= 4.5, "{look} the main action's label: {label:.2}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_background_is_a_picture_kept_once() {
|
||||
let root = std::env::temp_dir().join(format!("horizon-background-{}", std::process::id()));
|
||||
|
||||
@@ -46,31 +46,22 @@ impl Launcher {
|
||||
/// The theme Settings ask for, under the window's own light or dark: the player's look,
|
||||
/// under the launcher theme's sky if they picked one.
|
||||
pub(super) fn settings_theme(&self, window: &Window) -> Theme {
|
||||
let theme = Theme::for_appearance(
|
||||
self.settings.appearance,
|
||||
self.settings.accent,
|
||||
window.appearance(),
|
||||
)
|
||||
.with_glass(self.settings.glass_tint(), self.settings.glass_blur());
|
||||
match themes::theme(self.settings.theme.as_deref()) {
|
||||
Some(picked) => theme.with_sky(picked.sky),
|
||||
None => theme,
|
||||
}
|
||||
crate::theme::from_settings(&self.settings, window.appearance())
|
||||
}
|
||||
|
||||
/// Settings' launcher theme: Horizon's own sky or First Light, free for everyone. Picking one
|
||||
/// brings its accent too; the accent row can change it after.
|
||||
/// Settings' launcher theme: Horizon's own sky, First Light or Nord, free for everyone.
|
||||
/// Picking one brings its accent too; the accent row can change it after.
|
||||
pub(super) fn launcher_theme_row(&mut self, cx: &mut Context<Self>) -> Div {
|
||||
let t = self.theme;
|
||||
let current = themes::theme(self.settings.theme.as_deref()).map(|th| th.id);
|
||||
let mut options: Vec<(Option<&'static str>, &'static str)> = vec![(None, "Horizon")];
|
||||
options.extend(themes::THEMES.iter().map(|th| (Some(th.id), th.name)));
|
||||
let this = cx.entity().downgrade();
|
||||
let picker = ui::segments("launcher-theme", &options, current, &t, move |id, _, cx| {
|
||||
let picker = segments("launcher-theme", &options, current, &t, move |id, _, cx| {
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.settings.theme = id.map(str::to_owned);
|
||||
this.settings.accent = match themes::theme(id) {
|
||||
Some(picked) => Accent::custom(picked.accent_hue),
|
||||
Some(picked) => picked.accent.accent(),
|
||||
None => Accent::VIOLET,
|
||||
};
|
||||
this.save_settings();
|
||||
@@ -78,7 +69,8 @@ impl Launcher {
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
ui::setting_row(
|
||||
index_row(
|
||||
0,
|
||||
tr("Launcher theme"),
|
||||
Some(
|
||||
div()
|
||||
@@ -88,6 +80,7 @@ impl Launcher {
|
||||
.into_any_element(),
|
||||
),
|
||||
Some(picker.into_any_element()),
|
||||
&t,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user