basalt/interface/bin/
style.rs

1use std::sync::Arc;
2
3use vulkano::format::FormatFeatures;
4use vulkano::image::{Image, ImageType};
5
6use crate::image_cache::ImageCacheKey;
7use crate::interface::{Bin, Color};
8use crate::NonExhaustive;
9
10/// Position of a `Bin`
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
12pub enum BinPosition {
13    /// Position will be done from the window's dimensions
14    #[default]
15    Window,
16    /// Position will be done from the parent's dimensions
17    Parent,
18    /// Position will be done from the parent's dimensions
19    /// and other siblings the same type.
20    Floating,
21}
22
23/// How floating children `Bin` are placed.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum ChildFloatMode {
26    #[default]
27    Row,
28    Column,
29}
30
31/// Text wrap method used
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum TextWrap {
34    Shift,
35    #[default]
36    Normal,
37    None,
38}
39
40/// Text horizonal alignment
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum TextHoriAlign {
43    #[default]
44    Left,
45    Center,
46    Right,
47}
48
49/// Text vertical alignment
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum TextVertAlign {
52    #[default]
53    Top,
54    Center,
55    Bottom,
56}
57
58/// Weight of a font
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum FontWeight {
61    Thin,
62    ExtraLight,
63    Light,
64    #[default]
65    Normal,
66    Medium,
67    Semibold,
68    Bold,
69    Extrabold,
70    Black,
71}
72
73impl From<FontWeight> for cosmic_text::Weight {
74    fn from(weight: FontWeight) -> Self {
75        Self(match weight {
76            FontWeight::Thin => 100,
77            FontWeight::ExtraLight => 200,
78            FontWeight::Light => 300,
79            FontWeight::Normal => 400,
80            FontWeight::Medium => 500,
81            FontWeight::Semibold => 600,
82            FontWeight::Bold => 700,
83            FontWeight::Extrabold => 800,
84            FontWeight::Black => 900,
85        })
86    }
87}
88
89/// Stretch of a font
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum FontStretch {
92    UltraCondensed,
93    ExtraCondensed,
94    Condensed,
95    SemiCondensed,
96    #[default]
97    Normal,
98    SemiExpanded,
99    Expanded,
100    ExtraExpanded,
101    UltraExpanded,
102}
103
104impl From<FontStretch> for cosmic_text::Stretch {
105    fn from(stretch: FontStretch) -> Self {
106        match stretch {
107            FontStretch::UltraCondensed => Self::UltraCondensed,
108            FontStretch::ExtraCondensed => Self::ExtraCondensed,
109            FontStretch::Condensed => Self::Condensed,
110            FontStretch::SemiCondensed => Self::SemiCondensed,
111            FontStretch::Normal => Self::Normal,
112            FontStretch::SemiExpanded => Self::SemiExpanded,
113            FontStretch::Expanded => Self::Expanded,
114            FontStretch::ExtraExpanded => Self::ExtraExpanded,
115            FontStretch::UltraExpanded => Self::UltraExpanded,
116        }
117    }
118}
119
120/// Style of a font
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum FontStyle {
123    #[default]
124    Normal,
125    Italic,
126    Oblique,
127}
128
129impl From<FontStyle> for cosmic_text::Style {
130    fn from(style: FontStyle) -> Self {
131        match style {
132            FontStyle::Normal => Self::Normal,
133            FontStyle::Italic => Self::Italic,
134            FontStyle::Oblique => Self::Oblique,
135        }
136    }
137}
138
139/// Style of a `Bin`
140#[derive(Clone)]
141pub struct BinStyle {
142    /// Determines the positioning type
143    pub position: Option<BinPosition>,
144    /// Overrides the z-index automatically calculated.
145    pub z_index: Option<i16>,
146    /// Offsets the z-index automatically calculated.
147    pub add_z_index: Option<i16>,
148    /// How children of this `Bin` float.
149    pub child_float_mode: Option<ChildFloatMode>,
150    /// The floating weight of this `Bin`.
151    ///
152    /// Lesser values will be left-most and greator values right-most in `ChildFloatMode::Row`.
153    /// Likewise with `ChildFloatMode::Column` lesser is top-most and greator is bottom-most.
154    ///
155    /// ***Note:** When setting the weight explicitly, all other silbings's weights should be set
156    /// to ensure that they are displayed as intended.*
157    pub float_weight: Option<i16>,
158    /// Determines if the `Bin` is hidden.
159    /// - `None`: Inherited from the parent `Bin`.
160    /// - `Some(true)`: Always hidden.
161    /// - `Some(false)`: Always visible even when the parent is hidden.
162    pub hidden: Option<bool>,
163    /// Set the opacity of the bin's content.
164    pub opacity: Option<f32>,
165    // Position from Edges
166    pub pos_from_t: Option<f32>,
167    pub pos_from_b: Option<f32>,
168    pub pos_from_l: Option<f32>,
169    pub pos_from_r: Option<f32>,
170    pub pos_from_t_pct: Option<f32>,
171    pub pos_from_b_pct: Option<f32>,
172    pub pos_from_l_pct: Option<f32>,
173    pub pos_from_r_pct: Option<f32>,
174    pub pos_from_l_offset: Option<f32>,
175    pub pos_from_t_offset: Option<f32>,
176    pub pos_from_r_offset: Option<f32>,
177    pub pos_from_b_offset: Option<f32>,
178    // Size
179    pub width: Option<f32>,
180    pub width_pct: Option<f32>,
181    /// Used in conjunction with `width_pct` to provide additional flexibility
182    pub width_offset: Option<f32>,
183    pub height: Option<f32>,
184    pub height_pct: Option<f32>,
185    /// Used in conjunction with `height_pct` to provide additional flexibility
186    pub height_offset: Option<f32>,
187    pub margin_t: Option<f32>,
188    pub margin_b: Option<f32>,
189    pub margin_l: Option<f32>,
190    pub margin_r: Option<f32>,
191    // Padding
192    pub pad_t: Option<f32>,
193    pub pad_b: Option<f32>,
194    pub pad_l: Option<f32>,
195    pub pad_r: Option<f32>,
196    // Scrolling
197    pub scroll_y: Option<f32>,
198    pub scroll_x: Option<f32>,
199    pub overflow_y: Option<bool>,
200    pub overflow_x: Option<bool>,
201    // Border
202    pub border_size_t: Option<f32>,
203    pub border_size_b: Option<f32>,
204    pub border_size_l: Option<f32>,
205    pub border_size_r: Option<f32>,
206    pub border_color_t: Option<Color>,
207    pub border_color_b: Option<Color>,
208    pub border_color_l: Option<Color>,
209    pub border_color_r: Option<Color>,
210    pub border_radius_tl: Option<f32>,
211    pub border_radius_tr: Option<f32>,
212    pub border_radius_bl: Option<f32>,
213    pub border_radius_br: Option<f32>,
214    // Background
215    pub back_color: Option<Color>,
216    pub back_image: Option<ImageCacheKey>,
217    pub back_image_vk: Option<Arc<Image>>,
218    pub back_image_coords: Option<[f32; 4]>,
219    pub back_image_effect: Option<ImageEffect>,
220    // Text
221    pub text: String,
222    pub text_color: Option<Color>,
223    pub text_height: Option<f32>,
224    pub text_secret: Option<bool>,
225    pub line_spacing: Option<f32>,
226    pub line_limit: Option<usize>,
227    pub text_wrap: Option<TextWrap>,
228    pub text_vert_align: Option<TextVertAlign>,
229    pub text_hori_align: Option<TextHoriAlign>,
230    pub font_family: Option<String>,
231    pub font_weight: Option<FontWeight>,
232    pub font_stretch: Option<FontStretch>,
233    pub font_style: Option<FontStyle>,
234    // Misc
235    pub custom_verts: Vec<BinVert>,
236    pub _ne: NonExhaustive,
237}
238
239impl Default for BinStyle {
240    fn default() -> Self {
241        Self {
242            position: None,
243            z_index: None,
244            add_z_index: None,
245            child_float_mode: None,
246            float_weight: None,
247            hidden: None,
248            opacity: None,
249            pos_from_t: None,
250            pos_from_b: None,
251            pos_from_l: None,
252            pos_from_r: None,
253            pos_from_t_pct: None,
254            pos_from_b_pct: None,
255            pos_from_l_pct: None,
256            pos_from_r_pct: None,
257            pos_from_l_offset: None,
258            pos_from_t_offset: None,
259            pos_from_r_offset: None,
260            pos_from_b_offset: None,
261            width: None,
262            width_pct: None,
263            width_offset: None,
264            height: None,
265            height_pct: None,
266            height_offset: None,
267            margin_t: None,
268            margin_b: None,
269            margin_l: None,
270            margin_r: None,
271            pad_t: None,
272            pad_b: None,
273            pad_l: None,
274            pad_r: None,
275            scroll_y: None,
276            scroll_x: None,
277            overflow_y: None,
278            overflow_x: None,
279            border_size_t: None,
280            border_size_b: None,
281            border_size_l: None,
282            border_size_r: None,
283            border_color_t: None,
284            border_color_b: None,
285            border_color_l: None,
286            border_color_r: None,
287            border_radius_tl: None,
288            border_radius_tr: None,
289            border_radius_bl: None,
290            border_radius_br: None,
291            back_color: None,
292            back_image: None,
293            back_image_vk: None,
294            back_image_coords: None,
295            back_image_effect: None,
296            text: String::new(),
297            text_color: None,
298            text_height: None,
299            text_secret: None,
300            line_spacing: None,
301            line_limit: None,
302            text_wrap: None,
303            text_vert_align: None,
304            text_hori_align: None,
305            font_family: None,
306            font_weight: None,
307            font_stretch: None,
308            font_style: None,
309            custom_verts: Vec::new(),
310            _ne: NonExhaustive(()),
311        }
312    }
313}
314
315/// Error produced from an invalid style
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct BinStyleError {
318    pub ty: BinStyleErrorType,
319    pub desc: String,
320}
321
322impl std::fmt::Display for BinStyleError {
323    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
324        write!(f, "{}: {}", self.ty, self.desc)
325    }
326}
327
328/// Type of error produced from an invalid style
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330#[non_exhaustive]
331pub enum BinStyleErrorType {
332    /// Two fields are conflicted only one must be set.
333    ConflictingFields,
334    /// Too many fields are defining an attribute.
335    TooManyConstraints,
336    /// Not enough fields are defining an attribute.
337    NotEnoughConstraints,
338    /// Provided Image isn't valid.
339    InvalidImage,
340}
341
342impl std::fmt::Display for BinStyleErrorType {
343    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
344        match self {
345            Self::ConflictingFields => write!(f, "Conflicting Fields"),
346            Self::TooManyConstraints => write!(f, "Too Many Constraints"),
347            Self::NotEnoughConstraints => write!(f, "Not Enough Constraints"),
348            _ => write!(f, "Unknown"),
349        }
350    }
351}
352
353/// Warning produced for a suboptimal style
354#[derive(Debug, Clone, PartialEq, Eq)]
355pub struct BinStyleWarn {
356    pub ty: BinStyleWarnType,
357    pub desc: String,
358}
359
360impl std::fmt::Display for BinStyleWarn {
361    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
362        write!(f, "{}: {}", self.ty, self.desc)
363    }
364}
365
366/// Type of warning produced for a suboptimal style
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368#[non_exhaustive]
369pub enum BinStyleWarnType {
370    /// Field is set, but isn't used or incompatible with other styles.
371    UselessField,
372}
373
374impl std::fmt::Display for BinStyleWarnType {
375    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
376        match self {
377            Self::UselessField => write!(f, "Useless Field"),
378        }
379    }
380}
381
382/// Validation result produced from updating a `BinStyle`
383///
384/// To remove the `#[must_use]` attribute, enable the `style_validation_debug_on_drop` feature.
385/// This feature will call the `debug` method automatically if no other method was used.
386#[cfg_attr(not(feature = "style_validation_debug_on_drop"), must_use)]
387pub struct BinStyleValidation {
388    errors: Vec<BinStyleError>,
389    warnings: Vec<BinStyleWarn>,
390    location: Option<String>,
391    used: bool,
392}
393
394impl BinStyleValidation {
395    fn new() -> Self {
396        Self {
397            errors: Vec::new(),
398            warnings: Vec::new(),
399            location: None,
400            used: false,
401        }
402    }
403
404    #[track_caller]
405    fn error<D: Into<String>>(&mut self, ty: BinStyleErrorType, desc: D) {
406        self.errors.push(BinStyleError {
407            ty,
408            desc: desc.into(),
409        });
410
411        if self.location.is_none() {
412            self.location = Some(format!("{}", std::panic::Location::caller()));
413        }
414    }
415
416    #[track_caller]
417    fn warning<D: Into<String>>(&mut self, ty: BinStyleWarnType, desc: D) {
418        self.warnings.push(BinStyleWarn {
419            ty,
420            desc: desc.into(),
421        });
422
423        if self.location.is_none() {
424            self.location = Some(format!("{}", std::panic::Location::caller()));
425        }
426    }
427
428    /// Expect `BinStyle` provided to `style_update()` is valid panicking if that is not the case.
429    pub fn expect_valid(mut self) {
430        self.used = true;
431
432        if !self.errors.is_empty() {
433            let mut panic_msg = format!(
434                "BinStyleValidation-Error {{ caller: {},",
435                self.location.take().unwrap()
436            );
437            let error_count = self.errors.len();
438
439            if error_count == 1 {
440                panic_msg = format!(
441                    "{} error: Error {{ ty: {}, desc: {} }} }}",
442                    panic_msg, self.errors[0].ty, self.errors[0].desc
443                );
444            } else {
445                for (i, error) in self.errors.iter().enumerate() {
446                    if i == 0 {
447                        panic_msg = format!(
448                            "{} errors: [ Error {{ ty: {}, desc: {} }},",
449                            panic_msg, error.ty, error.desc
450                        );
451                    } else if i == error_count - 1 {
452                        panic_msg = format!(
453                            "{} Error {{ ty: {}, desc: {} }} ] }}",
454                            panic_msg, error.ty, error.desc
455                        );
456                    } else {
457                        panic_msg = format!(
458                            "{} Error {{ ty: {}, desc: {} }},",
459                            panic_msg, error.ty, error.desc
460                        );
461                    }
462                }
463            }
464
465            panic!("{}", panic_msg);
466        }
467    }
468
469    /// Does the same thing as `expect_valid`, but in the case of no errors, it'll print pretty warnings to the terminal.
470    pub fn expect_valid_debug_warn(mut self) {
471        self.used = true;
472
473        if self.errors.is_empty() {
474            if !self.warnings.is_empty() {
475                let mut msg = format!(
476                    "BinStyleValidation-Warn: {}:\n",
477                    self.location.take().unwrap()
478                );
479
480                for warning in &self.warnings {
481                    msg = format!("{}  {}: {}\n", msg, warning.ty, warning.desc);
482                }
483
484                msg.pop();
485                println!("{}", msg);
486            }
487        } else {
488            self.expect_valid();
489        }
490    }
491
492    /// Returns `true` if errors are present.
493    pub fn errors_present(&self) -> bool {
494        !self.errors.is_empty()
495    }
496
497    /// Return an `Iterator` of `BinStyleError`
498    ///
499    /// ***Note:** This method should only be called once. As it move the errors out.*
500    pub fn errors(&mut self) -> impl Iterator<Item = BinStyleError> {
501        self.used = true;
502        self.errors.split_off(0).into_iter()
503    }
504
505    /// Returns `true` if warnings are present.
506    pub fn warnings_present(&self) -> bool {
507        !self.warnings.is_empty()
508    }
509
510    /// Return an `Iterator` of `BinStyleWarn`
511    ///
512    /// ***Note:** This method should only be called once. As it move the warnings out.*
513    pub fn warnings(&mut self) -> impl Iterator<Item = BinStyleWarn> {
514        self.used = true;
515        self.warnings.split_off(0).into_iter()
516    }
517
518    /// Acknowlege the style update may have not be successful and just print pretty errors/warnings to the terminal.
519    pub fn debug(mut self) {
520        self.debug_impl();
521    }
522
523    fn debug_impl(&mut self) {
524        if self.used {
525            return;
526        }
527
528        self.used = true;
529
530        if !self.errors.is_empty() || !self.warnings.is_empty() {
531            let mut msg = format!("BinStyleValidation: {}:\n", self.location.take().unwrap());
532
533            for error in &self.errors {
534                msg = format!("{}  Error {}: {}\n", msg, error.ty, error.desc);
535            }
536
537            for warning in &self.warnings {
538                msg = format!("{}  Warning {}: {}\n", msg, warning.ty, warning.desc);
539            }
540
541            msg.pop();
542            println!("{}", msg);
543        }
544    }
545}
546
547#[cfg(feature = "style_validation_debug_on_drop")]
548impl Drop for BinStyleValidation {
549    fn drop(&mut self) {
550        self.debug_impl();
551    }
552}
553
554macro_rules! useless_field {
555    ($style:ident, $field:ident, $name:literal, $validation:ident) => {
556        if $style.$field.is_some() {
557            $validation.warning(
558                BinStyleWarnType::UselessField,
559                format!("'{}' is defined, but is ignored.", $name),
560            );
561        }
562    };
563}
564
565impl BinStyle {
566    #[track_caller]
567    pub(crate) fn validate(&self, bin: &Arc<Bin>) -> BinStyleValidation {
568        let mut validation = BinStyleValidation::new();
569        let has_parent = bin.hrchy.load().parent.is_some();
570
571        match self.position.unwrap_or(BinPosition::Window) {
572            BinPosition::Window | BinPosition::Parent => {
573                useless_field!(self, float_weight, "float_weight", validation);
574
575                if self.pos_from_t.is_some() && self.pos_from_t_pct.is_some() {
576                    validation.error(
577                        BinStyleErrorType::ConflictingFields,
578                        "Both 'pos_from_t' and 'pos_from_t_pct' are set.",
579                    );
580                }
581
582                if self.pos_from_b.is_some() && self.pos_from_b_pct.is_some() {
583                    validation.error(
584                        BinStyleErrorType::ConflictingFields,
585                        "Both 'pos_from_b' and 'pos_from_b_pct' are set.",
586                    );
587                }
588
589                if self.pos_from_l.is_some() && self.pos_from_l_pct.is_some() {
590                    validation.error(
591                        BinStyleErrorType::ConflictingFields,
592                        "Both 'pos_from_l' and 'pos_from_l_pct' are set.",
593                    );
594                }
595
596                if self.pos_from_r.is_some() && self.pos_from_r_pct.is_some() {
597                    validation.error(
598                        BinStyleErrorType::ConflictingFields,
599                        "Both 'pos_from_r' and 'pos_from_r_pct' are set.",
600                    );
601                }
602
603                if self.width.is_some() && self.width_pct.is_some() {
604                    validation.error(
605                        BinStyleErrorType::ConflictingFields,
606                        "Both 'width' and 'width_pct' are set.",
607                    );
608                }
609
610                if self.height.is_some() && self.height_pct.is_some() {
611                    validation.error(
612                        BinStyleErrorType::ConflictingFields,
613                        "Both 'height' and 'height_pct' are set.",
614                    );
615                }
616
617                if validation.errors.is_empty() {
618                    let pft = self.pos_from_t.is_some() || self.pos_from_t_pct.is_some();
619                    let pfb = self.pos_from_b.is_some() || self.pos_from_b_pct.is_some();
620                    let pfl = self.pos_from_l.is_some() || self.pos_from_l_pct.is_some();
621                    let pfr = self.pos_from_r.is_some() || self.pos_from_r_pct.is_some();
622                    let width = self.width.is_some() || self.width_pct.is_some();
623                    let height = self.height.is_some() || self.height_pct.is_some();
624
625                    match (pft, pfb, height) {
626                        (true, true, true) => {
627                            let pft_field = if self.pos_from_t.is_some() {
628                                "pos_from_t"
629                            } else {
630                                "pos_from_t_pct"
631                            };
632
633                            let pfb_field = if self.pos_from_b.is_some() {
634                                "pos_from_b"
635                            } else {
636                                "pos_from_b_pct"
637                            };
638
639                            let height_field = if self.height.is_some() {
640                                "height"
641                            } else {
642                                "height_pct"
643                            };
644
645                            validation.error(
646                                BinStyleErrorType::TooManyConstraints,
647                                format!(
648                                    "'{}', '{}' & '{}' are all defined. Only two can be defined.",
649                                    pft_field, pfb_field, height_field,
650                                ),
651                            );
652                        },
653                        (true, false, false) => {
654                            let pft_field = if self.pos_from_t.is_some() {
655                                "pos_from_t"
656                            } else {
657                                "pos_from_t_pct"
658                            };
659
660                            validation.error(
661                                BinStyleErrorType::NotEnoughConstraints,
662                                format!(
663                                    "'{}' is defined, but one of `pos_from_b`, `pos_from_b_pct`, \
664                                     `height` or `height_pct` must also be defined.",
665                                    pft_field,
666                                ),
667                            );
668                        },
669                        (false, true, false) => {
670                            let pfb_field = if self.pos_from_b.is_some() {
671                                "pos_from_b"
672                            } else {
673                                "pos_from_b_pct"
674                            };
675
676                            validation.error(
677                                BinStyleErrorType::NotEnoughConstraints,
678                                format!(
679                                    "'{}' is defined, but one of `pos_from_t`, `pos_from_t_pct`, \
680                                     `height` or `height_pct` must also be defined.",
681                                    pfb_field,
682                                ),
683                            );
684                        },
685                        (false, false, true) => {
686                            let height_field = if self.height.is_some() {
687                                "height"
688                            } else {
689                                "height_pct"
690                            };
691
692                            validation.error(
693                                BinStyleErrorType::NotEnoughConstraints,
694                                format!(
695                                    "'{}' is defined, but one of `pos_from_t`, `pos_from_t_pct`, \
696                                     `pos_from_b` or `pos_from_b_pct` must also be defined.",
697                                    height_field,
698                                ),
699                            );
700                        },
701                        _ => (),
702                    }
703
704                    match (pfl, pfr, width) {
705                        (true, true, true) => {
706                            let pfl_field = if self.pos_from_l.is_some() {
707                                "pos_from_l"
708                            } else {
709                                "pos_from_l_pct"
710                            };
711
712                            let pfr_field = if self.pos_from_r.is_some() {
713                                "pos_from_r"
714                            } else {
715                                "pos_from_r_pct"
716                            };
717
718                            let width_field = if self.width.is_some() {
719                                "width"
720                            } else {
721                                "width_pct"
722                            };
723
724                            validation.error(
725                                BinStyleErrorType::TooManyConstraints,
726                                format!(
727                                    "'{}', '{}' & '{}' are all defined. Only two can be defined.",
728                                    pfl_field, pfr_field, width_field,
729                                ),
730                            );
731                        },
732                        (true, false, false) => {
733                            let pfl_field = if self.pos_from_t.is_some() {
734                                "pos_from_l"
735                            } else {
736                                "pos_from_l_pct"
737                            };
738
739                            validation.error(
740                                BinStyleErrorType::NotEnoughConstraints,
741                                format!(
742                                    "'{}' is defined, but one of `pos_from_r`, `pos_from_r_pct`, \
743                                     `width` or `width_pct` must also be defined.",
744                                    pfl_field,
745                                ),
746                            );
747                        },
748                        (false, true, false) => {
749                            let pfr_field = if self.pos_from_t.is_some() {
750                                "pos_from_r"
751                            } else {
752                                "pos_from_r_pct"
753                            };
754
755                            validation.error(
756                                BinStyleErrorType::NotEnoughConstraints,
757                                format!(
758                                    "'{}' is defined, but one of `pos_from_l`, `pos_from_l_pct`, \
759                                     `width` or `width_pct` must also be defined.",
760                                    pfr_field,
761                                ),
762                            );
763                        },
764                        (false, false, true) => {
765                            let width_field = if self.pos_from_t.is_some() {
766                                "width"
767                            } else {
768                                "width_pct"
769                            };
770
771                            validation.error(
772                                BinStyleErrorType::NotEnoughConstraints,
773                                format!(
774                                    "'{}' is defined, but one of `pos_from_l`, `pos_from_l_pct`, \
775                                     `pos_from_r` or `pos_from_r_pct` must also be defined.",
776                                    width_field,
777                                ),
778                            );
779                        },
780                        _ => (),
781                    }
782                }
783            },
784            BinPosition::Floating => {
785                useless_field!(self, pos_from_t, "pos_from_t", validation);
786                useless_field!(self, pos_from_b, "pos_from_b", validation);
787                useless_field!(self, pos_from_l, "pos_from_l", validation);
788                useless_field!(self, pos_from_r, "pos_from_r", validation);
789                useless_field!(self, pos_from_t_pct, "pos_from_t_pct", validation);
790                useless_field!(self, pos_from_b_pct, "pos_from_b_pct", validation);
791                useless_field!(self, pos_from_l_pct, "pos_from_l_pct", validation);
792                useless_field!(self, pos_from_r_pct, "pos_from_r_pct", validation);
793                useless_field!(self, pos_from_t_offset, "pos_from_t_offset", validation);
794                useless_field!(self, pos_from_t_offset, "pos_from_b_offset", validation);
795                useless_field!(self, pos_from_t_offset, "pos_from_l_offset", validation);
796                useless_field!(self, pos_from_t_offset, "pos_from_r_offset", validation);
797
798                if !has_parent {
799                    validation.error(
800                        BinStyleErrorType::NotEnoughConstraints,
801                        "Floating Bin's must have a parent.",
802                    );
803                }
804
805                if self.width.is_none() && self.width_pct.is_none() {
806                    validation.error(
807                        BinStyleErrorType::NotEnoughConstraints,
808                        "'width' or 'width_pct' must be defined.",
809                    );
810                }
811
812                if self.height.is_none() && self.height_pct.is_none() {
813                    validation.error(
814                        BinStyleErrorType::NotEnoughConstraints,
815                        "'height' or 'height_pct' must be defined.",
816                    );
817                }
818            },
819        }
820
821        if self.back_image.is_some() && self.back_image_vk.is_some() {
822            validation.error(
823                BinStyleErrorType::ConflictingFields,
824                "Both 'back_image' and 'back_image_vk' are set.",
825            );
826        }
827
828        if let Some(back_image_vk) = self.back_image_vk.as_ref() {
829            if back_image_vk.image_type() != ImageType::Dim2d {
830                validation.error(
831                    BinStyleErrorType::InvalidImage,
832                    "Image provided with 'back_image_vk' isn't a 2d.",
833                );
834            }
835
836            if back_image_vk.array_layers() != 1 {
837                validation.error(
838                    BinStyleErrorType::InvalidImage,
839                    "Image provided with 'back_image_vk' must not have array layers.",
840                );
841            }
842
843            if back_image_vk.mip_levels() != 1 {
844                validation.error(
845                    BinStyleErrorType::InvalidImage,
846                    "Image provided with 'back_image_vk' must not have multiple mip levels.",
847                );
848            }
849
850            if !back_image_vk.format_features().contains(
851                FormatFeatures::TRANSFER_DST
852                    | FormatFeatures::TRANSFER_SRC
853                    | FormatFeatures::SAMPLED_IMAGE
854                    | FormatFeatures::SAMPLED_IMAGE_FILTER_LINEAR,
855            ) {
856                validation.error(
857                    BinStyleErrorType::InvalidImage,
858                    "Image provided with 'back_image_vk' must have a format that supports, \
859                     'TRANSFER_DST`, `TRANSFER_SRC`, `SAMPLED_IMAGE`, & \
860                     `SAMPLED_IMAGE_FILTER_LINEAR`.",
861                );
862            }
863        }
864
865        if let Some(image_cache_key) = self.back_image.as_ref() {
866            if matches!(image_cache_key, ImageCacheKey::Glyph(..)) {
867                validation.error(
868                    BinStyleErrorType::InvalidImage,
869                    "'ImageCacheKey' provided with 'back_image' must not be \
870                     'ImageCacheKey::Glyph'. 'ImageCacheKey::User' should be used instead.",
871                );
872            }
873
874            if matches!(image_cache_key, ImageCacheKey::User(..))
875                && bin
876                    .basalt
877                    .image_cache_ref()
878                    .obtain_image_info(image_cache_key.clone())
879                    .is_none()
880            {
881                validation.error(
882                    BinStyleErrorType::InvalidImage,
883                    "'ImageCacheKey::User' provided with 'back_image' must be preloaded into the \
884                     `ImageCache`.",
885                );
886            }
887        }
888
889        validation
890    }
891}
892
893/// Effect used on the background image of a `Bin`
894#[derive(Clone, Copy, Debug, PartialEq, Eq)]
895pub enum ImageEffect {
896    BackColorAdd,
897    BackColorBehind,
898    BackColorSubtract,
899    BackColorMultiply,
900    BackColorDivide,
901    GlyphWithColor,
902    Invert,
903}
904
905impl ImageEffect {
906    pub(crate) fn vert_type(&self) -> i32 {
907        match *self {
908            ImageEffect::BackColorAdd => 102,
909            ImageEffect::BackColorBehind => 103,
910            ImageEffect::BackColorSubtract => 104,
911            ImageEffect::BackColorMultiply => 105,
912            ImageEffect::BackColorDivide => 106,
913            ImageEffect::Invert => 107,
914            ImageEffect::GlyphWithColor => 108,
915        }
916    }
917}
918
919/// Custom vertex for `Bin`
920///
921/// Used for `BinStyle.custom_verts`
922#[derive(Default, Clone, Debug, PartialEq)]
923pub struct BinVert {
924    pub position: (f32, f32, i16),
925    pub color: Color,
926}