diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 64e9604ab0c933d64339386d0954cfcb76c055ff..c8d6ac4f0d910d8a7df9b2d2f44984fd5a9a1a83 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -394,8 +394,8 @@ pub struct Spec<'a> { /// paint or input, and its boxes take the group's rectangle, so what opens from them /// opens from the form shown. pub fold: Option, - /// How far the children fade out into the box's fill towards its leading and trailing - /// edges, as a row of them cut there does. + /// How far the children fade out into the box's fill towards its start and end along + /// its axis, as a row or list of them cut there does. pub fade: [f32; 2], /// What the box is to assistive technology; without one, its text shows as a label and /// its children as its parent's. `Ui::access` adds the rest of what it says. @@ -1791,12 +1791,13 @@ impl Ui { } } - /// Fades box `index`'s children out towards its ends by its `fade`, in steps of a point, - /// into its fill however opaque that is: each step clears what lies beneath towards - /// transparency and lays the fill over it. + /// Fades box `index`'s children out towards its ends along its axis by its `fade`, in + /// steps of a point, into its fill however opaque that is: each step clears what lies + /// beneath towards transparency and lays the fill over it. fn fade(&mut self, index: usize) { let node = &self.nodes[index]; - let [left, top, right, bottom] = node.rect; + let along = usize::from(node.axis == Axis::Y); + let rect = node.rect; let fill = node.fill.unwrap_or_default(); let mut steps = Vec::new(); for (side, width) in node.fade.into_iter().enumerate() { @@ -1805,15 +1806,22 @@ impl Ui { // Through the step's middle, eased so the fade leaves the children softly. let alpha = 1.0 - (step as f32 + 0.5) / count as f32; let alpha = alpha * alpha * (3.0 - 2.0 * alpha); - let x = if side == 0 { - left + step as f32 + let at = if side == 0 { + rect[along] + step as f32 } else { - right - 1.0 - step as f32 + rect[along + 2] - 1.0 - step as f32 }; - steps.push(([x, top, x + 1.0, bottom], alpha)); + let mut band = rect; + [band[along], band[along + 2]] = [at, at + 1.0]; + steps.push((band, alpha)); } } - let data = format!("M0 0H1V{}H0Z", bottom - top); + let [width, height] = [rect[2] - rect[0], rect[3] - rect[1]]; + let data = if along == 0 { + format!("M0 0H1V{height}H0Z") + } else { + format!("M0 0H{width}V1H0Z") + }; for (rect, alpha) in steps { // Laid over what the erasing leaves, the fill's share makes up the rest of the // children's lost opacity: all of it where the fill is opaque, none where clear. diff --git a/crates/ui/src/list.rs b/crates/ui/src/list.rs index 9ab81d72671a2c69a90f9c0b894e5a758807a744..f2239326690c3c4c58d91304f11c15aa5b336f40 100644 --- a/crates/ui/src/list.rs +++ b/crates/ui/src/list.rs @@ -6,6 +6,8 @@ use winit::keyboard::NamedKey; /// Room beside a scrolling list's rows for its scrollbar. pub(crate) const GUTTER: f32 = 12.0; +/// How far rows fade out towards an end of the list they are cut at. +const FADE: f32 = 12.0; /// Items a list shows, in order. pub trait Rows { @@ -181,8 +183,13 @@ pub fn list( } if *selected != state.selected { if let Some(at) = selected.and_then(|key| rows.find(key)) { - let lowest = (top(at) + height - view).min(top(at)); - state.target = state.target.clamp(lowest, top(at)).clamp(0.0, most); + // The selection settles clear of the fades; the list's own ends have none. + let [first, last] = [ + top(at) - f64::from(FADE), + top(at) + height + f64::from(FADE), + ]; + let lowest = (last - view).min(first); + state.target = state.target.clamp(lowest, first).clamp(0.0, most); } state.selected = *selected; } @@ -199,11 +206,14 @@ pub fn list( // Rows fill all but a gutter for the scrollbar, from their first frame: the padding // narrows what they fill, and they float from the corner. let gutter = if most > 0.0 { GUTTER / 2.0 } else { 0.0 }; + let fade = |cut: bool| if cut { FADE } else { 0.0 }; ui.open_as( id, Spec { flags: spec.flags | Flags::SCROLL | Flags::CLIP, + axis: Axis::Y, pad: [spec.pad[0] + gutter, spec.pad[1]], + fade: [fade(scroll > 0.5), fade(scroll < most - 0.5)], ..spec }, ); diff --git a/crates/ui/src/tests.rs b/crates/ui/src/tests.rs index 84c3b4b8c2831a0e04740111aeff06aec4175e13..3bc93204a6bb413a7b51552742e7562a42105736 100644 --- a/crates/ui/src/tests.rs +++ b/crates/ui/src/tests.rs @@ -1989,6 +1989,47 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() { ); } +#[test] +fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end() { + let names: Vec = (0..40).map(|index| format!("Item {index}")).collect(); + let items: Vec<_> = names + .iter() + .map(|text| popup::Item { + text, + ..popup::Item::default() + }) + .collect(); + let build = |ui: &mut Ui| { + frame(ui, |ui| { + popup::menu(ui, menu_id(), BELOW, &items, None); + }) + }; + let rows = menu_id().child("rows"); + let fade = |ui: &Ui| ui.nodes.iter().find(|node| node.id == rows).unwrap().fade; + let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); + build(&mut ui); + ui.open_popup(menu_id()); + build(&mut ui); + build(&mut ui); + assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom"); + let thumb = ui.rect(rows.child("bar")).unwrap(); + let x = (thumb[0] + thumb[2]) / 2.0; + ui.event(Event::PointerMoved([x, thumb[1] + 2.0])); + press(&mut ui, Instant::now(), true); + build(&mut ui); + ui.event(Event::PointerMoved([x, 1000.0])); + build(&mut ui); + build(&mut ui); + assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top"); + let list = ui.rect(rows).unwrap(); + let thumb = ui.rect(rows.child("bar")).unwrap(); + assert_eq!( + thumb[3], + list[3] - 4.0, + "the thumb ends as far from the bottom as it starts" + ); +} + #[test] fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() { let names: Vec = (0..40).map(|index| format!("Item {index}")).collect();