1pub mod list;
3use crate::style::{
4 Status, StyleFn,
5 selection_list::{Catalog, Style},
6};
7
8use iced_core::{
9 Border, Clipboard, Element, Event, Font, Layout, Length, Padding, Pixels, Rectangle, Shell,
10 Size, Widget,
11 alignment::Vertical,
12 layout::{Limits, Node},
13 mouse::{self, Cursor},
14 renderer,
15 text::{Paragraph, Text, paragraph},
16 widget::{Tree, tree},
17};
18use iced_widget::{
19 Container, Scrollable, container, scrollable,
20 text::{self, LineHeight, Wrapping},
21};
22use std::{fmt::Display, hash::Hash, marker::PhantomData};
23
24pub use list::List;
25
26#[allow(missing_debug_implementations)]
28#[allow(clippy::type_repetition_in_bounds)]
29pub struct SelectionList<
30 'a,
31 T,
32 Message,
33 Theme = iced_widget::Theme,
34 Renderer = iced_widget::Renderer,
35> where
36 T: Clone + ToString + Eq + Hash,
37 [T]: ToOwned<Owned = Vec<T>>,
38 Renderer: renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
39 Theme: Catalog + container::Catalog,
40{
41 container: Container<'a, Message, Theme, Renderer>,
43 options: &'a [T],
45 font: Renderer::Font,
47 width: Length,
49 height: Length,
51 padding: Padding,
53 text_size: f32,
55 class: <Theme as Catalog>::Class<'a>,
57}
58
59#[allow(clippy::type_repetition_in_bounds)]
60impl<'a, T, Message, Theme, Renderer> SelectionList<'a, T, Message, Theme, Renderer>
61where
62 Message: 'a + Clone,
63 Renderer: 'a + renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
64 Theme: 'a + Catalog + container::Catalog + scrollable::Catalog,
65 T: Clone + Display + Eq + Hash,
66 [T]: ToOwned<Owned = Vec<T>>,
67{
68 pub fn new(options: &'a [T], on_selected: impl Fn(usize, T) -> Message + 'static) -> Self {
73 let container = Container::new(Scrollable::new(List {
74 options,
75 font: Font::default(),
76 text_size: 12.0,
77 padding: 5.0.into(),
78 class: <Theme as Catalog>::default(),
79 on_selected: Box::new(on_selected),
80 selected: None,
81 phantomdata: PhantomData,
82 }))
83 .padding(1);
84
85 Self {
86 options,
87 font: Font::default(),
88 class: <Theme as Catalog>::default(),
89 container,
90 width: Length::Fill,
91 height: Length::Fill,
92 padding: 5.0.into(),
93 text_size: 12.0,
94 }
95 }
96
97 pub fn new_with(
101 options: &'a [T],
102 on_selected: impl Fn(usize, T) -> Message + 'static,
103 text_size: f32,
104 padding: impl Into<Padding>,
105 style: impl Fn(&Theme, Status) -> Style + 'a + Clone,
106 selected: Option<usize>,
107 font: Font,
108 ) -> Self
109 where
110 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme, Style>>,
111 {
112 let class: <Theme as Catalog>::Class<'a> =
113 (Box::new(style.clone()) as StyleFn<'a, Theme, Style>).into();
114 let class2: <Theme as Catalog>::Class<'a> =
115 (Box::new(style) as StyleFn<'a, Theme, Style>).into();
116
117 let padding = padding.into();
118
119 let container = Container::new(Scrollable::new(List {
120 options,
121 font,
122 text_size,
123 padding,
124 class: class2,
125 selected,
126 on_selected: Box::new(on_selected),
127 phantomdata: PhantomData,
128 }))
129 .padding(1);
130
131 Self {
132 options,
133 font,
134 class,
135 container,
136 width: Length::Fill,
137 height: Length::Fill,
138 padding,
139 text_size,
140 }
141 }
142
143 #[must_use]
145 pub fn width(mut self, width: impl Into<Length>) -> Self {
146 self.width = width.into();
147 self
148 }
149
150 #[must_use]
152 pub fn height(mut self, height: impl Into<Length>) -> Self {
153 self.height = height.into();
154 self
155 }
156
157 #[must_use]
159 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
160 where
161 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme, Style>>,
162 {
163 self.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into();
164 self
165 }
166
167 #[must_use]
169 pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
170 self.class = class.into();
171 self
172 }
173}
174
175impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
176 for SelectionList<'a, T, Message, Theme, Renderer>
177where
178 T: 'a + Clone + ToString + Eq + Hash + Display,
179 Message: 'static,
180 Renderer: renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font> + 'a,
181 Theme: Catalog + container::Catalog,
182{
183 fn children(&self) -> Vec<Tree> {
184 vec![Tree::new(&self.container as &dyn Widget<_, _, _>)]
185 }
186
187 fn diff(&self, tree: &mut Tree) {
188 tree.diff_children(&[&self.container as &dyn Widget<_, _, _>]);
189 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
190
191 state.values = self
192 .options
193 .iter()
194 .map(|_| paragraph::Plain::<Renderer::Paragraph>::default())
195 .collect();
196 }
197
198 fn size(&self) -> Size<Length> {
199 Size::new(self.width, Length::Shrink)
200 }
201
202 fn tag(&self) -> tree::Tag {
203 tree::Tag::of::<State<Renderer::Paragraph>>()
204 }
205
206 fn state(&self) -> tree::State {
207 tree::State::new(State::<Renderer::Paragraph>::new(self.options))
208 }
209
210 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
211 use std::f32;
212
213 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
214
215 let limits = limits.width(self.width).height(self.height);
216
217 let max_width = match self.width {
218 Length::Shrink => self
219 .options
220 .iter()
221 .enumerate()
222 .map(|(id, val)| {
223 let s: &str = &val.to_string();
224 let text = Text {
225 content: s,
226 size: Pixels(self.text_size),
227 line_height: LineHeight::default(),
228 bounds: Size::INFINITE,
229 font: self.font,
230 align_x: text::Alignment::Left,
231 align_y: Vertical::Top,
232 shaping: text::Shaping::Advanced,
233 wrapping: Wrapping::default(),
234 };
235
236 let _ = state.values[id].update(text);
237 (state.values[id].min_bounds().width + self.padding.x()).round() as u32
238 })
239 .max()
240 .unwrap_or(100),
241 _ => limits.max().width as u32,
242 };
243
244 let limits = limits.max_width(max_width as f32 + self.padding.x());
245
246 let content = self
247 .container
248 .layout(&mut tree.children[0], renderer, &limits);
249 let size = limits.resolve(self.width, self.height, content.size());
250 Node::with_children(size, vec![content])
251 }
252
253 fn update(
254 &mut self,
255 state: &mut Tree,
256 event: &Event,
257 layout: Layout<'_>,
258 cursor: Cursor,
259 renderer: &Renderer,
260 clipboard: &mut dyn Clipboard,
261 shell: &mut Shell<Message>,
262 viewport: &Rectangle,
263 ) {
264 self.container.update(
265 &mut state.children[0],
266 event,
267 layout
268 .children()
269 .next()
270 .expect("Scrollable Child Missing in Selection List"),
271 cursor,
272 renderer,
273 clipboard,
274 shell,
275 viewport,
276 );
277 }
278
279 fn mouse_interaction(
280 &self,
281 state: &Tree,
282 layout: Layout<'_>,
283 cursor: Cursor,
284 viewport: &Rectangle,
285 renderer: &Renderer,
286 ) -> mouse::Interaction {
287 self.container
288 .mouse_interaction(&state.children[0], layout, cursor, viewport, renderer)
289 }
290
291 fn draw(
292 &self,
293 state: &Tree,
294 renderer: &mut Renderer,
295 theme: &Theme,
296 style: &renderer::Style,
297 layout: Layout<'_>,
298 cursor: Cursor,
299 viewport: &Rectangle,
300 ) {
301 let bounds = layout.bounds();
302 let style_sheet = <Theme as Catalog>::style(theme, &self.class, Status::Active);
303
304 if let Some(clipped_viewport) = bounds.intersection(viewport) {
305 renderer.fill_quad(
306 renderer::Quad {
307 bounds,
308 border: Border {
309 radius: (0.0).into(),
310 width: style_sheet.border_width,
311 color: style_sheet.border_color,
312 },
313 ..renderer::Quad::default()
314 },
315 style_sheet.background,
316 );
317
318 self.container.draw(
319 &state.children[0],
320 renderer,
321 theme,
322 style,
323 layout
324 .children()
325 .next()
326 .expect("Scrollable Child Missing in Selection List"),
327 cursor,
328 &clipped_viewport,
329 );
330 }
331 }
332}
333
334impl<'a, T, Message, Theme, Renderer> From<SelectionList<'a, T, Message, Theme, Renderer>>
335 for Element<'a, Message, Theme, Renderer>
336where
337 T: Clone + ToString + Eq + Hash + Display,
338 Message: 'static,
339 Renderer: 'a + renderer::Renderer + iced_core::text::Renderer<Font = iced_core::Font>,
340 Theme: 'a + Catalog + container::Catalog,
341{
342 fn from(selection_list: SelectionList<'a, T, Message, Theme, Renderer>) -> Self {
343 Element::new(selection_list)
344 }
345}
346
347#[derive(Default, Clone)]
349pub struct State<P: Paragraph> {
350 values: Vec<paragraph::Plain<P>>,
351}
352
353impl<P: Paragraph> State<P> {
354 pub fn new<T>(options: &[T]) -> Self
356 where
357 T: Clone + Display + Eq + Hash,
358 [T]: ToOwned<Owned = Vec<T>>,
359 {
360 Self {
361 values: options
362 .iter()
363 .map(|_| paragraph::Plain::<P>::default())
364 .collect(),
365 }
366 }
367}