git.lucas.co / cce-mail
mail client (IMAP/SMTP)
git clone https://git.lucas.co/cce-mail.git

commit624274b80566d214ac89ad555ed82dfaebfef4fd
parentc436f0fbc7
authorLucas Galante <[email protected]>
date2026-06-09 10:09
Refactor and adapt widgets to use UiContext and scale factor

 Cargo.toml  |   2 +-
 Makefile    |   2 +-
 src/main.rs | 188 ++++++++++++++++++++++++++++++++++++------------------------
 3 files changed, 114 insertions(+), 78 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index f4d8841..029b131 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "clear-email-interface"
+name = "cce-email"
 version = "0.1.0"
 edition = "2021"
 
diff --git a/Makefile b/Makefile
index d14d1ab..f720f40 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ build:
 
 install: build
 	mkdir -p ~/.local/bin
-	install -m 755 target/release/clear-email-interface ~/.local/bin/clear-email-interface
+	install -m 755 target/release/cce-email ~/.local/bin/cce-email
 
 run:
 	cargo run
diff --git a/src/main.rs b/src/main.rs
index 53e97e4..9ead4d3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ use clear_ui::widget::{
     MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element,
     TextBox, Button, TextLabel, Key, ScrollingList, Paginator
 };
+use clear_ui::context::UiContext;
 use native_tls::TlsConnector;
 use lettre::transport::smtp::authentication::Credentials;
 use lettre::{Message, SmtpTransport, Transport};
