iced_aw/widget/
selection_list.rs

1//! Display a dropdown list of selectable values.
2pub 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/// A widget for selecting a single value from a dynamic scrollable list of options.
27#[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 for Rendering List.
42    container: Container<'a, Message, Theme, Renderer>,
43    /// List of Elements to Render.
44    options: &'a [T],
45    /// Label Font
46    font: Renderer::Font,
47    /// The Containers Width
48    width: Length,
49    /// The Containers height
50    height: Length,
51    /// The padding Width
52    padding: Padding,
53    /// The Text Size
54    text_size: f32,
55    /// Style for Looks
56    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    /// Creates a new [`SelectionList`] with the given list of `options`,
69    /// the current selected value, and the `message` to produce when an option is
70    /// selected. This will default the `style`, `text_size` and `padding`. use `new_with`
71    /// to set those.
72    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    /// Creates a new [`SelectionList`] with the given list of `options`,
98    /// the current selected value, the message to produce when an option is
99    /// selected, the `style`, `text_size`, `padding` and `font`.
100    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    /// Sets the width of the [`SelectionList`].
144    #[must_use]
145    pub fn width(mut self, width: impl Into<Length>) -> Self {
146        self.width = width.into();
147        self
148    }
149
150    /// Sets the height of the [`SelectionList`].
151    #[must_use]
152    pub fn height(mut self, height: impl Into<Length>) -> Self {
153        self.height = height.into();
154        self
155    }
156
157    /// Sets the style of the [`SelectionList`].
158    #[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    /// Sets the class of the input of the [`SelectionList`].
168    #[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/// A Paragraph cache to enhance speed of layouting.
348#[derive(Default, Clone)]
349pub struct State<P: Paragraph> {
350    values: Vec<paragraph::Plain<P>>,
351}
352
353impl<P: Paragraph> State<P> {
354    /// Creates a new [`State`], representing an unfocused [`TextInput`](iced_widget::TextInput).
355    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}