| 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,8 +394,8 @@ pub struct Spec<'a> { |
| 394 | /// paint or input, and its boxes take the group's rectangle, so what opens from them | 394 | /// paint or input, and its boxes take the group's rectangle, so what opens from them |
| 395 | /// opens from the form shown. | 395 | /// opens from the form shown. |
| 396 | pub fold: Option<u32>, | 396 | pub fold: Option<u32>, |
| 397 | /// How far the children fade out into the box's fill towards its leading and trailing | 397 | /// How far the children fade out into the box's fill towards its start and end along |
| 398 | /// edges, as a row of them cut there does. | 398 | /// its axis, as a row or list of them cut there does. |
| 399 | pub fade: [f32; 2], | 399 | pub fade: [f32; 2], |
| 400 | /// What the box is to assistive technology; without one, its text shows as a label and | 400 | /// What the box is to assistive technology; without one, its text shows as a label and |
| 401 | /// its children as its parent's. `Ui::access` adds the rest of what it says. | 401 | /// its children as its parent's. `Ui::access` adds the rest of what it says. |
| ... | @@ -1791,12 +1791,13 @@ impl Ui { | ... | @@ -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, | 1794 | /// Fades box `index`'s children out towards its ends along its axis by its `fade`, in |
| 1795 | /// into its fill however opaque that is: each step clears what lies beneath towards | 1795 | /// steps of a point, into its fill however opaque that is: each step clears what lies |
| 1796 | /// transparency and lays the fill over it. | 1796 | /// beneath towards transparency and lays the fill over it. |
| 1797 | fn fade(&mut self, index: usize) { | 1797 | fn fade(&mut self, index: usize) { |
| 1798 | let node = &self.nodes[index]; | 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 | let fill = node.fill.unwrap_or_default(); | 1801 | let fill = node.fill.unwrap_or_default(); |
| 1801 | let mut steps = Vec::new(); | 1802 | let mut steps = Vec::new(); |
| 1802 | for (side, width) in node.fade.into_iter().enumerate() { | 1803 | for (side, width) in node.fade.into_iter().enumerate() { |
| ... | @@ -1805,15 +1806,22 @@ impl Ui { | ... | @@ -1805,15 +1806,22 @@ impl Ui { |
| 1805 | // Through the step's middle, eased so the fade leaves the children softly. | 1806 | // Through the step's middle, eased so the fade leaves the children softly. |
| 1806 | let alpha = 1.0 - (step as f32 + 0.5) / count as f32; | 1807 | let alpha = 1.0 - (step as f32 + 0.5) / count as f32; |
| 1807 | let alpha = alpha * alpha * (3.0 - 2.0 * alpha); | 1808 | let alpha = alpha * alpha * (3.0 - 2.0 * alpha); |
| 1808 | let x = if side == 0 { | 1809 | let at = if side == 0 { |
| 1809 | left + step as f32 | 1810 | rect[along] + step as f32 |
| 1810 | } else { | 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 | for (rect, alpha) in steps { | 1825 | for (rect, alpha) in steps { |
| 1818 | // Laid over what the erasing leaves, the fill's share makes up the rest of the | 1826 | // Laid over what the erasing leaves, the fill's share makes up the rest of the |
| 1819 | // children's lost opacity: all of it where the fill is opaque, none where clear. | 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 +6,8 @@ use winit::keyboard::NamedKey; |
| 6 | 6 | ||
| 7 | /// Room beside a scrolling list's rows for its scrollbar. | 7 | /// Room beside a scrolling list's rows for its scrollbar. |
| 8 | pub(crate) const GUTTER: f32 = 12.0; | 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 | /// Items a list shows, in order. | 12 | /// Items a list shows, in order. |
| 11 | pub trait Rows { | 13 | pub trait Rows { |
| ... | @@ -181,8 +183,13 @@ pub fn list<R: Rows>( | ... | @@ -181,8 +183,13 @@ pub fn list<R: Rows>( |
| 181 | } | 183 | } |
| 182 | if *selected != state.selected { | 184 | if *selected != state.selected { |
| 183 | if let Some(at) = selected.and_then(|key| rows.find(key)) { | 185 | if let Some(at) = selected.and_then(|key| rows.find(key)) { |
| 184 | let lowest = (top(at) + height - view).min(top(at)); | 186 | // The selection settles clear of the fades; the list's own ends have none. |
| 185 | state.target = state.target.clamp(lowest, top(at)).clamp(0.0, most); | 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 | state.selected = *selected; | 194 | state.selected = *selected; |
| 188 | } | 195 | } |
| ... | @@ -199,11 +206,14 @@ pub fn list<R: Rows>( | ... | @@ -199,11 +206,14 @@ pub fn list<R: Rows>( |
| 199 | // Rows fill all but a gutter for the scrollbar, from their first frame: the padding | 206 | // Rows fill all but a gutter for the scrollbar, from their first frame: the padding |
| 200 | // narrows what they fill, and they float from the corner. | 207 | // narrows what they fill, and they float from the corner. |
| 201 | let gutter = if most > 0.0 { GUTTER / 2.0 } else { 0.0 }; | 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 | ui.open_as( | 210 | ui.open_as( |
| 203 | id, | 211 | id, |
| 204 | Spec { | 212 | Spec { |
| 205 | flags: spec.flags | Flags::SCROLL | Flags::CLIP, | 213 | flags: spec.flags | Flags::SCROLL | Flags::CLIP, |
| 214 | axis: Axis::Y, | ||
| 206 | pad: [spec.pad[0] + gutter, spec.pad[1]], | 215 | pad: [spec.pad[0] + gutter, spec.pad[1]], |
| 216 | fade: [fade(scroll > 0.5), fade(scroll < most - 0.5)], | ||
| 207 | ..spec | 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,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 | #[test] | 2033 | #[test] |
| 1993 | fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() { | 2034 | fn a_command_menu_opens_at_its_top_and_a_picker_on_its_current_value() { |
| 1994 | let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect(); | 2035 | let names: Vec<String> = (0..40).map(|index| format!("Item {index}")).collect(); |