wip: menus

This commit is contained in:
Ashley Wulber 2025-01-22 16:31:51 -05:00
parent b316d8901b
commit 04b51bc1ba
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
25 changed files with 1358 additions and 243 deletions

View file

@ -66,6 +66,7 @@ tokio = [
# Wayland window support # Wayland window support
wayland = [ wayland = [
"ashpd?/wayland", "ashpd?/wayland",
"autosize",
"iced_runtime/wayland", "iced_runtime/wayland",
"iced/wayland", "iced/wayland",
"iced_winit/wayland", "iced_winit/wayland",

View file

@ -1,32 +1,65 @@
use std::sync::{Arc, Mutex};
use cosmic::app::message::{SurfaceMessage, SurfaceMessageHandler}; use cosmic::app::message::{SurfaceMessage, SurfaceMessageHandler};
use cosmic::app::Core; use cosmic::app::Core;
use cosmic::iced::application;
use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup};
use cosmic::iced::window::Id; use cosmic::iced::window::Id;
use cosmic::iced::{Length, Limits, Task}; use cosmic::iced::{Length, Limits, Task};
use cosmic::iced_runtime::core::window; use cosmic::iced_runtime::core::window;
use cosmic::iced_runtime::platform_specific::wayland::popup::SctkPopupSettings;
use cosmic::iced_runtime::platform_specific::wayland::subsurface; use cosmic::iced_runtime::platform_specific::wayland::subsurface;
use cosmic::theme::iced; use cosmic::widget::dropdown::DropdownView;
use cosmic::widget::{layer_container, list_column, settings, toggler}; use cosmic::widget::{autosize, dropdown, layer_container, list_column, settings, toggler};
use cosmic::{applet, iced_core, Element, Theme}; use cosmic::{iced_core, Element};
use once_cell::sync::Lazy;
const ID: &str = "com.system76.CosmicAppletExample"; const ID: &str = "com.system76.CosmicAppletExample";
#[derive(Default)] static SUBSURFACE_ID: Lazy<cosmic::widget::Id> =
Lazy::new(|| cosmic::widget::Id::new("subsurface"));
pub struct Window { pub struct Window {
core: Core, core: Core,
popup: Option<Id>, popup: Option<Id>,
example_row: bool, example_row: bool,
subsurface_id: Option<Id>, selected: Option<usize>,
subsurface_id: Id,
dropdown_id: Id,
} }
#[derive(Clone, Debug)] impl Default for Window {
fn default() -> Self {
Self {
core: Core::default(),
popup: None,
example_row: false,
selected: None,
subsurface_id: Id::unique(),
dropdown_id: Id::unique(),
}
}
}
#[derive(Clone)]
pub enum Message { pub enum Message {
PopupClosed(Id), PopupClosed(Id),
PopupCloseRequested(Id),
ToggleExampleRow(bool), ToggleExampleRow(bool),
Selected(usize),
Surface(SurfaceMessage), Surface(SurfaceMessage),
OpenDropdown(SctkPopupSettings, DropdownView<Message>),
}
impl std::fmt::Debug for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PopupClosed(arg0) => f.debug_tuple("PopupClosed").field(arg0).finish(),
Self::PopupCloseRequested(arg0) => {
f.debug_tuple("PopupCloseRequested").field(arg0).finish()
}
Self::ToggleExampleRow(arg0) => f.debug_tuple("ToggleExampleRow").field(arg0).finish(),
Self::Selected(arg0) => f.debug_tuple("Selected").field(arg0).finish(),
Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
Self::OpenDropdown(arg0, _) => f.debug_tuple("OpenDropdown").field(arg0).finish(),
}
}
} }
impl SurfaceMessageHandler for Message { impl SurfaceMessageHandler for Message {
@ -82,6 +115,29 @@ impl cosmic::Application for Window {
} }
Message::Surface(surface_message) => {} Message::Surface(surface_message) => {}
Message::Selected(i) => {
self.selected = Some(i);
return cosmic::task::message(cosmic::app::message::destroy_popup::<Window>(
self.dropdown_id,
));
}
Message::OpenDropdown(sctk_popup_settings, view) => {
self.dropdown_id = sctk_popup_settings.id;
return cosmic::task::message(cosmic::app::message::get_popup::<
Window,
>(
move |_: &mut Window| sctk_popup_settings.clone(),
Some(
move |_: &Window| -> cosmic::Element<'static, cosmic::app::Message<Message>> {
view().map(cosmic::app::Message::App)
},
),
));
}
Message::PopupCloseRequested(id) => {
dbg!(id, self.dropdown_id);
return cosmic::task::message(cosmic::app::message::destroy_popup::<Window>(id));
}
} }
Task::none() Task::none()
} }
@ -129,8 +185,8 @@ impl cosmic::Application for Window {
) )
.height(Length::Fixed(50.)), .height(Length::Fixed(50.)),
), ),
).add(dropdown(&["1", "asdf", "hello", "test"], state.selected, Message::Selected).with_popup(state.popup.unwrap_or(Id::NONE), Message::OpenDropdown).on_close_popup(Message::PopupCloseRequested)
); );
Element::from( Element::from(
state.core.applet.popup_container(content_list), state.core.applet.popup_container(content_list),
) )
@ -141,25 +197,29 @@ impl cosmic::Application for Window {
) )
}, },
), ),
|layout| { |_| {
cosmic::app::message::get_subsurface::<Window>( cosmic::app::message::get_subsurface(
|app: &mut Window| { |app: &mut Window| {
let id = window::Id::unique();
app.subsurface_id = Some(id);
subsurface::SctkSubsurfaceSettings { subsurface::SctkSubsurfaceSettings {
parent: window::Id::RESERVED, parent: window::Id::RESERVED,
id, id: app.subsurface_id,
loc: iced_core::Point { x: -100., y: 0. }, loc: iced_core::Point { x: -100., y: 0. },
size: Some(iced_core::Size::new(100., 18.)), size: Some((10., 10.).into()),
z: 1, z: 1,
} }
}, },
Some(|app: &Window| layer_container(cosmic::widget::text("hello")).into()), Some(|_: &Window| {
autosize::autosize(
layer_container(cosmic::widget::text("hello"))
.layer(cosmic::cosmic_theme::Layer::Background),
SUBSURFACE_ID.clone(),
)
.into()
}),
) )
}, },
cosmic::app::message::destroy_subsurface::<Window>( cosmic::app::message::destroy_subsurface::<Window>(
self.subsurface_id.unwrap_or(window::Id::NONE), self.subsurface_id
), ),
) )
.into() .into()

View file

@ -3,12 +3,26 @@
//! Application API example //! Application API example
use std::collections::HashMap;
use std::sync::LazyLock;
use cosmic::app::message::{SurfaceMessage, SurfaceMessageHandler};
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::widget::column; use cosmic::iced::widget::column;
use cosmic::iced::Length;
use cosmic::iced_core::Size; use cosmic::iced_core::Size;
use cosmic::widget::nav_bar; use cosmic::widget::icon::{from_name, Handle};
use cosmic::widget::menu::KeyBind;
use cosmic::widget::{
container,
menu::{self, action::MenuAction},
nav_bar, responsive,
};
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{executor, iced, ApplicationExt, Element};
static MENU_ID: LazyLock<iced::id::Id> = LazyLock::new(|| iced::id::Id::new("menu_id"));
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Page { pub enum Page {
Page1, Page1,
@ -28,11 +42,24 @@ impl Page {
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Action {
Hi,
}
impl MenuAction for Action {
type Message = Message;
fn message(&self) -> Message {
Message::Hi
}
}
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init(); // tracing_subscriber::fmt::init();
let _ = tracing_log::LogTracer::init(); // let _ = tracing_log::LogTracer::init();
let input = vec![ let input = vec![
(Page::Page1, "🖖 Hello from libcosmic.".into()), (Page::Page1, "🖖 Hello from libcosmic.".into()),
@ -56,6 +83,23 @@ pub enum Message {
Input2(String), Input2(String),
Ignore, Ignore,
ToggleHide, ToggleHide,
Surface(SurfaceMessage),
Hi,
}
impl SurfaceMessageHandler for Message {
fn to_surface_message(self) -> cosmic::app::message::MessageWrapper<Self> {
match self {
Message::Surface(msg) => cosmic::app::message::MessageWrapper::Surface(msg),
msg => cosmic::app::message::MessageWrapper::Message(msg),
}
}
}
impl From<SurfaceMessage> for Message {
fn from(value: SurfaceMessage) -> Self {
Message::Surface(value)
}
} }
/// The [`App`] stores application-specific state. /// The [`App`] stores application-specific state.
@ -65,6 +109,7 @@ pub struct App {
input_1: String, input_1: String,
input_2: String, input_2: String,
hidden: bool, hidden: bool,
keybinds: HashMap<KeyBind, Action>,
} }
/// Implement [`cosmic::Application`] to integrate with COSMIC. /// Implement [`cosmic::Application`] to integrate with COSMIC.
@ -105,6 +150,7 @@ impl cosmic::Application for App {
input_1: String::new(), input_1: String::new(),
input_2: String::new(), input_2: String::new(),
hidden: true, hidden: true,
keybinds: HashMap::new(),
}; };
let command = app.update_title(); let command = app.update_title();
@ -136,6 +182,12 @@ impl cosmic::Application for App {
Message::ToggleHide => { Message::ToggleHide => {
self.hidden = !self.hidden; self.hidden = !self.hidden;
} }
Message::Surface(_) => {
unimplemented!()
}
Message::Hi => {
dbg!("hi");
}
} }
Task::none() Task::none()
} }
@ -178,6 +230,49 @@ impl cosmic::Application for App {
Element::from(centered) Element::from(centered)
} }
fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![self.core.responsive_menu_bar(
MENU_ID.clone(),
vec![
menu::Tree::with_children(
menu::root("hiiiiiiiiiiiiiiiiiii 1"),
menu::items(
&self.keybinds,
vec![menu::Item::Button("hi", None, Action::Hi)],
),
),
menu::Tree::with_children(
menu::root("hiiiiiiiiiiiiiiiiii 2"),
menu::items(
&self.keybinds,
vec![menu::Item::Button("hi 2", None, Action::Hi)],
),
),
menu::Tree::with_children(
menu::root("hiiiiiiiiiiiiiiiiiiiii 3"),
menu::items(
&self.keybinds,
vec![menu::Item::Button("hi 3", None, Action::Hi)],
),
),
menu::Tree::with_children(
menu::root("hi 3"),
menu::items(
&self.keybinds,
vec![menu::Item::Button("hi 3", None, Action::Hi)],
),
),
menu::Tree::with_children(
menu::root("hi 4"),
menu::items(
&self.keybinds,
vec![menu::Item::Button("hi 3", None, Action::Hi)],
),
),
],
)]
}
} }
impl App impl App

