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

commitb4b58655e37d1375b2ed1b6e356d231269eedb01
parent890a67c523
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 10:26
fix(controls): every control at the toolkit's control height

The menubar's mail button and switchers were a hand-set 26px, the search
band and compose fields 26px, the compose and HTML-view buttons 28px and
the attachment chips 22/24px. They now ask cce-ui (button_height,
dropdown_height, textbox_height), the menubar band and compose dialog are
sized from the same numbers, and the chip labels centre on the chip.

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

 src/main.rs | 90 +++++++++++++++++++++++++++++++++----------------------------
 1 file changed, 49 insertions(+), 41 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 4cf11b7..21f7112 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -842,9 +842,12 @@ fn window_pad() -> f32 {
     cce_ui::layout::root_plate_inset()
 }
 
-/// VISIBLE height of one menubar control — what you see, not the rect it is
-/// given. See [`bar_item_carve`].
-const BAR_ITEM_H: f32 = 26.0;
+/// Height of the menubar's row of controls: the toolkit's control height
+/// (the mail button at `button_height`, the switchers at `dropdown_height`;
+/// one number unless a config sets them apart).
+fn bar_item_h() -> f32 {
+    cce_ui::layout::button_height().max(cce_ui::layout::dropdown_height())
+}
 
 /// The menubar band's height. One padding above its items, which then sit
 /// flush with the band's bottom edge, so the gap from an item down to the
@@ -852,11 +855,11 @@ const BAR_ITEM_H: f32 = 26.0;
 /// what makes every vertical gap in the window one number: window rim to
 /// items, items to plates, plates to rim.
 fn menubar_h() -> f32 {
-    window_pad() + BAR_ITEM_H
+    window_pad() + bar_item_h()
 }
 
 /// Widths of the two switchers in the bar; their heights and every gap
-/// around them come from [`BAR_ITEM_H`] and the DE spacings.
+/// around them come from [`bar_item_h`] and the DE spacings.
 const FOLDER_W: f32 = 134.0;
 const ACCOUNT_W: f32 = 210.0;
 
@@ -924,7 +927,7 @@ const CONTEXT_ROW_H: f32 = 24.0;
 // The search band. It is not permanent chrome: the `open_search` chord
 // ("/" by default) reveals and focuses it, and closing hands its strip back
 // to the list, so `list_geom` is the single source for where the rows start.
-const SEARCH_ROW_H: f32 = 26.0;
+// Its height is the toolkit's `textbox_height`.
 
 // Detail header layout, every offset measured from the HEADER plate's
 // top-left of its CONTENT — the plate's rim taken in by `pane_inner_pad`
@@ -945,7 +948,6 @@ const DETAIL_DATE_Y: f32 = 65.0;
 const DETAIL_LINE_H: f32 = 14.0;
 const DETAIL_LINES_BOTTOM: f32 = DETAIL_DATE_Y + DETAIL_LINE_H;
 const DETAIL_CHIPS_Y: f32 = 80.0;
-const DETAIL_CHIP_H: f32 = 22.0;
 
 // Compose modal geometry. One source of truth: the background quads, the
 // input rects, the labels, the chip row and the outside-click test all
@@ -958,10 +960,7 @@ const COMPOSE_W: f32 = 500.0;
 // number of its own.
 const COMPOSE_TITLE_H: f32 = 22.0;
 const COMPOSE_LABEL_W: f32 = 65.0;
-const COMPOSE_FIELD_H: f32 = 26.0;
 const COMPOSE_BODY_H: f32 = 195.0;
-const COMPOSE_CHIP_H: f32 = 24.0;
-const COMPOSE_BTN_H: f32 = 28.0;
 const COMPOSE_BTN_W: f32 = 75.0;
 const COMPOSE_ATTACH_W: f32 = 80.0;
 
@@ -991,10 +990,10 @@ fn compose_layout(modal_x: f32, modal_y: f32) -> ComposeLayout {
     let gap = cce_ui::layout::plate_gap();
     let title_y = modal_y + pad;
     let rows0 = title_y + COMPOSE_TITLE_H + gap;
-    let pitch = COMPOSE_FIELD_H + gap;
+    let pitch = cce_ui::layout::textbox_height() + gap;
     let body_y = rows0 + 4.0 * pitch;
     let chips_y = body_y + COMPOSE_BODY_H + gap;
-    let buttons_y = chips_y + COMPOSE_CHIP_H + gap;
+    let buttons_y = chips_y + cce_ui::layout::button_height() + gap;
     let cancel_x = modal_x + COMPOSE_W - pad - COMPOSE_BTN_W;
     ComposeLayout {
         x: modal_x,
@@ -1017,7 +1016,7 @@ fn compose_layout(modal_x: f32, modal_y: f32) -> ComposeLayout {
 /// The dialog's height: the bottom of its button row plus the padding.
 fn compose_h() -> f32 {
     let l = compose_layout(0.0, 0.0);
-    l.buttons_y + COMPOSE_BTN_H + l.pad
+    l.buttons_y + cce_ui::layout::button_height() + l.pad
 }
 
 fn compose_modal_origin(w: f32, h: f32) -> (f32, f32) {
@@ -1043,7 +1042,7 @@ fn compose_chip_rects(attachments: &[String], modal_x: f32, modal_y: f32) -> Vec
         // between paint and hit-test (it is: both use this fn). The 10 past
         // the name is the room the remove mark (the `x` glyph) is drawn in.
         let w = shown.chars().count() as f32 * 6.0 + 2.0 * cce_ui::layout::CONTROL_TEXT_INSET + 10.0;
-        rects.push((x, y, w, COMPOSE_CHIP_H));
+        rects.push((x, y, w, cce_ui::layout::button_height()));
         x += w + l.gap;
     }
     rects
@@ -1070,7 +1069,7 @@ fn detail_chip_rects(atts: &[RemoteAttachment], detail_x: f32, origin_y: f32) ->
     let y = origin_y + DETAIL_CHIPS_Y;
     for att in atts {
         let w = detail_chip_label(att).chars().count() as f32 * 6.0 + 2.0 * cce_ui::layout::CONTROL_TEXT_INSET;
-        rects.push((x, y, w, DETAIL_CHIP_H));
+        rects.push((x, y, w, cce_ui::layout::button_height()));
         x += w + cce_ui::layout::plate_gap();
     }
     rects
@@ -3468,7 +3467,7 @@ impl ClearEmailApp {
         // One padding below the bar's items (an interior gap); the open search
         // band and its own gap push the plate further down. The bottom is a
         // WINDOW edge, so it clears the plate's roll as well.
-        let band = if self.search_open { SEARCH_ROW_H + pane_gap() } else { 0.0 };
+        let band = if self.search_open { cce_ui::layout::textbox_height() + pane_gap() } else { 0.0 };
         let top = menubar_h() + pane_pad() + band;
         (top, (self.height as f32 - top - window_pad()).max(50.0))
     }
@@ -3493,7 +3492,7 @@ impl ClearEmailApp {
             .and_then(|id| self.emails.iter().find(|e| e.id == id))
             .is_some_and(|e| !e.remote_attachments.is_empty());
         if chips {
-            DETAIL_CHIPS_Y + DETAIL_CHIP_H
+            DETAIL_CHIPS_Y + cce_ui::layout::button_height()
         } else {
             DETAIL_LINES_BOTTOM
         }
@@ -3869,7 +3868,8 @@ impl ClearEmailApp {
         let shown = !self.compose_open
             && self.selected_email_id.is_some()
             && self.html_loaded == self.selected_email_id;
-        let y = self.detail_origin_y() + DETAIL_LINES_BOTTOM - COMPOSE_BTN_H;
+        let btn_h = cce_ui::layout::button_height();
+        let y = self.detail_origin_y() + DETAIL_LINES_BOTTOM - btn_h;
         let (px, _, pw, _) = self.detail_pane_geom();
         let mut right = px + pw - pane_inner_pad();
         let view_label = if self.show_text { "View: Text" } else { "View: HTML" };
@@ -3889,7 +3889,7 @@ impl ClearEmailApp {
             // (the floor also stops the toggles jittering between labels).
             let w = btn.intrinsic_size().map_or(0.0, |s| s.width).max(COMPOSE_BTN_W);
             right -= w;
-            btn.set_rect(right, y, w, COMPOSE_BTN_H);
+            btn.set_rect(right, y, w, btn_h);
             right -= cce_ui::layout::plate_gap();
         }
     }
@@ -4223,7 +4223,7 @@ impl ClearEmailApp {
                 for (text, dy, font_size, color) in header_lines {
                     let mut bounds = header_bounds;
                     if let Some(bl) = buttons_left {
-                        let buttons_top = DETAIL_LINES_BOTTOM - COMPOSE_BTN_H;
+                        let buttons_top = DETAIL_LINES_BOTTOM - cce_ui::layout::button_height();
                         if dy + DETAIL_LINE_H > buttons_top {
                             if let Some(b) = bounds.as_mut() {
                                 b[2] = b[2].min(bl - cce_ui::layout::plate_gap());
@@ -4243,7 +4243,7 @@ impl ClearEmailApp {
 
                 // Server-attachment chip labels (quads paint in display_list;
                 // both sides lay out via detail_chip_rects).
-                for (att, (cx, cy, _, _)) in email
+                for (att, (cx, cy, _, ch)) in email
                     .remote_attachments
                     .iter()
                     .zip(detail_chip_rects(&email.remote_attachments, detail_x, self.detail_origin_y()))
@@ -4251,7 +4251,7 @@ impl ClearEmailApp {
                     labels.push(TextLabel {
                         text: detail_chip_label(att),
                         x: cx + cce_ui::layout::CONTROL_TEXT_INSET,
-                        y: cy + 5.0,
+                        y: cy + (ch - 12.0) / 2.0,
                         font_size: 10.0,
                         color: [0xc8, 0xc8, 0xd2],
                     });
@@ -4301,14 +4301,16 @@ impl ClearEmailApp {
             });
 
             for (name, row_y) in ["To:", "Cc:", "Bcc:", "Subject:"].iter().zip(l.rows_y) {
-                // The label's baseline sits on the field's: 4px below its top at this size.
-                labels.push(TextLabel { text: name.to_string(), x: l.label_x, y: row_y + 4.0, font_size: 11.0, color: [0x83, 0x83, 0x8a] });
+                // The label's baseline sits on the field's (the field centres
+                // its text): 4px below its top at a 26px field and this size.
+                let label_y = row_y + (cce_ui::layout::textbox_height() - 18.0) / 2.0;
+                labels.push(TextLabel { text: name.to_string(), x: l.label_x, y: label_y, font_size: 11.0, color: [0x83, 0x83, 0x8a] });
             }
 
             // Attachment chips: the name (its `x` glyph is drawn with the
             // chip's plate), clickable to remove (hit-test in
             // handle_mouse_input via the same compose_chip_rects).
-            for (path, (cx, cy, _cw, _ch)) in self
+            for (path, (cx, cy, _cw, ch)) in self
                 .compose_attachments
                 .iter()
                 .zip(compose_chip_rects(&self.compose_attachments, modal_x, modal_y))
@@ -4320,7 +4322,7 @@ impl ClearEmailApp {
                 labels.push(TextLabel {
                     text: ellipsize(name, 22),
                     x: cx + cce_ui::layout::CONTROL_TEXT_INSET,
-                    y: cy + 6.0,
+                    y: cy + (ch - 12.0) / 2.0,
                     font_size: 10.0,
                     color: [0xc8, 0xc8, 0xd2],
                 });
@@ -4549,7 +4551,7 @@ impl Application for ClearEmailApp {
         // The bar's items are plates of the panes' material, in the same
         // stance: flat, so the face is a quad and carries the frost, and the
         // silhouette is the rect so they line up with the panes.
-        let mail_button = Button::new_icon("mail", "Mail", 0.0, 0.0, BAR_ITEM_H, BAR_ITEM_H)
+        let mail_button = Button::new_icon("mail", "Mail", 0.0, 0.0, bar_item_h(), bar_item_h())
             .with_flat(true)
             .with_bg(pane_fill());
         let folder_dropdown = Dropdown::new(
@@ -4611,9 +4613,10 @@ impl Application for ClearEmailApp {
         compose_body.font_size = 12.0;
         compose_body.font_family = "sans-serif".to_string();
 
-        let btn_compose_send = Button::new(0.0, 0.0, 75.0, 28.0).with_label("Send");
-        let btn_compose_cancel = Button::new_reset(0.0, 0.0, 75.0, 28.0).with_label("Cancel");
-        let btn_compose_attach = Button::new(0.0, 0.0, 80.0, 28.0).with_label("Attach...");
+        let btn_h = cce_ui::layout::button_height();
+        let btn_compose_send = Button::new(0.0, 0.0, COMPOSE_BTN_W, btn_h).with_label("Send");
+        let btn_compose_cancel = Button::new_reset(0.0, 0.0, COMPOSE_BTN_W, btn_h).with_label("Cancel");
+        let btn_compose_attach = Button::new(0.0, 0.0, COMPOSE_ATTACH_W, btn_h).with_label("Attach...");
 
         // A mailto: argv (this is the x-scheme-handler/mailto handler) opens
         // the compose dialog prefilled. Both text and edit_buffer are set,
@@ -5522,11 +5525,14 @@ impl Application for ClearEmailApp {
             // Flat plates, so each rect IS its silhouette and the bar sits on
             // the same grid as the panes with no compensation at all.
             let (edge, gap) = (window_pad(), pane_gap());
-            self.mail_button.set_rect(edge, edge, BAR_ITEM_H, BAR_ITEM_H);
+            // Bottoms on the band's, should button and dropdown heights differ.
+            let (btn_h, dd_h) = (cce_ui::layout::button_height(), cce_ui::layout::dropdown_height());
+            let bottom = edge + bar_item_h();
+            self.mail_button.set_rect(edge, bottom - btn_h, btn_h, btn_h);
             let folder_x = w_f32 - edge - FOLDER_W;
-            self.folder_dropdown.set_rect(folder_x, edge, FOLDER_W, BAR_ITEM_H);
+            self.folder_dropdown.set_rect(folder_x, bottom - dd_h, FOLDER_W, dd_h);
             self.account_dropdown
-                .set_rect(folder_x - gap - ACCOUNT_W, edge, ACCOUNT_W, BAR_ITEM_H);
+                .set_rect(folder_x - gap - ACCOUNT_W, bottom - dd_h, ACCOUNT_W, dd_h);
 
             cce_ui::scale::set_scale_factor(scale as f32);
 
@@ -5534,7 +5540,7 @@ impl Application for ClearEmailApp {
             // when closed so a stale rect can't be hit by anything that still
             // routes to it.
             if self.search_open {
-                self.search_box.set_rect(list_x, menubar_h() + pane_pad(), list_w, SEARCH_ROW_H);
+                self.search_box.set_rect(list_x, menubar_h() + pane_pad(), list_w, cce_ui::layout::textbox_height());
             } else {
                 self.search_box.set_rect(-9999.0, -9999.0, 0.0, 0.0);
             }
@@ -5605,16 +5611,18 @@ impl Application for ClearEmailApp {
                 let (modal_x, modal_y) = compose_modal_origin(w_f32, h_f32);
                 let l = compose_layout(modal_x, modal_y);
 
-                self.compose_to.set_rect(l.field_x, l.rows_y[0], l.field_w, COMPOSE_FIELD_H);
-                self.compose_cc.set_rect(l.field_x, l.rows_y[1], l.field_w, COMPOSE_FIELD_H);
-                self.compose_bcc.set_rect(l.field_x, l.rows_y[2], l.field_w, COMPOSE_FIELD_H);
-                self.compose_subject.set_rect(l.field_x, l.rows_y[3], l.field_w, COMPOSE_FIELD_H);
+                let field_h = cce_ui::layout::textbox_height();
+                self.compose_to.set_rect(l.field_x, l.rows_y[0], l.field_w, field_h);
+                self.compose_cc.set_rect(l.field_x, l.rows_y[1], l.field_w, field_h);
+                self.compose_bcc.set_rect(l.field_x, l.rows_y[2], l.field_w, field_h);
+                self.compose_subject.set_rect(l.field_x, l.rows_y[3], l.field_w, field_h);
                 let (bx, by, bw, bh) = l.body;
                 self.compose_body.set_rect(bx, by, bw, bh);
 
-                self.btn_compose_attach.set_rect(l.attach_x, l.buttons_y, COMPOSE_ATTACH_W, COMPOSE_BTN_H);
-                self.btn_compose_send.set_rect(l.send_x, l.buttons_y, COMPOSE_BTN_W, COMPOSE_BTN_H);
-                self.btn_compose_cancel.set_rect(l.cancel_x, l.buttons_y, COMPOSE_BTN_W, COMPOSE_BTN_H);
+                let btn_h = cce_ui::layout::button_height();
+                self.btn_compose_attach.set_rect(l.attach_x, l.buttons_y, COMPOSE_ATTACH_W, btn_h);
+                self.btn_compose_send.set_rect(l.send_x, l.buttons_y, COMPOSE_BTN_W, btn_h);
+                self.btn_compose_cancel.set_rect(l.cancel_x, l.buttons_y, COMPOSE_BTN_W, btn_h);
             }