feat: Tooltips and Better Surface Management
This commit is contained in:
parent
c7edd37b03
commit
337b80d4ca
90 changed files with 3651 additions and 977 deletions
|
|
@ -183,7 +183,7 @@ pub fn about<'a, Message: Clone + 'static>(
|
|||
.align_y(Alignment::Center),
|
||||
)
|
||||
.class(crate::theme::Button::Text)
|
||||
.on_press(on_url_press(url.unwrap_or(String::new())))
|
||||
.on_press(on_url_press(url.unwrap_or_default()))
|
||||
.width(Length::Fill),
|
||||
)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ use iced_core::{
|
|||
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget,
|
||||
};
|
||||
|
||||
use iced_widget::container;
|
||||
pub use iced_widget::container::{Catalog, Style};
|
||||
|
||||
pub fn aspect_ratio_container<'a, Message: 'static, T>(
|
||||
|
|
@ -35,7 +34,7 @@ where
|
|||
container: Container<'a, Message, crate::Theme, Renderer>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer>
|
||||
impl<Message, Renderer> AspectRatio<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
@ -146,8 +145,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for AspectRatio<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for AspectRatio<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Autosize<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Autosize<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn icon<'a, Message>(handle: impl Into<Handle>) -> Button<'a, Message> {
|
|||
})
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
impl<Message> Button<'_, Message> {
|
||||
pub fn new(icon: Icon) -> Self {
|
||||
let guard = crate::theme::THEME.lock().unwrap();
|
||||
let theme = guard.cosmic();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{Builder, Style};
|
||||
use super::Builder;
|
||||
use crate::{
|
||||
widget::{self, image::Handle},
|
||||
Element,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub struct Builder<'a, Message, Variant> {
|
|||
variant: Variant,
|
||||
}
|
||||
|
||||
impl<'a, Message, Variant> Builder<'a, Message, Variant> {
|
||||
impl<Message, Variant> Builder<'_, Message, Variant> {
|
||||
/// Set the value of [`on_press`] as either `Some` or `None`.
|
||||
pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
|
||||
self.on_press = on_press;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use super::{Builder, ButtonClass, Style};
|
||||
use super::{Builder, ButtonClass};
|
||||
use crate::widget::{icon, row, tooltip};
|
||||
use crate::{ext::CollectionWidget, Element};
|
||||
use apply::Apply;
|
||||
|
|
@ -42,6 +42,12 @@ pub struct Text {
|
|||
pub(super) trailing_icon: Option<icon::Handle>,
|
||||
}
|
||||
|
||||
impl Default for Text {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -51,7 +57,7 @@ impl Text {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
impl<Message> Button<'_, Message> {
|
||||
pub fn new(text: Text) -> Self {
|
||||
let guard = crate::theme::THEME.lock().unwrap();
|
||||
let theme = guard.cosmic();
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ pub struct Button<'a, Message> {
|
|||
selected: bool,
|
||||
style: crate::theme::Button,
|
||||
variant: Variant<Message>,
|
||||
force_enabled: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
|
|
@ -77,6 +78,7 @@ impl<'a, Message> Button<'a, Message> {
|
|||
selected: false,
|
||||
style: crate::theme::Button::default(),
|
||||
variant: Variant::Normal,
|
||||
force_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +92,7 @@ impl<'a, Message> Button<'a, Message> {
|
|||
name: None,
|
||||
#[cfg(feature = "a11y")]
|
||||
description: None,
|
||||
force_enabled: false,
|
||||
#[cfg(feature = "a11y")]
|
||||
label: None,
|
||||
content: content.into(),
|
||||
|
|
@ -163,6 +166,12 @@ impl<'a, Message> Button<'a, Message> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the the [`Button`] to enabled whether or not it has handlers for on press.
|
||||
pub fn force_enabled(mut self, enabled: bool) -> Self {
|
||||
self.force_enabled = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the widget to a selected state.
|
||||
///
|
||||
/// Displays a selection indicator on image buttons.
|
||||
|
|
@ -348,7 +357,8 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
|||
|
||||
let mut headerbar_alpha = None;
|
||||
|
||||
let is_enabled = self.on_press.is_some() || self.on_press_down.is_some();
|
||||
let is_enabled =
|
||||
self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled;
|
||||
let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p));
|
||||
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
|
@ -583,12 +593,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
|||
}
|
||||
match self.description.as_ref() {
|
||||
Some(iced_accessibility::Description::Id(id)) => {
|
||||
node.set_described_by(
|
||||
id.iter()
|
||||
.cloned()
|
||||
.map(|id| NodeId::from(id))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
node.set_described_by(id.iter().cloned().map(NodeId::from).collect::<Vec<_>>());
|
||||
}
|
||||
Some(iced_accessibility::Description::Text(text)) => {
|
||||
node.set_description(text.clone());
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl CalendarModel {
|
|||
let now = Local::now();
|
||||
let naive_now = NaiveDate::from(now.naive_local());
|
||||
CalendarModel {
|
||||
selected: naive_now.clone(),
|
||||
selected: naive_now,
|
||||
visible: naive_now,
|
||||
}
|
||||
}
|
||||
|
|
@ -65,36 +65,34 @@ impl CalendarModel {
|
|||
pub fn show_prev_month(&mut self) {
|
||||
let prev_month_date = self
|
||||
.visible
|
||||
.clone()
|
||||
.checked_sub_months(Months::new(1))
|
||||
.expect("valid naivedate");
|
||||
|
||||
self.visible = prev_month_date.clone();
|
||||
self.visible = prev_month_date;
|
||||
}
|
||||
|
||||
pub fn show_next_month(&mut self) {
|
||||
let next_month_date = self
|
||||
.visible
|
||||
.clone()
|
||||
.checked_add_months(Months::new(1))
|
||||
.expect("valid naivedate");
|
||||
|
||||
self.visible = next_month_date.clone();
|
||||
self.visible = next_month_date;
|
||||
}
|
||||
|
||||
pub fn set_prev_month(&mut self) {
|
||||
self.show_prev_month();
|
||||
self.selected = self.visible.clone();
|
||||
self.selected = self.visible;
|
||||
}
|
||||
|
||||
pub fn set_next_month(&mut self) {
|
||||
self.show_next_month();
|
||||
self.selected = self.visible.clone();
|
||||
self.selected = self.visible;
|
||||
}
|
||||
|
||||
pub fn set_selected_visible(&mut self, selected: NaiveDate) {
|
||||
self.selected = selected;
|
||||
self.visible = self.selected.clone();
|
||||
self.visible = self.selected;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -469,7 +469,7 @@ where
|
|||
text_input("", self.input_color)
|
||||
.on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
|
||||
.on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
|
||||
.on_submit(on_update(ColorPickerUpdate::AppliedColor))
|
||||
.on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor))
|
||||
.leading_icon(
|
||||
color_button(
|
||||
None,
|
||||
|
|
@ -611,7 +611,7 @@ pub struct ColorPicker<'a, Message> {
|
|||
must_clear_cache: Rc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'a, Message>
|
||||
impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
{
|
||||
|
|
@ -874,7 +874,7 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> ColorPicker<'a, Message> where Message: Clone + 'static {}
|
||||
impl<Message> ColorPicker<'_, Message> where Message: Clone + 'static {}
|
||||
// TODO convert active color to hex or rgba
|
||||
fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
|
||||
let srgb = palette::Srgb::from_color(c);
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ pub(super) struct Overlay<'a, 'b, Message> {
|
|||
pub(super) width: f32,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message> overlay::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, 'b, Message>
|
||||
impl<Message> overlay::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, '_, Message>
|
||||
where
|
||||
Message: Clone,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'a, Message> {
|
||||
impl<Message: Clone> Widget<Message, crate::Theme, Renderer> for ContextDrawer<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content), Tree::new(&self.drawer)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,9 +46,7 @@ pub struct ContextMenu<'a, Message> {
|
|||
context_menu: Option<Vec<menu::Tree<'a, Message>>>,
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for ContextMenu<'a, Message>
|
||||
{
|
||||
impl<Message: Clone> Widget<Message, crate::Theme, crate::Renderer> for ContextMenu<'_, Message> {
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<LocalState>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ pub struct Dialog<'a, Message> {
|
|||
tertiary_action: Option<Element<'a, Message>>,
|
||||
}
|
||||
|
||||
impl<Message> Default for Dialog<'_, Message> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Dialog<'a, Message> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -243,8 +243,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for DndDestination<'a, Message>
|
||||
impl<Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for DndDestination<'_, Message>
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.container)]
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ impl<
|
|||
clipboard,
|
||||
false,
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
Some(iced_core::clipboard::DndSource::Surface(window.clone()))
|
||||
Some(iced_core::clipboard::DndSource::Surface(*window))
|
||||
} else {
|
||||
Some(iced_core::clipboard::DndSource::Widget(self.id.clone()))
|
||||
},
|
||||
|
|
@ -153,10 +153,9 @@ impl<
|
|||
}
|
||||
|
||||
impl<
|
||||
'a,
|
||||
Message: Clone + 'static,
|
||||
D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static,
|
||||
> Widget<Message, crate::Theme, crate::Renderer> for DndSource<'a, Message, D>
|
||||
> Widget<Message, crate::Theme, crate::Renderer> for DndSource<'_, Message, D>
|
||||
{
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.container)]
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@
|
|||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||
|
||||
mod appearance;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub use appearance::{Appearance, StyleSheet};
|
||||
|
||||
use crate::widget::{icon, Container};
|
||||
use crate::surface;
|
||||
use crate::widget::{icon, Container, RcWrapper};
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout::{self, Layout};
|
||||
use iced_core::text::{self, Text};
|
||||
|
|
@ -21,13 +25,15 @@ use iced_widget::scrollable::Scrollable;
|
|||
pub struct Menu<'a, S, Message>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
state: &'a mut State,
|
||||
options: &'a [S],
|
||||
icons: &'a [icon::Handle],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
state: State,
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||
close_on_selected: Option<Message>,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
width: f32,
|
||||
padding: Padding,
|
||||
|
|
@ -36,17 +42,21 @@ where
|
|||
style: (),
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
||||
impl<'a, S: AsRef<str>, Message: 'a + std::clone::Clone> Menu<'a, S, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
/// Creates a new [`Menu`] with the given [`State`], a list of options, and
|
||||
/// the message to produced when an option is selected.
|
||||
pub fn new(
|
||||
state: &'a mut State,
|
||||
options: &'a [S],
|
||||
icons: &'a [icon::Handle],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
state: State,
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: impl FnMut(usize) -> Message + 'a,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
close_on_selected: Option<Message>,
|
||||
) -> Self {
|
||||
Menu {
|
||||
state,
|
||||
|
|
@ -61,6 +71,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
|||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
style: Default::default(),
|
||||
close_on_selected,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,20 +113,31 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
|
|||
) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> {
|
||||
overlay::Element::new(Box::new(Overlay::new(self, target_height, position)))
|
||||
}
|
||||
|
||||
/// Turns the [`Menu`] into a popup [`Element`] at the given target
|
||||
/// position.
|
||||
///
|
||||
/// The `target_height` will be used to display the menu either on top
|
||||
/// of the target or under it, depending on the screen position and the
|
||||
/// dimensions of the [`Menu`].
|
||||
#[must_use]
|
||||
pub fn popup(self, position: Point, target_height: f32) -> crate::Element<'a, Message> {
|
||||
Overlay::new(self, target_height, position).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Menu`].
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
tree: Tree,
|
||||
pub(crate) tree: RcWrapper<Tree>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`] for a [`Menu`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tree: Tree::empty(),
|
||||
tree: RcWrapper::new(Tree::empty()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +149,7 @@ impl Default for State {
|
|||
}
|
||||
|
||||
struct Overlay<'a, Message> {
|
||||
state: &'a mut Tree,
|
||||
state: RcWrapper<Tree>,
|
||||
container: Container<'a, Message, crate::Theme, crate::Renderer>,
|
||||
width: f32,
|
||||
target_height: f32,
|
||||
|
|
@ -135,12 +157,15 @@ struct Overlay<'a, Message> {
|
|||
position: Point,
|
||||
}
|
||||
|
||||
impl<'a, Message: 'a> Overlay<'a, Message> {
|
||||
impl<'a, Message: Clone + 'a> Overlay<'a, Message> {
|
||||
pub fn new<S: AsRef<str>>(
|
||||
menu: Menu<'a, S, Message>,
|
||||
target_height: f32,
|
||||
position: Point,
|
||||
) -> Self {
|
||||
) -> Self
|
||||
where
|
||||
[S]: ToOwned,
|
||||
{
|
||||
let Menu {
|
||||
state,
|
||||
options,
|
||||
|
|
@ -154,6 +179,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
text_size,
|
||||
text_line_height,
|
||||
style,
|
||||
close_on_selected,
|
||||
} = menu;
|
||||
|
||||
let mut container = Container::new(Scrollable::new(
|
||||
|
|
@ -163,6 +189,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
hovered_option,
|
||||
selected_option,
|
||||
on_selected,
|
||||
close_on_selected,
|
||||
on_option_hovered,
|
||||
text_size,
|
||||
text_line_height,
|
||||
|
|
@ -172,10 +199,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
))
|
||||
.class(crate::style::Container::Dropdown);
|
||||
|
||||
state.tree.diff(&mut container as &mut dyn Widget<_, _, _>);
|
||||
state
|
||||
.tree
|
||||
.with_data_mut(|tree| tree.diff(&mut container as &mut dyn Widget<_, _, _>));
|
||||
|
||||
Self {
|
||||
state: &mut state.tree,
|
||||
state: state.tree.clone(),
|
||||
container,
|
||||
width,
|
||||
target_height,
|
||||
|
|
@ -183,20 +212,15 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||
let position = self.position;
|
||||
let space_below = bounds.height - (position.y + self.target_height);
|
||||
let space_above = position.y;
|
||||
fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||
let space_below = bounds.height - (self.position.y + self.target_height);
|
||||
let space_above = self.position.y;
|
||||
|
||||
let limits = layout::Limits::new(
|
||||
Size::ZERO,
|
||||
Size::new(
|
||||
bounds.width - position.x,
|
||||
bounds.width - self.position.x,
|
||||
if space_below > space_above {
|
||||
space_below
|
||||
} else {
|
||||
|
|
@ -206,16 +230,18 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
|||
)
|
||||
.width(self.width);
|
||||
|
||||
let node = self.container.layout(self.state, renderer, &limits);
|
||||
let node = self
|
||||
.state
|
||||
.with_data_mut(|tree| self.container.layout(tree, renderer, &limits));
|
||||
|
||||
node.clone().move_to(if space_below > space_above {
|
||||
position + Vector::new(0.0, self.target_height)
|
||||
self.position + Vector::new(0.0, self.target_height)
|
||||
} else {
|
||||
position - Vector::new(0.0, node.size().height)
|
||||
self.position - Vector::new(0.0, node.size().height)
|
||||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn _on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
|
|
@ -226,23 +252,27 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
|||
) -> event::Status {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
self.container.on_event(
|
||||
self.state, event, layout, cursor, renderer, clipboard, shell, &bounds,
|
||||
)
|
||||
self.state.with_data_mut(|tree| {
|
||||
self.container.on_event(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, &bounds,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
fn _mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &crate::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.container
|
||||
.mouse_interaction(self.state, layout, cursor, viewport, renderer)
|
||||
self.state.with_data(|tree| {
|
||||
self.container
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
fn _draw(
|
||||
&self,
|
||||
renderer: &mut crate::Renderer,
|
||||
theme: &crate::Theme,
|
||||
|
|
@ -266,25 +296,138 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
|||
appearance.background,
|
||||
);
|
||||
|
||||
self.container
|
||||
.draw(self.state, renderer, theme, style, layout, cursor, &bounds);
|
||||
self.state.with_data(|tree| {
|
||||
self.container
|
||||
.draw(tree, renderer, theme, style, layout, cursor, &bounds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, S: AsRef<str>, Message> {
|
||||
options: &'a [S],
|
||||
icons: &'a [icon::Handle],
|
||||
hovered_option: &'a mut Option<usize>,
|
||||
impl<'a, Message: Clone + 'a> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||
self._layout(renderer, bounds)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &crate::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self._on_event(event, layout, cursor, renderer, clipboard, shell)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &crate::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self._mouse_interaction(layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut crate::Renderer,
|
||||
theme: &crate::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
self._draw(renderer, theme, style, layout, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'a> crate::widget::Widget<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(Length::Fixed(self.width), Length::Shrink)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut iced_core::widget::Tree,
|
||||
renderer: &crate::Renderer,
|
||||
limits: &iced::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width);
|
||||
|
||||
self.state
|
||||
.with_data_mut(|tree| self.container.layout(tree, renderer, &limits))
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &crate::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self._mouse_interaction(layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
_tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &crate::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self._on_event(event, layout, cursor, renderer, clipboard, shell)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut crate::Renderer,
|
||||
theme: &crate::Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
self._draw(renderer, theme, style, layout, cursor);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'a> From<Overlay<'a, Message>> for crate::Element<'a, Message> {
|
||||
fn from(widget: Overlay<'a, Message>) -> Self {
|
||||
Element::new(widget)
|
||||
}
|
||||
}
|
||||
|
||||
struct List<'a, S: AsRef<str>, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
options: Cow<'a, [S]>,
|
||||
icons: Cow<'a, [icon::Handle]>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
|
||||
close_on_selected: Option<Message>,
|
||||
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
text_line_height: text::LineHeight,
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for List<'a, S, Message>
|
||||
impl<S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer> for List<'_, S, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
Message: Clone,
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size::new(Length::Fill, Length::Shrink)
|
||||
|
|
@ -330,9 +473,13 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
|
||||
let hovered_guard = self.hovered_option.lock().unwrap();
|
||||
if cursor.is_over(layout.bounds()) {
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(index) = *hovered_guard {
|
||||
shell.publish((self.on_selected)(index));
|
||||
if let Some(close_on_selected) = self.close_on_selected.clone() {
|
||||
shell.publish(close_on_selected);
|
||||
}
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
|
|
@ -348,14 +495,15 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
+ self.padding.vertical();
|
||||
|
||||
let new_hovered_option = (cursor_position.y / option_height) as usize;
|
||||
let mut hovered_guard = self.hovered_option.lock().unwrap();
|
||||
|
||||
if let Some(on_option_hovered) = self.on_option_hovered {
|
||||
if *self.hovered_option != Some(new_hovered_option) {
|
||||
if *hovered_guard != Some(new_hovered_option) {
|
||||
shell.publish(on_option_hovered(new_hovered_option));
|
||||
}
|
||||
}
|
||||
|
||||
*self.hovered_option = Some(new_hovered_option);
|
||||
*hovered_guard = Some(new_hovered_option);
|
||||
}
|
||||
}
|
||||
Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
|
|
@ -367,11 +515,15 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
let option_height =
|
||||
f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
|
||||
+ self.padding.vertical();
|
||||
let mut hovered_guard = self.hovered_option.lock().unwrap();
|
||||
|
||||
*self.hovered_option = Some((cursor_position.y / option_height) as usize);
|
||||
*hovered_guard = Some((cursor_position.y / option_height) as usize);
|
||||
|
||||
if let Some(index) = *self.hovered_option {
|
||||
if let Some(index) = *hovered_guard {
|
||||
shell.publish((self.on_selected)(index));
|
||||
if let Some(close_on_selected) = self.close_on_selected.clone() {
|
||||
shell.publish(close_on_selected);
|
||||
}
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
|
|
@ -434,6 +586,8 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
height: option_height,
|
||||
};
|
||||
|
||||
let hovered_guard = self.hovered_option.lock().unwrap();
|
||||
|
||||
let (color, font) = if self.selected_option == Some(i) {
|
||||
let item_x = bounds.x + appearance.border_width;
|
||||
let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
|
||||
|
|
@ -471,7 +625,7 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
);
|
||||
|
||||
(appearance.selected_text_color, crate::font::semibold())
|
||||
} else if *self.hovered_option == Some(i) {
|
||||
} else if *hovered_guard == Some(i) {
|
||||
let item_x = bounds.x + appearance.border_width;
|
||||
let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
|
||||
|
||||
|
|
@ -538,6 +692,9 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
|
|||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>>
|
||||
for Element<'a, Message, crate::Theme, crate::Renderer>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
Message: Clone,
|
||||
{
|
||||
fn from(list: List<'a, S, Message>) -> Self {
|
||||
Element::new(list)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
//! Displays a list of options in a popover menu on select.
|
||||
|
||||
pub mod menu;
|
||||
use iced_core::window;
|
||||
pub use menu::Menu;
|
||||
|
||||
pub mod multi;
|
||||
|
|
@ -12,11 +13,40 @@ pub mod multi;
|
|||
mod widget;
|
||||
pub use widget::*;
|
||||
|
||||
use crate::surface;
|
||||
|
||||
/// Displays a list of options in a popover menu on select.
|
||||
pub fn dropdown<'a, S: AsRef<str>, Message: 'a>(
|
||||
selections: &'a [S],
|
||||
pub fn dropdown<
|
||||
S: AsRef<str> + std::clone::Clone + Send + Sync + 'static,
|
||||
Message: 'static + Clone,
|
||||
>(
|
||||
selections: &[S],
|
||||
selected: Option<usize>,
|
||||
on_selected: impl Fn(usize) -> Message + 'a,
|
||||
) -> Dropdown<'a, S, Message> {
|
||||
on_selected: impl Fn(usize) -> Message + Send + Sync + 'static,
|
||||
) -> Dropdown<'_, S, Message, Message> {
|
||||
Dropdown::new(selections, selected, on_selected)
|
||||
}
|
||||
|
||||
/// Displays a list of options in a popover menu on select.
|
||||
/// AppMessage must be the App's toplevel message.
|
||||
pub fn popup_dropdown<
|
||||
'a,
|
||||
S: AsRef<str> + std::clone::Clone + Send + Sync + 'static,
|
||||
Message: 'static + Clone,
|
||||
AppMessage: 'static + Clone,
|
||||
>(
|
||||
selections: &'a [S],
|
||||
selected: Option<usize>,
|
||||
on_selected: impl Fn(usize) -> Message + Send + Sync + 'static,
|
||||
_parent_id: window::Id,
|
||||
_on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static,
|
||||
_map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static,
|
||||
) -> Dropdown<'a, S, Message, AppMessage> {
|
||||
let dropdown: Dropdown<'_, S, Message, AppMessage> =
|
||||
Dropdown::new(selections, selected, on_selected);
|
||||
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action);
|
||||
|
||||
dropdown
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,9 +180,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
|
||||
for Overlay<'a, Message>
|
||||
{
|
||||
impl<Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer> for Overlay<'_, Message> {
|
||||
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
|
||||
let position = self.position;
|
||||
let space_below = bounds.height - (position.y + self.target_height);
|
||||
|
|
@ -279,8 +277,8 @@ struct InnerList<'a, S, Item, Message> {
|
|||
text_line_height: text::LineHeight,
|
||||
}
|
||||
|
||||
impl<'a, S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for InnerList<'a, S, Item, Message>
|
||||
impl<S, Item, Message> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for InnerList<'_, S, Item, Message>
|
||||
where
|
||||
S: AsRef<str>,
|
||||
Item: Clone + PartialEq,
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ impl<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static>
|
|||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| crate::font::default());
|
||||
let font = self.font.unwrap_or_else(crate::font::default);
|
||||
|
||||
draw(
|
||||
renderer,
|
||||
|
|
@ -278,7 +278,7 @@ pub fn layout(
|
|||
bounds: Size::new(f32::MAX, f32::MAX),
|
||||
size: iced::Pixels(text_size),
|
||||
line_height: text_line_height,
|
||||
font: font.unwrap_or_else(|| crate::font::default()),
|
||||
font: font.unwrap_or_else(crate::font::default),
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
shaping: text::Shaping::Advanced,
|
||||
|
|
@ -422,7 +422,7 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a, Item: Clone + PartialEq + 'static
|
|||
bounds: Size::new(f32::MAX, f32::MAX),
|
||||
size: iced::Pixels(text_size),
|
||||
line_height,
|
||||
font: font.unwrap_or_else(|| crate::font::default()),
|
||||
font: font.unwrap_or_else(crate::font::default),
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
shaping: text::Shaping::Advanced,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||
|
||||
use super::menu::{self, Menu};
|
||||
use crate::widget::icon;
|
||||
use crate::widget::icon::{self, Handle};
|
||||
use crate::{surface, Element};
|
||||
use derive_setters::Setters;
|
||||
use iced::Radians;
|
||||
use iced::window;
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::text::{self, Paragraph, Text};
|
||||
use iced_core::widget::tree::{self, Tree};
|
||||
|
|
@ -14,14 +15,24 @@ use iced_core::{
|
|||
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
|
||||
};
|
||||
use iced_widget::pick_list::{self, Catalog};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
pub type DropdownView<Message> = Arc<dyn Fn() -> Element<'static, Message> + Send + Sync>;
|
||||
static AUTOSIZE_ID: LazyLock<crate::widget::Id> =
|
||||
LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize"));
|
||||
/// A widget for selecting a single value from a list of selections.
|
||||
#[derive(Setters)]
|
||||
pub struct Dropdown<'a, S: AsRef<str>, Message> {
|
||||
pub struct Dropdown<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
#[setters(skip)]
|
||||
on_selected: Box<dyn Fn(usize) -> Message + 'a>,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
|
||||
#[setters(skip)]
|
||||
selections: &'a [S],
|
||||
#[setters]
|
||||
|
|
@ -38,9 +49,21 @@ pub struct Dropdown<'a, S: AsRef<str>, Message> {
|
|||
text_line_height: text::LineHeight,
|
||||
#[setters(strip_option)]
|
||||
font: Option<crate::font::Font>,
|
||||
#[setters(skip)]
|
||||
on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
|
||||
#[setters(skip)]
|
||||
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>,
|
||||
#[setters(strip_option)]
|
||||
window_id: Option<window::Id>,
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
||||
impl<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: 'static, AppMessage: 'static>
|
||||
Dropdown<'a, S, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
/// The default gap.
|
||||
pub const DEFAULT_GAP: f32 = 4.0;
|
||||
|
||||
|
|
@ -52,10 +75,10 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
|||
pub fn new(
|
||||
selections: &'a [S],
|
||||
selected: Option<usize>,
|
||||
on_selected: impl Fn(usize) -> Message + 'a,
|
||||
on_selected: impl Fn(usize) -> Message + 'static + Send + Sync,
|
||||
) -> Self {
|
||||
Self {
|
||||
on_selected: Box::new(on_selected),
|
||||
on_selected: Arc::new(on_selected),
|
||||
selections,
|
||||
icons: &[],
|
||||
selected,
|
||||
|
|
@ -65,12 +88,73 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
|
|||
text_size: None,
|
||||
text_line_height: text::LineHeight::Relative(1.2),
|
||||
font: None,
|
||||
window_id: None,
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
|
||||
on_surface_action: None,
|
||||
action_map: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
/// Handle dropdown requests for popup creation.
|
||||
/// Intended to be used with [`crate::app::message::get_popup`]
|
||||
pub fn with_popup<NewAppMessage>(
|
||||
mut self,
|
||||
parent_id: window::Id,
|
||||
on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static,
|
||||
action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static,
|
||||
) -> Dropdown<'a, S, Message, NewAppMessage> {
|
||||
let Self {
|
||||
on_selected,
|
||||
selections,
|
||||
icons,
|
||||
selected,
|
||||
width,
|
||||
gap,
|
||||
padding,
|
||||
text_size,
|
||||
text_line_height,
|
||||
font,
|
||||
positioner,
|
||||
..
|
||||
} = self;
|
||||
|
||||
Dropdown::<'a, S, Message, NewAppMessage> {
|
||||
on_selected,
|
||||
selections,
|
||||
icons,
|
||||
selected,
|
||||
width,
|
||||
gap,
|
||||
padding,
|
||||
text_size,
|
||||
text_line_height,
|
||||
font,
|
||||
on_surface_action: Some(Arc::new(on_surface_action)),
|
||||
action_map: Some(Arc::new(action_map)),
|
||||
window_id: Some(parent_id),
|
||||
positioner,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
pub fn with_positioner(
|
||||
mut self,
|
||||
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
|
||||
) -> Self {
|
||||
self.positioner = positioner;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for Dropdown<'a, S, Message>
|
||||
impl<
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: 'static + Clone,
|
||||
AppMessage: 'static + Clone,
|
||||
> Widget<Message, crate::Theme, crate::Renderer> for Dropdown<'_, S, Message, AppMessage>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
|
|
@ -153,15 +237,26 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
update(
|
||||
update::<S, Message, AppMessage>(
|
||||
&event,
|
||||
layout,
|
||||
cursor,
|
||||
shell,
|
||||
self.on_selected.as_ref(),
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
self.positioner.clone(),
|
||||
self.on_selected.clone(),
|
||||
self.selected,
|
||||
self.selections,
|
||||
|| tree.state.downcast_mut::<State>(),
|
||||
self.window_id,
|
||||
self.on_surface_action.clone(),
|
||||
self.action_map.clone(),
|
||||
self.icons,
|
||||
self.gap,
|
||||
self.padding,
|
||||
self.text_size,
|
||||
self.font,
|
||||
self.selected,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +281,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| crate::font::default());
|
||||
let font = self.font.unwrap_or_else(crate::font::default);
|
||||
draw(
|
||||
renderer,
|
||||
theme,
|
||||
|
|
@ -211,6 +306,11 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
renderer: &crate::Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if self.window_id.is_some() || self.on_surface_action.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
overlay(
|
||||
|
|
@ -225,8 +325,9 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
self.selections,
|
||||
self.icons,
|
||||
self.selected,
|
||||
&self.on_selected,
|
||||
self.on_selected.as_ref(),
|
||||
translation,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -242,24 +343,31 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
|
|||
// }
|
||||
}
|
||||
|
||||
impl<'a, S: AsRef<str>, Message: 'a> From<Dropdown<'a, S, Message>>
|
||||
for crate::Element<'a, Message>
|
||||
impl<
|
||||
'a,
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: 'static + std::clone::Clone,
|
||||
AppMessage: 'static + std::clone::Clone,
|
||||
> From<Dropdown<'a, S, Message, AppMessage>> for crate::Element<'a, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
fn from(pick_list: Dropdown<'a, S, Message>) -> Self {
|
||||
fn from(pick_list: Dropdown<'a, S, Message, AppMessage>) -> Self {
|
||||
Self::new(pick_list)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Dropdown`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
icon: Option<svg::Handle>,
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
is_open: Arc<AtomicBool>,
|
||||
hovered_option: Arc<Mutex<Option<usize>>>,
|
||||
hashes: Vec<u64>,
|
||||
selections: Vec<crate::Plain>,
|
||||
popup_id: window::Id,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
|
@ -276,10 +384,11 @@ impl State {
|
|||
},
|
||||
menu: menu::State::default(),
|
||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||
is_open: false,
|
||||
hovered_option: None,
|
||||
is_open: Arc::new(AtomicBool::new(false)),
|
||||
hovered_option: Arc::new(Mutex::new(None)),
|
||||
selections: Vec::new(),
|
||||
hashes: Vec::new(),
|
||||
popup_id: window::Id::unique(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -316,7 +425,7 @@ pub fn layout(
|
|||
bounds: Size::new(f32::MAX, f32::MAX),
|
||||
size: iced::Pixels(text_size),
|
||||
line_height: text_line_height,
|
||||
font: font.unwrap_or_else(|| crate::font::default()),
|
||||
font: font.unwrap_or_else(crate::font::default),
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
shaping: text::Shaping::Advanced,
|
||||
|
|
@ -348,32 +457,136 @@ pub fn layout(
|
|||
|
||||
/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`]
|
||||
/// accordingly.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update<'a, S: AsRef<str>, Message>(
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
pub fn update<
|
||||
'a,
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: Clone + 'static,
|
||||
AppMessage: Clone + 'static,
|
||||
>(
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
on_selected: &dyn Fn(usize) -> Message,
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
|
||||
selected: Option<usize>,
|
||||
selections: &[S],
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
_window_id: Option<window::Id>,
|
||||
on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
|
||||
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + Send + Sync + 'static>>,
|
||||
icons: &[icon::Handle],
|
||||
gap: f32,
|
||||
padding: Padding,
|
||||
text_size: Option<f32>,
|
||||
font: Option<crate::font::Font>,
|
||||
selected_option: Option<usize>,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = state();
|
||||
|
||||
if state.is_open {
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
if is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside it's
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open = false;
|
||||
|
||||
state.is_open.store(false, Ordering::Relaxed);
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(on_close) = on_surface_action {
|
||||
shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
|
||||
}
|
||||
event::Status::Captured
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
state.is_open = true;
|
||||
state.hovered_option = selected;
|
||||
state.is_open.store(true, Ordering::Relaxed);
|
||||
let mut hovered_guard = state.hovered_option.lock().unwrap();
|
||||
*hovered_guard = selected;
|
||||
let id = window::Id::unique();
|
||||
state.popup_id = id;
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
if let Some(((on_surface_action, parent), action_map)) =
|
||||
on_surface_action.zip(_window_id).zip(action_map)
|
||||
{
|
||||
use iced_runtime::platform_specific::wayland::popup::{
|
||||
SctkPopupSettings, SctkPositioner,
|
||||
};
|
||||
let bounds = layout.bounds();
|
||||
let anchor_rect = Rectangle {
|
||||
x: bounds.x as i32,
|
||||
y: bounds.y as i32,
|
||||
width: bounds.width as i32,
|
||||
height: bounds.height as i32,
|
||||
};
|
||||
let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
|
||||
let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
|
||||
selection_paragraph.min_width().round()
|
||||
};
|
||||
let pad_width = padding.horizontal().mul_add(2.0, 16.0);
|
||||
|
||||
let selections_width = selections
|
||||
.iter()
|
||||
.zip(state.selections.iter_mut())
|
||||
.map(|(label, selection)| measure(label.as_ref(), selection.raw()))
|
||||
.fold(0.0, |next, current| current.max(next));
|
||||
|
||||
let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec());
|
||||
let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec());
|
||||
let state = state.clone();
|
||||
let on_close = surface::action::destroy_popup(id);
|
||||
let on_surface_action_clone = on_surface_action.clone();
|
||||
let get_popup_action = surface::action::simple_popup::<
|
||||
AppMessage,
|
||||
Box<
|
||||
dyn Fn() -> Element<'static, crate::Action<AppMessage>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>,
|
||||
>(
|
||||
move || {
|
||||
SctkPopupSettings {
|
||||
parent,
|
||||
id,
|
||||
input_zone: None,
|
||||
positioner: SctkPositioner {
|
||||
size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)),
|
||||
anchor_rect,
|
||||
// TODO: left or right alignment based on direction?
|
||||
anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
|
||||
gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
|
||||
reactive: true,
|
||||
offset: (-padding.left as i32, 0),
|
||||
constraint_adjustment: 9,
|
||||
..Default::default()
|
||||
},
|
||||
parent_size: None,
|
||||
grab: true,
|
||||
close_with_children: true,
|
||||
}
|
||||
},
|
||||
Some(Box::new(move || {
|
||||
let action_map = action_map.clone();
|
||||
let on_selected = on_selected.clone();
|
||||
let e: Element<'static, crate::Action<AppMessage>> =
|
||||
Element::from(menu_widget(
|
||||
bounds,
|
||||
&state,
|
||||
gap,
|
||||
padding,
|
||||
text_size.unwrap_or(14.0),
|
||||
selections.clone(),
|
||||
icons.clone(),
|
||||
selected_option,
|
||||
Arc::new(move |i| on_selected.clone()(i)),
|
||||
Some(on_surface_action_clone(on_close.clone())),
|
||||
))
|
||||
.map(move |m| crate::Action::App(action_map.clone()(m)));
|
||||
e
|
||||
})),
|
||||
);
|
||||
shell.publish(on_surface_action(get_popup_action));
|
||||
}
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
|
|
@ -383,11 +596,9 @@ pub fn update<'a, S: AsRef<str>, Message>(
|
|||
delta: mouse::ScrollDelta::Lines { .. },
|
||||
}) => {
|
||||
let state = state();
|
||||
let is_open = state.is_open.load(Ordering::Relaxed);
|
||||
|
||||
if state.keyboard_modifiers.command()
|
||||
&& cursor.is_over(layout.bounds())
|
||||
&& !state.is_open
|
||||
{
|
||||
if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
|
||||
let next_index = selected.map(|index| index + 1).unwrap_or_default();
|
||||
|
||||
if selections.len() < next_index {
|
||||
|
|
@ -423,9 +634,72 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "winit", feature = "wayland"))]
|
||||
/// Returns the current menu widget of a [`Dropdown`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn menu_widget<
|
||||
S: AsRef<str> + Send + Sync + Clone + 'static,
|
||||
Message: 'static + std::clone::Clone,
|
||||
>(
|
||||
bounds: Rectangle,
|
||||
state: &State,
|
||||
gap: f32,
|
||||
padding: Padding,
|
||||
text_size: f32,
|
||||
selections: Cow<'static, [S]>,
|
||||
icons: Cow<'static, [icon::Handle]>,
|
||||
selected_option: Option<usize>,
|
||||
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
|
||||
close_on_selected: Option<Message>,
|
||||
) -> crate::Element<'static, Message>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
|
||||
let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
|
||||
selection_paragraph.min_width().round()
|
||||
};
|
||||
let selections_width = selections
|
||||
.iter()
|
||||
.zip(state.selections.iter())
|
||||
.map(|(label, selection)| measure(label.as_ref(), selection.raw()))
|
||||
.fold(0.0, |next, current| current.max(next));
|
||||
let pad_width = padding.horizontal().mul_add(2.0, 16.0);
|
||||
|
||||
let width = selections_width + gap + pad_width + icon_width;
|
||||
let is_open = state.is_open.clone();
|
||||
let menu: Menu<'static, S, Message> = Menu::new(
|
||||
state.menu.clone(),
|
||||
selections,
|
||||
icons,
|
||||
state.hovered_option.clone(),
|
||||
selected_option,
|
||||
move |option| {
|
||||
is_open.store(false, Ordering::Relaxed);
|
||||
|
||||
(on_selected)(option)
|
||||
},
|
||||
None,
|
||||
close_on_selected,
|
||||
)
|
||||
.width(width)
|
||||
.padding(padding)
|
||||
.text_size(text_size);
|
||||
|
||||
crate::widget::autosize::autosize(
|
||||
menu.popup(iced::Point::new(0., 0.), bounds.height),
|
||||
AUTOSIZE_ID.clone(),
|
||||
)
|
||||
.auto_height(true)
|
||||
.auto_width(true)
|
||||
.min_height(1.)
|
||||
.min_width(width)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Returns the current overlay of a [`Dropdown`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
||||
pub fn overlay<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>(
|
||||
layout: Layout<'_>,
|
||||
_renderer: &crate::Renderer,
|
||||
state: &'a mut State,
|
||||
|
|
@ -439,22 +713,27 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
|
|||
selected_option: Option<usize>,
|
||||
on_selected: &'a dyn Fn(usize) -> Message,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>> {
|
||||
if state.is_open {
|
||||
close_on_selected: Option<Message>,
|
||||
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
|
||||
where
|
||||
[S]: std::borrow::ToOwned,
|
||||
{
|
||||
if state.is_open.load(Ordering::Relaxed) {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let menu = Menu::new(
|
||||
&mut state.menu,
|
||||
selections,
|
||||
icons,
|
||||
&mut state.hovered_option,
|
||||
state.menu.clone(),
|
||||
Cow::Borrowed(selections),
|
||||
Cow::Borrowed(icons),
|
||||
state.hovered_option.clone(),
|
||||
selected_option,
|
||||
|option| {
|
||||
state.is_open = false;
|
||||
state.is_open.store(false, Ordering::Relaxed);
|
||||
|
||||
(on_selected)(option)
|
||||
},
|
||||
None,
|
||||
close_on_selected,
|
||||
)
|
||||
.width({
|
||||
let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
|
||||
|
|
|
|||
|
|
@ -85,9 +85,7 @@ impl<'a, Message> FlexRow<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
|
||||
for FlexRow<'a, Message>
|
||||
{
|
||||
impl<Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for FlexRow<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ pub struct Grid<'a, Message> {
|
|||
row: u16,
|
||||
}
|
||||
|
||||
impl<Message> Default for Grid<'_, Message> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Grid<'a, Message> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -106,7 +112,7 @@ impl<'a, Message> Grid<'a, Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for Grid<'a, Message> {
|
||||
impl<Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for Grid<'_, Message> {
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
|
@ -303,6 +309,12 @@ pub struct Assignment {
|
|||
pub(super) height: u16,
|
||||
}
|
||||
|
||||
impl Default for Assignment {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Assignment {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -120,8 +120,8 @@ pub struct HeaderBarWidget<'a, Message> {
|
|||
header_bar_inner: Element<'a, Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for HeaderBarWidget<'a, Message>
|
||||
impl<Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
|
||||
for HeaderBarWidget<'_, Message>
|
||||
{
|
||||
fn diff(&mut self, tree: &mut tree::Tree) {
|
||||
tree.diff_children(&mut [&mut self.header_bar_inner]);
|
||||
|
|
@ -306,7 +306,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
Density::Spacious => 48.0,
|
||||
Density::Standard => 48.0,
|
||||
};
|
||||
|
||||
let portion = ((start.len().max(end.len()) as f32 / center.len().max(1) as f32).round()
|
||||
as u16)
|
||||
.max(1);
|
||||
let center_empty = center.is_empty() && self.title.is_empty();
|
||||
// Creates the headerbar widget.
|
||||
let mut widget = widget::row::with_capacity(3)
|
||||
// If elements exist in the start region, append them here.
|
||||
|
|
@ -316,7 +319,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
.align_y(iced::Alignment::Center)
|
||||
.apply(widget::container)
|
||||
.align_x(iced::Alignment::Start)
|
||||
.width(Length::Shrink),
|
||||
.width(Length::FillPortion(portion)),
|
||||
)
|
||||
// If elements exist in the center region, use them here.
|
||||
// This will otherwise use the title as a widget if a title was defined.
|
||||
|
|
@ -338,7 +341,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
.align_y(iced::Alignment::Center)
|
||||
.apply(widget::container)
|
||||
.align_x(iced::Alignment::End)
|
||||
.width(Length::Shrink),
|
||||
.width(if center_empty {
|
||||
Length::Fill
|
||||
} else {
|
||||
Length::FillPortion(portion)
|
||||
}),
|
||||
)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.height(Length::Fixed(height))
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ impl Icon {
|
|||
self.height
|
||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
||||
)
|
||||
.rotation(self.rotation.unwrap_or_else(Rotation::default))
|
||||
.rotation(self.rotation.unwrap_or_default())
|
||||
.content_fit(self.content_fit)
|
||||
.into()
|
||||
};
|
||||
|
|
@ -100,7 +100,7 @@ impl Icon {
|
|||
self.height
|
||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
||||
)
|
||||
.rotation(self.rotation.unwrap_or_else(Rotation::default))
|
||||
.rotation(self.rotation.unwrap_or_default())
|
||||
.content_fit(self.content_fit)
|
||||
.symbolic(self.handle.symbolic)
|
||||
.into()
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl From<Named> for Icon {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> From<Named> for crate::Element<'a, Message> {
|
||||
impl<Message: 'static> From<Named> for crate::Element<'_, Message> {
|
||||
fn from(builder: Named) -> Self {
|
||||
builder.icon().into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for IdContainer<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for IdContainer<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -138,8 +138,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer>
|
||||
for LayerContainer<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, Theme, Renderer> for LayerContainer<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub struct ListColumn<'a, Message> {
|
|||
children: Vec<Element<'a, Message>>,
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static> Default for ListColumn<'a, Message> {
|
||||
impl<Message: 'static> Default for ListColumn<'_, Message> {
|
||||
fn default() -> Self {
|
||||
let cosmic_theme::Spacing {
|
||||
space_xxs, space_m, ..
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
//!
|
||||
|
||||
pub mod action;
|
||||
|
||||
pub use action::MenuAction as Action;
|
||||
|
||||
mod flex;
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ impl KeyBind {
|
|||
pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool {
|
||||
let key_eq = match (key, &self.key) {
|
||||
// CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way
|
||||
(Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(&b),
|
||||
(Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
|
||||
(a, b) => a.eq(b),
|
||||
};
|
||||
key_eq
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ impl Default for MenuBarState {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn menu_roots_children<'a, Message, Renderer>(
|
||||
menu_roots: &Vec<MenuTree<'a, Message, Renderer>>,
|
||||
pub(crate) fn menu_roots_children<Message, Renderer>(
|
||||
menu_roots: &Vec<MenuTree<'_, Message, Renderer>>,
|
||||
) -> Vec<Tree>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -95,8 +95,8 @@ where
|
|||
}
|
||||
|
||||
#[allow(invalid_reference_casting)]
|
||||
pub(crate) fn menu_roots_diff<'a, Message, Renderer>(
|
||||
menu_roots: &mut Vec<MenuTree<'a, Message, Renderer>>,
|
||||
pub(crate) fn menu_roots_diff<Message, Renderer>(
|
||||
menu_roots: &mut Vec<MenuTree<'_, Message, Renderer>>,
|
||||
tree: &mut Tree,
|
||||
) where
|
||||
Renderer: renderer::Renderer,
|
||||
|
|
@ -280,8 +280,7 @@ where
|
|||
self
|
||||
}
|
||||
}
|
||||
impl<'a, Message, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for MenuBar<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, crate::Theme, Renderer> for MenuBar<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
@ -366,6 +365,8 @@ where
|
|||
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
|
||||
state.view_cursor = view_cursor;
|
||||
state.open = true;
|
||||
// #[cfg(feature = "wayland")]
|
||||
// TODO emit Message to open menu
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
@ -437,6 +438,9 @@ where
|
|||
_renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
|
||||
// #[cfg(feature = "wayland")]
|
||||
// return None;
|
||||
|
||||
let state = tree.state.downcast_ref::<MenuBarState>();
|
||||
if !state.open {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ where
|
|||
pub(crate) style: &'b <crate::Theme as StyleSheet>::Style,
|
||||
pub(crate) position: Point,
|
||||
}
|
||||
impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer>
|
||||
impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
@ -455,8 +455,8 @@ where
|
|||
overlay::Element::new(Box::new(self))
|
||||
}
|
||||
}
|
||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Menu<'a, 'b, Message, Renderer>
|
||||
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Menu<'_, '_, Message, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ use std::rc::Rc;
|
|||
use iced_widget::core::{renderer, Element};
|
||||
|
||||
use crate::iced_core::{Alignment, Length};
|
||||
use crate::widget::icon;
|
||||
use crate::widget::menu::action::MenuAction;
|
||||
use crate::widget::menu::key_bind::KeyBind;
|
||||
use crate::widget::{icon, Button};
|
||||
use crate::{theme, widget};
|
||||
|
||||
/// Nested menu is essentially a tree of items, a menu is a collection of items
|
||||
|
|
@ -192,14 +192,13 @@ pub enum MenuItem<A: MenuAction, L: Into<Cow<'static, str>>> {
|
|||
/// - A button for the root menu item.
|
||||
pub fn menu_root<'a, Message, Renderer: renderer::Renderer>(
|
||||
label: impl Into<Cow<'a, str>> + 'a,
|
||||
) -> iced::Element<'a, Message, crate::Theme, Renderer>
|
||||
) -> Button<'a, Message>
|
||||
where
|
||||
Element<'a, Message, crate::Theme, Renderer>: From<widget::Button<'a, Message>>,
|
||||
{
|
||||
widget::button::custom(widget::text(label))
|
||||
.padding([4, 12])
|
||||
.class(theme::Button::MenuRoot)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Create a list of menu items from a vector of `MenuItem`.
|
||||
|
|
|
|||
|
|
@ -97,6 +97,14 @@ pub mod aspect_ratio;
|
|||
#[cfg(feature = "autosize")]
|
||||
pub mod autosize;
|
||||
|
||||
pub(crate) mod responsive_container;
|
||||
|
||||
#[cfg(feature = "surface-message")]
|
||||
mod responsive_menu_bar;
|
||||
#[cfg(feature = "surface-message")]
|
||||
#[doc(inline)]
|
||||
pub use responsive_menu_bar::responsive_menu_bar;
|
||||
|
||||
pub mod button;
|
||||
#[doc(inline)]
|
||||
pub use button::{Button, IconButton, LinkButton, TextButton};
|
||||
|
|
@ -335,9 +343,12 @@ pub use toggler::toggler;
|
|||
|
||||
#[doc(inline)]
|
||||
pub use tooltip::{tooltip, Tooltip};
|
||||
|
||||
#[cfg(all(feature = "wayland", feature = "winit"))]
|
||||
pub mod wayland;
|
||||
|
||||
pub mod tooltip {
|
||||
use crate::Element;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use iced::widget::tooltip::Position;
|
||||
|
||||
|
|
@ -362,6 +373,10 @@ pub mod warning;
|
|||
#[doc(inline)]
|
||||
pub use warning::*;
|
||||
|
||||
pub mod wrapper;
|
||||
#[doc(inline)]
|
||||
pub use wrapper::*;
|
||||
|
||||
#[cfg(feature = "markdown")]
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::markdown;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub fn nav_bar_toggle<Message>() -> NavBarToggle<Message> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, Message> {
|
||||
impl<Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'_, Message> {
|
||||
fn from(nav_bar_toggle: NavBarToggle<Message>) -> Self {
|
||||
let icon = if nav_bar_toggle.active {
|
||||
widget::icon::from_svg_bytes(
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> {
|
|||
// TODO More options for positioning similar to GdkPopup, xdg_popup
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for Popover<'a, Message, Renderer>
|
||||
impl<Message: Clone, Renderer> Widget<Message, crate::Theme, Renderer>
|
||||
for Popover<'_, Message, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
@ -305,8 +305,8 @@ pub struct Overlay<'a, 'b, Message, Renderer> {
|
|||
pos: Point,
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Overlay<'a, 'b, Message, Renderer>
|
||||
impl<Message, Renderer> overlay::Overlay<Message, crate::Theme, Renderer>
|
||||
for Overlay<'_, '_, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: iced_core::Renderer,
|
||||
|
|
@ -425,7 +425,7 @@ where
|
|||
) -> Option<overlay::Element<'c, Message, crate::Theme, Renderer>> {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(&mut self.tree, layout, renderer, Default::default())
|
||||
.overlay(self.tree, layout, renderer, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'a, Message, Renderer>
|
||||
impl<Message, Renderer> Widget<Message, Theme, Renderer> for Radio<'_, Message, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: iced_core::Renderer,
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ where
|
|||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let layout = self.container.layout(
|
||||
self.container.layout(
|
||||
tree,
|
||||
renderer,
|
||||
if self.ignore_bounds {
|
||||
|
|
@ -217,9 +217,7 @@ where
|
|||
} else {
|
||||
limits
|
||||
},
|
||||
);
|
||||
|
||||
layout
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
|
|
|
|||
299
src/widget/responsive_container.rs
Normal file
299
src/widget/responsive_container.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
//! Responsive Container, which will notify of size changes.
|
||||
|
||||
use iced::{Limits, Size};
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::layout;
|
||||
use iced_core::mouse;
|
||||
use iced_core::overlay;
|
||||
use iced_core::renderer;
|
||||
use iced_core::widget::{tree, Id, Tree};
|
||||
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget};
|
||||
|
||||
pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>(
|
||||
content: E,
|
||||
id: Id,
|
||||
on_action: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer>
|
||||
where
|
||||
E: Into<Element<'a, Message, Theme, crate::Renderer>>,
|
||||
Theme: iced_widget::container::Catalog,
|
||||
<Theme as iced_widget::container::Catalog>::Class<'a>: From<crate::theme::Container<'a>>,
|
||||
{
|
||||
ResponsiveContainer::new(content, id, on_action)
|
||||
}
|
||||
|
||||
/// An element decorating some content.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ResponsiveContainer<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
id: Id,
|
||||
size: Option<Size>,
|
||||
on_action: Box<dyn Fn(crate::surface::Action) -> Message>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
/// Creates an empty [`IdContainer`].
|
||||
pub(crate) fn new<T>(
|
||||
content: T,
|
||||
id: Id,
|
||||
on_action: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Into<Element<'a, Message, Theme, Renderer>>,
|
||||
{
|
||||
ResponsiveContainer {
|
||||
content: content.into(),
|
||||
id,
|
||||
size: None,
|
||||
on_action: Box::new(on_action),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn size(mut self, size: Size) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for ResponsiveContainer<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.children[0].diff(&mut self.content);
|
||||
}
|
||||
|
||||
fn size(&self) -> iced_core::Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let unrestricted_size = self.size.unwrap_or_else(|| {
|
||||
let node =
|
||||
self.content
|
||||
.as_widget()
|
||||
.layout(&mut tree.children[0], renderer, &Limits::NONE);
|
||||
node.size()
|
||||
});
|
||||
|
||||
let max_size = limits.max();
|
||||
let old_max = state.limits.max();
|
||||
state.needs_update = (unrestricted_size.width > max_size.width)
|
||||
^ (state.size.width > old_max.width)
|
||||
|| (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height);
|
||||
if state.needs_update {
|
||||
state.limits = *limits;
|
||||
state.size = unrestricted_size;
|
||||
}
|
||||
|
||||
let node = self
|
||||
.content
|
||||
.as_widget()
|
||||
.layout(&mut tree.children[0], renderer, limits);
|
||||
let size = node.size();
|
||||
layout::Node::with_children(size, vec![node])
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn iced_core::widget::Operation<()>,
|
||||
) {
|
||||
operation.container(Some(&self.id), layout.bounds(), &mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if state.needs_update {
|
||||
shell.publish((self.on_action)(
|
||||
crate::surface::Action::ResponsiveMenuBar {
|
||||
menu_bar: self.id.clone(),
|
||||
limits: state.limits,
|
||||
size: state.size,
|
||||
},
|
||||
));
|
||||
state.needs_update = false;
|
||||
}
|
||||
|
||||
self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event.clone(),
|
||||
layout.children().next().unwrap(),
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
content_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
renderer_style,
|
||||
content_layout,
|
||||
cursor_position,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
|
||||
fn drag_destinations(
|
||||
&self,
|
||||
state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
|
||||
) {
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
self.content.as_widget().drag_destinations(
|
||||
&state.children[0],
|
||||
content_layout,
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<crate::widget::Id> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: crate::widget::Id) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// get the a11y nodes for the widget
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
state: &Tree,
|
||||
p: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
let c_layout = layout.children().next().unwrap();
|
||||
let c_state = &state.children[0];
|
||||
self.content.as_widget().a11y_nodes(c_layout, c_state, p)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<ResponsiveContainer<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Renderer: 'a + iced_core::Renderer,
|
||||
Theme: 'a,
|
||||
{
|
||||
fn from(
|
||||
c: ResponsiveContainer<'a, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(c)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct State {
|
||||
limits: Limits,
|
||||
size: Size,
|
||||
needs_update: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
limits: Limits::NONE,
|
||||
size: Size::new(0., 0.),
|
||||
needs_update: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/widget/responsive_menu_bar.rs
Normal file
78
src/widget/responsive_menu_bar.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use apply::Apply;
|
||||
|
||||
use crate::{
|
||||
widget::{button, icon, responsive_container},
|
||||
Core, Element,
|
||||
};
|
||||
|
||||
use super::menu;
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if the menu bar collapses without tracking the size
|
||||
pub fn responsive_menu_bar<'a, Message: Clone + 'static, A: menu::Action<Message = Message>>(
|
||||
core: &Core,
|
||||
key_binds: &HashMap<menu::KeyBind, A>,
|
||||
id: crate::widget::Id,
|
||||
action_message: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
trees: Vec<(
|
||||
std::borrow::Cow<'static, str>,
|
||||
Vec<menu::Item<A, std::borrow::Cow<'static, str>>>,
|
||||
)>,
|
||||
) -> Element<'a, Message> {
|
||||
use crate::widget::id_container;
|
||||
|
||||
let menu_bar_size = core.menu_bars.get(&id);
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if !menu_bar_size.is_some_and(|(limits, size)| {
|
||||
let max_size = limits.max();
|
||||
max_size.width < size.width
|
||||
}) {
|
||||
responsive_container::responsive_container(
|
||||
id_container(
|
||||
menu::bar(
|
||||
trees
|
||||
.into_iter()
|
||||
.map(|mt| {
|
||||
menu::Tree::<_>::with_children(
|
||||
menu::root(mt.0),
|
||||
menu::items(key_binds, mt.1),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
crate::widget::Id::new(format!("menu_bar_expanded_{id}")),
|
||||
),
|
||||
id,
|
||||
action_message,
|
||||
)
|
||||
.apply(Element::from)
|
||||
} else {
|
||||
responsive_container::responsive_container(
|
||||
id_container(
|
||||
menu::bar(vec![menu::Tree::<_>::with_children(
|
||||
Element::from(
|
||||
button::icon(icon::from_name("open-menu-symbolic"))
|
||||
.padding([4, 12])
|
||||
.class(crate::theme::Button::MenuRoot),
|
||||
),
|
||||
menu::items(
|
||||
key_binds,
|
||||
trees
|
||||
.into_iter()
|
||||
.map(|mt| menu::Item::Folder(mt.0, mt.1))
|
||||
.collect(),
|
||||
),
|
||||
)]),
|
||||
crate::widget::Id::new(format!("menu_bar_collapsed_{id}")),
|
||||
),
|
||||
id,
|
||||
action_message,
|
||||
)
|
||||
.size(menu_bar_size.unwrap().1)
|
||||
.apply(Element::from)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,8 @@ where
|
|||
SegmentedButton::new(model)
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'a, Horizontal, SelectionMode, Message>
|
||||
impl<SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'_, Horizontal, SelectionMode, Message>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub struct EntityMut<'a, SelectionMode: Default> {
|
|||
pub(super) model: &'a mut Model<SelectionMode>,
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode>
|
||||
impl<SelectionMode: Default> EntityMut<'_, SelectionMode>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ where
|
|||
SegmentedButton::new(model)
|
||||
}
|
||||
|
||||
impl<'a, SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'a, Vertical, SelectionMode, Message>
|
||||
impl<SelectionMode, Message> SegmentedVariant
|
||||
for SegmentedButton<'_, Vertical, SelectionMode, Message>
|
||||
where
|
||||
Model<SelectionMode>: Selectable,
|
||||
SelectionMode: Default,
|
||||
|
|
|
|||
|
|
@ -547,8 +547,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
||||
for SegmentedButton<'a, Variant, SelectionMode, Message>
|
||||
impl<Variant, SelectionMode, Message> Widget<Message, crate::Theme, Renderer>
|
||||
for SegmentedButton<'_, Variant, SelectionMode, Message>
|
||||
where
|
||||
Self: SegmentedVariant,
|
||||
Model<SelectionMode>: Selectable,
|
||||
|
|
@ -562,7 +562,7 @@ where
|
|||
if let Some(ref context_menu) = self.context_menu {
|
||||
let mut tree = Tree::empty();
|
||||
tree.state = tree::State::new(MenuBarState::default());
|
||||
tree.children = menu_roots_children(&context_menu);
|
||||
tree.children = menu_roots_children(context_menu);
|
||||
children.push(tree);
|
||||
}
|
||||
|
||||
|
|
@ -719,7 +719,7 @@ where
|
|||
let on_dnd_enter =
|
||||
self.on_dnd_enter
|
||||
.as_ref()
|
||||
.zip(entity.clone())
|
||||
.zip(entity)
|
||||
.map(|(on_enter, entity)| {
|
||||
move |_, _, mime_types| on_enter(entity, mime_types)
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,9 +20,7 @@ pub fn section<'a, Message: 'static>() -> Section<'a, Message> {
|
|||
}
|
||||
|
||||
/// A section with a pre-defined list column.
|
||||
pub fn with_column<'a, Message: 'static>(
|
||||
children: ListColumn<'a, Message>,
|
||||
) -> Section<'a, Message> {
|
||||
pub fn with_column<Message: 'static>(children: ListColumn<'_, Message>) -> Section<'_, Message> {
|
||||
Section {
|
||||
title: Cow::Borrowed(""),
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ use crate::{
|
|||
Element,
|
||||
};
|
||||
use apply::Apply;
|
||||
use derive_setters::Setters;
|
||||
use iced::{alignment::Horizontal, Border, Shadow};
|
||||
use iced::{Alignment, Length};
|
||||
use std::marker::PhantomData;
|
||||
use iced::{Border, Shadow};
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Add, Sub};
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
/// Horizontal spin button widget.
|
||||
pub fn spin_button<'a, T, M>(
|
||||
|
|
@ -153,9 +151,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn horizontal_variant<'a, T, Message>(
|
||||
spin_button: SpinButton<'a, T, Message>,
|
||||
) -> Element<'a, Message>
|
||||
fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
||||
|
|
@ -193,7 +189,7 @@ where
|
|||
.into()
|
||||
}
|
||||
|
||||
fn vertical_variant<'a, T, Message>(spin_button: SpinButton<'a, T, Message>) -> Element<'a, Message>
|
||||
fn vertical_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ use super::style::StyleSheet;
|
|||
pub use super::value::Value;
|
||||
|
||||
use apply::Apply;
|
||||
use cosmic_theme::Theme;
|
||||
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
|
||||
use iced::clipboard::mime::AsMimeTypes;
|
||||
use iced::Limits;
|
||||
|
|
@ -40,10 +39,6 @@ use iced_core::{
|
|||
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
#[cfg(feature = "wayland")]
|
||||
use iced_renderer::core::event::{wayland, PlatformSpecific};
|
||||
#[cfg(feature = "wayland")]
|
||||
use iced_runtime::platform_specific;
|
||||
use iced_runtime::{task, Action, Task};
|
||||
|
||||
thread_local! {
|
||||
|
|
@ -200,7 +195,7 @@ pub struct TextInput<'a, Message> {
|
|||
error: Option<Cow<'a, str>>,
|
||||
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_submit: Option<Message>,
|
||||
on_submit: Option<Box<dyn Fn(String) -> Message + 'a>>,
|
||||
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
|
||||
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
||||
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
|
||||
|
|
@ -211,6 +206,8 @@ pub struct TextInput<'a, Message> {
|
|||
line_height: text::LineHeight,
|
||||
helper_line_height: text::LineHeight,
|
||||
always_active: bool,
|
||||
/// The text input tracks and manages the input value in its state.
|
||||
manage_value: bool,
|
||||
}
|
||||
|
||||
impl<'a, Message> TextInput<'a, Message>
|
||||
|
|
@ -255,6 +252,7 @@ where
|
|||
label: None,
|
||||
helper_text: None,
|
||||
always_active: false,
|
||||
manage_value: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -340,14 +338,24 @@ where
|
|||
|
||||
/// Sets the message that should be produced when the [`TextInput`] is
|
||||
/// focused and the enter key is pressed.
|
||||
pub fn on_submit(self, message: Message) -> Self {
|
||||
self.on_submit_maybe(Some(message))
|
||||
pub fn on_submit<F>(self, callback: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(String) -> Message,
|
||||
{
|
||||
self.on_submit_maybe(Some(Box::new(callback)))
|
||||
}
|
||||
|
||||
/// Maybe sets the message that should be produced when the [`TextInput`] is
|
||||
/// focused and the enter key is pressed.
|
||||
pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
|
||||
self.on_submit = message;
|
||||
pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
|
||||
where
|
||||
F: 'a + Fn(String) -> Message,
|
||||
{
|
||||
if let Some(callback) = callback {
|
||||
self.on_submit = Some(Box::new(callback));
|
||||
} else {
|
||||
self.on_submit = None;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +424,12 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the text input to manage its input value or not
|
||||
pub fn manage_value(mut self, manage_value: bool) -> Self {
|
||||
self.manage_value = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its
|
||||
/// [`Value`] if provided.
|
||||
///
|
||||
|
|
@ -507,7 +521,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'a, Message>
|
||||
impl<Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'_, Message>
|
||||
where
|
||||
Message: Clone + 'static,
|
||||
{
|
||||
|
|
@ -526,9 +540,14 @@ where
|
|||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value {
|
||||
state.tracked_value = self.value.clone();
|
||||
} else if self.value.is_empty() {
|
||||
self.value = state.tracked_value.clone();
|
||||
// std::mem::swap(&mut state.tracked_value, &mut self.value);
|
||||
}
|
||||
// Unfocus text input if it becomes disabled
|
||||
if self.on_input.is_none() {
|
||||
if self.on_input.is_none() && !self.manage_value {
|
||||
state.last_click = None;
|
||||
state.is_focused = None;
|
||||
state.is_pasting = None;
|
||||
|
|
@ -581,13 +600,10 @@ where
|
|||
// if the previous state was at the end of the text, keep it there
|
||||
let old_value = Value::new(&old_value);
|
||||
if state.is_focused.is_some() {
|
||||
match state.cursor.state(&old_value) {
|
||||
cursor::State::Index(index) => {
|
||||
if index == old_value.len() {
|
||||
state.cursor.move_to(self.value.len());
|
||||
}
|
||||
if let cursor::State::Index(index) = state.cursor.state(&old_value) {
|
||||
if index == old_value.len() {
|
||||
state.cursor.move_to(self.value.len());
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -597,6 +613,11 @@ where
|
|||
state.is_focused = None;
|
||||
}
|
||||
|
||||
// Stop pasting if input becomes disabled
|
||||
if !self.manage_value && self.on_input.is_none() {
|
||||
state.is_pasting = None;
|
||||
}
|
||||
|
||||
let mut children: Vec<_> = self
|
||||
.leading_icon
|
||||
.iter_mut()
|
||||
|
|
@ -779,7 +800,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
if tree.children.len() > 0 {
|
||||
if !tree.children.is_empty() {
|
||||
let index = tree.children.len() - 1;
|
||||
if let (Some(trailing_icon), Some(tree)) =
|
||||
(self.trailing_icon.as_mut(), tree.children.get_mut(index))
|
||||
|
|
@ -824,13 +845,14 @@ where
|
|||
self.is_editable,
|
||||
self.on_input.as_deref(),
|
||||
self.on_paste.as_deref(),
|
||||
&self.on_submit,
|
||||
self.on_submit.as_deref(),
|
||||
self.on_toggle_edit.as_deref(),
|
||||
|| tree.state.downcast_mut::<State>(),
|
||||
self.on_create_dnd_source.as_deref(),
|
||||
dnd_id,
|
||||
line_height,
|
||||
layout,
|
||||
self.manage_value,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -856,7 +878,7 @@ where
|
|||
&self.placeholder,
|
||||
self.size,
|
||||
self.font,
|
||||
self.on_input.is_none(),
|
||||
self.on_input.is_none() && !self.manage_value,
|
||||
self.is_secure,
|
||||
self.leading_icon.as_ref(),
|
||||
self.trailing_icon.as_ref(),
|
||||
|
|
@ -925,7 +947,11 @@ where
|
|||
}
|
||||
let mut children = layout.children();
|
||||
let layout = children.next().unwrap();
|
||||
mouse_interaction(layout, cursor_position, self.on_input.is_none())
|
||||
mouse_interaction(
|
||||
layout,
|
||||
cursor_position,
|
||||
self.on_input.is_none() && !self.manage_value,
|
||||
)
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<Id> {
|
||||
|
|
@ -1236,13 +1262,14 @@ pub fn update<'a, Message: 'static>(
|
|||
is_editable: bool,
|
||||
on_input: Option<&dyn Fn(String) -> Message>,
|
||||
on_paste: Option<&dyn Fn(String) -> Message>,
|
||||
on_submit: &Option<Message>,
|
||||
on_submit: Option<&dyn Fn(String) -> Message>,
|
||||
on_toggle_edit: Option<&dyn Fn(bool) -> Message>,
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
#[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>,
|
||||
#[allow(unused_variables)] dnd_id: u128,
|
||||
line_height: text::LineHeight,
|
||||
layout: Layout<'_>,
|
||||
manage_value: bool,
|
||||
) -> event::Status
|
||||
where
|
||||
Message: Clone,
|
||||
|
|
@ -1264,7 +1291,7 @@ where
|
|||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = state();
|
||||
|
||||
let click_position = if on_input.is_some() {
|
||||
let click_position = if on_input.is_some() || manage_value {
|
||||
cursor.position_over(layout.bounds())
|
||||
} else {
|
||||
None
|
||||
|
|
@ -1299,7 +1326,7 @@ where
|
|||
// single click that is on top of the selected text
|
||||
// is the click on selected text?
|
||||
|
||||
if let Some(on_input) = on_input {
|
||||
if manage_value || on_input.is_some() {
|
||||
let left = start.min(end);
|
||||
let right = end.max(start);
|
||||
|
||||
|
|
@ -1339,8 +1366,11 @@ where
|
|||
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
let message = (on_input)(contents);
|
||||
shell.publish(message);
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
if let Some(on_input) = on_input {
|
||||
let message = (on_input)(contents);
|
||||
shell.publish(message);
|
||||
}
|
||||
if let Some(on_start_dnd) = on_start_dnd_source {
|
||||
shell.publish(on_start_dnd(state.clone()));
|
||||
}
|
||||
|
|
@ -1349,7 +1379,7 @@ where
|
|||
iced_core::clipboard::start_dnd(
|
||||
clipboard,
|
||||
false,
|
||||
id.map(|id| iced_core::clipboard::DndSource::Widget(id)),
|
||||
id.map(iced_core::clipboard::DndSource::Widget),
|
||||
Some(iced_core::clipboard::IconSurface::new(
|
||||
Element::from(
|
||||
TextInput::<'static, ()>::new("", input_text.clone())
|
||||
|
|
@ -1531,7 +1561,7 @@ where
|
|||
let state = state();
|
||||
|
||||
if let Some(focus) = &mut state.is_focused {
|
||||
let Some(on_input) = on_input else {
|
||||
if !manage_value && on_input.is_none() {
|
||||
return event::Status::Ignored;
|
||||
};
|
||||
|
||||
|
|
@ -1545,8 +1575,8 @@ where
|
|||
|
||||
match key {
|
||||
keyboard::Key::Named(keyboard::key::Named::Enter) => {
|
||||
if let Some(on_submit) = on_submit.clone() {
|
||||
shell.publish(on_submit);
|
||||
if let Some(on_submit) = on_submit {
|
||||
shell.publish((on_submit)(unsecured_value.to_string()));
|
||||
}
|
||||
}
|
||||
keyboard::Key::Named(keyboard::key::Named::Backspace) => {
|
||||
|
|
@ -1566,9 +1596,11 @@ where
|
|||
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
let message = (on_input)(editor.contents());
|
||||
shell.publish(message);
|
||||
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
if let Some(on_input) = on_input {
|
||||
let message = (on_input)(editor.contents());
|
||||
shell.publish(message);
|
||||
}
|
||||
let value = if is_secure {
|
||||
unsecured_value.secure()
|
||||
} else {
|
||||
|
|
@ -1592,8 +1624,12 @@ where
|
|||
editor.delete();
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
let message = (on_input)(contents);
|
||||
shell.publish(message);
|
||||
if let Some(on_input) = on_input {
|
||||
let message = (on_input)(contents);
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
let value = if is_secure {
|
||||
unsecured_value.secure()
|
||||
} else {
|
||||
|
|
@ -1671,10 +1707,12 @@ where
|
|||
|
||||
let mut editor = Editor::new(value, &mut state.cursor);
|
||||
editor.delete();
|
||||
|
||||
let message = (on_input)(editor.contents());
|
||||
|
||||
shell.publish(message);
|
||||
let content = editor.contents();
|
||||
state.tracked_value = Value::new(&content);
|
||||
if let Some(on_input) = on_input {
|
||||
let message = (on_input)(content);
|
||||
shell.publish(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
keyboard::Key::Character(c)
|
||||
|
|
@ -1699,13 +1737,16 @@ where
|
|||
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
let message = if let Some(paste) = &on_paste {
|
||||
(paste)(contents)
|
||||
} else {
|
||||
(on_input)(contents)
|
||||
};
|
||||
shell.publish(message);
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
if let Some(on_input) = on_input {
|
||||
let message = if let Some(paste) = &on_paste {
|
||||
(paste)(contents)
|
||||
} else {
|
||||
(on_input)(contents)
|
||||
};
|
||||
|
||||
shell.publish(message);
|
||||
}
|
||||
state.is_pasting = Some(content);
|
||||
|
||||
let value = if is_secure {
|
||||
|
|
@ -1750,8 +1791,11 @@ where
|
|||
}
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
let message = (on_input)(contents);
|
||||
shell.publish(message);
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
if let Some(on_input) = on_input {
|
||||
let message = (on_input)(contents);
|
||||
shell.publish(message);
|
||||
}
|
||||
|
||||
focus.updated_at = Instant::now();
|
||||
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
|
||||
|
|
@ -1926,7 +1970,7 @@ where
|
|||
editor.paste(Value::new(content.as_str()));
|
||||
let contents = editor.contents();
|
||||
let unsecured_value = Value::new(&contents);
|
||||
|
||||
state.tracked_value = unsecured_value.clone();
|
||||
if let Some(on_paste) = on_paste.as_ref() {
|
||||
let message = (on_paste)(contents);
|
||||
shell.publish(message);
|
||||
|
|
@ -2408,6 +2452,7 @@ pub(crate) struct DndOfferState;
|
|||
#[derive(Debug, Default, Clone)]
|
||||
#[must_use]
|
||||
pub struct State {
|
||||
pub tracked_value: Value,
|
||||
pub value: crate::Plain,
|
||||
pub placeholder: crate::Plain,
|
||||
pub label: crate::Plain,
|
||||
|
|
@ -2482,6 +2527,7 @@ impl State {
|
|||
/// Creates a new [`State`], representing a focused [`TextInput`].
|
||||
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
|
||||
Self {
|
||||
tracked_value: Value::default(),
|
||||
is_secure,
|
||||
value: crate::Plain::default(),
|
||||
placeholder: crate::Plain::default(),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||
///
|
||||
/// [`TextInput`]: crate::widget::TextInput
|
||||
// TODO: Reduce allocations, cache results (?)
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct Value {
|
||||
graphemes: Vec<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use std::rc::Rc;
|
|||
|
||||
use crate::widget::container;
|
||||
use crate::widget::Column;
|
||||
use iced::{Padding, Task};
|
||||
use iced::Task;
|
||||
use iced_core::Element;
|
||||
use slotmap::new_key_type;
|
||||
use slotmap::SlotMap;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ impl<'a, Message, Theme, Renderer> Toaster<'a, Message, Theme, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toaster<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Toaster<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: iced_core::Renderer,
|
||||
{
|
||||
|
|
@ -191,8 +191,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for ToasterOverlay<'a, 'b, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for ToasterOverlay<'_, '_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
{
|
||||
|
|
|
|||
1
src/widget/wayland/mod.rs
Normal file
1
src/widget/wayland/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod tooltip;
|
||||
76
src/widget/wayland/tooltip/mod.rs
Normal file
76
src/widget/wayland/tooltip/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Change the apperance of a tooltip.
|
||||
|
||||
pub mod widget;
|
||||
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use iced_core::{border::Radius, Background, Color, Vector};
|
||||
|
||||
use crate::theme::THEME;
|
||||
|
||||
/// The appearance of a tooltip.
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The amount of offset to apply to the shadow of the tooltip.
|
||||
pub shadow_offset: Vector,
|
||||
|
||||
/// The [`Background`] of the tooltip.
|
||||
pub background: Option<Background>,
|
||||
|
||||
/// The border radius of the tooltip.
|
||||
pub border_radius: Radius,
|
||||
|
||||
/// The border width of the tooltip.
|
||||
pub border_width: f32,
|
||||
|
||||
/// The border [`Color`] of the tooltip.
|
||||
pub border_color: Color,
|
||||
|
||||
/// An outline placed around the border.
|
||||
pub outline_width: f32,
|
||||
|
||||
/// The [`Color`] of the outline.
|
||||
pub outline_color: Color,
|
||||
|
||||
/// The icon [`Color`] of the tooltip.
|
||||
pub icon_color: Option<Color>,
|
||||
|
||||
/// The text [`Color`] of the tooltip.
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
// TODO: `Radius` is not `const fn` compatible.
|
||||
pub fn new() -> Self {
|
||||
let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0;
|
||||
Self {
|
||||
shadow_offset: Vector::new(0.0, 0.0),
|
||||
background: None,
|
||||
border_radius: Radius::from(rad_0),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
outline_width: 0.0,
|
||||
outline_color: Color::TRANSPARENT,
|
||||
icon_color: None,
|
||||
text_color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for Style {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO update to match other styles
|
||||
/// A set of rules that dictate the style of a tooltip.
|
||||
pub trait Catalog {
|
||||
/// The supported style of the [`StyleSheet`].
|
||||
type Class: Default;
|
||||
|
||||
/// Produces the active [`Appearance`] of a tooltip.
|
||||
fn style(&self, style: &Self::Class) -> Style;
|
||||
}
|
||||
684
src/widget/wayland/tooltip/widget.rs
Normal file
684
src/widget/wayland/tooltip/widget.rs
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
// Copyright 2019 H<>ctor Ram<61>n, Iced contributors
|
||||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Allow your users to perform actions by pressing a button.
|
||||
//!
|
||||
//! A [`Tooltip`] has some local [`State`].
|
||||
|
||||
use std::any::Any;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use iced::Task;
|
||||
use iced_runtime::core::widget::Id;
|
||||
|
||||
use iced_core::event::{self, Event};
|
||||
use iced_core::renderer;
|
||||
use iced_core::touch;
|
||||
use iced_core::widget::tree::{self, Tree};
|
||||
use iced_core::widget::Operation;
|
||||
use iced_core::{layout, svg};
|
||||
use iced_core::{mouse, Border};
|
||||
use iced_core::{overlay, Shadow};
|
||||
use iced_core::{
|
||||
Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget,
|
||||
};
|
||||
|
||||
pub use super::{Catalog, Style};
|
||||
|
||||
/// Internally defines different button widget variants.
|
||||
enum Variant<Message> {
|
||||
Normal,
|
||||
Image {
|
||||
close_icon: svg::Handle,
|
||||
on_remove: Option<Message>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A generic button which emits a message when pressed.
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[must_use]
|
||||
pub struct Tooltip<'a, Message, TopLevelMessage> {
|
||||
id: Id,
|
||||
#[cfg(feature = "a11y")]
|
||||
name: Option<std::borrow::Cow<'a, str>>,
|
||||
#[cfg(feature = "a11y")]
|
||||
description: Option<iced_accessibility::Description<'a>>,
|
||||
#[cfg(feature = "a11y")]
|
||||
label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
|
||||
content: crate::Element<'a, Message>,
|
||||
on_leave: Message,
|
||||
on_surface_action: Box<dyn Fn(crate::surface::Action) -> Message>,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
selected: bool,
|
||||
style: crate::theme::Tooltip,
|
||||
delay: Option<Duration>,
|
||||
settings: Option<
|
||||
Arc<
|
||||
dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
view: Arc<
|
||||
dyn Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>> + Send + Sync + 'static,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> {
|
||||
/// Creates a new [`Tooltip`] with the given content.
|
||||
pub fn new(
|
||||
content: impl Into<crate::Element<'a, Message>>,
|
||||
settings: Option<
|
||||
impl Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>,
|
||||
view: impl Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
on_leave: Message,
|
||||
on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Id::unique(),
|
||||
#[cfg(feature = "a11y")]
|
||||
name: None,
|
||||
#[cfg(feature = "a11y")]
|
||||
description: None,
|
||||
#[cfg(feature = "a11y")]
|
||||
label: None,
|
||||
content: content.into(),
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
padding: Padding::new(0.0),
|
||||
selected: false,
|
||||
style: crate::theme::Tooltip::default(),
|
||||
on_leave,
|
||||
on_surface_action: Box::new(on_surface_action),
|
||||
delay: None,
|
||||
settings: if let Some(s) = settings {
|
||||
Some(Arc::new(s))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
view: Arc::new(view),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delay(mut self, dur: Duration) -> Self {
|
||||
self.delay = Some(dur);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Id`] of the [`Tooltip`].
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Tooltip`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Tooltip`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Tooltip`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the widget to a selected state.
|
||||
///
|
||||
/// Displays a selection indicator on image buttons.
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Tooltip`].
|
||||
pub fn class(mut self, style: crate::theme::Tooltip) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// Sets the name of the [`Tooltip`].
|
||||
pub fn name(mut self, name: impl Into<std::borrow::Cow<'a, str>>) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// Sets the description of the [`Tooltip`].
|
||||
pub fn description_widget<T: iced_accessibility::Describes>(mut self, description: &T) -> Self {
|
||||
self.description = Some(iced_accessibility::Description::Id(
|
||||
description.description(),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// Sets the description of the [`Tooltip`].
|
||||
pub fn description(mut self, description: impl Into<std::borrow::Cow<'a, str>>) -> Self {
|
||||
self.description = Some(iced_accessibility::Description::Text(description.into()));
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// Sets the label of the [`Tooltip`].
|
||||
pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
|
||||
self.label = Some(label.label().into_iter().map(|l| l.into()).collect());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone>
|
||||
Widget<Message, crate::Theme, crate::Renderer> for Tooltip<'a, Message, TopLevelMessage>
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
vec![Tree::new(&self.content)]
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(std::slice::from_mut(&mut self.content));
|
||||
}
|
||||
|
||||
fn size(&self) -> iced_core::Size<Length> {
|
||||
iced_core::Size::new(self.width, self.height)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &crate::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
layout(
|
||||
renderer,
|
||||
limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
|renderer, limits| {
|
||||
self.content
|
||||
.as_widget()
|
||||
.layout(&mut tree.children[0], renderer, limits)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &crate::Renderer,
|
||||
operation: &mut dyn Operation<()>,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.content.as_widget().operate(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &crate::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let status = update(
|
||||
self.id.clone(),
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor,
|
||||
shell,
|
||||
self.settings.as_ref(),
|
||||
&self.view,
|
||||
self.delay,
|
||||
&self.on_leave,
|
||||
&self.on_surface_action,
|
||||
|| tree.state.downcast_mut::<State>(),
|
||||
);
|
||||
status.merge(self.content.as_widget_mut().on_event(
|
||||
&mut tree.children[0],
|
||||
event,
|
||||
layout.children().next().unwrap(),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut crate::Renderer,
|
||||
theme: &crate::Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
if !viewport.intersects(&bounds) {
|
||||
return;
|
||||
}
|
||||
let content_layout = layout.children().next().unwrap();
|
||||
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let styling = theme.style(&self.style);
|
||||
|
||||
let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
|
||||
|
||||
draw::<_, crate::Theme>(
|
||||
renderer,
|
||||
bounds,
|
||||
*viewport,
|
||||
&styling,
|
||||
|renderer, _styling| {
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
&renderer::Style {
|
||||
icon_color,
|
||||
text_color: styling.text_color,
|
||||
scale_factor: renderer_style.scale_factor,
|
||||
},
|
||||
content_layout,
|
||||
cursor,
|
||||
&viewport.intersection(&bounds).unwrap_or_default(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &crate::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &crate::Renderer,
|
||||
mut translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
||||
let position = layout.bounds().position();
|
||||
translation.x += position.x;
|
||||
translation.y += position.y;
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout.children().next().unwrap(),
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "a11y")]
|
||||
/// get the a11y nodes for the widget
|
||||
fn a11y_nodes(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
state: &Tree,
|
||||
p: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
let c_layout = layout.children().next().unwrap();
|
||||
|
||||
self.content.as_widget().a11y_nodes(c_layout, state, p)
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<Id> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: Id) {
|
||||
self.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>
|
||||
From<Tooltip<'a, Message, TopLevelMessage>> for crate::Element<'a, Message>
|
||||
{
|
||||
fn from(button: Tooltip<'a, Message, TopLevelMessage>) -> Self {
|
||||
Self::new(button)
|
||||
}
|
||||
}
|
||||
|
||||
/// The local state of a [`Tooltip`].
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(clippy::struct_field_names)]
|
||||
pub struct State {
|
||||
is_hovered: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Returns whether the [`Tooltip`] is currently hovered or not.
|
||||
pub fn is_hovered(self) -> bool {
|
||||
let guard = self.is_hovered.lock().unwrap();
|
||||
*guard
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`]
|
||||
/// accordingly.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
|
||||
_id: Id,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
settings: Option<
|
||||
&Arc<
|
||||
dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
view: &Arc<
|
||||
dyn Fn() -> crate::Element<'static, crate::Action<TopLevelMessage>> + Send + Sync + 'static,
|
||||
>,
|
||||
delay: Option<Duration>,
|
||||
on_leave: &Message,
|
||||
on_surface_action: &dyn Fn(crate::surface::Action) -> Message,
|
||||
state: impl FnOnce() -> &'a mut State,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Touch(touch::Event::FingerLifted { .. }) => {
|
||||
let state = state();
|
||||
let mut guard = state.is_hovered.lock().unwrap();
|
||||
if *guard {
|
||||
*guard = false;
|
||||
|
||||
shell.publish(on_leave.clone());
|
||||
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
|
||||
Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => {
|
||||
let state = state();
|
||||
let mut guard = state.is_hovered.lock().unwrap();
|
||||
|
||||
if *guard {
|
||||
*guard = false;
|
||||
|
||||
shell.publish(on_leave.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
let state = state();
|
||||
let bounds = layout.bounds();
|
||||
let is_hovered = state.is_hovered.clone();
|
||||
let mut guard = state.is_hovered.lock().unwrap();
|
||||
|
||||
if *guard {
|
||||
*guard = cursor.is_over(bounds);
|
||||
if !*guard {
|
||||
shell.publish(on_leave.clone());
|
||||
}
|
||||
} else {
|
||||
*guard = cursor.is_over(bounds);
|
||||
if *guard {
|
||||
if let Some(settings) = settings {
|
||||
if let Some(delay) = delay {
|
||||
let s = settings.clone();
|
||||
let view = view.clone();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let sm = crate::surface::Action::Task(Arc::new(move || {
|
||||
let s = s.clone();
|
||||
let view = view.clone();
|
||||
let is_hovered = is_hovered.clone();
|
||||
Task::future(async move {
|
||||
#[cfg(feature = "tokio")]
|
||||
{
|
||||
_ = tokio::time::sleep(delay).await;
|
||||
}
|
||||
#[cfg(feature = "async-std")]
|
||||
{
|
||||
_ = async_std::task::sleep(delay).await;
|
||||
}
|
||||
let is_hovered = is_hovered.clone();
|
||||
let g = is_hovered.lock().unwrap();
|
||||
if !*g {
|
||||
return crate::surface::Action::Ignore;
|
||||
}
|
||||
let boxed: Box<
|
||||
dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || s(bounds));
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> =
|
||||
Box::new(boxed);
|
||||
crate::surface::Action::Popup(
|
||||
Arc::new(boxed),
|
||||
Some({
|
||||
let boxed: Box<
|
||||
dyn Fn() -> crate::Element<
|
||||
'static,
|
||||
crate::Action<TopLevelMessage>,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || view());
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> =
|
||||
Box::new(boxed);
|
||||
Arc::new(boxed)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}));
|
||||
|
||||
shell.publish((on_surface_action)(sm));
|
||||
} else {
|
||||
let s = settings.clone();
|
||||
let view = view.clone();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let boxed: Box<
|
||||
dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || s(bounds));
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
|
||||
|
||||
let sm = crate::surface::Action::Popup(
|
||||
Arc::new(boxed),
|
||||
Some({
|
||||
let boxed: Box<
|
||||
dyn Fn() -> crate::Element<
|
||||
'static,
|
||||
crate::Action<TopLevelMessage>,
|
||||
> + Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
> = Box::new(move || view());
|
||||
let boxed: Box<dyn Any + Send + Sync + 'static> =
|
||||
Box::new(boxed);
|
||||
Arc::new(boxed)
|
||||
}),
|
||||
);
|
||||
shell.publish((on_surface_action)(sm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn draw<Renderer: iced_core::Renderer, Theme>(
|
||||
renderer: &mut Renderer,
|
||||
bounds: Rectangle,
|
||||
viewport_bounds: Rectangle,
|
||||
styling: &super::Style,
|
||||
draw_contents: impl FnOnce(&mut Renderer, &Style),
|
||||
) where
|
||||
Theme: super::Catalog,
|
||||
{
|
||||
let doubled_border_width = styling.border_width * 2.0;
|
||||
let doubled_outline_width = styling.outline_width * 2.0;
|
||||
|
||||
if styling.outline_width > 0.0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x - styling.border_width - styling.outline_width,
|
||||
y: bounds.y - styling.border_width - styling.outline_width,
|
||||
width: bounds.width + doubled_border_width + doubled_outline_width,
|
||||
height: bounds.height + doubled_border_width + doubled_outline_width,
|
||||
},
|
||||
border: Border {
|
||||
width: styling.outline_width,
|
||||
color: styling.outline_color,
|
||||
radius: styling.border_radius,
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
Color::TRANSPARENT,
|
||||
);
|
||||
}
|
||||
|
||||
if styling.background.is_some() || styling.border_width > 0.0 {
|
||||
if styling.shadow_offset != Vector::default() {
|
||||
// TODO: Implement proper shadow support
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x + styling.shadow_offset.x,
|
||||
y: bounds.y + styling.shadow_offset.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
},
|
||||
border: Border {
|
||||
radius: styling.border_radius,
|
||||
..Default::default()
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
Background::Color([0.0, 0.0, 0.0, 0.5].into()),
|
||||
);
|
||||
}
|
||||
|
||||
// Draw the button background first.
|
||||
if let Some(background) = styling.background {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: Border {
|
||||
radius: styling.border_radius,
|
||||
..Default::default()
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
background,
|
||||
);
|
||||
}
|
||||
|
||||
// Then draw the button contents onto the background.
|
||||
draw_contents(renderer, styling);
|
||||
|
||||
let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default();
|
||||
clipped_bounds.height += styling.border_width;
|
||||
|
||||
renderer.with_layer(clipped_bounds, |renderer| {
|
||||
// Finish by drawing the border above the contents.
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: Border {
|
||||
width: styling.border_width,
|
||||
color: styling.border_color,
|
||||
radius: styling.border_radius,
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
Color::TRANSPARENT,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
draw_contents(renderer, styling);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the layout of a [`Tooltip`].
|
||||
pub fn layout<Renderer>(
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
width: Length,
|
||||
height: Length,
|
||||
padding: Padding,
|
||||
layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(width).height(height);
|
||||
|
||||
let mut content = layout_content(renderer, &limits.shrink(padding));
|
||||
let padding = padding.fit(content.size(), limits.max());
|
||||
let size = limits
|
||||
.shrink(padding)
|
||||
.resolve(width, height, content.size())
|
||||
.expand(padding);
|
||||
|
||||
content = content.move_to(Point::new(padding.left, padding.top));
|
||||
|
||||
layout::Node::with_children(size, vec![content])
|
||||
}
|
||||
220
src/widget/wrapper.rs
Normal file
220
src/widget/wrapper.rs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
thread::{self, ThreadId},
|
||||
};
|
||||
|
||||
use crate::Element;
|
||||
use iced::{event, Length, Rectangle, Size};
|
||||
use iced_core::{id::Id, widget, widget::tree, Widget};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RcWrapper<T> {
|
||||
pub(crate) data: Rc<RefCell<T>>,
|
||||
pub(crate) thread_id: ThreadId,
|
||||
}
|
||||
|
||||
impl<T> Clone for RcWrapper<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
data: self.data.clone(),
|
||||
thread_id: self.thread_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<M: 'static> Send for RcWrapper<M> {}
|
||||
unsafe impl<M: 'static> Sync for RcWrapper<M> {}
|
||||
|
||||
impl<T> RcWrapper<T> {
|
||||
pub fn new(element: T) -> Self {
|
||||
Self {
|
||||
data: Rc::new(RefCell::new(element)),
|
||||
thread_id: thread::current().id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if used outside of original thread.
|
||||
pub fn with_data<O>(&self, f: impl FnOnce(&T) -> O) -> O {
|
||||
assert_eq!(self.thread_id, thread::current().id());
|
||||
let my_ref: &T = &RefCell::borrow(self.data.as_ref());
|
||||
f(my_ref)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if used outside of original thread.
|
||||
pub fn with_data_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
|
||||
assert_eq!(self.thread_id, thread::current().id());
|
||||
let my_refmut: &mut T = &mut RefCell::borrow_mut(self.data.as_ref());
|
||||
f(my_refmut)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if used outside of original thread.
|
||||
pub(crate) unsafe fn as_ptr(&self) -> *mut T {
|
||||
assert_eq!(self.thread_id, thread::current().id());
|
||||
RefCell::as_ptr(self.data.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RcElementWrapper<M> {
|
||||
pub(crate) element: RcWrapper<Element<'static, M>>,
|
||||
}
|
||||
|
||||
impl<M> RcElementWrapper<M> {
|
||||
#[must_use]
|
||||
pub fn new(element: Element<'static, M>) -> Self {
|
||||
RcElementWrapper {
|
||||
element: RcWrapper::new(element),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Widget<M, crate::Theme, crate::Renderer> for RcElementWrapper<M> {
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.element.with_data(|e| e.as_widget().size())
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.element.with_data(move |e| e.as_widget().size_hint())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut tree::Tree,
|
||||
renderer: &crate::Renderer,
|
||||
limits: &crate::iced_core::layout::Limits,
|
||||
) -> crate::iced_core::layout::Node {
|
||||
self.element
|
||||
.with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits))
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &tree::Tree,
|
||||
renderer: &mut crate::Renderer,
|
||||
theme: &crate::Theme,
|
||||
style: &crate::iced_core::renderer::Style,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
cursor: crate::iced_core::mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.element.with_data(move |e| {
|
||||
e.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
});
|
||||
}
|
||||
|
||||
fn tag(&self) -> tree::Tag {
|
||||
self.element.with_data(|e| e.as_widget().tag())
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
self.element.with_data(|e| e.as_widget().state())
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<tree::Tree> {
|
||||
self.element.with_data(|e| e.as_widget().children())
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut tree::Tree) {
|
||||
self.element.with_data_mut(|e| e.as_widget_mut().diff(tree));
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
state: &mut tree::Tree,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
renderer: &crate::Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.element.with_data(|e| {
|
||||
e.as_widget().operate(state, layout, renderer, operation);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
state: &mut tree::Tree,
|
||||
event: crate::iced::Event,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
cursor: crate::iced_core::mouse::Cursor,
|
||||
renderer: &crate::Renderer,
|
||||
clipboard: &mut dyn crate::iced_core::Clipboard,
|
||||
shell: &mut crate::iced_core::Shell<'_, M>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.element.with_data_mut(|e| {
|
||||
e.as_widget_mut().on_event(
|
||||
state, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
state: &tree::Tree,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
cursor: crate::iced_core::mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &crate::Renderer,
|
||||
) -> crate::iced_core::mouse::Interaction {
|
||||
self.element.with_data(|e| {
|
||||
e.as_widget()
|
||||
.mouse_interaction(state, layout, cursor, viewport, renderer)
|
||||
})
|
||||
}
|
||||
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
state: &'a mut tree::Tree,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
renderer: &crate::Renderer,
|
||||
translation: crate::iced_core::Vector,
|
||||
) -> Option<crate::iced_core::overlay::Element<'a, M, crate::Theme, crate::Renderer>> {
|
||||
assert_eq!(self.element.thread_id, thread::current().id());
|
||||
Rc::get_mut(&mut self.element.data).and_then(|e| {
|
||||
e.get_mut()
|
||||
.as_widget_mut()
|
||||
.overlay(state, layout, renderer, translation)
|
||||
})
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<Id> {
|
||||
self.element.with_data_mut(|e| e.as_widget_mut().id())
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: Id) {
|
||||
self.element.with_data_mut(|e| e.as_widget_mut().set_id(id));
|
||||
}
|
||||
|
||||
fn drag_destinations(
|
||||
&self,
|
||||
state: &tree::Tree,
|
||||
layout: crate::iced_core::Layout<'_>,
|
||||
renderer: &crate::Renderer,
|
||||
dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles,
|
||||
) {
|
||||
self.element.with_data_mut(|e| {
|
||||
e.as_widget_mut()
|
||||
.drag_destinations(state, layout, renderer, dnd_rectangles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: 'static> From<RcElementWrapper<Message>> for Element<'static, Message> {
|
||||
fn from(wrapper: RcElementWrapper<Message>) -> Self {
|
||||
Element::new(wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: 'static> From<Element<'static, Message>> for RcElementWrapper<Message> {
|
||||
fn from(e: Element<'static, Message>) -> Self {
|
||||
RcElementWrapper::new(e)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue