Skip to main content

omp_tui/components/
custom.rs

1use std::mem;
2
3use omp_core::Str;
4
5use super::layout::{stack_height, stack_measure, stack_place};
6use crate::{
7	component::{
8		Cached, Component, EventCtx, Flow, HitTag, IntoChildren, PaintCtx, Slot, next_slot,
9	},
10	context::UiContext,
11	frame::Rect,
12	input::{Key, Mouse},
13	markup::Align,
14	props::{Prop, PropValue, Props},
15};
16
17/// A late-bound component backing a registry-defined markup tag.
18pub struct CustomElement {
19	props:    Props,
20	slot:     Slot,
21	name:     Str,
22	children: Vec<Cached>,
23	resolved: Option<Cached>,
24	tried:    bool,
25}
26
27impl CustomElement {
28	/// Creates an unresolved custom element for the supplied tag name.
29	pub fn new(name: impl Into<Str>) -> Self {
30		Self {
31			props:    Props::new(),
32			slot:     next_slot(),
33			name:     name.into(),
34			children: Vec::new(),
35			resolved: None,
36			tried:    false,
37		}
38	}
39
40	/// Sets one custom-element property.
41	pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
42		self.props.set(prop, value);
43		self
44	}
45
46	/// Sets one custom-element property from a string.
47	pub fn with_str(self, prop: Prop, value: &str) -> Self {
48		self.with(prop, value)
49	}
50
51	/// Appends fallback or factory-provided child content.
52	pub fn child(mut self, children: impl IntoChildren) -> Self {
53		children.extend_children(&mut self.children);
54		self
55	}
56
57	fn resolve(&mut self, ctx: &UiContext) {
58		if self.tried {
59			return;
60		}
61		self.tried = true;
62		let Some(factory) = ctx.elements.get(&self.name) else {
63			return;
64		};
65		let component =
66			factory.build(&self.name, mem::take(&mut self.props), mem::take(&mut self.children));
67		self.resolved = Some(Cached::new(component));
68	}
69}
70
71impl Component for CustomElement {
72	fn props(&self) -> &Props {
73		self
74			.resolved
75			.as_ref()
76			.map_or(&self.props, |resolved| resolved.comp().props())
77	}
78
79	fn props_mut(&mut self) -> &mut Props {
80		self
81			.resolved
82			.as_mut()
83			.map_or(&mut self.props, |resolved| resolved.comp_mut().props_mut())
84	}
85
86	fn slot(&self) -> Slot {
87		self.slot
88	}
89
90	fn children(&self) -> &[Cached] {
91		if let Some(resolved) = &self.resolved {
92			std::slice::from_ref(resolved)
93		} else {
94			&self.children
95		}
96	}
97
98	fn children_mut(&mut self) -> &mut [Cached] {
99		if let Some(resolved) = &mut self.resolved {
100			std::slice::from_mut(resolved)
101		} else {
102			&mut self.children
103		}
104	}
105
106	fn measure(&mut self, ctx: &UiContext) -> (u16, u16) {
107		self.resolve(ctx);
108		if let Some(resolved) = &mut self.resolved {
109			resolved.comp_mut().measure(ctx)
110		} else {
111			stack_measure(ctx, &mut self.children)
112		}
113	}
114
115	fn height(&mut self, ctx: &UiContext, width: u16) -> u16 {
116		self.resolve(ctx);
117		if let Some(resolved) = &mut self.resolved {
118			resolved.comp_mut().height(ctx, width)
119		} else {
120			stack_height(ctx, &mut self.children, width, 0)
121		}
122	}
123
124	fn place(&mut self, ctx: &UiContext, content: Rect) {
125		self.resolve(ctx);
126		if let Some(resolved) = &mut self.resolved {
127			resolved.rect = content;
128			resolved.comp_mut().place(ctx, content);
129		} else {
130			stack_place(ctx, &mut self.children, content, 0, None, Align::Start);
131		}
132	}
133
134	fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
135		self.resolve(pc.ctx);
136		if let Some(resolved) = &mut self.resolved {
137			resolved.rect = rect;
138			resolved.comp_mut().paint(pc, rect);
139		} else {
140			for child in self.children.iter_mut().filter(|child| child.visible) {
141				child.paint(pc);
142			}
143		}
144	}
145
146	fn focusable(&self) -> bool {
147		self
148			.resolved
149			.as_ref()
150			.is_some_and(|resolved| resolved.comp().focusable())
151	}
152
153	fn enter(&mut self, forward: bool) {
154		if let Some(resolved) = &mut self.resolved {
155			resolved.comp_mut().enter(forward);
156		}
157	}
158
159	fn ring(&self, out: &mut Vec<Slot>) {
160		if let Some(resolved) = &self.resolved {
161			resolved.comp().ring(out);
162		} else {
163			for child in self.children.iter().filter(|child| child.visible) {
164				child.comp().ring(out);
165			}
166		}
167	}
168
169	fn key(&mut self, ec: &mut EventCtx<'_>, key: Key) -> Flow {
170		self.resolve(ec.ctx);
171		self
172			.resolved
173			.as_mut()
174			.map_or(Flow::Skip, |resolved| resolved.comp_mut().key(ec, key))
175	}
176
177	fn mouse(
178		&mut self,
179		ec: &mut EventCtx<'_>,
180		tag: HitTag,
181		at: (u16, u16),
182		rect: Rect,
183		mouse: Mouse,
184	) -> Flow {
185		self.resolve(ec.ctx);
186		self
187			.resolved
188			.as_mut()
189			.map_or(Flow::Skip, |resolved| resolved.comp_mut().mouse(ec, tag, at, rect, mouse))
190	}
191
192	fn paste(&mut self, ec: &mut EventCtx<'_>, text: &str) -> Flow {
193		self.resolve(ec.ctx);
194		self
195			.resolved
196			.as_mut()
197			.map_or(Flow::Skip, |resolved| resolved.comp_mut().paste(ec, text))
198	}
199
200	fn paste_raw(&mut self, ec: &mut EventCtx<'_>, text: &str) -> Flow {
201		self.resolve(ec.ctx);
202		self
203			.resolved
204			.as_mut()
205			.map_or(Flow::Skip, |resolved| resolved.comp_mut().paste_raw(ec, text))
206	}
207
208	fn value(&self, out: &mut serde_json::Map<String, serde_json::Value>) {
209		if let Some(resolved) = &self.resolved {
210			resolved.comp().value(out);
211		}
212	}
213
214	fn set_text(&mut self, ctx: &UiContext, text: Str) -> bool {
215		self.resolve(ctx);
216		self
217			.resolved
218			.as_mut()
219			.is_some_and(|resolved| resolved.comp_mut().set_text(ctx, text))
220	}
221}
222
223#[cfg(test)]
224mod tests {
225	use super::*;
226	use crate::{
227		component::{Elements, IntoComponent},
228		components::TextLeaf,
229		frame::{Frame, Size},
230		test_support::frame_row_text,
231	};
232
233	fn paint(root: &mut Cached, ctx: &UiContext, width: u16, height: u16) -> Frame {
234		root.place(ctx, Rect::new(0, 0, width, height));
235		let mut frame = Frame::new(Size::new(width, height));
236		let mut hits = Vec::new();
237		root.paint(&mut PaintCtx::new(&mut frame, ctx, &mut hits, &mut Vec::new()));
238		frame
239	}
240
241	#[test]
242	fn registered_factory_resolves_and_paints() {
243		let ctx = UiContext {
244			elements: Elements::builder()
245				.with("badge", |_name: &str, _props: Props, _children: Vec<Cached>| {
246					TextLeaf::new().text("resolved").into_component()
247				})
248				.build(),
249			..UiContext::default()
250		};
251		let mut root = Cached::new(CustomElement::new("badge").into_component());
252		let height = root.height(&ctx, 12);
253		assert_eq!(height, 1);
254		assert_eq!(root.comp().children().len(), 1);
255		let frame = paint(&mut root, &ctx, 12, height);
256		assert_eq!(frame_row_text(&frame, 0), "resolved");
257	}
258
259	#[test]
260	fn unregistered_element_is_a_zero_gap_div() {
261		let ctx = UiContext::default();
262		let mut root = Cached::new(
263			CustomElement::new("unknown")
264				.child(TextLeaf::new().text("one"))
265				.child(TextLeaf::new().text("two"))
266				.into_component(),
267		);
268		let height = root.height(&ctx, 8);
269		assert_eq!(height, 2);
270		let frame = paint(&mut root, &ctx, 8, height);
271		assert_eq!(frame_row_text(&frame, 0), "one");
272		assert_eq!(frame_row_text(&frame, 1), "two");
273
274		let mut empty = CustomElement::new("empty");
275		assert_eq!(empty.height(&ctx, 8), 0);
276	}
277}