feat: Tooltips and Better Surface Management

This commit is contained in:
Ashley Wulber 2025-03-14 11:56:21 -04:00 committed by GitHub
parent c7edd37b03
commit 337b80d4ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 3651 additions and 977 deletions

View file

@ -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),
)
});

View file

@ -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,
{

View file

@ -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,
{

View file

@ -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();

View file

@ -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,

View file

@ -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;

View file

@ -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();

View file

@ -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());

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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,
{

View file

@ -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)]
}

View file

@ -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>()
}

View file

@ -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 {

View file

@ -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)]

View file

@ -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)]

View file

@ -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)

View file

@ -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
}

View file

@ -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,

View file

@ -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,

View file

@ -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 {

View file

@ -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()
}

View file

@ -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 {

View file

@ -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))

View file

@ -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()

View file

@ -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()
}

View file

@ -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,
{

View file

@ -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,
{

View file

@ -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, ..

View file

@ -55,6 +55,7 @@
//!
pub mod action;
pub use action::MenuAction as Action;
mod flex;

View file

@ -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

View file

@ -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;

View file

@ -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,
{

View file

@ -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`.

View file

@ -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;

View file

@ -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(

View file

@ -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())
}
}

View file

@ -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,

View file

@ -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(

View 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,
}
}
}

View 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)
}
}

View file

@ -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,

View file

@ -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,
{

View file

@ -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,

View file

@ -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)
});

View file

@ -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,

View file

@ -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,

View file

@ -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(),

View file

@ -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>,
}

View file

@ -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;

View file

@ -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,
{

View file

@ -0,0 +1 @@
pub mod tooltip;

View 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;
}

View 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
View 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)
}
}