git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitc1fc81df4e9b8c47607ca00ed1dbc8eeb3942b22
parent947a0641dd
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 21:44
feat: a dropdown trigger wears the field run's edge

The trigger's flush plate was a trough, whose outer half is a compressed
copy of a step; beside a text row's field its edge read differently from
the picker's. It is a field that is all run now (PaintCtx::flush_run,
ControlPlate::with_run_edge), on the trigger, the expanded menu and the
parameter pane's dropdown rows.

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

 CLAUDE.md                             |  10 +++
 src/backend/window_runner.rs          |  23 +++---
 src/layout.rs                         |  35 ++++++---
 src/scene/paint.rs                    |  40 +++++++++-
 src/widget/container/parameters_bg.rs | 144 ++++++++++++++--------------------
 src/widget/input/dropdown.rs          |   5 +-
 6 files changed, 148 insertions(+), 109 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index a08a273..6aea560 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -342,6 +342,16 @@ nothing at the outline, where both sides stand at half the step. Never grouped i
 plate (its profile is not a monotonic step); the overlay's host-box slot carries `split`.
 The legacy banded path and flat hosts (`layout.rs`'s bridge) draw the two-box form.
 
+**A dropdown trigger wears the run's edge too** (the same day): a field that is all run,
+its seam `FIELD_RUN_ONLY` px to its left (`PaintCtx::flush_run`), in place of
+`Prim::Trough` — `ControlPlate::with_run_edge`, which the Dropdown's raised trigger sets,
+the expanded menu's plate (`RenderTarget::flush_run`, which a host without the prim
+answers with its inset plate), and the pane's dropdown rows, now in `fields` (the pane's
+`troughs()` list went: nothing in the pane draws a trough any more). So every flush
+control in a parameter pane has one edge. Other flush controls — buttons, breadcrumbs,
+the font selector — still wear the trough. `a_dropdown_trigger_wears_the_runs_edge` is
+the test.
+
 The pieces that feed it: `ParametersBg::fields` (the pane's list, drawn after its troughs,
 hover-tinted like them; textpick rows and spinboxes with a run are in neither `reliefs`
 nor `troughs`), `TextBox::joined_right` (the box stops at the seam), `Dropdown::set_radii`
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 722da17..f2ea8dd 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2549,22 +2549,27 @@ pub fn tessellate_display_list(
                 // has no blended outline, and the legacy path exists only for
                 // A/B comparison.
                 let all = (true, true, true, true);
-                let lw = (*split - rect.x).max(0.0);
-                push_bevel_edge_vertices_banded(
-                    rect.x, rect.y, lw, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
-                    sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
-                    EdgeKind::Step, &mut verts,
-                );
-                let (rx, rw) = (*split, (rect.x + rect.width - *split).max(0.0));
+                if *split > rect.x {
+                    push_bevel_edge_vertices_banded(
+                        rect.x, rect.y, *split - rect.x, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
+                        sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
+                        EdgeKind::Step, &mut verts,
+                    );
+                }
+                let rx = split.max(rect.x);
+                let rw = (rect.x + rect.width - rx).max(0.0);
+                // All run (`PaintCtx::flush_run`): its own left corners.
+                let (l0, l3) = if *split > rect.x { (0.0, 0.0) } else { (radii.0, radii.3) };
                 let half = *depth * 0.5;
                 push_bevel_edge_vertices_banded(
-                    rx, rect.y, rw, rect.height, (0.0, radii.1, radii.2, 0.0), half,
+                    rx, rect.y, rw, rect.height, (l0, radii.1, radii.2, l3), half,
                     sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), all,
                     EdgeKind::Step, &mut verts,
                 );
                 let ir = (radii.1 - half).max(0.0);
+                let il = if *split > rect.x { 0.0 } else { (radii.0 - half).max(0.0) };
                 push_bevel_edge_vertices_banded(
-                    rx + half, rect.y + half, rw - *depth, rect.height - *depth, (0.0, ir, ir, 0.0), half,
+                    rx + half, rect.y + half, rw - *depth, rect.height - *depth, (il, ir, ir, il), half,
                     sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), all,
                     EdgeKind::Step, &mut verts,
                 );
