git.lucas.co / cce-display-manager
login greeter
git clone https://git.lucas.co/cce-display-manager.git

commitcb325a3e7abd76ffbde35016478381e255f6e9a2
parent886728a8ea
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-08 21:54
refactor: widgets are handles the context owns

Fields move from Owned to Handle; link_parent_child became link_ids and the unused widgets_iter_mut went. Shadow A/B of the greeter (typing, Tab, session pick) identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/main.rs | 208 ++++++++++++++++++++++++++++--------------------------------
 1 file changed, 98 insertions(+), 110 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 78839b7..c0276ab 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,5 @@
-use cce_ui::widget::Owned;
-use cce_ui::widget::{Button, ContentBg, WidgetHost, ElementState, MouseButton, Key, NamedKey, KeyEvent, TextBox, focus, MouseScrollDelta, WidgetHostExt};
+use cce_ui::widget::Handle;
+use cce_ui::widget::{Button, ContentBg, WidgetHost, ElementState, MouseButton, Key, NamedKey, KeyEvent, TextBox, MouseScrollDelta, WidgetHostExt};
 use cce_ui::engine::{EngineState, LogicalPosition, LogicalSize, WindowSettings, Vertex, quad_vertices};
 use calloop::channel;
 
@@ -306,13 +306,13 @@ impl cce_ui::widget::Input for SessionList {
 
 // ── App State and Renderer ──
 struct State {
-    bg: Owned<cce_ui::widget::Adapted<ContentBg>>,
-    card: Owned<cce_ui::widget::Adapted<LoginCard>>,
-    username_box: Owned<cce_ui::widget::Adapted<TextBox>>,
-    password_box: Owned<cce_ui::widget::Adapted<TextBox>>,
-    login_btn: Owned<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
-    status_lbl: Owned<cce_ui::widget::Adapted<StatusLabel>>,
-    session_list: Owned<cce_ui::widget::Adapted<SessionList>>,
+    bg: Handle<cce_ui::widget::Adapted<ContentBg>>,
+    card: Handle<cce_ui::widget::Adapted<LoginCard>>,
+    username_box: Handle<cce_ui::widget::Adapted<TextBox>>,
+    password_box: Handle<cce_ui::widget::Adapted<TextBox>>,
+    login_btn: Handle<cce_ui::widget::Adapted<cce_ui::widget::Button>>,
+    status_lbl: Handle<cce_ui::widget::Adapted<StatusLabel>>,
+    session_list: Handle<cce_ui::widget::Adapted<SessionList>>,
     ui_context: cce_ui::context::UiContext,
 
 
@@ -375,14 +375,14 @@ impl State {
     fn focus_field(&mut self, field: Field) {
         match field {
             Field::Username => {
-                self.ui_context.set_focused(&mut self.username_box);
-                self.ui_context.unfocus_widget(&mut self.password_box);
-                self.ui_context.focus_widget(&mut self.username_box);
+                self.ui_context.set_focused_id(self.username_box.id());
+                self.ui_context.unfocus_id(self.password_box.id());
+                self.ui_context.focus_id(self.username_box.id());
             }
             Field::Password => {
-                self.ui_context.set_focused(&mut self.password_box);
-                self.ui_context.unfocus_widget(&mut self.username_box);
-                self.ui_context.focus_widget(&mut self.password_box);
+                self.ui_context.set_focused_id(self.password_box.id());
+                self.ui_context.unfocus_id(self.username_box.id());
+                self.ui_context.focus_id(self.password_box.id());
             }
         }
     }
@@ -398,13 +398,10 @@ impl State {
         // Root Container DISSOLVED (Phase 6ax): the card and the session list are the two
         // dispatch/walk roots; register them directly (link_parent_child used to do it as a
         // side effect of the root links).
-        ctx.register_host(&mut self.bg);
-        ctx.register_host(&mut self.card);
-        ctx.register_host(&mut self.session_list);
-        focus::link_parent_child(&mut self.card, &mut self.username_box, ctx);
-        focus::link_parent_child(&mut self.card, &mut self.password_box, ctx);
-        focus::link_parent_child(&mut self.card, &mut self.login_btn, ctx);
-        focus::link_parent_child(&mut self.card, &mut self.status_lbl, ctx);
+        ctx.link_ids(self.card.id(), self.username_box.id());
+        ctx.link_ids(self.card.id(), self.password_box.id());
+        ctx.link_ids(self.card.id(), self.login_btn.id());
+        ctx.link_ids(self.card.id(), self.status_lbl.id());
         // Initial focus, on the first frame: new() cannot register it (its widgets are
         // about to move). It used to be read back off the boxes' own `base().focused`
         // flags, which a TextBox does not keep, so the context started with NO focus —
@@ -414,25 +411,12 @@ impl State {
         }
     }
 
-    #[allow(dead_code)]
-    fn widgets_iter_mut(&mut self) -> Vec<&mut dyn WidgetHost> {
-        vec![
-            &mut self.bg,
-            &mut self.card,
-            &mut self.username_box,
-            &mut self.password_box,
-            &mut self.login_btn,
-            &mut self.status_lbl,
-            &mut self.session_list,
-        ]
-    }
-
     fn apply_layout(&mut self) {
         let sw = self.width;
         let sh = self.height;
 
         // Background spans the whole screen
-        self.bg.set_rect(0.0, 0.0, sw, sh);
+        self.ui_context[self.bg].set_rect(0.0, 0.0, sw, sh);
 
         // Center card configuration
         let card_w = 360.0;
@@ -441,7 +425,7 @@ impl State {
         let card_y = (sh - card_h) / 2.0;
         
         // Card is full screen to render aspect-ratio centered oval custom graphic
-        self.card.set_rect(0.0, 0.0, sw, sh);
+        self.ui_context[self.card].set_rect(0.0, 0.0, sw, sh);
 
         // Child components inside login card
         let content_x = card_x + 30.0;
@@ -452,22 +436,24 @@ impl State {
         let btn_h = cce_ui::layout::button_height();
 
         // Username text box
-        self.username_box.set_rect(content_x, card_y + 80.0, 300.0, tb_h + self.username_box.label_strip());
+        let strip = self.ui_context[self.username_box].label_strip();
+        self.ui_context[self.username_box].set_rect(content_x, card_y + 80.0, 300.0, tb_h + strip);
         
         // Password password box
-        self.password_box.set_rect(content_x, card_y + 145.0, 300.0, tb_h + self.password_box.label_strip());
+        let strip = self.ui_context[self.password_box].label_strip();
+        self.ui_context[self.password_box].set_rect(content_x, card_y + 145.0, 300.0, tb_h + strip);
 
         // Login button (full-width of the contents)
         let login_y = card_y + 205.0;
-        self.login_btn.set_rect(content_x, login_y, 300.0, btn_h);
+        self.ui_context[self.login_btn].set_rect(content_x, login_y, 300.0, btn_h);
 
         // Status message
-        self.status_lbl.set_rect(content_x, login_y + btn_h + 11.0, 300.0, 20.0);
+        self.ui_context[self.status_lbl].set_rect(content_x, login_y + btn_h + 11.0, 300.0, 20.0);
 
         // Session list on top left
         let list_w = 260.0;
-        let list_h = 40.0 + self.session_list.sessions.len() as f32 * 36.0;
-        self.session_list.set_rect(30.0, 30.0, list_w, list_h);
+        let list_h = 40.0 + self.ui_context[self.session_list].sessions.len() as f32 * 36.0;
+        self.ui_context[self.session_list].set_rect(30.0, 30.0, list_w, list_h);
     }
 
     pub fn widgets_cursor_moved(&mut self, cx: f32, cy: f32) -> bool {
@@ -517,8 +503,8 @@ impl State {
         if button == MouseButton::Left && state == ElementState::Pressed {
             if !handled {
                 self.ui_context.clear_focus();
-                self.ui_context.unfocus_widget(&mut self.username_box);
-                self.ui_context.unfocus_widget(&mut self.password_box);
+                self.ui_context.unfocus_id(self.username_box.id());
+                self.ui_context.unfocus_id(self.password_box.id());
                 changed = true;
             }
         }
@@ -547,22 +533,22 @@ impl State {
         changed
     }
     fn trigger_auth(&mut self) {
-        let username = self.username_box.text.trim().to_string();
-        let password = self.password_box.text.clone();
+        let username = self.ui_context[self.username_box].text.trim().to_string();
+        let password = self.ui_context[self.password_box].text.clone();
 
         if username.is_empty() {
-            self.status_lbl.text = "Username cannot be empty".to_string();
-            self.status_lbl.is_error = true;
-            self.ui_context.set_focused(&mut self.username_box);
-            self.ui_context.focus_widget(&mut self.username_box);
+            self.ui_context[self.status_lbl].text = "Username cannot be empty".to_string();
+            self.ui_context[self.status_lbl].is_error = true;
+            self.ui_context.set_focused_id(self.username_box.id());
+            self.ui_context.focus_id(self.username_box.id());
         } else if password.is_empty() {
             if is_fprint_enabled() {
                 self.start_fprint_auth();
             } else {
-                self.status_lbl.text = "Password cannot be empty".to_string();
-                self.status_lbl.is_error = true;
-                self.ui_context.set_focused(&mut self.password_box);
-                self.ui_context.focus_widget(&mut self.password_box);
+                self.ui_context[self.status_lbl].text = "Password cannot be empty".to_string();
+                self.ui_context[self.status_lbl].is_error = true;
+                self.ui_context.set_focused_id(self.password_box.id());
+                self.ui_context.focus_id(self.password_box.id());
             }
         } else {
             // A typed password supersedes any fingerprint attempt still
@@ -570,32 +556,32 @@ impl State {
             self.cancel_fprint_auth();
             self.auth_request_id += 1;
             self.auth_password = password.clone();
-            self.status_lbl.text = "Authenticating...".to_string();
-            self.status_lbl.is_error = false;
+            self.ui_context[self.status_lbl].text = "Authenticating...".to_string();
+            self.ui_context[self.status_lbl].is_error = false;
             self.is_authenticating = true;
-            self.login_btn.base_mut().label = Some("Authenticating...".to_string());
+            self.ui_context[self.login_btn].base_mut().label = Some("Authenticating...".to_string());
             authenticate_user(self.auth_request_id, username, password, self.auth_sender.clone());
         }
     }
 
     /// Start (or restart) the fingerprint attempt for the username in the box.
     fn start_fprint_auth(&mut self) {
-        let username = self.username_box.text.trim().to_string();
+        let username = self.ui_context[self.username_box].text.trim().to_string();
         self.cancel_fprint_auth();
         self.auth_request_id += 1;
         self.auth_password.clear();
-        self.status_lbl.text = "Scan finger to login or type password".to_string();
-        self.status_lbl.is_error = false;
+        self.ui_context[self.status_lbl].text = "Scan finger to login or type password".to_string();
+        self.ui_context[self.status_lbl].is_error = false;
         self.is_authenticating = true;
-        self.login_btn.base_mut().label = Some("Authenticating...".to_string());
+        self.ui_context[self.login_btn].base_mut().label = Some("Authenticating...".to_string());
         match spawn_fprint_helper(self.auth_request_id, &username, self.auth_sender.clone()) {
             Ok(child) => self.fprint_child = Some(child),
             Err(e) => {
                 log::error!("Failed to spawn fingerprint helper: {}", e);
                 self.is_authenticating = false;
-                self.login_btn.base_mut().label = Some("Log In".to_string());
-                self.status_lbl.text = "Fingerprint unavailable — type password".to_string();
-                self.status_lbl.is_error = true;
+                self.ui_context[self.login_btn].base_mut().label = Some("Log In".to_string());
+                self.ui_context[self.status_lbl].text = "Fingerprint unavailable — type password".to_string();
+                self.ui_context[self.status_lbl].is_error = true;
             }
         }
     }
@@ -680,15 +666,17 @@ impl cce_ui::engine::Application for State {
         let mut session_list = SessionList::new(sessions);
         session_list.selected_idx = selected_idx;
 
+        // The context owns the widgets; the app keeps their handles.
+        let mut ui_context = cce_ui::context::UiContext::new();
         let mut app = Self {
-            bg: Owned::new(bg),
-            card: Owned::new(card),
-            username_box: Owned::new(username_box),
-            password_box: Owned::new(password_box),
-            login_btn: Owned::new(login_btn),
-            status_lbl: Owned::new(status_lbl),
-            session_list: Owned::new(session_list),
-            ui_context: cce_ui::context::UiContext::new(),
+            bg: ui_context.insert(bg),
+            card: ui_context.insert(card),
+            username_box: ui_context.insert(username_box),
+            password_box: ui_context.insert(password_box),
+            login_btn: ui_context.insert(login_btn),
+            status_lbl: ui_context.insert(status_lbl),
+            session_list: ui_context.insert(session_list),
+            ui_context,
             cursor_x: 0.0,
             cursor_y: 0.0,
             width: 1024.0,
@@ -712,11 +700,11 @@ impl cce_ui::engine::Application for State {
         // addresses at the top of every frame instead. Only the widgets' own focus FLAGS
         // (which move with the struct) are set here; relink_tree points focused_widget at
         // the flagged box.
-        let has_username = !app.username_box.text.trim().to_string().is_empty();
+        let has_username = !app.ui_context[app.username_box].text.trim().to_string().is_empty();
         let first = if has_username { Field::Password } else { Field::Username };
         match first {
-            Field::Password => app.ui_context.focus_widget(&mut app.password_box),
-            Field::Username => app.ui_context.focus_widget(&mut app.username_box),
+            Field::Password => app.ui_context.focus_id(app.password_box.id()),
+            Field::Username => app.ui_context.focus_id(app.username_box.id()),
         }
         app.initial_focus = Some(first);
 
@@ -728,11 +716,11 @@ impl cce_ui::engine::Application for State {
         // own: three respawns in 90s were three attempts nobody asked for.
         // After a respawn the user starts it explicitly (Enter on an empty
         // password box).
-        let username = app.username_box.text.trim().to_string();
+        let username = app.ui_context[app.username_box].text.trim().to_string();
         if !username.is_empty() && is_fprint_enabled() {
             if fprint_autostart_recently() {
                 log::info!("Greeter respawned within {}s of the last fingerprint auto-start; not auto-starting", FPRINT_AUTOSTART_COOLDOWN.as_secs());
-                app.status_lbl.text = "Press Enter to scan finger, or type password".to_string();
+                app.ui_context[app.status_lbl].text = "Press Enter to scan finger, or type password".to_string();
             } else {
                 mark_fprint_autostart();
                 app.start_fprint_auth();
@@ -782,7 +770,7 @@ impl cce_ui::engine::Application for State {
         // the legacy tuple views — every rounded quad, then every plain quad, then the
         // text — so the fields and the button had none of the relief every other app's
         // controls have.)
-        for root in [&*self.bg as &dyn WidgetHost, &*self.card, &*self.session_list] {
+        for root in [&self.ui_context[self.bg] as &dyn WidgetHost, &self.ui_context[self.card], &self.ui_context[self.session_list]] {
             cce_ui::scene::painter::paint_root_into(&self.ui_context, root, &mut pc);
         }
 
@@ -816,7 +804,7 @@ impl cce_ui::engine::Application for State {
         let sw = self.width;
         let sh = self.height;
 
-        let mut card_verts = widget_vertices(&self.card, sw, sh);
+        let mut card_verts = widget_vertices(&self.ui_context[self.card], sw, sh);
         for v in &mut card_verts {
             v.clip_circle = [-999.0, 0.0, 0.0];
         }
@@ -844,12 +832,12 @@ impl cce_ui::engine::Application for State {
                             AuthEvent::Success { username, .. } => {
                                 app.fprint_child = None;
                                 app.is_authenticating = false;
-                                app.login_btn.base_mut().label = Some("Log In".to_string());
-                                app.status_lbl.text = format!("Welcome, {}!", username);
-                                app.status_lbl.is_error = false;
+                                app.ui_context[app.login_btn].base_mut().label = Some("Log In".to_string());
+                                app.ui_context[app.status_lbl].text = format!("Welcome, {}!", username);
+                                app.ui_context[app.status_lbl].is_error = false;
                                 app.login_success = true;
-                                if let Some(session) = app.session_list.selected_session() {
-                                    println!("{}", auth_success_line(app.username_box.text.trim(), &session.exec, session.is_wayland, &app.auth_password));
+                                if let Some(session) = app.ui_context[app.session_list].selected_session() {
+                                    println!("{}", auth_success_line(app.ui_context[app.username_box].text.trim(), &session.exec, session.is_wayland, &app.auth_password));
                                     std::process::exit(0);
                                 }
                             }
@@ -859,23 +847,23 @@ impl cce_ui::engine::Application for State {
                                     let _ = child.wait();
                                 }
                                 app.is_authenticating = false;
-                                app.login_btn.base_mut().label = Some("Log In".to_string());
-                                app.status_lbl.text = if was_fprint {
+                                app.ui_context[app.login_btn].base_mut().label = Some("Log In".to_string());
+                                app.ui_context[app.status_lbl].text = if was_fprint {
                                     // Raw PAM codes ("AUTHINFO_UNAVAIL") told the
                                     // user nothing, least of all how to retry.
                                     format!("{} — press Enter to scan again, or type password", fprint_failure_text(&err_msg))
                                 } else {
                                     password_failure_text(&err_msg)
                                 };
-                                app.status_lbl.is_error = true;
-                                app.password_box.text.clear();
-                                app.password_box.edit_buffer.clear();
-                                app.ui_context.set_focused(&mut app.password_box);
-                                app.ui_context.focus_widget(&mut app.password_box);
+                                app.ui_context[app.status_lbl].is_error = true;
+                                app.ui_context[app.password_box].text.clear();
+                                app.ui_context[app.password_box].edit_buffer.clear();
+                                app.ui_context.set_focused_id(app.password_box.id());
+                                app.ui_context.focus_id(app.password_box.id());
                             }
                             AuthEvent::Info { msg, .. } => {
-                                app.status_lbl.text = msg;
-                                app.status_lbl.is_error = false;
+                                app.ui_context[app.status_lbl].text = msg;
+                                app.ui_context[app.status_lbl].is_error = false;
                             }
                         }
                         redraw = true;
@@ -930,7 +918,7 @@ impl cce_ui::engine::Application for State {
             changed = true;
         }
         if button == MouseButton::Left && state == ElementState::Pressed {
-            if self.login_btn.take_click() {
+            if self.ui_context[self.login_btn].take_click() {
                 self.trigger_auth();
                 changed = true;
             }
@@ -973,8 +961,8 @@ impl cce_ui::engine::Application for State {
                     Ok(()) => (action.progress().to_string(), false),
                     Err(e) => (format!("Could not {}: {}", action.verb(), e), true),
                 };
-                self.status_lbl.text = text;
-                self.status_lbl.is_error = is_error;
+                self.ui_context[self.status_lbl].text = text;
+                self.ui_context[self.status_lbl].is_error = is_error;
                 *needs_rebuild = true;
                 return None;
             }
@@ -993,7 +981,7 @@ impl cce_ui::engine::Application for State {
             // If we are currently in fingerprint authentication and the user starts typing a password,
             // cancel the fingerprint auth and let them type.
             if self.is_authenticating {
-                if self.password_box.text.is_empty() {
+                if self.ui_context[self.password_box].text.is_empty() {
                     let is_typing = !ctrl_pressed && match logical_key {
                         Key::Character(_) | Key::Named(NamedKey::Backspace) | Key::Named(NamedKey::Delete) | Key::Named(NamedKey::Space) => true,
                         _ => false,
@@ -1002,9 +990,9 @@ impl cce_ui::engine::Application for State {
                         self.cancel_fprint_auth();
                         self.auth_request_id += 1;
                         self.is_authenticating = false;
-                        self.login_btn.base_mut().label = Some("Log In".to_string());
-                        self.status_lbl.text = "Enter password to start".to_string();
-                        self.status_lbl.is_error = false;
+                        self.ui_context[self.login_btn].base_mut().label = Some("Log In".to_string());
+                        self.ui_context[self.status_lbl].text = "Enter password to start".to_string();
+                        self.ui_context[self.status_lbl].is_error = false;
                     } else {
                         let is_nav = match logical_key {
                             Key::Named(NamedKey::ArrowUp) | Key::Named(NamedKey::ArrowDown) | Key::Named(NamedKey::Tab) => true,
@@ -1022,18 +1010,18 @@ impl cce_ui::engine::Application for State {
             let mut changed = false;
 
             // Handle Up/Down or Ctrl+P/N navigation to cycle sessions
-            let cycle_up = (logical_key == &Key::Named(NamedKey::ArrowUp) || is_ctrl_p) && !self.session_list.sessions.is_empty();
-            let cycle_down = (logical_key == &Key::Named(NamedKey::ArrowDown) || is_ctrl_n) && !self.session_list.sessions.is_empty();
+            let cycle_up = (logical_key == &Key::Named(NamedKey::ArrowUp) || is_ctrl_p) && !self.ui_context[self.session_list].sessions.is_empty();
+            let cycle_down = (logical_key == &Key::Named(NamedKey::ArrowDown) || is_ctrl_n) && !self.ui_context[self.session_list].sessions.is_empty();
 
             if cycle_up {
-                let len = self.session_list.sessions.len();
-                self.session_list.selected_idx = (self.session_list.selected_idx + len - 1) % len;
-                self.session_list.hovered_idx = None;
+                let len = self.ui_context[self.session_list].sessions.len();
+                self.ui_context[self.session_list].selected_idx = (self.ui_context[self.session_list].selected_idx + len - 1) % len;
+                self.ui_context[self.session_list].hovered_idx = None;
                 changed = true;
             } else if cycle_down {
-                let len = self.session_list.sessions.len();
-                self.session_list.selected_idx = (self.session_list.selected_idx + 1) % len;
-                self.session_list.hovered_idx = None;
+                let len = self.ui_context[self.session_list].sessions.len();
+                self.ui_context[self.session_list].selected_idx = (self.ui_context[self.session_list].selected_idx + 1) % len;
+                self.ui_context[self.session_list].hovered_idx = None;
                 changed = true;
             } else if logical_key == &Key::Named(NamedKey::Tab) {
                 let next = match self.focused_field() {
@@ -1057,7 +1045,7 @@ impl cce_ui::engine::Application for State {
                     let kev = cce_ui::widget::Event::KeyInput(event.clone());
                     let _ = self.ui_context.propagate_event(&kev, root);
                 }
-                if field == Some(Field::Username) && self.password_box.text.is_empty() {
+                if field == Some(Field::Username) && self.ui_context[self.password_box].text.is_empty() {
                     self.focus_field(Field::Password);
                 } else {
                     self.trigger_auth();