Skip to main content

tui_lipan/style/
theme.rs

1use std::any::{Any, TypeId};
2use std::collections::HashMap;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::sync::Arc;
6
7use crate::app::ContrastPolicy;
8
9use super::{Color, HostTerminalColors, Paint};
10
11/// A relative transform applied to an already-resolved color.
12#[cfg_attr(
13    feature = "terminal-serde",
14    derive(serde::Serialize, serde::Deserialize)
15)]
16#[derive(Clone, Copy, Debug)]
17pub enum ColorTransform {
18    /// Dim toward black by an amount in `[0.0, 1.0]`.
19    Dim(f32),
20    /// Lighten toward white by an amount in `[0.0, 1.0]`.
21    Lighten(f32),
22    /// Raise a surface off its own background by an amount in `[0.0, 1.0]`.
23    ///
24    /// The relative form of [`Color::elevate`]: luminance-aware, so it lightens a
25    /// dark color and dims a light one, and hue- and chroma-preserving rather
26    /// than washing toward white or black. Reach for this over [`Self::Lighten`]
27    /// whenever the transform has to match an absolute `Color::elevate` step
28    /// elsewhere in the same UI - a hover lift over a row whose keyboard-cursor
29    /// counterpart is an elevated background, for instance.
30    Elevate(f32),
31    /// Blend toward the resolved background by alpha `(1.0 - opacity)`.
32    ///
33    /// `1.0` keeps the original color unchanged, while `0.0` fully washes it
34    /// into the current background. This is most useful for foreground colors
35    /// on both dark and light themes.
36    Opacity(f32),
37    /// Like [`Self::Opacity`], but blend toward a fixed `target` instead of the cell backdrop.
38    OpacityToward {
39        /// Same semantics as [`Self::Opacity`]: `1.0` is unchanged, `0.0` is fully `target`.
40        factor: f32,
41        /// Destination color when `factor` approaches `0.0`.
42        target: Color,
43    },
44    /// Blend toward `color` by `alpha` in `[0.0, 1.0]`.
45    Tint(Color, f32),
46}
47
48impl PartialEq for ColorTransform {
49    fn eq(&self, other: &Self) -> bool {
50        match (*self, *other) {
51            (Self::Dim(a), Self::Dim(b))
52            | (Self::Lighten(a), Self::Lighten(b))
53            | (Self::Elevate(a), Self::Elevate(b))
54            | (Self::Opacity(a), Self::Opacity(b)) => a.to_bits() == b.to_bits(),
55            (
56                Self::OpacityToward {
57                    factor: fa,
58                    target: ta,
59                },
60                Self::OpacityToward {
61                    factor: fb,
62                    target: tb,
63                },
64            ) => fa.to_bits() == fb.to_bits() && ta == tb,
65            (Self::Tint(color_a, alpha_a), Self::Tint(color_b, alpha_b)) => {
66                color_a == color_b && alpha_a.to_bits() == alpha_b.to_bits()
67            }
68            _ => false,
69        }
70    }
71}
72
73impl Eq for ColorTransform {}
74
75impl Hash for ColorTransform {
76    fn hash<H: Hasher>(&self, state: &mut H) {
77        match *self {
78            Self::Dim(amount) => {
79                0u8.hash(state);
80                amount.to_bits().hash(state);
81            }
82            Self::Lighten(amount) => {
83                1u8.hash(state);
84                amount.to_bits().hash(state);
85            }
86            Self::Opacity(amount) => {
87                2u8.hash(state);
88                amount.to_bits().hash(state);
89            }
90            Self::OpacityToward { factor, target } => {
91                4u8.hash(state);
92                factor.to_bits().hash(state);
93                target.hash(state);
94            }
95            Self::Tint(color, alpha) => {
96                3u8.hash(state);
97                color.hash(state);
98                alpha.to_bits().hash(state);
99            }
100            Self::Elevate(amount) => {
101                5u8.hash(state);
102                amount.to_bits().hash(state);
103            }
104        }
105    }
106}
107
108impl ColorTransform {
109    /// Apply this transform to `color`.
110    pub fn apply(self, color: Color) -> Color {
111        self.apply_with_backdrop(color, None)
112    }
113
114    /// Apply this transform to `color`, optionally using a resolved backdrop.
115    pub fn apply_with_backdrop(self, color: Color, backdrop: Option<Color>) -> Color {
116        if matches!(color, Color::Transparent | Color::Backdrop) {
117            return color;
118        }
119        match self {
120            Self::Dim(amount) => color.dim_by(amount),
121            Self::Lighten(amount) => color.lighten_by(amount),
122            Self::Elevate(amount) => color.elevate_by(amount),
123            Self::Opacity(opacity) => backdrop.map_or(color, |bg| {
124                color.blend_toward(bg, (1.0 - opacity).clamp(0.0, 1.0))
125            }),
126            Self::OpacityToward { factor, target } => {
127                color.blend_toward(target, (1.0 - factor).clamp(0.0, 1.0))
128            }
129            Self::Tint(target, alpha) => color.blend_toward(target, alpha),
130        }
131    }
132
133    /// Apply this transform to `paint`.
134    ///
135    /// Pigment transforms preserve the paint alpha; [`Self::Opacity`] composes
136    /// with the existing alpha by multiplying it by the opacity factor.
137    pub fn apply_paint(self, paint: Paint) -> Paint {
138        self.apply_paint_with_backdrop(paint, None)
139    }
140
141    /// Apply this transform to `paint`, optionally using a resolved backdrop paint.
142    pub fn apply_paint_with_backdrop(self, paint: Paint, backdrop: Option<Paint>) -> Paint {
143        if matches!(paint, Paint::Solid(Color::Transparent | Color::Backdrop)) {
144            return paint;
145        }
146        if let Self::Opacity(opacity) = self {
147            let alpha = (paint.alpha_u8() as f32 * opacity.clamp(0.0, 1.0))
148                .round()
149                .clamp(0.0, 255.0) as u8;
150            return Paint::from_color_alpha_u8(paint.color(), alpha);
151        }
152        let backdrop = backdrop.map(Paint::color);
153        match paint {
154            Paint::Solid(color) => Paint::Solid(self.apply_with_backdrop(color, backdrop)),
155            Paint::Alpha { color, alpha } => {
156                Paint::from_color_alpha_u8(self.apply_with_backdrop(color, backdrop), alpha)
157            }
158            // Transform the colour the slot currently holds; a slot is always opaque.
159            Paint::Animated { .. } => {
160                Paint::Solid(self.apply_with_backdrop(paint.resolved().color(), backdrop))
161            }
162        }
163    }
164
165    pub(crate) fn needs_backdrop(self) -> bool {
166        matches!(self, Self::Opacity(_))
167    }
168
169    fn normalized(self) -> Self {
170        match self {
171            Self::Dim(amount) => Self::Dim(amount.clamp(0.0, 1.0)),
172            Self::Lighten(amount) => Self::Lighten(amount.clamp(0.0, 1.0)),
173            Self::Elevate(amount) => Self::Elevate(amount.clamp(0.0, 1.0)),
174            Self::Opacity(opacity) => Self::Opacity(opacity.clamp(0.0, 1.0)),
175            Self::OpacityToward { factor, target } => Self::OpacityToward {
176                factor: factor.clamp(0.0, 1.0),
177                target,
178            },
179            Self::Tint(color, alpha) => Self::Tint(color, alpha.clamp(0.0, 1.0)),
180        }
181    }
182}
183
184/// Marker trait for typed app-specific theme data stored inside [`Theme`].
185///
186/// Use this when your app needs semantic theme tokens that are not part of the
187/// core framework palettes, while still keeping those tokens inside the active
188/// framework theme rather than a parallel global palette.
189pub trait ThemeExtension: Clone + fmt::Debug + PartialEq + 'static {}
190
191impl<T> ThemeExtension for T where T: Clone + fmt::Debug + PartialEq + 'static {}
192
193trait ThemeExtensionValue: Any {
194    fn as_any(&self) -> &dyn Any;
195    fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool;
196}
197
198impl<T> ThemeExtensionValue for T
199where
200    T: ThemeExtension,
201{
202    fn as_any(&self) -> &dyn Any {
203        self
204    }
205
206    fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool {
207        other.as_any().downcast_ref::<T>() == Some(self)
208    }
209}
210
211#[derive(Clone, Default)]
212#[doc(hidden)]
213pub struct ThemeExtensions(HashMap<TypeId, Arc<dyn ThemeExtensionValue>>);
214
215impl ThemeExtensions {
216    fn insert<T>(&mut self, extension: T)
217    where
218        T: ThemeExtension,
219    {
220        self.0.insert(TypeId::of::<T>(), Arc::new(extension));
221    }
222
223    fn get<T>(&self) -> Option<&T>
224    where
225        T: ThemeExtension,
226    {
227        self.0
228            .get(&TypeId::of::<T>())
229            .and_then(|value| value.as_any().downcast_ref::<T>())
230    }
231
232    fn remove<T>(&mut self)
233    where
234        T: ThemeExtension,
235    {
236        self.0.remove(&TypeId::of::<T>());
237    }
238}
239
240impl PartialEq for ThemeExtensions {
241    fn eq(&self, other: &Self) -> bool {
242        self.0.len() == other.0.len()
243            && self.0.iter().all(|(type_id, value)| {
244                other
245                    .0
246                    .get(type_id)
247                    .is_some_and(|other_value| value.eq_value(other_value.as_ref()))
248            })
249    }
250}
251
252impl Eq for ThemeExtensions {}
253
254impl fmt::Debug for ThemeExtensions {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.debug_struct("ThemeExtensions")
257            .field("count", &self.0.len())
258            .finish()
259    }
260}
261
262/// Styling information (kept backend-agnostic).
263#[cfg_attr(
264    feature = "terminal-serde",
265    derive(serde::Serialize, serde::Deserialize)
266)]
267#[derive(Clone, Copy, Debug, Default)]
268pub struct Style {
269    /// Foreground color.
270    pub fg: Option<Paint>,
271    /// Background color.
272    pub bg: Option<Paint>,
273    /// Relative transform applied to the resolved foreground color.
274    pub fg_transform: Option<ColorTransform>,
275    /// Relative transform applied to the resolved background color.
276    pub bg_transform: Option<ColorTransform>,
277    /// Contrast override applied after color transforms resolve.
278    pub contrast_policy: Option<ContrastPolicy>,
279    /// Bold modifier.
280    pub bold: Option<bool>,
281    /// Dim modifier.
282    pub dim: Option<bool>,
283    /// Italic modifier.
284    pub italic: Option<bool>,
285    /// Underline modifier.
286    pub underline: Option<bool>,
287    /// Reverse video modifier.
288    pub reverse: Option<bool>,
289    /// Strikethrough modifier.
290    pub strikethrough: Option<bool>,
291    /// Underline color (requires underline to be enabled).
292    pub underline_color: Option<Paint>,
293    /// Cell-level dim amount in `[0.0, 1.0]`.
294    ///
295    /// When set, the renderer scales the existing rendered colors of every cell
296    /// in the area by `(1.0 - dim_amount)` before drawing this style on top.
297    /// This makes `dim_by` work even when no explicit fg/bg colors are set,
298    /// which is the typical backdrop use-case.
299    pub dim_amount: Option<f32>,
300    /// Tint color and blend alpha in `[0.0, 1.0]`.
301    ///
302    /// When set, the renderer blends every existing cell color in the area
303    /// toward this color by the given alpha before drawing this style on top.
304    /// `Color::Reset` cell backgrounds are treated as black `(0, 0, 0)` for
305    /// blending, so the tint is visible even on transparent-background
306    /// terminals.
307    pub tint: Option<(Color, f32)>,
308}
309
310impl PartialEq for Style {
311    fn eq(&self, other: &Self) -> bool {
312        self.fg == other.fg
313            && self.bg == other.bg
314            && self.fg_transform == other.fg_transform
315            && self.bg_transform == other.bg_transform
316            && self.contrast_policy == other.contrast_policy
317            && self.bold == other.bold
318            && self.dim == other.dim
319            && self.italic == other.italic
320            && self.underline == other.underline
321            && self.reverse == other.reverse
322            && self.strikethrough == other.strikethrough
323            && self.underline_color == other.underline_color
324            && self.dim_amount.map(f32::to_bits) == other.dim_amount.map(f32::to_bits)
325            && self.tint.map(|(c, a)| (c, a.to_bits())) == other.tint.map(|(c, a)| (c, a.to_bits()))
326    }
327}
328
329impl Eq for Style {}
330
331#[cfg(all(test, feature = "terminal-serde"))]
332mod terminal_serde_tests {
333    use super::*;
334
335    #[test]
336    fn color_transform_round_trips() {
337        let transform = ColorTransform::OpacityToward {
338            factor: 0.42,
339            target: Color::rgb(1, 2, 3),
340        };
341        let json = serde_json::to_string(&transform).unwrap();
342        assert_eq!(
343            serde_json::from_str::<ColorTransform>(&json).unwrap(),
344            transform
345        );
346    }
347
348    #[test]
349    fn style_round_trips() {
350        let style = Style::default()
351            .fg(Paint::rgb(20, 30, 40))
352            .bg(Paint::rgba(1, 2, 3, 180))
353            .bold()
354            .underline()
355            .contrast_policy(ContrastPolicy::BlackOrWhite)
356            .tint_by(Color::Cyan, 0.25);
357        let json = serde_json::to_string(&style).unwrap();
358        assert_eq!(serde_json::from_str::<Style>(&json).unwrap(), style);
359    }
360}
361
362/// Describes how a widget-owned style slot consumes the active theme.
363///
364/// `Style` itself remains a partial overlay where `None` means “fall through to
365/// the layer below”. `StyleSlot` adds the missing slot-level intent for themed
366/// state styles such as selection, hover, focus, and active rows.
367#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
368pub enum StyleSlot {
369    /// Use the active theme role verbatim.
370    #[default]
371    Inherit,
372    /// Patch this style on top of the active theme role.
373    Extend(Style),
374    /// Use this style as the complete slot overlay; ignore the theme role.
375    Replace(Style),
376}
377
378impl StyleSlot {
379    /// Create a replacement slot from a style.
380    pub fn replace(style: Style) -> Self {
381        Self::Replace(style)
382    }
383
384    /// Create an extending slot from a style.
385    pub fn extend(style: Style) -> Self {
386        Self::Extend(style)
387    }
388
389    /// Return the explicit style when this slot carries one.
390    pub fn explicit_style(self) -> Option<Style> {
391        match self {
392            Self::Inherit => None,
393            Self::Extend(style) | Self::Replace(style) => Some(style),
394        }
395    }
396
397    /// Whether this slot carries a non-empty explicit style.
398    pub fn has_explicit_style(self) -> bool {
399        self.explicit_style().is_some_and(|style| !style.is_empty())
400    }
401
402    /// Whether this slot is guaranteed to resolve to an empty overlay without theme lookup.
403    pub fn is_empty(self) -> bool {
404        matches!(self, Self::Replace(style) if style.is_empty())
405    }
406}
407
408impl From<Style> for StyleSlot {
409    fn from(style: Style) -> Self {
410        Self::Replace(style)
411    }
412}
413
414/// Semantic style roles exposed by [`Theme`].
415#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
416pub enum ThemeRole {
417    /// Default widget text/surface style.
418    Base,
419    /// Interactive accent/emphasis style.
420    Accent,
421    /// Selected/current item style.
422    Selection,
423    /// Text/range selection style.
424    TextSelection,
425    /// Selection style for unfocused widgets. Falls back to [`Selection`](Self::Selection).
426    UnfocusedSelection,
427    /// Hover state style.
428    Hover,
429    /// Drag-source active style used while a source is being dragged.
430    ///
431    /// Currently resolves to the same palette as [`Hover`](Self::Hover) so existing
432    /// drag feedback stays unchanged, but it is semantically separate from pointer
433    /// hover and may receive a dedicated palette in a future theme revision.
434    DragSource,
435    /// Drop-zone affordance style for inactive but available drop targets.
436    ///
437    /// This role is intended for future always-visible or pre-active drop-zone
438    /// affordances. It currently resolves like [`Hover`](Self::Hover) and may be
439    /// unused by widgets until inactive drop-zone styling is introduced.
440    DropTarget,
441    /// Active drop-target style used while a compatible drag is over the target.
442    ///
443    /// Currently resolves to the same palette as [`Hover`](Self::Hover) so existing
444    /// drop highlight feedback stays unchanged, but it is semantically separate
445    /// from genuine pointer hover.
446    DropTargetActive,
447    /// Focus state style.
448    Focus,
449    /// Active state style. Currently resolves to the selection role by default.
450    Active,
451    /// Per-item hover state style.
452    ItemHover,
453    /// Border/frame style.
454    Border,
455    /// Disabled or muted secondary-content style.
456    Disabled,
457    /// Muted secondary-content style.
458    Muted,
459    /// Error/status style.
460    Error,
461    /// Focused input content style.
462    InputFocusContent,
463    /// Focused text-area content style.
464    TextAreaFocusContent,
465    /// Focused document-view content style.
466    DocumentViewFocusContent,
467    /// Focused hex-area content style.
468    HexAreaFocusContent,
469    /// Hex-area cursor style.
470    HexAreaCursor,
471    /// Focused terminal content style.
472    TerminalFocusContent,
473    /// Scrollbar thumb style.
474    ScrollbarThumb,
475    /// Focused scrollbar thumb style.
476    ScrollbarThumbFocus,
477    /// Scrollbar track style.
478    ScrollbarTrack,
479    /// Splitter hover handle style.
480    SplitterHover,
481    /// Splitter active handle style.
482    SplitterActive,
483}
484
485impl Hash for Style {
486    fn hash<H: Hasher>(&self, state: &mut H) {
487        self.fg.hash(state);
488        self.bg.hash(state);
489        self.fg_transform.hash(state);
490        self.bg_transform.hash(state);
491        self.contrast_policy.hash(state);
492        self.bold.hash(state);
493        self.dim.hash(state);
494        self.italic.hash(state);
495        self.underline.hash(state);
496        self.reverse.hash(state);
497        self.strikethrough.hash(state);
498        self.underline_color.hash(state);
499        self.dim_amount.map(f32::to_bits).hash(state);
500        if let Some((c, a)) = self.tint {
501            c.hash(state);
502            a.to_bits().hash(state);
503        }
504    }
505}
506
507impl Style {
508    /// Create a new, empty style.
509    pub fn new() -> Self {
510        Self::default()
511    }
512
513    /// Return the foreground paint's color, if one is set.
514    ///
515    /// This extracts the color from either an opaque or alpha paint. Alpha is
516    /// intentionally not composited because a style does not own its final
517    /// backdrop; use [`Paint::flatten_over`] when a concrete backdrop is known.
518    pub fn resolved_fg(&self) -> Option<Color> {
519        self.fg.map(Paint::color)
520    }
521
522    /// Return the background paint's color, if one is set.
523    ///
524    /// This extracts the color from either an opaque or alpha paint. Alpha is
525    /// intentionally not composited because a style does not own its final
526    /// backdrop; use [`Paint::flatten_over`] when a concrete backdrop is known.
527    pub fn resolved_bg(&self) -> Option<Color> {
528        self.bg.map(Paint::color)
529    }
530
531    /// Set foreground color.
532    pub fn fg(mut self, color: impl Into<Paint>) -> Self {
533        self.fg = Some(color.into());
534        self
535    }
536
537    /// Set background color.
538    pub fn bg(mut self, color: impl Into<Paint>) -> Self {
539        self.bg = Some(color.into());
540        self
541    }
542
543    /// Set foreground color with alpha in `[0.0, 1.0]`.
544    pub fn fg_alpha(mut self, color: Color, alpha: f32) -> Self {
545        self.fg = Some(Paint::from_color_alpha(color, alpha));
546        self
547    }
548
549    /// Set background color with alpha in `[0.0, 1.0]`.
550    pub fn bg_alpha(mut self, color: Color, alpha: f32) -> Self {
551        self.bg = Some(Paint::from_color_alpha(color, alpha));
552        self
553    }
554
555    /// Apply a relative transform to the resolved foreground color.
556    pub fn transform_fg(mut self, transform: ColorTransform) -> Self {
557        self.fg_transform = Some(transform.normalized());
558        self
559    }
560
561    /// Apply a relative transform to the resolved background color.
562    pub fn transform_bg(mut self, transform: ColorTransform) -> Self {
563        self.bg_transform = Some(transform.normalized());
564        self
565    }
566
567    /// Override contrast adjustment for this style only.
568    ///
569    /// Evaluated while resolving *this* style, before anything is drawn, so it can only weigh
570    /// the foreground against a background it can name: this style's own `bg`, else the
571    /// containing style's, else the terminal's. An alpha [`Paint`] is flattened against that
572    /// same assumed backdrop.
573    ///
574    /// That assumption is wrong for content floating over unrelated cells - a toast or popover
575    /// above live output. The real background is whatever those cells hold at paint time, which
576    /// no style-time pass can see, so the policy silently passes on a pairing that renders
577    /// unreadable.
578    ///
579    /// For that case use [`crate::widgets::EffectScope::contrast_policy`], which runs per cell
580    /// after compositing and therefore judges the colors actually on screen.
581    pub fn contrast_policy(mut self, policy: ContrastPolicy) -> Self {
582        self.contrast_policy = Some(policy);
583        self
584    }
585
586    /// Enable bold.
587    pub fn bold(mut self) -> Self {
588        self.bold = Some(true);
589        self
590    }
591
592    /// Explicitly disable bold.
593    ///
594    /// Sets `bold` to `Some(false)`, which prevents renderer-level bold
595    /// fallbacks from triggering and removes bold when patched onto a style
596    /// that already has it.
597    pub fn not_bold(mut self) -> Self {
598        self.bold = Some(false);
599        self
600    }
601
602    /// Enable dim.
603    pub fn dim(mut self) -> Self {
604        self.dim = Some(true);
605        self
606    }
607
608    /// Dim by an explicit amount in `[0.0, 1.0]`.
609    ///
610    /// - For explicit `fg`/`bg` colors the channels are scaled in color-space.
611    /// - Additionally, `dim_amount` is stored so the renderer can scale the
612    ///   existing rendered colors of every cell in the area (e.g. a backdrop)
613    ///   even when no explicit colors are set on this style.
614    pub fn dim_by(mut self, amount: f32) -> Self {
615        let amount = amount.clamp(0.0, 1.0);
616        self.fg_transform = Some(ColorTransform::Dim(amount));
617        self.bg_transform = Some(ColorTransform::Dim(amount));
618        self.dim_amount = Some(amount);
619        self
620    }
621
622    /// Blend toward `color` by `alpha` in `[0.0, 1.0]`.
623    ///
624    /// - `0.0` leaves colors unchanged.
625    /// - `1.0` replaces them with `color`.
626    ///
627    /// Unlike [`Self::dim_by`], this blends toward a specific color rather than black,
628    /// making it visible even on terminals that use [`Color::Reset`] as their
629    /// background (Reset is treated as black for blending).
630    ///
631    /// Like `dim_by`, this does two things:
632    ///
633    /// - transforms this style's own explicit `fg`/`bg` colors, which is what applies on an
634    ///   ordinary widget style; and
635    /// - stores a compositor hook so backdrop-style paths
636    ///   ([`crate::widgets::EffectScope`] and overlay backdrops) can blend the *already
637    ///   rendered* cell colors underneath, including cells this style set no color for.
638    ///
639    /// The two never double-apply: the compositor skips a transform that matches its own hook.
640    ///
641    /// Only the backdrop paths re-color cells beneath a widget. Setting this on a plain widget
642    /// style tints that widget's own colors and nothing else - to wash out a whole subtree,
643    /// wrap it in [`crate::widgets::EffectScope`].
644    pub fn tint_by(mut self, color: Color, alpha: f32) -> Self {
645        let alpha = alpha.clamp(0.0, 1.0);
646        self.fg_transform = Some(ColorTransform::Tint(color, alpha));
647        self.bg_transform = Some(ColorTransform::Tint(color, alpha));
648        self.tint = Some((color, alpha));
649        self
650    }
651
652    /// Lighten explicit `fg`/`bg` colors by an amount in `[0.0, 1.0]`.
653    ///
654    /// - `0.0` keeps explicit colors unchanged.
655    /// - `1.0` moves explicit colors to white.
656    ///
657    /// Unlike `dim_by`, this only affects colors explicitly set on this style.
658    pub fn lighten_by(mut self, amount: f32) -> Self {
659        let amount = amount.clamp(0.0, 1.0);
660        self.fg_transform = Some(ColorTransform::Lighten(amount));
661        self.bg_transform = Some(ColorTransform::Lighten(amount));
662        self
663    }
664
665    /// Elevate the resolved `fg`/`bg` colors by an amount in `[0.0, 1.0]`.
666    ///
667    /// The [`Style`] form of [`ColorTransform::Elevate`], and the one to reach for over
668    /// [`Self::lighten_by`] on a state layer such as a hover or focus style. Elevation is
669    /// luminance-aware, so it lifts a dark surface and dims a light one rather than washing both
670    /// toward white — a hover built on `lighten_by` loses contrast on light themes.
671    ///
672    /// Being a transform rather than an explicit color, it also composes with whatever the target
673    /// already resolved to. A hover style of `Style::new().bg(surface.elevate())` overwrites the
674    /// background of any element carrying its own — a marked row, a colored tab — while
675    /// `Style::new().elevate_by(0.08)` lifts that color instead of replacing it.
676    ///
677    /// ```
678    /// use tui_lipan::style::Style;
679    ///
680    /// // Hover lifts whatever this element already paints, including a per-item color.
681    /// let hover = Style::new().elevate_by(0.08);
682    /// ```
683    pub fn elevate_by(mut self, amount: f32) -> Self {
684        let amount = amount.clamp(0.0, 1.0);
685        self.fg_transform = Some(ColorTransform::Elevate(amount));
686        self.bg_transform = Some(ColorTransform::Elevate(amount));
687        self
688    }
689
690    /// Enable italic.
691    pub fn italic(mut self) -> Self {
692        self.italic = Some(true);
693        self
694    }
695
696    /// Enable underline.
697    pub fn underline(mut self) -> Self {
698        self.underline = Some(true);
699        self
700    }
701
702    /// Enable reverse video.
703    pub fn reverse(mut self) -> Self {
704        self.reverse = Some(true);
705        self
706    }
707
708    /// Enable strikethrough.
709    pub fn strikethrough(mut self) -> Self {
710        self.strikethrough = Some(true);
711        self
712    }
713
714    /// Set underline color. Also enables underline automatically.
715    pub fn underline_color(mut self, color: impl Into<Paint>) -> Self {
716        self.underline_color = Some(color.into());
717        self.underline = Some(true);
718        self
719    }
720
721    /// Returns `true` if this style has no colors or modifiers set.
722    ///
723    /// This is used to check whether a hover/focus style would have any effect.
724    pub fn is_empty(&self) -> bool {
725        self.fg.is_none()
726            && self.bg.is_none()
727            && self.fg_transform.is_none()
728            && self.bg_transform.is_none()
729            && self.contrast_policy.is_none()
730            && self.bold.is_none()
731            && self.dim.is_none()
732            && self.italic.is_none()
733            && self.underline.is_none()
734            && self.reverse.is_none()
735            && self.strikethrough.is_none()
736            && self.underline_color.is_none()
737            && self.dim_amount.is_none()
738            && self.tint.is_none()
739    }
740
741    /// Merge another style on top of this one.
742    ///
743    /// Colors from `other` take precedence if set.
744    /// Modifiers from `other` take precedence if set (Some).
745    ///
746    /// Color transforms (`transform_fg` / `transform_bg`) on `other` compose
747    /// on the *previous resolved color*: if `self` has an explicit color and
748    /// `other` only has a transform, the transform is applied to that resolved
749    /// color and baked in. Chaining `.patch()` calls therefore stacks
750    /// transforms in cascade order (e.g. `base.patch(hover).patch(focus)`
751    /// applies hover's transform to base, then focus's transform to the
752    /// hover-resolved color). Transforms only carry forward unresolved when
753    /// no color is yet available - see `merge_channel`.
754    pub fn patch(self, other: Style) -> Self {
755        let (bg, bg_transform) = merge_channel(
756            self.bg,
757            self.bg_transform,
758            other.bg,
759            other.bg_transform,
760            None,
761        );
762        let backdrop = bg.or(other.bg).or(self.bg);
763        let (fg, fg_transform) = merge_channel(
764            self.fg,
765            self.fg_transform,
766            other.fg,
767            other.fg_transform,
768            backdrop,
769        );
770
771        Self {
772            fg,
773            bg,
774            fg_transform,
775            bg_transform,
776            contrast_policy: other.contrast_policy.or(self.contrast_policy),
777            bold: other.bold.or(self.bold),
778            dim: other.dim.or(self.dim),
779            italic: other.italic.or(self.italic),
780            underline: other.underline.or(self.underline),
781            reverse: other.reverse.or(self.reverse),
782            strikethrough: other.strikethrough.or(self.strikethrough),
783            underline_color: merge_underline_color(other.underline_color, self.underline_color),
784            dim_amount: other.dim_amount.or(self.dim_amount),
785            tint: other.tint.or(self.tint),
786        }
787    }
788
789    pub(crate) fn resolve_color_transforms(self) -> Self {
790        let bg = resolve_channel(self.bg, self.bg_transform, None);
791        let mut fg = resolve_channel(self.fg, self.fg_transform, bg);
792        let mut fg_transform_remaining = None;
793
794        if matches!(fg, Some(Paint::Solid(Color::Transparent))) {
795            if let Some(c) = bg
796                && !matches!(c, Paint::Solid(Color::Transparent | Color::Backdrop))
797            {
798                fg = if let Some(t) = self.fg_transform {
799                    Some(t.apply_paint_with_backdrop(c, bg))
800                } else {
801                    Some(c)
802                };
803            } else {
804                fg_transform_remaining = self.fg_transform;
805            }
806        }
807        Self {
808            fg,
809            bg,
810            fg_transform: fg_transform_remaining,
811            bg_transform: None,
812            ..self
813        }
814    }
815}
816
817fn merge_underline_color(overlay: Option<Paint>, base: Option<Paint>) -> Option<Paint> {
818    match overlay {
819        None => base,
820        Some(Paint::Solid(Color::Transparent)) => base,
821        Some(c) => Some(c),
822    }
823}
824
825pub(crate) fn merge_channel(
826    base_color: Option<Paint>,
827    base_transform: Option<ColorTransform>,
828    overlay_color: Option<Paint>,
829    overlay_transform: Option<ColorTransform>,
830    backdrop: Option<Paint>,
831) -> (Option<Paint>, Option<ColorTransform>) {
832    let mut color = resolve_channel(base_color, base_transform, backdrop);
833    let mut transform = None;
834
835    if let Some(overlay_color) = overlay_color
836        && !matches!(overlay_color, Paint::Solid(Color::Transparent))
837    {
838        color = Some(overlay_color);
839    }
840
841    if let Some(overlay_transform) = overlay_transform {
842        if let Some(current) = color
843            && (!overlay_transform.needs_backdrop() || backdrop.is_some())
844        {
845            color = Some(overlay_transform.apply_paint_with_backdrop(current, backdrop));
846        } else {
847            transform = Some(overlay_transform.normalized());
848        }
849    }
850
851    (color, transform)
852}
853
854pub(crate) fn resolve_channel(
855    color: Option<Paint>,
856    transform: Option<ColorTransform>,
857    backdrop: Option<Paint>,
858) -> Option<Paint> {
859    match (color, transform) {
860        (Some(color), Some(transform)) => {
861            Some(transform.apply_paint_with_backdrop(color, backdrop))
862        }
863        (color, None) => color,
864        (None, Some(_)) => None,
865    }
866}
867
868#[cfg(test)]
869mod tests {
870    use super::{CaretShape, ColorTransform, Style, Theme, ThemePalette, ThemeRole};
871    use crate::app::ContrastPolicy;
872    use crate::style::{Color, HostTerminalColors, Paint};
873
874    fn p(color: Color) -> Option<Paint> {
875        Some(Paint::Solid(color))
876    }
877
878    #[test]
879    fn tint_by_transforms_this_styles_own_colors_as_well_as_the_backdrop_hook() {
880        // Regression: `tint_by` used to set only the compositor hook, which nothing outside a
881        // backdrop path reads. On an ordinary widget style it therefore did nothing at all, with
882        // no error - the caller just got an untinted widget.
883        let tinted = Style::new()
884            .fg(Color::Rgb(240, 240, 240))
885            .bg(Color::Rgb(200, 40, 40))
886            .tint_by(Color::Rgb(0, 0, 0), 0.5);
887
888        assert_eq!(
889            tinted.fg_transform,
890            Some(ColorTransform::Tint(Color::Rgb(0, 0, 0), 0.5)),
891        );
892        assert_eq!(
893            tinted.bg_transform,
894            Some(ColorTransform::Tint(Color::Rgb(0, 0, 0), 0.5)),
895        );
896        assert_eq!(tinted.tint, Some((Color::Rgb(0, 0, 0), 0.5)));
897
898        let resolved = tinted.resolve_color_transforms();
899        assert_eq!(resolved.fg, p(Color::Rgb(120, 120, 120)));
900        assert_eq!(resolved.bg, p(Color::Rgb(100, 20, 20)));
901    }
902
903    #[test]
904    fn elevate_transform_matches_the_absolute_elevate_step() {
905        // The point of the variant: a hover lift expressed as a transform has to land on exactly
906        // the color an absolute `Color::elevate` produces, so the same surface reached either way
907        // reads as one thing. `Lighten` cannot do that - it washes the surface cast out.
908        let surface = Color::Rgb(6, 14, 19);
909
910        assert_eq!(
911            ColorTransform::Elevate(0.08).apply(surface),
912            surface.elevate_by(0.08),
913        );
914        assert_ne!(
915            ColorTransform::Elevate(0.08).apply(surface),
916            ColorTransform::Lighten(0.08).apply(surface),
917        );
918    }
919
920    #[test]
921    fn elevate_transform_reverses_direction_on_a_light_surface() {
922        let light = Color::Rgb(245, 245, 245);
923        let lifted = ColorTransform::Elevate(0.08).apply(light);
924
925        assert!(
926            lifted.luminance() < light.luminance(),
927            "elevating a light surface dims it, got {lifted:?}",
928        );
929    }
930
931    #[test]
932    fn tint_by_mirrors_dim_by_so_neither_hook_is_silent_on_a_widget() {
933        // The two share a shape: transform this style's colors *and* leave a hook for backdrop
934        // paths. Keeping them aligned is what stops one of them being a no-op in the common case.
935        let dimmed = Style::new().fg(Color::Rgb(200, 200, 200)).dim_by(0.5);
936        let tinted = Style::new()
937            .fg(Color::Rgb(200, 200, 200))
938            .tint_by(Color::Black, 0.5);
939
940        assert!(dimmed.fg_transform.is_some() && dimmed.dim_amount.is_some());
941        assert!(tinted.fg_transform.is_some() && tinted.tint.is_some());
942    }
943
944    #[test]
945    fn resolved_style_channels_extract_opaque_and_alpha_colors() {
946        let style = Style::new()
947            .fg(Paint::rgba(10, 20, 30, 128))
948            .bg(Color::Backdrop);
949
950        assert_eq!(style.resolved_fg(), Some(Color::Rgb(10, 20, 30)));
951        assert_eq!(style.resolved_bg(), Some(Color::Backdrop));
952        assert_eq!(Style::default().resolved_fg(), None);
953        assert_eq!(Style::default().resolved_bg(), None);
954    }
955
956    #[test]
957    fn concretize_backdrop_resolves_all_sentinels_and_preserves_colors() {
958        let fallback = Color::Rgb(10, 20, 30);
959
960        for backdrop in [Color::Reset, Color::Backdrop, Color::Transparent] {
961            let mut theme = Theme::default();
962            theme.surface.backdrop = backdrop;
963            assert_eq!(theme.concretize_backdrop(Some(fallback)), fallback);
964        }
965
966        let mut theme = Theme::default();
967        theme.surface.backdrop = Color::Blue;
968        assert_eq!(theme.concretize_backdrop(Some(fallback)), Color::Blue);
969    }
970
971    #[test]
972    fn concretize_backdrop_uses_reset_without_primary_background() {
973        let mut theme = Theme::default();
974        theme.surface.backdrop = Color::Backdrop;
975        theme.surface.panel = Color::Reset;
976
977        assert_eq!(theme.concretize_backdrop(None), Color::Reset);
978    }
979
980    #[derive(Clone, Debug, PartialEq)]
981    struct BrandTheme {
982        accent_badge: Color,
983    }
984
985    #[test]
986    fn drag_drop_roles_initially_resolve_to_hover() {
987        let theme = Theme::default().hover(Style::new().fg(Color::White).bg(Color::Blue));
988
989        assert_eq!(theme.role(ThemeRole::DragSource), theme.hover);
990        assert_eq!(theme.role(ThemeRole::DropTarget), theme.hover);
991        assert_eq!(theme.role(ThemeRole::DropTargetActive), theme.hover);
992    }
993
994    #[test]
995    fn text_selection_role_is_distinct_from_item_selection() {
996        let theme = Theme::default()
997            .selection(Style::new().fg(Color::Red))
998            .text_selection(Style::new().fg(Color::Blue));
999
1000        assert_eq!(theme.role(ThemeRole::Selection), theme.selection);
1001        assert_eq!(theme.role(ThemeRole::TextSelection), theme.text_selection);
1002        assert_ne!(
1003            theme.role(ThemeRole::Selection),
1004            theme.role(ThemeRole::TextSelection)
1005        );
1006    }
1007
1008    #[test]
1009    fn focus_decoration_does_not_change_unfocused_selection() {
1010        let selection = Style::new().fg(Color::Yellow).bg(Color::Blue);
1011        let theme = Theme::default()
1012            .selection(selection)
1013            .focus(Style::new().fg(Color::Green))
1014            .focus_decoration(false);
1015
1016        assert!(theme.role(ThemeRole::Focus).is_empty());
1017        assert_eq!(theme.role(ThemeRole::UnfocusedSelection), selection);
1018    }
1019
1020    #[test]
1021    fn theme_palette_derives_distinct_selection_colors() {
1022        let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue)
1023            .selection(Color::Green)
1024            .text_selection(Color::Magenta)
1025            .into_theme();
1026
1027        assert_eq!(theme.selection.fg, p(Color::Green));
1028        assert_eq!(theme.text_selection.fg, p(Color::Magenta));
1029    }
1030
1031    #[test]
1032    fn theme_palette_derives_and_overrides_caret_defaults() {
1033        let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue).into_theme();
1034
1035        assert_eq!(theme.caret.shape, CaretShape::Block);
1036        assert_eq!(theme.caret.color, Some(Color::Blue));
1037
1038        let themed = ThemePalette::new(Color::White, Color::Black, Color::Blue)
1039            .caret_shape(CaretShape::Underline)
1040            .caret_color(Color::Magenta)
1041            .into_theme();
1042
1043        assert_eq!(themed.caret.shape, CaretShape::Underline);
1044        assert_eq!(themed.caret.color, Some(Color::Magenta));
1045    }
1046
1047    #[test]
1048    fn from_host_colors_uses_host_palette() {
1049        let mut ansi = std::array::from_fn(|i| Color::rgb(i as u8, i as u8, i as u8));
1050        ansi[1] = Color::rgb(210, 30, 40);
1051        ansi[2] = Color::rgb(30, 210, 40);
1052        ansi[3] = Color::rgb(210, 180, 40);
1053        ansi[4] = Color::rgb(30, 80, 210);
1054        let colors = HostTerminalColors {
1055            fg: Color::rgb(230, 231, 232),
1056            bg: Color::rgb(10, 11, 12),
1057            ansi,
1058        };
1059
1060        let theme = Theme::from_host_colors(colors);
1061
1062        assert_eq!(theme.primary.fg, p(colors.fg));
1063        assert_eq!(theme.primary.bg, p(colors.bg));
1064        assert_eq!(theme.accent.fg, p(colors.ansi[4]));
1065        assert_eq!(theme.status.success, colors.ansi[2]);
1066        assert_eq!(theme.status.warning, colors.ansi[3]);
1067        assert_eq!(theme.status.error, colors.ansi[1]);
1068        assert_eq!(theme.status.info, colors.ansi[4]);
1069    }
1070
1071    #[test]
1072    fn transform_fg_dims_inherited_color() {
1073        let base = Style::new().fg(Color::rgb(100, 120, 140));
1074        let overlay = Style::new().transform_fg(ColorTransform::Dim(0.5));
1075
1076        assert_eq!(
1077            base.patch(overlay).resolve_color_transforms().fg,
1078            p(Color::rgb(50, 60, 70))
1079        );
1080    }
1081
1082    #[test]
1083    fn lower_fg_transform_does_not_affect_overlay_color() {
1084        let base = Style::new()
1085            .fg(Color::rgb(100, 120, 140))
1086            .transform_fg(ColorTransform::Dim(0.5));
1087        let overlay = Style::new().fg(Color::rgb(10, 20, 30));
1088
1089        assert_eq!(
1090            base.patch(overlay).resolve_color_transforms().fg,
1091            p(Color::rgb(10, 20, 30))
1092        );
1093    }
1094
1095    #[test]
1096    fn patch_transparent_fg_preserves_base() {
1097        let base = Style::new().fg(Color::rgb(10, 20, 30));
1098        let overlay = Style::new().fg(Color::Transparent);
1099        assert_eq!(
1100            base.patch(overlay).resolve_color_transforms().fg,
1101            p(Color::rgb(10, 20, 30))
1102        );
1103    }
1104
1105    #[test]
1106    fn patch_transparent_bg_preserves_base() {
1107        let base = Style::new().bg(Color::rgb(40, 50, 60));
1108        let overlay = Style::new().bg(Color::Transparent);
1109        assert_eq!(
1110            base.patch(overlay).resolve_color_transforms().bg,
1111            p(Color::rgb(40, 50, 60))
1112        );
1113    }
1114
1115    #[test]
1116    fn patch_alpha_zero_bg_is_not_transparent_sentinel() {
1117        let base = Style::new().bg(Color::rgb(40, 50, 60));
1118        let overlay = Style::new().bg_alpha(Color::Red, 0.0);
1119        assert_eq!(
1120            base.patch(overlay).resolve_color_transforms().bg,
1121            Some(Paint::Alpha {
1122                color: Color::Red,
1123                alpha: 0,
1124            })
1125        );
1126    }
1127
1128    #[test]
1129    fn color_transform_apply_paint_preserves_alpha() {
1130        let paint = Paint::Alpha {
1131            color: Color::rgb(100, 120, 140),
1132            alpha: 128,
1133        };
1134
1135        assert_eq!(
1136            ColorTransform::Dim(0.5).apply_paint(paint),
1137            Paint::Alpha {
1138                color: Color::rgb(50, 60, 70),
1139                alpha: 128,
1140            }
1141        );
1142    }
1143
1144    #[test]
1145    fn patch_transparent_underline_color_preserves_base() {
1146        let base = Style::new().underline_color(Color::Red);
1147        let overlay = Style::new().underline_color(Color::Transparent);
1148        let patched = base.patch(overlay);
1149        assert_eq!(patched.underline_color, p(Color::Red));
1150    }
1151
1152    /// The reason `elevate_by` exists next to `lighten_by`: on a light surface a lighten pushes
1153    /// toward white and loses contrast, while an elevate moves away from the background. A hover
1154    /// or focus layer built on `lighten_by` is therefore wrong on light themes.
1155    #[test]
1156    fn style_elevate_by_moves_away_from_the_surface_on_both_polarities() {
1157        let dark = Color::rgb(20, 22, 30);
1158        let light = Color::rgb(235, 235, 240);
1159
1160        let dark_lifted = Style::new()
1161            .bg(dark)
1162            .elevate_by(0.08)
1163            .resolve_color_transforms()
1164            .bg;
1165        let light_lifted = Style::new()
1166            .bg(light)
1167            .elevate_by(0.08)
1168            .resolve_color_transforms()
1169            .bg;
1170
1171        assert_eq!(dark_lifted, p(ColorTransform::Elevate(0.08).apply(dark)));
1172        assert_eq!(light_lifted, p(ColorTransform::Elevate(0.08).apply(light)));
1173
1174        let lighter = |paint: Option<Paint>, base: Color| match paint {
1175            Some(Paint::Solid(color)) => color.luminance() > base.luminance(),
1176            other => panic!("expected a solid color, got {other:?}"),
1177        };
1178        assert!(lighter(dark_lifted, dark), "a dark surface must lift");
1179        assert!(!lighter(light_lifted, light), "a light surface must darken");
1180    }
1181
1182    /// `elevate_by` is a transform, so a state layer composes with whatever color the base already
1183    /// carries instead of replacing it — the property an explicit `bg(..)` hover style cannot have.
1184    #[test]
1185    fn style_elevate_by_lifts_the_base_color_rather_than_replacing_it() {
1186        let marked = Color::rgb(120, 40, 45);
1187        let plain = Color::rgb(30, 32, 40);
1188        let hover = Style::new().elevate_by(0.08);
1189
1190        let marked_hovered = Style::new()
1191            .bg(marked)
1192            .patch(hover)
1193            .resolve_color_transforms()
1194            .bg;
1195        let plain_hovered = Style::new()
1196            .bg(plain)
1197            .patch(hover)
1198            .resolve_color_transforms()
1199            .bg;
1200
1201        assert_ne!(
1202            marked_hovered,
1203            p(marked),
1204            "hover did not lift the base color"
1205        );
1206        assert_ne!(
1207            marked_hovered, plain_hovered,
1208            "hovering a marked surface collapsed it onto the plain hover color"
1209        );
1210    }
1211
1212    #[test]
1213    fn builder_order_does_not_change_transform_result() {
1214        let a = Style::new()
1215            .transform_fg(ColorTransform::Dim(0.5))
1216            .fg(Color::rgb(100, 120, 140));
1217        let b = Style::new()
1218            .fg(Color::rgb(100, 120, 140))
1219            .transform_fg(ColorTransform::Dim(0.5));
1220
1221        assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
1222        assert_eq!(a.resolve_color_transforms().fg, p(Color::rgb(50, 60, 70)));
1223    }
1224
1225    #[test]
1226    fn transform_chain_applies_in_patch_order() {
1227        let style = Style::new()
1228            .fg(Color::rgb(100, 120, 140))
1229            .patch(Style::new().transform_fg(ColorTransform::Dim(0.5)))
1230            .patch(Style::new().transform_fg(ColorTransform::Lighten(0.5)))
1231            .resolve_color_transforms();
1232
1233        assert_eq!(style.fg, p(Color::rgb(153, 158, 163)));
1234    }
1235
1236    #[test]
1237    fn state_cascade_stacks_bg_transforms_on_resolved_color() {
1238        // Models base.patch(hover).patch(focus) where hover and focus only
1239        // carry transforms - each must compose on the previous resolved bg,
1240        // not independently against the original base.
1241        let style = Style::new()
1242            .bg(Color::rgb(100, 100, 100))
1243            .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
1244            .patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
1245            .resolve_color_transforms();
1246
1247        assert_eq!(style.bg, p(Color::rgb(25, 25, 25)));
1248    }
1249
1250    #[test]
1251    fn opacity_turns_foreground_into_alpha_paint() {
1252        let style = Style::new()
1253            .fg(Color::rgb(245, 167, 66))
1254            .bg(Color::rgb(255, 255, 255))
1255            .transform_fg(ColorTransform::Opacity(0.6))
1256            .resolve_color_transforms();
1257
1258        assert_eq!(
1259            style.fg,
1260            Some(Paint::Alpha {
1261                color: Color::rgb(245, 167, 66),
1262                alpha: 153,
1263            })
1264        );
1265    }
1266
1267    #[test]
1268    fn opacity_builder_order_is_independent_when_background_arrives_later() {
1269        let a = Style::new()
1270            .transform_fg(ColorTransform::Opacity(0.6))
1271            .fg(Color::rgb(245, 167, 66))
1272            .bg(Color::rgb(255, 255, 255));
1273        let b = Style::new()
1274            .fg(Color::rgb(245, 167, 66))
1275            .bg(Color::rgb(255, 255, 255))
1276            .transform_fg(ColorTransform::Opacity(0.6));
1277
1278        assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
1279    }
1280
1281    #[test]
1282    fn opacity_toward_uses_fixed_target_not_backdrop() {
1283        let c = Color::rgb(0, 100, 200);
1284        let target = Color::rgb(200, 10, 30);
1285        assert_eq!(
1286            ColorTransform::OpacityToward {
1287                factor: 1.0,
1288                target,
1289            }
1290            .apply_with_backdrop(c, Some(Color::White)),
1291            c
1292        );
1293        assert_eq!(
1294            ColorTransform::OpacityToward {
1295                factor: 0.0,
1296                target,
1297            }
1298            .apply_with_backdrop(c, Some(Color::White)),
1299            target
1300        );
1301    }
1302
1303    #[test]
1304    fn patch_prefers_overlay_contrast_policy() {
1305        let base = Style::new().contrast_policy(ContrastPolicy::Wcag);
1306        let overlay = Style::new().contrast_policy(ContrastPolicy::Off);
1307
1308        assert_eq!(
1309            base.patch(overlay).contrast_policy,
1310            Some(ContrastPolicy::Off)
1311        );
1312    }
1313
1314    #[test]
1315    fn theme_extensions_roundtrip_and_affect_equality() {
1316        let a = Theme::default().with_extension(BrandTheme {
1317            accent_badge: Color::rgb(1, 2, 3),
1318        });
1319        let b = Theme::default().with_extension(BrandTheme {
1320            accent_badge: Color::rgb(1, 2, 3),
1321        });
1322        let c = Theme::default().with_extension(BrandTheme {
1323            accent_badge: Color::rgb(9, 8, 7),
1324        });
1325
1326        assert_eq!(a.extension::<BrandTheme>(), b.extension::<BrandTheme>());
1327        assert_eq!(a, b);
1328        assert_ne!(a, c);
1329    }
1330}
1331
1332/// Visual shape of the caret/cursor.
1333#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1334pub enum CaretShape {
1335    /// Block cursor (█), usually rendered with reverse video.
1336    #[default]
1337    Block,
1338    /// Bar cursor (│), rendered as a vertical line.
1339    Bar,
1340    /// Underline cursor (_), rendered as an underline.
1341    Underline,
1342}
1343
1344/// Global caret defaults for editable text-entry widgets.
1345///
1346/// Widget-level caret setters take precedence over these defaults. A `None`
1347/// color leaves the terminal's existing hardware caret color unchanged.
1348#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1349pub struct CaretPalette {
1350    /// Default caret shape.
1351    pub shape: CaretShape,
1352    /// Default hardware caret color sent through OSC 12 when supported.
1353    pub color: Option<Color>,
1354}
1355
1356impl CaretPalette {
1357    /// Create caret defaults with an optional hardware color.
1358    pub fn new(shape: CaretShape, color: Option<Color>) -> Self {
1359        Self { shape, color }
1360    }
1361
1362    /// Set the default hardware caret color.
1363    pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
1364        self.color = color.into();
1365        self
1366    }
1367}
1368
1369/// Custom glyphs for borders.
1370///
1371/// Mirrors `ratatui::symbols::border::Set`.
1372#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1373pub struct BorderGlyphs {
1374    /// Top left corner.
1375    pub top_left: &'static str,
1376    /// Top horizontal line.
1377    pub top: &'static str,
1378    /// Top right corner.
1379    pub top_right: &'static str,
1380    /// Left vertical line.
1381    pub left: &'static str,
1382    /// Right vertical line.
1383    pub right: &'static str,
1384    /// Bottom left corner.
1385    pub bottom_left: &'static str,
1386    /// Bottom horizontal line.
1387    pub bottom: &'static str,
1388    /// Bottom right corner.
1389    pub bottom_right: &'static str,
1390}
1391
1392impl Default for BorderGlyphs {
1393    fn default() -> Self {
1394        Self::PLAIN
1395    }
1396}
1397
1398impl BorderGlyphs {
1399    /// Standard plain border glyphs.
1400    pub const PLAIN: Self = Self {
1401        top_left: "┌",
1402        top: "─",
1403        top_right: "┐",
1404        left: "│",
1405        right: "│",
1406        bottom_left: "└",
1407        bottom: "─",
1408        bottom_right: "┘",
1409    };
1410
1411    /// Create a new custom border glyph set.
1412    pub fn new(parts: BorderGlyphsParts) -> Self {
1413        Self {
1414            top_left: parts.top_left,
1415            top: parts.top,
1416            top_right: parts.top_right,
1417            left: parts.left,
1418            right: parts.right,
1419            bottom_left: parts.bottom_left,
1420            bottom: parts.bottom,
1421            bottom_right: parts.bottom_right,
1422        }
1423    }
1424}
1425
1426/// Corner and edge glyphs for [`BorderGlyphs::new`].
1427pub struct BorderGlyphsParts {
1428    /// Top left corner.
1429    pub top_left: &'static str,
1430    /// Top horizontal line.
1431    pub top: &'static str,
1432    /// Top right corner.
1433    pub top_right: &'static str,
1434    /// Left vertical line.
1435    pub left: &'static str,
1436    /// Right vertical line.
1437    pub right: &'static str,
1438    /// Bottom left corner.
1439    pub bottom_left: &'static str,
1440    /// Bottom horizontal line.
1441    pub bottom: &'static str,
1442    /// Bottom right corner.
1443    pub bottom_right: &'static str,
1444}
1445
1446/// Border style for widgets.
1447#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1448pub enum BorderStyle {
1449    /// Standard single-line border (─│┌┐└┘).
1450    #[default]
1451    Plain,
1452    /// Rounded corners (─│╭╮╰╯).
1453    Rounded,
1454    /// Double-line border (═║╔╗╚╝).
1455    Double,
1456    /// Thick border.
1457    Thick,
1458    /// Light double-dashed border.
1459    LightDoubleDashed,
1460    /// Heavy double-dashed border.
1461    HeavyDoubleDashed,
1462    /// Light triple-dashed border.
1463    LightTripleDashed,
1464    /// Heavy triple-dashed border.
1465    HeavyTripleDashed,
1466    /// Light quadruple-dashed border.
1467    LightQuadrupleDashed,
1468    /// Heavy quadruple-dashed border.
1469    HeavyQuadrupleDashed,
1470    /// Custom border glyphs.
1471    Custom {
1472        /// The glyph set to use.
1473        glyphs: BorderGlyphs,
1474    },
1475}
1476
1477/// Scrollbar rendering variant.
1478#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1479pub enum ScrollbarVariant {
1480    /// Integrate scrollbar into the right border (lazygit-style).
1481    /// Falls back to `Standalone` when widget has no border.
1482    Integrated,
1483    /// Render scrollbar as a separate column consuming content width.
1484    #[default]
1485    Standalone,
1486}
1487
1488/// Scrollbar appearance (layout variant, gap, thumb, track styles).
1489///
1490/// Visibility is controlled by each widget's `.scrollbar(bool)` /
1491/// `.h_scrollbar(bool)`, not by this struct.
1492///
1493/// Node structs keep flat fields for efficient hot-path access; reconcile
1494/// unpacks `ScrollbarConfig` into individual fields.
1495#[derive(Clone, Debug, Default, PartialEq)]
1496pub struct ScrollbarConfig {
1497    /// Rendering variant (integrated into border or standalone column).
1498    pub variant: ScrollbarVariant,
1499    /// Empty cells reserved between content and a standalone scrollbar.
1500    pub gap: u16,
1501    /// Custom thumb character (default: '█').
1502    pub thumb: Option<char>,
1503    /// Custom thumb style.
1504    pub thumb_style: Option<Style>,
1505    /// Custom thumb style when the widget is focused.
1506    pub thumb_focus_style: Option<Style>,
1507    /// Custom track style.
1508    pub track_style: Option<Style>,
1509}
1510
1511impl ScrollbarConfig {
1512    /// Create a new `ScrollbarConfig` with defaults.
1513    pub fn new() -> Self {
1514        Self::default()
1515    }
1516
1517    /// Set scrollbar rendering variant.
1518    pub fn variant(mut self, variant: ScrollbarVariant) -> Self {
1519        self.variant = variant;
1520        self
1521    }
1522
1523    /// Reserve empty cells before a standalone scrollbar.
1524    pub fn gap(mut self, gap: u16) -> Self {
1525        self.gap = gap;
1526        self
1527    }
1528
1529    /// Set custom thumb character.
1530    pub fn thumb(mut self, ch: char) -> Self {
1531        self.thumb = Some(ch);
1532        self
1533    }
1534
1535    /// Set custom thumb style.
1536    pub fn thumb_style(mut self, style: Style) -> Self {
1537        self.thumb_style = Some(style);
1538        self
1539    }
1540
1541    /// Set custom thumb style when the widget is focused.
1542    pub fn thumb_focus_style(mut self, style: Style) -> Self {
1543        self.thumb_focus_style = Some(style);
1544        self
1545    }
1546
1547    /// Set custom track style.
1548    pub fn track_style(mut self, style: Style) -> Self {
1549        self.track_style = Some(style);
1550        self
1551    }
1552}
1553
1554/// Palette of semantic colors used for file icons.
1555///
1556/// These categories match the semantic grouping used by `mini.icons` in Neovim.
1557#[derive(Clone, Debug, PartialEq)]
1558pub struct FileIconPalette {
1559    /// Light blue (e.g. Markdown, documentation)
1560    pub azure: Color,
1561    /// Standard blue (e.g. Directories, CSS)
1562    pub blue: Color,
1563    /// Bright cyan (e.g. TypeScript, Docker)
1564    pub cyan: Color,
1565    /// Standard green (e.g. Go, Shell scripts)
1566    pub green: Color,
1567    /// Neutral grey (e.g. Lock files, logs)
1568    pub grey: Color,
1569    /// Vibrant orange (e.g. Java, HTML)
1570    pub orange: Color,
1571    /// Rich purple (e.g. C++, images)
1572    pub purple: Color,
1573    /// Standard red (e.g. Rust, Git files)
1574    pub red: Color,
1575    /// Bright yellow (e.g. JavaScript, Python)
1576    pub yellow: Color,
1577}
1578
1579impl Default for FileIconPalette {
1580    fn default() -> Self {
1581        Self {
1582            // One Dark inspired palette (vibrant and elastic)
1583            azure: Color::hex_u24(0x61AFEF),  // Light Blue
1584            blue: Color::hex_u24(0x4175E6),   // Standard Blue
1585            cyan: Color::hex_u24(0x56B6C2),   // Cyan
1586            green: Color::hex_u24(0x98C379),  // Green
1587            grey: Color::hex_u24(0xABB2BF),   // Grey
1588            orange: Color::hex_u24(0xD19A66), // Orange
1589            purple: Color::hex_u24(0xC678DD), // Purple
1590            red: Color::hex_u24(0xE06C75),    // Red
1591            yellow: Color::hex_u24(0xE5C07B), // Yellow
1592        }
1593    }
1594}
1595
1596/// Palette of colors for git status indicators.
1597#[derive(Clone, Copy, Debug, PartialEq)]
1598pub struct GitStatusPalette {
1599    /// Tracked file modified.
1600    pub modified: Color,
1601    /// New tracked file.
1602    pub added: Color,
1603    /// File deleted.
1604    pub deleted: Color,
1605    /// File renamed.
1606    pub renamed: Color,
1607    /// Untracked file.
1608    pub untracked: Color,
1609    /// Merge conflict.
1610    pub conflicted: Color,
1611}
1612
1613impl Default for GitStatusPalette {
1614    fn default() -> Self {
1615        Self {
1616            modified: Color::hex_u24(0xE5B767),
1617            added: Color::hex_u24(0x7EC699),
1618            deleted: Color::hex_u24(0xE57E7E),
1619            renamed: Color::hex_u24(0x76C5E5),
1620            untracked: Color::hex_u24(0xC59AE5),
1621            conflicted: Color::hex_u24(0xE57E7E),
1622        }
1623    }
1624}
1625
1626/// Palette of colors for scrollbars.
1627#[derive(Clone, Copy, Debug, PartialEq)]
1628pub struct ScrollbarPalette {
1629    /// Track color (the background of the scrollbar).
1630    pub track: Option<Color>,
1631    /// Thumb color (the draggable part).
1632    pub thumb: Color,
1633    /// Thumb color when focused.
1634    pub thumb_focus: Option<Color>,
1635}
1636
1637impl Default for ScrollbarPalette {
1638    fn default() -> Self {
1639        Self {
1640            track: None,
1641            thumb: Color::DarkGray,
1642            thumb_focus: Some(Color::Gray),
1643        }
1644    }
1645}
1646
1647/// Palette of colors for splitter interaction states.
1648#[derive(Clone, Copy, Debug, PartialEq)]
1649pub struct SplitterPalette {
1650    /// Handle/seam color when hovered.
1651    pub hover: Color,
1652    /// Handle/seam color while actively dragging.
1653    pub active: Color,
1654}
1655
1656impl Default for SplitterPalette {
1657    fn default() -> Self {
1658        Self {
1659            hover: Color::hex_u24(0x2563EB),
1660            active: Color::hex_u24(0x22D3EE),
1661        }
1662    }
1663}
1664
1665/// Surface colors used for layered UI chrome.
1666#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1667pub struct SurfacePalette {
1668    /// Base panel surface.
1669    pub panel: Color,
1670    /// Nested element/input surface.
1671    pub element: Color,
1672    /// Menu/popover surface.
1673    pub menu: Color,
1674    /// Backdrop/base underlay surface.
1675    pub backdrop: Color,
1676}
1677
1678/// Semantic status colors used across widgets and apps.
1679#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1680pub struct StatusPalette {
1681    /// Success state color.
1682    pub success: Color,
1683    /// Warning state color.
1684    pub warning: Color,
1685    /// Error state color.
1686    pub error: Color,
1687    /// Informational state color.
1688    pub info: Color,
1689}
1690
1691/// Semantic style palette for diff rendering.
1692#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1693pub struct DiffPalette {
1694    /// Style for unchanged/context lines.
1695    pub context: Style,
1696    /// Style for added lines.
1697    pub added: Style,
1698    /// Style for removed lines.
1699    pub removed: Style,
1700    /// Style for filler/empty lines in split diff layouts.
1701    pub empty: Style,
1702    /// Style for added word-level segments.
1703    pub added_word: Style,
1704    /// Style for removed word-level segments.
1705    pub removed_word: Style,
1706    /// Style for the added marker in the prefix gutter.
1707    pub added_marker: Style,
1708    /// Style for the removed marker in the prefix gutter.
1709    pub removed_marker: Style,
1710    /// Style for the line-number segment of unchanged/context lines in the gutter.
1711    pub context_line_number: Style,
1712    /// Style for the line-number segment of added lines in the gutter.
1713    pub added_line_number: Style,
1714    /// Style for the line-number segment of removed lines in the gutter.
1715    pub removed_line_number: Style,
1716    /// Style for context-collapse separator lines.
1717    pub context_separator_style: Style,
1718    /// Style for unified-diff `diff --git …` metadata (and inline file headers in multi-file patches).
1719    pub patch_header: Style,
1720}
1721
1722/// Semantic style palette for formatted documents and markdown.
1723#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1724pub struct DocumentPalette {
1725    /// Heading styles (h1 through h6).
1726    pub heading_styles: [Style; 6],
1727    /// Inline code style.
1728    pub code_inline: Style,
1729    /// Code block style.
1730    pub code_block: Style,
1731    /// Emphasis style.
1732    pub emphasis: Style,
1733    /// Strong style.
1734    pub strong: Style,
1735    /// Strikethrough style.
1736    pub strikethrough: Style,
1737    /// Link style.
1738    pub link: Style,
1739    /// Blockquote bar style.
1740    pub blockquote_bar: Style,
1741    /// Table border style.
1742    pub table_border: Style,
1743    /// Table header style.
1744    pub table_header: Style,
1745    /// Horizontal-rule style.
1746    pub hr: Style,
1747    /// Bullet point style for unordered lists.
1748    pub list_item: Style,
1749    /// Enumeration number style for ordered lists.
1750    pub list_enumeration: Style,
1751    /// Diagram node fill style.
1752    pub diagram_node_fill_style: Style,
1753    /// Diagram node border style.
1754    pub diagram_node_border_style: Style,
1755    /// Diagram node label style.
1756    pub diagram_node_label_style: Style,
1757    /// Diagram edge style.
1758    pub diagram_edge_style: Style,
1759    /// Diagram muted style for auxiliary glyphs (sequence lifelines, etc.).
1760    pub diagram_muted_style: Style,
1761}
1762
1763impl Default for DocumentPalette {
1764    fn default() -> Self {
1765        Self {
1766            heading_styles: [
1767                Style::new().bold().fg(Color::LightBlue),
1768                Style::new().bold().fg(Color::LightBlue),
1769                Style::new().bold().fg(Color::LightBlue),
1770                Style::new().bold(),
1771                Style::new().bold(),
1772                Style::new().bold().dim(),
1773            ],
1774            code_inline: Style::new().fg(Color::Green),
1775            code_block: Style::default(),
1776            emphasis: Style::new().italic(),
1777            strong: Style::new().bold(),
1778            strikethrough: Style::new().strikethrough(),
1779            link: Style::new().fg(Color::LightBlue).underline(),
1780            blockquote_bar: Style::new().fg(Color::DarkGray).dim(),
1781            table_border: Style::new().fg(Color::DarkGray).dim(),
1782            table_header: Style::new().bold(),
1783            hr: Style::new().fg(Color::DarkGray).dim(),
1784            list_item: Style::new().fg(Color::LightBlue).bold(),
1785            list_enumeration: Style::new().fg(Color::LightBlue).bold(),
1786            diagram_node_fill_style: Style::default(),
1787            diagram_node_border_style: Style::default(),
1788            diagram_node_label_style: Style::default(),
1789            diagram_edge_style: Style::default(),
1790            diagram_muted_style: Style::default(),
1791        }
1792    }
1793}
1794
1795/// Semantic style palette for syntax highlighting overlays.
1796#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1797pub struct SyntaxPalette {
1798    /// Style for comments and documentation text.
1799    pub comment: Style,
1800    /// Style for keywords and language control words.
1801    pub keyword: Style,
1802    /// Style for string literals.
1803    pub string: Style,
1804    /// Style for numeric literals.
1805    pub number: Style,
1806    /// Style for named constants, booleans, null-like values, and character literals.
1807    pub constant: Style,
1808    /// Style for function and method identifiers.
1809    pub function: Style,
1810    /// Style for built-in functions, types, classes, and constants.
1811    pub builtin: Style,
1812    /// Style for type names and class-like identifiers.
1813    pub type_name: Style,
1814    /// Style for regular identifiers/variables.
1815    pub variable: Style,
1816    /// Style for function parameters and argument-like bindings.
1817    pub parameter: Style,
1818    /// Style for operators and punctuation-like emphasis.
1819    pub operator: Style,
1820}
1821
1822impl Default for SyntaxPalette {
1823    fn default() -> Self {
1824        let number = Style::new().fg(Color::Yellow);
1825        let function = Style::new().fg(Color::Cyan);
1826        let variable = Style::new().fg(Color::White);
1827
1828        Self {
1829            comment: Style::new().fg(Color::DarkGray).italic().dim(),
1830            keyword: Style::new().fg(Color::LightMagenta),
1831            string: Style::new().fg(Color::Green),
1832            number,
1833            constant: number.lighten_by(0.12),
1834            function,
1835            builtin: function.italic(),
1836            type_name: Style::new().fg(Color::LightBlue),
1837            variable,
1838            parameter: variable.italic(),
1839            operator: Style::new().fg(Color::LightRed),
1840        }
1841    }
1842}
1843
1844/// Semantic interaction palette for single-line text inputs.
1845#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1846pub struct InputPalette {
1847    /// Style applied to focused input content when the theme opts in.
1848    ///
1849    /// This is intentionally empty by default so focused inputs keep their base
1850    /// text color unless the theme author explicitly requests otherwise.
1851    pub focus: Style,
1852}
1853
1854/// Semantic interaction palette for multi-line text editors.
1855#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1856pub struct TextAreaPalette {
1857    /// Style applied to focused text-area content when the theme opts in.
1858    ///
1859    /// This is intentionally empty by default so focused editors keep their
1860    /// base text color unless the theme author explicitly requests otherwise.
1861    pub focus: Style,
1862}
1863
1864/// Semantic interaction palette for read-only document surfaces.
1865#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1866pub struct DocumentViewPalette {
1867    /// Style applied to focused document content when the theme opts in.
1868    ///
1869    /// This is intentionally empty by default so focused documents keep their
1870    /// base text color unless the theme author explicitly requests otherwise.
1871    pub focus: Style,
1872}
1873
1874/// Semantic interaction palette for hex editors/viewers.
1875#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1876pub struct HexAreaPalette {
1877    /// Style applied to focused hex content when the theme opts in.
1878    pub focus: Style,
1879    /// Style applied to the active hex cursor/caret.
1880    pub cursor: Style,
1881}
1882
1883/// Semantic interaction palette for terminal surfaces.
1884#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1885pub struct TerminalPalette {
1886    /// Style applied to focused terminal content when the theme opts in.
1887    ///
1888    /// This is intentionally empty by default so focused terminals keep their
1889    /// base text color unless the theme author explicitly requests otherwise.
1890    pub focus: Style,
1891}
1892
1893/// Theme palette for common widget defaults.
1894#[derive(Clone, Debug, PartialEq)]
1895pub struct Theme {
1896    /// Primary style (e.g. text color).
1897    pub primary: Style,
1898    /// Style for interactive emphasis.
1899    ///
1900    /// Used for control hover, cursors, active glyphs, and other
1901    /// foreground-only emphasis that should not imply selection ownership.
1902    pub accent: Style,
1903    /// Global caret defaults for editable text-entry widgets.
1904    pub caret: CaretPalette,
1905    /// Style for selected/current items.
1906    pub selection: Style,
1907    /// Style for selected text or byte ranges.
1908    pub text_selection: Style,
1909    /// Style for focused widget chrome and focus affordances.
1910    ///
1911    /// Resolved at render time by widgets whose focus slot inherits or extends
1912    /// the theme role. Keep this empty to suppress theme-provided focus visuals
1913    /// while still allowing explicit widget-level `focus_style(...)` overrides.
1914    pub focus: Style,
1915    /// Whether theme-provided focus decoration is enabled.
1916    ///
1917    /// Disabling this suppresses focus roles derived from the theme, including
1918    /// focused-content palettes and focused scrollbar thumbs. Explicit widget
1919    /// focus styles still apply.
1920    pub focus_decoration: bool,
1921    /// Style for hovered items.
1922    pub hover: Style,
1923    /// Style for borders and frames.
1924    ///
1925    /// When set, frame borders and dividers use this `fg` instead of
1926    /// `primary.fg`, letting you dim borders independently of text.
1927    pub border: Style,
1928    /// Style for muted/secondary content.
1929    ///
1930    /// Applied to placeholders, disabled widgets, line numbers, scroll
1931    /// indicators, and empty-state text.
1932    pub muted: Style,
1933    /// Derived layered UI surfaces.
1934    pub surface: SurfacePalette,
1935    /// Semantic status colors.
1936    pub status: StatusPalette,
1937    /// Active/emphasized border color.
1938    pub border_active: Color,
1939    /// Color palette for file icons.
1940    pub file_icons: FileIconPalette,
1941    /// Colors for git status.
1942    pub git_status: GitStatusPalette,
1943    /// Semantic styles for diff views.
1944    pub diff: DiffPalette,
1945    /// Semantic styles for formatted documents and markdown.
1946    pub document: DocumentPalette,
1947    /// Semantic styles for syntax token recoloring.
1948    pub syntax: SyntaxPalette,
1949    /// Semantic interaction styles for single-line inputs.
1950    pub input: InputPalette,
1951    /// Semantic interaction styles for multi-line text editors.
1952    pub text_area: TextAreaPalette,
1953    /// Semantic interaction styles for read-only document surfaces.
1954    pub document_view: DocumentViewPalette,
1955    /// Semantic interaction styles for hex editors/viewers.
1956    pub hex_area: HexAreaPalette,
1957    /// Semantic interaction styles for terminal surfaces.
1958    pub terminal: TerminalPalette,
1959    /// Colors for scrollbars.
1960    pub scrollbar: ScrollbarPalette,
1961    /// Colors for splitter interaction states.
1962    pub splitter: SplitterPalette,
1963    /// Typed app-specific theme data stored alongside the framework palettes.
1964    #[doc(hidden)]
1965    pub extensions: ThemeExtensions,
1966}
1967
1968impl Theme {
1969    /// Build a deterministic theme from probed host terminal colors.
1970    pub fn from_host_colors(colors: HostTerminalColors) -> Self {
1971        ThemePalette::new(colors.fg, colors.bg, colors.ansi[4])
1972            .success(colors.ansi[2])
1973            .warning(colors.ansi[3])
1974            .error(colors.ansi[1])
1975            .info(colors.ansi[4])
1976            .into_theme()
1977            .with_extension(colors)
1978    }
1979
1980    /// Return the style associated with a semantic theme role.
1981    pub fn role(&self, role: ThemeRole) -> Style {
1982        match role {
1983            ThemeRole::Base => self.primary,
1984            ThemeRole::Accent => {
1985                let mut style = self.accent;
1986                if style.fg.is_none() {
1987                    style.fg = self.primary.fg;
1988                }
1989                if style.fg_transform.is_none() {
1990                    style.fg_transform = self.primary.fg_transform;
1991                }
1992                style
1993            }
1994            ThemeRole::Selection | ThemeRole::UnfocusedSelection => self.selection,
1995            ThemeRole::TextSelection => self.text_selection,
1996            ThemeRole::Hover
1997            | ThemeRole::DragSource
1998            | ThemeRole::DropTarget
1999            | ThemeRole::DropTargetActive
2000            | ThemeRole::ItemHover => self.hover,
2001            ThemeRole::Focus if self.focus_decoration => self.focus,
2002            ThemeRole::Focus => Style::default(),
2003            ThemeRole::Active => self.selection,
2004            ThemeRole::Border => self.primary.patch(self.border),
2005            ThemeRole::Disabled | ThemeRole::Muted => self.primary.patch(self.muted),
2006            ThemeRole::Error => Style::new().fg(self.status.error),
2007            ThemeRole::InputFocusContent if self.focus_decoration => self.input.focus,
2008            ThemeRole::TextAreaFocusContent if self.focus_decoration => self.text_area.focus,
2009            ThemeRole::DocumentViewFocusContent if self.focus_decoration => {
2010                self.document_view.focus
2011            }
2012            ThemeRole::HexAreaFocusContent if self.focus_decoration => self.hex_area.focus,
2013            ThemeRole::HexAreaCursor if self.focus_decoration => self.hex_area.cursor,
2014            ThemeRole::TerminalFocusContent if self.focus_decoration => self.terminal.focus,
2015            ThemeRole::InputFocusContent
2016            | ThemeRole::TextAreaFocusContent
2017            | ThemeRole::DocumentViewFocusContent
2018            | ThemeRole::HexAreaFocusContent
2019            | ThemeRole::HexAreaCursor
2020            | ThemeRole::TerminalFocusContent => Style::default(),
2021            ThemeRole::ScrollbarThumb => Style::new().bg(self.scrollbar.thumb),
2022            ThemeRole::ScrollbarThumbFocus if self.focus_decoration => self
2023                .scrollbar
2024                .thumb_focus
2025                .map(|color| Style::new().bg(color))
2026                .unwrap_or_default(),
2027            ThemeRole::ScrollbarThumbFocus => Style::default(),
2028            ThemeRole::ScrollbarTrack => self
2029                .scrollbar
2030                .track
2031                .map(|color| Style::new().bg(color))
2032                .unwrap_or_default(),
2033            ThemeRole::SplitterHover => Style::new().fg(self.splitter.hover),
2034            ThemeRole::SplitterActive => Style::new().fg(self.splitter.active),
2035        }
2036    }
2037
2038    /// Resolve the surface backdrop to a concrete host-usable color.
2039    ///
2040    /// Concrete authored backdrops are preserved. Sentinels prefer a concrete
2041    /// host background and otherwise fall back to the theme's panel surface.
2042    pub fn concretize_backdrop(&self, host_bg: Option<Color>) -> Color {
2043        if !self.surface.backdrop.is_sentinel() {
2044            return self.surface.backdrop;
2045        }
2046        host_bg
2047            .filter(|color| !color.is_sentinel())
2048            .unwrap_or_else(|| self.surface.panel.resolve(Color::Reset))
2049    }
2050
2051    /// Create a theme from the three core colors most apps care about.
2052    ///
2053    /// - `primary_fg`: default text/foreground color
2054    /// - `primary_bg`: base surface/background color
2055    /// - `accent`: interactive accent used for control emphasis, selection,
2056    ///   text selection, carets, splitters, and focused scrollbar thumbs
2057    pub fn custom(primary_fg: Color, primary_bg: Color, accent: Color) -> Self {
2058        let success = Color::Green;
2059        let warning = Color::Yellow;
2060        let error = Color::Red;
2061        let info = accent;
2062        let muted = primary_fg.blend_toward(primary_bg, 0.42);
2063        let border_active = accent.lighten_by(0.08);
2064        Self {
2065            primary: Style::new().fg(primary_fg).bg(primary_bg),
2066            accent: Style::new().fg(accent),
2067            caret: CaretPalette::new(CaretShape::default(), Some(accent)),
2068            selection: Style::new()
2069                .fg(accent)
2070                .bg(primary_bg.blend_toward(accent, 0.22)),
2071            text_selection: Style::new()
2072                .fg(accent)
2073                .bg(primary_bg.blend_toward(accent, 0.22)),
2074            focus: Style::new().fg(border_active),
2075            focus_decoration: true,
2076            hover: Style::default(),
2077            border: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.40)),
2078            muted: Style::new().fg(muted),
2079            surface: SurfacePalette {
2080                panel: primary_bg.elevate_by(0.07),
2081                element: primary_bg.elevate_by(0.04),
2082                menu: primary_bg.elevate_by(0.12),
2083                backdrop: primary_bg,
2084            },
2085            status: StatusPalette {
2086                success,
2087                warning,
2088                error,
2089                info,
2090            },
2091            border_active,
2092            file_icons: FileIconPalette::default(),
2093            git_status: GitStatusPalette::default(),
2094            diff: DiffPalette {
2095                context: Style::default(),
2096                added: Style::new().bg(primary_bg.blend_toward(success, 0.14)),
2097                removed: Style::new().bg(primary_bg.blend_toward(error, 0.16)),
2098                empty: Style::new().dim(),
2099                added_word: Style::new().bg(primary_bg.blend_toward(success, 0.24)),
2100                removed_word: Style::new().bg(primary_bg.blend_toward(error, 0.28)),
2101                added_marker: Style::new().fg(success),
2102                removed_marker: Style::new().fg(error),
2103                context_line_number: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.50)),
2104                added_line_number: Style::default(),
2105                removed_line_number: Style::default(),
2106                context_separator_style: Style::new()
2107                    .fg(primary_fg.blend_toward(primary_bg, 0.40))
2108                    .dim(),
2109                patch_header: Style::new()
2110                    .fg(accent.blend_toward(primary_fg, 0.35))
2111                    .bold(),
2112            },
2113            document: DocumentPalette {
2114                heading_styles: [
2115                    Style::new().bold().fg(accent.lighten_by(0.20)),
2116                    Style::new().bold().fg(accent.lighten_by(0.12)),
2117                    Style::new().bold().fg(accent),
2118                    Style::new().bold().fg(primary_fg),
2119                    Style::new().bold().fg(primary_fg),
2120                    Style::new().bold().fg(primary_fg).dim(),
2121                ],
2122                code_inline: Style::new().fg(success),
2123                code_block: Style::default(),
2124                emphasis: Style::new().italic(),
2125                strong: Style::new().bold(),
2126                strikethrough: Style::new().strikethrough(),
2127                link: Style::new().fg(accent).underline(),
2128                blockquote_bar: Style::new().fg(muted).dim(),
2129                table_border: Style::new()
2130                    .fg(primary_fg.blend_toward(primary_bg, 0.40))
2131                    .dim(),
2132                table_header: Style::new().bold(),
2133                hr: Style::new()
2134                    .fg(primary_fg.blend_toward(primary_bg, 0.40))
2135                    .dim(),
2136                list_item: Style::new().fg(accent).bold(),
2137                list_enumeration: Style::new().fg(accent).bold(),
2138                diagram_node_fill_style: Style::new().bg(primary_bg.blend_toward(accent, 0.10)),
2139                diagram_node_border_style: Style::new().fg(accent.lighten_by(0.08)),
2140                diagram_node_label_style: Style::new().fg(primary_fg),
2141                diagram_edge_style: Style::new().fg(accent.blend_toward(primary_fg, 0.20)),
2142                diagram_muted_style: Style::new().fg(muted).dim(),
2143            },
2144            syntax: SyntaxPalette {
2145                comment: Style::new().fg(muted).italic().dim(),
2146                keyword: Style::new().fg(accent),
2147                string: Style::new().fg(accent.blend_toward(success, 0.55)),
2148                number: Style::new().fg(accent.blend_toward(Color::Yellow, 0.60)),
2149                constant: Style::new()
2150                    .fg(accent.blend_toward(Color::Yellow, 0.52).lighten_by(0.10)),
2151                function: Style::new().fg(info.blend_toward(accent, 0.12)),
2152                builtin: Style::new().fg(info.blend_toward(accent, 0.28)).italic(),
2153                type_name: Style::new().fg(accent.blend_toward(info, 0.32)),
2154                variable: Style::new().fg(primary_fg),
2155                parameter: Style::new()
2156                    .fg(primary_fg.blend_toward(accent, 0.12))
2157                    .italic(),
2158                operator: Style::new().fg(accent.blend_toward(error, 0.45)),
2159            },
2160            input: InputPalette::default(),
2161            text_area: TextAreaPalette::default(),
2162            document_view: DocumentViewPalette::default(),
2163            hex_area: HexAreaPalette {
2164                focus: Style::default(),
2165                cursor: Style::new().fg(accent),
2166            },
2167            terminal: TerminalPalette::default(),
2168            scrollbar: ScrollbarPalette {
2169                track: Some(primary_bg.elevate_by(0.05)),
2170                thumb: primary_bg.elevate_by(0.20),
2171                thumb_focus: Some(accent.lighten_by(0.08)),
2172            },
2173            splitter: SplitterPalette {
2174                hover: accent.lighten_by(0.08),
2175                active: accent.lighten_by(0.18),
2176            },
2177            extensions: ThemeExtensions::default(),
2178        }
2179    }
2180
2181    /// Set the primary text/foreground style.
2182    ///
2183    /// This is the base color for labels, text, and borders across all widgets.
2184    /// Start from a preset or `Theme::default()` and override only what you need:
2185    ///
2186    /// ```rust
2187    /// use tui_lipan::{Theme, Style, Color};
2188    ///
2189    /// let theme = Theme::default()
2190    ///     .primary(Style::new().fg(Color::hex_u24(0xE0E0E0)))
2191    ///     .selection(Style::new().fg(Color::hex_u24(0xFF8000)));
2192    /// ```
2193    pub fn primary(mut self, style: Style) -> Self {
2194        self.primary = style;
2195        self
2196    }
2197
2198    /// Set the interactive accent style.
2199    ///
2200    /// Used for button hover, cursors, matches, active glyphs, and other
2201    /// non-selection emphasis. Unlike [`Theme::selection`], this should usually
2202    /// avoid painting a selection background.
2203    pub fn accent(mut self, style: Style) -> Self {
2204        self.accent = style;
2205        self
2206    }
2207
2208    /// Set global caret defaults for editable text-entry widgets.
2209    ///
2210    /// Explicit caret settings on an individual widget still take precedence.
2211    pub fn caret(mut self, palette: CaretPalette) -> Self {
2212        self.caret = palette;
2213        self
2214    }
2215
2216    /// Set the global caret shape for editable text-entry widgets.
2217    pub fn caret_shape(mut self, shape: CaretShape) -> Self {
2218        self.caret.shape = shape;
2219        self
2220    }
2221
2222    /// Set the global hardware caret color for editable text-entry widgets.
2223    ///
2224    /// Pass `None` to leave the terminal's existing caret color unchanged.
2225    pub fn caret_color(mut self, color: impl Into<Option<Color>>) -> Self {
2226        self.caret.color = color.into();
2227        self
2228    }
2229
2230    /// Set the focused-widget chrome style.
2231    ///
2232    /// `ThemeProvider` applies this to widget `focus_style` defaults. Keep it
2233    /// empty when you want the theme itself to stay visually quiet on focus
2234    /// while still allowing widgets to opt into explicit focus styles.
2235    pub fn focus(mut self, style: Style) -> Self {
2236        self.focus = style;
2237        self
2238    }
2239
2240    /// Enable or disable focus decoration supplied by this theme.
2241    ///
2242    /// Explicit widget focus styles remain active when this is disabled.
2243    pub fn focus_decoration(mut self, focus_decoration: bool) -> Self {
2244        self.focus_decoration = focus_decoration;
2245        self
2246    }
2247
2248    /// Attach typed app-specific theme data to this theme.
2249    ///
2250    /// This keeps app semantic tokens inside the framework theme tree so app
2251    /// code can read them through `Context::theme_extension::<T>()` while still
2252    /// relying on `ThemeProvider` as the single source of truth.
2253    pub fn with_extension<T>(mut self, extension: T) -> Self
2254    where
2255        T: ThemeExtension,
2256    {
2257        self.extensions.insert(extension);
2258        self
2259    }
2260
2261    /// Remove a previously attached typed theme extension.
2262    pub fn without_extension<T>(mut self) -> Self
2263    where
2264        T: ThemeExtension,
2265    {
2266        self.extensions.remove::<T>();
2267        self
2268    }
2269
2270    /// Return a typed app-specific theme extension if present.
2271    pub fn extension<T>(&self) -> Option<&T>
2272    where
2273        T: ThemeExtension,
2274    {
2275        self.extensions.get::<T>()
2276    }
2277
2278    /// Return a cloned typed app-specific theme extension if present.
2279    pub fn extension_cloned<T>(&self) -> Option<T>
2280    where
2281        T: ThemeExtension,
2282    {
2283        self.extension::<T>().cloned()
2284    }
2285
2286    /// Set the selected/current item style.
2287    ///
2288    /// Used for selected/current items in lists, tables, trees, and active tabs.
2289    /// Text/range selections use [`Theme::text_selection`] instead.
2290    pub fn selection(mut self, style: Style) -> Self {
2291        self.selection = style;
2292        self
2293    }
2294
2295    /// Set the text/range selection style.
2296    ///
2297    /// Used for selected ranges in `Input`, `TextArea`, `DocumentView`,
2298    /// `Terminal`, and `HexArea`.
2299    pub fn text_selection(mut self, style: Style) -> Self {
2300        self.text_selection = style;
2301        self
2302    }
2303
2304    /// Set the hover style.
2305    ///
2306    /// Hover is disabled by default for non-button widgets. Set this when you
2307    /// want row/surface hover feedback in lists, tables, trees, inputs, and
2308    /// similar widgets.
2309    pub fn hover(mut self, style: Style) -> Self {
2310        self.hover = style;
2311        self
2312    }
2313
2314    /// Set the border/frame style.
2315    ///
2316    /// Controls the foreground color used for frame borders and dividers.
2317    /// When set, borders are decoupled from the primary text color.
2318    pub fn border(mut self, style: Style) -> Self {
2319        self.border = style;
2320        self
2321    }
2322
2323    /// Set the muted/secondary style.
2324    ///
2325    /// Used for placeholders, disabled widgets, line numbers, scroll
2326    /// indicators, and empty-state text.
2327    pub fn muted(mut self, style: Style) -> Self {
2328        self.muted = style;
2329        self
2330    }
2331
2332    /// Set the scrollbar color palette.
2333    pub fn scrollbar(mut self, palette: ScrollbarPalette) -> Self {
2334        self.scrollbar = palette;
2335        self
2336    }
2337
2338    /// Set the splitter color palette.
2339    pub fn splitter(mut self, palette: SplitterPalette) -> Self {
2340        self.splitter = palette;
2341        self
2342    }
2343
2344    /// Set the file icon color palette.
2345    pub fn file_icons(mut self, palette: FileIconPalette) -> Self {
2346        self.file_icons = palette;
2347        self
2348    }
2349
2350    /// Set the git status color palette.
2351    pub fn git_status(mut self, palette: GitStatusPalette) -> Self {
2352        self.git_status = palette;
2353        self
2354    }
2355
2356    /// Set the semantic diff style palette.
2357    pub fn diff(mut self, palette: DiffPalette) -> Self {
2358        self.diff = palette;
2359        self
2360    }
2361
2362    /// Set the semantic document/markdown style palette.
2363    pub fn document(mut self, palette: DocumentPalette) -> Self {
2364        self.document = palette;
2365        self
2366    }
2367
2368    /// Set the semantic syntax highlighting palette.
2369    pub fn syntax(mut self, palette: SyntaxPalette) -> Self {
2370        self.syntax = palette;
2371        self
2372    }
2373
2374    /// Set the semantic interaction palette for single-line inputs.
2375    pub fn input(mut self, palette: InputPalette) -> Self {
2376        self.input = palette;
2377        self
2378    }
2379
2380    /// Set the semantic interaction palette for multi-line text editors.
2381    pub fn text_area(mut self, palette: TextAreaPalette) -> Self {
2382        self.text_area = palette;
2383        self
2384    }
2385
2386    /// Set the semantic interaction palette for read-only document surfaces.
2387    pub fn document_view(mut self, palette: DocumentViewPalette) -> Self {
2388        self.document_view = palette;
2389        self
2390    }
2391
2392    /// Set the semantic interaction palette for hex editors/viewers.
2393    pub fn hex_area(mut self, palette: HexAreaPalette) -> Self {
2394        self.hex_area = palette;
2395        self
2396    }
2397
2398    /// Set the semantic interaction palette for terminal surfaces.
2399    pub fn terminal(mut self, palette: TerminalPalette) -> Self {
2400        self.terminal = palette;
2401        self
2402    }
2403}
2404
2405/// A minimal color palette that derives a complete [`Theme`].
2406///
2407/// Set 3 required colors (text, background, accent) and optionally override
2408/// a few more. Everything else - accent, caret, selection, text selection, border, muted,
2409/// scrollbar, splitter, toast, diff, document, syntax, text-surface interaction, file-icon,
2410/// and git-status palettes - is derived automatically so every widget in the
2411/// tree shares a coherent look. Focus chrome is derived separately from the
2412/// accent token so apps can mute or restyle focus without affecting hover,
2413/// cursors, or other accent-driven states. Generic hover is intentionally
2414/// disabled by default and can be opted into later with [`Theme::hover`].
2415///
2416/// # Quick start
2417///
2418/// ```rust
2419/// use tui_lipan::{ThemePalette, Color};
2420///
2421/// // Three colors → full theme
2422/// let theme = ThemePalette::new(
2423///     Color::hex_u24(0xCDD6F4),  // text
2424///     Color::hex_u24(0x1E1E2E),  // background
2425///     Color::hex_u24(0xCBA6F7),  // accent
2426/// ).into_theme();
2427///
2428/// // Override just what you need
2429/// let theme = ThemePalette::new(
2430///     Color::hex_u24(0xCDD6F4),
2431///     Color::hex_u24(0x1E1E2E),
2432///     Color::hex_u24(0xCBA6F7),
2433/// )
2434/// .border(Color::hex_u24(0x585B70))
2435/// .selection(Color::hex_u24(0xCBA6F7))
2436/// .text_selection(Color::hex_u24(0x89B4FA))
2437/// .success(Color::hex_u24(0xA6E3A1))
2438/// .error(Color::hex_u24(0xF38BA8))
2439/// .into_theme();
2440/// ```
2441#[derive(Clone, Debug)]
2442pub struct ThemePalette {
2443    /// Main text/foreground color. Applied to all text and labels.
2444    pub text: Color,
2445    /// Primary background color.
2446    pub background: Color,
2447    /// Accent color used to derive interactive emphasis, caret color, and default selection styles.
2448    pub accent: Color,
2449    /// Global caret defaults for editable text-entry widgets. The default shape
2450    /// is `Block` and the default color is the palette accent.
2451    pub caret: CaretPalette,
2452    /// Color used to derive selected/current item styles. Default: accent.
2453    pub selection: Option<Color>,
2454    /// Color used to derive text/range selection styles. Default: accent.
2455    pub text_selection: Option<Color>,
2456    /// Border/frame color. Default: text blended 40 % toward background.
2457    pub border: Option<Color>,
2458    /// Muted/secondary text color (placeholders, disabled, line numbers).
2459    /// Default: text blended 50 % toward background.
2460    pub muted: Option<Color>,
2461    /// Scrollbar thumb color. Default: background lightened 16 %.
2462    pub scrollbar: Option<Color>,
2463    /// Success/green semantic color. Default: `#34D399`.
2464    pub success: Option<Color>,
2465    /// Warning/yellow semantic color. Default: `#FBBF24`.
2466    pub warning: Option<Color>,
2467    /// Error/red semantic color. Default: `#F43F5E`.
2468    pub error: Option<Color>,
2469    /// Info/blue semantic color. Default: accent.
2470    pub info: Option<Color>,
2471}
2472
2473impl ThemePalette {
2474    /// Create a palette from the three essential colors.
2475    pub fn new(text: Color, background: Color, accent: Color) -> Self {
2476        Self {
2477            text,
2478            background,
2479            accent,
2480            caret: CaretPalette::new(CaretShape::default(), Some(accent)),
2481            selection: None,
2482            text_selection: None,
2483            border: None,
2484            muted: None,
2485            scrollbar: None,
2486            success: None,
2487            warning: None,
2488            error: None,
2489            info: None,
2490        }
2491    }
2492
2493    /// Override the selected/current item color.
2494    pub fn selection(mut self, color: Color) -> Self {
2495        self.selection = Some(color);
2496        self
2497    }
2498
2499    /// Override the text/range selection color.
2500    pub fn text_selection(mut self, color: Color) -> Self {
2501        self.text_selection = Some(color);
2502        self
2503    }
2504
2505    /// Override the border/frame color.
2506    pub fn border(mut self, color: Color) -> Self {
2507        self.border = Some(color);
2508        self
2509    }
2510
2511    /// Override the muted/secondary text color.
2512    pub fn muted(mut self, color: Color) -> Self {
2513        self.muted = Some(color);
2514        self
2515    }
2516
2517    /// Override the scrollbar thumb color.
2518    pub fn scrollbar(mut self, color: Color) -> Self {
2519        self.scrollbar = Some(color);
2520        self
2521    }
2522
2523    /// Override the success semantic color.
2524    pub fn success(mut self, color: Color) -> Self {
2525        self.success = Some(color);
2526        self
2527    }
2528
2529    /// Override the warning semantic color.
2530    pub fn warning(mut self, color: Color) -> Self {
2531        self.warning = Some(color);
2532        self
2533    }
2534
2535    /// Override the error semantic color.
2536    pub fn error(mut self, color: Color) -> Self {
2537        self.error = Some(color);
2538        self
2539    }
2540
2541    /// Override the info semantic color.
2542    pub fn info(mut self, color: Color) -> Self {
2543        self.info = Some(color);
2544        self
2545    }
2546
2547    /// Override the global caret defaults for editable text-entry widgets.
2548    pub fn caret(mut self, palette: CaretPalette) -> Self {
2549        self.caret = palette;
2550        self
2551    }
2552
2553    /// Override the global caret shape for editable text-entry widgets.
2554    pub fn caret_shape(mut self, shape: CaretShape) -> Self {
2555        self.caret.shape = shape;
2556        self
2557    }
2558
2559    /// Override the global hardware caret color for editable text-entry widgets.
2560    ///
2561    /// Pass `None` to leave the terminal's existing caret color unchanged.
2562    pub fn caret_color(mut self, color: impl Into<Option<Color>>) -> Self {
2563        self.caret.color = color.into();
2564        self
2565    }
2566
2567    /// Convert this palette into a fully-derived [`Theme`].
2568    pub fn into_theme(self) -> Theme {
2569        Theme::from(self)
2570    }
2571}
2572
2573impl From<ThemePalette> for Theme {
2574    fn from(p: ThemePalette) -> Self {
2575        let border_color = p
2576            .border
2577            .unwrap_or_else(|| p.text.blend_toward(p.background, 0.40));
2578        let muted_color = p
2579            .muted
2580            .unwrap_or_else(|| p.text.blend_toward(p.background, 0.42));
2581        let scrollbar_thumb = p.scrollbar.unwrap_or_else(|| p.background.elevate_by(0.20));
2582
2583        let success = p.success.unwrap_or(Color::hex_u24(0x34D399));
2584        let warning = p.warning.unwrap_or(Color::hex_u24(0xFBBF24));
2585        let error = p.error.unwrap_or(Color::hex_u24(0xF43F5E));
2586        let info = p.info.unwrap_or(p.accent);
2587        let border_active = p.border.unwrap_or(p.accent).lighten_by(0.08);
2588        let selection = p.selection.unwrap_or(p.accent);
2589        let text_selection = p.text_selection.unwrap_or(p.accent);
2590
2591        Theme {
2592            primary: Style::new().fg(p.text).bg(p.background),
2593            accent: Style::new().fg(p.accent),
2594            caret: p.caret,
2595            selection: Style::new()
2596                .fg(selection)
2597                .bg(p.background.blend_toward(selection, 0.22)),
2598            text_selection: Style::new()
2599                .fg(text_selection)
2600                .bg(p.background.blend_toward(text_selection, 0.22)),
2601            focus: Style::new().fg(border_active),
2602            focus_decoration: true,
2603            hover: Style::default(),
2604            border: Style::new().fg(border_color),
2605            muted: Style::new().fg(muted_color),
2606            surface: SurfacePalette {
2607                panel: p.background.elevate_by(0.07),
2608                element: p.background.elevate_by(0.04),
2609                menu: p.background.elevate_by(0.12),
2610                backdrop: p.background,
2611            },
2612            status: StatusPalette {
2613                success,
2614                warning,
2615                error,
2616                info,
2617            },
2618            border_active,
2619            file_icons: FileIconPalette {
2620                green: success,
2621                red: error,
2622                yellow: warning,
2623                azure: info,
2624                blue: p.accent,
2625                cyan: info.lighten_by(0.10),
2626                grey: muted_color,
2627                orange: warning.blend_toward(error, 0.40),
2628                purple: p.accent.blend_toward(error, 0.30),
2629            },
2630            git_status: GitStatusPalette {
2631                modified: warning,
2632                added: success,
2633                deleted: error,
2634                renamed: info,
2635                untracked: p.accent.blend_toward(error, 0.30),
2636                conflicted: error,
2637            },
2638            diff: DiffPalette {
2639                context: Style::default(),
2640                added: Style::new().bg(p.background.blend_toward(success, 0.14)),
2641                removed: Style::new().bg(p.background.blend_toward(error, 0.16)),
2642                empty: Style::new().dim(),
2643                added_word: Style::new().bg(p.background.blend_toward(success, 0.24)),
2644                removed_word: Style::new().bg(p.background.blend_toward(error, 0.28)),
2645                added_marker: Style::new().fg(success),
2646                removed_marker: Style::new().fg(error),
2647                context_line_number: Style::new().fg(p.text.blend_toward(p.background, 0.50)),
2648                added_line_number: Style::default(),
2649                removed_line_number: Style::default(),
2650                context_separator_style: Style::new()
2651                    .fg(p.text.blend_toward(p.background, 0.40))
2652                    .dim(),
2653                patch_header: Style::new().fg(p.accent.blend_toward(p.text, 0.25)).bold(),
2654            },
2655            document: DocumentPalette {
2656                heading_styles: [
2657                    Style::new().bold().fg(p.accent.lighten_by(0.20)),
2658                    Style::new().bold().fg(p.accent.lighten_by(0.12)),
2659                    Style::new().bold().fg(p.accent),
2660                    Style::new().bold().fg(p.text),
2661                    Style::new().bold().fg(p.text),
2662                    Style::new().bold().fg(p.text).dim(),
2663                ],
2664                code_inline: Style::new().fg(success),
2665                code_block: Style::default(),
2666                emphasis: Style::new().italic(),
2667                strong: Style::new().bold(),
2668                strikethrough: Style::new().strikethrough(),
2669                link: Style::new().fg(p.accent).underline(),
2670                blockquote_bar: Style::new().fg(muted_color).dim(),
2671                table_border: Style::new().fg(border_color).dim(),
2672                table_header: Style::new().bold(),
2673                hr: Style::new().fg(border_color).dim(),
2674                list_item: Style::new().fg(p.accent).bold(),
2675                list_enumeration: Style::new().fg(p.accent).bold(),
2676                diagram_node_fill_style: Style::new().bg(p.background.blend_toward(p.accent, 0.10)),
2677                diagram_node_border_style: Style::new().fg(p.accent.lighten_by(0.08)),
2678                diagram_node_label_style: Style::new().fg(p.text),
2679                diagram_edge_style: Style::new().fg(p.accent.blend_toward(p.text, 0.20)),
2680                diagram_muted_style: Style::new().fg(muted_color).dim(),
2681            },
2682            syntax: SyntaxPalette {
2683                comment: Style::new().fg(muted_color).italic().dim(),
2684                keyword: Style::new().fg(p.accent),
2685                string: Style::new().fg(success.blend_toward(p.accent, 0.15)),
2686                number: Style::new().fg(warning.blend_toward(p.accent, 0.20)),
2687                constant: Style::new().fg(warning.blend_toward(p.text, 0.18)),
2688                function: Style::new().fg(info.blend_toward(p.accent, 0.10)),
2689                builtin: Style::new()
2690                    .fg(info.blend_toward(muted_color, 0.22))
2691                    .italic(),
2692                type_name: Style::new().fg(p.accent.blend_toward(info, 0.35)),
2693                variable: Style::new().fg(p.text),
2694                parameter: Style::new().fg(p.text).italic(),
2695                operator: Style::new().fg(error.blend_toward(p.accent, 0.45)),
2696            },
2697            input: InputPalette::default(),
2698            text_area: TextAreaPalette::default(),
2699            document_view: DocumentViewPalette::default(),
2700            hex_area: HexAreaPalette {
2701                focus: Style::default(),
2702                cursor: Style::new().fg(p.accent),
2703            },
2704            terminal: TerminalPalette::default(),
2705            scrollbar: ScrollbarPalette {
2706                track: Some(p.background.elevate_by(0.05)),
2707                thumb: scrollbar_thumb,
2708                thumb_focus: Some(p.accent.lighten_by(0.08)),
2709            },
2710            splitter: SplitterPalette {
2711                hover: p.accent.lighten_by(0.08),
2712                active: p.accent.lighten_by(0.18),
2713            },
2714            extensions: ThemeExtensions::default(),
2715        }
2716    }
2717}
2718
2719impl Default for Theme {
2720    fn default() -> Self {
2721        let mut theme: Self = ThemePalette::new(
2722            Color::hex_u24(0xE2E8F0),
2723            Color::hex_u24(0x0B121F),
2724            Color::hex_u24(0x7DCFFF),
2725        )
2726        .success(Color::hex_u24(0x34D399))
2727        .warning(Color::hex_u24(0xFBBF24))
2728        .error(Color::hex_u24(0xF43F5E))
2729        .info(Color::hex_u24(0x38BDF8))
2730        .into();
2731
2732        theme.file_icons = FileIconPalette {
2733            azure: Color::hex_u24(0x7DCFFF),
2734            blue: Color::hex_u24(0x60A5FA),
2735            cyan: Color::hex_u24(0x2DD4BF),
2736            green: Color::hex_u24(0x4ADE80),
2737            grey: Color::hex_u24(0x94A3B8),
2738            orange: Color::hex_u24(0xFB923C),
2739            purple: Color::hex_u24(0xC4B5FD),
2740            red: Color::hex_u24(0xF87171),
2741            yellow: Color::hex_u24(0xFBBF24),
2742        };
2743        theme.git_status = GitStatusPalette {
2744            modified: Color::hex_u24(0xFBBF24),
2745            added: Color::hex_u24(0x34D399),
2746            deleted: Color::hex_u24(0xFB7171),
2747            renamed: Color::hex_u24(0x38BDF8),
2748            untracked: Color::hex_u24(0xA78BFA),
2749            conflicted: Color::hex_u24(0xF43F5E),
2750        };
2751
2752        theme
2753    }
2754}