1use crate::{
2 highlighter::HighlightTheme, list::ListSettings, notification::NotificationSettings,
3 scroll::ScrollbarMode, sheet::SheetSettings,
4};
5use gpui::{
6 App, Global, Hsla, IsZero as _, Pixels, SharedString, Window, WindowAppearance,
7 prelude::FluentBuilder as _, px,
8};
9pub use gpui_base::{
10 ColorTokens, RadiusTokens, SemanticThemeTokens, ShadowTokens, SpacingTokens, TextStyleToken,
11 TypographyTokens,
12};
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use std::{
16 ops::{Deref, DerefMut},
17 rc::Rc,
18 sync::Arc,
19 time::Duration,
20};
21
22mod color;
23mod mono_font;
24mod motion;
25mod registry;
26mod schema;
27mod system_font;
28mod theme_color;
29
30pub use color::*;
31pub use motion::*;
32pub use registry::*;
33pub use schema::*;
34pub use theme_color::*;
35
36pub fn init(cx: &mut App) {
37 registry::init(cx);
38
39 Theme::change(ThemeMode::Light, None, cx);
41 Theme::sync_scrollbar_appearance(cx);
42}
43
44pub trait ActiveTheme {
45 fn theme(&self) -> &Theme;
46}
47
48impl ActiveTheme for App {
49 #[inline(always)]
50 fn theme(&self) -> &Theme {
51 Theme::global(self)
52 }
53}
54
55fn default_true() -> bool {
56 true
57}
58
59const RADIUS_FULL: Pixels = px(9999.);
63
64const SCROLLBAR_IDLE: Duration = Duration::from_secs(2);
66const SCROLLBAR_ENTER: Duration = Duration::from_millis(300);
68const SCROLLBAR_EXIT: Duration = Duration::from_millis(500);
70const SCROLLBAR_EXPAND: Duration = Duration::from_millis(300);
72
73const MOBILE_SCROLLBAR_THUMB_WIDTH: Pixels = px(3.);
77const MOBILE_SCROLLBAR_THUMB_INSET: Pixels = px(2.);
80const SCROLLBAR_THUMB_HOVER_WIDTH: Pixels = px(6.);
83const SCROLLBAR_THUMB_ACTIVE_WIDTH: Pixels = px(8.);
85const SCROLLBAR_THUMB_INSET: Pixels = px(4.);
87
88fn scrollbar_motion(mode: ScrollbarMode) -> gpui_base::ScrollbarMotion {
93 gpui_base::ScrollbarMotion::default()
94 .with_idle(SCROLLBAR_IDLE)
95 .with_enter(SCROLLBAR_ENTER)
96 .with_exit(SCROLLBAR_EXIT)
97 .with_expand(SCROLLBAR_EXPAND)
98 .with_entrance(gpui_base::ScrollbarEntrance::Fade)
99 .with_thumb_hover_entrance(match mode {
100 ScrollbarMode::Scrolling | ScrollbarMode::Always => gpui_base::ScrollbarEntrance::Fade,
101 ScrollbarMode::Hover => gpui_base::ScrollbarEntrance::SlideAndFade,
102 })
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
107pub struct Theme {
108 pub colors: ThemeColor,
109 #[serde(default)]
114 pub tokens: ThemeTokens,
115 pub highlight_theme: Arc<HighlightTheme>,
116 pub light_theme: Rc<ThemeConfig>,
117 pub dark_theme: Rc<ThemeConfig>,
118
119 pub mode: ThemeMode,
120 pub font_family: SharedString,
127 pub font_size: Pixels,
129 pub mono_font_family: SharedString,
142 pub mono_font_size: Pixels,
144 pub radius: Pixels,
146 pub radius_lg: Pixels,
148 pub shadow: bool,
149 #[serde(default = "default_true")]
156 pub focus_ring: bool,
157 pub transparent: Hsla,
158 #[serde(alias = "scrollbar_show")]
160 pub scrollbar_mode: ScrollbarMode,
161 #[serde(skip)]
163 pub notification: NotificationSettings,
164 pub list: ListSettings,
166 pub sheet: SheetSettings,
168 #[serde(skip)]
170 pub motion: MotionTokens,
171}
172
173impl Default for Theme {
174 fn default() -> Self {
175 Self::from(&ThemeColor::default())
176 }
177}
178
179impl Deref for Theme {
180 type Target = ThemeColor;
181
182 fn deref(&self) -> &Self::Target {
183 &self.colors
184 }
185}
186
187impl DerefMut for Theme {
188 fn deref_mut(&mut self) -> &mut Self::Target {
189 &mut self.colors
190 }
191}
192
193impl Global for Theme {}
194
195impl Theme {
196 #[inline(always)]
198 pub fn global(cx: &App) -> &Theme {
199 cx.global::<Theme>()
200 }
201
202 #[inline(always)]
208 pub fn global_mut(cx: &mut App) -> &mut Theme {
209 cx.global_mut::<Theme>()
210 }
211
212 #[inline(always)]
214 pub fn is_dark(&self) -> bool {
215 self.mode.is_dark()
216 }
217
218 pub fn theme_name(&self) -> &SharedString {
220 if self.is_dark() {
221 &self.dark_theme.name
222 } else {
223 &self.light_theme.name
224 }
225 }
226
227 pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
229 let appearance = window
232 .as_ref()
233 .map(|window| window.appearance())
234 .unwrap_or_else(|| cx.window_appearance());
235
236 Self::change(appearance, window, cx);
237 }
238
239 pub fn sync_scrollbar_appearance(cx: &mut App) {
241 let mode = if cx.should_auto_hide_scrollbars() {
242 ScrollbarMode::Scrolling
243 } else {
244 ScrollbarMode::Hover
245 };
246 Self::set_scrollbar_mode(mode, cx);
247 }
248
249 pub fn set_scrollbar_mode(mode: ScrollbarMode, cx: &mut App) {
251 Theme::global_mut(cx).scrollbar_mode = mode;
252 let base_theme = gpui_base::Theme::global_mut(cx);
253 base_theme.scrollbar = base_theme
254 .scrollbar
255 .clone()
256 .with_mode(mode)
257 .with_motion(scrollbar_motion(mode));
258 }
259
260 pub fn change(mode: impl Into<ThemeMode>, window: Option<&mut Window>, cx: &mut App) {
262 let mode = mode.into();
263 if !cx.has_global::<Theme>() {
264 let mut theme = Theme::default();
265 theme.light_theme = ThemeRegistry::global(cx).default_light_theme().clone();
266 theme.dark_theme = ThemeRegistry::global(cx).default_dark_theme().clone();
267 cx.set_global(theme);
268 }
269
270 {
271 let theme = cx.global_mut::<Theme>();
272 theme.mode = mode;
273 if mode.is_dark() {
274 theme.apply_config(&theme.dark_theme.clone());
275 } else {
276 theme.apply_config(&theme.light_theme.clone());
277 }
278 }
279 system_font::resolve_default_font(cx);
280 mono_font::resolve_default_mono_font(cx);
281 let theme = cx.global::<Theme>().clone();
282
283 let base_theme = theme.base_theme();
284 cx.set_global(base_theme);
285 crate::text::install_text_view_defaults(&theme, cx);
286
287 if let Some(window) = window {
288 window.refresh();
289 }
290 }
291
292 fn base_theme(&self) -> gpui_base::Theme {
295 gpui_base::Theme {
296 appearance: if self.mode.is_dark() {
297 gpui_base::ThemeAppearance::Dark
298 } else {
299 gpui_base::ThemeAppearance::Light
300 },
301 tokens: self.semantic_tokens(),
302 scrollbar: gpui_base::ScrollbarTheme::new()
303 .with_mode(self.scrollbar_mode)
304 .with_motion(scrollbar_motion(self.scrollbar_mode))
305 .with_styles(
306 gpui_base::ScrollbarStyles::default()
307 .track(|style| style.bg(self.scrollbar))
308 .track_hover(|style| style.bg(self.scrollbar))
309 .track_active(|style| style.bg(self.scrollbar).border_color(self.border))
310 .thumb(|style| {
311 style
312 .bg(self.tokens.scrollbar_thumb)
313 .radius(self.radius)
314 .when(gpui_base::is_mobile(), |style| {
315 style
316 .width(MOBILE_SCROLLBAR_THUMB_WIDTH)
317 .inset(MOBILE_SCROLLBAR_THUMB_INSET)
318 .radius(RADIUS_FULL)
319 })
320 })
321 .thumb_hover(|style| {
322 style
323 .bg(self.tokens.scrollbar_thumb_hover)
324 .radius(self.radius)
325 .when(gpui_base::is_mobile(), |style| {
326 style
327 .width(SCROLLBAR_THUMB_HOVER_WIDTH)
328 .inset(SCROLLBAR_THUMB_INSET)
329 })
330 })
331 .thumb_active(|style| {
332 style
333 .bg(self.tokens.scrollbar_thumb_hover)
334 .radius(self.radius)
335 .when(gpui_base::is_mobile(), |style| {
336 style
337 .width(SCROLLBAR_THUMB_ACTIVE_WIDTH)
338 .inset(SCROLLBAR_THUMB_INSET)
339 })
340 }),
341 ),
342 resizable: gpui_base::ResizableTheme {
343 handle: Some(self.border),
344 active_handle: Some(self.drag_border),
345 },
346 }
347 }
348
349 pub fn sync_base(cx: &mut App) {
368 let theme = Theme::global(cx).clone();
369 let base_theme = theme.base_theme();
370 cx.set_global(base_theme);
371 crate::text::install_text_view_defaults(&theme, cx);
372 }
373
374 #[inline]
379 pub fn input_background(&self) -> Hsla {
380 if self.is_dark() {
381 self.input.mix_oklab(self.transparent, 0.3)
382 } else {
383 self.background
384 }
385 }
386
387 #[inline]
389 pub(crate) fn editor_background(&self) -> Hsla {
390 self.highlight_theme
391 .style
392 .editor_background
393 .unwrap_or_else(|| self.input_background())
394 }
395
396 pub fn semantic_tokens(&self) -> SemanticThemeTokens {
400 SemanticThemeTokens {
401 colors: self.color_tokens(),
402 radius: self.radius_tokens(),
403 spacing: self.spacing_tokens(),
404 typography: self.typography_tokens(),
405 shadow: self.shadow_tokens(),
406 }
407 }
408
409 pub fn motion_tokens(&self) -> &MotionTokens {
411 &self.motion
412 }
413
414 pub fn color_tokens(&self) -> ColorTokens {
415 ColorTokens {
416 background: self.background,
417 foreground: self.foreground,
418 surface: self.popover,
419 surface_foreground: self.popover_foreground,
420 primary: self.primary,
421 primary_foreground: self.primary_foreground,
422 secondary: self.secondary,
423 secondary_foreground: self.secondary_foreground,
424 muted: self.muted,
425 muted_foreground: self.muted_foreground,
426 accent: self.accent,
427 accent_foreground: self.accent_foreground,
428 destructive: self.danger,
429 destructive_foreground: self.danger_foreground,
430 border: self.border,
431 input: self.input,
432 ring: self.ring,
433 selection: self.selection,
434 }
435 }
436
437 pub fn radius_full(&self) -> Pixels {
446 if self.radius.is_zero() {
447 px(0.)
448 } else {
449 RADIUS_FULL
450 }
451 }
452
453 pub fn radius_2xl(&self) -> Pixels {
458 self.radius * 2.5
459 }
460
461 pub fn radius_3xl(&self) -> Pixels {
463 self.radius * 3.
464 }
465
466 pub fn radius_4xl(&self) -> Pixels {
468 self.radius * 3.5
469 }
470
471 pub fn radius_tokens(&self) -> RadiusTokens {
472 RadiusTokens {
473 none: px(0.),
474 sm: self.radius / 2.,
475 md: self.radius,
476 lg: self.radius_lg,
477 xl: self.radius * 2.,
478 full: self.radius_full(),
479 }
480 }
481
482 pub fn spacing_tokens(&self) -> SpacingTokens {
483 SpacingTokens::default()
484 }
485
486 pub fn typography_tokens(&self) -> TypographyTokens {
487 let mut tokens = TypographyTokens::default();
488 tokens.sans = self.font_family.clone();
489 tokens.mono = self.mono_font_family.clone();
490 tokens.md.size = self.font_size;
491 tokens.mono_md.size = self.mono_font_size;
492 tokens
493 }
494
495 pub fn shadow_tokens(&self) -> ShadowTokens {
496 if self.shadow {
497 ShadowTokens::elevations(self.transparent.alpha(0.18))
498 } else {
499 ShadowTokens::default()
500 }
501 }
502
503 pub fn apply_semantic_tokens(&mut self, tokens: &SemanticThemeTokens) {
507 let colors = tokens.colors;
508 self.background = colors.background;
509 self.foreground = colors.foreground;
510 self.popover = colors.surface;
511 self.popover_foreground = colors.surface_foreground;
512 self.primary = colors.primary;
513 self.primary_foreground = colors.primary_foreground;
514 self.secondary = colors.secondary;
515 self.secondary_foreground = colors.secondary_foreground;
516 self.muted = colors.muted;
517 self.muted_foreground = colors.muted_foreground;
518 self.accent = colors.accent;
519 self.accent_foreground = colors.accent_foreground;
520 self.danger = colors.destructive;
521 self.danger_foreground = colors.destructive_foreground;
522 self.border = colors.border;
523 self.input = colors.input;
524 self.ring = colors.ring;
525
526 self.tokens.background = colors.background.into();
527 self.tokens.popover = colors.surface.into();
528 self.tokens.primary = colors.primary.into();
529 self.tokens.secondary = colors.secondary.into();
530 self.tokens.muted = colors.muted.into();
531 self.tokens.accent = colors.accent.into();
532 self.tokens.danger = colors.destructive.into();
533
534 self.radius = tokens.radius.md;
535 self.radius_lg = tokens.radius.lg;
536 self.font_family = tokens.typography.sans.clone();
537 self.mono_font_family = tokens.typography.mono.clone();
538 self.font_size = tokens.typography.md.size;
539 self.mono_font_size = tokens.typography.mono_md.size;
540 self.shadow = !tokens.shadow.sm.is_empty()
541 || !tokens.shadow.md.is_empty()
542 || !tokens.shadow.lg.is_empty();
543 }
544
545 pub fn resolve_semantic_config(&self, config: &SemanticThemeConfig) -> SemanticThemeTokens {
548 let mut tokens = self.semantic_tokens();
549 config.apply_to(&mut tokens);
550 tokens
551 }
552
553 pub fn apply_semantic_config(&mut self, config: &SemanticThemeConfig) -> SemanticThemeTokens {
556 let tokens = self.resolve_semantic_config(config);
557 self.apply_semantic_tokens(&tokens);
558 tokens
559 }
560
561 pub fn apply_semantic_config_str(
563 &mut self,
564 content: &str,
565 ) -> anyhow::Result<SemanticThemeTokens> {
566 let config = serde_json::from_str::<SemanticThemeConfigFile>(content)?;
567 Ok(self.apply_semantic_config(&config.tokens))
568 }
569}
570
571#[cfg(test)]
572mod semantic_token_tests {
573 use gpui::{Hsla, IsZero as _, px};
574
575 use super::{RADIUS_FULL, Theme};
576
577 #[test]
578 fn semantic_colors_are_a_live_projection_of_legacy_fields() {
579 let mut theme = Theme::default();
580 let primary = Hsla::default().alpha(0.42);
581 theme.primary = primary;
582
583 assert_eq!(theme.color_tokens().primary, primary);
584 assert_eq!(theme.semantic_tokens().colors.primary, primary);
585 }
586
587 #[test]
588 fn applying_semantic_tokens_only_updates_generic_legacy_colors() {
589 let mut theme = Theme::default();
590 let component_color = theme.button_primary;
591 let mut tokens = theme.semantic_tokens();
592 tokens.colors.primary = Hsla::default().alpha(0.25);
593 tokens.colors.destructive = Hsla::default().alpha(0.75);
594 tokens.radius.md = px(10.);
595
596 theme.apply_semantic_tokens(&tokens);
597
598 assert_eq!(theme.primary, tokens.colors.primary);
599 assert_eq!(theme.tokens.primary.color, tokens.colors.primary);
600 assert_eq!(theme.danger, tokens.colors.destructive);
601 assert_eq!(theme.radius, px(10.));
602 assert_eq!(theme.button_primary, component_color);
603 }
604
605 #[test]
606 fn square_themes_square_off_pills_and_circles() {
607 let mut theme = Theme::default();
608 assert_eq!(theme.radius_full(), RADIUS_FULL);
609 assert_eq!(theme.radius_tokens().full, RADIUS_FULL);
610
611 theme.radius = px(0.);
614 assert_eq!(theme.radius_full(), px(0.));
615 assert_eq!(theme.radius_tokens().full, px(0.));
616 }
617
618 #[test]
619 fn larger_surface_radii_follow_the_theme_radius() {
620 let mut theme = Theme::default();
621 assert!(theme.radius_tokens().xl < theme.radius_2xl());
622 assert!(theme.radius_2xl() < theme.radius_3xl());
623 assert!(theme.radius_3xl() < theme.radius_4xl());
624
625 theme.radius = px(10.);
626 assert_eq!(theme.radius_2xl(), px(25.));
627 assert_eq!(theme.radius_3xl(), px(30.));
628 assert_eq!(theme.radius_4xl(), px(35.));
629
630 theme.radius = px(0.);
631 assert_eq!(theme.radius_2xl(), px(0.));
632 assert_eq!(theme.radius_3xl(), px(0.));
633 assert_eq!(theme.radius_4xl(), px(0.));
634 }
635
636 #[test]
637 fn base_projection_carries_a_square_radius_to_the_scrollbar() {
638 let mut theme = Theme::default();
639 assert!(!theme.base_theme().tokens.radius.md.is_zero());
640
641 theme.radius = px(0.);
644 assert!(theme.base_theme().tokens.radius.md.is_zero());
645 }
646
647 #[test]
648 fn disabled_legacy_shadows_project_to_empty_elevations() {
649 let mut theme = Theme::default();
650 theme.shadow = false;
651
652 let shadows = theme.shadow_tokens();
653 assert!(shadows.sm.is_empty());
654 assert!(shadows.md.is_empty());
655 assert!(shadows.lg.is_empty());
656 }
657}
658
659impl From<&ThemeColor> for Theme {
660 fn from(colors: &ThemeColor) -> Self {
661 Theme {
662 mode: ThemeMode::default(),
663 transparent: Hsla::transparent_black(),
664 font_family: ".SystemUIFont".into(),
665 font_size: px(16.),
666 mono_font_family: mono_font::default_mono_font_family(),
667 mono_font_size: px(13.),
668 radius: px(6.),
669 radius_lg: px(8.),
670 shadow: true,
671 focus_ring: true,
672 scrollbar_mode: ScrollbarMode::default(),
673 notification: NotificationSettings::default(),
674 list: ListSettings::default(),
675 colors: *colors,
676 tokens: ThemeTokens::from(colors),
677 light_theme: Rc::new(ThemeConfig::default()),
678 dark_theme: Rc::new(ThemeConfig::default()),
679 highlight_theme: HighlightTheme::default_light(),
680 sheet: SheetSettings::default(),
681 motion: MotionTokens::default(),
682 }
683 }
684}
685
686#[derive(
687 Debug,
688 Clone,
689 Copy,
690 Default,
691 PartialEq,
692 PartialOrd,
693 Eq,
694 Ord,
695 Hash,
696 Serialize,
697 Deserialize,
698 JsonSchema,
699)]
700#[serde(rename_all = "snake_case")]
701pub enum ThemeMode {
702 #[default]
703 Light,
704 Dark,
705}
706
707impl ThemeMode {
708 #[inline(always)]
709 pub fn is_dark(&self) -> bool {
710 matches!(self, Self::Dark)
711 }
712
713 pub fn name(&self) -> &'static str {
715 match self {
716 ThemeMode::Light => "light",
717 ThemeMode::Dark => "dark",
718 }
719 }
720}
721
722impl From<WindowAppearance> for ThemeMode {
723 fn from(appearance: WindowAppearance) -> Self {
724 match appearance {
725 WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
726 WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
727 }
728 }
729}
730
731#[cfg(test)]
732mod base_theme_projection_tests {
733 use super::*;
734 use gpui::TestAppContext;
735
736 #[gpui::test]
737 fn base_theme_tracks_initialization_and_mode_changes(cx: &mut TestAppContext) {
738 cx.update(|cx| {
739 init(cx);
740 assert_styled_projection(cx);
741
742 Theme::change(ThemeMode::Dark, None, cx);
743 assert_styled_projection(cx);
744
745 Theme::set_scrollbar_mode(ScrollbarMode::Always, cx);
746 assert_eq!(Theme::global(cx).scrollbar_mode, ScrollbarMode::Always);
747 assert_eq!(
748 gpui_base::Theme::global(cx).scrollbar.mode(),
749 gpui_base::ScrollbarMode::Always
750 );
751 assert_styled_projection(cx);
752 });
753 }
754
755 #[gpui::test]
756 fn scrollbar_motion_is_owned_here_and_projected_onto_base(cx: &mut TestAppContext) {
757 cx.update(|cx| {
758 init(cx);
759
760 let bare = gpui_base::ScrollbarMotion::default();
762 assert_eq!(bare.enter(), Duration::ZERO);
763 assert_eq!(bare.exit(), Duration::ZERO);
764 assert_eq!(bare.expand(), Duration::ZERO);
765
766 Theme::set_scrollbar_mode(ScrollbarMode::Scrolling, cx);
767 let motion = gpui_base::Theme::global(cx).scrollbar.motion();
768 assert_eq!(motion.idle(), SCROLLBAR_IDLE);
769 assert_eq!(motion.enter(), SCROLLBAR_ENTER);
770 assert_eq!(motion.exit(), SCROLLBAR_EXIT);
771 assert_eq!(motion.expand(), SCROLLBAR_EXPAND);
772 assert_eq!(
773 motion.entrance(),
774 gpui_base::ScrollbarEntrance::Fade,
775 "scroll-revealed scrollbars fade in without sliding"
776 );
777
778 Theme::set_scrollbar_mode(ScrollbarMode::Hover, cx);
779 let motion = gpui_base::Theme::global(cx).scrollbar.motion();
780 assert_eq!(motion.entrance(), gpui_base::ScrollbarEntrance::Fade);
781 assert_eq!(
782 motion.thumb_hover_entrance(),
783 gpui_base::ScrollbarEntrance::SlideAndFade
784 );
785 });
786 }
787
788 #[test]
789 fn default_motion_tokens_form_a_coherent_semantic_scale() {
790 let theme = Theme::default();
791 let motion = theme.motion_tokens();
792
793 assert_eq!(motion.duration_instant, Duration::ZERO);
794 assert!(motion.duration_fast < motion.duration_normal);
795 assert!(motion.duration_normal < motion.duration_slow);
796 assert!(motion.distance_short.0 < motion.distance_medium.0);
797 assert_eq!(motion.easing_enter.sample(0.0), 0.0);
798 assert_eq!(motion.easing_enter.sample(1.0), 1.0);
799 }
800
801 fn assert_styled_projection(cx: &App) {
802 let theme = Theme::global(cx);
803 let base = gpui_base::Theme::global(cx);
804
805 assert_eq!(base.tokens, theme.semantic_tokens());
806 assert_eq!(base.scrollbar.mode(), theme.scrollbar_mode);
807 assert_eq!(
808 base.scrollbar.motion(),
809 scrollbar_motion(theme.scrollbar_mode)
810 );
811 assert_eq!(base.resizable.handle, Some(theme.border));
812 assert_eq!(base.resizable.active_handle, Some(theme.drag_border));
813 }
814
815 #[gpui::test]
816 fn default_component_palettes_match_base_light_and_dark_tokens(cx: &mut gpui::TestAppContext) {
817 fn assert_close(left: ColorTokens, right: ColorTokens) {
818 macro_rules! color {
819 ($field:ident) => {
820 assert!(
821 (left.$field.h - right.$field.h).abs() < 1e-6
822 && (left.$field.s - right.$field.s).abs() < 1e-6
823 && (left.$field.l - right.$field.l).abs() < 1e-6
824 && (left.$field.a - right.$field.a).abs() < 1e-6,
825 "{} differs: {:?} != {:?}",
826 stringify!($field),
827 left.$field,
828 right.$field
829 );
830 };
831 }
832 color!(background);
833 color!(foreground);
834 color!(surface);
835 color!(surface_foreground);
836 color!(primary);
837 color!(primary_foreground);
838 color!(secondary);
839 color!(secondary_foreground);
840 color!(muted);
841 color!(muted_foreground);
842 color!(accent);
843 color!(accent_foreground);
844 color!(destructive);
845 color!(destructive_foreground);
846 color!(border);
847 color!(input);
848 color!(ring);
849 color!(selection);
850 }
851
852 cx.update(crate::init);
853 cx.update(|cx| {
854 assert_close(Theme::global(cx).color_tokens(), ColorTokens::light());
855 });
856
857 cx.update(|cx| Theme::change(ThemeMode::Dark, None, cx));
858 cx.update(|cx| {
859 assert_close(Theme::global(cx).color_tokens(), ColorTokens::dark());
860 });
861 }
862}