iced_aw/widget/selection_list/
list.rs1use crate::selection_list::Catalog;
4
5use iced_core::{
6 Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle,
7 Shell, Size, Widget,
8 alignment::Vertical,
9 layout::{Limits, Node},
10 mouse::{self, Cursor},
11 renderer, touch,
12 widget::text::{LineHeight, Wrapping},
13 widget::{
14 Tree,
15 tree::{State, Tag},
16 },
17};
18use std::{
19 collections::hash_map::DefaultHasher,
20 fmt::Display,
21 hash::{Hash, Hasher},
22 marker::PhantomData,
23};
24
25#[allow(missing_debug_implementations)]
27pub struct List<'a, T: 'a, Message, Theme, Renderer>
28where
29 T: Clone + Display + Eq + Hash,
30 [T]: ToOwned<Owned = Vec<T>>,
31 Renderer: renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
32 Theme: Catalog,
33{
34 pub options: &'a [T],
36 pub font: Renderer::Font,
39 pub class: <Theme as Catalog>::Class<'a>,
41 pub on_selected: Box<dyn Fn(usize, T) -> Message>,
43 pub padding: Padding,
45 pub text_size: f32,
47 pub selected: Option<usize>,
49 pub phantomdata: PhantomData<Renderer>,
51}
52
53#[derive(Debug, Clone, Default)]
55pub struct ListState {
56 pub hovered_option: Option<usize>,
58 pub last_selected_index: Option<(usize, u64)>,
60 }
63
64impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
65 for List<'_, T, Message, Theme, Renderer>
66where
67 T: Clone + Display + Eq + Hash,
68 Renderer: renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
69 Theme: Catalog,
70{
71 fn tag(&self) -> Tag {
72 Tag::of::<ListState>()
73 }
74
75 fn state(&self) -> State {
76 State::new(ListState::default())
77 }
78
79 fn diff(&self, state: &mut Tree) {
80 let list_state = state.state.downcast_mut::<ListState>();
81
82 if let Some(id) = self.selected {
83 if let Some(option) = self.options.get(id) {
84 let mut hasher = DefaultHasher::new();
85 option.hash(&mut hasher);
86
87 list_state.last_selected_index = Some((id, hasher.finish()));
88 } else {
89 list_state.last_selected_index = None;
90 }
91 } else if let Some((id, hash)) = list_state.last_selected_index {
92 if let Some(option) = self.options.get(id) {
93 let mut hasher = DefaultHasher::new();
94 option.hash(&mut hasher);
95
96 if hash != hasher.finish() {
97 list_state.last_selected_index = None;
98 }
99 } else {
100 list_state.last_selected_index = None;
101 }
102 }
103
104 }
106
107 fn size(&self) -> Size<Length> {
108 Size::new(Length::Fill, Length::Shrink)
109 }
110
111 fn layout(&mut self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
112 use std::f32;
113 let limits = limits.height(Length::Fill).width(Length::Fill);
114
115 #[allow(clippy::cast_precision_loss)]
116 let intrinsic = Size::new(
117 limits.max().width,
118 (self.text_size + self.padding.y()) * self.options.len() as f32,
119 );
120
121 Node::new(intrinsic)
122 }
123
124 fn update(
125 &mut self,
126 state: &mut Tree,
127 event: &Event,
128 layout: Layout<'_>,
129 cursor: Cursor,
130 _renderer: &Renderer,
131 _clipboard: &mut dyn Clipboard,
132 shell: &mut Shell<Message>,
133 _viewport: &Rectangle,
134 ) {
135 let bounds = layout.bounds();
136 let list_state = state.state.downcast_mut::<ListState>();
137 let cursor = cursor.position().unwrap_or_default();
138
139 if bounds.contains(cursor) {
140 match event {
141 Event::Mouse(mouse::Event::CursorMoved { .. }) => {
142 list_state.hovered_option = Some(
143 ((cursor.y - bounds.y) / (self.text_size + self.padding.y())) as usize,
144 );
145
146 shell.request_redraw();
147 }
148 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
149 | Event::Touch(touch::Event::FingerPressed { .. }) => {
150 list_state.hovered_option = Some(
151 ((cursor.y - bounds.y) / (self.text_size + self.padding.y())) as usize,
152 );
153
154 if let Some(index) = list_state.hovered_option
155 && let Some(option) = self.options.get(index)
156 {
157 let mut hasher = DefaultHasher::new();
158 option.hash(&mut hasher);
159 list_state.last_selected_index = Some((index, hasher.finish()));
160 }
161
162 list_state.last_selected_index.iter().for_each(|last| {
163 if let Some(option) = self.options.get(last.0) {
164 shell.publish((self.on_selected)(last.0, option.clone()));
165 shell.capture_event();
166 }
167 });
168
169 shell.request_redraw();
170 }
171 _ => {}
172 }
173 } else if list_state.hovered_option.is_some() {
174 list_state.hovered_option = None;
175 shell.request_redraw();
176 }
177 }
178
179 fn mouse_interaction(
180 &self,
181 _state: &Tree,
182 layout: Layout<'_>,
183 cursor: Cursor,
184 _viewport: &Rectangle,
185 _renderer: &Renderer,
186 ) -> mouse::Interaction {
187 let bounds = layout.bounds();
188
189 if bounds.contains(cursor.position().unwrap_or_default()) {
190 mouse::Interaction::Pointer
191 } else {
192 mouse::Interaction::default()
193 }
194 }
195
196 fn draw(
197 &self,
198 state: &Tree,
199 renderer: &mut Renderer,
200 theme: &Theme,
201 _style: &renderer::Style,
202 layout: Layout<'_>,
203 _cursor: Cursor,
204 viewport: &Rectangle,
205 ) {
206 use std::f32;
207
208 let bounds = layout.bounds();
209 let option_height = self.text_size + self.padding.y();
210 let offset = viewport.y - bounds.y;
211 let start = (offset / option_height) as usize;
212 let end = ((offset + viewport.height) / option_height).ceil() as usize;
213 let list_state = state.state.downcast_ref::<ListState>();
214
215 for i in start..end.min(self.options.len()) {
216 let is_selected = list_state.last_selected_index.is_some_and(|u| u.0 == i);
217 let is_hovered = list_state.hovered_option == Some(i);
218
219 let bounds = Rectangle {
220 x: bounds.x,
221 y: bounds.y + option_height * i as f32,
222 width: bounds.width,
223 height: self.text_size + self.padding.y(),
224 };
225
226 if (is_selected || is_hovered) && (bounds.width > 0.) && (bounds.height > 0.) {
227 renderer.fill_quad(
228 renderer::Quad {
229 bounds,
230 border: Border {
231 radius: (0.0).into(),
232 width: 0.0,
233 color: Color::TRANSPARENT,
234 },
235 ..renderer::Quad::default()
236 },
237 if is_selected {
238 theme
239 .style(&self.class, crate::style::Status::Selected)
240 .background
241 } else {
242 theme
243 .style(&self.class, crate::style::Status::Hovered)
244 .background
245 },
246 );
247 }
248
249 let text_color = if is_selected {
250 theme
251 .style(&self.class, crate::style::Status::Selected)
252 .text_color
253 } else if is_hovered {
254 theme
255 .style(&self.class, crate::style::Status::Hovered)
256 .text_color
257 } else {
258 theme
259 .style(&self.class, crate::style::Status::Active)
260 .text_color
261 };
262
263 renderer.fill_text(
264 iced_core::text::Text {
265 content: self.options[i].to_string(),
266 bounds: Size::new(f32::INFINITY, bounds.height),
267 size: Pixels(self.text_size),
268 font: self.font,
269 align_x: iced_widget::text::Alignment::Left,
270 align_y: Vertical::Center,
271 line_height: LineHeight::default(),
272 shaping: iced_widget::text::Shaping::Advanced,
273 wrapping: Wrapping::default(),
274 },
275 Point::new(bounds.x, bounds.center_y()),
276 text_color,
277 bounds,
278 );
279 }
280 }
281}
282
283impl<'a, T, Message, Theme, Renderer> From<List<'a, T, Message, Theme, Renderer>>
284 for Element<'a, Message, Theme, Renderer>
285where
286 T: Clone + Display + Eq + Hash,
287 Message: 'a,
288 Renderer: 'a + renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
289 Theme: 'a + Catalog,
290{
291 fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self {
292 Element::new(list)
293 }
294}