diff --git a/src/layout.rs b/src/layout.rs
index faefeed..061489f 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4119,6 +4119,15 @@ pub trait RenderTarget {
     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, _tint: [f32; 3]) {
         self.inset_plate(color, x, y, w, h, radius, depth);
     }
+    /// A flush plate with a field run's edge
+    /// ([`crate::scene::paint::PaintCtx::flush_run`]) — the dropdown's grown
+    /// trigger. Hosts without the field prim draw the inset plate.
+    fn flush_run(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: Option<[f32; 3]>) {
+        match tint {
+            Some(t) => self.inset_plate_tinted(color, x, y, w, h, radius, depth, t),
+            None => self.inset_plate(color, x, y, w, h, radius, depth),
+        }
+    }
     /// One step carve from a widget's `paint` ([`ReliefCarve`]) — offered here
     /// for the same reason as `inset_plate`: the legacy `all_quads` stream
     /// carries no relief prims, so a flat-path host never sees them.
@@ -4346,18 +4355,22 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
             // `Prim::Field` replaced — the well to the seam, the run's flush
             // plate past it.
             Prim::Field { rect, radii, depth, split, tint } => {
-                pc.relief_carve(&ReliefCarve {
-                    kind: CarveKind::Recess { tint },
-                    x: rect.x,
-                    y: rect.y,
-                    w: (split - rect.x).max(0.0),
-                    h: rect.height,
-                    radii: (radii.0, 0.0, 0.0, radii.3),
-                    depth,
-                    edges: (true, true, true, true),
-                });
+                // All run and no well (`PaintCtx::flush_run`): the plate alone.
+                if split > rect.x {
+                    pc.relief_carve(&ReliefCarve {
+                        kind: CarveKind::Recess { tint },
+                        x: rect.x,
+                        y: rect.y,
+                        w: split - rect.x,
+                        h: rect.height,
+                        radii: (radii.0, 0.0, 0.0, radii.3),
+                        depth,
+                        edges: (true, true, true, true),
+                    });
+                }
                 let r = radii.1.max(radii.2);
-                let (rx, rw) = (split, (rect.x + rect.width - split).max(0.0));
+                let rx = split.max(rect.x);
+                let rw = (rect.x + rect.width - rx).max(0.0);
                 match tint {
                     Some(t) => pc.inset_plate_tinted([0.0; 4], rx, rect.y, rw, rect.height, r, depth, t),
                     None => pc.inset_plate([0.0; 4], rx, rect.y, rw, rect.height, r, depth),
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 0cb92e8..dcb3233 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -217,6 +217,11 @@ pub enum PlateStance {
     Flat,
 }
 
+/// How far left of a [`Prim::Field`] its seam is put to make it all run and
+/// no well ([`PaintCtx::flush_run`]): far enough that the blend across the
+/// seam and the seam's own wall land nowhere near the field.
+pub const FIELD_RUN_ONLY: f32 = 1.0e4;
+
 /// A control plate: the thing you press, at the control rung of the plate
 /// ladder. One description for every control face — Button, Dropdown,
 /// FontSelector, Breadcrumb, a ButtonStrip's selected plateau — so their
@@ -244,6 +249,12 @@ pub struct ControlPlate {
     /// ring, drawn on the plate's own relief rather than as extra geometry.
     /// `None` untinted.
     pub tint: Option<[f32; 3]>,
+    /// A flush plate's edge in the profile a field's run wears
+    /// ([`PaintCtx::flush_run`]) rather than [`Prim::Trough`]'s: the outer
+    /// half a well's own fall, the inner half mirrored back up to the face.
+    /// The dropdown trigger sets it, so its edge matches the run at the end
+    /// of a text row's field beside it.
+    pub run_edge: bool,
 }
 
 impl ControlPlate {
@@ -251,7 +262,13 @@ impl ControlPlate {
     /// the DE relief width, capped at a fifth of the plate's height.
     pub fn control(rect: Rect, radius: f32, stance: PlateStance, face: Option<Material>) -> Self {
         let depth = crate::layout::bevel_width().min(rect.height * 0.2);
-        Self { rect, radii: (radius, radius, radius, radius), stance, face, depth, tint: None }
+        Self { rect, radii: (radius, radius, radius, radius), stance, face, depth, tint: None, run_edge: false }
+    }
+
+    /// [`Self::run_edge`].
+    pub fn with_run_edge(mut self, on: bool) -> Self {
+        self.run_edge = on;
+        self
     }
 
     /// Light the rim — the focus ring on the plate's silhouette. Pass the
@@ -1285,6 +1302,10 @@ impl PaintCtx {
             }
             PlateStance::Flush => {
                 let (trough, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
+                if plate.run_edge {
+                    self.flush_run(trough, radii, plate.faced(), plate.depth, plate.tint);
+                    return;
+                }
                 match plate.tint {
                     Some(t) => self.inset_plate_tinted(trough, radii, plate.faced(), plate.depth, t),
                     None => self.inset_plate(trough, radii, plate.faced(), plate.depth),
@@ -1424,6 +1445,20 @@ impl PaintCtx {
         self.trough(rect, radii, depth);
     }
 
+    /// A flush plate whose edge is a field's RUN ([`Prim::Field`] with no
+    /// well — its seam put [`FIELD_RUN_ONLY`] px to the left of it): the
+    /// outer half of the edge a well's fall, the inner half that fall
+    /// mirrored back up to the face. [`inset_plate`](Self::inset_plate) with
+    /// that edge in place of [`Prim::Trough`]'s, whose outer half is a
+    /// compressed copy of a step — so a dropdown trigger's edge matches the
+    /// run at the end of a text row's field. `tint` lights it.
+    pub fn flush_run(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32, tint: Option<[f32; 3]>) {
+        if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
+            self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
+        }
+        self.field(rect, radii, depth, rect.x - FIELD_RUN_ONLY, tint);
+    }
+
     /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused flush
     /// control plate's ring (`ControlPlate::with_tint`); the face fill as
     /// there, the trough tinted.
@@ -1844,6 +1879,9 @@ impl crate::layout::RenderTarget for PaintCtx {
     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
         PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth);
     }
+    fn flush_run(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: Option<[f32; 3]>) {
+        PaintCtx::flush_run(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth, tint);
+    }
     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
         PaintCtx::inset_plate_tinted(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth, tint);
     }
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 18d43f2..cad86ee 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1722,19 +1722,13 @@ impl ParametersBg {
             }
             // (control, its configured corner radius, raised vs recessed)
             let ctl: Option<(&dyn WidgetHost, f32, bool)> = if is_text_row(&p.2) {
-                // The textpick picker button is NOT in this list: it is a
-                // flush inset control (face level with the well floor, a
-                // valley seam around it — the dropdown trigger's trough
-                // language, not a boss), and troughs travel through
-                // [`Self::picker_troughs`]. A boss here read as a raised
-                // island, which no other inset control in the DE does.
+                // The textpick picker is NOT in this list: with it the row is
+                // a field ([`Self::fields`]) — a well ending in a flush run.
                 self.texts[i].as_ref().map(|w| (w as &dyn WidgetHost, crate::layout::textbox_corner_radius(), false))
             } else if p.2.starts_with("choice") {
-                // The dropdown trigger is a FLUSH inset control: the widget's
-                // own raised paint is one `inset_plate` (face level with the
-                // plate, a valley seam around it), so its ring travels through
-                // [`Self::troughs`]. A boss here read as a raised island the
-                // widget itself never draws.
+                // The dropdown trigger is a FLUSH control, a field that is all
+                // run ([`Self::fields`]). A boss here read as a raised island
+                // the widget itself never draws.
                 None
             } else if p.2 == "button" {
                 self.buttons[i].as_ref().map(|w| (w as &dyn WidgetHost, crate::layout::button_corner_radius(), true))
@@ -1743,8 +1737,7 @@ impl ParametersBg {
                 // widget's own (`Toggle::flat_carves`, exactly what its paint
                 // emits, in the order it emits them) — ARE the control: the
                 // track's well (a recess) and the plate gliding on its floor
-                // (a boss). Neither is flush, so nothing here rides
-                // [`Self::troughs`].
+                // (a boss). Neither is flush.
                 if let Some(t) = &self.toggles[i] {
                     let (x, y, w, h) = t.rect();
                     if w > 0.0 && h > 0.0 {
@@ -1762,12 +1755,10 @@ impl ParametersBg {
                 }
                 None
             } else if p.2.starts_with("spinbox") {
-                // The well recess only — the -/+ run's trough and its seam
-                // travel through [`Self::troughs`] / [`Self::grooves`] (this
-                // tuple speaks boss/recess). The widget's own `relief_parts`
-                // decides where the well ends: at the run, square there, the
-                // run being the control's right end. Same side-label inset,
-                // same content band, same depth cap as its paint.
+                // A plain well only when there is no -/+ run; with one the
+                // control is a field ([`Self::fields`]) and its -/+ seam a
+                // groove ([`Self::grooves`]). Same side-label inset, same
+                // content band, same depth cap as its paint.
                 if let Some(sb) = &self.spinboxes[i] {
                     let (x, y, w, h) = sb.rect();
                     let ty = sb.label_strip();
@@ -1813,8 +1804,6 @@ impl ParametersBg {
                 // The top-label band stays outside the relief like every other host.
                 let ty = w.label_strip();
                 let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
-                // A text box joined to its picker is square at the seam,
-                // as the box's own carve is (`TextBox::well`).
                 // A text box joined to its picker is half of a field
                 // ([`Self::fields`]), drawn there.
                 if is_text_row(&p.2) && self.texts[i].as_ref().is_some_and(|t| t.inner().joined_right) {
@@ -1826,65 +1815,14 @@ impl ParametersBg {
         out
     }
 
-    /// The textpick picker buttons' trough rings — `(x, y, w, h, radii, depth)`
-    /// for [`crate::scene::paint::PaintCtx::trough_edges`], drawn by the host
-    /// AFTER [`Self::reliefs`]. These are the rows' FLUSH inset controls — the
-    /// textpick picker button, the spinbox's -/+ run, the dropdown trigger
-    /// (the widget's own `inset_plate`): faces
-    /// level with the surface they sit in, so the control reads as part of
-    /// the plate, marked off by its seam alone. On the sides a control adjoins its well, the WELL'S
-    /// OWN WALL is the seam's far side (the face reaches the wall's base and
-    /// that trough edge is suppressed — a lip of its own there doubles the
-    /// valley); only edges facing open floor carve their own wall. They
-    /// cannot ride in [`Self::reliefs`], whose tuple only speaks boss/recess.
-    /// Radii are the well radius's parallel curve at each control's inset;
-    /// depths match the well's carve, so seam and wall read as one family.
-    #[allow(clippy::type_complexity)]
-    pub fn troughs(
-        &self,
-    ) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32, (bool, bool, bool, bool))> {
-        if !self.visible || !crate::layout::control_relief() {
-            return Vec::new();
-        }
-        let mut out = Vec::new();
-        let hidden = self.hidden_rows();
-        for (i, p) in self.display_params.iter().enumerate() {
-            if hidden[i] {
-                continue;
-            }
-            // A textpick row's picker and a spinbox's -/+ run are the flush
-            // ends of fields ([`Self::fields`]), not troughs of their own.
-            if p.2.starts_with("textpick") || p.2.starts_with("spinbox") {
-                continue;
-            }
-            if p.2.starts_with("choice") {
-                // The dropdown trigger: the widget's own raised paint is one
-                // `inset_plate` on its content band — the same ring here, on
-                // the same band (top-label band excluded),
-                // same radius, same depth cap. The face stays the plate: the
-                // pane carries no dropdown fill (a `Border` face never reaches
-                // the rounded-quad view), so the trigger is flush and bare.
-                if let Some(d) = &self.choices[i] {
-                    let (x, y, w, h) = d.rect();
-                    if w > 0.0 && h > 0.0 {
-                        let ty = d.label_strip();
-                        let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
-                        let r = crate::layout::dropdown_corner_radius();
-                        out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, (true, true, true, true)));
-                    }
-                }
-            }
-            // A toggle contributes nothing here: its well and its glider plate
-            // are a recess and a boss, and both ride [`Self::reliefs`].
-        }
-        out
-    }
-
     /// The rows that are ONE field ([`crate::scene::paint::Prim::Field`]):
     /// a sunken well ending in a flush run, one outline round both —
-    /// `(x, y, w, h, radii, depth, split)`, drawn AFTER [`Self::troughs`].
+    /// `(x, y, w, h, radii, depth, split)`, drawn AFTER [`Self::reliefs`].
     /// A textpick row is its text box's well and its picker; a spinbox, its
-    /// value's well and its -/+ run. On the pane's own rule for wells and
+    /// value's well and its -/+ run; a dropdown row, a field that is all run
+    /// (its seam [`crate::scene::paint::FIELD_RUN_ONLY`] to the left), so
+    /// every flush control in the pane has the same edge. They replaced
+    /// `troughs()` on 2026-10-01. On the pane's own rule for wells and
     /// troughs: the content band as the outline, the wall straddling it.
     #[allow(clippy::type_complexity)]
     pub fn fields(&self) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32, f32)> {
@@ -1911,6 +1849,21 @@ impl ParametersBg {
                         out.push((x, y + ty, dx + dw - x, h - ty, (r, r, r, r), depth, dx));
                     }
                 }
+            } else if p.2.starts_with("choice") {
+                // The dropdown trigger: a field that is all run — the
+                // widget's own raised paint (`ControlPlate::with_run_edge`)
+                // on the same band, radius and depth cap. Until 2026-10-01
+                // a trough, whose edge differed from the run's at the end of
+                // a text row's field.
+                if let Some(d) = &self.choices[i] {
+                    let (x, y, w, h) = d.rect();
+                    let ty = d.label_strip();
+                    if w > 0.0 && h - ty > 0.0 {
+                        let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
+                        let r = crate::layout::dropdown_corner_radius();
+                        out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - crate::scene::paint::FIELD_RUN_ONLY));
+                    }
+                }
             } else if p.2.starts_with("spinbox") {
                 if let Some(sb) = &self.spinboxes[i] {
                     let (x, y, w, h) = sb.rect();
@@ -1929,7 +1882,7 @@ impl ParametersBg {
     }
 
     /// The engraved seams companion — `(a, b, width, depth, host)` for
-    /// [`crate::scene::paint::PaintCtx::groove`], drawn AFTER [`Self::troughs`]
+    /// [`crate::scene::paint::PaintCtx::groove`], drawn AFTER [`Self::fields`]
     /// (a groove engraves the surface the trough's control face provides).
     /// Today: the seam dividing a spinbox's -/+ run into its two buttons —
     /// the breadcrumb's segment-seam language at miniature scale.
@@ -2135,14 +2088,6 @@ impl Paint for ParametersBg {
                 (false, false) => ctx.recess_edges(rect, radii, rd, edges),
             }
         }
-        for (tx2, ty2, tw2, th2, radii, td, tedges) in self.troughs() {
-            let rect = Rect { x: tx2, y: ty2, width: tw2, height: th2 };
-            if hovered(tx2, ty2, tw2, th2) {
-                ctx.trough_edges_tinted(rect, radii, td, tedges, Self::HOVER_TINT);
-            } else {
-                ctx.trough_edges(rect, radii, td, tedges);
-            }
-        }
         for (fx, fy, fw, fh, radii, fd, split) in self.fields() {
             let rect = Rect { x: fx, y: fy, width: fw, height: fh };
             let tint = hovered(fx, fy, fw, fh).then_some(Self::HOVER_TINT);
@@ -3923,7 +3868,6 @@ mod tests {
         assert_eq!((fx, fy, fx + fw, fy + fh), (tx, dy, dx + dw, dy + dh), "the field spans box and picker");
         assert_eq!(fradii, (rr, rr, rr, rr), "its own radius at all four corners");
         assert_eq!(split, dx, "the seam is where the picker begins");
-        assert!(p.inner().troughs().iter().all(|t| t.0 != dx), "the picker draws no ring of its own");
         assert!(p.inner().reliefs().iter().all(|r| !(r.0 == tx && r.2 == tw)), "nor the box a well of its own");
 
         // Both text variants lay out at the same row height.
@@ -3943,6 +3887,32 @@ mod tests {
         assert!(py_ >= ty + th - 1.0, "menu hangs below the box");
     }
 
+    /// A dropdown trigger's edge is a field's run: its own paint and the
+    /// pane's re-emission both draw a field that is all run, so its edge
+    /// matches the picker at the end of a text row and the -/+ run of a
+    /// spinbox. Never a trough, whose outer half differed.
+    #[test]
+    fn a_dropdown_trigger_wears_the_runs_edge() {
+        use crate::scene::paint::{PaintCtx, Prim, FIELD_RUN_ONLY};
+        if !crate::layout::control_relief() {
+            return;
+        }
+        let p = panel_with(&[("Mode", "b", "choice:a,b,c")]);
+        let d = p.choices[0].as_ref().expect("a dropdown");
+        let (x, y, w, h) = d.rect();
+        let ty = d.label_strip();
+        let fields = p.inner().fields();
+        assert_eq!(fields.len(), 1, "{fields:?}");
+        let (fx, fy, fw, fh, _, _, split) = fields[0];
+        assert_eq!((fx, fy, fw, fh), (x, y + ty, w, h - ty), "on the trigger's band");
+        assert_eq!(split, x - FIELD_RUN_ONLY, "all run, no well");
+        let mut pc = PaintCtx::new();
+        Paint::paint(d.inner(), Rect { x, y: y + ty, width: w, height: h - ty }, &mut pc);
+        let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+        assert!(prims.iter().any(|p| matches!(p, Prim::Field { rect, split, .. } if *split < rect.x - 100.0)), "{prims:?}");
+        assert!(!prims.iter().any(|p| matches!(p, Prim::Trough { .. })), "no trough");
+    }
+
     #[test]
     fn param_controller_roundtrip_and_row_widgets() {
         let p = panel_with(&[
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 5f98926..b00e51e 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -650,6 +650,9 @@ impl Dropdown {
             )
             .with_radii((r4[0], r4[1], r4[2], r4[3]))
             .with_depth(depth)
+            // The edge a field's run wears, not a trough's: the same edge
+            // as the picker at the end of a text row beside it.
+            .with_run_edge(true)
             .with_tint(self.focused.then(crate::widget::ControlPlate::focus_tint));
             ctx.control_plate(&plate);
             return;
@@ -1082,7 +1085,7 @@ impl Paint for Dropdown {
                 (radius, radius, radius, radius),
                 depth,
             );
-            pc.inset_plate(face, t.x, t.y, t.width, t.height, tr.0, depth);
+            pc.flush_run(face, t.x, t.y, t.width, t.height, tr.0, depth, None);
         } else {
             pc.rect_with_radius(self.border_color(), ux, uy, uw, uh, radius);
             pc.rect_with_radius(face, ux + 1.0, uy + 1.0, uw - 2.0, uh - 2.0, (radius - 1.0).max(0.0));