raui_core/widget/component/containers/
responsive_box.rs1use crate::{
2 PropsData, Scalar, pre_hooks, unpack_named_slots,
3 view_model::{ViewModelProperties, ViewModelValue},
4 widget::{
5 component::{ResizeListenerSignal, use_resize_listener},
6 context::WidgetContext,
7 node::WidgetNode,
8 unit::area::AreaBoxNode,
9 utils::Vec2,
10 },
11};
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14
15pub struct MediaQueryContext<'a> {
16 pub widget_width: Scalar,
17 pub widget_height: Scalar,
18 pub view_model: Option<&'a MediaQueryViewModel>,
19}
20
21#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
22pub enum MediaQueryOrientation {
23 #[default]
24 Portrait,
25 Landscape,
26}
27
28#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
29pub enum MediaQueryNumber {
30 Exact(Scalar),
31 Min(Scalar),
32 Max(Scalar),
33 Range { min: Scalar, max: Scalar },
34}
35
36impl Default for MediaQueryNumber {
37 fn default() -> Self {
38 Self::Exact(0.0)
39 }
40}
41
42impl MediaQueryNumber {
43 pub fn is_valid(&self, value: Scalar) -> bool {
44 match self {
45 Self::Exact(v) => (*v - value).abs() < Scalar::EPSILON,
46 Self::Min(v) => *v <= value,
47 Self::Max(v) => *v >= value,
48 Self::Range { min, max } => *min <= value && *max >= value,
49 }
50 }
51}
52
53#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
54#[props_data(crate::props::PropsData)]
55#[prefab(crate::Prefab)]
56pub enum MediaQueryExpression {
57 #[default]
58 Any,
59 And(Vec<Self>),
60 Or(Vec<Self>),
61 Not(Box<Self>),
62 WidgetOrientation(MediaQueryOrientation),
63 WidgetAspectRatio(MediaQueryNumber),
64 WidgetWidth(MediaQueryNumber),
65 WidgetHeight(MediaQueryNumber),
66 ScreenOrientation(MediaQueryOrientation),
67 ScreenAspectRatio(MediaQueryNumber),
68 ScreenWidth(MediaQueryNumber),
69 ScreenHeight(MediaQueryNumber),
70 HasFlag(String),
71 HasNumber(String),
72 Number(String, MediaQueryNumber),
73}
74
75impl MediaQueryExpression {
76 pub fn is_valid(&self, context: &MediaQueryContext) -> bool {
77 match self {
78 Self::Any => true,
79 Self::And(conditions) => conditions
80 .iter()
81 .all(|condition| condition.is_valid(context)),
82 Self::Or(conditions) => conditions
83 .iter()
84 .any(|condition| condition.is_valid(context)),
85 Self::Not(condition) => !condition.is_valid(context),
86 Self::WidgetOrientation(orientation) => {
87 let is_portrait = context.widget_height > context.widget_width;
88 match orientation {
89 MediaQueryOrientation::Portrait => is_portrait,
90 MediaQueryOrientation::Landscape => !is_portrait,
91 }
92 }
93 Self::WidgetAspectRatio(aspect_ratio) => {
94 let ratio = context.widget_width / context.widget_height;
95 aspect_ratio.is_valid(ratio)
96 }
97 Self::WidgetWidth(width) => width.is_valid(context.widget_width),
98 Self::WidgetHeight(height) => height.is_valid(context.widget_height),
99 Self::ScreenOrientation(orientation) => context
100 .view_model
101 .map(|view_model| {
102 let is_portrait = view_model.screen_size.y > view_model.screen_size.x;
103 match orientation {
104 MediaQueryOrientation::Portrait => is_portrait,
105 MediaQueryOrientation::Landscape => !is_portrait,
106 }
107 })
108 .unwrap_or_default(),
109 Self::ScreenAspectRatio(aspect_ratio) => context
110 .view_model
111 .map(|view_model| {
112 let ratio = view_model.screen_size.x / view_model.screen_size.y;
113 aspect_ratio.is_valid(ratio)
114 })
115 .unwrap_or_default(),
116 Self::ScreenWidth(width) => context
117 .view_model
118 .map(|view_model| width.is_valid(view_model.screen_size.x))
119 .unwrap_or_default(),
120 Self::ScreenHeight(height) => context
121 .view_model
122 .map(|view_model| height.is_valid(view_model.screen_size.y))
123 .unwrap_or_default(),
124 Self::HasFlag(flag) => context
125 .view_model
126 .map(|view_model| view_model.flags.contains(flag))
127 .unwrap_or_default(),
128 Self::HasNumber(name) => context
129 .view_model
130 .map(|view_model| view_model.numbers.contains_key(name))
131 .unwrap_or_default(),
132 Self::Number(name, number) => context
133 .view_model
134 .map(|view_model| {
135 view_model
136 .numbers
137 .get(name)
138 .map(|value| number.is_valid(*value))
139 .unwrap_or_default()
140 })
141 .unwrap_or_default(),
142 }
143 }
144}
145
146#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
147#[props_data(crate::props::PropsData)]
148#[prefab(crate::Prefab)]
149pub struct ResponsiveBoxState {
150 pub size: Vec2,
151}
152
153pub fn use_responsive_box(context: &mut WidgetContext) {
154 context.life_cycle.mount(|mut context| {
155 if let Some(mut bindings) = context.view_models.bindings(
156 MediaQueryViewModel::VIEW_MODEL,
157 MediaQueryViewModel::NOTIFIER,
158 ) {
159 bindings.bind(context.id.to_owned());
160 }
161 });
162
163 context.life_cycle.unmount(|mut context| {
164 if let Some(mut bindings) = context.view_models.bindings(
165 MediaQueryViewModel::VIEW_MODEL,
166 MediaQueryViewModel::NOTIFIER,
167 ) {
168 bindings.unbind(context.id);
169 }
170 });
171
172 context.life_cycle.change(|context| {
173 for msg in context.messenger.messages {
174 if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() {
175 let _ = context.state.write(ResponsiveBoxState { size: *size });
176 }
177 }
178 });
179}
180
181#[pre_hooks(use_resize_listener, use_responsive_box)]
182pub fn responsive_box(mut context: WidgetContext) -> WidgetNode {
183 let WidgetContext {
184 id,
185 state,
186 mut listed_slots,
187 view_models,
188 ..
189 } = context;
190
191 let state = state.read_cloned_or_default::<ResponsiveBoxState>();
192 let view_model = view_models
193 .view_model(MediaQueryViewModel::VIEW_MODEL)
194 .and_then(|vm| vm.read::<MediaQueryViewModel>());
195 let ctx = MediaQueryContext {
196 widget_width: state.size.x,
197 widget_height: state.size.y,
198 view_model: view_model.as_deref(),
199 };
200 let item = if let Some(index) = listed_slots.iter().position(|slot| {
201 slot.props()
202 .map(|props| {
203 props
204 .read::<MediaQueryExpression>()
205 .ok()
206 .map(|query| query.is_valid(&ctx))
207 .unwrap_or(true)
208 })
209 .unwrap_or_default()
210 }) {
211 listed_slots.remove(index)
212 } else {
213 Default::default()
214 };
215
216 AreaBoxNode {
217 id: id.to_owned(),
218 slot: Box::new(item),
219 }
220 .into()
221}
222
223#[pre_hooks(use_resize_listener, use_responsive_box)]
224pub fn responsive_props_box(mut context: WidgetContext) -> WidgetNode {
225 let WidgetContext {
226 id,
227 state,
228 listed_slots,
229 named_slots,
230 view_models,
231 ..
232 } = context;
233 unpack_named_slots!(named_slots => content);
234
235 let state = state.read_cloned_or_default::<ResponsiveBoxState>();
236 let view_model = view_models
237 .view_model(MediaQueryViewModel::VIEW_MODEL)
238 .and_then(|vm| vm.read::<MediaQueryViewModel>());
239 let ctx = MediaQueryContext {
240 widget_width: state.size.x,
241 widget_height: state.size.y,
242 view_model: view_model.as_deref(),
243 };
244
245 let props = listed_slots
246 .iter()
247 .find_map(|slot| {
248 slot.props().and_then(|props| {
249 props
250 .read::<MediaQueryExpression>()
251 .ok()
252 .map(|query| query.is_valid(&ctx))
253 .unwrap_or(true)
254 .then_some(props.clone())
255 })
256 })
257 .unwrap_or_default();
258
259 if let Some(p) = content.props_mut() {
260 p.merge_from(props.without::<MediaQueryExpression>());
261 }
262
263 AreaBoxNode {
264 id: id.to_owned(),
265 slot: Box::new(content),
266 }
267 .into()
268}
269
270#[derive(Debug)]
271pub struct MediaQueryViewModel {
272 pub screen_size: ViewModelValue<Vec2>,
273 pub flags: ViewModelValue<HashSet<String>>,
274 pub numbers: ViewModelValue<HashMap<String, Scalar>>,
275}
276
277impl MediaQueryViewModel {
278 pub const VIEW_MODEL: &str = "MediaQueryViewModel";
279 pub const NOTIFIER: &str = "";
280
281 pub fn new(properties: &mut ViewModelProperties) -> Self {
282 let notifier = properties.notifier(Self::NOTIFIER);
283 Self {
284 screen_size: ViewModelValue::new(Default::default(), notifier.clone()),
285 flags: ViewModelValue::new(Default::default(), notifier.clone()),
286 numbers: ViewModelValue::new(Default::default(), notifier.clone()),
287 }
288 }
289}