Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/scene.rs
1use crate::elements::Fill;
2use crate::geometry::vector::vec2f;
3use crate::image_cache::StaticImage;
4use crate::{
5 elements::Point,
6 fonts::{FontId, GlyphId},
7 rendering,
8};
9use ordered_float::OrderedFloat;
10use pathfinder_color::ColorU;
11use pathfinder_geometry::rect::RectF;
12use pathfinder_geometry::vector::Vector2F;
13use rstar::{primitives::Rectangle, RTree};
14use std::sync::Arc;
15use vec1::{vec1, Vec1};
16 
17#[derive(Clone)]
18pub struct Scene {
19 scale_factor: f32,
20 rendering_config: rendering::Config,
21 active_layer_index_stack: Vec1<ZIndex>,
22 layers: Vec1<Layer>,
23 overlay_layers: Vec<Layer>,
24 #[cfg(debug_assertions)]
25 /// Custom panic location, set with [`Scene::set_location_for_panic_logging`]
26 panic_location: Option<&'static std::panic::Location<'static>>,
27}
28 
29#[derive(Clone, Default)]
30pub struct Layer {
31 hit_map: RTree<Rectangle<[OrderedFloat<f32>; 2]>>,
32 pub clip_bounds: Option<RectF>,
33 pub rects: Vec<Rect>,
34 pub images: Vec<Image>,
35 pub glyphs: Vec<Glyph>,
36 pub icons: Vec<Icon>,
37 pub click_through: bool,
38}
39 
40/// Clip bounds to use for a layer.
41pub enum ClipBounds {
42 /// Use the bounds of the active layer.
43 ActiveLayer,
44 /// Use the specified bounds as the bounds for the new layer.
45 ///
46 /// Note that this ignores any clip bounds applied to the currently-active
47 /// layer.
48 BoundedBy(RectF),
49 /// Intersect the active layer's bounds and the provided rect
50 /// to get the bounds for the new layer.
51 BoundedByActiveLayerAnd(RectF),
52 /// No clipping
53 None,
54}
55 
56impl Layer {
57 fn record_hit_rect(&mut self, rect: RectF) {
58 if let Some(intersected) = self
59 .clip_bounds
60 .map_or(Some(rect), |c| rect.intersection(c))
61 {
62 self.hit_map.insert(Rectangle::from_corners(
63 [intersected.min_x().into(), intersected.min_y().into()],
64 [intersected.max_x().into(), intersected.max_y().into()],
65 ));
66 }
67 }
68}
69 
70#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
71pub struct GlyphKey {
72 pub glyph_id: GlyphId,
73 pub font_id: FontId,
74 pub font_size: OrderedFloat<f32>,
75}
76 
77#[derive(Debug, Copy, Clone)]
78pub enum GlyphFade {
79 /// A horizontal fade from alpha 1 to 0 with start and end positions in screen coordinates
80 /// start - where the fade is transparent
81 /// end - where the fade is most opaque
82 Horizontal { start: f32, end: f32 },
83}
84 
85impl GlyphFade {
86 pub fn horizontal(start: f32, end: f32) -> Self {
87 GlyphFade::Horizontal { start, end }
88 }
89}
90 
91#[derive(Clone, Debug)]
92pub struct Glyph {
93 pub glyph_key: GlyphKey,
94 pub position: Vector2F,
95 pub fade: Option<GlyphFade>,
96 pub color: ColorU,
97}
98 
99#[derive(Clone, Default)]
100pub struct Rect {
101 pub bounds: RectF,
102 pub drop_shadow: Option<DropShadow>,
103 pub corner_radius: CornerRadius,
104 pub background: Fill,
105 pub border: Border,
106}
107 
108#[derive(Clone)]
109pub struct Image {
110 pub bounds: RectF,
111 pub asset: Arc<StaticImage>,
112 pub opacity: f32,
113 pub corner_radius: CornerRadius,
114}
115 
116#[derive(Clone)]
117pub struct Icon {
118 pub bounds: RectF,
119 pub asset: Arc<StaticImage>,
120 pub opacity: f32,
121 pub color: ColorU,
122}
123 
124// These were picked empirically to make the shadows look decent by
125// default, but there is nothing special about them.
126const DEFAULT_DROP_SHADOW_OFFSET_X: f32 = 0.;
127const DEFAULT_DROP_SHADOW_OFFSET_Y: f32 = 10.;
128const DEFAULT_DROP_SHADOW_BLUR_RADIUS: f32 = 10.;
129const DEFAULT_DROP_SHADOW_SPREAD_RADIUS: f32 = 30.;
130 
131#[derive(Clone, Copy)]
132pub struct DropShadow {
133 pub color: ColorU,
134 
135 // How the shadow is offset from the target rect
136 pub offset: Vector2F,
137 
138 // Controls how tightly sampled the shadow is - the larger the number
139 // the more spread out the shadow.
140 pub blur_radius: f32,
141 
142 // Controls how wide the shadow is outside the target.
143 pub spread_radius: f32,
144}
145 
146impl DropShadow {
147 pub fn new_with_standard_offset_and_spread(color: ColorU) -> Self {
148 Self {
149 color,
150 offset: vec2f(DEFAULT_DROP_SHADOW_OFFSET_X, DEFAULT_DROP_SHADOW_OFFSET_Y),
151 blur_radius: DEFAULT_DROP_SHADOW_BLUR_RADIUS,
152 spread_radius: DEFAULT_DROP_SHADOW_SPREAD_RADIUS,
153 }
154 }
155 
156 pub fn with_offset(mut self, offset: Vector2F) -> Self {
157 self.offset = offset;
158 self
159 }
160}
161 
162impl Default for DropShadow {
163 fn default() -> Self {
164 Self::new_with_standard_offset_and_spread(ColorU::new(0, 0, 0, 32))
165 }
166}
167 
168#[derive(Debug, Clone, Copy, Default, PartialEq)]
169pub struct Border {
170 pub width: f32,
171 pub color: Fill,
172 pub top: bool,
173 pub left: bool,
174 pub bottom: bool,
175 pub right: bool,
176 pub dash: Option<Dash>,
177}
178 
179#[derive(Debug, Clone, Copy, Default, PartialEq)]
180pub struct Dash {
181 pub dash_length: f32,
182 pub gap_length: f32,
183 
184 /// If true, gaps will always be the length specified in `gap_length`.
185 /// Otherwise, gap length may be adjusted slightly to guarantee that the
186 /// dashed line starts and ends with a dash.
187 pub force_consistent_gap_length: bool,
188}
189 
190impl Border {
191 pub fn top_width(&self) -> f32 {
192 if self.top {
193 self.width
194 } else {
195 0.0
196 }
197 }
198 
199 pub fn right_width(&self) -> f32 {
200 if self.right {
201 self.width
202 } else {
203 0.0
204 }
205 }
206 
207 pub fn bottom_width(&self) -> f32 {
208 if self.bottom {
209 self.width
210 } else {
211 0.0
212 }
213 }
214 
215 pub fn left_width(&self) -> f32 {
216 if self.left {
217 self.width
218 } else {
219 0.0
220 }
221 }
222}
223 
224#[derive(Clone, Copy, Debug, PartialEq)]
225pub enum Radius {
226 /// Specify a radius in absolute pixels.
227 Pixels(f32),
228 /// Specify a radius as a percentage of the rectangle's smaller dimension.
229 /// For example, using `Percentage(50.)` will produce a pill shape.
230 Percentage(f32),
231}
232 
233impl Default for Radius {
234 fn default() -> Self {
235 Radius::Pixels(0.)
236 }
237}
238 
239#[derive(Clone, Copy, Debug, Default, PartialEq)]
240pub struct CornerRadius {
241 /// Top left corner radius
242 top_left: Option<Radius>,
243 /// Top right corner radius
244 top_right: Option<Radius>,
245 /// Bottom left corner radius
246 bottom_left: Option<Radius>,
247 /// Bottom right corner radius
248 bottom_right: Option<Radius>,
249}
250 
251impl CornerRadius {
252 /// Merge this CornerRadius struct with another.
253 /// `Some(r)` takes precedence over `None`.
254 /// If both are present, `other`'s values, take precedence over `self`'s existing values.
255 pub fn merge(&mut self, other: CornerRadius) {
256 self.top_left = other.top_left.or(self.top_left);
257 self.top_right = other.top_right.or(self.top_right);
258 self.bottom_left = other.bottom_left.or(self.bottom_left);
259 self.bottom_right = other.bottom_right.or(self.bottom_right);
260 }
261 
262 pub fn get_top_left(&self) -> Radius {
263 self.top_left.unwrap_or(Radius::Pixels(0.))
264 }
265 
266 pub fn get_top_right(&self) -> Radius {
267 self.top_right.unwrap_or(Radius::Pixels(0.))
268 }
269 
270 pub fn get_bottom_left(&self) -> Radius {
271 self.bottom_left.unwrap_or(Radius::Pixels(0.))
272 }
273 
274 pub fn get_bottom_right(&self) -> Radius {
275 self.bottom_right.unwrap_or(Radius::Pixels(0.))
276 }
277 
278 pub const fn with_all(radius: Radius) -> Self {
279 CornerRadius {
280 top_left: Some(radius),
281 top_right: Some(radius),
282 bottom_left: Some(radius),
283 bottom_right: Some(radius),
284 }
285 }
286 pub const fn with_top(radius: Radius) -> Self {
287 CornerRadius {
288 top_left: Some(radius),
289 top_right: Some(radius),
290 bottom_left: None,
291 bottom_right: None,
292 }
293 }
294 pub const fn with_bottom(radius: Radius) -> Self {
295 CornerRadius {
296 top_left: None,
297 top_right: None,
298 bottom_left: Some(radius),
299 bottom_right: Some(radius),
300 }
301 }
302 pub const fn with_left(radius: Radius) -> Self {
303 CornerRadius {
304 top_left: Some(radius),
305 top_right: None,
306 bottom_left: Some(radius),
307 bottom_right: None,
308 }
309 }
310 pub const fn with_right(radius: Radius) -> Self {
311 CornerRadius {
312 top_left: None,
313 top_right: Some(radius),
314 bottom_left: None,
315 bottom_right: Some(radius),
316 }
317 }
318 pub const fn with_top_left(radius: Radius) -> Self {
319 CornerRadius {
320 top_left: Some(radius),
321 top_right: None,
322 bottom_left: None,
323 bottom_right: None,
324 }
325 }
326 pub const fn with_top_right(radius: Radius) -> Self {
327 CornerRadius {
328 top_left: None,
329 top_right: Some(radius),
330 bottom_left: None,
331 bottom_right: None,
332 }
333 }
334 pub const fn with_bottom_left(radius: Radius) -> Self {
335 CornerRadius {
336 top_left: None,
337 top_right: None,
338 bottom_left: Some(radius),
339 bottom_right: None,
340 }
341 }
342 pub const fn with_bottom_right(radius: Radius) -> Self {
343 CornerRadius {
344 top_left: None,
345 top_right: None,
346 bottom_left: None,
347 bottom_right: Some(radius),
348 }
349 }
350 
351 /// Filters this [`CornerRadius`] to only have the top corners rounded.
352 pub const fn top(self) -> Self {
353 CornerRadius {
354 top_left: self.top_left,
355 top_right: self.top_right,
356 bottom_left: None,
357 bottom_right: None,
358 }
359 }
360 
361 /// Filters this [`CornerRadius`] to only have the bottom corners rounded.
362 pub const fn bottom(self) -> Self {
363 CornerRadius {
364 top_left: None,
365 top_right: None,
366 bottom_left: self.bottom_left,
367 bottom_right: self.bottom_right,
368 }
369 }
370}
371 
372#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
373/// Newtype to encapsulate a Z index, which actually represents a layer index in the list of layers
374pub enum ZIndex {
375 Normal(usize),
376 Overlay(usize),
377}
378 
379impl ZIndex {
380 #[cfg(test)]
381 pub fn new(layer: usize) -> Self {
382 ZIndex::Normal(layer)
383 }
384}
385 
386impl Scene {
387 pub fn new(scale_factor: f32, rendering_config: rendering::Config) -> Self {
388 Self {
389 scale_factor,
390 rendering_config,
391 active_layer_index_stack: vec1![ZIndex::Normal(0)],
392 layers: vec1![Layer::default()],
393 overlay_layers: Vec::new(),
394 #[cfg(debug_assertions)]
395 panic_location: None,
396 }
397 }
398 
399 /// Temporarily set the panic location for the scene. This is cleared
400 /// during the next draw call.
401 #[cfg(debug_assertions)]
402 pub fn set_location_for_panic_logging(
403 &mut self,
404 panic_location: Option<&'static std::panic::Location<'static>>,
405 ) {
406 self.panic_location = panic_location;
407 }
408 
409 fn active_layer(&mut self) -> &mut Layer {
410 match *self.active_layer_index_stack.last() {
411 ZIndex::Normal(index) => &mut self.layers[index],
412 ZIndex::Overlay(index) => &mut self.overlay_layers[index],
413 }
414 }
415 
416 pub fn is_covered(&self, position: Point) -> bool {
417 // Does any layer at a higher z-index contain this point?
418 let point = [position.x().into(), position.y().into()];
419 let predicate = |l: &Layer| !l.click_through && l.hit_map.locate_at_point(&point).is_some();
420 
421 match position.z_index() {
422 ZIndex::Normal(index) => self
423 .layers
424 .get((index + 1)..)
425 .into_iter()
426 .flatten()
427 .chain(self.overlay_layers.iter())
428 .any(predicate),
429 ZIndex::Overlay(index) => self
430 .overlay_layers
431 .get((index + 1)..)
432 .into_iter()
433 .flatten()
434 .any(predicate),
435 }
436 }
437 
438 // Compute the intersection between the bound of the element and the clip bound
439 // on its current layer. The intersection is then checked against the event position
440 // to determine whether we should dispatch the event.
441 pub fn visible_rect(&self, origin: Point, size: Vector2F) -> Option<RectF> {
442 // TODO: Investigate how / when we would pass a z-index that isn't in the scene
443 // This appears to be fairly common, based on adding sentry reporting to it, however it
444 // doesn't seem to dramatically impact app usage. Perhaps it's something that happens on
445 // a view teardown frame?
446 let maybe_layer = match origin.z_index() {
447 ZIndex::Normal(index) => self.layers.get(index),
448 ZIndex::Overlay(index) => self.overlay_layers.get(index),
449 };
450 let maybe_bounds = maybe_layer.and_then(|layer| layer.clip_bounds);
451 
452 let input_rect = RectF::new(origin.xy(), size);
453 match maybe_bounds {
454 Some(clip_rect) => clip_rect.intersection(input_rect),
455 None => Some(input_rect),
456 }
457 }
458 
459 /// Get the Z-Index of the currently-active layer
460 pub fn z_index(&self) -> ZIndex {
461 *self.active_layer_index_stack.last()
462 }
463 
464 /// Get the maximum Z-Index in the active layer stack (whether Normal or Overlay).
465 pub fn max_active_z_index(&self) -> ZIndex {
466 match self.active_layer_index_stack.last() {
467 ZIndex::Normal(_) => ZIndex::Normal(self.layers.len() - 1),
468 // Safety: If the active layer is an overlay layer, then there must be at least one
469 // overlay layer, so subtracting one from the length is valid.
470 ZIndex::Overlay(_) => ZIndex::Overlay(self.overlay_layers.len() - 1),
471 }
472 }
473 
474 pub fn start_layer(&mut self, bounds: ClipBounds) {
475 let layer = self.create_layer(bounds);
476 
477 match *self.active_layer_index_stack.last() {
478 ZIndex::Normal(_) => self.push_normal_layer(layer),
479 ZIndex::Overlay(_) => self.push_overlay_layer(layer),
480 }
481 }
482 
483 pub(crate) fn start_overlay_layer(&mut self, bounds: ClipBounds) {
484 let layer = self.create_layer(bounds);
485 self.push_overlay_layer(layer);
486 }
487 
488 fn create_layer(&mut self, bounds: ClipBounds) -> Layer {
489 let clip_bounds = match bounds {
490 ClipBounds::ActiveLayer => self.active_layer().clip_bounds,
491 ClipBounds::BoundedBy(bounds) => Some(bounds),
492 ClipBounds::BoundedByActiveLayerAnd(bounds) => {
493 if let Some(current_layer_bounds) = self.active_layer().clip_bounds {
494 // If the current layer has bounds, return the intersection...
495 current_layer_bounds
496 .intersection(bounds)
497 // ...or, if the regions don't overlap, an empty bounding rect.
498 .or(Some(RectF::default()))
499 } else {
500 // If the current layer has no bounds, return the bounds
501 // for the new layer.
502 Some(bounds)
503 }
504 }
505 ClipBounds::None => None,
506 };
507 
508 Layer {
509 clip_bounds,
510 ..Default::default()
511 }
512 }
513 
514 fn push_normal_layer(&mut self, layer: Layer) {
515 self.active_layer_index_stack
516 .push(ZIndex::Normal(self.layers.len()));
517 self.layers.push(layer);
518 }
519 
520 fn push_overlay_layer(&mut self, layer: Layer) {
521 self.active_layer_index_stack
522 .push(ZIndex::Overlay(self.overlay_layers.len()));
523 self.overlay_layers.push(layer);
524 }
525 
526 pub fn set_active_layer_click_through(&mut self) {
527 self.active_layer().click_through = true;
528 }
529 
530 pub fn stop_layer(&mut self) {
531 if self.active_layer_index_stack.pop().is_err() {
532 panic!("popped the last layer from active_layer_index_stack");
533 }
534 }
535 
536 fn validate_rect(rect: &RectF, location: Option<&'static std::panic::Location<'static>>) {
537 #[cfg(debug_assertions)]
538 let location_info = location
539 .map(|loc| {
540 format!(
541 " (element created at {}:{}:{})",
542 loc.file(),
543 loc.line(),
544 loc.column()
545 )
546 })
547 .unwrap_or_default();
548 #[cfg(not(debug_assertions))]
549 let location_info = "";
550 debug_assert!(
551 !rect.origin().y().is_infinite(),
552 "!rect.origin().y().is_infinite(){location_info}"
553 );
554 debug_assert!(
555 !rect.origin().y().is_nan(),
556 "!rect.origin().y().is_nan(){location_info}"
557 );
558 
559 debug_assert!(
560 !rect.size().x().is_infinite(),
561 "!rect.size().x().is_infinite(){location_info}"
562 );
563 debug_assert!(
564 !rect.size().x().is_nan(),
565 "!rect.size().x().is_nan(){location_info}"
566 );
567 debug_assert!(
568 !rect.size().y().is_infinite(),
569 "!rect.size().y().is_infinite(){location_info}"
570 );
571 debug_assert!(
572 !rect.size().y().is_nan(),
573 "!rect.size().y().is_nan(){location_info}"
574 );
575 }
576 
577 /// This method draws a rectangle without recording any information about it in the current
578 /// layer. Note this should be used with caution. In most cases, what you need is
579 /// `draw_rect_with_hit_recording` instead. However, in rare cases this may be useful for
580 /// performance reasons when many intermediate rects are drawn. If this is called, it is up to
581 /// the caller to also draw a rect (via draw_rect_with_hit_recording) that encompasses the range
582 /// of the rects drawn so that layer recording for event dispatching is correctly kept
583 /// up-to-date.
584 pub fn draw_rect_without_hit_recording(&mut self, rect: RectF) -> &mut Rect {
585 #[cfg(debug_assertions)]
586 let location = self.panic_location.take();
587 #[cfg(not(debug_assertions))]
588 let location = None;
589 let layer = self.active_layer();
590 Self::validate_rect(&rect, location);
591 
592 layer.rects.push(Rect {
593 bounds: rect,
594 ..Default::default()
595 });
596 layer.rects.last_mut().unwrap()
597 }
598 
599 pub fn draw_rect_with_hit_recording(&mut self, rect: RectF) -> &mut Rect {
600 let layer = self.active_layer();
601 layer.record_hit_rect(rect);
602 self.draw_rect_without_hit_recording(rect)
603 }
604 
605 pub fn draw_image(
606 &mut self,
607 rect: RectF,
608 asset: Arc<StaticImage>,
609 opacity: f32,
610 corner_radius: CornerRadius,
611 ) {
612 #[cfg(debug_assertions)]
613 let location = self.panic_location.take();
614 #[cfg(not(debug_assertions))]
615 let location = None;
616 let layer = self.active_layer();
617 Self::validate_rect(&rect, location);
618 
619 layer.images.push(Image {
620 bounds: rect,
621 asset,
622 opacity,
623 corner_radius,
624 });
625 layer.record_hit_rect(rect);
626 }
627 
628 pub fn draw_icon(&mut self, rect: RectF, asset: Arc<StaticImage>, opacity: f32, color: ColorU) {
629 #[cfg(debug_assertions)]
630 let location = self.panic_location.take();
631 #[cfg(not(debug_assertions))]
632 let location = None;
633 let layer = self.active_layer();
634 Self::validate_rect(&rect, location);
635 
636 layer.icons.push(Icon {
637 bounds: rect,
638 asset,
639 opacity,
640 color,
641 });
642 layer.record_hit_rect(rect);
643 }
644 
645 /// Adds a glyph that should be drawn in the scene.
646 ///
647 /// `position` is the point at which the glyph's left edge meets the
648 /// baseline.
649 pub fn draw_glyph(
650 &mut self,
651 position: Vector2F,
652 glyph_id: GlyphId,
653 font_id: FontId,
654 font_size: f32,
655 color: ColorU,
656 ) -> &mut Glyph {
657 // TODO: Support hit testing on glyphs?
658 let layer = self.active_layer();
659 layer.glyphs.push(Glyph {
660 glyph_key: GlyphKey {
661 glyph_id,
662 font_id,
663 font_size: font_size.into(),
664 },
665 position,
666 color,
667 fade: None,
668 });
669 layer.glyphs.last_mut().unwrap()
670 }
671 
672 /// Get an iterator over all layers in order, from bottom to top
673 pub fn layers(&self) -> impl Iterator<Item = &Layer> {
674 self.layers.iter().chain(self.overlay_layers.iter())
675 }
676 
677 /// Get the total number of layers
678 #[cfg(test)]
679 pub fn layer_count(&self) -> usize {
680 self.layers.len() + self.overlay_layers.len()
681 }
682 
683 pub fn scale_factor(&self) -> f32 {
684 self.scale_factor
685 }
686 
687 pub fn rendering_config(&self) -> &rendering::Config {
688 &self.rendering_config
689 }
690}
691 
692impl Rect {
693 pub fn with_corner_radius(&mut self, radius: CornerRadius) -> &mut Self {
694 self.corner_radius.merge(radius);
695 self
696 }
697 
698 pub fn with_border(&mut self, border: Border) -> &mut Self {
699 self.border = border;
700 self
701 }
702 
703 pub fn with_background<F>(&mut self, background: F) -> &mut Self
704 where
705 F: Into<Fill>,
706 {
707 self.background = background.into();
708 self
709 }
710 
711 pub fn with_drop_shadow(&mut self, drop_shadow: DropShadow) -> &mut Self {
712 self.drop_shadow = Some(drop_shadow);
713 self
714 }
715}
716 
717impl Glyph {
718 pub fn with_fade(&mut self, fade: Option<GlyphFade>) -> &mut Self {
719 self.fade = fade;
720 self
721 }
722}
723 
724#[cfg(test)]
725#[path = "scene_test.rs"]
726mod tests;
727