1use iced_core::{
6 Color,
7 theme::palette::Extended,
8 theme::{Custom, Palette},
9};
10use palette::{
11 Clamp, FromColor, Hsva, IsWithinBounds, OklabHue, Oklch, Srgb,
12 convert::FromColorUnclamped,
13 rgb::{Rgb, Rgba},
14};
15use serde::{Deserialize, Serialize};
16use std::cmp::Ordering;
17
18pub const ACCENT_COUNT: usize = ThemeAccent::ALL.len();
20const CHROMATIC_ACCENT_COUNT: usize = ACCENT_COUNT - 1;
23const MIN_ACCENT_HUE_SEPARATION: f32 = 42.0;
25const MIN_SIGNAL_HUE_SEPARATION: f32 = 12.0;
27const MIN_HUE_REFERENCE_CHROMA: f32 = 0.04;
29const MAX_HUE_DISTANCE: f32 = 180.0;
31const HUE_CANDIDATE_STEP: f32 = 0.5;
33const HUE_CANDIDATE_COUNT: usize = (360.0 / HUE_CANDIDATE_STEP) as usize;
34const TARGET_ACCENT_CHROMA: f32 = 0.14;
35const ENDPOINT_ACCENT_CHROMA: f32 = 0.02;
36
37#[derive(Debug, Clone)]
38pub struct Theme(pub iced_core::Theme);
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum ThemeAccent {
42 One,
43 Two,
44 Three,
45 Four,
46 Five,
47 Six,
48 Seven,
49 Eight,
50}
51
52impl ThemeAccent {
53 pub const ALL: [Self; 8] = [
54 Self::One,
55 Self::Two,
56 Self::Three,
57 Self::Four,
58 Self::Five,
59 Self::Six,
60 Self::Seven,
61 Self::Eight,
62 ];
63
64 pub const fn index(self) -> usize {
65 match self {
66 Self::One => 0,
67 Self::Two => 1,
68 Self::Three => 2,
69 Self::Four => 3,
70 Self::Five => 4,
71 Self::Six => 5,
72 Self::Seven => 6,
73 Self::Eight => 7,
74 }
75 }
76}
77
78impl std::fmt::Display for ThemeAccent {
79 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(formatter, "Accent {}", self.index() + 1)
81 }
82}
83
84pub fn generated_theme_accents(palette: &Extended) -> [Color; ACCENT_COUNT] {
90 let lightness = if palette.is_dark { 0.7 } else { 0.5 };
91 let hues = AccentHues::new(palette).place_all();
92
93 std::array::from_fn(|index| {
94 hues.get(index).map_or_else(
95 || endpoint_accent(palette),
96 |hue| accent_color(*hue, lightness, TARGET_ACCENT_CHROMA),
97 )
98 })
99}
100
101struct AccentHues {
107 signal_hues: Vec<f32>,
108 identity_hue: Option<f32>,
109 placed: Vec<f32>,
110}
111
112impl AccentHues {
113 fn new(palette: &Extended) -> Self {
114 Self {
115 signal_hues: [
116 palette.primary.base.color,
117 palette.success.base.color,
118 palette.danger.base.color,
119 palette.warning.base.color,
120 ]
121 .into_iter()
122 .filter_map(chromatic_hue)
123 .collect(),
124 identity_hue: chromatic_hue(palette.primary.base.color),
125 placed: Vec::with_capacity(CHROMATIC_ACCENT_COUNT),
126 }
127 }
128
129 fn place_all(mut self) -> Vec<f32> {
130 self.placed.push(self.initial_hue());
131
132 while self.placed.len() < CHROMATIC_ACCENT_COUNT {
133 self.placed.push(self.next_hue());
134 }
135
136 self.placed
137 }
138
139 fn initial_hue(&self) -> f32 {
142 match self.identity_hue {
143 Some(identity_hue) => self.best_hue(|hue| self.identity_rank(hue, identity_hue)),
144 None => self.best_hue(|hue| self.accent_rank(hue)),
145 }
146 }
147
148 fn next_hue(&self) -> f32 {
150 self.best_hue(|hue| self.accent_rank(hue))
151 }
152
153 fn accent_rank(&self, hue: f32) -> (bool, f32, f32) {
158 let accent_clearance = self.accent_clearance(hue);
159 let signal_clearance = self.signal_clearance(hue);
160
161 (
162 accent_clearance >= MIN_ACCENT_HUE_SEPARATION
163 && signal_clearance >= MIN_SIGNAL_HUE_SEPARATION,
164 accent_clearance,
165 signal_clearance,
166 )
167 }
168
169 fn identity_rank(&self, hue: f32, identity_hue: f32) -> (bool, f32, f32) {
172 let (within_budget, _, signal_clearance) = self.accent_rank(hue);
173
174 (
175 within_budget,
176 -hue_distance(hue, identity_hue),
177 signal_clearance,
178 )
179 }
180
181 fn best_hue(&self, rank: impl Fn(f32) -> (bool, f32, f32)) -> f32 {
183 let mut best_hue = 0.0;
184 let mut best_rank = (false, f32::NEG_INFINITY, f32::NEG_INFINITY);
185
186 for hue in candidate_hues() {
187 let candidate = rank(hue);
188 if candidate.partial_cmp(&best_rank) == Some(Ordering::Greater) {
189 best_rank = candidate;
190 best_hue = hue;
191 }
192 }
193
194 best_hue
195 }
196
197 fn accent_clearance(&self, hue: f32) -> f32 {
199 self.placed
200 .iter()
201 .map(|placed| hue_distance(hue, *placed))
202 .fold(MAX_HUE_DISTANCE, f32::min)
203 }
204
205 fn signal_clearance(&self, hue: f32) -> f32 {
207 self.signal_hues
208 .iter()
209 .map(|signal_hue| hue_distance(hue, *signal_hue))
210 .fold(MAX_HUE_DISTANCE, f32::min)
211 }
212}
213
214fn chromatic_hue(color: Color) -> Option<f32> {
216 let oklch = to_oklch(color);
217
218 (oklch.chroma >= MIN_HUE_REFERENCE_CHROMA).then(|| oklch.hue.into_positive_degrees())
219}
220
221fn endpoint_accent(palette: &Extended) -> Color {
225 let lightness = if palette.is_dark { 0.96 } else { 0.04 };
226
227 match chromatic_hue(palette.primary.base.color) {
228 Some(hue) => accent_color(hue, lightness, ENDPOINT_ACCENT_CHROMA),
230 None => accent_color(0.0, lightness, 0.0),
231 }
232}
233
234fn accent_color(hue: f32, lightness: f32, chroma: f32) -> Color {
235 let mut oklch = Oklch::from_color(Srgb::new(0.0, 0.0, 0.0));
236 oklch.l = lightness;
237 oklch.chroma = chroma;
238 oklch.hue = OklabHue::from_degrees(hue);
239
240 let rgb = gamut_mapped_srgb(oklch);
241 Color {
242 r: rgb.red,
243 g: rgb.green,
244 b: rgb.blue,
245 a: 1.0,
246 }
247}
248
249fn candidate_hues() -> impl Iterator<Item = f32> {
250 (0..HUE_CANDIDATE_COUNT).map(|step| step as f32 * HUE_CANDIDATE_STEP)
251}
252
253fn hue_distance(first: f32, second: f32) -> f32 {
254 let distance = (first - second).abs().rem_euclid(360.0);
255 distance.min(360.0 - distance)
256}
257
258fn gamut_mapped_srgb(mut oklch: Oklch) -> Srgb {
259 let mut rgb = Srgb::from_color_unclamped(oklch);
260
261 if rgb.is_within_bounds() {
262 return rgb;
263 }
264
265 let mut lower_chroma = 0.0;
266 let mut upper_chroma = oklch.chroma;
267
268 for _ in 0..12 {
269 let chroma = (lower_chroma + upper_chroma) / 2.0;
270 oklch.chroma = chroma;
271 rgb = Srgb::from_color_unclamped(oklch);
272
273 if rgb.is_within_bounds() {
274 lower_chroma = chroma;
275 } else {
276 upper_chroma = chroma;
277 }
278 }
279
280 oklch.chroma = lower_chroma;
281 Srgb::from_color_unclamped(oklch).clamp()
282}
283
284fn to_oklch(color: Color) -> Oklch {
285 Oklch::from_color(Srgb::new(color.r, color.g, color.b))
286}
287
288#[derive(Serialize, Deserialize)]
289struct SerTheme {
290 name: String,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 palette: Option<Palette>,
293}
294
295impl Default for Theme {
296 fn default() -> Self {
297 Self(iced_core::Theme::Custom(default_theme().into()))
298 }
299}
300
301impl From<Theme> for iced_core::Theme {
302 fn from(val: Theme) -> Self {
303 val.0
304 }
305}
306
307pub fn default_theme() -> Custom {
308 Custom::new(
309 "Flowsurface".to_string(),
310 Palette {
311 background: Color::from_rgb8(24, 22, 22),
312 text: Color::from_rgb8(197, 201, 197),
313 primary: Color::from_rgb8(169, 174, 191),
314 success: Color::from_rgb8(81, 205, 160),
315 danger: Color::from_rgb8(192, 80, 77),
316 warning: Color::from_rgb8(238, 216, 139),
317 },
318 )
319}
320
321impl Serialize for Theme {
322 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
323 where
324 S: serde::Serializer,
325 {
326 if let iced_core::Theme::Custom(custom) = &self.0 {
327 let is_default_theme = custom.to_string() == "Flowsurface";
328 let ser_theme = SerTheme {
329 name: if is_default_theme {
330 "flowsurface"
331 } else {
332 "custom"
333 }
334 .to_string(),
335 palette: if is_default_theme {
336 None
337 } else {
338 Some(self.0.palette())
339 },
340 };
341 ser_theme.serialize(serializer)
342 } else {
343 let theme_str = match self.0 {
344 iced_core::Theme::Ferra => "ferra",
345 iced_core::Theme::Dark => "dark",
346 iced_core::Theme::Light => "light",
347 iced_core::Theme::Dracula => "dracula",
348 iced_core::Theme::Nord => "nord",
349 iced_core::Theme::SolarizedLight => "solarized_light",
350 iced_core::Theme::SolarizedDark => "solarized_dark",
351 iced_core::Theme::GruvboxLight => "gruvbox_light",
352 iced_core::Theme::GruvboxDark => "gruvbox_dark",
353 iced_core::Theme::CatppuccinLatte => "catppuccino_latte",
354 iced_core::Theme::CatppuccinFrappe => "catppuccino_frappe",
355 iced_core::Theme::CatppuccinMacchiato => "catppuccino_macchiato",
356 iced_core::Theme::CatppuccinMocha => "catppuccino_mocha",
357 iced_core::Theme::TokyoNight => "tokyo_night",
358 iced_core::Theme::TokyoNightStorm => "tokyo_night_storm",
359 iced_core::Theme::TokyoNightLight => "tokyo_night_light",
360 iced_core::Theme::KanagawaWave => "kanagawa_wave",
361 iced_core::Theme::KanagawaDragon => "kanagawa_dragon",
362 iced_core::Theme::KanagawaLotus => "kanagawa_lotus",
363 iced_core::Theme::Moonfly => "moonfly",
364 iced_core::Theme::Nightfly => "nightfly",
365 iced_core::Theme::Oxocarbon => "oxocarbon",
366 _ => unreachable!(),
367 };
368 theme_str.serialize(serializer)
369 }
370 }
371}
372
373impl<'de> Deserialize<'de> for Theme {
374 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
375 where
376 D: serde::Deserializer<'de>,
377 {
378 let value =
379 serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?;
380
381 if let Some(s) = value.as_str() {
382 let theme = match s {
383 "ferra" => iced_core::Theme::Ferra,
384 "dark" => iced_core::Theme::Dark,
385 "light" => iced_core::Theme::Light,
386 "dracula" => iced_core::Theme::Dracula,
387 "nord" => iced_core::Theme::Nord,
388 "solarized_light" => iced_core::Theme::SolarizedLight,
389 "solarized_dark" => iced_core::Theme::SolarizedDark,
390 "gruvbox_light" => iced_core::Theme::GruvboxLight,
391 "gruvbox_dark" => iced_core::Theme::GruvboxDark,
392 "catppuccino_latte" => iced_core::Theme::CatppuccinLatte,
393 "catppuccino_frappe" => iced_core::Theme::CatppuccinFrappe,
394 "catppuccino_macchiato" => iced_core::Theme::CatppuccinMacchiato,
395 "catppuccino_mocha" => iced_core::Theme::CatppuccinMocha,
396 "tokyo_night" => iced_core::Theme::TokyoNight,
397 "tokyo_night_storm" => iced_core::Theme::TokyoNightStorm,
398 "tokyo_night_light" => iced_core::Theme::TokyoNightLight,
399 "kanagawa_wave" => iced_core::Theme::KanagawaWave,
400 "kanagawa_dragon" => iced_core::Theme::KanagawaDragon,
401 "kanagawa_lotus" => iced_core::Theme::KanagawaLotus,
402 "moonfly" => iced_core::Theme::Moonfly,
403 "nightfly" => iced_core::Theme::Nightfly,
404 "oxocarbon" => iced_core::Theme::Oxocarbon,
405 "flowsurface" => Theme::default().0,
406 _ => {
407 return Err(serde::de::Error::custom(format!("Invalid theme: {}", s)));
408 }
409 };
410 return Ok(Theme(theme));
411 }
412
413 let serialized = SerTheme::deserialize(value).map_err(serde::de::Error::custom)?;
414
415 let theme = match serialized.name.as_str() {
416 "flowsurface" => Theme::default().0,
417 "custom" => {
418 if let Some(palette) = serialized.palette {
419 iced_core::Theme::Custom(Custom::new("Custom".to_string(), palette).into())
420 } else {
421 return Err(serde::de::Error::custom(
422 "Custom theme missing palette data",
423 ));
424 }
425 }
426 _ => return Err(serde::de::Error::custom("Invalid theme")),
427 };
428
429 Ok(Theme(theme))
430 }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434pub enum ThemeColorRole {
435 Neutral,
436 Secondary,
437 Primary,
438 Success,
439 Danger,
440 Warning,
441}
442
443impl ThemeColorRole {
444 pub const ALL: &'static [Self] = &[
445 Self::Neutral,
446 Self::Secondary,
447 Self::Primary,
448 Self::Success,
449 Self::Danger,
450 Self::Warning,
451 ];
452}
453
454impl std::str::FromStr for ThemeColorRole {
455 type Err = UnknownThemeColor;
456
457 fn from_str(value: &str) -> Result<Self, Self::Err> {
458 Self::ALL
459 .iter()
460 .copied()
461 .find(|role| role.to_string().eq_ignore_ascii_case(value))
462 .ok_or_else(|| UnknownThemeColor::Role(value.to_owned()))
463 }
464}
465
466impl std::fmt::Display for ThemeColorRole {
467 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 let label = match self {
469 Self::Neutral => "Neutral",
470 Self::Secondary => "Secondary",
471 Self::Primary => "Primary",
472 Self::Success => "Success",
473 Self::Danger => "Danger",
474 Self::Warning => "Warning",
475 };
476 formatter.write_str(label)
477 }
478}
479
480#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
481pub enum ThemeColorShade {
482 Faint,
483 Soft,
484 #[default]
485 Base,
486 Strong,
487 Vivid,
488}
489
490impl ThemeColorShade {
491 pub const ALL: &'static [Self] = &[
492 Self::Faint,
493 Self::Soft,
494 Self::Base,
495 Self::Strong,
496 Self::Vivid,
497 ];
498}
499
500impl std::fmt::Display for ThemeColorShade {
501 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 let label = match self {
503 Self::Faint => "Faint",
504 Self::Soft => "Soft",
505 Self::Base => "Base",
506 Self::Strong => "Strong",
507 Self::Vivid => "Vivid",
508 };
509 formatter.write_str(label)
510 }
511}
512
513impl std::str::FromStr for ThemeColorShade {
514 type Err = UnknownThemeColor;
515
516 fn from_str(value: &str) -> Result<Self, Self::Err> {
517 Self::ALL
518 .iter()
519 .copied()
520 .find(|shade| shade.to_string().eq_ignore_ascii_case(value))
521 .ok_or_else(|| UnknownThemeColor::Shade(value.to_owned()))
522 }
523}
524
525#[derive(Debug, Clone, PartialEq, Eq)]
527pub enum UnknownThemeColor {
528 Role(String),
529 Shade(String),
530}
531
532impl std::fmt::Display for UnknownThemeColor {
533 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534 match self {
535 Self::Role(value) => write!(formatter, "unknown theme role '{value}'"),
536 Self::Shade(value) => write!(formatter, "unknown theme shade '{value}'"),
537 }
538 }
539}
540
541impl std::error::Error for UnknownThemeColor {}
542
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
544pub struct ThemeColor {
545 pub role: ThemeColorRole,
546 pub shade: ThemeColorShade,
547 #[serde(default)]
548 pub accent: Option<ThemeAccent>,
549}
550
551impl Default for ThemeColor {
552 fn default() -> Self {
553 Self::new(ThemeColorRole::Neutral, ThemeColorShade::Base)
554 }
555}
556
557impl ThemeColor {
558 pub const fn new(role: ThemeColorRole, shade: ThemeColorShade) -> Self {
559 Self {
560 role,
561 shade,
562 accent: None,
563 }
564 }
565
566 pub const fn with_accent(
567 role: ThemeColorRole,
568 accent: Option<ThemeAccent>,
569 shade: ThemeColorShade,
570 ) -> Self {
571 Self {
572 role,
573 shade,
574 accent,
575 }
576 }
577}
578
579pub fn hex_to_color(hex: &str) -> Option<Color> {
580 if hex.len() == 7 || hex.len() == 9 {
581 let hash = &hex[0..1];
582 let r = u8::from_str_radix(&hex[1..3], 16);
583 let g = u8::from_str_radix(&hex[3..5], 16);
584 let b = u8::from_str_radix(&hex[5..7], 16);
585 let a = (hex.len() == 9)
586 .then(|| u8::from_str_radix(&hex[7..9], 16).ok())
587 .flatten();
588
589 return match (hash, r, g, b, a) {
590 ("#", Ok(r), Ok(g), Ok(b), None) => Some(Color {
591 r: f32::from(r) / 255.0,
592 g: f32::from(g) / 255.0,
593 b: f32::from(b) / 255.0,
594 a: 1.0,
595 }),
596 ("#", Ok(r), Ok(g), Ok(b), Some(a)) => Some(Color {
597 r: f32::from(r) / 255.0,
598 g: f32::from(g) / 255.0,
599 b: f32::from(b) / 255.0,
600 a: f32::from(a) / 255.0,
601 }),
602 _ => None,
603 };
604 }
605
606 None
607}
608
609pub fn color_to_hex(color: Color) -> String {
610 use std::fmt::Write;
611
612 let mut hex = String::with_capacity(9);
613
614 let [r, g, b, a] = color.into_rgba8();
615
616 let _ = write!(&mut hex, "#");
617 let _ = write!(&mut hex, "{r:02X}");
618 let _ = write!(&mut hex, "{g:02X}");
619 let _ = write!(&mut hex, "{b:02X}");
620
621 if a < u8::MAX {
622 let _ = write!(&mut hex, "{a:02X}");
623 }
624
625 hex
626}
627
628pub fn from_hsva(color: Hsva) -> Color {
629 to_color(palette::Srgba::from_color(color))
630}
631
632fn to_color(rgba: Rgba) -> Color {
633 Color {
634 r: rgba.color.red,
635 g: rgba.color.green,
636 b: rgba.color.blue,
637 a: rgba.alpha,
638 }
639}
640
641pub fn to_hsva(color: Color) -> Hsva {
642 Hsva::from_color(to_rgba(color))
643}
644
645fn to_rgb(color: Color) -> Rgb {
646 Rgb {
647 red: color.r,
648 green: color.g,
649 blue: color.b,
650 ..Rgb::default()
651 }
652}
653
654fn to_rgba(color: Color) -> Rgba {
655 Rgba {
656 alpha: color.a,
657 color: to_rgb(color),
658 }
659}
660
661pub fn darken(color: Color, amount: f32) -> Color {
662 let mut hsl = to_hsl(color);
663
664 hsl.l = if hsl.l - amount < 0.0 {
665 0.0
666 } else {
667 hsl.l - amount
668 };
669
670 from_hsl(hsl)
671}
672
673pub fn lighten(color: Color, amount: f32) -> Color {
674 let mut hsl = to_hsl(color);
675
676 hsl.l = if hsl.l + amount > 1.0 {
677 1.0
678 } else {
679 hsl.l + amount
680 };
681
682 from_hsl(hsl)
683}
684
685fn to_hsl(color: Color) -> Hsl {
686 let x_max = color.r.max(color.g).max(color.b);
687 let x_min = color.r.min(color.g).min(color.b);
688 let c = x_max - x_min;
689 let l = x_max.midpoint(x_min);
690
691 let h = if c == 0.0 {
692 0.0
693 } else if x_max == color.r {
694 60.0 * ((color.g - color.b) / c).rem_euclid(6.0)
695 } else if x_max == color.g {
696 60.0 * (((color.b - color.r) / c) + 2.0)
697 } else {
698 60.0 * (((color.r - color.g) / c) + 4.0)
700 };
701
702 let s = if l == 0.0 || l == 1.0 {
703 0.0
704 } else {
705 (x_max - l) / l.min(1.0 - l)
706 };
707
708 Hsl {
709 h,
710 s,
711 l,
712 a: color.a,
713 }
714}
715
716pub fn is_dark(color: Color) -> bool {
717 let brightness = (color.r * 299.0 + color.g * 587.0 + color.b * 114.0) / 1000.0;
718 brightness < 0.5
719}
720
721struct Hsl {
722 h: f32,
723 s: f32,
724 l: f32,
725 a: f32,
726}
727
728fn from_hsl(hsl: Hsl) -> Color {
730 let c = (1.0 - (2.0 * hsl.l - 1.0).abs()) * hsl.s;
731 let h = hsl.h / 60.0;
732 let x = c * (1.0 - (h.rem_euclid(2.0) - 1.0).abs());
733
734 let (r1, g1, b1) = if h < 1.0 {
735 (c, x, 0.0)
736 } else if h < 2.0 {
737 (x, c, 0.0)
738 } else if h < 3.0 {
739 (0.0, c, x)
740 } else if h < 4.0 {
741 (0.0, x, c)
742 } else if h < 5.0 {
743 (x, 0.0, c)
744 } else {
745 (c, 0.0, x)
747 };
748
749 let m = hsl.l - (c / 2.0);
750
751 Color {
752 r: r1 + m,
753 g: g1 + m,
754 b: b1 + m,
755 a: hsl.a,
756 }
757}
758
759pub fn composite_color(foreground: Color, background: Color) -> Color {
761 let alpha = foreground.a.clamp(0.0, 1.0);
762 Color {
763 r: foreground.r.mul_add(alpha, background.r * (1.0 - alpha)),
764 g: foreground.g.mul_add(alpha, background.g * (1.0 - alpha)),
765 b: foreground.b.mul_add(alpha, background.b * (1.0 - alpha)),
766 a: 1.0,
767 }
768}
769
770pub fn contrast_ratio(a: Color, b: Color) -> f32 {
772 let l1 = relative_luminance(a);
773 let l2 = relative_luminance(b);
774 let lighter = l1.max(l2);
775 let darker = l1.min(l2);
776
777 (lighter + 0.05) / (darker + 0.05)
778}
779
780pub fn relative_luminance(color: Color) -> f32 {
782 let channel = |value: f32| {
783 if value <= 0.03928 {
784 value / 12.92
785 } else {
786 ((value + 0.055) / 1.055).powf(2.4)
787 }
788 };
789
790 (0.2126 * channel(color.r)) + (0.7152 * channel(color.g)) + (0.0722 * channel(color.b))
791}
792
793pub fn mix_color(foreground: Color, background: Color, foreground_weight: f32) -> Color {
795 let foreground_weight = foreground_weight.clamp(0.0, 1.0);
796 let background_weight = 1.0 - foreground_weight;
797
798 Color {
799 r: foreground
800 .r
801 .mul_add(foreground_weight, background.r * background_weight),
802 g: foreground
803 .g
804 .mul_add(foreground_weight, background.g * background_weight),
805 b: foreground
806 .b
807 .mul_add(foreground_weight, background.b * background_weight),
808 a: foreground
809 .a
810 .mul_add(foreground_weight, background.a * background_weight),
811 }
812}