GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/main.rs (24.7K)
1 //! The reference `Application` (Phase 6ad) — a small widget gallery on the target
2 //! architecture, end to end. This is the file to copy when starting a new `cce-*` client.
3 //!
4 //! The shape every migrated app shares:
5 //!
6 //! 1. **One paint path.** The whole frame — geometry AND text — is built in
7 //! [`Application::display_list`] as prims on a [`PaintCtx`], with
8 //! [`Application::display_list_text`] returning `true`. There is no `view*`/`text_items`
9 //! pair, no app-side `FontSystem`, no cosmic-text buffers: text is a `Prim::Text` shaped by
10 //! the engine's shared cache.
11 //! 2. **Layout via the scene solver.** The frame is a plain `Arena<LayoutBox>` tree the
12 //! app builds and solves with [`compute_layout`]; widgets get their rects from the
13 //! solved leaves. No container widgets, no hand-summed offsets.
14 //! 3. **Routed events.** Each input handler builds one [`Event`] and routes it through
15 //! `UiContext::propagate_event` per widget root. The router owns press hit-gating,
16 //! Enter/Leave synthesis, drag-target recording, and KeyInput-to-focused delivery;
17 //! the app keeps only state-gated `take_*` plumbing.
18 //! 4. **No embedded bases.** Widgets are app-owned values (all [`Adapted`]); the window
19 //! plate is prims, not a root plate container; popovers draw INTO the frame (there is no popup
20 //! surface); app state — not any widget tree — is the source of truth.
21
22 use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
23 use cce_ui::scene::arena::Arena;
24 use cce_ui::scene::layout::{
25 compute_layout, FitMode, LayoutBox, Length, Rect, Size as LSize, Style,
26 };
27 use cce_ui::scene::paint::{DisplayList, PaintCtx};
28 use cce_ui::widget::{
29 Adapted, Button, Dropdown, ImageView, WidgetHost, WidgetId, ElementState, Event, KeyEvent,
30 MouseButton, MouseScrollDelta, Slider, TextBox, Toggle,
31 };
32 use wayland_client::QueueHandle;
33
34 #[derive(Debug, Clone)]
35 enum DemoMessage {
36 Exit,
37 }
38
39 /// Chrome typography: the title/status font sizes, and the layout leaves that
40 /// hold them derived as one line-height (×1.2, the toolkit convention) — so the
41 /// header and status bands, which anchor to those solved rects, resize with the
42 /// typography instead of relying on magic leaf heights.
43 const TITLE_FONT_SIZE: f32 = 15.0;
44 const STATUS_FONT_SIZE: f32 = 12.0;
45 /// Vertical padding on each side of the status band's text line.
46 const STATUS_BAND_PAD: f32 = 5.0;
47 fn text_leaf_height(font_size: f32) -> f32 {
48 (font_size * 1.2).ceil()
49 }
50
51 struct DemoApp {
52 // ── Widgets: app-owned values on the narrow-trait adapter. Their addresses must be
53 // stable across frames (plain struct fields, not Vec elements): the UiContext
54 // registry and the router's drag-target bookkeeping hold pointers to them.
55 button: Adapted<Button>,
56 toggle: Adapted<Toggle>,
57 slider: Adapted<Slider>,
58 name_box: Adapted<TextBox>,
59 theme_dropdown: Adapted<Dropdown>,
60 // ImageView pair sharing ONE uploaded texture (the widget borrows ids —
61 // upload/free stay app-side): Contain letterboxes, Stretch fills.
62 image_contain: Adapted<ImageView>,
63 image_stretch: Adapted<ImageView>,
64
65 // ── App state: the source of truth. Widgets are re-asserted from it every rebuild
66 // (`set_toggled` below); `take_*` changes flow back into it, never the reverse.
67 toggle_on: bool,
68 clicks: u32,
69 status: String,
70
71 ui_context: cce_ui::context::UiContext,
72 width: u32,
73 height: u32,
74 scale_factor: f64,
75 needs_rebuild: bool,
76 widgets_registered: bool,
77 title_rect: Rect,
78 status_rect: Rect,
79 }
80
81 impl DemoApp {
82 /// The widget root ids, in paint order — what the router dispatches over.
83 /// `propagate_event` takes a `WidgetId` and resolves it through the registry, so the
84 /// event paths need no raw pointers and no unsafe self-alias.
85 fn root_ids(&self) -> [WidgetId; 7] {
86 [
87 self.button.id(),
88 self.toggle.id(),
89 self.slider.id(),
90 self.name_box.id(),
91 self.theme_dropdown.id(),
92 self.image_contain.id(),
93 self.image_stretch.id(),
94 ]
95 }
96
97 /// The widget roots as pointers, for the one genuinely pointer-consuming path left:
98 /// registration (the registry stores them). The paint walk takes shared borrows.
99 fn roots(&mut self) -> [*mut (dyn WidgetHost + 'static); 7] {
100 [
101 self.button.as_ptr_mut(),
102 self.toggle.as_ptr_mut(),
103 self.slider.as_ptr_mut(),
104 self.name_box.as_ptr_mut(),
105 self.theme_dropdown.as_ptr_mut(),
106 self.image_contain.as_ptr_mut(),
107 self.image_stretch.as_ptr_mut(),
108 ]
109 }
110
111 /// `take_*` plumbing: translate widget changes into app state. Runs after any routed
112 /// dispatch; every check is STATE-gated, so it does not matter which propagate call
113 /// consumed the event (see the KeyInput note in `handle_key_input`).
114 fn drain_widget_changes(&mut self) {
115 if self.button.take_click() {
116 self.clicks += 1;
117 self.status = format!("Button clicked {} time(s)", self.clicks);
118 self.needs_rebuild = true;
119 }
120 if self.toggle.take_change() {
121 self.toggle_on = !self.toggle_on;
122 self.status = format!("Toggle: {}", if self.toggle_on { "on" } else { "off" });
123 self.needs_rebuild = true;
124 }
125 if self.slider.take_change() {
126 self.status = format!("Slider: {:.0}", self.slider.get_scaled_value());
127 self.needs_rebuild = true;
128 }
129 if self.theme_dropdown.take_change() {
130 let idx = self.theme_dropdown.selected;
131 if let Some(opt) = self.theme_dropdown.options.get(idx) {
132 self.status = format!("Theme: {opt}");
133 }
134 self.needs_rebuild = true;
135 }
136 if self.name_box.take_change() {
137 self.status = format!("Name: {}", self.name_box.text);
138 self.needs_rebuild = true;
139 }
140 }
141 }
142
143 impl Application for DemoApp {
144 type Message = DemoMessage;
145
146 fn new(
147 _qh: &QueueHandle<EngineState<Self>>,
148 _sender: calloop::channel::Sender<Self::Message>,
149 ) -> Self {
150 cce_ui::scale::set_scale_factor(1.0);
151 // One procedurally generated gradient (no asset dependency), uploaded
152 // once and SHARED by both ImageViews — the widget borrows ids;
153 // upload/free stay app-side. upload_rgba queues into the renderer's
154 // pending list, so calling it before the first frame is safe.
155 const GRADIENT_W: u32 = 64;
156 const GRADIENT_H: u32 = 40;
157 let mut gradient = Vec::with_capacity((GRADIENT_W * GRADIENT_H * 4) as usize);
158 for y in 0..GRADIENT_H {
159 for x in 0..GRADIENT_W {
160 gradient.push((x * 255 / (GRADIENT_W - 1)) as u8);
161 gradient.push((y * 255 / (GRADIENT_H - 1)) as u8);
162 gradient.push(160);
163 gradient.push(255);
164 }
165 }
166 let gradient_id = cce_ui::vk::upload_rgba(gradient, GRADIENT_W, GRADIENT_H);
167 Self {
168 // Relief styling (raised buttons/toggles/dropdowns, recessed
169 // wells) is the `control_relief` config default — no opt-in.
170 button: Button::new(0.0, 0.0, 0.0, 0.0).with_label("Click me"),
171 toggle: Toggle::new(),
172 // Slider `value` is NORMALIZED 0..1; `with_range` only scales the readout
173 // (`get_scaled_value`). Wheel nudging is an explicit opt-in.
174 slider: Slider::new()
175 .with_range(0.0, 100.0)
176 .with_value(0.4)
177 .with_scroll(true),
178 name_box: TextBox::new(String::new())
179 .with_placeholder("Type a name..."),
180 theme_dropdown: Dropdown::new(
181 vec!["Forest".into(), "Ocean".into(), "Ember".into()],
182 0,
183 ),
184 image_contain: ImageView::new()
185 .with_image(gradient_id, GRADIENT_W, GRADIENT_H)
186 .with_fit(FitMode::Contain { max_upscale: 4.0 })
187 .with_bg([0.10, 0.10, 0.16, 1.0]),
188 image_stretch: ImageView::new()
189 .with_image(gradient_id, GRADIENT_W, GRADIENT_H)
190 .with_fit(FitMode::Stretch),
191 toggle_on: false,
192 clicks: 0,
193 status: "Ready.".to_string(),
194 ui_context: cce_ui::context::UiContext::new(),
195 width: 560,
196 height: 420,
197 scale_factor: 1.0,
198 needs_rebuild: true,
199 widgets_registered: false,
200 title_rect: Rect::ZERO,
201 status_rect: Rect::ZERO,
202 }
203 }
204
205 fn settings(&self) -> WindowSettings {
206 WindowSettings {
207 title: "cce-ui reference gallery".to_string(),
208 app_id: "cce-ui-demo".to_string(),
209 width: 560,
210 height: 420,
211 fullscreen: false,
212 min_size: Some((360, 300)),
213 }
214 }
215
216 fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
217 match msg {
218 DemoMessage::Exit => *exit = true,
219 }
220 }
221
222 /// Widget animations (cursor blink, hover fades) tick through the UiContext; the
223 /// loop is demand-driven, so returning a redraw request only when something moved
224 /// keeps the app idle otherwise.
225 fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
226 if self.ui_context.tick(dt) {
227 *needs_rebuild = true;
228 self.needs_rebuild = true;
229 }
230 }
231
232 fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<DisplayList> {
233 // Register once: the registry backs the router (drag targets are looked up by
234 // widget id) and drag_allowed_at. Pointers into `self` are stable only once
235 // `self` sits at its final address — hence here, not in `new()`.
236 if !self.widgets_registered {
237 self.widgets_registered = true;
238 let self_ptr = self as *mut Self;
239 unsafe {
240 for w in (*self_ptr).roots() {
241 let id = (*w).base().id();
242 self.ui_context.register_widget(id, w);
243 }
244 }
245 }
246
247 let size_changed = self.width != size.width as u32
248 || self.height != size.height as u32
249 || self.scale_factor != scale;
250 if self.needs_rebuild || size_changed {
251 self.width = size.width as u32;
252 self.height = size.height as u32;
253 self.scale_factor = scale;
254 cce_ui::scale::set_scale_factor(scale as f32);
255
256 // Re-assert widget visuals from app state (the app is the source of truth).
257 self.toggle.set_toggled(self.toggle_on);
258 self.toggle.set_label(if self.toggle_on { "ON" } else { "OFF" });
259
260 // ── Layout: a plain LayoutBox tree, solved in one call. Leaves carry their
261 // intrinsic sizes; `grow` distributes leftover space; the solved rects are
262 // assigned straight onto the widgets.
263 // DE-wide spacing by rung, never by number: the root preset insets
264 // by the plate's roll plus one padding and spaces siblings by the
265 // root gap; the controls preset puts the control gap between a
266 // form's controls (`Style::root_column` / `Style::controls_row`).
267 let mut arena: Arena<LayoutBox> = Arena::new();
268 let root = arena.insert(LayoutBox::container(Style::root_column()));
269 let title = arena.insert(LayoutBox::leaf(
270 Style::row(),
271 LSize::new(0.0, text_leaf_height(TITLE_FONT_SIZE)),
272 ));
273 // Clearance under the header band: the recess step rolls over `bevel_width`
274 // past the band's bottom edge, so the first content row must stand off by at
275 // least that or it crowds the carve.
276 let band_gap = arena.insert(LayoutBox::leaf(
277 Style::row(),
278 LSize::new(0.0, cce_ui::layout::bevel_width()),
279 ));
280 // One shared height for the whole control row, so the button,
281 // toggle, and dropdown plates land on the same top and bottom edge.
282 const CONTROL_H: f32 = 28.0;
283 let controls = arena.insert(LayoutBox::container(
284 Style::controls_row().height(Length::Fixed(CONTROL_H)),
285 ));
286 // `shrink` lets the fixed leaves give up width when the window is at its
287 // minimum instead of overflowing the row.
288 let button = arena.insert(LayoutBox::leaf(Style::row().shrink(1.0), LSize::new(120.0, CONTROL_H)));
289 let toggle = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(64.0, CONTROL_H)));
290 let dropdown = arena.insert(LayoutBox::leaf(Style::row().shrink(1.0), LSize::new(150.0, CONTROL_H)));
291 let slider = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 24.0)));
292 let name_box = arena.insert(LayoutBox::leaf(Style::row(), LSize::new(0.0, 30.0)));
293 // ImageView row: same texture through two fit modes side by side.
294 let images = arena.insert(LayoutBox::container(
295 Style::row().gap(cce_ui::layout::root_plate_gap()).height(Length::Fixed(72.0)),
296 ));
297 let image_contain = arena.insert(LayoutBox::leaf(Style::row().grow(1.0), LSize::new(0.0, 72.0)));
298 let image_stretch = arena.insert(LayoutBox::leaf(Style::row().grow(1.0), LSize::new(0.0, 72.0)));
299 let spacer = arena.insert(LayoutBox::container(Style::column().grow(1.0)));
300 let status = arena.insert(LayoutBox::leaf(
301 Style::row(),
302 LSize::new(0.0, text_leaf_height(STATUS_FONT_SIZE)),
303 ));
304 arena.append_child(root, title);
305 arena.append_child(root, band_gap);
306 arena.append_child(root, controls);
307 arena.append_child(controls, button);
308 arena.append_child(controls, toggle);
309 arena.append_child(controls, dropdown);
310 arena.append_child(root, slider);
311 arena.append_child(root, name_box);
312 arena.append_child(root, images);
313 arena.append_child(images, image_contain);
314 arena.append_child(images, image_stretch);
315 arena.append_child(root, spacer);
316 arena.append_child(root, status);
317 compute_layout(
318 &mut arena,
319 root,
320 LSize::new(self.width as f32, self.height as f32),
321 );
322
323 // Stretched children fill the column width; fixed leaves keep their size.
324 let r = |id| arena.value(id).unwrap().rect;
325 let b = r(button);
326 self.button.set_rect(b.x, b.y, b.width, b.height);
327 let t = r(toggle);
328 self.toggle.set_rect(t.x, t.y, t.width, t.height);
329 let d = r(dropdown);
330 self.theme_dropdown.set_rect(d.x, d.y, d.width, d.height);
331 let s = r(slider);
332 self.slider.set_rect(s.x, s.y, s.width, s.height);
333 let n = r(name_box);
334 self.name_box.set_rect(n.x, n.y, n.width, n.height);
335 let ic = r(image_contain);
336 self.image_contain.set_rect(ic.x, ic.y, ic.width, ic.height);
337 let is = r(image_stretch);
338 self.image_stretch.set_rect(is.x, is.y, is.width, is.height);
339 self.title_rect = r(title);
340 self.status_rect = r(status);
341
342 self.needs_rebuild = false;
343 self.ui_context.rebuild_spatial_grid();
344 }
345
346 // ── Popover registration: ui_context ONLY. It drives the engine's display-list
347 // text occlusion clamp (labels under the open popover get clipped); the popover
348 // itself is drawn into this frame below — there is no popup surface.
349 self.ui_context.clear_popovers();
350 if self.theme_dropdown.popover_rect().is_some() {
351 self.ui_context.register_popover(&mut self.theme_dropdown);
352 }
353
354 let mut pc = PaintCtx::new();
355 let w = self.width as f32;
356 let h = self.height as f32;
357
358 // The standard root plate (`PlateSpec::window`): the DE's root
359 // material at its configured opacity, the shared silhouette arc on
360 // all four corners, the perimeter rolled over `bevel_width`. This
361 // demo is the reference app, so its base is the one every cce app
362 // should paint first.
363 pc.root_plate(w, h);
364
365 // Header band: the title strip carved one step down into the plate. Flush to the
366 // window's top and sides, so its only real wall is the bottom one facing the
367 // content (the recessed-MenuBar idiom — the other three would fight the plate's
368 // own rolled perimeter).
369 let band_h = self.title_rect.y + self.title_rect.height + 10.0;
370 pc.recess_edges(
371 Rect { x: 0.0, y: 0.0, width: w, height: band_h },
372 (0.0, 0.0, 0.0, 0.0),
373 cce_ui::layout::bar_wall_width(),
374 (false, false, true, false),
375 );
376
377 // Status band: the header's mirror — carved into the bottom of the
378 // plate, flush to the window's bottom and sides, its only wall the top
379 // one facing the content. (Neither band CSG-groups: edge-suppressed
380 // carves never do — their extended walls would smear across the
381 // plate's whole-surface draw. Both shade through the overlay fallback,
382 // whose host-box fade owns the junction with the roll.) Sized from
383 // the status font plus a symmetric pad (the layout's status leaf only
384 // reserves the space; the band and its text center independently).
385 let status_h = text_leaf_height(STATUS_FONT_SIZE) + 2.0 * STATUS_BAND_PAD;
386 let status_top = h - status_h;
387 pc.recess_edges(
388 Rect { x: 0.0, y: status_top, width: w, height: status_h },
389 (0.0, 0.0, 0.0, 0.0),
390 cce_ui::layout::bar_wall_width(),
391 (true, false, false, false),
392 );
393
394 // App chrome text: plain prims. `text_with` carries an optional font family and
395 // optional bounds; unbounded text is clamped to the surface by the engine.
396 pc.text_with(
397 "cce-ui reference gallery".to_string(),
398 self.title_rect.x,
399 self.title_rect.y,
400 TITLE_FONT_SIZE,
401 [0xdd, 0xdd, 0xe2],
402 Some("monospace".to_string()),
403 None,
404 );
405 pc.text_with(
406 self.status.clone(),
407 self.status_rect.x,
408 cce_ui::layout::align_text_y(status_top, status_h, STATUS_FONT_SIZE, 0.0),
409 STATUS_FONT_SIZE,
410 [0x9a, 0x9a, 0xa4],
411 // None here falls through fontconfig's unbundled sans alias to the
412 // serif fallback — always name a family.
413 Some("monospace".to_string()),
414 None,
415 );
416
417 // Widgets: each root walked through the single paint pass. The walk recurses,
418 // clips, and emits each widget's own geometry AND text (`Adapted::paint_self`
419 // serves per-widget fonts and bounds).
420 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.button, &mut pc);
421 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.toggle, &mut pc);
422 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.slider, &mut pc);
423 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.name_box, &mut pc);
424 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.image_contain, &mut pc);
425 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.image_stretch, &mut pc);
426 cce_ui::scene::painter::paint_root_into(&self.ui_context, &self.theme_dropdown, &mut pc);
427
428 // The dropdown popover — geometry and labels last, on top of everything, exactly
429 // where it hit-tests. Labels carry bounds equal to the popover rect: that clips
430 // them to the plate AND exempts them from the occlusion clamp (text whose bounds
431 // equal an overlay rect is treated as the overlay's own).
432 if self.theme_dropdown.popover_rect().is_some() {
433 // PaintCtx is a RenderTarget: the popover draws its real prims (the
434 // dropdown's expanded inset-plate surface) with its own bounds.
435 self.theme_dropdown.render_popover(&mut pc);
436 }
437
438 Some(pc.finish())
439 }
440
441 /// Text prims in the display list ARE the frame's text — no `text_items` twin.
442 fn display_list_text(&self) -> bool {
443 true
444 }
445
446 fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
447 Some(&self.ui_context)
448 }
449
450 // Engine-driven animation frames for the dropdown expand/contract.
451 fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
452 Some(&mut self.ui_context)
453 }
454
455 /// Window dragging for a dissolved root: the surface is the movable plate; drag
456 /// anywhere a drag-blocking registered widget isn't.
457 fn is_movable_root_plate_at(&self, px: f32, py: f32) -> bool {
458 self.ui_context.drag_allowed_at(px, py)
459 }
460
461 fn clear_color(&self) -> [f32; 4] {
462 [0.0, 0.0, 0.0, 0.0]
463 }
464
465 fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
466 let (px, py) = (pos.x, pos.y);
467 let ev = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
468 let mut changed = false;
469 // PointerMove visits every root: hover bookkeeping everywhere, and the
470 // router forwards DragUpdate to the recorded drag target (slider thumb,
471 // text selection) once its 3px threshold trips.
472 for root in self.root_ids() {
473 if self.ui_context.propagate_event(&ev, root) {
474 changed = true;
475 }
476 }
477 self.drain_widget_changes();
478 if changed || self.needs_rebuild {
479 *needs_rebuild = true;
480 self.needs_rebuild = true;
481 }
482 }
483
484 fn handle_mouse_input(
485 &mut self,
486 button: MouseButton,
487 state: ElementState,
488 pos: LogicalPosition,
489 needs_rebuild: &mut bool,
490 ) -> Option<Self::Message> {
491 let (px, py) = (pos.x, pos.y);
492 let ev = Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py };
493 let mut changed = false;
494 // Presses are hit-gated per widget by the adapter and releases delivered
495 // everywhere (press-tracking widgets commit or cancel on them) — a straight
496 // loop is correct for pointer-positioned events.
497 for root in self.root_ids() {
498 if self.ui_context.propagate_event(&ev, root) {
499 changed = true;
500 }
501 }
502 self.drain_widget_changes();
503 if changed || self.needs_rebuild {
504 *needs_rebuild = true;
505 self.needs_rebuild = true;
506 }
507 None
508 }
509
510 fn handle_mouse_wheel(
511 &mut self,
512 delta: &MouseScrollDelta,
513 pos: LogicalPosition,
514 needs_rebuild: &mut bool,
515 ) {
516 let (px, py) = (pos.x, pos.y);
517 let ev = Event::MouseWheel { delta: delta.clone(), x: px, y: py, local_x: px, local_y: py };
518 let mut changed = false;
519 // Wheel is hit-scoped per widget (the slider nudges its value under the
520 // cursor); roots that miss return false.
521 for root in self.root_ids() {
522 if self.ui_context.propagate_event(&ev, root) {
523 changed = true;
524 }
525 }
526 self.drain_widget_changes();
527 if changed || self.needs_rebuild {
528 *needs_rebuild = true;
529 self.needs_rebuild = true;
530 }
531 }
532
533 fn handle_key_input(
534 &mut self,
535 event: &KeyEvent,
536 needs_rebuild: &mut bool,
537 ) -> Option<Self::Message> {
538 // App-level shortcuts before widget routing.
539 if event.ctrl && event.state == ElementState::Pressed {
540 if let cce_ui::widget::Key::Character(ref c) = event.logical_key {
541 if c == "q" {
542 return Some(DemoMessage::Exit);
543 }
544 }
545 }
546
547 // KeyInput MUST short-circuit: the router delivers keys to the ctx-focused
548 // widget FIRST on every propagate call, so a non-short-circuited chain would
549 // hand a typed character to the focused widget once per root (N-time
550 // insertion). Plumbing that would key off "which call handled it" belongs in
551 // the state-gated `drain_widget_changes` instead.
552 let ev = Event::KeyInput(event.clone());
553 let mut handled = false;
554 for root in self.root_ids() {
555 if self.ui_context.propagate_event(&ev, root) {
556 handled = true;
557 break;
558 }
559 }
560 self.drain_widget_changes();
561 if handled || self.needs_rebuild {
562 *needs_rebuild = true;
563 self.needs_rebuild = true;
564 }
565 None
566 }
567 }
568
569 fn main() {
570 cce_ui::engine::run::<DemoApp>();
571 }