feat(dropdown): optional icons for dropdowns
This commit is contained in:
parent
52ab37c1eb
commit
9a8a56952d
2 changed files with 59 additions and 33 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
mod appearance;
|
mod appearance;
|
||||||
pub use appearance::{Appearance, StyleSheet};
|
pub use appearance::{Appearance, StyleSheet};
|
||||||
|
|
||||||
use crate::widget::Container;
|
use crate::widget::{icon, Container};
|
||||||
use iced_core::event::{self, Event};
|
use iced_core::event::{self, Event};
|
||||||
use iced_core::layout::{self, Layout};
|
use iced_core::layout::{self, Layout};
|
||||||
use iced_core::text::{self, Text};
|
use iced_core::text::{self, Text};
|
||||||
|
|
@ -24,6 +24,7 @@ where
|
||||||
{
|
{
|
||||||
state: &'a mut State,
|
state: &'a mut State,
|
||||||
options: &'a [S],
|
options: &'a [S],
|
||||||
|
icons: &'a [icon::Handle],
|
||||||
hovered_option: &'a mut Option<usize>,
|
hovered_option: &'a mut Option<usize>,
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||||
|
|
@ -41,6 +42,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
state: &'a mut State,
|
state: &'a mut State,
|
||||||
options: &'a [S],
|
options: &'a [S],
|
||||||
|
icons: &'a [icon::Handle],
|
||||||
hovered_option: &'a mut Option<usize>,
|
hovered_option: &'a mut Option<usize>,
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: impl FnMut(usize) -> Message + 'a,
|
on_selected: impl FnMut(usize) -> Message + 'a,
|
||||||
|
|
@ -49,6 +51,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
||||||
Menu {
|
Menu {
|
||||||
state,
|
state,
|
||||||
options,
|
options,
|
||||||
|
icons,
|
||||||
hovered_option,
|
hovered_option,
|
||||||
selected_option,
|
selected_option,
|
||||||
on_selected: Box::new(on_selected),
|
on_selected: Box::new(on_selected),
|
||||||
|
|
@ -141,6 +144,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
let Menu {
|
let Menu {
|
||||||
state,
|
state,
|
||||||
options,
|
options,
|
||||||
|
icons,
|
||||||
hovered_option,
|
hovered_option,
|
||||||
selected_option,
|
selected_option,
|
||||||
on_selected,
|
on_selected,
|
||||||
|
|
@ -154,6 +158,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||||
|
|
||||||
let mut container = Container::new(Scrollable::new(List {
|
let mut container = Container::new(Scrollable::new(List {
|
||||||
options,
|
options,
|
||||||
|
icons,
|
||||||
hovered_option,
|
hovered_option,
|
||||||
selected_option,
|
selected_option,
|
||||||
on_selected,
|
on_selected,
|
||||||
|
|
@ -268,6 +273,7 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||||
|
|
||||||
struct List<'a, S: AsRef<str>, Message> {
|
struct List<'a, S: AsRef<str>, Message> {
|
||||||
options: &'a [S],
|
options: &'a [S],
|
||||||
|
icons: &'a [icon::Handle],
|
||||||
hovered_option: &'a mut Option<usize>,
|
hovered_option: &'a mut Option<usize>,
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||||
|
|
@ -395,12 +401,12 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
_state: &Tree,
|
state: &Tree,
|
||||||
renderer: &mut crate::Renderer,
|
renderer: &mut crate::Renderer,
|
||||||
theme: &crate::Theme,
|
theme: &crate::Theme,
|
||||||
_style: &renderer::Style,
|
style: &renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
_cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let appearance = theme.appearance(&());
|
let appearance = theme.appearance(&());
|
||||||
|
|
@ -452,6 +458,7 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
iced_core::Svg::new(crate::widget::common::object_select().clone())
|
iced_core::Svg::new(crate::widget::common::object_select().clone())
|
||||||
.color(appearance.selected_text_color)
|
.color(appearance.selected_text_color)
|
||||||
.border_radius(appearance.border_radius);
|
.border_radius(appearance.border_radius);
|
||||||
|
|
||||||
svg::Renderer::draw_svg(
|
svg::Renderer::draw_svg(
|
||||||
renderer,
|
renderer,
|
||||||
svg_handle,
|
svg_handle,
|
||||||
|
|
@ -489,12 +496,25 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
(appearance.text_color, crate::font::default())
|
(appearance.text_color, crate::font::default())
|
||||||
};
|
};
|
||||||
|
|
||||||
let bounds = Rectangle {
|
let mut bounds = Rectangle {
|
||||||
x: bounds.x + self.padding.left,
|
x: bounds.x + self.padding.left,
|
||||||
y: bounds.center_y(),
|
y: bounds.center_y(),
|
||||||
width: f32::INFINITY,
|
width: f32::INFINITY,
|
||||||
..bounds
|
..bounds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(handle) = self.icons.get(i) {
|
||||||
|
let icon_bounds = Rectangle {
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y + 8.0 - (bounds.height / 2.0),
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
bounds.x += 24.0;
|
||||||
|
icon::draw(renderer, handle, icon_bounds);
|
||||||
|
}
|
||||||
|
|
||||||
text::Renderer::fill_text(
|
text::Renderer::fill_text(
|
||||||
renderer,
|
renderer,
|
||||||
Text {
|
Text {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ pub struct Dropdown<'a, S: AsRef<str>, Message> {
|
||||||
on_selected: Box<dyn Fn(usize) -> Message + 'a>,
|
on_selected: Box<dyn Fn(usize) -> Message + 'a>,
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
selections: &'a [S],
|
selections: &'a [S],
|
||||||
|
#[setters]
|
||||||
|
icons: &'a [icon::Handle],
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
#[setters(into)]
|
#[setters(into)]
|
||||||
|
|
@ -55,6 +57,7 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||||
Self {
|
Self {
|
||||||
on_selected: Box::new(on_selected),
|
on_selected: Box::new(on_selected),
|
||||||
selections,
|
selections,
|
||||||
|
icons: &[],
|
||||||
selected,
|
selected,
|
||||||
width: Length::Shrink,
|
width: Length::Shrink,
|
||||||
gap: Self::DEFAULT_GAP,
|
gap: Self::DEFAULT_GAP,
|
||||||
|
|
@ -64,29 +67,6 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||||
font: None,
|
font: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_paragraphs(&self, state: &mut tree::State) {
|
|
||||||
let state = state.downcast_mut::<State>();
|
|
||||||
|
|
||||||
state
|
|
||||||
.selections
|
|
||||||
.resize_with(self.selections.len(), crate::Plain::default);
|
|
||||||
for (i, selection) in self.selections.iter().enumerate() {
|
|
||||||
state.selections[i].update(Text {
|
|
||||||
content: selection.as_ref(),
|
|
||||||
bounds: Size::INFINITY,
|
|
||||||
// TODO use the renderer default size
|
|
||||||
size: iced::Pixels(self.text_size.unwrap_or(14.0)),
|
|
||||||
|
|
||||||
line_height: self.text_line_height,
|
|
||||||
font: self.font.unwrap_or(crate::font::default()),
|
|
||||||
horizontal_alignment: alignment::Horizontal::Left,
|
|
||||||
vertical_alignment: alignment::Vertical::Top,
|
|
||||||
shaping: text::Shaping::Advanced,
|
|
||||||
wrapping: text::Wrapping::default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer>
|
impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer>
|
||||||
|
|
@ -158,6 +138,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.zip(tree.state.downcast_mut::<State>().selections.get_mut(id))
|
.zip(tree.state.downcast_mut::<State>().selections.get_mut(id))
|
||||||
}),
|
}),
|
||||||
|
!self.icons.is_empty(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,6 +198,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
||||||
self.text_line_height,
|
self.text_line_height,
|
||||||
font,
|
font,
|
||||||
self.selected.and_then(|id| self.selections.get(id)),
|
self.selected.and_then(|id| self.selections.get(id)),
|
||||||
|
self.selected.and_then(|id| self.icons.get(id)),
|
||||||
tree.state.downcast_ref::<State>(),
|
tree.state.downcast_ref::<State>(),
|
||||||
viewport,
|
viewport,
|
||||||
);
|
);
|
||||||
|
|
@ -241,6 +223,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
||||||
self.text_line_height,
|
self.text_line_height,
|
||||||
self.font,
|
self.font,
|
||||||
self.selections,
|
self.selections,
|
||||||
|
self.icons,
|
||||||
self.selected,
|
self.selected,
|
||||||
&self.on_selected,
|
&self.on_selected,
|
||||||
translation,
|
translation,
|
||||||
|
|
@ -319,6 +302,7 @@ pub fn layout(
|
||||||
text_line_height: text::LineHeight,
|
text_line_height: text::LineHeight,
|
||||||
font: Option<crate::font::Font>,
|
font: Option<crate::font::Font>,
|
||||||
selection: Option<(&str, &mut crate::Plain)>,
|
selection: Option<(&str, &mut crate::Plain)>,
|
||||||
|
has_icons: bool,
|
||||||
) -> layout::Node {
|
) -> layout::Node {
|
||||||
use std::f32;
|
use std::f32;
|
||||||
|
|
||||||
|
|
@ -346,9 +330,11 @@ pub fn layout(
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let icon_size = if has_icons { 24.0 } else { 0.0 };
|
||||||
|
|
||||||
let size = {
|
let size = {
|
||||||
let intrinsic = Size::new(
|
let intrinsic = Size::new(
|
||||||
max_width + gap + 16.0,
|
max_width + icon_size + gap + 16.0,
|
||||||
f32::from(text_line_height.to_absolute(Pixels(text_size))),
|
f32::from(text_line_height.to_absolute(Pixels(text_size))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -449,6 +435,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
||||||
_text_line_height: text::LineHeight,
|
_text_line_height: text::LineHeight,
|
||||||
_font: Option<crate::font::Font>,
|
_font: Option<crate::font::Font>,
|
||||||
selections: &'a [S],
|
selections: &'a [S],
|
||||||
|
icons: &'a [icon::Handle],
|
||||||
selected_option: Option<usize>,
|
selected_option: Option<usize>,
|
||||||
on_selected: &'a dyn Fn(usize) -> Message,
|
on_selected: &'a dyn Fn(usize) -> Message,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
|
|
@ -459,6 +446,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
||||||
let menu = Menu::new(
|
let menu = Menu::new(
|
||||||
&mut state.menu,
|
&mut state.menu,
|
||||||
selections,
|
selections,
|
||||||
|
icons,
|
||||||
&mut state.hovered_option,
|
&mut state.hovered_option,
|
||||||
selected_option,
|
selected_option,
|
||||||
|option| {
|
|option| {
|
||||||
|
|
@ -473,15 +461,18 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
||||||
selection_paragraph.min_width().round()
|
selection_paragraph.min_width().round()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let pad_width = padding.horizontal().mul_add(2.0, 16.0);
|
||||||
|
|
||||||
|
let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
|
||||||
|
|
||||||
selections
|
selections
|
||||||
.iter()
|
.iter()
|
||||||
.zip(state.selections.iter_mut())
|
.zip(state.selections.iter_mut())
|
||||||
.map(|(label, selection)| measure(label.as_ref(), selection.raw()))
|
.map(|(label, selection)| measure(label.as_ref(), selection.raw()))
|
||||||
.fold(0.0, |next, current| current.max(next))
|
.fold(0.0, |next, current| current.max(next))
|
||||||
+ gap
|
+ gap
|
||||||
+ 16.0
|
+ pad_width
|
||||||
+ padding.horizontal()
|
+ icon_width
|
||||||
+ padding.horizontal()
|
|
||||||
})
|
})
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.text_size(text_size);
|
.text_size(text_size);
|
||||||
|
|
@ -509,6 +500,7 @@ pub fn draw<'a, S>(
|
||||||
text_line_height: text::LineHeight,
|
text_line_height: text::LineHeight,
|
||||||
font: crate::font::Font,
|
font: crate::font::Font,
|
||||||
selected: Option<&'a S>,
|
selected: Option<&'a S>,
|
||||||
|
icon: Option<&'a icon::Handle>,
|
||||||
state: &'a State,
|
state: &'a State,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
) where
|
) where
|
||||||
|
|
@ -550,12 +542,26 @@ pub fn draw<'a, S>(
|
||||||
|
|
||||||
if let Some(content) = selected.map(AsRef::as_ref) {
|
if let Some(content) = selected.map(AsRef::as_ref) {
|
||||||
let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
|
let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
|
||||||
let bounds = Rectangle {
|
|
||||||
|
let mut bounds = Rectangle {
|
||||||
x: bounds.x + padding.left,
|
x: bounds.x + padding.left,
|
||||||
y: bounds.center_y(),
|
y: bounds.center_y(),
|
||||||
width: bounds.width - padding.horizontal(),
|
width: bounds.width - padding.horizontal(),
|
||||||
height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
|
height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(handle) = icon {
|
||||||
|
let icon_bounds = Rectangle {
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y - (bounds.height / 2.0) - 2.0,
|
||||||
|
width: 20.0,
|
||||||
|
height: 20.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
bounds.x += 24.0;
|
||||||
|
icon::draw(renderer, handle, icon_bounds);
|
||||||
|
}
|
||||||
|
|
||||||
text::Renderer::fill_text(
|
text::Renderer::fill_text(
|
||||||
renderer,
|
renderer,
|
||||||
Text {
|
Text {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue