1use serde::{Deserialize, Serialize};
9
10use crate::runtime::resize::NodeResizeConstraints;
11use crate::schema::{
12 NodeChromeDescriptor, NodeChromeKind, NodeChromePlacement, NodeChromeVisibility,
13};
14use jellyflow_core::core::{CanvasPoint, CanvasRect, CanvasSize, NodeId};
15
16const DEFAULT_CONTROL_SIZE: f32 = 16.0;
17const DEFAULT_BAR_HEIGHT: f32 = 28.0;
18const DEFAULT_MARGIN: f32 = 8.0;
19const MIN_ZOOM_SCALE: f32 = 0.25;
20
21fn default_zoom() -> f32 {
22 1.0
23}
24
25fn is_false(value: &bool) -> bool {
26 !*value
27}
28
29#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub struct NodeChromeState {
32 #[serde(default, skip_serializing_if = "is_false")]
33 pub selected: bool,
34 #[serde(default, skip_serializing_if = "is_false")]
35 pub hovered: bool,
36 #[serde(default, skip_serializing_if = "is_false")]
37 pub focused: bool,
38}
39
40impl NodeChromeState {
41 pub fn selected() -> Self {
42 Self {
43 selected: true,
44 hovered: false,
45 focused: false,
46 }
47 }
48
49 pub fn hovered() -> Self {
50 Self {
51 selected: false,
52 hovered: true,
53 focused: false,
54 }
55 }
56
57 pub fn focused() -> Self {
58 Self {
59 selected: false,
60 hovered: false,
61 focused: true,
62 }
63 }
64
65 pub fn is_visible(self, descriptor: &NodeChromeDescriptor) -> bool {
66 descriptor.is_visible_for_state(self.selected, self.hovered, self.focused)
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
72pub struct NodeChromeLayoutPolicy {
73 pub control_size: f32,
74 pub bar_height: f32,
75 pub margin: f32,
76 #[serde(default = "default_zoom")]
77 pub zoom: f32,
78}
79
80impl Default for NodeChromeLayoutPolicy {
81 fn default() -> Self {
82 Self {
83 control_size: DEFAULT_CONTROL_SIZE,
84 bar_height: DEFAULT_BAR_HEIGHT,
85 margin: DEFAULT_MARGIN,
86 zoom: 1.0,
87 }
88 }
89}
90
91impl NodeChromeLayoutPolicy {
92 pub fn with_control_size(mut self, control_size: f32) -> Self {
93 self.control_size = control_size;
94 self
95 }
96
97 pub fn with_bar_height(mut self, bar_height: f32) -> Self {
98 self.bar_height = bar_height;
99 self
100 }
101
102 pub fn with_margin(mut self, margin: f32) -> Self {
103 self.margin = margin;
104 self
105 }
106
107 pub fn with_zoom(mut self, zoom: f32) -> Self {
108 self.zoom = zoom;
109 self
110 }
111
112 fn scale(self) -> f32 {
113 if self.zoom.is_finite() && self.zoom > 0.0 {
114 (1.0 / self.zoom).max(MIN_ZOOM_SCALE)
115 } else {
116 1.0
117 }
118 }
119
120 fn normalized(self) -> Self {
121 let scale = self.scale();
122 Self {
123 control_size: positive_or_default(self.control_size, DEFAULT_CONTROL_SIZE) * scale,
124 bar_height: positive_or_default(self.bar_height, DEFAULT_BAR_HEIGHT) * scale,
125 margin: positive_or_default(self.margin, DEFAULT_MARGIN) * scale,
126 zoom: self.zoom,
127 }
128 }
129}
130
131#[derive(Debug, Clone, PartialEq)]
133pub struct NodeChromeFactsRequest<'a> {
134 pub node: NodeId,
135 pub node_rect: CanvasRect,
136 pub descriptors: &'a [NodeChromeDescriptor],
137 pub state: NodeChromeState,
138 pub policy: NodeChromeLayoutPolicy,
139 pub resize_constraints: NodeResizeConstraints,
140}
141
142impl<'a> NodeChromeFactsRequest<'a> {
143 pub fn new(
144 node: NodeId,
145 node_rect: CanvasRect,
146 descriptors: &'a [NodeChromeDescriptor],
147 ) -> Self {
148 Self {
149 node,
150 node_rect,
151 descriptors,
152 state: NodeChromeState::default(),
153 policy: NodeChromeLayoutPolicy::default(),
154 resize_constraints: NodeResizeConstraints::default(),
155 }
156 }
157
158 pub fn with_state(mut self, state: NodeChromeState) -> Self {
159 self.state = state;
160 self
161 }
162
163 pub fn with_policy(mut self, policy: NodeChromeLayoutPolicy) -> Self {
164 self.policy = policy;
165 self
166 }
167
168 pub fn with_resize_constraints(mut self, constraints: NodeResizeConstraints) -> Self {
169 self.resize_constraints = constraints;
170 self
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct NodeChromeFacts {
177 pub node: NodeId,
178 pub chrome: Vec<ResolvedNodeChrome>,
179}
180
181impl NodeChromeFacts {
182 pub fn is_empty(&self) -> bool {
183 self.chrome.is_empty()
184 }
185
186 pub fn get(&self, key: impl AsRef<str>) -> Option<&ResolvedNodeChrome> {
187 let key = key.as_ref();
188 self.chrome.iter().find(|chrome| chrome.key == key)
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct ResolvedNodeChrome {
195 pub key: String,
196 pub kind: NodeChromeKind,
197 pub placement: NodeChromePlacement,
198 pub rect: CanvasRect,
199 pub visibility: NodeChromeVisibility,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub label: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub renderer_key: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub icon_key: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub order: Option<i32>,
208 #[serde(default, skip_serializing_if = "is_false")]
209 pub interactive: bool,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub resize_constraints: Option<NodeResizeConstraints>,
212}
213
214impl ResolvedNodeChrome {
215 pub fn contains_point(&self, point: CanvasPoint) -> bool {
216 point.x >= self.rect.origin.x
217 && point.x <= self.rect.origin.x + self.rect.size.width
218 && point.y >= self.rect.origin.y
219 && point.y <= self.rect.origin.y + self.rect.size.height
220 }
221}
222
223pub fn resolve_node_chrome_facts(request: NodeChromeFactsRequest<'_>) -> Option<NodeChromeFacts> {
225 if !request.node_rect.is_positive_finite() {
226 return None;
227 }
228
229 let policy = request.policy.normalized();
230 let mut chrome: Vec<_> = request
231 .descriptors
232 .iter()
233 .filter(|descriptor| request.state.is_visible(descriptor))
234 .filter_map(|descriptor| {
235 let rect = chrome_rect(
236 request.node_rect,
237 descriptor.kind,
238 descriptor.placement,
239 policy,
240 )?;
241 Some(ResolvedNodeChrome {
242 key: descriptor.key.clone(),
243 kind: descriptor.kind,
244 placement: descriptor.placement,
245 rect,
246 visibility: descriptor.effective_visibility(),
247 label: descriptor.label.clone(),
248 renderer_key: descriptor.renderer_key.clone(),
249 icon_key: descriptor.icon_key.clone(),
250 order: descriptor.order,
251 interactive: descriptor.interactive,
252 resize_constraints: (descriptor.kind == NodeChromeKind::Resizer)
253 .then_some(request.resize_constraints),
254 })
255 })
256 .collect();
257
258 chrome.sort_by(|left, right| {
259 left.order
260 .unwrap_or(0)
261 .cmp(&right.order.unwrap_or(0))
262 .then_with(|| left.key.cmp(&right.key))
263 });
264
265 Some(NodeChromeFacts {
266 node: request.node,
267 chrome,
268 })
269}
270
271fn chrome_rect(
272 node_rect: CanvasRect,
273 kind: NodeChromeKind,
274 placement: NodeChromePlacement,
275 policy: NodeChromeLayoutPolicy,
276) -> Option<CanvasRect> {
277 let size = chrome_size(node_rect, kind, placement, policy)?;
278 let margin = policy.margin;
279 let left = node_rect.origin.x;
280 let top = node_rect.origin.y;
281 let right = left + node_rect.size.width;
282 let bottom = top + node_rect.size.height;
283
284 let origin = match placement {
285 NodeChromePlacement::Top => CanvasPoint {
286 x: left + (node_rect.size.width - size.width) / 2.0,
287 y: top - margin - size.height,
288 },
289 NodeChromePlacement::TopRight => CanvasPoint {
290 x: right - size.width,
291 y: top - margin - size.height,
292 },
293 NodeChromePlacement::Right => CanvasPoint {
294 x: right + margin,
295 y: top + (node_rect.size.height - size.height) / 2.0,
296 },
297 NodeChromePlacement::BottomRight => CanvasPoint {
298 x: right - size.width,
299 y: bottom + margin,
300 },
301 NodeChromePlacement::Bottom => CanvasPoint {
302 x: left + (node_rect.size.width - size.width) / 2.0,
303 y: bottom + margin,
304 },
305 NodeChromePlacement::BottomLeft => CanvasPoint {
306 x: left,
307 y: bottom + margin,
308 },
309 NodeChromePlacement::Left => CanvasPoint {
310 x: left - margin - size.width,
311 y: top + (node_rect.size.height - size.height) / 2.0,
312 },
313 NodeChromePlacement::TopLeft => CanvasPoint {
314 x: left,
315 y: top - margin - size.height,
316 },
317 NodeChromePlacement::InsideHeader => CanvasPoint {
318 x: left + margin,
319 y: top + margin,
320 },
321 NodeChromePlacement::InsideFooter => CanvasPoint {
322 x: left + margin,
323 y: bottom - margin - size.height,
324 },
325 };
326
327 let rect = CanvasRect { origin, size };
328 rect.is_positive_finite().then_some(rect)
329}
330
331fn chrome_size(
332 node_rect: CanvasRect,
333 kind: NodeChromeKind,
334 placement: NodeChromePlacement,
335 policy: NodeChromeLayoutPolicy,
336) -> Option<CanvasSize> {
337 let inline_width = (node_rect.size.width - policy.margin * 2.0).max(policy.control_size);
338 let size = match kind {
339 NodeChromeKind::Resizer | NodeChromeKind::InspectorAnchor => CanvasSize {
340 width: policy.control_size,
341 height: policy.control_size,
342 },
343 NodeChromeKind::Toolbar => {
344 let width = match placement {
345 NodeChromePlacement::Left | NodeChromePlacement::Right => policy.control_size,
346 _ => node_rect
347 .size
348 .width
349 .min(160.0 * policy.scale())
350 .max(64.0 * policy.scale()),
351 };
352 CanvasSize {
353 width,
354 height: policy.bar_height,
355 }
356 }
357 NodeChromeKind::StatusStrip
358 | NodeChromeKind::ValidationBanner
359 | NodeChromeKind::RunActionStrip => CanvasSize {
360 width: inline_width,
361 height: policy.bar_height,
362 },
363 };
364 size.is_positive_finite().then_some(size)
365}
366
367fn positive_or_default(value: f32, default: f32) -> f32 {
368 if value.is_finite() && value > 0.0 {
369 value
370 } else {
371 default
372 }
373}