Compare commits

..

No commits in common. "c60495c66ca643725fba59e3d5c37c6cca02e1ce" and "173d6ebd32bdc76bff066e8ca95de1faf2620082" have entirely different histories.

32 changed files with 419 additions and 773 deletions

View file

@ -176,7 +176,6 @@ zbus = { workspace = true, optional = true }
float-cmp = "0.10.0" float-cmp = "0.10.0"
ron = { workspace = true, optional = true } ron = { workspace = true, optional = true }
enumflags2 = "0.7.12" enumflags2 = "0.7.12"
roxmltree = "0.20"
# Enable DBus feature on Linux targets # Enable DBus feature on Linux targets
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies] [target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies]

@ -1 +1 @@
Subproject commit 343c007f37cd71716e68f01c43ecf2764b7f7c47 Subproject commit 5252095787cc96e2aed64604158f94e450703455

View file

@ -35,7 +35,7 @@ pub enum Message {
PopupClosed(Id), PopupClosed(Id),
ToggleExampleRow(bool), ToggleExampleRow(bool),
Selected(usize), Selected(usize),
Surface(cosmic::surface::Action<Message>), Surface(cosmic::surface::Action),
Toggle(bool), Toggle(bool),
} }
@ -76,7 +76,9 @@ impl cosmic::Application for Window {
self.example_row = toggled; self.example_row = toggled;
} }
Message::Surface(a) => { Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Surface(a)); return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
} }
Message::Selected(i) => { Message::Selected(i) => {
self.selected = Some(i); self.selected = Some(i);

View file

@ -98,7 +98,7 @@ pub enum Message {
Input2(String), Input2(String),
Ignore, Ignore,
ToggleHide, ToggleHide,
Surface(cosmic::surface::Action<Message>), Surface(cosmic::surface::Action),
Hi, Hi,
Hi2, Hi2,
Hi3, Hi3,
@ -255,7 +255,9 @@ impl cosmic::Application for App {
self.hidden = !self.hidden; self.hidden = !self.hidden;
} }
Message::Surface(a) => { Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Surface(a)); return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
} }
Message::Hi => { Message::Hi => {
dbg!("hi"); dbg!("hi");

View file

@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
pub enum Message { pub enum Message {
Clicked, Clicked,
WindowClose, WindowClose,
Surface(cosmic::surface::Action<Message>), Surface(cosmic::surface::Action),
ToggleHideContent, ToggleHideContent,
ToggleSomeAction, ToggleSomeAction,
WindowNew, WindowNew,
@ -89,7 +89,9 @@ impl cosmic::Application for App {
self.button_label = format!("Clicked {message:?}"); self.button_label = format!("Clicked {message:?}");
} }
Message::Surface(action) => { Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Surface(action)); return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
} }
Message::WindowClose Message::WindowClose
| Message::ToggleHideContent | Message::ToggleHideContent

View file

@ -34,7 +34,7 @@ pub enum Message {
OpenError(Arc<file_chooser::Error>), OpenError(Arc<file_chooser::Error>),
OpenFile, OpenFile,
Selected(Url), Selected(Url),
Surface(cosmic::surface::Action<Message>), Surface(cosmic::surface::Action),
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -187,7 +187,9 @@ impl cosmic::Application for App {
self.error_status = None; self.error_status = None;
} }
Message::Surface(action) => { Message::Surface(action) => {
return cosmic::task::message(cosmic::Action::Surface(action)); return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(action),
));
} }
} }

2
iced

@ -1 +1 @@
Subproject commit d12bb8e056712ab87efbdd4296261331772ec413 Subproject commit e891f34b3224b25fc4bdf286b3f9dba26c8abe5b

View file

