gpui/style.rs
1use crate::{
2 AbsoluteLength, App, Background, BackgroundTag, BorderStyle, Bounds, ColorExt, ContentMask,
3 Corners, CornersRefinement, CursorStyle, DefiniteLength, DevicePixels, Edges, EdgesRefinement,
4 Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, GridLocation, Length, Pixels, Point,
5 PointRefinement, ScaledPixels, SharedString, Size, SizeRefinement, Styled, TextRun, Window,
6 black, phi, point, px, quad, rems, size,
7};
8use collections::HashSet;
9use palette::{Hsla, IntoColor, rgb::Rgba};
10use refineable::Refineable;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use std::{iter, mem, ops::Range};
14
15/// Use this struct for interfacing with the 'debug_below' styling from your own elements.
16/// If a parent element has this style set on it, then this struct will be set as a global in
17/// GPUI.
18#[cfg(debug_assertions)]
19pub struct DebugBelow;
20
21#[cfg(debug_assertions)]
22impl crate::Global for DebugBelow {}
23
24/// How to fit the image into the bounds of the element.
25pub enum ObjectFit {
26 /// The image will be stretched to fill the bounds of the element.
27 Fill,
28 /// The image will be scaled to fit within the bounds of the element.
29 Contain,
30 /// The image will be scaled to cover the bounds of the element.
31 Cover,
32 /// The image will be scaled down to fit within the bounds of the element.
33 ScaleDown,
34 /// The image will maintain its original size.
35 None,
36}
37
38impl ObjectFit {
39 /// Get the bounds of the image within the given bounds.
40 pub fn get_bounds(
41 &self,
42 bounds: Bounds<Pixels>,
43 image_size: Size<DevicePixels>,
44 ) -> Bounds<Pixels> {
45 let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
46 let image_ratio = image_size.width / image_size.height;
47 let bounds_ratio = bounds.size.width / bounds.size.height;
48
49 match self {
50 ObjectFit::Fill => bounds,
51 ObjectFit::Contain => {
52 let new_size = if bounds_ratio > image_ratio {
53 size(
54 image_size.width * (bounds.size.height / image_size.height),
55 bounds.size.height,
56 )
57 } else {
58 size(
59 bounds.size.width,
60 image_size.height * (bounds.size.width / image_size.width),
61 )
62 };
63
64 Bounds {
65 origin: point(
66 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
67 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
68 ),
69 size: new_size,
70 }
71 }
72 ObjectFit::ScaleDown => {
73 // Check if the image is larger than the bounds in either dimension.
74 if image_size.width > bounds.size.width || image_size.height > bounds.size.height {
75 // If the image is larger, use the same logic as Contain to scale it down.
76 let new_size = if bounds_ratio > image_ratio {
77 size(
78 image_size.width * (bounds.size.height / image_size.height),
79 bounds.size.height,
80 )
81 } else {
82 size(
83 bounds.size.width,
84 image_size.height * (bounds.size.width / image_size.width),
85 )
86 };
87
88 Bounds {
89 origin: point(
90 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
91 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
92 ),
93 size: new_size,
94 }
95 } else {
96 // If the image is smaller than or equal to the container, display it at its original size,
97 // centered within the container.
98 let original_size = size(image_size.width, image_size.height);
99 Bounds {
100 origin: point(
101 bounds.origin.x + (bounds.size.width - original_size.width) / 2.0,
102 bounds.origin.y + (bounds.size.height - original_size.height) / 2.0,
103 ),
104 size: original_size,
105 }
106 }
107 }
108 ObjectFit::Cover => {
109 let new_size = if bounds_ratio > image_ratio {
110 size(
111 bounds.size.width,
112 image_size.height * (bounds.size.width / image_size.width),
113 )
114 } else {
115 size(
116 image_size.width * (bounds.size.height / image_size.height),
117 bounds.size.height,
118 )
119 };
120
121 Bounds {
122 origin: point(
123 bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
124 bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
125 ),
126 size: new_size,
127 }
128 }
129 ObjectFit::None => Bounds {
130 origin: bounds.origin,
131 size: image_size,
132 },
133 }
134 }
135}
136
137/// The minimum size of a column or row in a grid layout
138#[derive(
139 Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, JsonSchema, Serialize, Deserialize,
140)]
141pub enum GridTemplateMinSize {
142 /// The column or row size may be 0
143 #[default]
144 Zero,
145 /// The column or row size can be determined by the min content
146 MinContent,
147 /// The column or row size can be determined by the max content
148 MaxContent,
149}
150
151/// A simplified representation of the grid-template-* value
152#[derive(
153 Copy,
154 Clone,
155 Refineable,
156 PartialEq,
157 Eq,
158 PartialOrd,
159 Ord,
160 Debug,
161 Default,
162 JsonSchema,
163 Serialize,
164 Deserialize,
165)]
166pub struct GridTemplate {
167 /// How this template directive should be repeated
168 pub repeat: u16,
169 /// The minimum size in the repeat(<>, minmax(_, 1fr)) equation
170 pub min_size: GridTemplateMinSize,
171}
172
173/// The CSS styling that can be applied to an element via the `Styled` trait
174#[derive(Clone, Refineable, Debug)]
175#[refineable(Debug, PartialEq, Serialize, Deserialize)]
176pub struct Style {
177 /// What layout strategy should be used?
178 pub display: Display,
179
180 /// Should the element be painted on screen?
181 pub visibility: Visibility,
182
183 // Overflow properties
184 /// How children overflowing their container should affect layout
185 #[refineable]
186 pub overflow: Point<Overflow>,
187 /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
188 pub scrollbar_width: AbsoluteLength,
189 /// Whether both x and y axis should be scrollable at the same time.
190 pub allow_concurrent_scroll: bool,
191 /// Whether scrolling should be restricted to the input gesture's axis.
192 ///
193 /// Pixel-based scroll gestures are locked to their initially dominant axis. The lock may be
194 /// released when the gesture changes direction strongly. Touch phases delimit gestures when
195 /// available, with a timeout fallback for platforms that only emit moved events.
196 ///
197 /// This also prevents input from being remapped to another axis. For example, horizontal input
198 /// will not scroll a container that only has vertical overflow enabled. Mouse wheel platforms
199 /// typically report ordinary wheel input on the Y axis and Shift-modified input on the X axis.
200 ///
201 /// ## Motivation
202 ///
203 /// On the web when scrolling with the mouse wheel, scrolling up and down will always scroll the Y axis, even when
204 /// the mouse is over a horizontally-scrollable element.
205 ///
206 /// The only way to scroll horizontally is to hold down `Shift` while scrolling, which then changes the scroll axis
207 /// to the X axis.
208 ///
209 /// Currently, GPUI operates differently from the web in that it will scroll an element in either the X or Y axis
210 /// when scrolling with just the mouse wheel. This causes problems when scrolling in a vertical list that contains
211 /// horizontally-scrollable elements, as when you get to the horizontally-scrollable elements the scroll will be
212 /// hijacked.
213 ///
214 /// Ideally we would match the web's behavior and not have a need for this, but right now we're adding this opt-in
215 /// style property to limit the potential blast radius.
216 pub restrict_scroll_to_axis: bool,
217
218 // Position properties
219 /// What should the `position` value of this struct use as a base offset?
220 pub position: Position,
221 /// How should the position of this element be tweaked relative to the layout defined?
222 #[refineable]
223 pub inset: Edges<Length>,
224
225 // Size properties
226 /// Sets the initial size of the item
227 #[refineable]
228 pub size: Size<Length>,
229 /// Controls the minimum size of the item
230 #[refineable]
231 pub min_size: Size<Length>,
232 /// Controls the maximum size of the item
233 #[refineable]
234 pub max_size: Size<Length>,
235 /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
236 pub aspect_ratio: Option<f32>,
237
238 // Spacing Properties
239 /// How large should the margin be on each side?
240 #[refineable]
241 pub margin: Edges<Length>,
242 /// How large should the padding be on each side?
243 #[refineable]
244 pub padding: Edges<DefiniteLength>,
245 /// How large should the border be on each side?
246 #[refineable]
247 pub border_widths: Edges<AbsoluteLength>,
248
249 // Alignment properties
250 /// How this node's children aligned in the cross/block axis?
251 pub align_items: Option<AlignItems>,
252 /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
253 pub align_self: Option<AlignSelf>,
254 /// How should content contained within this item be aligned in the cross/block axis
255 pub align_content: Option<AlignContent>,
256 /// How should contained within this item be aligned in the main/inline axis
257 pub justify_content: Option<JustifyContent>,
258 /// How large should the gaps between items in a flex container be?
259 #[refineable]
260 pub gap: Size<DefiniteLength>,
261
262 // Flexbox properties
263 /// Which direction does the main axis flow in?
264 pub flex_direction: FlexDirection,
265 /// Should elements wrap, or stay in a single line?
266 pub flex_wrap: FlexWrap,
267 /// Sets the initial main axis size of the item
268 pub flex_basis: Length,
269 /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
270 pub flex_grow: f32,
271 /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
272 pub flex_shrink: f32,
273
274 /// The fill color of this element
275 pub background: Option<Fill>,
276
277 /// The border color of this element
278 pub border_color: Option<Hsla>,
279
280 /// The border style of this element
281 pub border_style: BorderStyle,
282
283 /// The radius of the corners of this element
284 #[refineable]
285 pub corner_radii: Corners<AbsoluteLength>,
286
287 /// Box shadow of the element
288 pub box_shadow: Vec<BoxShadow>,
289
290 /// Filters applied to this element's own content and children (CSS `filter`).
291 pub filter: Vec<Filter>,
292
293 /// Filters applied to the content rendered behind this element (CSS `backdrop-filter`).
294 pub backdrop_filter: Vec<Filter>,
295
296 /// The text style of this element
297 #[refineable]
298 pub text: TextStyleRefinement,
299
300 /// The mouse cursor style shown when the mouse pointer is over an element.
301 pub mouse_cursor: Option<CursorStyle>,
302
303 /// The opacity of this element
304 pub opacity: Option<f32>,
305
306 /// The grid columns of this element
307 /// Roughly equivalent to the Tailwind `grid-cols-<number>`
308 pub grid_cols: Option<GridTemplate>,
309
310 /// The row span of this element
311 /// Equivalent to the Tailwind `grid-rows-<number>`
312 pub grid_rows: Option<GridTemplate>,
313
314 /// The grid location of this element
315 pub grid_location: Option<GridLocation>,
316
317 /// Whether to draw a red debugging outline around this element
318 #[cfg(debug_assertions)]
319 pub debug: bool,
320
321 /// Whether to draw a red debugging outline around this element and all of its conforming children
322 #[cfg(debug_assertions)]
323 pub debug_below: bool,
324}
325
326impl Styled for StyleRefinement {
327 fn style(&mut self) -> &mut StyleRefinement {
328 self
329 }
330}
331
332impl StyleRefinement {
333 /// The grid location of this element
334 pub fn grid_location_mut(&mut self) -> &mut GridLocation {
335 self.grid_location.get_or_insert_default()
336 }
337}
338
339/// The value of the visibility property, similar to the CSS property `visibility`
340#[derive(Default, Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
341pub enum Visibility {
342 /// The element should be drawn as normal.
343 #[default]
344 Visible,
345 /// The element should not be drawn, but should still take up space in the layout.
346 Hidden,
347}
348
349/// The possible values of the box-shadow property
350#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
351pub struct BoxShadow {
352 /// What color should the shadow have?
353 pub color: Hsla,
354 /// How should it be offset from its element?
355 pub offset: Point<Pixels>,
356 /// How much should the shadow be blurred?
357 pub blur_radius: Pixels,
358 /// How much should the shadow spread?
359 pub spread_radius: Pixels,
360 /// Whether this is an inset shadow (drawn inside the element's bounds).
361 pub inset: bool,
362}
363
364impl BoxShadow {
365 /// Creates a new [`BoxShadow`] with the given offset and color, matching the order
366 /// of the CSS `box-shadow` property. Use the builder methods to set blur radius,
367 /// spread radius, and inset.
368 pub fn new(offset_x: Pixels, offset_y: Pixels, color: Hsla) -> Self {
369 Self {
370 color,
371 offset: point(offset_x, offset_y),
372 blur_radius: px(0.),
373 spread_radius: px(0.),
374 inset: false,
375 }
376 }
377
378 /// Sets the shadow blur radius.
379 pub fn blur_radius(mut self, blur_radius: Pixels) -> Self {
380 self.blur_radius = blur_radius;
381 self
382 }
383
384 /// Sets the shadow spread radius.
385 pub fn spread_radius(mut self, spread_radius: Pixels) -> Self {
386 self.spread_radius = spread_radius;
387 self
388 }
389
390 /// Marks the shadow as inset (drawn inside the element's bounds).
391 pub fn inset(mut self) -> Self {
392 self.inset = true;
393 self
394 }
395}
396
397/// A graphical filter that can be applied either to an element's own content
398/// (via [`Styled::filter`], like CSS `filter`) or to the content rendered behind
399/// it (via [`Styled::backdrop_filter`], like CSS `backdrop-filter`).
400#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
401pub enum Filter {
402 /// A gaussian blur with the given radius, in logical pixels. Maps to CSS `blur(<px>)`.
403 Blur(Pixels),
404}
405
406impl Filter {
407 /// Whether this filter has no visible effect, so painting can skip it entirely (and the
408 /// element can avoid the offscreen isolation pass when *all* of its filters are identities).
409 ///
410 /// Each variant declares its own no-op case here rather than the pipeline special-casing
411 /// blur — adding a filter that this returns `true` for is silently dropped before it ever
412 /// reaches the renderer.
413 pub fn is_identity(&self) -> bool {
414 match self {
415 Filter::Blur(radius) => *radius <= Pixels::ZERO,
416 }
417 }
418
419 /// Lower this logical-pixel filter into its scene-space ([`ScaledFilter`]) form for the
420 /// renderer, scaling any pixel magnitudes by `factor` (the window scale factor).
421 pub fn scale(&self, factor: f32) -> ScaledFilter {
422 match self {
423 Filter::Blur(radius) => ScaledFilter::Blur(radius.scale(factor)),
424 }
425 }
426}
427
428/// The scene-space (device-pixel) form of a [`Filter`], carried on the scene primitives that the
429/// renderers consume. Produced by [`Filter::scale`]; pixel magnitudes are in [`ScaledPixels`].
430///
431/// This is intentionally a separate enum from [`Filter`] (rather than reusing it) so the scene
432/// stays in device space like every other primitive, and so the renderers `match` on it
433/// exhaustively — adding a filter variant breaks each backend's match, forcing a deliberate
434/// implement-or-decline decision per backend instead of silently rendering nothing.
435#[derive(Clone, Copy, Debug, PartialEq)]
436pub enum ScaledFilter {
437 /// A gaussian blur with the given radius, in scaled (device) pixels.
438 Blur(ScaledPixels),
439}
440
441/// How to handle whitespace in text
442#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
443pub enum WhiteSpace {
444 /// Normal line wrapping when text overflows the width of the element
445 #[default]
446 Normal,
447 /// No line wrapping, text will overflow the width of the element
448 Nowrap,
449}
450
451/// How to truncate text that overflows the width of the element
452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
453pub enum TextOverflow {
454 /// Truncate the text at the end when it doesn't fit, and represent this truncation by
455 /// displaying the provided string (e.g., "very long te…").
456 Truncate(SharedString),
457 /// Truncate the text at the start when it doesn't fit, and represent this truncation by
458 /// displaying the provided string at the beginning (e.g., "…ong text here").
459 /// Typically more adequate for file paths where the end is more important than the beginning.
460 TruncateStart(SharedString),
461 /// Truncate the text in the middle when it doesn't fit, preserving both the start and end
462 /// of the string (e.g., "long fi…name.rs"). Useful for filenames where both the prefix
463 /// and the extension are important context.
464 TruncateMiddle(SharedString),
465}
466
467/// How to align text within the element
468#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
469pub enum TextAlign {
470 /// Align the text to the left of the element
471 #[default]
472 Left,
473
474 /// Center the text within the element
475 Center,
476
477 /// Align the text to the right of the element
478 Right,
479}
480
481/// Case mapping applied at layout time while keeping **UTF-8 byte lengths** unchanged.
482///
483/// [`TextTransform::Uppercase`] and [`TextTransform::Lowercase`] use Unicode full case folding via
484/// [`char::to_uppercase`] / [`char::to_lowercase`]. If mapping a code point would change its UTF-8
485/// length, or would replace one scalar value with more than one character, the **original**
486/// character is kept. That keeps indices into the underlying buffer aligned with hit-testing and
487/// editor-style caret positions that use raw byte offsets.
488///
489/// [`TextTransform::Capitalize`] matches CSS / Tailwind [`capitalize`][tw-cap] using Unicode word
490/// boundaries: only the first alphabetic code point in each word is uppercased; other characters
491/// are unchanged.
492///
493/// [tw-cap]: https://tailwindcss.com/docs/text-transform
494#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
495pub enum TextTransform {
496 /// Do not transform text.
497 #[default]
498 None,
499 /// Uppercase text (Unicode, byte-length preserving — see [`TextTransform`]).
500 Uppercase,
501 /// Lowercase text (Unicode, byte-length preserving — see [`TextTransform`]).
502 Lowercase,
503 /// `text-transform: capitalize` semantics (Tailwind class `capitalize`): per Unicode word, only
504 /// the first alphabetic character is mapped to uppercase; the rest of the string is unchanged.
505 Capitalize,
506}
507
508/// The properties that can be used to style text in GPUI
509#[derive(Refineable, Clone, Debug, PartialEq)]
510#[refineable(Debug, PartialEq, Serialize, Deserialize)]
511pub struct TextStyle {
512 /// The color of the text
513 pub color: Hsla,
514
515 /// The font family to use
516 pub font_family: SharedString,
517
518 /// The font features to use
519 pub font_features: FontFeatures,
520
521 /// The fallback fonts to use
522 pub font_fallbacks: Option<FontFallbacks>,
523
524 /// The font size to use, in pixels or rems.
525 pub font_size: AbsoluteLength,
526
527 /// The line height to use, in pixels or fractions
528 pub line_height: DefiniteLength,
529
530 /// The font weight, e.g. bold
531 pub font_weight: FontWeight,
532
533 /// The font style, e.g. italic
534 pub font_style: FontStyle,
535
536 /// The background color of the text
537 pub background_color: Option<Hsla>,
538
539 /// The underline style of the text
540 pub underline: Option<UnderlineStyle>,
541
542 /// The strikethrough style of the text
543 pub strikethrough: Option<StrikethroughStyle>,
544
545 /// How to handle whitespace in the text
546 pub white_space: WhiteSpace,
547
548 /// The text should be truncated if it overflows the width of the element
549 pub text_overflow: Option<TextOverflow>,
550
551 /// How the text should be aligned within the element
552 pub text_align: TextAlign,
553
554 /// The number of lines to display before truncating the text
555 pub line_clamp: Option<usize>,
556
557 /// Letter spacing added between characters, in pixels (positive widens, negative tightens).
558 ///
559 /// The platform text stack may clamp values outside the range it supports.
560 pub letter_spacing: Option<Pixels>,
561
562 /// Case transformation applied at layout time.
563 pub text_transform: Option<TextTransform>,
564}
565
566impl Default for TextStyle {
567 fn default() -> Self {
568 TextStyle {
569 color: black(),
570 // todo(linux) make this configurable or choose better default
571 font_family: ".SystemUIFont".into(),
572 font_features: FontFeatures::default(),
573 font_fallbacks: None,
574 font_size: rems(1.).into(),
575 line_height: phi(),
576 font_weight: FontWeight::default(),
577 font_style: FontStyle::default(),
578 background_color: None,
579 underline: None,
580 strikethrough: None,
581 white_space: WhiteSpace::Normal,
582 text_overflow: None,
583 text_align: TextAlign::default(),
584 line_clamp: None,
585 letter_spacing: None,
586 text_transform: None,
587 }
588 }
589}
590
591impl TextStyle {
592 /// Create a new text style with the given highlighting applied.
593 pub fn highlight(mut self, style: impl Into<HighlightStyle>) -> Self {
594 let style = style.into();
595 if let Some(weight) = style.font_weight {
596 self.font_weight = weight;
597 }
598 if let Some(style) = style.font_style {
599 self.font_style = style;
600 }
601
602 if let Some(color) = style.color {
603 self.color = self.color.blend(&color);
604 }
605
606 if let Some(factor) = style.fade_out {
607 self.color.fade_out(factor);
608 }
609
610 if let Some(background_color) = style.background_color {
611 self.background_color = Some(background_color);
612 }
613
614 if let Some(underline) = style.underline {
615 self.underline = Some(underline);
616 }
617
618 if let Some(strikethrough) = style.strikethrough {
619 self.strikethrough = Some(strikethrough);
620 }
621
622 self
623 }
624
625 /// Get the font configured for this text style.
626 pub fn font(&self) -> Font {
627 Font {
628 family: self.font_family.clone(),
629 features: self.font_features.clone(),
630 fallbacks: self.font_fallbacks.clone(),
631 weight: self.font_weight,
632 style: self.font_style,
633 }
634 }
635
636 /// Returns the rounded line height in pixels.
637 pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
638 self.line_height.to_pixels(self.font_size, rem_size).round()
639 }
640
641 /// Convert this text style into a [`TextRun`], for the given length of the text.
642 pub fn to_run(&self, len: usize) -> TextRun {
643 TextRun {
644 len,
645 font: Font {
646 family: self.font_family.clone(),
647 features: self.font_features.clone(),
648 fallbacks: self.font_fallbacks.clone(),
649 weight: self.font_weight,
650 style: self.font_style,
651 },
652 color: self.color,
653 background_color: self.background_color,
654 underline: self.underline,
655 strikethrough: self.strikethrough,
656 letter_spacing: self.letter_spacing,
657 }
658 }
659}
660
661/// A highlight style to apply, similar to a `TextStyle` except
662/// for a single font, uniformly sized and spaced text.
663///
664/// Layout extras on the base [`TextStyle`] — such as [`TextStyle::letter_spacing`] and
665/// [`TextStyle::text_transform`] — are not stored here; highlighted segments inherit them
666/// from the surrounding style (see [`TextStyle::highlight`] and
667/// [`crate::StyledText::with_default_highlights`]).
668#[derive(Copy, Clone, Debug, Default, PartialEq)]
669pub struct HighlightStyle {
670 /// The color of the text
671 pub color: Option<Hsla>,
672
673 /// The font weight, e.g. bold
674 pub font_weight: Option<FontWeight>,
675
676 /// The font style, e.g. italic
677 pub font_style: Option<FontStyle>,
678
679 /// The background color of the text
680 pub background_color: Option<Hsla>,
681
682 /// The underline style of the text
683 pub underline: Option<UnderlineStyle>,
684
685 /// The underline style of the text
686 pub strikethrough: Option<StrikethroughStyle>,
687
688 /// Similar to the CSS `opacity` property, this will cause the text to be less vibrant.
689 pub fade_out: Option<f32>,
690}
691
692impl Style {
693 /// Returns true if the style is visible and the background is opaque.
694 pub fn has_opaque_background(&self) -> bool {
695 self.background
696 .as_ref()
697 .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
698 }
699
700 /// Get the text style in this element style.
701 pub fn text_style(&self) -> Option<&TextStyleRefinement> {
702 if self.text.is_some() {
703 Some(&self.text)
704 } else {
705 None
706 }
707 }
708
709 /// Get the content mask for this element style, based on the given bounds.
710 /// If the element does not hide its overflow, this will return `None`.
711 pub fn overflow_mask(
712 &self,
713 bounds: Bounds<Pixels>,
714 rem_size: Pixels,
715 ) -> Option<ContentMask<Pixels>> {
716 match self.overflow {
717 Point {
718 x: Overflow::Visible,
719 y: Overflow::Visible,
720 } => None,
721 _ => {
722 let mut min = bounds.origin;
723 let mut max = bounds.bottom_right();
724
725 if self.border_color.is_some_and(|color| color.alpha > 0.) {
726 min.x += self.border_widths.left.to_pixels(rem_size);
727 max.x -= self.border_widths.right.to_pixels(rem_size);
728 min.y += self.border_widths.top.to_pixels(rem_size);
729 max.y -= self.border_widths.bottom.to_pixels(rem_size);
730 }
731
732 let bounds = match (
733 self.overflow.x == Overflow::Visible,
734 self.overflow.y == Overflow::Visible,
735 ) {
736 // x and y both visible
737 (true, true) => return None,
738 // x visible, y hidden
739 (true, false) => Bounds::from_corners(
740 point(min.x, bounds.origin.y),
741 point(max.x, bounds.bottom_right().y),
742 ),
743 // x hidden, y visible
744 (false, true) => Bounds::from_corners(
745 point(bounds.origin.x, min.y),
746 point(bounds.bottom_right().x, max.y),
747 ),
748 // both hidden
749 (false, false) => Bounds::from_corners(min, max),
750 };
751
752 Some(ContentMask { bounds })
753 }
754 }
755 }
756
757 /// Paints the background of an element styled with this style.
758 pub fn paint(
759 &self,
760 bounds: Bounds<Pixels>,
761 window: &mut Window,
762 cx: &mut App,
763 continuation: impl FnOnce(&mut Window, &mut App),
764 ) {
765 #[cfg(debug_assertions)]
766 if self.debug_below {
767 cx.set_global(DebugBelow)
768 }
769
770 #[cfg(debug_assertions)]
771 if self.debug || cx.has_global::<DebugBelow>() {
772 window.paint_quad(crate::outline(bounds, crate::red(), BorderStyle::default()));
773 }
774
775 let rem_size = window.rem_size();
776 let corner_radii = self
777 .corner_radii
778 .to_pixels(rem_size)
779 .clamp_radii_for_quad_size(bounds.size);
780
781 window.paint_drop_shadows(bounds, corner_radii, &self.box_shadow);
782
783 // Blur the content behind this element before its (typically translucent) background
784 // is painted on top, so the background tints the frosted backdrop (CSS `backdrop-filter`).
785 if !self.backdrop_filter.is_empty() {
786 window.paint_backdrop_filter(bounds, corner_radii, &self.backdrop_filter);
787 }
788
789 // The element's own box — background, inset shadows, children, and border — painted as a
790 // unit. A `filter` (CSS `filter`) wraps this whole unit so the renderer blurs the element
791 // and its children together as one group; without a filter it paints directly.
792 let paint_box = |window: &mut Window, cx: &mut App| {
793 let background_color = self.background.as_ref().and_then(Fill::color);
794 if background_color.is_some_and(|color| !color.is_transparent()) {
795 let mut border_color = match background_color {
796 Some(color) => match color.tag {
797 BackgroundTag::Solid
798 | BackgroundTag::PatternSlash
799 | BackgroundTag::Checkerboard => color.solid.into(),
800
801 BackgroundTag::LinearGradient => color
802 .colors
803 .first()
804 .map(|stop| stop.color.into())
805 .unwrap_or_default(),
806 },
807 None => Hsla::default(),
808 };
809 border_color.alpha = 0.;
810 window.paint_quad(quad(
811 bounds,
812 corner_radii,
813 background_color.unwrap_or_default(),
814 Edges::default(),
815 border_color,
816 self.border_style,
817 ));
818 }
819
820 window.paint_inset_shadows(bounds, corner_radii, &self.box_shadow);
821
822 continuation(window, cx);
823
824 if self.is_border_visible() {
825 let border_widths = self.border_widths.to_pixels(rem_size);
826 let mut background = self.border_color.unwrap_or_default();
827 background.alpha = 0.;
828 window.paint_quad(quad(
829 bounds,
830 corner_radii,
831 background,
832 border_widths,
833 self.border_color.unwrap_or_default(),
834 self.border_style,
835 ));
836 }
837 };
838
839 if self.filter.is_empty() {
840 paint_box(window, cx);
841 } else {
842 window.with_filter_layer(bounds, corner_radii, &self.filter, |window| {
843 paint_box(window, cx);
844 });
845 }
846
847 #[cfg(debug_assertions)]
848 if self.debug_below {
849 cx.remove_global::<DebugBelow>();
850 }
851 }
852
853 fn is_border_visible(&self) -> bool {
854 self.border_color.is_some_and(|color| color.alpha > 0.)
855 && self.border_widths.any(|length| !length.is_zero())
856 }
857}
858
859impl Default for Style {
860 fn default() -> Self {
861 Style {
862 display: Display::Block,
863 visibility: Visibility::Visible,
864 overflow: Point {
865 x: Overflow::Visible,
866 y: Overflow::Visible,
867 },
868 allow_concurrent_scroll: false,
869 restrict_scroll_to_axis: false,
870 scrollbar_width: AbsoluteLength::default(),
871 position: Position::Relative,
872 inset: Edges::auto(),
873 margin: Edges::<Length>::zero(),
874 padding: Edges::<DefiniteLength>::zero(),
875 border_widths: Edges::<AbsoluteLength>::zero(),
876 size: Size::auto(),
877 min_size: Size::auto(),
878 max_size: Size::auto(),
879 aspect_ratio: None,
880 gap: Size::default(),
881 // Alignment
882 align_items: None,
883 align_self: None,
884 align_content: None,
885 justify_content: None,
886 // Flexbox
887 flex_direction: FlexDirection::Row,
888 flex_wrap: FlexWrap::NoWrap,
889 flex_grow: 0.0,
890 flex_shrink: 1.0,
891 flex_basis: Length::Auto,
892 background: None,
893 border_color: None,
894 border_style: BorderStyle::default(),
895 corner_radii: Corners::default(),
896 box_shadow: Default::default(),
897 filter: Default::default(),
898 backdrop_filter: Default::default(),
899 text: TextStyleRefinement::default(),
900 mouse_cursor: None,
901 opacity: None,
902 grid_rows: None,
903 grid_cols: None,
904 grid_location: None,
905
906 #[cfg(debug_assertions)]
907 debug: false,
908 #[cfg(debug_assertions)]
909 debug_below: false,
910 }
911 }
912}
913
914/// The properties that can be applied to an underline.
915#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
916pub struct UnderlineStyle {
917 /// The thickness of the underline.
918 pub thickness: Pixels,
919
920 /// The color of the underline.
921 pub color: Option<Hsla>,
922
923 /// Whether the underline should be wavy, like in a spell checker.
924 pub wavy: bool,
925}
926
927/// The properties that can be applied to a strikethrough.
928#[derive(Refineable, Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
929pub struct StrikethroughStyle {
930 /// The thickness of the strikethrough.
931 pub thickness: Pixels,
932
933 /// The color of the strikethrough.
934 pub color: Option<Hsla>,
935}
936
937/// The kinds of fill that can be applied to a shape.
938#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
939pub enum Fill {
940 /// A solid color fill.
941 Color(Background),
942}
943
944impl Fill {
945 /// Unwrap this fill into a solid color, if it is one.
946 ///
947 /// If the fill is not a solid color, this method returns `None`.
948 pub fn color(&self) -> Option<Background> {
949 match self {
950 Fill::Color(color) => Some(*color),
951 }
952 }
953}
954
955impl Default for Fill {
956 fn default() -> Self {
957 Self::Color(Background::default())
958 }
959}
960
961impl<T: Into<Background>> From<T> for Fill {
962 fn from(color: T) -> Self {
963 Self::Color(color.into())
964 }
965}
966
967impl From<TextStyle> for HighlightStyle {
968 fn from(other: TextStyle) -> Self {
969 Self::from(&other)
970 }
971}
972
973impl From<&TextStyle> for HighlightStyle {
974 fn from(other: &TextStyle) -> Self {
975 Self {
976 color: Some(other.color),
977 font_weight: Some(other.font_weight),
978 font_style: Some(other.font_style),
979 background_color: other.background_color,
980 underline: other.underline,
981 strikethrough: other.strikethrough,
982 fade_out: None,
983 }
984 }
985}
986
987impl HighlightStyle {
988 /// Create a highlight style with just a color
989 pub fn color(color: Hsla) -> Self {
990 Self {
991 color: Some(color),
992 ..Default::default()
993 }
994 }
995 /// Blend this highlight style with another.
996 /// Non-continuous properties, like font_weight and font_style, are overwritten.
997 #[must_use]
998 pub fn highlight(self, other: HighlightStyle) -> Self {
999 Self {
1000 color: other
1001 .color
1002 .map(|other_color| {
1003 if let Some(color) = self.color {
1004 color.blend(&other_color)
1005 } else {
1006 other_color
1007 }
1008 })
1009 .or(self.color),
1010 font_weight: other.font_weight.or(self.font_weight),
1011 font_style: other.font_style.or(self.font_style),
1012 background_color: other.background_color.or(self.background_color),
1013 underline: other.underline.or(self.underline),
1014 strikethrough: other.strikethrough.or(self.strikethrough),
1015 fade_out: other
1016 .fade_out
1017 .map(|source_fade| {
1018 self.fade_out
1019 .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
1020 .unwrap_or(source_fade)
1021 })
1022 .or(self.fade_out),
1023 }
1024 }
1025}
1026
1027impl From<Hsla> for HighlightStyle {
1028 fn from(color: Hsla) -> Self {
1029 Self {
1030 color: Some(color),
1031 ..Default::default()
1032 }
1033 }
1034}
1035
1036impl From<FontWeight> for HighlightStyle {
1037 fn from(font_weight: FontWeight) -> Self {
1038 Self {
1039 font_weight: Some(font_weight),
1040 ..Default::default()
1041 }
1042 }
1043}
1044
1045impl From<FontStyle> for HighlightStyle {
1046 fn from(font_style: FontStyle) -> Self {
1047 Self {
1048 font_style: Some(font_style),
1049 ..Default::default()
1050 }
1051 }
1052}
1053
1054impl From<Rgba> for HighlightStyle {
1055 fn from(color: Rgba) -> Self {
1056 Self {
1057 color: Some(color.into_color()),
1058 ..Default::default()
1059 }
1060 }
1061}
1062
1063/// Combine and merge the highlights and ranges in the two iterators.
1064pub fn combine_highlights(
1065 a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1066 b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
1067) -> impl Iterator<Item = (Range<usize>, HighlightStyle)> {
1068 let mut endpoints = Vec::new();
1069 let mut highlights = Vec::new();
1070 for (range, highlight) in a.into_iter().chain(b) {
1071 if !range.is_empty() {
1072 let highlight_id = highlights.len();
1073 endpoints.push((range.start, highlight_id, true));
1074 endpoints.push((range.end, highlight_id, false));
1075 highlights.push(highlight);
1076 }
1077 }
1078 endpoints.sort_unstable_by_key(|(position, _, _)| *position);
1079 let mut endpoints = endpoints.into_iter().peekable();
1080
1081 let mut active_styles = HashSet::default();
1082 let mut ix = 0;
1083 iter::from_fn(move || {
1084 while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
1085 let prev_index = mem::replace(&mut ix, *endpoint_ix);
1086 if ix > prev_index && !active_styles.is_empty() {
1087 let current_style = active_styles
1088 .iter()
1089 .fold(HighlightStyle::default(), |acc, highlight_id| {
1090 acc.highlight(highlights[*highlight_id])
1091 });
1092 return Some((prev_index..ix, current_style));
1093 }
1094
1095 if *is_start {
1096 active_styles.insert(*highlight_id);
1097 } else {
1098 active_styles.remove(highlight_id);
1099 }
1100 endpoints.next();
1101 }
1102 None
1103 })
1104}
1105
1106/// Used to control how child nodes are aligned.
1107/// For Flexbox it controls alignment in the cross axis
1108/// For Grid it controls alignment in the block axis
1109///
1110/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)
1111#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1112// Copy of taffy::style type of the same name, to derive JsonSchema.
1113pub enum AlignItems {
1114 /// Items are packed toward the start of the axis
1115 Start,
1116 /// Items are packed toward the end of the axis
1117 End,
1118 /// Items are packed towards the flex-relative start of the axis.
1119 ///
1120 /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1121 /// to End. In all other cases it is equivalent to Start.
1122 FlexStart,
1123 /// Items are packed towards the flex-relative end of the axis.
1124 ///
1125 /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1126 /// to Start. In all other cases it is equivalent to End.
1127 FlexEnd,
1128 /// Items are packed along the center of the cross axis
1129 Center,
1130 /// Items are aligned such as their baselines align
1131 Baseline,
1132 /// Stretch to fill the container
1133 Stretch,
1134}
1135/// Used to control how child nodes are aligned.
1136/// Does not apply to Flexbox, and will be ignored if specified on a flex container
1137/// For Grid it controls alignment in the inline axis
1138///
1139/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-items)
1140pub type JustifyItems = AlignItems;
1141/// Used to control how the specified nodes is aligned.
1142/// Overrides the parent Node's `AlignItems` property.
1143/// For Flexbox it controls alignment in the cross axis
1144/// For Grid it controls alignment in the block axis
1145///
1146/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-self)
1147pub type AlignSelf = AlignItems;
1148/// Used to control how the specified nodes is aligned.
1149/// Overrides the parent Node's `JustifyItems` property.
1150/// Does not apply to Flexbox, and will be ignored if specified on a flex child
1151/// For Grid it controls alignment in the inline axis
1152///
1153/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-self)
1154pub type JustifySelf = AlignItems;
1155
1156/// Sets the distribution of space between and around content items
1157/// For Flexbox it controls alignment in the cross axis
1158/// For Grid it controls alignment in the block axis
1159///
1160/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)
1161#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
1162// Copy of taffy::style type of the same name, to derive JsonSchema.
1163pub enum AlignContent {
1164 /// Items are packed toward the start of the axis
1165 Start,
1166 /// Items are packed toward the end of the axis
1167 End,
1168 /// Items are packed towards the flex-relative start of the axis.
1169 ///
1170 /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1171 /// to End. In all other cases it is equivalent to Start.
1172 FlexStart,
1173 /// Items are packed towards the flex-relative end of the axis.
1174 ///
1175 /// For flex containers with flex_direction RowReverse or ColumnReverse this is equivalent
1176 /// to Start. In all other cases it is equivalent to End.
1177 FlexEnd,
1178 /// Items are centered around the middle of the axis
1179 Center,
1180 /// Items are stretched to fill the container
1181 Stretch,
1182 /// The first and last items are aligned flush with the edges of the container (no gap)
1183 /// The gap between items is distributed evenly.
1184 SpaceBetween,
1185 /// The gap between the first and last items is exactly THE SAME as the gap between items.
1186 /// The gaps are distributed evenly
1187 SpaceEvenly,
1188 /// The gap between the first and last items is exactly HALF the gap between items.
1189 /// The gaps are distributed evenly in proportion to these ratios.
1190 SpaceAround,
1191}
1192
1193/// Sets the distribution of space between and around content items
1194/// For Flexbox it controls alignment in the main axis
1195/// For Grid it controls alignment in the inline axis
1196///
1197/// [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)
1198pub type JustifyContent = AlignContent;
1199
1200/// Sets the layout used for the children of this node
1201///
1202/// The default values depends on on which feature flags are enabled. The order of precedence is: Flex, Grid, Block, None.
1203#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1204// Copy of taffy::style type of the same name, to derive JsonSchema.
1205pub enum Display {
1206 /// The children will follow the block layout algorithm
1207 Block,
1208 /// The children will follow the flexbox layout algorithm
1209 #[default]
1210 Flex,
1211 /// The children will follow the CSS Grid layout algorithm
1212 Grid,
1213 /// The children will not be laid out, and will follow absolute positioning
1214 None,
1215}
1216
1217/// Controls whether flex items are forced onto one line or can wrap onto multiple lines.
1218///
1219/// Defaults to [`FlexWrap::NoWrap`]
1220///
1221/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property)
1222#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1223// Copy of taffy::style type of the same name, to derive JsonSchema.
1224pub enum FlexWrap {
1225 /// Items will not wrap and stay on a single line
1226 #[default]
1227 NoWrap,
1228 /// Items will wrap according to this item's [`FlexDirection`]
1229 Wrap,
1230 /// Items will wrap in the opposite direction to this item's [`FlexDirection`]
1231 WrapReverse,
1232}
1233
1234/// The direction of the flexbox layout main axis.
1235///
1236/// There are always two perpendicular layout axes: main (or primary) and cross (or secondary).
1237/// Adding items will cause them to be positioned adjacent to each other along the main axis.
1238/// By varying this value throughout your tree, you can create complex axis-aligned layouts.
1239///
1240/// Items are always aligned relative to the cross axis, and justified relative to the main axis.
1241///
1242/// The default behavior is [`FlexDirection::Row`].
1243///
1244/// [Specification](https://www.w3.org/TR/css-flexbox-1/#flex-direction-property)
1245#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1246// Copy of taffy::style type of the same name, to derive JsonSchema.
1247pub enum FlexDirection {
1248 /// Defines +x as the main axis
1249 ///
1250 /// Items will be added from left to right in a row.
1251 #[default]
1252 Row,
1253 /// Defines +y as the main axis
1254 ///
1255 /// Items will be added from top to bottom in a column.
1256 Column,
1257 /// Defines -x as the main axis
1258 ///
1259 /// Items will be added from right to left in a row.
1260 RowReverse,
1261 /// Defines -y as the main axis
1262 ///
1263 /// Items will be added from bottom to top in a column.
1264 ColumnReverse,
1265}
1266
1267/// How children overflowing their container should affect layout
1268///
1269/// In CSS the primary effect of this property is to control whether contents of a parent container that overflow that container should
1270/// be displayed anyway, be clipped, or trigger the container to become a scroll container. However it also has secondary effects on layout,
1271/// the main ones being:
1272///
1273/// - The automatic minimum size Flexbox/CSS Grid items with non-`Visible` overflow is `0` rather than being content based
1274/// - `Overflow::Scroll` nodes have space in the layout reserved for a scrollbar (width controlled by the `scrollbar_width` property)
1275///
1276/// In Taffy, we only implement the layout related secondary effects as we are not concerned with drawing/painting. The amount of space reserved for
1277/// a scrollbar is controlled by the `scrollbar_width` property. If this is `0` then `Scroll` behaves identically to `Hidden`.
1278///
1279/// <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow>
1280#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1281// Copy of taffy::style type of the same name, to derive JsonSchema.
1282pub enum Overflow {
1283 /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1284 /// Content that overflows this node *should* contribute to the scroll region of its parent.
1285 #[default]
1286 Visible,
1287 /// The automatic minimum size of this node as a flexbox/grid item should be based on the size of its content.
1288 /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1289 Clip,
1290 /// The automatic minimum size of this node as a flexbox/grid item should be `0`.
1291 /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1292 Hidden,
1293 /// The automatic minimum size of this node as a flexbox/grid item should be `0`. Additionally, space should be reserved
1294 /// for a scrollbar. The amount of space reserved is controlled by the `scrollbar_width` property.
1295 /// Content that overflows this node should *not* contribute to the scroll region of its parent.
1296 Scroll,
1297}
1298
1299/// The positioning strategy for this item.
1300///
1301/// This controls both how the origin is determined for the [`Style::position`] field,
1302/// and whether or not the item will be controlled by flexbox's layout algorithm.
1303///
1304/// WARNING: this enum follows the behavior of [CSS's `position` property](https://developer.mozilla.org/en-US/docs/Web/CSS/position),
1305/// which can be unintuitive.
1306///
1307/// [`Position::Relative`] is the default value, in contrast to the default behavior in CSS.
1308#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, JsonSchema)]
1309// Copy of taffy::style type of the same name, to derive JsonSchema.
1310pub enum Position {
1311 /// The offset is computed relative to the final position given by the layout algorithm.
1312 /// Offsets do not affect the position of any other items; they are effectively a correction factor applied at the end.
1313 #[default]
1314 Relative,
1315 /// The offset is computed relative to this item's closest positioned ancestor, if any.
1316 /// Otherwise, it is placed relative to the origin.
1317 /// No space is created for the item in the page layout, and its size will not be altered.
1318 ///
1319 /// WARNING: to opt-out of layouting entirely, you must use [`Display::None`] instead on your [`Style`] object.
1320 Absolute,
1321}
1322
1323impl From<AlignItems> for taffy::style::AlignItems {
1324 fn from(value: AlignItems) -> Self {
1325 match value {
1326 AlignItems::Start => Self::START,
1327 AlignItems::End => Self::END,
1328 AlignItems::FlexStart => Self::FLEX_START,
1329 AlignItems::FlexEnd => Self::FLEX_END,
1330 AlignItems::Center => Self::CENTER,
1331 AlignItems::Baseline => Self::BASELINE,
1332 AlignItems::Stretch => Self::STRETCH,
1333 }
1334 }
1335}
1336
1337impl From<AlignContent> for taffy::style::AlignContent {
1338 fn from(value: AlignContent) -> Self {
1339 match value {
1340 AlignContent::Start => Self::START,
1341 AlignContent::End => Self::END,
1342 AlignContent::FlexStart => Self::FLEX_START,
1343 AlignContent::FlexEnd => Self::FLEX_END,
1344 AlignContent::Center => Self::CENTER,
1345 AlignContent::Stretch => Self::STRETCH,
1346 AlignContent::SpaceBetween => Self::SPACE_BETWEEN,
1347 AlignContent::SpaceEvenly => Self::SPACE_EVENLY,
1348 AlignContent::SpaceAround => Self::SPACE_AROUND,
1349 }
1350 }
1351}
1352
1353impl From<Display> for taffy::style::Display {
1354 fn from(value: Display) -> Self {
1355 match value {
1356 Display::Block => Self::Block,
1357 Display::Flex => Self::Flex,
1358 Display::Grid => Self::Grid,
1359 Display::None => Self::None,
1360 }
1361 }
1362}
1363
1364impl From<FlexWrap> for taffy::style::FlexWrap {
1365 fn from(value: FlexWrap) -> Self {
1366 match value {
1367 FlexWrap::NoWrap => Self::NoWrap,
1368 FlexWrap::Wrap => Self::Wrap,
1369 FlexWrap::WrapReverse => Self::WrapReverse,
1370 }
1371 }
1372}
1373
1374impl From<FlexDirection> for taffy::style::FlexDirection {
1375 fn from(value: FlexDirection) -> Self {
1376 match value {
1377 FlexDirection::Row => Self::Row,
1378 FlexDirection::Column => Self::Column,
1379 FlexDirection::RowReverse => Self::RowReverse,
1380 FlexDirection::ColumnReverse => Self::ColumnReverse,
1381 }
1382 }
1383}
1384
1385impl From<Overflow> for taffy::style::Overflow {
1386 fn from(value: Overflow) -> Self {
1387 match value {
1388 Overflow::Visible => Self::Visible,
1389 Overflow::Clip => Self::Clip,
1390 Overflow::Hidden => Self::Hidden,
1391 Overflow::Scroll => Self::Scroll,
1392 }
1393 }
1394}
1395
1396impl From<Position> for taffy::style::Position {
1397 fn from(value: Position) -> Self {
1398 match value {
1399 Position::Relative => Self::Relative,
1400 Position::Absolute => Self::Absolute,
1401 }
1402 }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407 use crate::{blue, green, px, red, yellow};
1408 use palette::WithAlpha;
1409
1410 use super::*;
1411
1412 #[test]
1413 fn test_basic_highlight_style_combination() {
1414 let style_a = HighlightStyle::default();
1415 let style_b = HighlightStyle::default();
1416 let style_a = style_a.highlight(style_b);
1417 assert_eq!(
1418 style_a,
1419 HighlightStyle::default(),
1420 "Combining empty styles should not produce a non-empty style."
1421 );
1422
1423 let mut style_b = HighlightStyle {
1424 color: Some(red()),
1425 strikethrough: Some(StrikethroughStyle {
1426 thickness: px(2.),
1427 color: Some(blue()),
1428 }),
1429 fade_out: Some(0.),
1430 font_style: Some(FontStyle::Italic),
1431 font_weight: Some(FontWeight(300.)),
1432 background_color: Some(yellow()),
1433 underline: Some(UnderlineStyle {
1434 thickness: px(2.),
1435 color: Some(red()),
1436 wavy: true,
1437 }),
1438 };
1439 let expected_style = style_b;
1440
1441 let style_a = style_a.highlight(style_b);
1442 assert_eq!(
1443 style_a, expected_style,
1444 "Blending an empty style with another style should return the other style"
1445 );
1446
1447 let style_b = style_b.highlight(Default::default());
1448 assert_eq!(
1449 style_b, expected_style,
1450 "Blending a style with an empty style should not change the style."
1451 );
1452
1453 let mut style_c = expected_style;
1454
1455 let style_d = HighlightStyle {
1456 color: Some(blue().with_alpha(0.7)),
1457 strikethrough: Some(StrikethroughStyle {
1458 thickness: px(4.),
1459 color: Some(crate::red()),
1460 }),
1461 fade_out: Some(0.),
1462 font_style: Some(FontStyle::Oblique),
1463 font_weight: Some(FontWeight(800.)),
1464 background_color: Some(green()),
1465 underline: Some(UnderlineStyle {
1466 thickness: px(4.),
1467 color: None,
1468 wavy: false,
1469 }),
1470 };
1471
1472 let expected_style = HighlightStyle {
1473 color: Some(red().blend(&blue().with_alpha(0.7))),
1474 strikethrough: Some(StrikethroughStyle {
1475 thickness: px(4.),
1476 color: Some(red()),
1477 }),
1478 // TODO this does not seem right
1479 fade_out: Some(0.),
1480 font_style: Some(FontStyle::Oblique),
1481 font_weight: Some(FontWeight(800.)),
1482 background_color: Some(green()),
1483 underline: Some(UnderlineStyle {
1484 thickness: px(4.),
1485 color: None,
1486 wavy: false,
1487 }),
1488 };
1489
1490 let style_c = style_c.highlight(style_d);
1491 assert_eq!(
1492 style_c, expected_style,
1493 "Blending styles should blend properties where possible and override all others"
1494 );
1495 }
1496
1497 #[test]
1498 fn test_combine_highlights() {
1499 assert_eq!(
1500 combine_highlights(
1501 [
1502 (0..5, green().into()),
1503 (4..10, FontWeight::BOLD.into()),
1504 (15..20, yellow().into()),
1505 ],
1506 [
1507 (2..6, FontStyle::Italic.into()),
1508 (1..3, blue().into()),
1509 (21..23, red().into()),
1510 ]
1511 )
1512 .collect::<Vec<_>>(),
1513 [
1514 (
1515 0..1,
1516 HighlightStyle {
1517 color: Some(green()),
1518 ..Default::default()
1519 }
1520 ),
1521 (
1522 1..2,
1523 HighlightStyle {
1524 color: Some(blue()),
1525 ..Default::default()
1526 }
1527 ),
1528 (
1529 2..3,
1530 HighlightStyle {
1531 color: Some(blue()),
1532 font_style: Some(FontStyle::Italic),
1533 ..Default::default()
1534 }
1535 ),
1536 (
1537 3..4,
1538 HighlightStyle {
1539 color: Some(green()),
1540 font_style: Some(FontStyle::Italic),
1541 ..Default::default()
1542 }
1543 ),
1544 (
1545 4..5,
1546 HighlightStyle {
1547 color: Some(green()),
1548 font_weight: Some(FontWeight::BOLD),
1549 font_style: Some(FontStyle::Italic),
1550 ..Default::default()
1551 }
1552 ),
1553 (
1554 5..6,
1555 HighlightStyle {
1556 font_weight: Some(FontWeight::BOLD),
1557 font_style: Some(FontStyle::Italic),
1558 ..Default::default()
1559 }
1560 ),
1561 (
1562 6..10,
1563 HighlightStyle {
1564 font_weight: Some(FontWeight::BOLD),
1565 ..Default::default()
1566 }
1567 ),
1568 (
1569 15..20,
1570 HighlightStyle {
1571 color: Some(yellow()),
1572 ..Default::default()
1573 }
1574 ),
1575 (
1576 21..23,
1577 HighlightStyle {
1578 color: Some(red()),
1579 ..Default::default()
1580 }
1581 )
1582 ]
1583 );
1584 }
1585
1586 #[test]
1587 fn test_text_style_refinement() {
1588 let mut style = Style::default();
1589 style.refine(&StyleRefinement::default().text_size(px(20.0)));
1590 style.refine(&StyleRefinement::default().font_weight(FontWeight::SEMIBOLD));
1591
1592 assert_eq!(
1593 Some(AbsoluteLength::from(px(20.0))),
1594 style.text_style().unwrap().font_size
1595 );
1596
1597 assert_eq!(
1598 Some(FontWeight::SEMIBOLD),
1599 style.text_style().unwrap().font_weight
1600 );
1601 }
1602}