View file

@ -3,17 +3,24 @@
use std::{cell::OnceCell, collections::HashMap}; use std::{cell::OnceCell, collections::HashMap};
use crate::widget::nav_bar; use crate::{
theme::menu_bar,
widget::{
button, icon,
menu::{self, Tree},
nav_bar, responsive_container,
},
};
use cosmic_config::CosmicConfigEntry; use cosmic_config::CosmicConfigEntry;
use cosmic_theme::ThemeMode; use cosmic_theme::ThemeMode;
use iced::window; use iced::{window, Limits, Size};
use iced_core::window::Id; use iced_core::{window::Id, Element};
use palette::Srgba; use palette::Srgba;
use slotmap::Key; use slotmap::Key;
use crate::Theme; use crate::Theme;
use super::{Element, Task}; use super::{message::SurfaceMessage, Renderer, Task};
/// Status of the nav bar and its panels. /// Status of the nav bar and its panels.
#[derive(Clone)] #[derive(Clone)]
@ -97,6 +104,8 @@ pub struct Core {
pub(crate) main_window: Option<window::Id>, pub(crate) main_window: Option<window::Id>,
pub(crate) exit_on_main_window_closed: bool, pub(crate) exit_on_main_window_closed: bool,
pub(crate) menu_bars: HashMap<crate::widget::Id, (Limits, Size)>,
} }
impl Default for Core { impl Default for Core {
@ -153,6 +162,7 @@ impl Default for Core {
portal_is_high_contrast: None, portal_is_high_contrast: None,
main_window: None, main_window: None,
exit_on_main_window_closed: true, exit_on_main_window_closed: true,
menu_bars: HashMap::new(),
} }
} }
} }
@ -376,4 +386,35 @@ impl Core {
std::mem::swap(&mut self.main_window, &mut id); std::mem::swap(&mut self.main_window, &mut id);
id id
} }
pub fn responsive_menu_bar<'a, Message: Clone + From<SurfaceMessage> + 'static>(
&self,
id: crate::widget::Id,
roots: Vec<Tree<'a, Message, Renderer>>,
) -> crate::Element<'a, Message> {
let menu_bar_size = self.menu_bars.get(&id);
if !menu_bar_size.is_some_and(|(limits, size)| {
let max_size = limits.max();
max_size.width < size.width
}) {
crate::Element::from(responsive_container::responsive_container(
menu::bar(roots),
id,
))
} else {
crate::Element::from(
responsive_container::responsive_container(
menu::bar(vec![menu::Tree::<'a, _, _>::with_children(
button::icon(icon::from_name("open-menu-symbolic"))
.padding([4, 12])
.class(crate::theme::Button::MenuRoot),
roots,
)]),
id,
)
.size(menu_bar_size.unwrap().1),
)
}
}
} }

View file

@ -124,8 +124,7 @@ where
let message = match message { let message = match message {
super::Message::App(message) => { super::Message::App(message) => {
match super::message::SurfaceMessageHandler::to_surface_message(message) { match super::message::SurfaceMessageHandler::to_surface_message(message) {
super::message::MessageWrapper::Surface(surface_message) => super::message::MessageWrapper::Surface(surface_message) => {
{
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
match surface_message { match surface_message {
super::message::SurfaceMessage::Subsurface(settings, view) => { super::message::SurfaceMessage::Subsurface(settings, view) => {
@ -160,7 +159,6 @@ where
)) ))
} }
} }
#[cfg(feature = "wayland")]
super::message::SurfaceMessage::Popup(settings, view) => { super::message::SurfaceMessage::Popup(settings, view) => {
let Some(settings) = std::sync::Arc::try_unwrap(settings) let Some(settings) = std::sync::Arc::try_unwrap(settings)
.ok() .ok()
@ -200,8 +198,19 @@ where
super::message::SurfaceMessage::DestroySubsurface(id) => { super::message::SurfaceMessage::DestroySubsurface(id) => {
iced_winit::commands::subsurface::destroy_subsurface(id) iced_winit::commands::subsurface::destroy_subsurface(id)
} }
super::message::SurfaceMessage::ResponsiveMenuBar {
menu_bar,
limits,
size,
} => {
let core = self.app.core_mut();
core.menu_bars.insert(menu_bar, (limits, size));
iced::Task::none()
} }
} }
#[cfg(not(feature = "wayland"))]
iced::Task::none()
}
super::message::MessageWrapper::Message(message) => self.app.update(message), super::message::MessageWrapper::Message(message) => self.app.update(message),
} }
} }

View file

@ -15,6 +15,8 @@ pub(crate) mod multi_window;
pub mod settings; pub mod settings;
pub mod message { pub mod message {
use iced::{Limits, Size};
pub enum MessageWrapper<M> { pub enum MessageWrapper<M> {
Surface(SurfaceMessage), Surface(SurfaceMessage),
Message(M), Message(M),
@ -31,6 +33,7 @@ pub mod message {
} }
} }
/// Ignore this message in your application. It will be intercepted.
#[derive(Clone)] #[derive(Clone)]
pub enum SurfaceMessage { pub enum SurfaceMessage {
/// Create a subsurface with a view function /// Create a subsurface with a view function
@ -47,20 +50,39 @@ pub mod message {
), ),
/// Destroy a subsurface with a view function /// Destroy a subsurface with a view function
DestroyPopup(iced::window::Id), DestroyPopup(iced::window::Id),
/// Responsive menu bar update
ResponsiveMenuBar {
/// Id of the menu bar
menu_bar: crate::widget::Id,
/// Limits of the menu bar
limits: Limits,
/// Requested Full Size for expanded menu bar
size: Size,
},
} }
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
impl std::fmt::Debug for SurfaceMessage { impl std::fmt::Debug for SurfaceMessage {
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 {
SurfaceMessage::Subsurface(any, any1) => f.debug_tuple("Subsurface").finish(), Self::Subsurface(arg0, arg1) => {
SurfaceMessage::DestroySubsurface(id) => { f.debug_tuple("Subsurface").field(arg0).field(arg1).finish()
f.debug_tuple("DestroySubsurface").field(id).finish()
} }
SurfaceMessage::Popup(any, any1) => f.debug_tuple("Popup").finish(), Self::DestroySubsurface(arg0) => {
SurfaceMessage::DestroyPopup(id) => { f.debug_tuple("DestroySubsurface").field(arg0).finish()
f.debug_tuple("DestroyPopup").field(id).finish()
} }
Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(),
Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(),
Self::ResponsiveMenuBar {
menu_bar,
limits,
size,
} => f
.debug_struct("ResponsiveMenuBar")
.field("menu_bar", menu_bar)
.field("limits", limits)
.field("size", size)
.finish(),
} }
} }
} }
@ -149,6 +171,20 @@ pub mod message {
)) ))
} }
// TODO could this somehow be used by widgets? It is the wrong message type though.
// #[cfg(feature = "wayland")]
// pub fn get_simple_popup<App: super::Application>(
// settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
// view: Option<
// impl Fn() -> crate::Element<'static, super::Message<App::Message>> + Send + Sync + 'static,
// >,
// ) -> App::Message
// where
// App::Message: SurfaceMessageHandler + From<SurfaceMessage>,
// {
// todo!()
// }
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
pub fn get_subsurface<App: super::Application>( pub fn get_subsurface<App: super::Application>(
settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync + 'static,
@ -477,7 +513,7 @@ impl DbusActivation {
pub fn run_single_instance<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result pub fn run_single_instance<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result
where where
App::Flags: CosmicFlags, App::Flags: CosmicFlags,
App::Message: Clone + std::fmt::Debug + Send + 'static, App::Message: Clone + std::fmt::Debug + Send + SurfaceMessageHandler + 'static,
{ {
let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok();

View file

@ -308,6 +308,7 @@ impl Context {
}, },
parent_size: None, parent_size: None,
grab: true, grab: true,
close_with_children: false,
} }
} }