@@ -130,10 +131,11 @@ struct ClearEmailApp {
     text_items: Vec<TextItem>,
     font_system: FontSystem,
     needs_rebuild: bool,
+    ui_context: UiContext,
 }
 
 fn get_accounts_path() -> std::path::PathBuf {
-    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec");
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/cce");
     if !p.exists() {
         let _ = std::fs::create_dir_all(&p);
         #[cfg(unix)]
@@ -193,7 +195,7 @@ fn save_accounts(accounts: &[AccountInfo]) {
 }
 
 fn get_account_emails_path(email: &str) -> std::path::PathBuf {
-    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec");
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/cce");
     if !p.exists() {
         let _ = std::fs::create_dir_all(&p);
     }
@@ -270,7 +272,7 @@ struct GoogleClientConfig {
 }
 
 fn load_google_client_config() -> GoogleClientConfig {
-    let p = std::path::PathBuf::from("/home/lsgalante/.config/ccec/google_client.json");
+    let p = std::path::PathBuf::from("/home/lsgalante/.config/cce/google_client.json");
     if p.exists() {
         if let Ok(content) = std::fs::read_to_string(&p) {
             if let Ok(config) = serde_json::from_str::<GoogleClientConfig>(&content) {
@@ -719,8 +721,8 @@ fn get_default_mock_emails() -> Vec<Email> {
             id: 3,
             from: "Codeberg CI <[email protected]>".to_string(),
             to: "[email protected]".to_string(),
-            subject: "Build Success: clear-email-interface (main)".to_string(),
-            body: "Repository: lsgalante/clear-email-interface\nBranch: main\nCommit: da8cf20fcb2c993c1c048ced4020\nStatus: SUCCESS\n\nAll unit tests passed. Binary compiled in 48.2s.\n\n---\nCodeberg Actions".to_string(),
+            subject: "Build Success: cce-email (main)".to_string(),
+            body: "Repository: lsgalante/cce-email\nBranch: main\nCommit: da8cf20fcb2c993c1c048ced4020\nStatus: SUCCESS\n\nAll unit tests passed. Binary compiled in 48.2s.\n\n---\nCodeberg Actions".to_string(),
             date: "June 3".to_string(),
             read: true,
             folder: "inbox".to_string(),
@@ -749,6 +751,7 @@ impl ClearEmailApp {
         self.text_items.clear();
         let mut labels = Vec::new();
         let font_system = &mut self.font_system;
+        let ctx = &self.ui_context;
 
         let w_f32 = self.width as f32;
         let h_f32 = self.height as f32;
@@ -783,7 +786,7 @@ impl ClearEmailApp {
             labels.extend(self.btn_add_account.text_labels());
         } else {
             self.search_box.prepare_text(font_system);
-            for (label, bounds) in self.search_box.text_labels_with_bounds() {
+            for (label, bounds) in self.search_box.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -941,7 +944,7 @@ impl ClearEmailApp {
 
                 // Body rendering
                 self.detail_body.prepare_text(font_system);
-                for (label, bounds) in self.detail_body.text_labels_with_bounds() {
+                for (label, bounds) in self.detail_body.text_labels_with_bounds(ctx) {
                     let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                     let mut buf = Buffer::new(font_system, metrics);
                     buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1000,7 +1003,7 @@ impl ClearEmailApp {
 
             // Compose inputs text labels
             self.compose_to.prepare_text(font_system);
-            for (label, bounds) in self.compose_to.text_labels_with_bounds() {
+            for (label, bounds) in self.compose_to.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1015,7 +1018,7 @@ impl ClearEmailApp {
             }
 
             self.compose_subject.prepare_text(font_system);
-            for (label, bounds) in self.compose_subject.text_labels_with_bounds() {
+            for (label, bounds) in self.compose_subject.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1030,7 +1033,7 @@ impl ClearEmailApp {
             }
 
             self.compose_body.prepare_text(font_system);
-            for (label, bounds) in self.compose_body.text_labels_with_bounds() {
+            for (label, bounds) in self.compose_body.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1085,7 +1088,7 @@ impl ClearEmailApp {
 
             // 7a. Add Account Inputs text labels
             self.add_acc_email.prepare_text(font_system);
-            for (label, bounds) in self.add_acc_email.text_labels_with_bounds() {
+            for (label, bounds) in self.add_acc_email.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1100,7 +1103,7 @@ impl ClearEmailApp {
             }
 
             self.add_acc_password.prepare_text(font_system);
-            for (label, bounds) in self.add_acc_password.text_labels_with_bounds() {
+            for (label, bounds) in self.add_acc_password.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1115,7 +1118,7 @@ impl ClearEmailApp {
             }
 
             self.add_acc_imap.prepare_text(font_system);
-            for (label, bounds) in self.add_acc_imap.text_labels_with_bounds() {
+            for (label, bounds) in self.add_acc_imap.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1130,7 +1133,7 @@ impl ClearEmailApp {
             }
 
             self.add_acc_smtp.prepare_text(font_system);
-            for (label, bounds) in self.add_acc_smtp.text_labels_with_bounds() {
+            for (label, bounds) in self.add_acc_smtp.text_labels_with_bounds(ctx) {
                 let metrics = Metrics::new(label.font_size, label.font_size * 1.4);
                 let mut buf = Buffer::new(font_system, metrics);
                 buf.set_text(font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
@@ -1275,13 +1278,14 @@ impl Application for ClearEmailApp {
             text_items: Vec::new(),
             font_system: FontSystem::new(),
             needs_rebuild: true,
+            ui_context: UiContext::new(),
         }
     }
 
     fn settings(&self) -> WindowSettings {
         WindowSettings {
             title: "Clear Email Client".to_string(),
-            app_id: "clear-email-interface".to_string(),
+            app_id: "cce-email".to_string(),
             width: 1000,
             height: 600,
             fullscreen: false,
@@ -1625,7 +1629,7 @@ impl Application for ClearEmailApp {
                 self.needs_rebuild = true;
             }
         }
-        if self.paginator.tick(dt) {
+        if self.paginator.tick(dt, &mut self.ui_context) {
             *needs_rebuild = true;
             self.needs_rebuild = true;
         }
@@ -1970,57 +1974,58 @@ impl Application for ClearEmailApp {
         let mut changed = false;
         let px = pos.x as f32;
         let py = pos.y as f32;
+        let ctx = &mut self.ui_context;
 
         if self.account_dialog_open {
-            if self.add_acc_email.on_cursor_moved(px, py) { changed = true; }
-            if self.add_acc_password.on_cursor_moved(px, py) { changed = true; }
-            if self.add_acc_imap.on_cursor_moved(px, py) { changed = true; }
-            if self.add_acc_smtp.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_add_acc_save.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_add_acc_cancel.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_add_acc_oauth.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_add_acc_icloud.on_cursor_moved(px, py) { changed = true; }
+            if self.add_acc_email.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.add_acc_password.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.add_acc_imap.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.add_acc_smtp.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_add_acc_save.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_add_acc_cancel.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_add_acc_oauth.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_add_acc_icloud.on_cursor_moved(px, py, ctx) { changed = true; }
         } else if self.compose_open {
-            if self.compose_to.on_cursor_moved(px, py) { changed = true; }
-            if self.compose_subject.on_cursor_moved(px, py) { changed = true; }
-            if self.compose_body.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_compose_send.on_cursor_moved(px, py) { changed = true; }
-            if self.btn_compose_cancel.on_cursor_moved(px, py) { changed = true; }
+            if self.compose_to.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.compose_subject.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.compose_body.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_compose_send.on_cursor_moved(px, py, ctx) { changed = true; }
+            if self.btn_compose_cancel.on_cursor_moved(px, py, ctx) { changed = true; }
         } else {
             // Sidebar buttons
-            if self.btn_compose.on_cursor_moved(px, py) { changed = true; }
+            if self.btn_compose.on_cursor_moved(px, py, ctx) { changed = true; }
             let sidebar_w = self.paginator.sidebar_w();
             if px < sidebar_w {
-                if self.paginator.on_cursor_moved(px, py) { changed = true; }
+                if self.paginator.on_cursor_moved(px, py, ctx) { changed = true; }
             }
 
             // Search / Add account and lists
             if self.current_folder == Folder::Accounts {
-                if self.btn_add_account.on_cursor_moved(px, py) { changed = true; }
+                if self.btn_add_account.on_cursor_moved(px, py, ctx) { changed = true; }
             } else {
-                if self.search_box.on_cursor_moved(px, py) { changed = true; }
+                if self.search_box.on_cursor_moved(px, py, ctx) { changed = true; }
             }
-            if self.email_list.on_cursor_moved(px, py) { changed = true; }
+            if self.email_list.on_cursor_moved(px, py, ctx) { changed = true; }
 
             for btn in &mut self.email_buttons {
                 if btn.rect().0 > -9000.0 {
-                    if btn.on_cursor_moved(px, py) { changed = true; }
+                    if btn.on_cursor_moved(px, py, ctx) { changed = true; }
                 }
             }
 
             // Detail view buttons
             if self.current_folder == Folder::Accounts {
                 if self.selected_account_idx < self.accounts.len() {
-                    if self.btn_make_default.on_cursor_moved(px, py) { changed = true; }
+                    if self.btn_make_default.on_cursor_moved(px, py, ctx) { changed = true; }
                     if self.accounts[self.selected_account_idx].is_oauth {
-                        if self.btn_login_oauth.on_cursor_moved(px, py) { changed = true; }
+                        if self.btn_login_oauth.on_cursor_moved(px, py, ctx) { changed = true; }
                     }
                 }
             } else if self.selected_email_id.is_some() {
-                if self.btn_reply.on_cursor_moved(px, py) { changed = true; }
-                if self.btn_delete.on_cursor_moved(px, py) { changed = true; }
-                if self.btn_unread.on_cursor_moved(px, py) { changed = true; }
-                if self.detail_body.on_cursor_moved(px, py) { changed = true; }
+                if self.btn_reply.on_cursor_moved(px, py, ctx) { changed = true; }
+                if self.btn_delete.on_cursor_moved(px, py, ctx) { changed = true; }
+                if self.btn_unread.on_cursor_moved(px, py, ctx) { changed = true; }
+                if self.detail_body.on_cursor_moved(px, py, ctx) { changed = true; }
             }
         }
 
@@ -2035,32 +2040,45 @@ impl Application for ClearEmailApp {
         let mut msg_out = None;
         let px = pos.x as f32;
         let py = pos.y as f32;
+        let ctx = &mut self.ui_context;
 
         if self.account_dialog_open {
-            if self.add_acc_email.mouse_input(button, state, px, py) { changed = true; }
-            if self.add_acc_password.mouse_input(button, state, px, py) { changed = true; }
-            if self.add_acc_imap.mouse_input(button, state, px, py) { changed = true; }
-            if self.add_acc_smtp.mouse_input(button, state, px, py) { changed = true; }
+            if self.add_acc_email.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.add_acc_email); }
+            }
+            if self.add_acc_password.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.add_acc_password); }
+            }
+            if self.add_acc_imap.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.add_acc_imap); }
+            }
+            if self.add_acc_smtp.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.add_acc_smtp); }
+            }
 
-            if self.btn_add_acc_save.mouse_input(button, state, px, py) {
+            if self.btn_add_acc_save.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_add_acc_save.take_click() {
                     msg_out = Some(AppMessage::AddAccountSave);
                 }
             }
-            if self.btn_add_acc_cancel.mouse_input(button, state, px, py) {
+            if self.btn_add_acc_cancel.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_add_acc_cancel.take_click() {
                     msg_out = Some(AppMessage::AddAccountCancel);
                 }
             }
-            if self.btn_add_acc_oauth.mouse_input(button, state, px, py) {
+            if self.btn_add_acc_oauth.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_add_acc_oauth.take_click() {
                     msg_out = Some(AppMessage::AddAccountOAuth);
                 }
             }
-            if self.btn_add_acc_icloud.mouse_input(button, state, px, py) {
+            if self.btn_add_acc_icloud.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_add_acc_icloud.take_click() {
                     msg_out = Some(AppMessage::AddAccountICloudHelp);
@@ -2075,6 +2093,7 @@ impl Application for ClearEmailApp {
                 let modal_y = ((h_f32 - 360.0) / 2.0).max(0.0);
 
                 if px < modal_x || px > modal_x + 500.0 || py < modal_y || py > modal_y + 360.0 {
+                    ctx.clear_focus();
                     self.add_acc_email.unfocus();
                     self.add_acc_password.unfocus();
                     self.add_acc_imap.unfocus();
@@ -2083,17 +2102,26 @@ impl Application for ClearEmailApp {
                 }
             }
         } else if self.compose_open {
-            if self.compose_to.mouse_input(button, state, px, py) { changed = true; }
-            if self.compose_subject.mouse_input(button, state, px, py) { changed = true; }
-            if self.compose_body.mouse_input(button, state, px, py) { changed = true; }
+            if self.compose_to.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.compose_to); }
+            }
+            if self.compose_subject.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.compose_subject); }
+            }
+            if self.compose_body.mouse_input(button, state, px, py, ctx) {
+                changed = true;
+                if state == ElementState::Pressed { ctx.set_focused(&mut self.compose_body); }
+            }
 
-            if self.btn_compose_send.mouse_input(button, state, px, py) {
+            if self.btn_compose_send.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_compose_send.take_click() {
                     msg_out = Some(AppMessage::ComposeSend);
                 }
             }
-            if self.btn_compose_cancel.mouse_input(button, state, px, py) {
+            if self.btn_compose_cancel.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_compose_cancel.take_click() {
                     msg_out = Some(AppMessage::ComposeCancel);
@@ -2108,6 +2136,7 @@ impl Application for ClearEmailApp {
                 let modal_y = ((h_f32 - 420.0) / 2.0).max(0.0);
 
                 if px < modal_x || px > modal_x + 500.0 || py < modal_y || py > modal_y + 420.0 {
+                    ctx.clear_focus();
                     self.compose_to.unfocus();
                     self.compose_subject.unfocus();
                     self.compose_body.unfocus();
@@ -2116,7 +2145,7 @@ impl Application for ClearEmailApp {
             }
         } else {
             // Sidebar buttons
-            if self.btn_compose.mouse_input(button, state, px, py) {
+            if self.btn_compose.mouse_input(button, state, px, py, ctx) {
                 changed = true;
                 if state == ElementState::Released && self.btn_compose.take_click() {
                     msg_out = Some(AppMessage::ComposeNew);
@@ -2124,7 +2153,7 @@ impl Application for ClearEmailApp {
             }
             let sidebar_w = self.paginator.sidebar_w();
             if px < sidebar_w {
-                if self.paginator.mouse_input(button, state, px, py) {
+                if self.paginator.mouse_input(button, state, px, py, ctx) {
                     changed = true;
                     if self.paginator.take_click() {
                         let page = self.paginator.selected_page();
@@ -2141,7 +2170,7 @@ impl Application for ClearEmailApp {
             }
 
             if self.current_folder == Folder::Accounts {
-                if self.btn_add_account.mouse_input(button, state, px, py) {
+                if self.btn_add_account.mouse_input(button, state, px, py, ctx) {
                     changed = true;
                     if state == ElementState::Released && self.btn_add_account.take_click() {
                         msg_out = Some(AppMessage::AddAccount);
@@ -2149,18 +2178,20 @@ impl Application for ClearEmailApp {
                 }
             } else {
                 // Search input
-                if self.search_box.mouse_input(button, state, px, py) {
+                if self.search_box.mouse_input(button, state, px, py, ctx) {
                     changed = true;
+                    if state == ElementState::Pressed { ctx.set_focused(&mut self.search_box); }
                     if self.search_box.take_change() {
                         msg_out = Some(AppMessage::SearchChanged);
                     }
                 } else if state == ElementState::Pressed && button == MouseButton::Left {
+                    ctx.clear_focus();
                     self.search_box.unfocus();
                     changed = true;
                 }
             }
 
-            if self.email_list.mouse_input(button, state, px, py) {
+            if self.email_list.mouse_input(button, state, px, py, ctx) {
                 changed = true;
             }
 
@@ -2169,7 +2200,7 @@ impl Application for ClearEmailApp {
                     if idx < self.email_buttons.len() {
                         let btn = &mut self.email_buttons[idx];
                         if btn.rect().0 > -9000.0 {
-                            if btn.mouse_input(button, state, px, py) {
+                            if btn.mouse_input(button, state, px, py, ctx) {
                                 changed = true;
                                 if state == ElementState::Released && btn.take_click() {
                                     msg_out = Some(AppMessage::SelectAccount(idx));
@@ -2204,7 +2235,7 @@ impl Application for ClearEmailApp {
                     if idx < self.email_buttons.len() {
                         let btn = &mut self.email_buttons[idx];
                         if btn.rect().0 > -9000.0 {
-                            if btn.mouse_input(button, state, px, py) {
+                            if btn.mouse_input(button, state, px, py, ctx) {
                                 changed = true;
                                 if state == ElementState::Released && btn.take_click() {
                                     msg_out = Some(AppMessage::SelectEmail(email.id));
@@ -2218,14 +2249,14 @@ impl Application for ClearEmailApp {
             // Detail View action buttons
             if self.current_folder == Folder::Accounts {
                 if self.selected_account_idx < self.accounts.len() {
-                    if self.btn_make_default.mouse_input(button, state, px, py) {
+                    if self.btn_make_default.mouse_input(button, state, px, py, ctx) {
                         changed = true;
                         if state == ElementState::Released && self.btn_make_default.take_click() {
                             msg_out = Some(AppMessage::MakeDefaultAccount);
                         }
                     }
                     if self.accounts[self.selected_account_idx].is_oauth {
-                        if self.btn_login_oauth.mouse_input(button, state, px, py) {
+                        if self.btn_login_oauth.mouse_input(button, state, px, py, ctx) {
                             changed = true;
                             if state == ElementState::Released && self.btn_login_oauth.take_click() {
                                 msg_out = Some(AppMessage::AddAccountOAuth);
@@ -2234,26 +2265,27 @@ impl Application for ClearEmailApp {
                     }
                 }
             } else if self.selected_email_id.is_some() {
-                if self.btn_reply.mouse_input(button, state, px, py) {
+                if self.btn_reply.mouse_input(button, state, px, py, ctx) {
                     changed = true;
                     if state == ElementState::Released && self.btn_reply.take_click() {
                         msg_out = Some(AppMessage::Reply);
                     }
                 }
-                if self.btn_delete.mouse_input(button, state, px, py) {
+                if self.btn_delete.mouse_input(button, state, px, py, ctx) {
                     changed = true;
                     if state == ElementState::Released && self.btn_delete.take_click() {
                         msg_out = Some(AppMessage::DeleteSelected);
                     }
                 }
-                if self.btn_unread.mouse_input(button, state, px, py) {
+                if self.btn_unread.mouse_input(button, state, px, py, ctx) {
                     changed = true;
                     if state == ElementState::Released && self.btn_unread.take_click() {
                         msg_out = Some(AppMessage::ToggleUnread);
                     }
                 }
-                if self.detail_body.mouse_input(button, state, px, py) {
+                if self.detail_body.mouse_input(button, state, px, py, ctx) {
                     changed = true;
+                    if state == ElementState::Pressed { ctx.set_focused(&mut self.detail_body); }
                 }
             }
         }
@@ -2270,10 +2302,11 @@ impl Application for ClearEmailApp {
         let mut changed = false;
         let px = pos.x as f32;
         let py = pos.y as f32;
+        let ctx = &mut self.ui_context;
 
         if !self.compose_open {
             if px >= 66.0 && px <= 366.0 {
-                if self.email_list.mouse_wheel(delta, px, py) {
+                if self.email_list.mouse_wheel(delta, px, py, ctx) {
                     changed = true;
                 }
             }
@@ -2288,10 +2321,11 @@ impl Application for ClearEmailApp {
     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
         let mut handled = false;
         let mut msg_out = None;
+        let ctx = &mut self.ui_context;
 
         if self.account_dialog_open {
             if self.add_acc_email.editing {
-                if self.add_acc_email.keyboard_input(event) {
+                if self.add_acc_email.keyboard_input(event, ctx) {
                     handled = true;
                     // Auto-fill configuration based on email domain
                     let email_val = self.add_acc_email.edit_buffer.trim().to_lowercase();
@@ -2313,11 +2347,11 @@ impl Application for ClearEmailApp {
                     }
                 }
             } else if self.add_acc_password.editing {
-                if self.add_acc_password.keyboard_input(event) { handled = true; }
+                if self.add_acc_password.keyboard_input(event, ctx) { handled = true; }
             } else if self.add_acc_imap.editing {
-                if self.add_acc_imap.keyboard_input(event) { handled = true; }
+                if self.add_acc_imap.keyboard_input(event, ctx) { handled = true; }
             } else if self.add_acc_smtp.editing {
-                if self.add_acc_smtp.keyboard_input(event) { handled = true; }
+                if self.add_acc_smtp.keyboard_input(event, ctx) { handled = true; }
             }
 
             // Escape closes dialog
@@ -2327,11 +2361,11 @@ impl Application for ClearEmailApp {
             }
         } else if self.compose_open {
             if self.compose_to.editing {
-                if self.compose_to.keyboard_input(event) { handled = true; }
+                if self.compose_to.keyboard_input(event, ctx) { handled = true; }
             } else if self.compose_subject.editing {
-                if self.compose_subject.keyboard_input(event) { handled = true; }
+                if self.compose_subject.keyboard_input(event, ctx) { handled = true; }
             } else if self.compose_body.editing {
-                if self.compose_body.keyboard_input(event) { handled = true; }
+                if self.compose_body.keyboard_input(event, ctx) { handled = true; }
             }
 
             // Escape closes compose dialog
@@ -2350,6 +2384,7 @@ impl Application for ClearEmailApp {
                         }
                         "f" => {
                             if self.current_folder != Folder::Accounts {
+                                ctx.set_focused(&mut self.search_box);
                                 self.search_box.focus();
                                 handled = true;
                             }
@@ -2360,7 +2395,7 @@ impl Application for ClearEmailApp {
             }
 
             if !handled && self.current_folder != Folder::Accounts && self.search_box.editing {
-                if self.search_box.keyboard_input(event) {
+                if self.search_box.keyboard_input(event, ctx) {
                     handled = true;
                     if self.search_box.take_change() {
                         msg_out = Some(AppMessage::SearchChanged);
@@ -2371,6 +2406,7 @@ impl Application for ClearEmailApp {
             // Escape unfocuses search
             if !handled && event.state == ElementState::Pressed && event.logical_key == Key::Named(clear_ui::widget::NamedKey::Escape) {
                 if self.current_folder != Folder::Accounts && self.search_box.editing {
+                    ctx.clear_focus();
                     self.search_box.unfocus();
                     handled = true;
                 }