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

src/widget/display/list_item.rs (5.9K)

  1 //! `TextItem` (retained cosmic-text buffer holder, unchanged) and the narrow-trait
  2 //! `InteractiveListItem` (Phase 5j): Button-style press/release with themed
  3 //! selected/hover/press overlays and title/subtitle text.
  4 
  5 use crate::colors;
  6 use crate::scene::layout::Rect;
  7 use crate::scene::paint::PaintCtx;
  8 use crate::widget::{Adapted, ElementState, Event, EventCtx, Input, Layout, MouseButton, Paint};
  9 
 10 #[derive(Debug, Clone)]
 11 pub struct TextItem {
 12     pub buffer: cosmic_text::Buffer,
 13     pub x: f32,
 14     pub y: f32,
 15     pub color: cosmic_text::Color,
 16     pub bounds: Option<[f32; 4]>,
 17     /// Optional circular clip `[cx, cy, r]` in logical pixels (a display-list item's
 18     /// `clip_circle` carried through to the glyph pass). `None` for ordinary labels.
 19     pub clip_circle: Option<[f32; 3]>,
 20     /// Optional rounded-rect clip `[cx, cy, bx, by, r]` in logical pixels (a display-list
 21     /// item's `clip_rrect` carried through to the glyph pass). `None` for ordinary labels.
 22     pub clip_rrect: Option<[f32; 5]>,
 23 }
 24 
 25 impl TextItem {
 26     pub fn new(
 27         fs: &mut cosmic_text::FontSystem,
 28         text: &str,
 29         size: f32,
 30         x: f32,
 31         y: f32,
 32         color: cosmic_text::Color,
 33         font: Option<&str>,
 34         bounds: Option<[f32; 4]>,
 35     ) -> Self {
 36         let buffer = crate::backend::window_runner::get_text_buffer(fs, text, size, font);
 37         Self { buffer, x, y, color, bounds, clip_circle: None, clip_rrect: None }
 38     }
 39 }
 40 
 41 #[derive(Debug, Clone)]
 42 pub struct InteractiveListItem {
 43     pub title: String,
 44     pub subtitle: Option<String>,
 45     pub selected: bool,
 46     pub pressed: bool,
 47     pub just_clicked: bool,
 48     hovered: bool,
 49 }
 50 
 51 impl InteractiveListItem {
 52     pub fn new(title: &str) -> Adapted<InteractiveListItem> {
 53         Adapted::new(InteractiveListItem {
 54             title: title.to_string(),
 55             subtitle: None,
 56             selected: false,
 57             pressed: false,
 58             just_clicked: false,
 59             hovered: false,
 60         })
 61     }
 62 
 63     pub fn set_selected(&mut self, selected: bool) {
 64         self.selected = selected;
 65     }
 66 }
 67 
 68 impl Adapted<InteractiveListItem> {
 69     pub fn with_subtitle(mut self, subtitle: &str) -> Self {
 70         self.subtitle = Some(subtitle.to_string());
 71         self
 72     }
 73 }
 74 
 75 impl Layout for InteractiveListItem {
 76     fn inline_label(&self) -> bool {
 77         true
 78     }
 79 }
 80 
 81 impl Paint for InteractiveListItem {
 82     /// A list row's text is in the list font — the rows of a TreeList, a menu's items.
 83     fn widget_font(&self) -> Option<String> {
 84         Some(crate::layout::list_font())
 85     }
 86 
 87     fn color(&self) -> [f32; 4] {
 88         let theme = colors::active_theme();
 89         if self.selected {
 90             let mut base_color = colors::highlight_primary_color();
 91             if self.pressed {
 92                 base_color[3] = (base_color[3] + theme.press_overlay[3]).min(1.0);
 93             } else if self.hovered {
 94                 base_color[3] = (base_color[3] + theme.hover_overlay[3]).min(1.0);
 95             }
 96             base_color
 97         } else if self.pressed {
 98             let mut base_color = theme.surface_bg;
 99             base_color[3] = (base_color[3] + theme.press_overlay[3]).min(1.0);
100             base_color
101         } else if self.hovered {
102             let mut base_color = theme.surface_bg;
103             base_color[3] = (base_color[3] + theme.hover_overlay[3]).min(1.0);
104             base_color
105         } else {
106             [0.0, 0.0, 0.0, 0.0]
107         }
108     }
109 
110     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
111         let col = self.color();
112         if col[3] > 0.0 {
113             // The state wash, rounded like a list-row Button's.
114             let r = crate::layout::button_corner_radius();
115             ctx.rounded_rect(rect, r, (true, true, true, true), col);
116         }
117 
118         let (x, y, h) = (rect.x, rect.y, rect.height);
119         let title_y = if self.subtitle.is_some() {
120             crate::layout::align_text_y(y, h, 22.0, 0.0)
121         } else {
122             crate::layout::align_text_y(y, h, 12.0, 0.0)
123         };
124         let fc = colors::list_font_color();
125         let title_col = [(fc[0] * 255.0) as u8, (fc[1] * 255.0) as u8, (fc[2] * 255.0) as u8];
126         // Clipped to the row. A row's width comes from the LIST, not from its
127         // own text, so a long title or path is routine here — and unbounded it
128         // simply kept drawing past the row's right edge, over the scrollbar and
129         // out of the list.
130         let clip = Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]);
131         ctx.text_with(self.title.clone(), x + 8.0, title_y, 12.0, title_col, None, clip);
132         if let Some(ref sub) = self.subtitle {
133             ctx.text_with(sub.clone(), x + 8.0, title_y + 13.0, 10.0, [140, 140, 153], None, clip);
134         }
135     }
136 }
137 
138 impl Input for InteractiveListItem {
139     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
140         match event {
141             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
142                 self.pressed = true;
143                 true
144             }
145             Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, x, y, .. } => {
146                 if self.pressed && self.hit(ectx.rect, *x, *y) {
147                     self.just_clicked = true;
148                 }
149                 std::mem::take(&mut self.pressed)
150             }
151             Event::MouseEnter => {
152                 // Hover flips are visual changes: report them handled so the
153                 // demand-driven frame loop repaints now — returning false left
154                 // the highlight waiting for the next unrelated rebuild.
155                 self.hovered = true;
156                 true
157             }
158             Event::MouseLeave => {
159                 self.hovered = false;
160                 true
161             }
162             _ => false,
163         }
164     }
165 
166     fn take_click(&mut self) -> bool {
167         std::mem::take(&mut self.just_clicked)
168     }
169 
170     fn set_selected(&mut self, selected: bool) {
171         self.selected = selected;
172     }
173 }