| author | |
| committer | |
| log | 0aabcf30b762dc823a9b403a7a514e5a86f62fd5 |
| tree | 6c200c0de16a480278fe568c74603df9c9329a99 |
| parent | 9aaaf21dee13990267048e726313ee04c7861882 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
A long menu or list cut its rows off flat where it ended, so the notebook
menu opened on half of Notebook Recycle Bin with its highlight sliced
through. Rows now fade out over 12 points towards an end where more lie
beyond it, as the section tabs fade at the ends of their row, and arrowing
through the list stops the highlighted row clear of the fade.
Assisted-by: claude-opus-5.53 files changed, 72 insertions(+), 13 deletions(-)
crates/ui/src/lib.rs+19-11| ... | ... | @@ -394,8 +394,8 @@ pub struct Spec<'a> { |
| 394 | 394 | /// paint or input, and its boxes take the group's rectangle, so what opens from them |
| 395 | 395 | /// opens from the form shown. |
| 396 | 396 | pub fold: Option<u32>, |
| 397 | /// How far the children fade out into the box's fill towards its leading and trailing | |
| 398 | /// edges, as a row of them cut there does. | |
| 397 | /// How far the children fade out into the box's fill towards its start and end along | |
| 398 | /// its axis, as a row or list of them cut there does. | |
| 399 | 399 | pub fade: [f32; 2], |
| 400 | 400 | /// What the box is to assistive technology; without one, its text shows as a label and |
| 401 | 401 | /// its children as its parent's. `Ui::access` adds the rest of what it says. |
| ... | ... | @@ -1791,12 +1791,13 @@ impl Ui { |
| 1791 | 1791 | } |
| 1792 | 1792 | } |
| 1793 | 1793 | |
| 1794 | /// Fades box `index`'s children out towards its ends by its `fade`, in steps of a point, | |
| 1795 | /// into its fill however opaque that is: each step clears what lies beneath towards | |
| 1796 | /// transparency and lays the fill over it. | |
| 1794 | /// Fades box `index`'s children out towards its ends along its axis by its `fade`, in | |
| 1795 | /// steps of a point, into its fill however opaque that is: each step clears what lies | |
| 1796 | /// beneath towards transparency and lays the fill over it. | |
| 1797 | 1797 | fn fade(&mut self, index: usize) { |
| 1798 | 1798 | let node = &self.nodes[index]; |
| 1799 | let [left, top, right, bottom] = node.rect; | |
| 1799 | let along = usize::from(node.axis == Axis::Y); | |
| 1800 | let rect = node.rect; | |
| 1800 | 1801 | let fill = node.fill.unwrap_or_default(); |
| 1801 | 1802 | let mut steps = Vec::new(); |
| 1802 | 1803 | for (side, width) in node.fade.into_iter().enumerate() { |
| ... | ... | @@ -1805,15 +1806,22 @@ impl Ui { |
| 1805 | 1806 | // Through the step's middle, eased so the fade leaves the children softly. |
| 1806 | 1807 | let alpha = 1.0 - (step as f32 + 0.5) / count as f32; |
| 1807 | 1808 | let alpha = alpha * alpha * (3.0 - 2.0 * alpha); |
| 1808 | let x = if side == 0 { | |
| 1809 | left + step as f32 | |
| 1809 | let at = if side == 0 { | |
| 1810 | rect[along] + step as f32 | |
| 1810 | 1811 | } else { |
| 1811 | right - 1.0 - step as f32 | |
| 1812 | rect[along + 2] - 1.0 - step as f32 | |
| 1812 | 1813 | }; |
| 1813 | steps.push(([x, top, x + 1.0, bottom], alpha)); | |
| 1814 | let mut band = rect; | |
| 1815 | [band[along], band[along + 2]] = [at, at + 1.0]; | |
| 1816 | steps.push((band, alpha)); | |
| 1814 | 1817 | } |
| 1815 | 1818 | } |
| 1816 | let data = format!("M0 0H1V{}H0Z", bottom - top); | |
| 1819 | let [width, height] = [rect[2] - rect[0], rect[3] - rect[1]]; | |
| 1820 | let data = if along == 0 { | |
| 1821 | format!("M0 0H1V{height}H0Z") | |
| 1822 | } else { | |
| 1823 | format!("M0 0H{width}V1H0Z") | |
| 1824 | }; | |
| 1817 | 1825 | for (rect, alpha) in steps { |
| 1818 | 1826 | // Laid over what the erasing leaves, the fill's share makes up the rest of the |
| 1819 | 1827 | // children's lost opacity: all of it where the fill is opaque, none where clear. |
crates/ui/src/list.rs+12-2| ... | ... | @@ -6,6 +6,8 @@ use winit::keyboard::NamedKey; |
| 6 | 6 | |
| 7 | 7 | /// Room beside a scrolling list's rows for its scrollbar. |
| 8 | 8 | pub(crate) const GUTTER: f32 = 12.0; |
| 9 | /// How far rows fade out towards an end of the list they are cut at. | |
| 10 | const FADE: f32 = 12.0; | |
| 9 | 11 | |
| 10 | 12 | /// Items a list shows, in order. |
| 11 | 13 | pub trait Rows { |
| ... | ... | @@ -181,8 +183,13 @@ pub fn list<R: Rows>( |
| 181 | 183 | } |
| 182 | 184 | if *selected != state.selected { |
| 183 | 185 | if let Some(at) = selected.and_then(|key| rows.find(key)) { |
| 184 | let lowest = (top(at) + height - view).min(top(at)); | |
| 185 | state.target = state.target.clamp(lowest, top(at)).clamp(0.0, most); | |
| 186 | // The selection settles clear of the fades; the list's own ends have none. | |
| 187 | let [first, last] = [ | |
| 188 | top(at) - f64::from(FADE), | |
| 189 | top(at) + height + f64::from(FADE), | |
| 190 | ]; | |
| 191 | let lowest = (last - view).min(first); | |
| 192 | state.target = state.target.clamp(lowest, first).clamp(0.0, most); | |
| 186 | 193 | } |
| 187 | 194 | state.selected = *selected; |
| 188 | 195 | } |
| ... | ... | @@ -199,11 +206,14 @@ pub fn list<R: Rows>( |
| 199 | 206 | // Rows fill all but a gutter for the scrollbar, from their first frame: the padding |
| 200 | 207 | // narrows what they fill, and they float from the corner. |
| 201 | 208 | let gutter = if most > 0.0 { GUTTER / 2.0 } else { 0.0 }; |
| 209 | let fade = |cut: bool| if cut { FADE } else { 0.0 }; | |
| 202 | 210 | ui.open_as( |
| 203 | 211 | id, |
| 204 | 212 | Spec { |
| 205 | 213 | flags: spec.flags | Flags::SCROLL | Flags::CLIP, |
| 214 | axis: Axis::Y, | |
| 206 | 215 | pad: [spec.pad[0] + gutter, spec.pad[1]], |
| 216 | fade: [fade(scroll > 0.5), fade(scroll < most - 0.5)], | |
| 207 | 217 | ..spec |
| 208 | 218 | }, |
| 209 | 219 | ); |
crates/ui/src/tests.rs+41| ... | ... | @@ -1989,6 +1989,47 @@ fn a_long_menu_scrolls_by_dragging_its_thumb() { |
| 1989 | 1989 | ); |
| 1990 | 1990 | } |
| 1991 | 1991 | |
| 1992 | #[test] | |
| 1993 | fn a_long_menu_fades_the_ends_its_rows_are_cut_at_and_its_thumb_reaches_its_end() { | |
| 1994 | let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect(); | |
| 1995 | let items: Vec<_> = names | |
| 1996 | .iter() | |
| 1997 | .map(|text| popup::Item { | |
| 1998 | text, | |
| 1999 | ..popup::Item::default() | |
| 2000 | }) | |
| 2001 | .collect(); | |
| 2002 | let build = |ui: &mut Ui| { | |
| 2003 | frame(ui, |ui| { | |
| 2004 | popup::menu(ui, menu_id(), BELOW, &items, None); | |
| 2005 | }) | |
| 2006 | }; | |
| 2007 | let rows = menu_id().child("rows"); | |
| 2008 | let fade = |ui: &Ui| ui.nodes.iter().find(|node| node.id == rows).unwrap().fade; | |
| 2009 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 2010 | build(&mut ui); | |
| 2011 | ui.open_popup(menu_id()); | |
| 2012 | build(&mut ui); | |
| 2013 | build(&mut ui); | |
| 2014 | assert_eq!(fade(&ui), [0.0, 12.0], "cut only at the bottom"); | |
| 2015 | let thumb = ui.rect(rows.child("bar")).unwrap(); | |
| 2016 | let x = (thumb[0] + thumb[2]) / 2.0; | |
| 2017 | ui.event(Event::PointerMoved([x, thumb[1] + 2.0])); | |
| 2018 | press(&mut ui, Instant::now(), true); | |
| 2019 | build(&mut ui); | |
| 2020 | ui.event(Event::PointerMoved([x, 1000.0])); | |
| 2021 | build(&mut ui); | |
| 2022 | build(&mut ui); | |
| 2023 | assert_eq!(fade(&ui), [12.0, 0.0], "cut only at the top"); | |
| 2024 | let list = ui.rect(rows).unwrap(); | |
| 2025 | let thumb = ui.rect(rows.child("bar")).unwrap(); | |
| 2026 | assert_eq!( | |
| 2027 | thumb[3], | |
| 2028 | list[3] - 4.0, | |
| 2029 | "the thumb ends as far from the bottom as it starts" | |
| 2030 | ); | |
| 2031 | } | |
| 2032 | ||
| 1992 | 2033 | #[test] |
| 1993 | 2034 | fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() { |
| 1994 | 2035 | let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect(); |