/// Responsive — a value that varies by breakpoint. /// /// The base value is always required (mobile-first). `sm`, `md`, `lg`, `xl` /// are all optional — if not set, the nearest smaller value is used. /// /// You generally don't need Responsive for layout — VStack and Grid handle /// reflow automatically. Use Responsive when you need to vary a non-layout /// value (e.g. font size, column count, visibility) at specific breakpoints. use crate::breakpoint::Breakpoint; /// A value that varies by viewport breakpoint. /// /// Follows mobile-first cascade: base < sm < md < lg < xl. /// If a breakpoint value is not set, it inherits from the next smaller one. #[derive(Debug, Clone, PartialEq)] pub struct Responsive { /// Mobile-first base value. Always required. pub base: T, /// 640dp+. If None, uses `base`. pub sm: Option, /// 768dp+. If None, uses `sm` or `base`. pub md: Option, /// 1024dp+. If None, uses `md`, `sm`, or `base`. pub lg: Option, /// 1280dp+. If None, uses `lg`, `md`, `sm`, or `base`. pub xl: Option, } impl Responsive { /// Create a responsive value with only the base (same on all screen sizes). pub fn fixed(value: T) -> Self { Self { base: value, sm: None, md: None, lg: None, xl: None, } } /// Create a fully-specified responsive value. pub fn new( base: T, sm: Option, md: Option, lg: Option, xl: Option, ) -> Self { Self { base, sm, md, lg, xl } } /// Resolve to the most-specific value that applies at the given breakpoint. /// /// Cascades downward: xl → lg → md → sm → base. pub fn resolve(&self, breakpoint: Breakpoint) -> &T { match breakpoint { Breakpoint::Xl => { self.xl.as_ref() .or(self.lg.as_ref()) .or(self.md.as_ref()) .or(self.sm.as_ref()) .unwrap_or(&self.base) } Breakpoint::Lg => { self.lg.as_ref() .or(self.md.as_ref()) .or(self.sm.as_ref()) .unwrap_or(&self.base) } Breakpoint::Md => { self.md.as_ref() .or(self.sm.as_ref()) .unwrap_or(&self.base) } Breakpoint::Sm => { self.sm.as_ref().unwrap_or(&self.base) } Breakpoint::Base => &self.base, } } /// Set the sm value (builder pattern). pub fn sm(mut self, value: T) -> Self { self.sm = Some(value); self } /// Set the md value. pub fn md(mut self, value: T) -> Self { self.md = Some(value); self } /// Set the lg value. pub fn lg(mut self, value: T) -> Self { self.lg = Some(value); self } /// Set the xl value. pub fn xl(mut self, value: T) -> Self { self.xl = Some(value); self } } #[cfg(test)] mod tests { use super::*; #[test] fn fixed_always_returns_base() { let r = Responsive::fixed(42u32); assert_eq!(*r.resolve(Breakpoint::Base), 42); assert_eq!(*r.resolve(Breakpoint::Xl), 42); } #[test] fn resolves_most_specific() { let r = Responsive::fixed(1u32).sm(2).md(3).lg(4).xl(5); assert_eq!(*r.resolve(Breakpoint::Base), 1); assert_eq!(*r.resolve(Breakpoint::Sm), 2); assert_eq!(*r.resolve(Breakpoint::Md), 3); assert_eq!(*r.resolve(Breakpoint::Lg), 4); assert_eq!(*r.resolve(Breakpoint::Xl), 5); } #[test] fn cascades_down_when_specific_missing() { let r = Responsive::fixed(1u32).md(3); // sm not set → falls back to base assert_eq!(*r.resolve(Breakpoint::Sm), 1); // lg not set → falls back to md assert_eq!(*r.resolve(Breakpoint::Lg), 3); // xl not set → falls back to md (lg not set either) assert_eq!(*r.resolve(Breakpoint::Xl), 3); } #[test] fn cascade_xl_to_lg_to_sm_to_base() { let r = Responsive::fixed("base").sm("sm"); assert_eq!(*r.resolve(Breakpoint::Md), "sm"); assert_eq!(*r.resolve(Breakpoint::Lg), "sm"); assert_eq!(*r.resolve(Breakpoint::Xl), "sm"); } #[test] fn responsive_with_strings() { let r = Responsive::fixed("mobile").lg("desktop"); assert_eq!(*r.resolve(Breakpoint::Base), "mobile"); assert_eq!(*r.resolve(Breakpoint::Lg), "desktop"); } }