View file

@ -1,6 +1,8 @@
// From iced_aw, license MIT // From iced_aw, license MIT
//! Change the appearance of menu bars and their menus. //! Change the appearance of menu bars and their menus.
use std::sync::Arc;
use crate::Theme; use crate::Theme;
use iced_widget::core::Color; use iced_widget::core::Color;
@ -33,19 +35,19 @@ pub trait StyleSheet {
} }
/// The style of a menu bar and its menus /// The style of a menu bar and its menus
#[derive(Default)] #[derive(Default, Clone)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub enum MenuBarStyle { pub enum MenuBarStyle {
/// The default style. /// The default style.
#[default] #[default]
Default, Default,
/// A [`Theme`] that uses a `Custom` palette. /// A [`Theme`] that uses a `Custom` palette.
Custom(Box<dyn StyleSheet<Style = Theme>>), Custom(Arc<dyn StyleSheet<Style = Theme>>),
} }
impl From<fn(&Theme) -> Appearance> for MenuBarStyle { impl From<fn(&Theme) -> Appearance> for MenuBarStyle {
fn from(f: fn(&Theme) -> Appearance) -> Self { fn from(f: fn(&Theme) -> Appearance) -> Self {
Self::Custom(Box::new(f)) Self::Custom(Arc::new(f))
} }
} }

View file

@ -32,5 +32,7 @@ mod text_input;
#[doc(inline)] #[doc(inline)]
pub use self::text_input::TextInput; pub use self::text_input::TextInput;
#[cfg(feature = "wayland")]
pub mod tooltip; pub mod tooltip;
#[cfg(feature = "wayland")]
pub use tooltip::Tooltip; pub use tooltip::Tooltip;

View file

