155 lines
4.6 KiB
Rust
155 lines
4.6 KiB
Rust
/// Responsive<T> — 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<T> for layout — VStack and Grid handle
|
|
/// reflow automatically. Use Responsive<T> 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<T: Clone> {
|
|
/// Mobile-first base value. Always required.
|
|
pub base: T,
|
|
/// 640dp+. If None, uses `base`.
|
|
pub sm: Option<T>,
|
|
/// 768dp+. If None, uses `sm` or `base`.
|
|
pub md: Option<T>,
|
|
/// 1024dp+. If None, uses `md`, `sm`, or `base`.
|
|
pub lg: Option<T>,
|
|
/// 1280dp+. If None, uses `lg`, `md`, `sm`, or `base`.
|
|
pub xl: Option<T>,
|
|
}
|
|
|
|
impl<T: Clone> Responsive<T> {
|
|
/// 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<T>,
|
|
md: Option<T>,
|
|
lg: Option<T>,
|
|
xl: Option<T>,
|
|
) -> 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");
|
|
}
|
|
}
|