@ -16,11 +16,6 @@ pub const fn none<M>() -> Action<M> {
Action::None Action::None
} }
/// Wrap a surface action, typically produced by a widget, to be handled by libcosmic.
pub const fn surface<M>(action: crate::surface::Action<M>) -> Action<M> {
Action::Surface(action)
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[must_use] #[must_use]
pub enum Action<M> { pub enum Action<M> {
@ -31,45 +26,10 @@ pub enum Action<M> {
#[cfg(feature = "single-instance")] #[cfg(feature = "single-instance")]
/// Dbus activation messages /// Dbus activation messages
DbusActivation(dbus_activation::Message), DbusActivation(dbus_activation::Message),
/// Surface (popup, subsurface, window, layer shell) requests, handled by libcosmic.
Surface(crate::surface::Action<M>),
/// Do nothing /// Do nothing
None, None,
} }
impl<M: 'static> Action<M> {
/// Map the application message inside, leaving libcosmic's own variants untouched.
#[must_use]
pub fn map<N: 'static>(self, f: impl Fn(M) -> N + Clone + Send + Sync + 'static) -> Action<N> {
match self {
Action::App(message) => Action::App(f(message)),
#[cfg(feature = "winit")]
Action::Cosmic(action) => Action::Cosmic(action),
#[cfg(feature = "single-instance")]
Action::DbusActivation(message) => Action::DbusActivation(message),
Action::Surface(action) => Action::Surface(action.map(f)),
Action::None => Action::None,
}
}
}
impl<M: 'static> Action<Action<M>> {
/// Collapse a doubly wrapped action, as produced by widgets whose message type is already
/// an [`Action`], into a single one.
#[must_use]
pub fn flatten(self) -> Action<M> {
match self {
Action::App(action) => action,
#[cfg(feature = "winit")]
Action::Cosmic(action) => Action::Cosmic(action),
#[cfg(feature = "single-instance")]
Action::DbusActivation(message) => Action::DbusActivation(message),
Action::Surface(action) => Action::Surface(action.flatten()),
Action::None => Action::None,
}
}
}
impl<M> From<M> for Action<M> { impl<M> From<M> for Action<M> {
fn from(value: M) -> Self { fn from(value: M) -> Self {
Self::App(value) Self::App(value)

View file

@ -45,6 +45,8 @@ pub enum Action {
/// Tracks updates to window suggested size. /// Tracks updates to window suggested size.
#[cfg(feature = "applet")] #[cfg(feature = "applet")]
SuggestedBounds(Option<iced::Size>), SuggestedBounds(Option<iced::Size>),
/// Internal surface message
Surface(surface::Action),
/// Notifies that a surface was closed. /// Notifies that a surface was closed.
/// Any data relating to the surface should be cleaned up. /// Any data relating to the surface should be cleaned up.
SurfaceClosed(iced::window::Id), SurfaceClosed(iced::window::Id),

View file

@ -153,7 +153,7 @@ where
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn surface_update( pub fn surface_update(
&mut self, &mut self,
_surface_message: crate::surface::Action<T::Message>, _surface_message: crate::surface::Action,
) -> iced::Task<crate::Action<T::Message>> { ) -> iced::Task<crate::Action<T::Message>> {
#[cfg(feature = "surface-message")] #[cfg(feature = "surface-message")]
match _surface_message { match _surface_message {
@ -194,7 +194,18 @@ where
}; };
let settings = settings(); let settings = settings();
if let Some(view) = view { if let Some(view) = view.and_then(|view| {
match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
>>() {
Ok(v) => Some(v),
Err(err) => {
tracing::error!("Invalid view for subsurface view: {err:?}");
None
}
}
}) {
self.get_subsurface(settings, Some(Box::new(move |_| view()))) self.get_subsurface(settings, Some(Box::new(move |_| view())))
} else { } else {
self.get_subsurface(settings, None) self.get_subsurface(settings, None)
@ -290,7 +301,17 @@ where
let settings = settings(); let settings = settings();
let live_settings = Box::new(move |_: &T| live_settings()); let live_settings = Box::new(move |_: &T| live_settings());
if let Some(view) = view { if let Some(view) = view.and_then(|view| {
match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
>>() {
Ok(v) => Some(v),
Err(err) => {
tracing::error!("Invalid view for subsurface view: {err:?}");
None
}
}
}) {
self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) self.get_popup(settings, live_settings, Some(Box::new(move |_| view())))
} else { } else {
self.get_popup(settings, live_settings, None) self.get_popup(settings, live_settings, None)
@ -356,7 +377,17 @@ where
return Task::none(); return Task::none();
}; };
if let Some(view) = view { if let Some(view) = view.and_then(|view| {
match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
>>() {
Ok(v) => Some(v),
Err(err) => {
tracing::error!("Invalid view for Window: {err:?}");
None
}
}
}) {
let settings = settings(); let settings = settings();
self.get_window( self.get_window(
@ -378,7 +409,9 @@ where
} }
crate::surface::Action::Ignore => iced::Task::none(), crate::surface::Action::Ignore => iced::Task::none(),
crate::surface::Action::Task(f) => f().map(crate::Action::Surface), crate::surface::Action::Task(f) => {
f().map(|sm| crate::Action::Cosmic(Action::Surface(sm)))
}
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
crate::surface::Action::AppLayerShell(settings, live_settings, view) => { crate::surface::Action::AppLayerShell(settings, live_settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
@ -441,7 +474,17 @@ where
let live_settings = live_settings(); let live_settings = live_settings();
let live_settings = Box::new(move |_app: &T| live_settings); let live_settings = Box::new(move |_app: &T| live_settings);
if let Some(view) = view { if let Some(view) = view.and_then(|view| {
match std::sync::Arc::try_unwrap(view).ok()?.downcast::<Box<
dyn Fn() -> Element<'static, crate::Action<T::Message>> + Send + Sync,
>>() {
Ok(v) => Some(v),
Err(err) => {
tracing::error!("Invalid view for layer surface: {err:?}");
None
}
}
}) {
self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view())))
} else { } else {
self.get_layer_shell(settings, live_settings, None) self.get_layer_shell(settings, live_settings, None)
@ -472,7 +515,6 @@ where
let mut task = match message { let mut task = match message {
crate::Action::App(message) => self.app.update(message), crate::Action::App(message) => self.app.update(message),
crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::Cosmic(message) => self.cosmic_update(message),
crate::Action::Surface(action) => self.surface_update(action),
crate::Action::None => iced::Task::none(), crate::Action::None => iced::Task::none(),
#[cfg(feature = "single-instance")] #[cfg(feature = "single-instance")]
crate::Action::DbusActivation(message) => { crate::Action::DbusActivation(message) => {
@ -562,13 +604,9 @@ where
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => {
match event { match event {
wayland::Event::Popup(wayland::PopupEvent::Done, _, popup) => { wayland::Event::Popup(wayland::PopupEvent::Done, _, id)
if popup == id { | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => {
return Some(Action::SurfaceClosed(popup)); return Some(Action::SurfaceClosed(id));
}
}
wayland::Event::Layer(wayland::LayerEvent::Done, _, layer) => {
return Some(Action::SurfaceClosed(layer));
} }
#[cfg(feature = "applet")] #[cfg(feature = "applet")]
wayland::Event::Window( wayland::Event::Window(
@ -1189,6 +1227,8 @@ impl<T: Application> Cosmic<T> {
} }
} }
Action::Surface(action) => return self.surface_update(action),
Action::SurfaceClosed(id) => { Action::SurfaceClosed(id) => {
if self.opened_surfaces.get_mut(&id).is_some_and(|v| { if self.opened_surfaces.get_mut(&id).is_some_and(|v| {
*v = v.saturating_sub(1); *v = v.saturating_sub(1);

View file

@ -394,7 +394,7 @@ where
{ {
nav = nav nav = nav
.window_id_maybe(self.core().main_window_id()) .window_id_maybe(self.core().main_window_id())
.on_surface_action(|action| crate::Action::Surface(action.flatten())) .on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m)))
} }
let mut nav = nav let mut nav = nav
.into_container() .into_container()

View file

@ -295,7 +295,7 @@ impl Context {
content: impl Into<Element<'a, Message>>, content: impl Into<Element<'a, Message>>,
tooltip: impl Into<Cow<'static, str>>, tooltip: impl Into<Cow<'static, str>>,
has_popup: bool, has_popup: bool,
on_surface_action: impl Fn(crate::surface::Action<Message>) -> Message + 'static, on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static,
parent_id: Option<window::Id>, parent_id: Option<window::Id>,
) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> { ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> {
let window_id = *TOOLTIP_WINDOW_ID; let window_id = *TOOLTIP_WINDOW_ID;

View file

@ -1,8 +1,7 @@
// Copyright 2025 System76 <info@system76.com> // Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use super::{Action, View}; use super::Action;
#[cfg(feature = "winit")]
use crate::Application; use crate::Application;
use iced::{Rectangle, window}; use iced::{Rectangle, window};
@ -11,31 +10,30 @@ use iced_runtime::platform_specific::wayland::CornerRadius;
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin;
use std::any::Any; use std::any::Any;
use std::sync::Arc; use std::sync::Arc;
/// Used to produce a destroy popup message from within a widget. /// Used to produce a destroy popup message from within a widget.
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
#[must_use] #[must_use]
pub fn destroy_popup<M>(id: iced_core::window::Id) -> Action<M> { pub fn destroy_popup(id: iced_core::window::Id) -> Action {
Action::DestroyPopup(id) Action::DestroyPopup(id)
} }
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
#[must_use] #[must_use]
pub fn destroy_subsurface<M>(id: iced_core::window::Id) -> Action<M> { pub fn destroy_subsurface(id: iced_core::window::Id) -> Action {
Action::DestroySubsurface(id) Action::DestroySubsurface(id)
} }
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
#[must_use] #[must_use]
pub fn destroy_window<M>(id: iced_core::window::Id) -> Action<M> { pub fn destroy_window(id: iced_core::window::Id) -> Action {
Action::DestroyWindow(id) Action::DestroyWindow(id)
} }
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
#[must_use] #[must_use]
pub fn destroy_layer_shell<M>(id: iced_core::window::Id) -> Action<M> { pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action {
Action::DestroyLayerShell(id) Action::DestroyLayerShell(id)
} }
@ -67,7 +65,7 @@ pub fn app_window<App: Application>(
live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static,
settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static,
view: BoxedView<App>, view: BoxedView<App>,
) -> (window::Id, Action<App::Message>) { ) -> (window::Id, Action) {
let id = window::Id::unique(); let id = window::Id::unique();
let boxed: Box<dyn Fn(&mut App) -> window::Settings + Send + Sync + 'static> = let boxed: Box<dyn Fn(&mut App) -> window::Settings + Send + Sync + 'static> =
@ -101,7 +99,7 @@ pub fn simple_window<Message: 'static>(
view: Option< view: Option<
impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static, impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
>, >,
) -> (window::Id, Action<Message>) { ) -> (window::Id, Action) {
let id = window::Id::unique(); let id = window::Id::unique();
let boxed: Box<dyn Fn() -> window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box<dyn Fn() -> window::Settings + Send + Sync + 'static> = Box::new(settings);
@ -116,7 +114,16 @@ pub fn simple_window<Message: 'static>(
id, id,
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
view.map(|view| Arc::new(view) as View<Message>), view.map(|view| {
let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>>
+ Send
+ Sync
+ 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}),
), ),
) )
} }
@ -130,7 +137,7 @@ pub fn app_popup<App: Application>(
+ Sync + Sync
+ 'static, + 'static,
view: BoxedView<App>, view: BoxedView<App>,
) -> Action<App::Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send + Send
@ -164,7 +171,7 @@ pub fn simple_subsurface<Message: 'static>(
view: Option< view: Option<
Box<dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static>, Box<dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static>,
>, >,
) -> Action<Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings
+ Send + Send
@ -176,7 +183,10 @@ pub fn simple_subsurface<Message: 'static>(
Action::Subsurface( Action::Subsurface(
Arc::new(boxed), Arc::new(boxed),
Arc::new(Box::new(LiveSettings::default)), Arc::new(Box::new(LiveSettings::default)),
view.map(|view| Arc::from(view) as View<Message>), view.map(|view| {
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(view);
Arc::new(boxed)
}),
) )
} }
@ -192,7 +202,7 @@ pub fn simple_popup<Message: 'static>(
view: Option< view: Option<
impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static, impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
>, >,
) -> Action<Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings
+ Send + Send
@ -207,7 +217,13 @@ pub fn simple_popup<Message: 'static>(
Action::Popup( Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
view.map(|view| Arc::new(view) as View<Message>), view.map(|view| {
let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}),
) )
} }
@ -223,7 +239,7 @@ pub fn subsurface<App: Application>(
+ 'static, + 'static,
// XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways.
view: BoxedView<App>, view: BoxedView<App>,
) -> Action<App::Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn( dyn Fn(
&mut App, &mut App,
@ -257,7 +273,7 @@ pub fn simple_layer_shell<Message: 'static>(
view: Option< view: Option<
impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static, impl Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
>, >,
) -> Action<Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn() dyn Fn()
-> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings
@ -271,7 +287,13 @@ pub fn simple_layer_shell<Message: 'static>(
Action::LayerShell( Action::LayerShell(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
view.map(|view| Arc::new(view) as View<Message>), view.map(|view| {
let boxed: Box<
dyn Fn() -> crate::Element<'static, crate::Action<Message>> + Send + Sync + 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}),
) )
} }
@ -288,7 +310,7 @@ pub fn app_layer_shell<App: Application>(
+ 'static, + 'static,
// XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways.
view: BoxedView<App>, view: BoxedView<App>,
) -> Action<App::Message> { ) -> Action {
let boxed: Box< let boxed: Box<
dyn Fn( dyn Fn(
&mut App, &mut App,

View file

@ -11,28 +11,19 @@ use std::sync::Arc;
type BoxedSetting = Arc<Box<dyn Any + Send + Sync + 'static>>; type BoxedSetting = Arc<Box<dyn Any + Send + Sync + 'static>>;
/// Produces the content of a surface created from within a widget.
///
/// Typed on the message the widget publishes.
pub type View<M> =
Arc<dyn Fn() -> crate::Element<'static, crate::Action<M>> + Send + Sync + 'static>;
/// Ignore this message in your application. It will be intercepted. /// Ignore this message in your application. It will be intercepted.
///
/// `M` is the message type of whoever created the action. The ones prefixed with `App` take the
/// application itself and are type-erased, the others carry a [`View`] typed on `M`.
#[derive(Clone)] #[derive(Clone)]
pub enum Action<M> { pub enum Action {
/// Create a subsurface with a view function accepting the App as a parameter /// Create a subsurface with a view function accepting the App as a parameter
AppSubsurface(BoxedSetting, BoxedSetting, Option<BoxedSetting>), AppSubsurface(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Create a subsurface with a view function /// Create a subsurface with a view function
Subsurface(BoxedSetting, BoxedSetting, Option<View<M>>), Subsurface(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroySubsurface(iced::window::Id), DestroySubsurface(iced::window::Id),
/// Create a popup with a view function accepting the App as a parameter /// Create a popup with a view function accepting the App as a parameter
AppPopup(BoxedSetting, BoxedSetting, Option<BoxedSetting>), AppPopup(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Create a popup /// Create a popup
Popup(BoxedSetting, BoxedSetting, Option<View<M>>), Popup(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroyPopup(iced::window::Id), DestroyPopup(iced::window::Id),
/// Destroys the global tooltip popup subsurface /// Destroys the global tooltip popup subsurface
@ -50,7 +41,7 @@ pub enum Action<M> {
iced::window::Id, iced::window::Id,
BoxedSetting, BoxedSetting,
BoxedSetting, BoxedSetting,
Option<View<M>>, Option<BoxedSetting>,
), ),
/// Destroy a window /// Destroy a window
DestroyWindow(iced::window::Id), DestroyWindow(iced::window::Id),
@ -59,7 +50,7 @@ pub enum Action<M> {
AppLayerShell(BoxedSetting, BoxedSetting, Option<BoxedSetting>), AppLayerShell(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Create a layer shell surface with a view function /// Create a layer shell surface with a view function
LayerShell(BoxedSetting, BoxedSetting, Option<View<M>>), LayerShell(BoxedSetting, BoxedSetting, Option<BoxedSetting>),
/// Destroy a layer shell surface /// Destroy a layer shell surface
DestroyLayerShell(iced::window::Id), DestroyLayerShell(iced::window::Id),
@ -75,74 +66,14 @@ pub enum Action<M> {
}, },
Ignore, Ignore,
SyncLiveSettings(iced::window::Id), SyncLiveSettings(iced::window::Id),
Task(Arc<dyn Fn() -> Task<Action<M>> + Send + Sync>), Task(Arc<dyn Fn() -> Task<Action> + Send + Sync>),
} }
impl<M: 'static> Action<M> { pub fn surface_task<M: Send + 'static>(action: Action) -> Task<crate::Action<M>> {
/// Re-type the action for a component whose messages are wrapped by `f`. crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action)))
///
/// Similar to [`iced::Element::map`]. A component that maps a widget's messages must
/// map the widget's surface actions too.
#[must_use]
pub fn map<N: 'static>(self, f: impl Fn(M) -> N + Clone + Send + Sync + 'static) -> Action<N> {
self.map_actions(move |action| action.map(f.clone()))
}
fn map_actions<N: 'static>(
self,
g: impl Fn(crate::Action<M>) -> crate::Action<N> + Clone + Send + Sync + 'static,
) -> Action<N> {
let map_view = |view: Option<View<M>>| -> Option<View<N>> {
let view = view?;
let g = g.clone();
Some(Arc::new(move || view().map(g.clone())))
};
match self {
Action::AppSubsurface(a, b, c) => Action::AppSubsurface(a, b, c),
Action::Subsurface(a, b, view) => Action::Subsurface(a, b, map_view(view)),
Action::DestroySubsurface(id) => Action::DestroySubsurface(id),
Action::AppPopup(a, b, c) => Action::AppPopup(a, b, c),
Action::Popup(a, b, view) => Action::Popup(a, b, map_view(view)),
Action::DestroyPopup(id) => Action::DestroyPopup(id),
Action::DestroyTooltipPopup => Action::DestroyTooltipPopup,
Action::AppWindow(id, a, b, c) => Action::AppWindow(id, a, b, c),
Action::Window(id, a, b, view) => Action::Window(id, a, b, map_view(view)),
Action::DestroyWindow(id) => Action::DestroyWindow(id),
Action::AppLayerShell(a, b, c) => Action::AppLayerShell(a, b, c),
Action::LayerShell(a, b, view) => Action::LayerShell(a, b, map_view(view)),
Action::DestroyLayerShell(id) => Action::DestroyLayerShell(id),
Action::ResponsiveMenuBar {
menu_bar,
limits,
size,
} => Action::ResponsiveMenuBar {
menu_bar,
limits,
size,
},
Action::Ignore => Action::Ignore,
Action::SyncLiveSettings(id) => Action::SyncLiveSettings(id),
Action::Task(task) => Action::Task(Arc::new(move || {
let g = g.clone();
task().map(move |action| action.map_actions(g.clone()))
})),
}
}
} }
impl<M: 'static> Action<crate::Action<M>> { impl std::fmt::Debug for Action {
#[must_use]
pub fn flatten(self) -> Action<M> {
self.map_actions(crate::Action::flatten)
}
}
#[cfg(feature = "winit")]
pub fn surface_task<M: Send + 'static>(action: Action<M>) -> Task<crate::Action<M>> {
crate::task::message(crate::Action::Surface(action))
}
impl<M> std::fmt::Debug for Action<M> {
#[cold] #[cold]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -152,11 +83,11 @@ impl<M> std::fmt::Debug for Action<M> {
.field(arg1) .field(arg1)
.field(arg2) .field(arg2)
.finish(), .finish(),
Self::Subsurface(arg0, arg1, view) => f Self::Subsurface(arg0, arg1, arg2) => f
.debug_tuple("Subsurface") .debug_tuple("Subsurface")
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(&view.as_ref().map(|_| "view")) .field(arg2)
.finish(), .finish(),
Self::DestroySubsurface(arg0) => { Self::DestroySubsurface(arg0) => {
f.debug_tuple("DestroySubsurface").field(arg0).finish() f.debug_tuple("DestroySubsurface").field(arg0).finish()
@ -167,11 +98,11 @@ impl<M> std::fmt::Debug for Action<M> {
.field(arg1) .field(arg1)
.field(arg2) .field(arg2)
.finish(), .finish(),
Self::Popup(arg0, arg1, view) => f Self::Popup(arg0, arg1, arg2) => f
.debug_tuple("Popup") .debug_tuple("Popup")
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(&view.as_ref().map(|_| "view")) .field(arg2)
.finish(), .finish(),
Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(),
Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(),
@ -193,12 +124,12 @@ impl<M> std::fmt::Debug for Action<M> {
.field(arg1) .field(arg1)
.field(arg2) .field(arg2)
.finish(), .finish(),
Self::Window(id, arg0, arg1, view) => f Self::Window(id, arg0, arg1, arg2) => f
.debug_tuple("Window") .debug_tuple("Window")
.field(id) .field(id)
.field(arg0) .field(arg0)
.field(arg1) .field(arg1)
.field(&view.as_ref().map(|_| "view")) .field(arg2)
.finish(), .finish(),
Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(),
Self::Task(_) => f.debug_tuple("Future").finish(), Self::Task(_) => f.debug_tuple("Future").finish(),
@ -208,11 +139,11 @@ impl<M> std::fmt::Debug for Action<M> {
.field(arg1) .field(arg1)
.field(arg2) .field(arg2)
.finish(), .finish(),
Self::LayerShell(arg0, arg1, view) => f Self::LayerShell(arg, arg1, arg2) => f
.debug_tuple("LayerShell") .debug_tuple("LayerShell")
.field(arg0) .field(arg)
.field(arg1) .field(arg1)
.field(&view.as_ref().map(|_| "view")) .field(arg2)
.finish(), .finish(),
Self::DestroyLayerShell(arg0) => { Self::DestroyLayerShell(arg0) => {
f.debug_tuple("DestroyLayerShell").field(arg0).finish() f.debug_tuple("DestroyLayerShell").field(arg0).finish()

View file

@ -25,7 +25,6 @@ pub enum Button {
IconVertical, IconVertical,
Image, Image,
Link, Link,
LinkActive,
ListItem([f32; 4]), ListItem([f32; 4]),
MenuFolder, MenuFolder,
MenuItem, MenuItem,
@ -122,13 +121,6 @@ pub fn appearance(
corner_radii = &cosmic.corner_radii.radius_0; corner_radii = &cosmic.corner_radii.radius_0;
} }
Button::LinkActive => {
appearance.background = Some(Background::Color(cosmic.text_button.hover.into()));
appearance.icon_color = Some(cosmic.accent_text_color().into());
appearance.text_color = Some(cosmic.accent_text_color().into());
corner_radii = &cosmic.corner_radii.radius_xs;
}
Button::Custom { .. } => (), Button::Custom { .. } => (),
Button::AppletMenu => { Button::AppletMenu => {
let (background, _, _) = color(&cosmic.text_button); let (background, _, _) = color(&cosmic.text_button);

View file

@ -33,9 +33,6 @@ pub fn context_menu<'a, Message: 'static + Clone>(
}), }),
close_on_escape: true, close_on_escape: true,
window_id: window::Id::RESERVED, window_id: window::Id::RESERVED,
item_width: ItemWidth::Uniform(240),
on_open: None,
on_close: None,
on_surface_action: None, on_surface_action: None,
}; };
@ -56,33 +53,12 @@ pub struct ContextMenu<'a, Message> {
context_menu: Option<Vec<menu::Tree<Message>>>, context_menu: Option<Vec<menu::Tree<Message>>>,
pub window_id: window::Id, pub window_id: window::Id,
pub close_on_escape: bool, pub close_on_escape: bool,
/// Width of each menu item, and therefore of the menu.
pub item_width: ItemWidth,
/// Emitted when the menu opens, so the application can mark what was right-clicked.
#[setters(strip_option)]
pub on_open: Option<Message>,
/// Emitted when the menu closes by any path, including the compositor dismissing it.
#[setters(strip_option)]
pub on_close: Option<Message>,
#[setters(skip)] #[setters(skip)]
pub(crate) on_surface_action: pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>, Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
} }
impl<Message: Clone + 'static> ContextMenu<'_, Message> { impl<Message: Clone + 'static> ContextMenu<'_, Message> {
/// Publish `on_open`/`on_close` when the open state changed since the last report.
fn report_open_state(&self, state: &mut LocalState, shell: &mut iced_core::Shell<'_, Message>) {
let open = state.menu_bar_state.inner.with_data(|d| d.open);
if open == state.reported_open {
return;
}
state.reported_open = open;
let message = if open { &self.on_open } else { &self.on_close };
if let Some(message) = message.clone() {
shell.publish(message);
}
}
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn create_popup( fn create_popup(
@ -115,12 +91,16 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id)));
state.view_cursor = view_cursor; state.view_cursor = view_cursor;
(
id,
layout.children().map(|lo| lo.bounds()).collect::<Vec<_>>(),
)
} else {
(
window::Id::unique(),
layout.children().map(|lo| lo.bounds()).collect(),
)
} }
// A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's
(
window::Id::unique(),
layout.children().map(|lo| lo.bounds()).collect::<Vec<_>>(),
)
}); });
let Some(context_menu) = self.context_menu.as_mut() else { let Some(context_menu) = self.context_menu.as_mut() else {
return; return;
@ -136,7 +116,7 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
click_outside: true, click_outside: true,
click_inside: true, click_inside: true,
}, },
item_width: self.item_width, item_width: ItemWidth::Uniform(240),
item_height: ItemHeight::Dynamic(40), item_height: ItemHeight::Dynamic(40),
bar_bounds: bounds, bar_bounds: bounds,
main_offset: -(bounds.height as i32), main_offset: -(bounds.height as i32),
@ -235,7 +215,7 @@ impl<Message: Clone + 'static> ContextMenu<'_, Message> {
pub fn on_surface_action( pub fn on_surface_action(
mut self, mut self,
handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static, handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
) -> Self { ) -> Self {
self.on_surface_action = Some(Arc::new(handler)); self.on_surface_action = Some(Arc::new(handler));
self self
@ -255,7 +235,6 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
context_cursor: Point::default(), context_cursor: Point::default(),
fingers_pressed: Default::default(), fingers_pressed: Default::default(),
menu_bar_state: Default::default(), menu_bar_state: Default::default(),
reported_open: false,
}) })
} }
@ -290,11 +269,9 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.content)); tree.diff_children(std::slice::from_mut(&mut self.content));
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
if let Some(context_menu) = self.context_menu.as_mut() { state.menu_bar_state.inner.with_data_mut(|inner| {
state.menu_bar_state.inner.with_data_mut(|inner| { menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree);
menu_roots_diff(context_menu, &mut inner.tree); });
});
}
// if let Some(ref mut context_menus) = self.context_menu { // if let Some(ref mut context_menus) = self.context_menu {
// for (menu, tree) in context_menus // for (menu, tree) in context_menus
@ -342,38 +319,6 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
); );
} }
fn mouse_interaction(
&self,
tree: &Tree,
layout: iced_core::Layout<'_>,
cursor: iced_core::mouse::Cursor,
viewport: &iced::Rectangle,
renderer: &crate::Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
&tree.children[0],
layout,
cursor,
viewport,
renderer,
)
}
fn drag_destinations(
&self,
tree: &Tree,
layout: iced_core::Layout<'_>,
renderer: &crate::Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
self.content.as_widget().drag_destinations(
&tree.children[0],
layout,
renderer,
dnd_rectangles,
);
}
fn operate( fn operate(
&mut self, &mut self,
tree: &mut Tree, tree: &mut Tree,
@ -401,20 +346,6 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
let bounds = layout.bounds(); let bounds = layout.bounds();
// The compositor dismissed our popup: nothing else tells this state about it.
#[cfg(wayland_platform)]
if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
)) = event
{
state.menu_bar_state.inner.with_data_mut(|d| {
if d.popup_id.get(&self.window_id) == Some(popup) {
d.popup_id.remove(&self.window_id);
d.reset();
}
});
}
// XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open.
let reset = self.window_id != window::Id::NONE let reset = self.window_id != window::Id::NONE
&& state && state
@ -494,9 +425,7 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
self.create_popup(layout, cursor, renderer, shell, viewport, state); self.create_popup(layout, cursor, renderer, shell, viewport, state);
} }
shell.request_redraw();
shell.capture_event(); shell.capture_event();
self.report_open_state(tree.state.downcast_mut::<LocalState>(), shell);
return; return;
} else if !was_open && right_button_released(event) } else if !was_open && right_button_released(event)
|| (touch_lifted(event)) || (touch_lifted(event))
@ -532,76 +461,62 @@ impl<Message: 'static + Clone> Widget<Message, crate::Theme, crate::Renderer>
shell, shell,
viewport, viewport,
); );
self.report_open_state(tree.state.downcast_mut::<LocalState>(), shell);
} }
fn overlay<'b>( fn overlay<'b>(
&'b mut self, &'b mut self,
tree: &'b mut Tree, tree: &'b mut Tree,
layout: iced_core::Layout<'b>, layout: iced_core::Layout<'_>,
renderer: &crate::Renderer, _renderer: &crate::Renderer,
viewport: &iced::Rectangle, _viewport: &iced::Rectangle,
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
// The wrapped content's overlays (tooltips, dropdowns, ...) always pass through
let content = self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout,
renderer,
viewport,
translation,
);
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland))
&& self.window_id != window::Id::NONE && self.window_id != window::Id::NONE
&& self.on_surface_action.is_some() && self.on_surface_action.is_some()
{ {
return content; return None;
} }
let state = tree.state.downcast_ref::<LocalState>(); let state = tree.state.downcast_ref::<LocalState>();
let Some(context_menu) = self.context_menu.as_mut() else {
return content; let context_menu = self.context_menu.as_mut()?;
};
if !state.menu_bar_state.inner.with_data(|state| state.open) { if !state.menu_bar_state.inner.with_data(|state| state.open) {
return content; return None;
} }
// Anchor the menu to a 1x1 rectangle at the click, like the popup path does let mut bounds = layout.bounds();
let bounds = iced::Rectangle::new(state.context_cursor, Size::new(1.0, 1.0)); bounds.x = state.context_cursor.x;
let menu = crate::widget::menu::Menu { bounds.y = state.context_cursor.y;
tree: state.menu_bar_state.clone(), Some(
menu_roots: std::borrow::Cow::Owned(context_menu.clone()), crate::widget::menu::Menu {
bounds_expand: 16, tree: state.menu_bar_state.clone(),
menu_overlays_parent: true, menu_roots: std::borrow::Cow::Owned(context_menu.clone()),
close_condition: CloseCondition { bounds_expand: 16,
leave: false, menu_overlays_parent: true,
click_outside: true, close_condition: CloseCondition {
click_inside: true, leave: false,
}, click_outside: true,
item_width: self.item_width, click_inside: true,
item_height: ItemHeight::Dynamic(40), },
bar_bounds: bounds, item_width: ItemWidth::Uniform(240),
main_offset: 0, item_height: ItemHeight::Dynamic(40),
cross_offset: 0, bar_bounds: bounds,
root_bounds_list: vec![bounds], main_offset: -(bounds.height as i32),
path_highlight: Some(PathHighlight::MenuActive), cross_offset: 0,
style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), root_bounds_list: vec![bounds],
position: Point::new(translation.x, translation.y), path_highlight: Some(PathHighlight::MenuActive),
is_overlay: true, style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default),
window_id: window::Id::NONE, position: Point::new(translation.x, translation.y),
depth: 0, is_overlay: true,
on_surface_action: None, window_id: window::Id::NONE,
} depth: 0,
.overlay(); on_surface_action: None,
Some(match content {
Some(content) => {
iced_core::overlay::Group::with_children(vec![content, menu]).overlay()
} }
None => menu, .overlay(),
}) )
} }
#[cfg(feature = "a11y")] #[cfg(feature = "a11y")]
@ -645,5 +560,4 @@ pub struct LocalState {
context_cursor: Point, context_cursor: Point,
fingers_pressed: HashSet<Finger>, fingers_pressed: HashSet<Finger>,
menu_bar_state: MenuBarState, menu_bar_state: MenuBarState,
reported_open: bool,
} }