@ -6,24 +6,30 @@
use crate::widget::menu::{ use crate::widget::menu::{
self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight,
}; };
use crate::widget::ArcElementWrapper;
use derive_setters::Setters; use derive_setters::Setters;
use iced::touch::Finger; use iced::touch::Finger;
use iced::{Event, Vector}; use iced::{Event, Vector};
use iced_core::widget::{tree, Tree, Widget}; use iced_core::widget::{tree, Tree, Widget};
use iced_core::{event, mouse, touch, Length, Point, Size}; use iced_core::{event, mouse, touch, Length, Point, Size};
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use super::dropdown;
/// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation.
pub fn context_menu<'a, Message: 'a>( pub fn context_menu<Message: Clone + 'static>(
content: impl Into<crate::Element<'a, Message>> + 'a, content: impl Into<crate::Element<'static, Message>> + 'static,
// on_context: Message, // on_context: Message,
context_menu: Option<Vec<menu::Tree<'a, Message>>>, context_menu: Option<Vec<menu::Tree<Message>>>,
) -> ContextMenu<'a, Message> { ) -> ContextMenu<'static, Message> {
let mut this = ContextMenu { let mut this = ContextMenu {
content: content.into(), content: content.into(),
context_menu: context_menu.map(|menus| { context_menu: context_menu.map(|menus| {
vec![menu::Tree::with_children( vec![menu::Tree::with_children(
ArcElementWrapper(Arc::new(Mutex::new(crate::Element::from(
crate::widget::row::<'static, Message>(), crate::widget::row::<'static, Message>(),
)))),
menus, menus,
)] )]
}), }),
@ -43,10 +49,10 @@ pub struct ContextMenu<'a, Message> {
#[setters(skip)] #[setters(skip)]
content: crate::Element<'a, Message>, content: crate::Element<'a, Message>,
#[setters(skip)] #[setters(skip)]
context_menu: Option<Vec<menu::Tree<'a, Message>>>, context_menu: Option<Vec<menu::Tree<Message>>>,
} }
impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer> impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer>
for ContextMenu<'a, Message> for ContextMenu<'a, Message>
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -58,6 +64,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
tree::State::new(LocalState { tree::State::new(LocalState {
context_cursor: Point::default(), context_cursor: Point::default(),
fingers_pressed: Default::default(), fingers_pressed: Default::default(),
menu_state: Default::default(),
}) })
} }
@ -75,9 +82,12 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
.map(|root| { .map(|root| {
let mut tree = Tree::empty(); let mut tree = Tree::empty();
let flat = root let flat = root
.flattern() .flatten()
.iter() .iter()
.map(|mt| Tree::new(mt.item.as_widget())) .map(|mt| {
let guard = mt.item.0.lock().unwrap();
Tree::new(guard.as_widget())
})
.collect(); .collect();
tree.children = flat; tree.children = flat;
tree tree
@ -214,7 +224,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
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>> {
let state = tree.state.downcast_ref::<LocalState>(); let state = tree.state.downcast_ref::<LocalState>();
let menu_state = state.menu_state.clone();
let Some(context_menu) = self.context_menu.as_mut() else { let Some(context_menu) = self.context_menu.as_mut() else {
return None; return None;
}; };
@ -229,8 +239,8 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
Some( Some(
crate::widget::menu::Menu { crate::widget::menu::Menu {
tree: &mut tree.children[1], tree: menu_state,
menu_roots: context_menu, menu_roots: std::borrow::Cow::Borrowed(context_menu),
bounds_expand: 16, bounds_expand: 16,
menu_overlays_parent: true, menu_overlays_parent: true,
close_condition: CloseCondition { close_condition: CloseCondition {
@ -245,7 +255,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
cross_offset: 0, cross_offset: 0,
root_bounds_list: vec![bounds], root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive), path_highlight: Some(PathHighlight::MenuActive),
style: &crate::theme::menu_bar::MenuBarStyle::Default, style: std::borrow::Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default),
position: Point::new(translation.x, translation.y), position: Point::new(translation.x, translation.y),
} }
.overlay(), .overlay(),
@ -265,8 +275,10 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
} }
} }
impl<'a, Message: Clone + 'a> From<ContextMenu<'a, Message>> for crate::Element<'a, Message> { impl<Message: Clone + 'static> From<ContextMenu<'static, Message>>
fn from(widget: ContextMenu<'a, Message>) -> Self { for crate::Element<'static, Message>
{
fn from(widget: ContextMenu<'static, Message>) -> Self {
Self::new(widget) Self::new(widget)
} }
} }
@ -285,4 +297,5 @@ fn touch_lifted(event: &Event) -> bool {
pub struct LocalState { pub struct LocalState {
context_cursor: Point, context_cursor: Point,
fingers_pressed: HashSet<Finger>, fingers_pressed: HashSet<Finger>,
menu_state: dropdown::menu::State,
} }

View file

@ -3,7 +3,11 @@
// SPDX-License-Identifier: MPL-2.0 AND MIT // SPDX-License-Identifier: MPL-2.0 AND MIT
mod appearance; mod appearance;
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
pub use appearance::{Appearance, StyleSheet}; pub use appearance::{Appearance, StyleSheet};
use iced::advanced::widget;
use crate::widget::{icon, Container}; use crate::widget::{icon, Container};
use iced_core::event::{self, Event}; use iced_core::event::{self, Event};
@ -21,11 +25,12 @@ use iced_widget::scrollable::Scrollable;
pub struct Menu<'a, S, Message> pub struct Menu<'a, S, Message>
where where
S: AsRef<str>, S: AsRef<str>,
[S]: std::borrow::ToOwned,
{ {
state: &'a mut State, state: State,
options: &'a [S], options: Cow<'a, [S]>,
icons: &'a [icon::Handle], icons: Cow<'a, [icon::Handle]>,
hovered_option: &'a mut Option<usize>, hovered_option: Arc<Mutex<Option<usize>>>,
selected_option: Option<usize>, selected_option: Option<usize>,
on_selected: Box<dyn FnMut(usize) -> Message + 'a>, on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
@ -36,14 +41,17 @@ where
style: (), 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 /// Creates a new [`Menu`] with the given [`State`], a list of options, and
/// the message to produced when an option is selected. /// the message to produced when an option is selected.
pub fn new( pub fn new(
state: &'a mut State, state: State,
options: &'a [S], options: Cow<'a, [S]>,
icons: &'a [icon::Handle], icons: Cow<'a, [icon::Handle]>,
hovered_option: &'a mut Option<usize>, hovered_option: Arc<Mutex<Option<usize>>>,
selected_option: Option<usize>, selected_option: Option<usize>,
on_selected: impl FnMut(usize) -> Message + 'a, on_selected: impl FnMut(usize) -> Message + 'a,
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
@ -102,20 +110,35 @@ impl<'a, S: AsRef<str>, Message: 'a> Menu<'a, S, Message> {
) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> {
overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) 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`]. /// The local state of a [`Menu`].
#[must_use] #[must_use]
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State { pub struct State {
tree: Tree, pub(crate) tree: Arc<Mutex<Tree>>,
} }
// XXX The tree is only used on one thread, but this is needed to make a message
unsafe impl Send for State {}
unsafe impl Sync for State {}
impl State { impl State {
/// Creates a new [`State`] for a [`Menu`]. /// Creates a new [`State`] for a [`Menu`].
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
tree: Tree::empty(), tree: Arc::new(Mutex::new(Tree::empty())),
} }
} }
} }
@ -127,7 +150,7 @@ impl Default for State {
} }
struct Overlay<'a, Message> { struct Overlay<'a, Message> {
state: &'a mut Tree, state: Arc<Mutex<Tree>>,
container: Container<'a, Message, crate::Theme, crate::Renderer>, container: Container<'a, Message, crate::Theme, crate::Renderer>,
width: f32, width: f32,
target_height: f32, target_height: f32,
@ -140,7 +163,10 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
menu: Menu<'a, S, Message>, menu: Menu<'a, S, Message>,
target_height: f32, target_height: f32,
position: Point, position: Point,
) -> Self { ) -> Self
where
[S]: ToOwned,
{
let Menu { let Menu {
state, state,
options, options,
@ -172,10 +198,11 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
)) ))
.class(crate::style::Container::Dropdown); .class(crate::style::Container::Dropdown);
state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); let mut tree_guard = state.tree.lock().unwrap();
tree_guard.diff(&mut container as &mut dyn Widget<_, _, _>);
drop(tree_guard);
Self { Self {
state: &mut state.tree, state: state.tree.clone(),
container, container,
width, width,
target_height, target_height,
@ -183,20 +210,17 @@ impl<'a, Message: 'a> Overlay<'a, Message> {
position, position,
} }
} }
}
impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer> fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
for Overlay<'a, Message> let mut guard = self.state.lock().unwrap();
{
fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { let space_below = bounds.height - (self.position.y + self.target_height);
let position = self.position; let space_above = self.position.y;
let space_below = bounds.height - (position.y + self.target_height);
let space_above = position.y;
let limits = layout::Limits::new( let limits = layout::Limits::new(
Size::ZERO, Size::ZERO,
Size::new( Size::new(
bounds.width - position.x, bounds.width - self.position.x,
if space_below > space_above { if space_below > space_above {
space_below space_below
} else { } else {
@ -206,16 +230,16 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
) )
.width(self.width); .width(self.width);
let node = self.container.layout(self.state, renderer, &limits); let node = self.container.layout(&mut guard, renderer, &limits);
node.clone().move_to(if space_below > space_above { 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 { } 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, &mut self,
event: Event, event: Event,
layout: Layout<'_>, layout: Layout<'_>,
@ -225,24 +249,28 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
) -> event::Status { ) -> event::Status {
let bounds = layout.bounds(); let bounds = layout.bounds();
let mut guard = self.state.lock().unwrap();
let state = &mut *guard;
self.container.on_event( self.container.on_event(
self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, state, event, layout, cursor, renderer, clipboard, shell, &bounds,
) )
} }
fn mouse_interaction( fn _mouse_interaction(
&self, &self,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
renderer: &crate::Renderer, renderer: &crate::Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
let guard = self.state.lock().unwrap();
self.container self.container
.mouse_interaction(self.state, layout, cursor, viewport, renderer) .mouse_interaction(&guard, layout, cursor, viewport, renderer)
} }
fn draw( fn _draw(
&self, &self,
renderer: &mut crate::Renderer, renderer: &mut crate::Renderer,
theme: &crate::Theme, theme: &crate::Theme,
@ -250,6 +278,8 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) { ) {
let state = self.state.lock().unwrap();
let appearance = theme.appearance(&self.style); let appearance = theme.appearance(&self.style);
let bounds = layout.bounds(); let bounds = layout.bounds();
@ -267,14 +297,127 @@ impl<'a, Message> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
); );
self.container self.container
.draw(self.state, renderer, theme, style, layout, cursor, &bounds); .draw(&*state, renderer, theme, style, layout, cursor, &bounds);
} }
} }
struct List<'a, S: AsRef<str>, Message> { impl<'a, Message: 'a> iced_core::Overlay<Message, crate::Theme, crate::Renderer>
options: &'a [S], for Overlay<'a, Message>
icons: &'a [icon::Handle], {
hovered_option: &'a mut Option<usize>, fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node {
let mut guard = self.state.lock().unwrap();
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,
) {
let guard = self.state.lock().unwrap();
self._draw(renderer, theme, style, layout, cursor);
}
}
impl<'a, Message: '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 mut guard = self.state.lock().unwrap();
let limits = limits.width(self.width);
self.container.layout(&mut guard, 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>, selected_option: Option<usize>,
on_selected: Box<dyn FnMut(usize) -> Message + 'a>, on_selected: Box<dyn FnMut(usize) -> Message + 'a>,
on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>,
@ -285,6 +428,8 @@ struct List<'a, S: AsRef<str>, Message> {
impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer> impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
for List<'a, S, Message> for List<'a, S, Message>
where
[S]: std::borrow::ToOwned,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size::new(Length::Fill, Length::Shrink) Size::new(Length::Fill, Length::Shrink)
@ -330,8 +475,9 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
) -> event::Status { ) -> event::Status {
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let hovered_guard = self.hovered_option.lock().unwrap();
if cursor.is_over(layout.bounds()) { 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)); shell.publish((self.on_selected)(index));
return event::Status::Captured; return event::Status::Captured;
} }
@ -348,14 +494,15 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
+ self.padding.vertical(); + self.padding.vertical();
let new_hovered_option = (cursor_position.y / option_height) as usize; 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 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)); 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 { .. }) => { Event::Touch(touch::Event::FingerPressed { .. }) => {
@ -367,10 +514,11 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
let option_height = let option_height =
f32::from(self.text_line_height.to_absolute(Pixels(text_size))) f32::from(self.text_line_height.to_absolute(Pixels(text_size)))
+ self.padding.vertical(); + 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)); shell.publish((self.on_selected)(index));
return event::Status::Captured; return event::Status::Captured;
} }
@ -434,6 +582,8 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
height: option_height, height: option_height,
}; };
let hovered_guard = self.hovered_option.lock().unwrap();
let (color, font) = if self.selected_option == Some(i) { let (color, font) = if self.selected_option == Some(i) {
let item_x = bounds.x + appearance.border_width; let item_x = bounds.x + appearance.border_width;
let item_width = appearance.border_width.mul_add(-2.0, bounds.width); let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
@ -471,7 +621,7 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
); );
(appearance.selected_text_color, crate::font::semibold()) (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_x = bounds.x + appearance.border_width;
let item_width = appearance.border_width.mul_add(-2.0, bounds.width); let item_width = appearance.border_width.mul_add(-2.0, bounds.width);
@ -538,6 +688,8 @@ impl<'a, S: AsRef<str>, Message> Widget<Message, crate::Theme, crate::Renderer>
impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>> impl<'a, S: AsRef<str>, Message: 'a> From<List<'a, S, Message>>
for Element<'a, Message, crate::Theme, crate::Renderer> for Element<'a, Message, crate::Theme, crate::Renderer>
where
[S]: std::borrow::ToOwned,
{ {
fn from(list: List<'a, S, Message>) -> Self { fn from(list: List<'a, S, Message>) -> Self {
Element::new(list) Element::new(list)

View file

@ -13,10 +13,14 @@ mod widget;
pub use widget::*; pub use widget::*;
/// Displays a list of options in a popover menu on select. /// Displays a list of options in a popover menu on select.
pub fn dropdown<'a, S: AsRef<str>, Message: 'a>( pub fn dropdown<
'a,
S: AsRef<str> + std::clone::Clone + Send + Sync + 'static,
Message: 'static + Clone,
>(
selections: &'a [S], selections: &'a [S],
selected: Option<usize>, selected: Option<usize>,
on_selected: impl Fn(usize) -> Message + 'a, on_selected: impl Fn(usize) -> Message + Send + Sync + 'static,
) -> Dropdown<'a, S, Message> { ) -> Dropdown<'a, S, Message> {
Dropdown::new(selections, selected, on_selected) Dropdown::new(selections, selected, on_selected)
} }

View file

@ -3,9 +3,10 @@
// SPDX-License-Identifier: MPL-2.0 AND MIT // SPDX-License-Identifier: MPL-2.0 AND MIT
use super::menu::{self, Menu}; use super::menu::{self, Menu};
use crate::widget::icon; use crate::widget::icon::{self, Handle};
use crate::Element;
use derive_setters::Setters; use derive_setters::Setters;
use iced::Radians; use iced::{window, Radians};
use iced_core::event::{self, Event}; use iced_core::event::{self, Event};
use iced_core::text::{self, Paragraph, Text}; use iced_core::text::{self, Paragraph, Text};
use iced_core::widget::tree::{self, Tree}; use iced_core::widget::tree::{self, Tree};
@ -14,14 +15,23 @@ use iced_core::{
Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget,
}; };
use iced_widget::pick_list::{self, Catalog}; use iced_widget::pick_list::{self, Catalog};
use std::borrow::Cow;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
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. /// A widget for selecting a single value from a list of selections.
#[derive(Setters)] #[derive(Setters)]
pub struct Dropdown<'a, S: AsRef<str>, Message> { pub struct Dropdown<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message>
where
[S]: std::borrow::ToOwned,
{
#[setters(skip)] #[setters(skip)]
on_selected: Box<dyn Fn(usize) -> Message + 'a>, on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
#[setters(skip)] #[setters(skip)]
selections: &'a [S], selections: &'a [S],
#[setters] #[setters]
@ -38,9 +48,29 @@ pub struct Dropdown<'a, S: AsRef<str>, Message> {
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
#[setters(strip_option)] #[setters(strip_option)]
font: Option<crate::font::Font>, font: Option<crate::font::Font>,
#[cfg(feature = "wayland")]
#[setters(skip)]
on_open: Option<
Arc<
dyn Fn(
iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
DropdownView<Message>,
) -> Message
+ 'a,
>,
>,
#[setters(skip)]
on_close_popup: Option<Box<dyn Fn(window::Id) -> Message + 'a>>,
#[setters(strip_option)]
window_id: Option<window::Id>,
#[cfg(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> Dropdown<'a, S, Message>
where
[S]: std::borrow::ToOwned,
{
/// The default gap. /// The default gap.
pub const DEFAULT_GAP: f32 = 4.0; pub const DEFAULT_GAP: f32 = 4.0;
@ -52,10 +82,10 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
pub fn new( pub fn new(
selections: &'a [S], selections: &'a [S],
selected: Option<usize>, selected: Option<usize>,
on_selected: impl Fn(usize) -> Message + 'a, on_selected: impl Fn(usize) -> Message + 'static + Send + Sync,
) -> Self { ) -> Self {
Self { Self {
on_selected: Box::new(on_selected), on_selected: Arc::new(on_selected),
selections, selections,
icons: &[], icons: &[],
selected, selected,
@ -65,12 +95,54 @@ impl<'a, S: AsRef<str>, Message> Dropdown<'a, S, Message> {
text_size: None, text_size: None,
text_line_height: text::LineHeight::Relative(1.2), text_line_height: text::LineHeight::Relative(1.2),
font: None, font: None,
#[cfg(feature = "wayland")]
on_open: None,
window_id: None,
#[cfg(feature = "wayland")]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
on_close_popup: None,
} }
} }
#[cfg(feature = "wayland")]
/// Handle dropdown requests for popup creation.
/// Intended to be used with [`crate::app::message::get_popup`]
pub fn with_popup(
mut self,
parent_id: window::Id,
on_open: impl Fn(
iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
DropdownView<Message>,
) -> Message
+ 'a,
) -> Self {
self.window_id = Some(parent_id);
self.on_open = Some(Arc::new(on_open));
self
}
#[cfg(feature = "wayland")]
pub fn with_positioner(
mut self,
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
) -> Self {
self.positioner = positioner;
self
}
#[cfg(feature = "wayland")]
/// Handle dropdown requests for popup removal.
/// Intended to be used with [`crate::app::message::destroy_popup`]
pub fn on_close_popup(mut self, on_close: impl Fn(window::Id) -> Message + 'a) -> Self {
self.on_close_popup = Some(Box::new(on_close));
self
}
} }
impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Renderer> impl<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone>
for Dropdown<'a, S, Message> Widget<Message, crate::Theme, crate::Renderer> for Dropdown<'a, S, Message>
where
[S]: std::borrow::ToOwned,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>() tree::Tag::of::<State>()
@ -158,10 +230,22 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
layout, layout,
cursor, cursor,
shell, shell,
self.on_selected.as_ref(), #[cfg(feature = "wayland")]
self.on_open.clone(),
#[cfg(feature = "wayland")]
self.positioner.clone(),
self.on_selected.clone(),
self.selected, self.selected,
self.selections, self.selections,
|| tree.state.downcast_mut::<State>(), || tree.state.downcast_mut::<State>(),
self.window_id,
self.on_close_popup.as_deref(),
self.icons,
self.gap,
self.padding,
self.text_size,
self.font,
self.selected,
) )
} }
@ -211,6 +295,9 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
renderer: &crate::Renderer, renderer: &crate::Renderer,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> { ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
#[cfg(feature = "wayland")]
return None;
let state = tree.state.downcast_mut::<State>(); let state = tree.state.downcast_mut::<State>();
overlay( overlay(
@ -225,7 +312,7 @@ impl<'a, S: AsRef<str>, Message: 'a> Widget<Message, crate::Theme, crate::Render
self.selections, self.selections,
self.icons, self.icons,
self.selected, self.selected,
&self.on_selected, self.on_selected.as_ref(),
translation, translation,
) )
} }
@ -242,8 +329,10 @@ 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>> impl<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: 'static + std::clone::Clone>
for crate::Element<'a, Message> From<Dropdown<'a, S, Message>> 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>) -> Self {
Self::new(pick_list) Self::new(pick_list)
@ -251,15 +340,16 @@ impl<'a, S: AsRef<str>, Message: 'a> From<Dropdown<'a, S, Message>>
} }
/// The local state of a [`Dropdown`]. /// The local state of a [`Dropdown`].
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct State { pub struct State {
icon: Option<svg::Handle>, icon: Option<svg::Handle>,
menu: menu::State, menu: menu::State,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
is_open: bool, is_open: Arc<AtomicBool>,
hovered_option: Option<usize>, hovered_option: Arc<Mutex<Option<usize>>>,
hashes: Vec<u64>, hashes: Vec<u64>,
selections: Vec<crate::Plain>, selections: Vec<crate::Plain>,
popup_id: window::Id,
} }
impl State { impl State {
@ -276,10 +366,11 @@ impl State {
}, },
menu: menu::State::default(), menu: menu::State::default(),
keyboard_modifiers: keyboard::Modifiers::default(), keyboard_modifiers: keyboard::Modifiers::default(),
is_open: false, is_open: Arc::new(AtomicBool::new(false)),
hovered_option: None, hovered_option: Arc::new(Mutex::new(None)),
selections: Vec::new(), selections: Vec::new(),
hashes: Vec::new(), hashes: Vec::new(),
popup_id: window::Id::unique(),
} }
} }
} }
@ -349,31 +440,111 @@ pub fn layout(
/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] /// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`]
/// accordingly. /// accordingly.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn update<'a, S: AsRef<str>, Message>( pub fn update<
'a,
S: AsRef<str> + Send + Sync + std::clone::Clone + 'static,
Message: Clone + 'static,
>(
event: &Event, event: &Event,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
on_selected: &dyn Fn(usize) -> Message, #[cfg(feature = "wayland")] on_open: Option<
Arc<
dyn Fn(
iced_runtime::platform_specific::wayland::popup::SctkPopupSettings,
DropdownView<Message>,
) -> Message
+ 'a,
>,
>,
#[cfg(feature = "wayland")]
positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
selected: Option<usize>, selected: Option<usize>,
selections: &[S], selections: &[S],
state: impl FnOnce() -> &'a mut State, state: impl FnOnce() -> &'a mut State,
window_id: Option<window::Id>,
on_close: Option<&'a dyn Fn(window::Id) -> Message>,
icons: &[icon::Handle],
gap: f32,
padding: Padding,
text_size: Option<f32>,
font: Option<crate::font::Font>,
selected_option: Option<usize>,
) -> event::Status { ) -> event::Status {
match event { match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => { | Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state(); let state = state();
let is_open = state.is_open.load(Ordering::Relaxed);
if state.is_open { if is_open {
// Event wasn't processed by overlay, so cursor was clicked either outside it's // 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. // bounds or on the drop-down, either way we close the overlay.
state.is_open = false; state.is_open.store(false, Ordering::Relaxed);
if let Some(on_close) = on_close {
shell.publish(on_close(state.popup_id));
}
event::Status::Captured event::Status::Captured
} else if cursor.is_over(layout.bounds()) { } else if cursor.is_over(layout.bounds()) {
state.is_open = true; state.is_open.store(true, Ordering::Relaxed);
state.hovered_option = selected; let mut hovered_guard = state.hovered_option.lock().unwrap();
*hovered_guard = selected;
let id = window::Id::unique();
state.popup_id = id;
#[cfg(feature = "wayland")]
if let Some((on_open, parent)) = on_open.zip(window_id) {
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();
shell.publish(on_open(
SctkPopupSettings {
parent,
id,
positioner: SctkPositioner {
size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)),
anchor_rect,
anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::Top,
gravity:cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::Bottom,
reactive: true,
..Default::default()
},
parent_size: None,
grab: true,
close_with_children: true
},
Arc::new(move || {
Element::from(
menu_widget(
bounds, &state, gap, padding,text_size.unwrap_or(14.0), selections.clone(), icons.clone(), selected_option, on_selected.clone()
)
)
}),
));
}
event::Status::Captured event::Status::Captured
} else { } else {
event::Status::Ignored event::Status::Ignored
@ -383,11 +554,9 @@ pub fn update<'a, S: AsRef<str>, Message>(
delta: mouse::ScrollDelta::Lines { .. }, delta: mouse::ScrollDelta::Lines { .. },
}) => { }) => {
let state = state(); let state = state();
let is_open = state.is_open.load(Ordering::Relaxed);
if state.keyboard_modifiers.command() if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
&& cursor.is_over(layout.bounds())
&& !state.is_open
{
let next_index = selected.map(|index| index + 1).unwrap_or_default(); let next_index = selected.map(|index| index + 1).unwrap_or_default();
if selections.len() < next_index { if selections.len() < next_index {
@ -423,9 +592,70 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In
} }
} }
#[cfg(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>,
) -> 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,
)
.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`]. /// Returns the current overlay of a [`Dropdown`].
#[allow(clippy::too_many_arguments)] #[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<'_>, layout: Layout<'_>,
_renderer: &crate::Renderer, _renderer: &crate::Renderer,
state: &'a mut State, state: &'a mut State,
@ -439,18 +669,21 @@ pub fn overlay<'a, S: AsRef<str>, Message: 'a>(
selected_option: Option<usize>, selected_option: Option<usize>,
on_selected: &'a dyn Fn(usize) -> Message, on_selected: &'a dyn Fn(usize) -> Message,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>> { ) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
if state.is_open { where
[S]: std::borrow::ToOwned,
{
if state.is_open.load(Ordering::Relaxed) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let menu = Menu::new( let menu = Menu::new(
&mut state.menu, state.menu.clone(),
selections, Cow::Borrowed(selections),
icons, Cow::Borrowed(icons),
&mut state.hovered_option, state.hovered_option.clone(),
selected_option, selected_option,
|option| { |option| {
state.is_open = false; state.is_open.store(false, Ordering::Relaxed);
(on_selected)(option) (on_selected)(option)
}, },

View file

@ -306,7 +306,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
Density::Spacious => 48.0, Density::Spacious => 48.0,
Density::Standard => 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. // Creates the headerbar widget.
let mut widget = widget::row::with_capacity(3) let mut widget = widget::row::with_capacity(3)
// If elements exist in the start region, append them here. // 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) .align_y(iced::Alignment::Center)
.apply(widget::container) .apply(widget::container)
.align_x(iced::Alignment::Start) .align_x(iced::Alignment::Start)
.width(Length::Shrink), .width(Length::FillPortion(portion)),
) )
// If elements exist in the center region, use them here. // If elements exist in the center region, use them here.
// This will otherwise use the title as a widget if a title was defined. // 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) .align_y(iced::Alignment::Center)
.apply(widget::container) .apply(widget::container)
.align_x(iced::Alignment::End) .align_x(iced::Alignment::End)
.width(Length::Shrink), .width(if center_empty {
Length::Fill
} else {
Length::FillPortion(portion)
}),
) )
.align_y(iced::Alignment::Center) .align_y(iced::Alignment::Center)
.height(Length::Fixed(height)) .height(Length::Fixed(height))

View file

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

View file

@ -84,7 +84,7 @@ where
.map(|root| { .map(|root| {
let mut tree = Tree::empty(); let mut tree = Tree::empty();
let flat = root let flat = root
.flattern() .flatten()
.iter() .iter()
.map(|mt| Tree::new(mt.item.as_widget())) .map(|mt| Tree::new(mt.item.as_widget()))
.collect(); .collect();
@ -110,7 +110,7 @@ pub(crate) fn menu_roots_diff<'a, Message, Renderer>(
.zip(menu_roots.iter()) .zip(menu_roots.iter())
.for_each(|(t, root)| { .for_each(|(t, root)| {
let mut flat = root let mut flat = root
.flattern() .flatten()
.iter() .iter()
.map(|mt| { .map(|mt| {
let widget = mt.item.as_widget(); let widget = mt.item.as_widget();
@ -129,7 +129,7 @@ pub(crate) fn menu_roots_diff<'a, Message, Renderer>(
let extended = menu_roots[tree.children.len()..].iter().map(|root| { let extended = menu_roots[tree.children.len()..].iter().map(|root| {
let mut tree = Tree::empty(); let mut tree = Tree::empty();
let flat = root let flat = root
.flattern() .flatten()
.iter() .iter()
.map(|mt| Tree::new(mt.item.as_widget())) .map(|mt| Tree::new(mt.item.as_widget()))
.collect(); .collect();
@ -366,6 +366,8 @@ where
if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) {
state.view_cursor = view_cursor; state.view_cursor = view_cursor;
state.open = true; state.open = true;
// #[cfg(feature = "wayland")]
// TODO emit Message to open menu
} }
} }
_ => (), _ => (),
@ -437,6 +439,9 @@ where
_renderer: &Renderer, _renderer: &Renderer,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, crate::Theme, Renderer>> {
// #[cfg(feature = "wayland")]
// return None;
let state = tree.state.downcast_ref::<MenuBarState>(); let state = tree.state.downcast_ref::<MenuBarState>();
if !state.open { if !state.open {
return None; return None;

View file

@ -1,8 +1,13 @@
// From iced_aw, license MIT // From iced_aw, license MIT
//! Menu tree overlay //! Menu tree overlay
use std::{
borrow::Cow,
sync::{Arc, Mutex},
};
use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use super::{menu_bar::MenuBarState, menu_tree::MenuTree};
use crate::style::menu_bar::StyleSheet; use crate::{style::menu_bar::StyleSheet, widget::dropdown};
use iced_core::{Border, Shadow}; use iced_core::{Border, Shadow};
use iced_widget::core::{ use iced_widget::core::{
@ -240,7 +245,7 @@ struct MenuBounds {
impl MenuBounds { impl MenuBounds {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn new<Message, Renderer>( fn new<Message, Renderer>(
menu_tree: &MenuTree<'_, Message, Renderer>, menu_tree: &MenuTree<Message>,
renderer: &Renderer, renderer: &Renderer,
item_width: ItemWidth, item_width: ItemWidth,
item_height: ItemHeight, item_height: ItemHeight,
@ -300,7 +305,7 @@ impl MenuState {
overlay_offset: Vector, overlay_offset: Vector,
slice: MenuSlice, slice: MenuSlice,
renderer: &Renderer, renderer: &Renderer,
menu_tree: &MenuTree<'_, Message, Renderer>, menu_tree: &MenuTree<Message>,
tree: &mut [Tree], tree: &mut [Tree],
) -> Node ) -> Node
where where
@ -354,7 +359,7 @@ impl MenuState {
overlay_offset: Vector, overlay_offset: Vector,
index: usize, index: usize,
renderer: &Renderer, renderer: &Renderer,
menu_tree: &MenuTree<'_, Message, Renderer>, menu_tree: &MenuTree<Message>,
tree: &mut Tree, tree: &mut Tree,
) -> Node ) -> Node
where where
@ -427,12 +432,9 @@ impl MenuState {
} }
} }
pub(crate) struct Menu<'a, 'b, Message, Renderer> pub(crate) struct Menu<'b, Message: std::clone::Clone> {
where pub(crate) tree: dropdown::menu::State,
Renderer: renderer::Renderer, pub(crate) menu_roots: Cow<'b, Vec<MenuTree<Message>>>,
{
pub(crate) tree: &'b mut Tree,
pub(crate) menu_roots: &'b mut Vec<MenuTree<'a, Message, Renderer>>,
pub(crate) bounds_expand: u16, pub(crate) bounds_expand: u16,
/// Allows menu overlay items to overlap the parent /// Allows menu overlay items to overlap the parent
pub(crate) menu_overlays_parent: bool, pub(crate) menu_overlays_parent: bool,
@ -444,10 +446,10 @@ where
pub(crate) cross_offset: i32, pub(crate) cross_offset: i32,
pub(crate) root_bounds_list: Vec<Rectangle>, pub(crate) root_bounds_list: Vec<Rectangle>,
pub(crate) path_highlight: Option<PathHighlight>, pub(crate) path_highlight: Option<PathHighlight>,
pub(crate) style: &'b <crate::Theme as StyleSheet>::Style, pub(crate) style: Cow<'b, <crate::Theme as StyleSheet>::Style>,
pub(crate) position: Point, pub(crate) position: Point,
} }
impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer> impl<'b, Message: Clone + 'static, Renderer> Menu<'b, Message>
where where
Renderer: renderer::Renderer, Renderer: renderer::Renderer,
{ {
@ -751,7 +753,7 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle {
} }
pub(super) fn init_root_menu<Message, Renderer>( pub(super) fn init_root_menu<Message, Renderer>(
menu: &mut Menu<'_, '_, Message, Renderer>, menu: &mut Menu<'_, Message>,
renderer: &Renderer, renderer: &Renderer,
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
overlay_cursor: Point, overlay_cursor: Point,
@ -829,7 +831,7 @@ pub(super) fn init_root_menu<Message, Renderer>(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn process_menu_events<'b, Message, Renderer>( fn process_menu_events<'b, Message, Renderer>(
tree: &'b mut Tree, tree: &'b mut Tree,
menu_roots: &'b mut [MenuTree<'_, Message, Renderer>], menu_roots: &'b mut [MenuTree<Message>],
event: event::Event, event: event::Event,
view_cursor: Cursor, view_cursor: Cursor,
renderer: &Renderer, renderer: &Renderer,
@ -887,7 +889,7 @@ where
#[allow(unused_results)] #[allow(unused_results)]
fn process_overlay_events<Message, Renderer>( fn process_overlay_events<Message, Renderer>(
menu: &mut Menu<'_, '_, Message, Renderer>, menu: &mut Menu<'_, Message>,
renderer: &Renderer, renderer: &Renderer,
viewport_size: Size, viewport_size: Size,
overlay_offset: Vector, overlay_offset: Vector,
@ -897,6 +899,7 @@ fn process_overlay_events<Message, Renderer>(
) -> event::Status ) -> event::Status
where where
Renderer: renderer::Renderer, Renderer: renderer::Renderer,
Message: std::clone::Clone,
{ {
use event::Status::{Captured, Ignored}; use event::Status::{Captured, Ignored};
/* /*
@ -1077,16 +1080,13 @@ where
Captured Captured
} }
fn process_scroll_events<Message, Renderer>( fn process_scroll_events<Message: Clone>(
menu: &mut Menu<'_, '_, Message, Renderer>, menu: &mut Menu<'_, Message>,
delta: mouse::ScrollDelta, delta: mouse::ScrollDelta,
overlay_cursor: Point, overlay_cursor: Point,
viewport_size: Size, viewport_size: Size,
overlay_offset: Vector, overlay_offset: Vector,
) -> event::Status ) -> event::Status {
where
Renderer: renderer::Renderer,
{
use event::Status::{Captured, Ignored}; use event::Status::{Captured, Ignored};
use mouse::ScrollDelta; use mouse::ScrollDelta;
@ -1155,16 +1155,13 @@ where
#[allow(clippy::pedantic)] #[allow(clippy::pedantic)]
/// Returns (children_size, child_positions, child_sizes) /// Returns (children_size, child_positions, child_sizes)
fn get_children_layout<Message, Renderer>( fn get_children_layout<Message>(
menu_tree: &MenuTree<'_, Message, Renderer>, menu_tree: &MenuTree<Message>,
renderer: &Renderer, renderer: &crate::Renderer,
item_width: ItemWidth, item_width: ItemWidth,
item_height: ItemHeight, item_height: ItemHeight,
tree: &mut [Tree], tree: &mut [Tree],
) -> (Size, Vec<f32>, Vec<Size>) ) -> (Size, Vec<f32>, Vec<Size>) {
where
Renderer: renderer::Renderer,
{
let width = match item_width { let width = match item_width {
ItemWidth::Uniform(u) => f32::from(u), ItemWidth::Uniform(u) => f32::from(u),
ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)), ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)),
@ -1184,7 +1181,8 @@ where
.children .children
.iter() .iter()
.map(|mt| { .map(|mt| {
let w = mt.item.as_widget(); let guard = mt.item.0.lock().unwrap();
let w = guard.as_widget();
match w.size().height { match w.size().height {
Length::Fixed(f) => Size::new(width, f), Length::Fixed(f) => Size::new(width, f),
Length::Shrink => { Length::Shrink => {

View file

@ -5,13 +5,14 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, Mutex};
use iced_widget::core::{renderer, Element}; use iced_widget::core::{renderer, Element};
use crate::iced_core::{Alignment, Length}; use crate::iced_core::{Alignment, Length};
use crate::widget::icon;
use crate::widget::menu::action::MenuAction; use crate::widget::menu::action::MenuAction;
use crate::widget::menu::key_bind::KeyBind; use crate::widget::menu::key_bind::KeyBind;
use crate::widget::{icon, ArcElementWrapper};
use crate::{theme, widget}; use crate::{theme, widget};
/// Nested menu is essentially a tree of items, a menu is a collection of items /// Nested menu is essentially a tree of items, a menu is a collection of items
@ -23,27 +24,27 @@ use crate::{theme, widget};
/// but there's no need to explicitly distinguish them here, if a menu tree /// but there's no need to explicitly distinguish them here, if a menu tree
/// has children, it's a menu, otherwise it's an item /// has children, it's a menu, otherwise it's an item
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct MenuTree<'a, Message, Renderer = crate::Renderer> { #[derive(Clone)]
pub struct MenuTree<Message> {
/// The menu tree will be flatten into a vector to build a linear widget tree, /// The menu tree will be flatten into a vector to build a linear widget tree,
/// the `index` field is the index of the item in that vector /// the `index` field is the index of the item in that vector
pub(crate) index: usize, pub(crate) index: usize,
// TODO needs to be wrapped in Arc<Mutex kinda like in cosmic-files. Check the widget there for details
/// The item of the menu tree /// The item of the menu tree
pub(crate) item: Element<'a, Message, crate::Theme, Renderer>, pub(crate) item: ArcElementWrapper<Message>,
/// The children of the menu tree /// The children of the menu tree
pub(crate) children: Vec<MenuTree<'a, Message, Renderer>>, pub(crate) children: Vec<MenuTree<Message>>,
/// The width of the menu tree /// The width of the menu tree
pub(crate) width: Option<u16>, pub(crate) width: Option<u16>,
/// The height of the menu tree /// The height of the menu tree
pub(crate) height: Option<u16>, pub(crate) height: Option<u16>,
} }
impl<'a, Message, Renderer> MenuTree<'a, Message, Renderer> impl<Message: Clone + 'static> MenuTree<Message> {
where
Renderer: renderer::Renderer,
{
/// Create a new menu tree from a widget /// Create a new menu tree from a widget
pub fn new(item: impl Into<Element<'a, Message, crate::Theme, Renderer>>) -> Self { pub fn new(item: impl Into<ArcElementWrapper<Message>>) -> Self {
Self { Self {
index: 0, index: 0,
item: item.into(), item: item.into(),
@ -55,8 +56,8 @@ where
/// Create a menu tree from a widget and a vector of sub trees /// Create a menu tree from a widget and a vector of sub trees
pub fn with_children( pub fn with_children(
item: impl Into<Element<'a, Message, crate::Theme, Renderer>>, item: impl Into<ArcElementWrapper<Message>>,
children: Vec<impl Into<MenuTree<'a, Message, Renderer>>>, children: Vec<impl Into<MenuTree<Message>>>,
) -> Self { ) -> Self {
Self { Self {
index: 0, index: 0,
@ -92,7 +93,7 @@ where
/// Set the index of each item /// Set the index of each item
pub(crate) fn set_index(&mut self) { pub(crate) fn set_index(&mut self) {
/// inner counting function. /// inner counting function.
fn rec<Message, Renderer>(mt: &mut MenuTree<'_, Message, Renderer>, count: &mut usize) { fn rec<Message: Clone + 'static>(mt: &mut MenuTree<Message>, count: &mut usize) {
// keep items under the same menu line up // keep items under the same menu line up
mt.children.iter_mut().for_each(|c| { mt.children.iter_mut().for_each(|c| {
c.index = *count; c.index = *count;
@ -109,36 +110,32 @@ where
} }
/// Flatten the menu tree /// Flatten the menu tree
pub(crate) fn flattern(&'a self) -> Vec<&Self> { pub(crate) fn flatten(&self) -> Vec<Self> {
/// Inner flattening function /// Inner flattening function
fn rec<'a, Message, Renderer>( fn rec<Message: Clone + 'static>(
mt: &'a MenuTree<'a, Message, Renderer>, mt: &MenuTree<Message>,
flat: &mut Vec<&MenuTree<'a, Message, Renderer>>, flat: &mut Vec<MenuTree<Message>>,
) { ) {
mt.children.iter().for_each(|c| { mt.children.clone().into_iter().for_each(|c| {
flat.push(c); flat.push(c);
}); });
mt.children.iter().for_each(|c| { mt.children.iter().for_each(|c| {
rec(c, flat); rec(&c, flat);
}); });
} }
let mut flat = Vec::new(); let mut flat = Vec::new();
flat.push(self); flat.push(self.clone());
rec(self, &mut flat); rec(self, &mut flat);
flat flat
} }
} }
impl<'a, Message, Renderer> From<Element<'a, Message, crate::Theme, Renderer>> impl<Message: Clone + 'static> From<crate::Element<'static, Message>> for MenuTree<Message> {
for MenuTree<'a, Message, Renderer> fn from(value: crate::Element<'static, Message>) -> Self {
where Self::new(ArcElementWrapper(Arc::new(Mutex::new(value))))
Renderer: renderer::Renderer,
{
fn from(value: Element<'a, Message, crate::Theme, Renderer>) -> Self {
Self::new(value)
} }
} }
@ -157,6 +154,7 @@ pub fn menu_button<'a, Message: 'a>(
.class(theme::Button::MenuItem) .class(theme::Button::MenuItem)
} }
#[derive(Clone)]
/// Represents a menu item that performs an action when selected or a separator between menu items. /// Represents a menu item that performs an action when selected or a separator between menu items.
/// ///
/// - `Action` - Represents a menu item that performs an action when selected. /// - `Action` - Represents a menu item that performs an action when selected.
@ -213,18 +211,13 @@ where
/// # Returns /// # Returns
/// - A vector of `MenuTree`. /// - A vector of `MenuTree`.
pub fn menu_items< pub fn menu_items<
'a,
A: MenuAction<Message = Message>, A: MenuAction<Message = Message>,
L: Into<Cow<'static, str>> + 'static, L: Into<Cow<'static, str>> + 'static,
Message: 'a, Message: 'static + std::clone::Clone,
Renderer: renderer::Renderer + 'a,
>( >(
key_binds: &HashMap<KeyBind, A>, key_binds: &HashMap<KeyBind, A>,
children: Vec<MenuItem<A, L>>, children: Vec<MenuItem<A, L>>,
) -> Vec<MenuTree<'a, Message, Renderer>> ) -> Vec<MenuTree<Message>> {
where
Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>,
{
fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String { fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
for (key_bind, key_action) in key_binds { for (key_bind, key_action) in key_binds {
if action == key_action { if action == key_action {
@ -259,7 +252,7 @@ where
let menu_button = menu_button(items).on_press(action.message()); let menu_button = menu_button(items).on_press(action.message());
trees.push(MenuTree::<Message, Renderer>::new(menu_button)); trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
} }
MenuItem::ButtonDisabled(label, icon, action) => { MenuItem::ButtonDisabled(label, icon, action) => {
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
@ -277,7 +270,7 @@ where
let menu_button = menu_button(items); let menu_button = menu_button(items);
trees.push(MenuTree::<Message, Renderer>::new(menu_button)); trees.push(MenuTree::<Message>::from(Element::from(menu_button)));
} }
MenuItem::CheckBox(label, icon, value, action) => { MenuItem::CheckBox(label, icon, value, action) => {
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
@ -307,11 +300,14 @@ where
items.insert(2, widget::icon::icon(icon).size(14).into()); items.insert(2, widget::icon::icon(icon).size(14).into());
} }
trees.push(MenuTree::new(menu_button(items).on_press(action.message()))); trees.push(MenuTree::from(Element::from(
menu_button(items).on_press(action.message()),
)));
} }
MenuItem::Folder(label, children) => { MenuItem::Folder(label, children) => {
trees.push(MenuTree::<Message, Renderer>::with_children( trees.push(MenuTree::<Message>::with_children(
menu_button(vec![ ArcElementWrapper(Arc::new(Mutex::new(crate::Element::from(
menu_button::<'static, _>(vec![
widget::text(label).into(), widget::text(label).into(),
widget::horizontal_space().into(), widget::horizontal_space().into(),
widget::icon::from_name("pan-end-symbolic") widget::icon::from_name("pan-end-symbolic")
@ -329,14 +325,15 @@ where
theme::Button::MenuFolder theme::Button::MenuFolder
}, },
), ),
)))),
menu_items(key_binds, children), menu_items(key_binds, children),
)); ));
} }
MenuItem::Divider => { MenuItem::Divider => {
if i != size - 1 { if i != size - 1 {
trees.push(MenuTree::<Message, Renderer>::new( trees.push(MenuTree::<Message>::from(Element::from(
widget::divider::horizontal::light(), widget::divider::horizontal::light(),
)); )));
} }
} }
} }

View file

@ -97,6 +97,9 @@ pub mod aspect_ratio;
#[cfg(feature = "autosize")] #[cfg(feature = "autosize")]
pub mod autosize; pub mod autosize;
#[cfg(feature = "winit")]
pub(crate) mod responsive_container;
pub mod button; pub mod button;
#[doc(inline)] #[doc(inline)]
pub use button::{Button, IconButton, LinkButton, TextButton}; pub use button::{Button, IconButton, LinkButton, TextButton};
@ -366,6 +369,10 @@ pub mod warning;
#[doc(inline)] #[doc(inline)]
pub use warning::*; pub use warning::*;
pub mod wrapper;
#[doc(inline)]
pub use wrapper::*;
#[cfg(feature = "markdown")] #[cfg(feature = "markdown")]
#[doc(inline)] #[doc(inline)]
pub use iced::widget::markdown; pub use iced::widget::markdown;

View file

@ -67,7 +67,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> {
self self
} }
pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self { pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<Message>>>) -> Self {
self.segmented_button = self.segmented_button.context_menu(context_menu); self.segmented_button = self.segmented_button.context_menu(context_menu);
self self
} }

View file

@ -0,0 +1,293 @@
//! Autosize Container, which will resize the window to its contents.
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};
use crate::app::message::SurfaceMessage;
pub(crate) fn responsive_container<'a, Message: 'static + From<SurfaceMessage>, Theme, E>(
content: E,
id: Id,
) -> 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)
}
/// 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>,
}
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) -> Self
where
T: Into<Element<'a, Message, Theme, Renderer>>,
{
ResponsiveContainer {
content: content.into(),
id,
size: None,
}
}
pub(crate) fn size(mut self, size: Size) -> Self {
self.size = Some(size);
self
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ResponsiveContainer<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
Message: From<SurfaceMessage>,
{
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(Message::from(SurfaceMessage::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 + From<SurfaceMessage>,
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

@ -9,7 +9,7 @@ use crate::widget::menu::{
self, menu_roots_children, menu_roots_diff, CloseCondition, ItemHeight, ItemWidth, self, menu_roots_children, menu_roots_diff, CloseCondition, ItemHeight, ItemWidth,
MenuBarState, PathHighlight, MenuBarState, PathHighlight,
}; };
use crate::widget::{icon, Icon}; use crate::widget::{dropdown, icon, Icon};
use crate::{Element, Renderer}; use crate::{Element, Renderer};
use derive_setters::Setters; use derive_setters::Setters;
use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent};
@ -127,7 +127,7 @@ where
pub(super) style: Style, pub(super) style: Style,
/// The context menu to display when a context is activated /// The context menu to display when a context is activated
#[setters(skip)] #[setters(skip)]
pub(super) context_menu: Option<Vec<menu::Tree<'a, Message, crate::Renderer>>>, pub(super) context_menu: Option<Vec<menu::Tree<Message>>>,
/// Emits the ID of the item that was activated. /// Emits the ID of the item that was activated.
#[setters(skip)] #[setters(skip)]
pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message + 'static>>, pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
@ -197,13 +197,13 @@ where
} }
} }
pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<'a, Message>>>) -> Self pub fn context_menu(mut self, context_menu: Option<Vec<menu::Tree<Message>>>) -> Self
where where
Message: 'static, Message: Clone + 'static,
{ {
self.context_menu = context_menu.map(|menus| { self.context_menu = context_menu.map(|menus| {
vec![menu::Tree::with_children( vec![menu::Tree::with_children(
crate::widget::row::<'static, Message>(), crate::Element::from(crate::widget::row::<'static, Message>()),
menus, menus,
)] )]
}); });
@ -592,6 +592,7 @@ where
wheel_timestamp: Default::default(), wheel_timestamp: Default::default(),
dnd_state: Default::default(), dnd_state: Default::default(),
fingers_pressed: Default::default(), fingers_pressed: Default::default(),
menu_state: Default::default(),
}) })
} }
@ -1537,7 +1538,7 @@ where
translation: Vector, translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> { ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, Renderer>> {
let state = tree.state.downcast_ref::<LocalState>(); let state = tree.state.downcast_ref::<LocalState>();
let menu_state = state.menu_state.clone();
let Some(entity) = state.show_context else { let Some(entity) = state.show_context else {
return None; return None;
}; };
@ -1565,8 +1566,8 @@ where
Some( Some(
crate::widget::menu::Menu { crate::widget::menu::Menu {
tree: &mut tree.children[0], tree: menu_state,
menu_roots: context_menu, menu_roots: Cow::Borrowed(context_menu),
bounds_expand: 16, bounds_expand: 16,
menu_overlays_parent: true, menu_overlays_parent: true,
close_condition: CloseCondition { close_condition: CloseCondition {
@ -1581,7 +1582,7 @@ where
cross_offset: 0, cross_offset: 0,
root_bounds_list: vec![bounds], root_bounds_list: vec![bounds],
path_highlight: Some(PathHighlight::MenuActive), path_highlight: Some(PathHighlight::MenuActive),
style: &crate::theme::menu_bar::MenuBarStyle::Default, style: Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default),
position: Point::new(translation.x, translation.y), position: Point::new(translation.x, translation.y),
} }
.overlay(), .overlay(),
@ -1666,6 +1667,8 @@ pub struct LocalState {
pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>, pub dnd_state: crate::widget::dnd_destination::State<Option<Entity>>,
/// Tracks multi-touch events /// Tracks multi-touch events
fingers_pressed: HashSet<Finger>, fingers_pressed: HashSet<Finger>,
/// Menu state
pub(crate) menu_state: dropdown::menu::State,
} }
#[derive(Debug, Default, PartialEq)] #[derive(Debug, Default, PartialEq)]

View file

155
src/widget/wrapper.rs Normal file
View file

@ -0,0 +1,155 @@
use std::sync::{Arc, Mutex};
use crate::Element;
use iced::{event, Length, Rectangle, Size};
use iced_core::{id::Id, widget, widget::tree, Widget};
#[derive(Clone)]
pub struct ArcElementWrapper<M>(pub Arc<Mutex<Element<'static, M>>>);
impl<M> Widget<M, crate::Theme, crate::Renderer> for ArcElementWrapper<M> {
fn size(&self) -> Size<Length> {
self.0.lock().unwrap().as_widget().size()
}
fn size_hint(&self) -> Size<Length> {
self.0.lock().unwrap().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.0
.lock()
.unwrap()
.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.0
.lock()
.unwrap()
.as_widget()
.draw(tree, renderer, theme, style, layout, cursor, viewport)
}
fn tag(&self) -> tree::Tag {
self.0.lock().unwrap().as_widget().tag()
}
fn state(&self) -> tree::State {
self.0.lock().unwrap().as_widget().state()
}
fn children(&self) -> Vec<tree::Tree> {
self.0.lock().unwrap().as_widget().children()
}
fn diff(&mut self, tree: &mut tree::Tree) {
self.0.lock().unwrap().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.0
.lock()
.unwrap()
.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.0.lock().unwrap().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.0
.lock()
.unwrap()
.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>> {
// TODO
None
}
fn id(&self) -> Option<Id> {
self.0.lock().unwrap().as_widget().id()
}
fn set_id(&mut self, _id: Id) {
self.0.lock().unwrap().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.0.lock().unwrap().as_widget().drag_destinations(
_state,
_layout,
renderer,
_dnd_rectangles,
)
}
}
impl<Message: 'static> From<ArcElementWrapper<Message>> for Element<'static, Message> {
fn from(wrapper: ArcElementWrapper<Message>) -> Self {
Element::new(wrapper)
}
}
impl<Message: 'static> From<Element<'static, Message>> for ArcElementWrapper<Message> {
fn from(e: Element<'static, Message>) -> Self {
ArcElementWrapper(Arc::new(Mutex::new(e)))
}
}