1use std::{
2 cell::Cell,
3 mem::size_of,
4 ops::{Deref, DerefMut, Range},
5 rc::Rc,
6 sync::atomic::{AtomicU32, Ordering},
7};
8
9use cranpose_core::{NodeId, collections::map::HashSet};
10use cranpose_ui::{
11 GraphicsLayer, ModifierNodeSlices, Point, Rect, RenderEffect, TextLayoutOptions, TextOverflow,
12 TextStyle,
13 text::{AnnotatedString, RenderString, SpanStyle, TextDrawStyle},
14};
15pub use cranpose_ui_graphics::transform::{FrontPolygon, ProjectiveTransform, quad_bounds};
16use cranpose_ui_graphics::{
17 BlendMode, ColorFilter, CommandRecording, DrawPrimitive, RecordingSummary, ShadowPrimitive,
18};
19
20use crate::{
21 raster_cache::LayerRasterCacheHashes,
22 style_shared::DrawPlacement,
23 text_paint::{TextPaint, TextPaintCache},
24};
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct IsolationReasons {
29 pub explicit_offscreen: bool,
31 pub shape_clip: bool,
33 pub effect: bool,
35 pub backdrop: bool,
37 pub group_opacity: bool,
39 pub blend_mode: bool,
41}
42
43impl IsolationReasons {
44 pub fn has_any(self) -> bool {
46 self.explicit_offscreen
47 || self.shape_clip
48 || self.effect
49 || self.backdrop
50 || self.group_opacity
51 || self.blend_mode
52 }
53}
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum CachePolicy {
58 #[default]
60 None,
61 Auto,
63}
64
65#[derive(Clone)]
67pub struct HitTestNode {
68 pub handlers: Rc<ModifierNodeSlices>,
73}
74
75#[derive(Clone, Debug, PartialEq)]
77pub struct DrawPrimitiveNode {
78 pub primitive: DrawPrimitive,
80 pub clip: Option<Rect>,
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct TextPrimitiveNode {
87 pub node_id: NodeId,
89 pub rect: Rect,
91 pub text: Rc<AnnotatedString>,
94 pub render_text: std::sync::Arc<RenderString>,
97 pub text_style: std::sync::Arc<TextStyle>,
100 pub font_size: f32,
102 pub layout_options: TextLayoutOptions,
104 pub clip: Option<Rect>,
107 pub paint: TextPaintCache,
110}
111
112impl TextPrimitiveNode {
113 pub fn paint(&self) -> TextPaint {
115 self.paint.get_or(|| {
116 TextPaint::of(
117 &self.text_style,
118 self.text_style.render_hash(),
119 self.render_text.span_styles(),
120 self.font_size,
121 (self.render_text.render_hash(), self.render_text.is_empty()),
122 )
123 })
124 }
125
126 pub fn draw_bounds(&self) -> Option<Rect> {
129 if let Some(clip) = self.clip {
130 return Some(clip);
131 }
132 let style = &self.text_style.span_style;
133 let stroked =
134 |style: &SpanStyle| matches!(style.draw_style, Some(TextDrawStyle::Stroke { .. }));
135 (!matches!(self.layout_options.overflow, TextOverflow::Visible)
136 && style.shadow.is_none()
137 && style
138 .baseline_shift
139 .is_none_or(|shift| !shift.is_specified() || shift.0 == 0.0)
140 && !stroked(style)
141 && self
142 .text
143 .span_styles
144 .iter()
145 .all(|span| !stroked(&span.item)))
146 .then_some(self.rect)
147 }
148
149 fn draws_within(&self, bounds: Rect) -> bool {
150 self.draw_bounds()
151 .is_some_and(|drawn| rect_within(drawn, bounds))
152 }
153}
154
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum PrimitivePhase {
158 BeforeChildren,
160 AfterChildren,
162}
163
164#[derive(Clone, Debug, PartialEq)]
166pub enum PrimitiveNode {
167 Draw(Box<DrawPrimitiveNode>),
169 Text(Box<TextPrimitiveNode>),
171}
172
173#[derive(Clone, Debug, PartialEq)]
175pub struct PrimitiveEntry {
176 pub phase: PrimitivePhase,
178 pub node: PrimitiveNode,
180}
181
182#[derive(Clone, Debug, Default)]
184pub struct LayerProperties(Option<Box<GraphicsLayer>>);
185
186impl LayerProperties {
187 pub fn get(&self) -> Option<&GraphicsLayer> {
189 self.0.as_deref()
190 }
191
192 #[inline]
195 pub(crate) fn reads_as(&self, value: Option<&GraphicsLayer>) -> bool {
196 match (self.get(), value) {
197 (None, None) => true,
198 (stored, value) => {
199 stored.unwrap_or(&GraphicsLayer::DEFAULT)
200 == value.unwrap_or(&GraphicsLayer::DEFAULT)
201 }
202 }
203 }
204
205 pub(crate) fn replace(&mut self, value: Option<GraphicsLayer>) {
206 match (&mut self.0, value) {
207 (Some(current), Some(value)) => **current = value,
208 (storage, value) => *storage = value.map(Box::new),
209 }
210 }
211}
212
213impl Deref for LayerProperties {
214 type Target = GraphicsLayer;
215
216 fn deref(&self) -> &Self::Target {
217 self.0.as_deref().unwrap_or(&GraphicsLayer::DEFAULT)
218 }
219}
220
221impl DerefMut for LayerProperties {
222 fn deref_mut(&mut self) -> &mut Self::Target {
223 self.0.get_or_insert_with(Box::default)
224 }
225}
226
227impl From<GraphicsLayer> for LayerProperties {
228 fn from(value: GraphicsLayer) -> Self {
229 Self(Some(Box::new(value)))
230 }
231}
232
233impl From<Option<GraphicsLayer>> for LayerProperties {
234 fn from(value: Option<GraphicsLayer>) -> Self {
235 Self(value.map(Box::new))
236 }
237}
238
239impl PartialEq for LayerProperties {
240 fn eq(&self, other: &Self) -> bool {
241 self.reads_as(other.get())
242 }
243}
244
245#[derive(Clone)]
248pub struct LayerNode {
249 pub node_id: Option<NodeId>,
251 pub wraps: Option<NodeId>,
256 pub local_bounds: Rect,
259 pub node_bounds: Option<Rect>,
264 pub transform_to_parent: ProjectiveTransform,
267 pub content_offset: Point,
270 pub motion_context_animated: bool,
273 pub translated_content_context: bool,
276 pub translated_content_offset: Point,
278 pub origin_in_parent: Point,
283 pub graphics_layer: LayerProperties,
285 pub clip_to_bounds: bool,
287 pub hit_test: Option<HitTestNode>,
289 pub has_hit_targets: bool,
292 pub space: Cell<Option<bool>>,
295 pub has_origin_sinks: bool,
300 pub contained: Cell<Option<bool>>,
303 pub isolation: IsolationReasons,
305 pub draw_phase: Option<PrimitivePhase>,
310 pub cache_policy: CachePolicy,
312 pub cache_hashes: Cell<Option<LayerRasterCacheHashes>>,
315 pub content_changes: ContentChanges,
317 pub change: LayerChange,
319 pub own_change: LayerChange,
324 pub children: Vec<RenderNode>,
326}
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
333pub struct LayerChange(u32);
334
335impl LayerChange {
336 fn next() -> Self {
338 static LAST: AtomicU32 = AtomicU32::new(0);
339 Self(LAST.fetch_add(1, Ordering::Relaxed).wrapping_add(1))
340 }
341}
342
343impl Default for LayerChange {
344 fn default() -> Self {
345 Self::next()
346 }
347}
348
349#[derive(Clone, Copy, Debug, Default)]
352pub struct ContentChanges {
353 last_update: u64,
354 run: u32,
355}
356
357impl ContentChanges {
358 fn note(&mut self, update: u64) {
359 if self.run > 0 && self.last_update == update {
360 return;
361 }
362 self.run = if self.run > 0 && self.last_update + 1 == update {
363 self.run + 1
364 } else {
365 1
366 };
367 self.last_update = update;
368 }
369}
370
371impl Default for LayerNode {
372 fn default() -> Self {
373 Self {
374 node_id: None,
375 wraps: None,
376 local_bounds: Rect {
377 x: 0.0,
378 y: 0.0,
379 width: 0.0,
380 height: 0.0,
381 },
382 node_bounds: None,
383 transform_to_parent: ProjectiveTransform::identity(),
384 content_offset: Point::default(),
385 motion_context_animated: false,
386 translated_content_context: false,
387 translated_content_offset: Point::default(),
388 origin_in_parent: Point::default(),
389 graphics_layer: LayerProperties::default(),
390 clip_to_bounds: false,
391 hit_test: None,
392 has_hit_targets: false,
393 space: Cell::new(None),
394 has_origin_sinks: false,
395 contained: Cell::new(None),
396 isolation: IsolationReasons::default(),
397 draw_phase: None,
398 cache_policy: CachePolicy::None,
399 cache_hashes: Cell::new(None),
400 content_changes: ContentChanges::default(),
401 change: LayerChange::default(),
402 own_change: LayerChange::default(),
403 children: Vec::new(),
404 }
405 }
406}
407
408pub const CONTAINED_DRAW_SLACK: f32 = 1.0;
411
412impl LayerNode {
413 pub fn painted_text(&self) -> Vec<String> {
415 let mut out = Vec::new();
416 self.collect_painted_text(&mut out);
417 out
418 }
419
420 fn collect_painted_text(&self, out: &mut Vec<String>) {
421 for child in &self.children {
422 match child {
423 RenderNode::Primitive(primitive) => {
424 if let PrimitiveNode::Text(text) = &primitive.node {
425 out.push(text.text.text.clone());
426 }
427 }
428 RenderNode::Layer(child) => child.collect_painted_text(out),
429 RenderNode::DrawRun(run) => {
430 for primitive in run.primitives() {
431 if let DrawPrimitive::Text(text) = primitive {
432 out.push(text.text.to_string());
433 }
434 }
435 }
436 }
437 }
438 }
439
440 pub fn draws_within_bounds(&self) -> bool {
445 if let Some(contained) = self.contained.get() {
446 return contained;
447 }
448 let contained = self.content_draws_within_bounds();
449 self.contained.set(Some(contained));
450 contained
451 }
452
453 pub fn content_draws_within_bounds(&self) -> bool {
457 if self.visual_clip_rect().is_some() {
458 return true;
459 }
460 let bounds = inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK);
461 self.children.iter().all(|child| match child {
462 RenderNode::DrawRun(run) => {
463 !run.summary.has_shadow
464 && run
465 .recording
466 .bounds()
467 .is_none_or(|drawn| rect_within(drawn, bounds))
468 }
469 RenderNode::Primitive(entry) => match &entry.node {
470 PrimitiveNode::Text(text) => text.draws_within(bounds),
471 PrimitiveNode::Draw(_) => false,
472 },
473 RenderNode::Layer(layer) => layer.draws_within_parent(bounds),
474 })
475 }
476
477 pub(crate) fn refresh_child_facts(&mut self) {
478 self.has_hit_targets = self.hit_test.is_some()
479 || self.children.iter().any(|child| match child {
480 RenderNode::Layer(child_layer) => child_layer.has_hit_targets,
481 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
482 });
483 self.contained.set(None);
484 self.space.set(None);
485 }
486
487 pub fn has_space(&self) -> bool {
491 if let Some(known) = self.space.get() {
492 return known;
493 }
494 let found = self.graphics_layer.space.is_some()
495 || self.children.iter().any(|child| match child {
496 RenderNode::Layer(child_layer) => child_layer.has_space(),
497 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => false,
498 });
499 self.space.set(Some(found));
500 found
501 }
502
503 fn draws_within_parent(&self, bounds: Rect) -> bool {
507 self.draws_within_bounds()
508 && self.graphics_layer.shadow_elevation <= 0.0
509 && self.effect().is_none()
510 && self.backdrop().is_none()
511 && rect_within(
512 self.transform_to_parent
513 .bounds_for_rect(inflate_rect(self.local_bounds, CONTAINED_DRAW_SLACK)),
514 bounds,
515 )
516 }
517
518 pub fn node_rect(&self) -> Rect {
520 self.node_bounds.unwrap_or(self.local_bounds)
521 }
522
523 pub fn clip_rect(&self) -> Option<Rect> {
526 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
527 }
528
529 pub fn shadow_clip(&self) -> Option<Rect> {
532 self.clip_to_bounds.then_some(self.local_bounds)
533 }
534
535 #[inline]
540 pub fn visual_clip_rect(&self) -> Option<Rect> {
541 self.clip_rect().or_else(|| {
542 (self.isolation.group_opacity || self.isolation.explicit_offscreen)
543 .then_some(self.local_bounds)
544 })
545 }
546
547 pub fn effect(&self) -> Option<&RenderEffect> {
549 self.graphics_layer.render_effect.as_ref()
550 }
551
552 pub fn backdrop(&self) -> Option<&RenderEffect> {
554 self.graphics_layer.backdrop_effect.as_ref()
555 }
556
557 pub fn opacity(&self) -> f32 {
559 self.graphics_layer.alpha
560 }
561
562 pub fn blend_mode(&self) -> BlendMode {
564 self.graphics_layer.blend_mode
565 }
566
567 pub fn color_filter(&self) -> Option<&ColorFilter> {
569 self.graphics_layer.color_filter.as_ref()
570 }
571
572 pub fn target_content_hash(&self) -> u64 {
575 self.raster_cache_hashes().target_content
576 }
577
578 pub fn motion_source_content_hash(&self) -> u64 {
581 crate::graph_hash::layer_motion_source_content_hash(self)
582 }
583
584 pub fn effect_hash(&self) -> u64 {
586 self.raster_cache_hashes().effect
587 }
588
589 fn raster_cache_hashes(&self) -> LayerRasterCacheHashes {
590 if let Some(hashes) = self.cache_hashes.get() {
591 return hashes;
592 }
593 let hashes = crate::graph_hash::layer_raster_cache_hashes(self);
594 self.cache_hashes.set(Some(hashes));
595 hashes
596 }
597
598 pub fn forget_raster_cache_hashes(&mut self) {
602 self.forget_own_raster_cache_hashes();
603 for child in &mut self.children {
604 if let RenderNode::Layer(child) = child {
605 child.forget_raster_cache_hashes();
606 }
607 }
608 }
609
610 pub(crate) fn forget_own_raster_cache_hashes(&mut self) {
613 self.cache_hashes.set(None);
614 self.change = LayerChange::next();
615 self.own_change = LayerChange::next();
616 }
617
618 pub(crate) fn note_descendant_change(&mut self, update: u64) {
622 self.cache_hashes.set(None);
623 self.change = LayerChange::next();
624 self.content_changes.note(update);
625 }
626
627 pub(crate) fn note_content_change(&mut self, update: u64) {
630 self.forget_own_raster_cache_hashes();
631 self.content_changes.note(update);
632 }
633
634 pub fn content_churns(&self, update: u64) -> bool {
638 self.content_changes.run >= 2 && self.content_changes.last_update == update
639 }
640}
641
642#[derive(Clone)]
644pub enum RenderNode {
645 Primitive(PrimitiveEntry),
647 DrawRun(DrawRunNode),
654 Layer(Box<LayerNode>),
656}
657
658impl RenderNode {
659 pub fn draw_phase(&self) -> Option<PrimitivePhase> {
663 match self {
664 RenderNode::Primitive(entry) => Some(entry.phase),
665 RenderNode::DrawRun(run) => Some(run.phase),
666 RenderNode::Layer(layer) => layer.draw_phase,
667 }
668 }
669
670 fn draws_after_children(&self) -> bool {
671 self.draw_phase() == Some(PrimitivePhase::AfterChildren)
672 }
673}
674
675pub fn for_each_in_draw_order(children: &[RenderNode], mut visit: impl FnMut(&RenderNode)) {
679 let mut first_deferred = children.len();
680 for (index, child) in children.iter().enumerate() {
681 if child.draws_after_children() {
682 first_deferred = first_deferred.min(index);
683 } else {
684 visit(child);
685 }
686 }
687 children[first_deferred..]
688 .iter()
689 .filter(|child| child.draws_after_children())
690 .for_each(visit);
691}
692
693#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
698pub struct DrawCommandId {
699 pub node_id: NodeId,
701 pub command_index: u32,
703 pub placement: DrawPlacement,
705}
706
707#[derive(Clone, Debug, PartialEq)]
709pub struct DrawRunNode {
710 pub phase: PrimitivePhase,
712 pub command: Option<DrawCommandId>,
715 pub recording: Rc<CommandRecording>,
721 pub segments: Range<u32>,
724 pub summary: DrawRunSummary,
727}
728
729pub type DrawRunSummary = RecordingSummary;
731
732impl DrawRunNode {
733 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
735 Self::for_command(phase, None, primitives)
736 }
737
738 pub fn for_command(
740 phase: PrimitivePhase,
741 command: Option<DrawCommandId>,
742 primitives: Vec<DrawPrimitive>,
743 ) -> Self {
744 let recording = CommandRecording::from_primitives(primitives);
745 let segments = recording.all_segments();
746 Self::for_command_shared(phase, command, Rc::new(recording), segments)
747 }
748
749 pub fn for_command_shared(
751 phase: PrimitivePhase,
752 command: Option<DrawCommandId>,
753 recording: Rc<CommandRecording>,
754 segments: Range<u32>,
755 ) -> Self {
756 let summary = recording.summary_in(&segments);
757 Self {
758 phase,
759 command,
760 recording,
761 segments,
762 summary,
763 }
764 }
765
766 pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
768 self.recording.primitives(self.segments.clone())
769 }
770
771 pub fn for_each_primitive(&self, visit: impl FnMut(DrawPrimitive)) {
774 self.recording
775 .for_each_primitive(self.segments.clone(), visit);
776 }
777
778 pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
780 self.recording.coverage_rects(self.segments.clone())
781 }
782
783 pub fn len(&self) -> usize {
785 self.recording.len_in(&self.segments)
786 }
787
788 pub fn is_empty(&self) -> bool {
790 self.recording.is_empty_in(&self.segments)
791 }
792}
793
794#[derive(Clone)]
796pub struct RenderGraph {
797 pub root: LayerNode,
799 pub update: u64,
801 pub(crate) retained_path: Option<crate::scene_builder::RetainedPath>,
804}
805
806impl RenderGraph {
807 pub fn new(root: LayerNode) -> Self {
809 Self {
810 root,
811 update: 0,
812 retained_path: None,
813 }
814 }
815
816 pub fn node_count(&self) -> usize {
819 fn count_layer(layer: &LayerNode) -> usize {
820 1 + layer
821 .children
822 .iter()
823 .map(|child| match child {
824 RenderNode::Primitive(_) => 1,
825 RenderNode::DrawRun(run) => run.len(),
826 RenderNode::Layer(child_layer) => count_layer(child_layer),
827 })
828 .sum::<usize>()
829 }
830
831 count_layer(&self.root)
832 }
833
834 pub fn heap_bytes(&self) -> usize {
836 layer_heap_bytes(&self.root)
837 }
838
839 pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
841 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
842 if let Some(node_id) = layer.node_id {
843 nodes.insert(node_id);
844 }
845 for child in &layer.children {
846 match child {
847 RenderNode::DrawRun(run) => {
848 if let Some(command) = run.command {
849 nodes.insert(command.node_id);
850 }
851 }
852 RenderNode::Layer(child) => collect(child, nodes),
853 RenderNode::Primitive(_) => {}
854 }
855 }
856 }
857
858 nodes.clear();
859 collect(&self.root, nodes);
860 }
861}
862
863fn layer_heap_bytes(layer: &LayerNode) -> usize {
864 size_of::<RenderNode>() * layer.children.capacity()
865 + layer
866 .graphics_layer
867 .0
868 .as_ref()
869 .map_or(0, |_| size_of::<GraphicsLayer>())
870 + layer
871 .children
872 .iter()
873 .map(render_node_heap_bytes)
874 .sum::<usize>()
875}
876
877fn render_node_heap_bytes(node: &RenderNode) -> usize {
878 match node {
879 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
880 RenderNode::DrawRun(run) => {
881 run.recording.pod_heap_bytes()
882 + std::mem::size_of_val(run.recording.others())
883 + run
884 .recording
885 .others()
886 .iter()
887 .map(draw_primitive_heap_bytes)
888 .sum::<usize>()
889 }
890 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
891 }
892}
893
894fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
895 match &entry.node {
896 PrimitiveNode::Draw(draw) => {
897 size_of::<DrawPrimitiveNode>() + draw_primitive_heap_bytes(&draw.primitive)
898 }
899 PrimitiveNode::Text(text) => {
900 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
901 }
902 }
903}
904
905fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
906 match primitive {
907 DrawPrimitive::Content
908 | DrawPrimitive::Group(_)
909 | DrawPrimitive::Rect { .. }
910 | DrawPrimitive::RoundRect { .. }
911 | DrawPrimitive::Arc { .. }
912 | DrawPrimitive::Line { .. }
913 | DrawPrimitive::Trapezoid { .. } => 0,
914 DrawPrimitive::Blend { primitive, .. } => {
915 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
916 }
917 DrawPrimitive::Image { .. } => 0,
918 DrawPrimitive::Text(text) => {
919 size_of::<cranpose_ui_graphics::TextPrimitive>()
920 + text.text.len()
921 + text
922 .style
923 .font_family
924 .as_ref()
925 .map_or(0, std::string::String::capacity)
926 }
927 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
928 }
929}
930
931fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
932 match shadow {
933 ShadowPrimitive::Drop { shape, .. } => {
934 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
935 }
936 ShadowPrimitive::Inner { fill, cutout, .. } => {
937 size_of::<DrawPrimitive>() * 2
938 + draw_primitive_heap_bytes(fill)
939 + draw_primitive_heap_bytes(cutout)
940 }
941 }
942}
943
944fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
945 text.text.capacity()
946 + text.span_styles.capacity() * size_of::<usize>() * 2
947 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
948 + text.string_annotations.capacity() * size_of::<usize>() * 2
949 + text.link_annotations.capacity() * size_of::<usize>() * 2
950 + text
951 .string_annotations
952 .iter()
953 .map(|annotation| {
954 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
955 })
956 .sum::<usize>()
957 + text
958 .link_annotations
959 .iter()
960 .map(|annotation| match &annotation.item {
961 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
962 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
963 })
964 .sum::<usize>()
965}
966
967fn inflate_rect(rect: Rect, by: f32) -> Rect {
968 Rect {
969 x: rect.x - by,
970 y: rect.y - by,
971 width: rect.width + by * 2.0,
972 height: rect.height + by * 2.0,
973 }
974}
975
976fn rect_within(inner: Rect, outer: Rect) -> bool {
977 inner.x >= outer.x
978 && inner.y >= outer.y
979 && inner.x + inner.width <= outer.x + outer.width
980 && inner.y + inner.height <= outer.y + outer.height
981}
982
983#[cfg(test)]
984#[path = "tests/graph_tests.rs"]
985mod tests;