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
17pub 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 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 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
42 self.props.set(prop, value);
43 self
44 }
45
46 pub fn with_str(self, prop: Prop, value: &str) -> Self {
48 self.with(prop, value)
49 }
50
51 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}