View file

@ -44,7 +44,7 @@ pub fn popup_dropdown<
selected: Option<usize>, selected: Option<usize>,
on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, on_selected: impl Fn(usize) -> Message + Send + Sync + 'static,
_parent_id: window::Id, _parent_id: window::Id,
_on_surface_action: impl Fn(surface::Action<AppMessage>) -> Message + Send + Sync + 'static, _on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static,
_map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static, _map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static,
) -> Dropdown<'a, S, Message, AppMessage> { ) -> Dropdown<'a, S, Message, AppMessage> {
let dropdown: Dropdown<'_, S, Message, AppMessage> = let dropdown: Dropdown<'_, S, Message, AppMessage> =

View file

@ -54,8 +54,7 @@ where
#[setters(strip_option)] #[setters(strip_option)]
font: Option<crate::font::Font>, font: Option<crate::font::Font>,
#[setters(skip)] #[setters(skip)]
on_surface_action: on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
Option<Arc<dyn Fn(surface::Action<AppMessage>) -> Message + Send + Sync + 'static>>,
#[setters(skip)] #[setters(skip)]
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>, action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>,
#[setters(strip_option)] #[setters(strip_option)]
@ -109,7 +108,7 @@ where
pub fn with_popup<NewAppMessage>( pub fn with_popup<NewAppMessage>(
self, self,
parent_id: window::Id, parent_id: window::Id,
on_surface_action: impl Fn(surface::Action<NewAppMessage>) -> Message + Send + Sync + 'static, on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static,
action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static, action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static,
) -> Dropdown<'a, S, Message, NewAppMessage> { ) -> Dropdown<'a, S, Message, NewAppMessage> {
let Self { let Self {
@ -552,9 +551,7 @@ pub fn update<
selections: &[S], selections: &[S],
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
_window_id: Option<window::Id>, _window_id: Option<window::Id>,
on_surface_action: Option< on_surface_action: Option<Arc<dyn Fn(surface::Action) -> Message + Send + Sync + 'static>>,
Arc<dyn Fn(surface::Action<AppMessage>) -> Message + Send + Sync + 'static>,
>,
action_map: Option<Arc<dyn Fn(Message) -> AppMessage + Send + Sync + 'static>>, action_map: Option<Arc<dyn Fn(Message) -> AppMessage + Send + Sync + 'static>>,
icons: &[icon::Handle], icons: &[icon::Handle],
gap: f32, gap: f32,

View file

@ -5,10 +5,8 @@ use super::Icon;
use crate::widget::{image, svg}; use crate::widget::{image, svg};
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs::File;
use std::hash::Hash; use std::hash::Hash;
use std::io::Read; use std::path::PathBuf;
use std::path::{Path, PathBuf};
#[must_use] #[must_use]
#[derive(Clone, Debug, Hash, derive_setters::Setters)] #[derive(Clone, Debug, Hash, derive_setters::Setters)]
@ -33,52 +31,6 @@ pub enum Data {
Svg(svg::Handle), Svg(svg::Handle),
} }
enum SvgSource {
Path,
Bytes(Vec<u8>),
}
fn svg_source(path: &Path) -> Option<SvgSource> {
if path
.extension()
.and_then(OsStr::to_str)
.is_some_and(|extension| extension.eq_ignore_ascii_case("svg"))
{
return Some(SvgSource::Path);
}
let Ok(mut file) = File::open(path) else {
return None;
};
let Ok(metadata) = file.metadata() else {
return None;
};
const MAX_SVG_SIZE: u64 = 16 * 1024 * 1024;
if !metadata.file_type().is_file() || metadata.len() > MAX_SVG_SIZE {
return None;
}
let mut prefix = [0; 32];
let Ok(length) = file.read(&mut prefix) else {
return None;
};
let prefix = &prefix[..length];
if ::image::guess_format(prefix).is_ok() {
return None;
}
let mut bytes = Vec::with_capacity(metadata.len() as usize);
bytes.extend_from_slice(prefix);
if file.read_to_end(&mut bytes).is_err() {
return None;
}
let document = roxmltree::Document::parse(std::str::from_utf8(&bytes).ok()?).ok()?;
(document.root_element().tag_name().name() == "svg").then_some(SvgSource::Bytes(bytes))
}
/// Create an icon handle from its path. /// Create an icon handle from its path.
pub fn from_path(path: PathBuf) -> Handle { pub fn from_path(path: PathBuf) -> Handle {
Handle { Handle {
@ -86,10 +38,10 @@ pub fn from_path(path: PathBuf) -> Handle {
.file_stem() .file_stem()
.and_then(OsStr::to_str) .and_then(OsStr::to_str)
.is_some_and(|name| name.ends_with("-symbolic")), .is_some_and(|name| name.ends_with("-symbolic")),
data: match svg_source(&path) { data: if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
Some(SvgSource::Path) => Data::Svg(svg::Handle::from_path(path)), Data::Svg(svg::Handle::from_path(path))
Some(SvgSource::Bytes(bytes)) => Data::Svg(svg::Handle::from_memory(bytes)), } else {
None => Data::Image(image::Handle::from_path(path)), Data::Image(image::Handle::from_path(path))
}, },
} }
} }

View file

@ -3,6 +3,7 @@
use super::{Handle, Icon}; use super::{Handle, Icon};
use std::borrow::Cow; use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -137,7 +138,11 @@ impl Named {
Handle { Handle {
symbolic: self.symbolic, symbolic: self.symbolic,
data: if let Some(path) = self.path() { data: if let Some(path) = self.path() {
super::from_path(path).data if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
super::Data::Svg(iced_core::svg::Handle::from_path(path))
} else {
super::Data::Image(iced_core::image::Handle::from_path(path))
}
} else { } else {
super::bundle::get(&name).unwrap_or_else(|| { super::bundle::get(&name).unwrap_or_else(|| {
let bytes: &'static [u8] = &[]; let bytes: &'static [u8] = &[];

View file

@ -71,8 +71,8 @@ pub mod menu_column;
mod menu_inner; mod menu_inner;
mod menu_tree; mod menu_tree;
pub use menu_tree::{ pub use menu_tree::{
Entry, IconSlot, MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root,
menu_root as root, nav_context, nav_context,
}; };
pub use crate::style::menu_bar::{Appearance, StyleSheet}; pub use crate::style::menu_bar::{Appearance, StyleSheet};

View file

@ -176,7 +176,7 @@ pub struct MenuBar<Message> {
#[cfg(wayland_platform)] #[cfg(wayland_platform)]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
pub(crate) on_surface_action: pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>, Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
} }
impl<Message> MenuBar<Message> impl<Message> MenuBar<Message>
@ -328,7 +328,7 @@ where
#[must_use] #[must_use]
pub fn on_surface_action( pub fn on_surface_action(
mut self, mut self,
handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static, handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
) -> Self { ) -> Self {
self.on_surface_action = Some(Arc::new(handler)); self.on_surface_action = Some(Arc::new(handler));
self self
@ -377,12 +377,13 @@ where
state.active_root.clear(); state.active_root.clear();
shell.publish(surface_action(destroy_popup(id))); shell.publish(surface_action(destroy_popup(id)));
state.view_cursor = view_cursor; state.view_cursor = view_cursor;
(id, layout.children().map(|lo| lo.bounds()).collect())
} else {
(
window::Id::unique(),
layout.children().map(|lo| lo.bounds()).collect(),
)
} }
// A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's
(
window::Id::unique(),
layout.children().map(|lo| lo.bounds()).collect(),
)
}); });
let mut popup_menu: Menu<'static, _> = Menu { let mut popup_menu: Menu<'static, _> = Menu {
@ -580,21 +581,6 @@ where
let my_state = tree.state.downcast_mut::<MenuBarState>(); let my_state = tree.state.downcast_mut::<MenuBarState>();
// The compositor dismissed our popup: nothing else tells this state about it.
#[cfg(wayland_platform)]
if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
)) = event
{
my_state.inner.with_data_mut(|d| {
if d.popup_id.get(&self.window_id) == Some(popup) {
// submenus were dismissed with it
d.popup_id.clear();
d.reset();
}
});
}
// XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open.
let reset = self.window_id != window::Id::NONE let reset = self.window_id != window::Id::NONE
&& my_state && my_state

View file

@ -455,7 +455,7 @@ pub(crate) struct Menu<'b, Message: std::clone::Clone> {
pub(crate) window_id: window::Id, pub(crate) window_id: window::Id,
pub(crate) depth: usize, pub(crate) depth: usize,
pub(crate) on_surface_action: pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>, Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
} }
impl<'b, Message: Clone + 'static> Menu<'b, Message> { impl<'b, Message: Clone + 'static> Menu<'b, Message> {
pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> {

View file

@ -180,84 +180,6 @@ pub enum MenuItem<A: MenuAction, L: Into<Cow<'static, str>>> {
Folder(L, Vec<MenuItem<A, L>>), Folder(L, Vec<MenuItem<A, L>>),
/// Represents a divider between menu items. /// Represents a divider between menu items.
Divider, Divider,
/// A menu entry with every option available; see [`Entry`].
Entry(Entry<A, L>),
}
impl<A: MenuAction, L: Into<Cow<'static, str>>> MenuItem<A, L> {
/// Create an [`Entry`] menu item, configure it with the builder methods on [`Entry`].
pub fn entry(label: L, action: A) -> Self {
MenuItem::Entry(Entry::new(label, action))
}
}
/// The leading icon column of a menu entry.
#[derive(Clone, Debug, Default)]
pub enum IconSlot {
/// No icon and no space reserved for one.
#[default]
None,
/// No icon, but the space for an icon is reserved (indented entry)
Reserved,
/// An icon.
Icon(icon::Handle),
}
impl From<Option<icon::Handle>> for IconSlot {
fn from(icon: Option<icon::Handle>) -> Self {
icon.map_or(IconSlot::None, IconSlot::Icon)
}
}
/// A menu entry: label, optional leading icon, optional check column, enabled state and action.
#[derive(Clone)]
pub struct Entry<A, L> {
label: L,
icon: IconSlot,
/// `Some` draws the check column
checked: Option<bool>,
enabled: bool,
action: A,
}
impl<A, L> Entry<A, L> {
pub fn new(label: L, action: A) -> Self {
Self {
label,
icon: IconSlot::None,
checked: None,
enabled: true,
action,
}
}
/// Draw a leading icon
#[must_use]
pub fn icon(mut self, icon: icon::Handle) -> Self {
self.icon = IconSlot::Icon(icon);
self
}
/// Draw no icon, but resever the space
#[must_use]
pub fn reserve_icon(mut self) -> Self {
self.icon = IconSlot::Reserved;
self
}
/// Show a check column, ticked when `checked` is true, empty sapce when false
#[must_use]
pub fn checked(mut self, checked: bool) -> Self {
self.checked = Some(checked);
self
}
/// Disabled entries are drawn dimmed and do not react to presses
#[must_use]
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
} }
/// Create a root menu item. /// Create a root menu item.
@ -279,84 +201,6 @@ where
.class(theme::Button::MenuRoot) .class(theme::Button::MenuRoot)
} }
fn entry_tree<
A: MenuAction<Message = Message>,
L: Into<Cow<'static, str>>,
Message: Clone + 'static,
>(
entry: Entry<A, L>,
key_binds: &HashMap<KeyBind, A>,
key_class: theme::Text,
) -> MenuTree<Message> {
let Entry {
label,
icon,
checked,
enabled,
action,
} = entry;
let spacing = crate::theme::spacing();
let key = key_binds
.iter()
.find(|(_, a)| **a == action)
.map_or_else(String::new, |(k, _)| k.to_string());
let mut items: Vec<crate::Element<'static, Message>> = Vec::with_capacity(7);
if let Some(checked) = checked {
items.push(if checked {
widget::icon::from_name("object-select-symbolic")
.size(16)
.icon()
.class(theme::Svg::Custom(Rc::new(|theme| {
iced_widget::svg::Style {
color: Some(theme.cosmic().accent_text_color().into()),
}
})))
.width(Length::Fixed(16.0))
.into()
} else {
widget::space::horizontal()
.width(Length::Fixed(16.0))
.into()
});
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
}
match icon {
IconSlot::Icon(icon) => {
items.push(widget::icon::icon(icon).size(14).into());
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
}
IconSlot::Reserved => {
items.push(
widget::space::horizontal()
.width(Length::Fixed(14.0))
.into(),
);
items.push(widget::space::horizontal().width(spacing.space_xxs).into());
}
IconSlot::None => {}
}
let ellipsize =
iced_core::text::Ellipsize::Middle(iced_core::text::EllipsizeHeightLimit::Lines(1));
items.push(widget::text(label.into()).ellipsize(ellipsize).into());
items.push(widget::space::horizontal().into());
items.push(
widget::text(key)
.class(key_class)
.ellipsize(ellipsize)
.into(),
);
let mut button = menu_button(items);
if enabled {
button = button.on_press(action.message());
}
MenuTree::from(Element::from(button))
}
/// Create a list of menu items from a vector of `MenuItem`. /// Create a list of menu items from a vector of `MenuItem`.
/// ///
/// The `MenuItem` can be either an action or a separator. /// The `MenuItem` can be either an action or a separator.
@ -376,6 +220,15 @@ pub fn menu_items<
key_binds: &HashMap<KeyBind, A>, key_binds: &HashMap<KeyBind, A>,
children: Vec<MenuItem<A, L>>, children: Vec<MenuItem<A, L>>,
) -> Vec<MenuTree<Message>> { ) -> Vec<MenuTree<Message>> {
fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
for (key_bind, key_action) in key_binds {
if action == key_action {
return key_bind.to_string();
}
}
String::new()
}
fn key_style(theme: &crate::Theme) -> TextStyle { fn key_style(theme: &crate::Theme) -> TextStyle {
let mut color = theme.cosmic().background(theme.transparent).component.on; let mut color = theme.cosmic().background(theme.transparent).component.on;
color.alpha *= 0.75; color.alpha *= 0.75;
@ -393,25 +246,117 @@ pub fn menu_items<
.enumerate() .enumerate()
.flat_map(|(i, item)| { .flat_map(|(i, item)| {
let mut trees = vec![]; let mut trees = vec![];
let spacing = crate::theme::spacing();
match item { match item {
MenuItem::Button(label, icon, action) => { MenuItem::Button(label, icon, action) => {
let mut entry = Entry::new(label, action); let l: Cow<'static, str> = label.into();
entry.icon = icon.into(); let key = find_key(&action, key_binds);
trees.push(entry_tree(entry, key_binds, key_class.clone())); let mut items = vec![
widget::text(l)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(),
widget::text(key)
.class(key_class)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
];
if let Some(icon) = icon {
items.insert(0, widget::icon::icon(icon).size(14).into());
items.insert(
1,
widget::space::horizontal().width(spacing.space_xxs).into(),
);
}
let menu_button = menu_button(items).on_press(action.message());
trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
} }
MenuItem::ButtonDisabled(label, icon, action) => { MenuItem::ButtonDisabled(label, icon, action) => {
let mut entry = Entry::new(label, action).enabled(false); let l: Cow<'static, str> = label.into();
entry.icon = icon.into();
trees.push(entry_tree(entry, key_binds, key_class.clone())); let key = find_key(&action, key_binds);
let mut items = vec![
widget::text(l)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
widget::space::horizontal().into(),
widget::text(key)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.class(key_class)
.into(),
];
if let Some(icon) = icon {
items.insert(0, widget::icon::icon(icon).size(14).into());
items.insert(
1,
widget::space::horizontal().width(spacing.space_xxs).into(),
);
}
let menu_button = menu_button(items);
trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
} }
MenuItem::CheckBox(label, icon, value, action) => { MenuItem::CheckBox(label, icon, value, action) => {
let mut entry = Entry::new(label, action).checked(value); let key = find_key(&action, key_binds);
entry.icon = icon.into(); let mut items = vec![
trees.push(entry_tree(entry, key_binds, key_class.clone())); if value {
} widget::icon::from_name("object-select-symbolic")
MenuItem::Entry(entry) => { .size(16)
trees.push(entry_tree(entry, key_binds, key_class.clone())); .icon()
.class(theme::Svg::Custom(Rc::new(|theme| {
iced_widget::svg::Style {
color: Some(theme.cosmic().accent_text_color().into()),
}
})))
.width(Length::Fixed(16.0))
.into()
} else {
widget::space::horizontal()
.width(Length::Fixed(16.0))
.into()
},
widget::space::horizontal().width(spacing.space_xxs).into(),
widget::text(label)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.align_x(iced::Alignment::Start)
.into(),
widget::space::horizontal().into(),
widget::text(key)
.class(key_class)
.ellipsize(iced_core::text::Ellipsize::Middle(
iced_core::text::EllipsizeHeightLimit::Lines(1),
))
.into(),
];
if let Some(icon) = icon {
items.insert(
1,
widget::space::horizontal().width(spacing.space_xxs).into(),
);
items.insert(2, widget::icon::icon(icon).size(14).into());
}
trees.push(MenuTree::from(Element::from(
menu_button(items).on_press(action.message()),
)));
} }
MenuItem::Folder(label, children) => { MenuItem::Folder(label, children) => {
let l: Cow<'static, str> = label.into(); let l: Cow<'static, str> = label.into();

View file

@ -159,7 +159,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> {
#[must_use] #[must_use]
pub fn on_surface_action( pub fn on_surface_action(
mut self, mut self,
handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static, handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
) -> Self { ) -> Self {
self.segmented_button = self.segmented_button.on_surface_action(handler); self.segmented_button = self.segmented_button.on_surface_action(handler);
self self

View file

@ -157,17 +157,9 @@ where
) { ) {
if self.popup.is_some() { if self.popup.is_some() {
if self.modal { if self.modal {
match event { if matches!(event, Event::Mouse(_) | Event::Touch(_)) {
Event::Mouse(_) | Event::Touch(_) => { shell.capture_event();
shell.capture_event(); return;
return;
}
// app-level keyboard navigation (Tab focus cycling, Escape)
// still receives keyboard events
Event::Keyboard(_) => {
return;
}
_ => {}
} }
} else if let Some(on_close) = self.on_close.as_ref() } else if let Some(on_close) = self.on_close.as_ref()
&& matches!( && matches!(

View file

@ -11,7 +11,7 @@ use iced_core::{
pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>(
content: E, content: E,
id: Id, id: Id,
on_action: impl Fn(crate::surface::Action<Message>) -> Message + 'static, on_action: impl Fn(crate::surface::Action) -> Message + 'static,
) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer> ) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer>
where where
E: Into<Element<'a, Message, Theme, crate::Renderer>>, E: Into<Element<'a, Message, Theme, crate::Renderer>>,
@ -32,7 +32,7 @@ where
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, Theme, Renderer>,
id: Id, id: Id,
size: Option<Size>, size: Option<Size>,
on_action: Box<dyn Fn(crate::surface::Action<Message>) -> Message>, on_action: Box<dyn Fn(crate::surface::Action) -> Message>,
} }
impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer>
@ -43,7 +43,7 @@ where
pub(crate) fn new<T>( pub(crate) fn new<T>(
content: T, content: T,
id: Id, id: Id,
on_action: impl Fn(crate::surface::Action<Message>) -> Message + 'static, on_action: impl Fn(crate::surface::Action) -> Message + 'static,
) -> Self ) -> Self
where where
T: Into<Element<'a, Message, Theme, Renderer>>, T: Into<Element<'a, Message, Theme, Renderer>>,

View file

@ -79,11 +79,7 @@ impl ResponsiveMenuBar {
core: &Core, core: &Core,
key_binds: &HashMap<menu::KeyBind, A>, key_binds: &HashMap<menu::KeyBind, A>,
id: crate::widget::Id, id: crate::widget::Id,
action_message: impl Fn(crate::surface::Action<Message>) -> Message action_message: impl Fn(crate::surface::Action) -> Message + Send + Sync + Clone + 'static,
+ Send
+ Sync
+ Clone
+ 'static,
trees: Vec<(S, Vec<menu::Item<A, S>>)>, trees: Vec<(S, Vec<menu::Item<A, S>>)>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
use crate::widget::id_container; use crate::widget::id_container;

View file

@ -203,7 +203,7 @@ where
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
#[setters(skip)] #[setters(skip)]
pub(crate) on_surface_action: pub(crate) on_surface_action:
Option<Arc<dyn Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static>>, Option<Arc<dyn Fn(crate::surface::Action) -> Message + Send + Sync + 'static>>,
/// Defines the implementation of this struct /// Defines the implementation of this struct
variant: PhantomData<Variant>, variant: PhantomData<Variant>,
@ -914,7 +914,7 @@ where
#[must_use] #[must_use]
pub fn on_surface_action( pub fn on_surface_action(
mut self, mut self,
handler: impl Fn(crate::surface::Action<Message>) -> Message + Send + Sync + 'static, handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static,
) -> Self { ) -> Self {
self.on_surface_action = Some(Arc::new(handler)); self.on_surface_action = Some(Arc::new(handler));
self self
@ -953,9 +953,10 @@ where
state.active_root.clear(); state.active_root.clear();
shell.publish(surface_action(destroy_popup(id))); shell.publish(surface_action(destroy_popup(id)));
state.view_cursor = view_cursor; state.view_cursor = view_cursor;
id
} else {
window::Id::unique()
} }
// A fresh id per popup, so the old popup's Done cannot be mistaken for the new one's
window::Id::unique()
}); });
let Some(entity) = state.show_context else { let Some(entity) = state.show_context else {
return; return;
@ -1087,7 +1088,7 @@ where
+ Sync + Sync
+ 'static, + 'static,
view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>, view: Option<impl Fn() -> crate::Element<'static, Message> + Send + Sync + 'static>,
) -> crate::surface::Action<Message> { ) -> crate::surface::Action {
use std::any::Any; use std::any::Any;
let boxed: Box< let boxed: Box<
@ -1106,8 +1107,11 @@ where
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
view.map(|view| { view.map(|view| {
Arc::new(move || view().map(crate::Action::App)) let boxed: Box<
as crate::surface::View<Message> dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static,
> = Box::new(view);
let boxed: Box<dyn Any + Send + Sync + 'static> = Box::new(boxed);
Arc::new(boxed)
}), }),
) )
} }
@ -1256,29 +1260,6 @@ where
let my_bounds = layout.bounds(); let my_bounds = layout.bounds();
let state = tree.state.downcast_mut::<LocalState>(); let state = tree.state.downcast_mut::<LocalState>();
// The compositor dismissed our context menu popup: nothing else tells this state about it.
#[cfg(wayland_platform)]
if let iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(
iced::event::wayland::Event::Popup(iced::event::wayland::PopupEvent::Done, _, popup),
)) = &event
{
let dismissed = state.menu_state.inner.with_data_mut(|data| {
if data.popup_id.get(&self.window_id) == Some(popup) {
data.popup_id.clear();
data.reset();
true
} else {
false
}
});
if dismissed {
state.show_context = None;
for key in self.model.order.iter().copied() {
self.update_entity_paragraph(state, key);
}
}
}
let hovered_before = state.hovered; let hovered_before = state.hovered;
let my_id = self.get_drag_id(); let my_id = self.get_drag_id();
@ -2259,7 +2240,12 @@ where
let menu_open = || { let menu_open = || {
state.show_context == Some(key) state.show_context == Some(key)
&& state.menu_state.inner.with_data(|data| data.open) && !tree.children.is_empty()
&& tree.children[0]
.state
.downcast_ref::<MenuBarState>()
.inner
.with_data(|data| data.open)
}; };
let key_is_active = self.model.is_active(key); let key_is_active = self.model.is_active(key);

View file

@ -7,7 +7,6 @@
//! A [`TextInput`] has some local [`State`]. //! A [`TextInput`] has some local [`State`].
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::{Cell, LazyCell}; use std::cell::{Cell, LazyCell};
use unicode_segmentation::UnicodeSegmentation;
use crate::ext::ColorExt; use crate::ext::ColorExt;
use crate::theme::THEME; use crate::theme::THEME;
@ -2540,63 +2539,6 @@ fn input_method<'b>(
} }
} }
#[allow(clippy::too_many_arguments)]
fn render_graphemes(
value: &Value,
state: &State,
left: usize,
right: usize,
text: &str,
text_color: Color,
bounds: Rectangle,
size: f32,
renderer: &mut crate::Renderer,
font: iced_core::Font,
) {
let lo_byte = value.byte_index_at_grapheme(left);
let hi_byte = value.byte_index_at_grapheme(right);
let rects = state.value.raw().highlight(
0,
(lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before),
);
if !rects.is_empty() {
let grapheme_range = text[lo_byte..hi_byte].to_string();
let origin = bounds.position();
let start_pos = origin + (rects.first().unwrap().position() - Point::ORIGIN);
for rect in rects {
let absolute_rect = Rectangle {
x: rect.x + origin.x,
y: rect.y + origin.y,
width: rect.width,
height: rect.height,
};
renderer.fill_text(
Text {
content: grapheme_range.clone(),
font,
bounds: bounds.size(),
size: iced::Pixels(size),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
},
start_pos,
text_color,
absolute_rect,
);
}
}
}
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided. /// [`Value`] if provided.
/// ///
@ -2805,7 +2747,7 @@ pub fn draw<'a, Message>(
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
#[cfg(not(wayland_platform))] #[cfg(not(wayland_platform))]
let handling_dnd_offer = false; let handling_dnd_offer = false;
let (cursors, offset, _) = if let Some(focus) = let (cursors, offset, _is_selecting) = if let Some(focus) =
state.is_focused.filter(|f| f.focused).or_else(|| { state.is_focused.filter(|f| f.focused).or_else(|| {
let now = Instant::now(); let now = Instant::now();
handling_dnd_offer.then_some(Focus { handling_dnd_offer.then_some(Focus {
@ -2879,6 +2821,7 @@ pub fn draw<'a, Message>(
(lo_byte, text::Affinity::After), (lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before), (hi_byte, text::Affinity::Before),
); );
let cursors: Vec<(renderer::Quad, Color)> = rects let cursors: Vec<(renderer::Quad, Color)> = rects
.into_iter() .into_iter()
.map(|r| { .map(|r| {
@ -2932,6 +2875,7 @@ pub fn draw<'a, Message>(
state.value.raw().min_width(), state.value.raw().min_width(),
effective_alignment(state.value.raw()), effective_alignment(state.value.raw()),
); );
if cursors.is_empty() { if cursors.is_empty() {
renderer.with_translation(Vector::ZERO, |_| {}); renderer.with_translation(Vector::ZERO, |_| {});
} else { } else {
@ -2948,77 +2892,33 @@ pub fn draw<'a, Message>(
width: actual_width, width: actual_width,
..text_bounds ..text_bounds
}; };
let tcolor = if text.is_empty() { let color = if text.is_empty() {
appearance.placeholder_color appearance.placeholder_color
} else { } else {
text_color text_color
}; };
if let cursor::State::Selection { start, end } = state.cursor.state(value) renderer.fill_text(
&& state.is_focused() Text {
{ content: if text.is_empty() {
let left = start.min(end); placeholder.to_string()
let right = end.max(start); } else {
let grapheme_len = text.graphemes(true).count(); text.clone()
if left > 0 {
render_graphemes(
value, state, 0, left, &text, tcolor, bounds, size, renderer, font,
);
}
if grapheme_len >= right {
render_graphemes(
value,
state,
left,
right,
&text,
appearance.selected_text_color,
bounds,
size,
renderer,
font,
);
}
if left < grapheme_len {
render_graphemes(
value,
state,
right,
grapheme_len,
&text,
tcolor,
bounds,
size,
renderer,
font,
);
}
} else {
renderer.fill_text(
Text {
content: if text.is_empty() {
placeholder.to_string()
} else {
text.clone()
},
font,
bounds: bounds.size(),
size: iced::Pixels(size),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
}, },
bounds.position(), font,
tcolor, bounds: bounds.size(),
text_bounds, size: iced::Pixels(size),
); align_x: text::Alignment::Default,
} align_y: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
},
bounds.position(),
color,
text_bounds,
);
}; };
// FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue.

View file

@ -142,26 +142,23 @@ where
viewport: &Rectangle, viewport: &Rectangle,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let (content_state, toasts_state) = state.children.split_at_mut(1); //TODO: this hides the overlay of the content during the toast
let content = self.content.as_widget_mut().overlay(
&mut content_state[0],
layout,
renderer,
viewport,
translation,
);
if self.is_empty { if self.is_empty {
return content; self.content.as_widget_mut().overlay(
&mut state.children[0],
layout,
renderer,
viewport,
translation,
)
} else {
let _bounds = layout.bounds();
Some(overlay::Element::new(Box::new(ToasterOverlay::new(
&mut state.children[1],
&mut self.toasts,
))))
} }
// keep the content's overlays (e.g. popovers, menus) visible while a toast is shown
let toaster = overlay::Element::new(Box::new(ToasterOverlay::new(
&mut toasts_state[0],
&mut self.toasts,
)));
Some(match content {
Some(content) => overlay::Group::with_children(vec![content, toaster]).overlay(),
None => toaster,
})
} }
fn drag_destinations( fn drag_destinations(

View file

@ -40,7 +40,7 @@ pub struct Tooltip<'a, Message, TopLevelMessage> {
label: Option<Vec<iced_accessibility::accesskit::NodeId>>, label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
content: crate::Element<'a, Message>, content: crate::Element<'a, Message>,
on_leave: Message, on_leave: Message,
on_surface_action: Box<dyn Fn(crate::surface::Action<TopLevelMessage>) -> Message>, on_surface_action: Box<dyn Fn(crate::surface::Action) -> Message>,
width: Length, width: Length,
height: Length, height: Length,
padding: Padding, padding: Padding,
@ -75,7 +75,7 @@ impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> {
+ Sync + Sync
+ 'static, + 'static,
on_leave: Message, on_leave: Message,
on_surface_action: impl Fn(crate::surface::Action<TopLevelMessage>) -> Message + 'static, on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static,
) -> Self { ) -> Self {
Self { Self {
id: Id::unique(), id: Id::unique(),
@ -440,7 +440,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
>, >,
delay: Option<Duration>, delay: Option<Duration>,
on_leave: &Message, on_leave: &Message,
on_surface_action: &dyn Fn(crate::surface::Action<TopLevelMessage>) -> Message, on_surface_action: &dyn Fn(crate::surface::Action) -> Message,
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
) { ) {
match event { match event {
@ -534,8 +534,19 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
crate::surface::Action::Popup( crate::surface::Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
Some(Arc::new(move || view()) Some({
as crate::surface::View<TopLevelMessage>), 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)
}),
) )
}) })
})); }));
@ -572,8 +583,19 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>(
let sm = crate::surface::Action::Popup( let sm = crate::surface::Action::Popup(
Arc::new(boxed), Arc::new(boxed),
Arc::new(boxed_live), Arc::new(boxed_live),
Some(Arc::new(move || view()) Some({
as crate::surface::View<TopLevelMessage>), 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)); shell.publish((on_surface_action)(sm));
} }