@@ -266,29 +266,41 @@ fn draw_slice_wheel(
266266 // Start at 12 o'clock, clockwise → angle0 = -π/2.
267267 let dir = if reverse { -1.0 } else { 1.0 } ;
268268
269- // Wedge fills
270- for i in 0 ..n {
271- let a0 = -std :: f32 :: consts :: FRAC_PI_2 + ( i as f32 / n as f32 ) * tau * dir ;
272- let a1 = -std :: f32 :: consts :: FRAC_PI_2 + ( ( i + 1 ) as f32 / n as f32 ) * tau * dir ;
273- let active = active_slice. map ( |s| s as usize ) == Some ( i ) ;
269+ if n == 1 {
270+ // Single slice = the whole sample. A triangle-fan "full wedge"
271+ // degenerates (first and last vertex coincide), which renders as
272+ // a visible seam. Paint a proper filled disc instead.
273+ let active = active_slice. is_some ( ) ;
274274 let fill = if active {
275275 egui:: Color32 :: from_gray ( 180 )
276276 } else {
277277 egui:: Color32 :: from_gray ( 40 )
278278 } ;
279- // Tessellate the wedge as a triangle fan around the center.
280- let steps = 12 ;
281- let mut pts = vec ! [ center] ;
282- for k in 0 ..=steps {
283- let t = k as f32 / steps as f32 ;
284- let a = a0 + ( a1 - a0) * t;
285- pts. push ( center + egui:: vec2 ( a. cos ( ) , a. sin ( ) ) * r_outer) ;
279+ painter. circle_filled ( center, r_outer, fill) ;
280+ } else {
281+ for i in 0 ..n {
282+ let a0 = -std:: f32:: consts:: FRAC_PI_2 + ( i as f32 / n as f32 ) * tau * dir;
283+ let a1 = -std:: f32:: consts:: FRAC_PI_2 + ( ( i + 1 ) as f32 / n as f32 ) * tau * dir;
284+ let active = active_slice. map ( |s| s as usize ) == Some ( i) ;
285+ let fill = if active {
286+ egui:: Color32 :: from_gray ( 180 )
287+ } else {
288+ egui:: Color32 :: from_gray ( 40 )
289+ } ;
290+ // Tessellate the wedge as a triangle fan around the center.
291+ let steps = 12 ;
292+ let mut pts = vec ! [ center] ;
293+ for k in 0 ..=steps {
294+ let t = k as f32 / steps as f32 ;
295+ let a = a0 + ( a1 - a0) * t;
296+ pts. push ( center + egui:: vec2 ( a. cos ( ) , a. sin ( ) ) * r_outer) ;
297+ }
298+ painter. add ( egui:: Shape :: convex_polygon (
299+ pts,
300+ fill,
301+ egui:: Stroke :: new ( 0.5 , egui:: Color32 :: from_gray ( 15 ) ) ,
302+ ) ) ;
286303 }
287- painter. add ( egui:: Shape :: convex_polygon (
288- pts,
289- fill,
290- egui:: Stroke :: new ( 0.5 , egui:: Color32 :: from_gray ( 15 ) ) ,
291- ) ) ;
292304 }
293305 // Inner hole
294306 painter. circle_filled ( center, r_inner, egui:: Color32 :: from_gray ( 12 ) ) ;
@@ -416,14 +428,19 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
416428 // Leave room for RND + LD buttons (two small buttons ~48px total
417429 // plus item_spacing). Using a fixed cap keeps the picker from
418430 // pushing outside the 3-cell module width on narrow rack layouts.
431+ // Track the previous path so we can detect a dropdown
432+ // selection AFTER the combobox closes — the earlier
433+ // "selectable_label.clicked() → load inline" pattern
434+ // sometimes lost the click when the combobox rebuilt its
435+ // popup on the same frame. Assigning selectable_value
436+ // directly to `path` and syncing state once after show_ui
437+ // is the more reliable pattern.
438+ let path_before = path. clone ( ) ;
419439 egui:: ComboBox :: from_id_source ( "amen_sample_picker" )
420- // Room for RND + LD + PLAY (3 small buttons).
421- . width ( ( ui. available_width ( ) - 96 .0) . max ( 60.0 ) )
440+ // Room for RANDOM + LOAD + PLAY (3 buttons, wider now ).
441+ . width ( ( ui. available_width ( ) - 150 .0) . max ( 60.0 ) )
422442 . selected_text ( egui:: RichText :: new ( current_name) . monospace ( ) . size ( 8.0 ) )
423443 . show_ui ( ui, |ui| {
424- // Cap the popup height so long sample-pack dirs don't
425- // overflow the rack/panel bounds (the user reported
426- // entries getting cut off). 200px ≈ 14 rows at 8pt.
427444 egui:: ScrollArea :: vertical ( )
428445 . max_height ( 200.0 )
429446 . show ( ui, |ui| {
@@ -434,22 +451,20 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
434451 . and_then ( |n| n. to_str ( ) )
435452 . unwrap_or ( sp_str. as_str ( ) )
436453 . to_string ( ) ;
437- if ui
438- . selectable_label (
439- path == sp_str,
440- egui:: RichText :: new ( name) . monospace ( ) . size ( 8.0 ) ,
441- )
442- . clicked ( )
443- {
444- path = sp_str;
445- app. state . write ( ) . amen . path = path. clone ( ) ;
446- load_and_cache ( app, & path) ;
447- }
454+ ui. selectable_value (
455+ & mut path,
456+ sp_str,
457+ egui:: RichText :: new ( name) . monospace ( ) . size ( 8.0 ) ,
458+ ) ;
448459 }
449460 } ) ;
450461 } ) ;
462+ if path != path_before {
463+ app. state . write ( ) . amen . path = path. clone ( ) ;
464+ load_and_cache ( app, & path) ;
465+ }
451466 if ui
452- . small_button ( egui:: RichText :: new ( "RND " ) . monospace ( ) . size ( 7.0 ) )
467+ . small_button ( egui:: RichText :: new ( "RANDOM " ) . monospace ( ) . size ( 7.0 ) )
453468 . on_hover_text ( "Load a random sample from samples/amen/" )
454469 . clicked ( )
455470 && let Some ( rand_path) = pick_random_sample ( )
@@ -459,7 +474,7 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
459474 load_and_cache ( app, & rand_path) ;
460475 }
461476 if ui
462- . small_button ( egui:: RichText :: new ( "LD " ) . monospace ( ) . size ( 7.0 ) )
477+ . small_button ( egui:: RichText :: new ( "LOAD " ) . monospace ( ) . size ( 7.0 ) )
463478 . on_hover_text ( "Reload the selected sample from disk" )
464479 . clicked ( )
465480 {
@@ -528,8 +543,13 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
528543 load_and_cache ( app, & path) ;
529544 }
530545 let active = current_amen_slice ( app) ;
546+ // Reserve the waveform vertical slot whether or not a sample is
547+ // loaded — otherwise the whole lower half of the panel shifts up
548+ // when loading a WAV, which jitters the knob positions. The rect
549+ // just stays empty until a thumbnail is available.
550+ let wave_h = 66.0 ;
551+ let wave_w = ui. available_width ( ) . min ( 260.0 ) ;
531552 if !app. amen_wave_cache . 1 . is_empty ( ) {
532- let wave_w = ui. available_width ( ) . min ( 260.0 ) ;
533553 let positions_snapshot: Vec < f32 > = app. state . read ( ) . amen . slice_positions . clone ( ) ;
534554 draw_waveform (
535555 ui,
@@ -540,7 +560,22 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
540560 & positions_snapshot,
541561 active,
542562 wave_w,
543- 44.0 ,
563+ wave_h,
564+ ) ;
565+ } else {
566+ // Reserve the same vertical slot so the panel layout doesn't
567+ // shift on load. A thin dark rect gives a visual "no sample"
568+ // hint without drawing anything noisy.
569+ let ( rect, _) = ui. allocate_exact_size ( egui:: vec2 ( wave_w, wave_h) , egui:: Sense :: hover ( ) ) ;
570+ ui. painter ( ) . rect_filled (
571+ rect,
572+ egui:: Rounding :: same ( 2.0 ) ,
573+ egui:: Color32 :: from_gray ( 10 ) ,
574+ ) ;
575+ ui. painter ( ) . rect_stroke (
576+ rect,
577+ egui:: Rounding :: same ( 2.0 ) ,
578+ egui:: Stroke :: new ( 0.5 , egui:: Color32 :: from_gray ( 30 ) ) ,
544579 ) ;
545580 }
546581 // AUTO detects transients and populates AmenState.slice_positions;
@@ -592,20 +627,28 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
592627 // STRETCH). Packing all three into that column frees the lower half
593628 // of the panel for a single knob row.
594629 let host_bpm = app. state . read ( ) . sequencer . bpm ;
630+ // Fixed label column so SLICES / DIR / BPM align even though the
631+ // words differ in length. "SLICES" is the longest at 6 chars.
632+ let label_col = 48.0_f32 ;
633+ let lbl = |ui : & mut egui:: Ui , text : & str | {
634+ let ( rect, _) = ui. allocate_exact_size ( egui:: vec2 ( label_col, 14.0 ) , egui:: Sense :: hover ( ) ) ;
635+ ui. painter ( ) . text (
636+ rect. left_center ( ) ,
637+ egui:: Align2 :: LEFT_CENTER ,
638+ text,
639+ egui:: FontId :: monospace ( 7.5 ) ,
640+ theme:: SMOKE ,
641+ ) ;
642+ } ;
595643 ui. horizontal ( |ui| {
596644 // Small padding around the wheel so it doesn't hug other widgets.
597- ui. add_space ( 4.0 ) ;
598- draw_slice_wheel ( ui, slice_count, active, reverse, loop_mode, 96.0 ) ;
599645 ui. add_space ( 6.0 ) ;
646+ draw_slice_wheel ( ui, slice_count, active, reverse, loop_mode, 144.0 ) ;
647+ ui. add_space ( 8.0 ) ;
600648 ui. vertical ( |ui| {
601649 // SLICES selector
602650 ui. horizontal ( |ui| {
603- ui. label (
604- egui:: RichText :: new ( "SLICES" )
605- . monospace ( )
606- . size ( 7.5 )
607- . color ( theme:: SMOKE ) ,
608- ) ;
651+ lbl ( ui, "SLICES" ) ;
609652 for & n in & [ 1u8 , 2 , 4 , 8 , 16 ] {
610653 let col = if slice_count == n {
611654 theme:: CHALK
@@ -632,12 +675,7 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
632675 // DIR (REV/FWD) + LOOP — label added to the direction toggle
633676 // so the state reads cleanly without needing the hub arrow.
634677 ui. horizontal ( |ui| {
635- ui. label (
636- egui:: RichText :: new ( "DIR" )
637- . monospace ( )
638- . size ( 7.5 )
639- . color ( theme:: SMOKE ) ,
640- ) ;
678+ lbl ( ui, "DIR" ) ;
641679 if widgets:: toggle_button ( ui, if reverse { "REV" } else { "FWD" } , & mut reverse) {
642680 changed = true ;
643681 }
@@ -655,12 +693,7 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
655693 // sequencer.bpm, matching the "default synced" behavior the
656694 // user expects.
657695 ui. horizontal ( |ui| {
658- ui. label (
659- egui:: RichText :: new ( "BPM" )
660- . monospace ( )
661- . size ( 7.5 )
662- . color ( theme:: SMOKE ) ,
663- ) ;
696+ lbl ( ui, "BPM" ) ;
664697 let mut v = source_bpm;
665698 if ui
666699 . add ( egui:: DragValue :: new ( & mut v) . range ( 40.0 ..=300.0 ) . speed ( 0.5 ) )
@@ -688,6 +721,15 @@ pub fn draw_amen(app: &mut ImpulseApp, ui: &mut egui::Ui) {
688721 } ) ;
689722 } ) ;
690723
724+ // Push the knob row down so it's glued to the bottom of the panel
725+ // regardless of whether the waveform / slice-wheel section ended up
726+ // with spare vertical room. Estimate the knob row's own height
727+ // (~40 px including the glass-pane padding) and eat the remaining
728+ // available height above it.
729+ let knob_row_h = 40.0_f32 ;
730+ let spacer = ( ui. available_height ( ) - knob_row_h) . max ( 0.0 ) ;
731+ ui. add_space ( spacer) ;
732+
691733 // ── Knob row — three glass-pane groups on one line ──────────────────────
692734 // LEVEL (vol/pitch) · REGION (start/end) · SHAPE (gate/stutter).
693735 // even_group_width evenly distributes the 3 panes across the panel's
0 commit comments