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/debug/view_tree_debug_view.rs
StratoSDK / crates / strato-ui-core / src / debug / view_tree_debug_view.rs
1use std::collections::HashMap;
2 
3use itertools::Itertools;
4use pathfinder_color::ColorU;
5 
6use crate::{
7 elements::{
8 Align, Container, Fill, Hoverable, MouseStateHandle, ScrollStateHandle, Scrollable,
9 ScrollableElement, ScrollbarWidth, Text, UniformList, UniformListState,
10 },
11 AppContext, Element, Entity, EntityId, TypedActionView, View, ViewContext, WeakViewHandle,
12 WindowId,
13};
14 
15/// Turns a map of parent->children into a list of (view, tree_depth) pairs,
16/// ordered via depth-first traversal of the input map.
17fn populate_view_list(
18 view_children_map: &HashMap<EntityId, Vec<EntityId>>,
19 current_view_id: EntityId,
20 depth: usize,
21 view_list: &mut Vec<(EntityId, usize)>,
22) {
23 view_list.push((current_view_id, depth));
24 if let Some(children) = view_children_map.get(&current_view_id) {
25 for child in children {
26 populate_view_list(view_children_map, *child, depth + 1, view_list);
27 }
28 }
29}
30 
31/// Helper structure containing state necessary to render information about a
32/// single view in a window's view hierarchy.
33#[derive(Debug, Clone)]
34struct ViewInfo {
35 view_id: EntityId,
36 view_depth: usize,
37 mouse_state_handle: MouseStateHandle,
38}
39 
40impl ViewInfo {
41 fn render(&self, window_id: WindowId, ctx: &AppContext) -> Box<dyn Element> {
42 let spacing = " ".repeat(self.view_depth);
43 let view_id = self.view_id;
44 let view_name = ctx
45 .view_name(window_id, view_id)
46 .expect("view should exist");
47 
48 Hoverable::new(self.mouse_state_handle.clone(), |mouse_state| {
49 let text = Text::new_inline(
50 format!("{spacing}{view_name} ({view_id:?})"),
51 // This relies on an expectation that the first font loaded is
52 // a reasonable one to draw this view with, but we lack a better
53 // method, at the moment, to intentionally select a font.
54 // TODO(vorporeal): Don't arbitrarily pick font family 0.
55 crate::fonts::FamilyId(0),
56 13.,
57 );
58 let background_color = if mouse_state.is_hovered() {
59 ColorU::new(0, 143, 143, 255)
60 } else {
61 ColorU::transparent_black()
62 };
63 Container::new(text.finish())
64 .with_background_color(background_color)
65 .finish()
66 })
67 .on_click(move |ctx, _, _| {
68 ctx.dispatch_typed_action(ViewTreeDebugAction::HighlightView(window_id, view_id));
69 })
70 .finish()
71 }
72}
73 
74/// Actions that can be taken within the View Tree debug view.
75#[derive(Debug, Clone)]
76pub(super) enum ViewTreeDebugAction {
77 /// Visually highlights a particular view in a given window.
78 HighlightView(WindowId, EntityId),
79}
80 
81/// A view to help visualize and interact with the view hierarchy for a
82/// particular window.
83///
84/// This only includes views that had been laid out at some point prior to the
85/// creation of this view. At present, this view caches the view hierarchy at
86/// creation time and does not dynamically update it as new views are laid out
87/// and rendered.
88pub(super) struct ViewTreeDebugView {
89 handle: WeakViewHandle<Self>,
90 target_window_id: WindowId,
91 view_info: Vec<ViewInfo>,
92 uniform_list_state: UniformListState,
93 scroll_state_handle: ScrollStateHandle,
94}
95 
96impl ViewTreeDebugView {
97 pub fn new(
98 target_window_id: WindowId,
99 view_parent_map: HashMap<EntityId, EntityId>,
100 root_view_id: EntityId,
101 ctx: &mut ViewContext<Self>,
102 ) -> Self {
103 let mut view_children_map: HashMap<EntityId, Vec<EntityId>> = Default::default();
104 for (child, parent) in view_parent_map.into_iter() {
105 view_children_map.entry(parent).or_default().push(child);
106 }
107 
108 let mut view_list: Vec<(EntityId, usize)> = vec![];
109 populate_view_list(&view_children_map, root_view_id, 0, &mut view_list);
110 
111 let view_info = view_list
112 .into_iter()
113 .map(|(view_id, view_depth)| ViewInfo {
114 view_id,
115 view_depth,
116 mouse_state_handle: MouseStateHandle::default(),
117 })
118 .collect_vec();
119 
120 Self {
121 handle: ctx.handle(),
122 target_window_id,
123 view_info,
124 uniform_list_state: Default::default(),
125 scroll_state_handle: Default::default(),
126 }
127 }
128}
129 
130impl Entity for ViewTreeDebugView {
131 type Event = ();
132}
133 
134impl View for ViewTreeDebugView {
135 fn ui_name() -> &'static str {
136 "ViewTreeDebugView"
137 }
138 
139 fn render(&self, _app: &AppContext) -> Box<dyn Element> {
140 let handle = self.handle.clone();
141 let window_id = self.target_window_id;
142 
143 let list = UniformList::new(
144 self.uniform_list_state.clone(),
145 self.view_info.len(),
146 move |range, ctx| {
147 handle
148 .upgrade(ctx)
149 .into_iter()
150 .flat_map(|handle| {
151 handle
152 .as_ref(ctx)
153 .view_info
154 .iter()
155 .skip(range.start)
156 .take(range.len())
157 .cloned()
158 })
159 .map(|view_info| view_info.render(window_id, ctx))
160 .collect_vec()
161 .into_iter()
162 },
163 );
164 
165 let scrollable = Scrollable::vertical(
166 self.scroll_state_handle.clone(),
167 list.finish_scrollable(),
168 ScrollbarWidth::Auto,
169 Fill::Solid(ColorU::new(255, 255, 255, 50)),
170 Fill::Solid(ColorU::new(255, 255, 255, 150)),
171 Fill::Solid(ColorU::black()),
172 );
173 
174 let view_content = Align::new(
175 Container::new(scrollable.finish())
176 .with_uniform_padding(8.)
177 .finish(),
178 )
179 .top_left();
180 
181 Container::new(view_content.finish())
182 .with_background_color(ColorU::black())
183 .with_padding_top(25.)
184 .finish()
185 }
186}
187 
188impl TypedActionView for ViewTreeDebugView {
189 type Action = ViewTreeDebugAction;
190 
191 fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
192 match action {
193 ViewTreeDebugAction::HighlightView(window_id, view_id) => {
194 if let Some(presenter) = ctx.presenter(*window_id) {
195 presenter
196 .as_ref()
197 .borrow_mut()
198 .set_highlighted_view(*view_id);
199 }
200 // This is a hacky way to get the other window to redraw, but it
201 // works. :)
202 ctx.invalidate_all_views();
203 }
204 }
205 }
206}
207