summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml1
-rw-r--r--core/Cargo.toml1
-rw-r--r--core/src/keyboard.rs2
-rw-r--r--core/src/keyboard/hotkey.rs18
-rw-r--r--core/src/keyboard/modifiers.rs76
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/menu.rs145
-rw-r--r--examples/game_of_life/src/main.rs8
-rw-r--r--examples/game_of_life/src/preset.rs13
-rw-r--r--examples/menu/Cargo.toml10
-rw-r--r--examples/menu/src/main.rs117
-rw-r--r--examples/pane_grid/src/main.rs2
-rw-r--r--glutin/Cargo.toml2
-rw-r--r--glutin/src/application.rs23
-rw-r--r--native/src/lib.rs4
-rw-r--r--native/src/program.rs2
-rw-r--r--native/src/widget/text_input.rs35
-rw-r--r--src/application.rs15
-rw-r--r--src/lib.rs6
-rw-r--r--src/sandbox.rs2
-rw-r--r--web/src/lib.rs4
-rw-r--r--winit/Cargo.toml2
-rw-r--r--winit/src/application.rs19
-rw-r--r--winit/src/application/state.rs19
-rw-r--r--winit/src/conversion.rs296
25 files changed, 749 insertions, 75 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 329877c8..5b1cfb0e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -85,6 +85,7 @@ members = [
"examples/tour",
"examples/tooltip",
"examples/url_handler",
+ "examples/menu",
]
[dependencies]
diff --git a/core/Cargo.toml b/core/Cargo.toml
index 88f257b7..54d927af 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -8,6 +8,7 @@ license = "MIT"
repository = "https://github.com/hecrj/iced"
[dependencies]
+bitflags = "1.2"
[dependencies.palette]
version = "0.5.0"
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs
index 61e017ad..cb64701a 100644
--- a/core/src/keyboard.rs
+++ b/core/src/keyboard.rs
@@ -1,8 +1,10 @@
//! Reuse basic keyboard types.
mod event;
+mod hotkey;
mod key_code;
mod modifiers;
pub use event::Event;
+pub use hotkey::Hotkey;
pub use key_code::KeyCode;
pub use modifiers::Modifiers;
diff --git a/core/src/keyboard/hotkey.rs b/core/src/keyboard/hotkey.rs
new file mode 100644
index 00000000..310ef286
--- /dev/null
+++ b/core/src/keyboard/hotkey.rs
@@ -0,0 +1,18 @@
+use crate::keyboard::{KeyCode, Modifiers};
+
+/// Representation of a hotkey, consists on the combination of a [`KeyCode`] and [`Modifiers`].
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Hotkey {
+ /// The key that represents this hotkey.
+ pub key: KeyCode,
+
+ /// The list of modifiers that represents this hotkey.
+ pub modifiers: Modifiers,
+}
+
+impl Hotkey {
+ /// Creates a new [`Hotkey`] with the given [`Modifiers`] and [`KeyCode`].
+ pub fn new(modifiers: Modifiers, key: KeyCode) -> Self {
+ Self { modifiers, key }
+ }
+}
diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs
index d2a0500e..383b9370 100644
--- a/core/src/keyboard/modifiers.rs
+++ b/core/src/keyboard/modifiers.rs
@@ -1,20 +1,53 @@
-/// The current state of the keyboard modifiers.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct Modifiers {
- /// Whether a shift key is pressed
- pub shift: bool,
+use bitflags::bitflags;
- /// Whether a control key is pressed
- pub control: bool,
-
- /// Whether an alt key is pressed
- pub alt: bool,
-
- /// Whether a logo key is pressed (e.g. windows key, command key...)
- pub logo: bool,
+bitflags! {
+ /// The current state of the keyboard modifiers.
+ #[derive(Default)]
+ pub struct Modifiers: u32{
+ /// The "shift" key.
+ const SHIFT = 0b100 << 0;
+ // const LSHIFT = 0b010 << 0;
+ // const RSHIFT = 0b001 << 0;
+ //
+ /// The "control" key.
+ const CTRL = 0b100 << 3;
+ // const LCTRL = 0b010 << 3;
+ // const RCTRL = 0b001 << 3;
+ //
+ /// The "alt" key.
+ const ALT = 0b100 << 6;
+ // const LALT = 0b010 << 6;
+ // const RALT = 0b001 << 6;
+ //
+ /// The "windows" key on Windows, "command" key on Mac, and
+ /// "super" key on Linux.
+ const LOGO = 0b100 << 9;
+ // const LLOGO = 0b010 << 9;
+ // const RLOGO = 0b001 << 9;
+ }
}
impl Modifiers {
+ /// Returns true if the [`SHIFT`] key is pressed in the [`Modifiers`].
+ pub fn shift(self) -> bool {
+ self.contains(Self::SHIFT)
+ }
+
+ /// Returns true if the [`CTRL`] key is pressed in the [`Modifiers`].
+ pub fn control(self) -> bool {
+ self.contains(Self::CTRL)
+ }
+
+ /// Returns true if the [`ALT`] key is pressed in the [`Modifiers`].
+ pub fn alt(self) -> bool {
+ self.contains(Self::ALT)
+ }
+
+ /// Returns true if the [`LOGO`] key is pressed in the [`Modifiers`].
+ pub fn logo(self) -> bool {
+ self.contains(Self::LOGO)
+ }
+
/// Returns true if a "command key" is pressed in the [`Modifiers`].
///
/// The "command key" is the main modifier key used to issue commands in the
@@ -22,24 +55,13 @@ impl Modifiers {
///
/// - It is the `logo` or command key (⌘) on macOS
/// - It is the `control` key on other platforms
- pub fn is_command_pressed(self) -> bool {
+ pub fn command(self) -> bool {
#[cfg(target_os = "macos")]
- let is_pressed = self.logo;
+ let is_pressed = self.logo();
#[cfg(not(target_os = "macos"))]
- let is_pressed = self.control;
+ let is_pressed = self.control();
is_pressed
}
-
- /// Returns true if the current [`Modifiers`] have at least the same
- /// keys pressed as the provided ones, and false otherwise.
- pub fn matches(&self, modifiers: Self) -> bool {
- let shift = !modifiers.shift || self.shift;
- let control = !modifiers.control || self.control;
- let alt = !modifiers.alt || self.alt;
- let logo = !modifiers.logo || self.logo;
-
- shift && control && alt && logo
- }
}
diff --git a/core/src/lib.rs b/core/src/lib.rs
index 6453d599..c4288158 100644
--- a/core/src/lib.rs
+++ b/core/src/lib.rs
@@ -15,6 +15,7 @@
#![forbid(unsafe_code)]
#![forbid(rust_2018_idioms)]
pub mod keyboard;
+pub mod menu;
pub mod mouse;
mod align;
@@ -33,6 +34,7 @@ pub use background::Background;
pub use color::Color;
pub use font::Font;
pub use length::Length;
+pub use menu::Menu;
pub use padding::Padding;
pub use point::Point;
pub use rectangle::Rectangle;
diff --git a/core/src/menu.rs b/core/src/menu.rs
new file mode 100644
index 00000000..8a679085
--- /dev/null
+++ b/core/src/menu.rs
@@ -0,0 +1,145 @@
+//! Build menus for your application.
+use crate::keyboard::Hotkey;
+
+/// Menu representation.
+///
+/// This can be used by `shell` implementations to create a menu.
+#[derive(Debug, Clone)]
+pub struct Menu<Message> {
+ entries: Vec<Entry<Message>>,
+}
+
+impl<Message> PartialEq for Menu<Message> {
+ fn eq(&self, other: &Self) -> bool {
+ self.entries == other.entries
+ }
+}
+
+impl<Message> Menu<Message> {
+ /// Creates an empty [`Menu`].
+ pub fn new() -> Self {
+ Self::with_entries(Vec::new())
+ }
+
+ /// Creates a new [`Menu`] with the given entries.
+ pub fn with_entries(entries: Vec<Entry<Message>>) -> Self {
+ Self { entries }
+ }
+
+ /// Returns a [`MenuEntry`] iterator.
+ pub fn iter(&self) -> impl Iterator<Item = &Entry<Message>> {
+ self.entries.iter()
+ }
+
+ /// Adds an [`Entry`] to the [`Menu`].
+ pub fn push(mut self, entry: Entry<Message>) -> Self {
+ self.entries.push(entry);
+ self
+ }
+
+ /// Maps the `Message` of the [`Menu`] using the provided function.
+ ///
+ /// This is useful to compose menus and split them into different
+ /// abstraction levels.
+ pub fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Menu<B> {
+ // TODO: Use a boxed trait to avoid reallocation of entries
+ Menu {
+ entries: self
+ .entries
+ .into_iter()
+ .map(|entry| entry.map(f))
+ .collect(),
+ }
+ }
+}
+
+/// Represents one of the possible entries used to build a [`Menu`].
+#[derive(Debug, Clone)]
+pub enum Entry<Message> {
+ /// Item for a [`Menu`]
+ Item {
+ /// The title of the item
+ title: String,
+ /// The [`Hotkey`] to activate the item, if any
+ hotkey: Option<Hotkey>,
+ /// The message generated when the item is activated
+ on_activation: Message,
+ },
+ /// Dropdown for a [`Menu`]
+ Dropdown {
+ /// Title of the dropdown
+ title: String,
+ /// The submenu of the dropdown
+ submenu: Menu<Message>,
+ },
+ /// Separator for a [`Menu`]
+ Separator,
+}
+
+impl<Message> Entry<Message> {
+ /// Creates an [`Entry::Item`].
+ pub fn item<S: Into<String>>(
+ title: S,
+ hotkey: impl Into<Option<Hotkey>>,
+ on_activation: Message,
+ ) -> Self {
+ let title = title.into();
+ let hotkey = hotkey.into();
+
+ Self::Item {
+ title,
+ hotkey,
+ on_activation,
+ }
+ }
+
+ /// Creates an [`Entry::Dropdown`].
+ pub fn dropdown<S: Into<String>>(title: S, submenu: Menu<Message>) -> Self {
+ let title = title.into();
+
+ Self::Dropdown { title, submenu }
+ }
+
+ fn map<B>(self, f: impl Fn(Message) -> B + Copy) -> Entry<B> {
+ match self {
+ Self::Item {
+ title,
+ hotkey,
+ on_activation,
+ } => Entry::Item {
+ title,
+ hotkey,
+ on_activation: f(on_activation),
+ },
+ Self::Dropdown { title, submenu } => Entry::Dropdown {
+ title,
+ submenu: submenu.map(f),
+ },
+ Self::Separator => Entry::Separator,
+ }
+ }
+}
+
+impl<Message> PartialEq for Entry<Message> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (
+ Entry::Item { title, hotkey, .. },
+ Entry::Item {
+ title: other_title,
+ hotkey: other_hotkey,
+ ..
+ },
+ ) => title == other_title && hotkey == other_hotkey,
+ (
+ Entry::Dropdown { title, submenu },
+ Entry::Dropdown {
+ title: other_title,
+ submenu: other_submenu,
+ },
+ ) => title == other_title && submenu == other_submenu,
+ (Entry::Separator, Entry::Separator) => true,
+ _ => false,
+ }
+ }
+}
diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs
index 64599163..877aa2d2 100644
--- a/examples/game_of_life/src/main.rs
+++ b/examples/game_of_life/src/main.rs
@@ -6,6 +6,7 @@ mod style;
use grid::Grid;
use iced::button::{self, Button};
use iced::executor;
+use iced::menu::{self, Menu};
use iced::pick_list::{self, PickList};
use iced::slider::{self, Slider};
use iced::time;
@@ -128,6 +129,13 @@ impl Application for GameOfLife {
}
}
+ fn menu(&self) -> Menu<Message> {
+ Menu::with_entries(vec![menu::Entry::dropdown(
+ "Presets",
+ Preset::menu().map(Message::PresetPicked),
+ )])
+ }
+
fn view(&mut self) -> Element<Message> {
let version = self.version;
let selected_speed = self.next_speed.unwrap_or(self.speed);
diff --git a/examples/game_of_life/src/preset.rs b/examples/game_of_life/src/preset.rs
index 05157b6a..1c199a72 100644
--- a/examples/game_of_life/src/preset.rs
+++ b/examples/game_of_life/src/preset.rs
@@ -1,3 +1,5 @@
+use iced::menu::{self, Menu};
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Preset {
Custom,
@@ -26,6 +28,17 @@ pub static ALL: &[Preset] = &[
];
impl Preset {
+ pub fn menu() -> Menu<Self> {
+ Menu::with_entries(
+ ALL.iter()
+ .copied()
+ .map(|preset| {
+ menu::Entry::item(preset.to_string(), None, preset)
+ })
+ .collect(),
+ )
+ }
+
pub fn life(self) -> Vec<(isize, isize)> {
#[rustfmt::skip]
let cells = match self {
diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml
new file mode 100644
index 00000000..44597734
--- /dev/null
+++ b/examples/menu/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "menu"
+version = "0.1.0"
+authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
+edition = "2018"
+publish = false
+
+[dependencies]
+iced = { path = "../.." }
+iced_native = { path = "../../native" } \ No newline at end of file
diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs
new file mode 100644
index 00000000..7403713c
--- /dev/null
+++ b/examples/menu/src/main.rs
@@ -0,0 +1,117 @@
+use iced::menu::{self, Menu};
+use iced::{
+ executor, Application, Clipboard, Command, Container, Element, Length,
+ Settings, Text,
+};
+use iced_native::keyboard::{Hotkey, KeyCode, Modifiers};
+
+pub fn main() -> iced::Result {
+ App::run(Settings::default())
+}
+
+#[derive(Debug, Default)]
+struct App {
+ selected: Option<Entry>,
+}
+
+#[derive(Debug, Clone)]
+enum Entry {
+ One,
+ Two,
+ Three,
+ A,
+ B,
+ C,
+}
+
+#[derive(Debug, Clone)]
+enum Message {
+ MenuActivated(Entry),
+}
+
+impl Application for App {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Flags = ();
+
+ fn new(_flags: ()) -> (App, Command<Message>) {
+ (App::default(), Command::none())
+ }
+
+ fn title(&self) -> String {
+ String::from("Menu - Iced")
+ }
+
+ fn menu(&self) -> Menu<Message> {
+ let alt = Modifiers::ALT;
+ let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT;
+
+ Menu::with_entries(vec![
+ menu::Entry::dropdown(
+ "First",
+ Menu::with_entries(vec![
+ menu::Entry::item(
+ "One",
+ Hotkey::new(alt, KeyCode::F1),
+ Message::MenuActivated(Entry::One),
+ ),
+ menu::Entry::item(
+ "Two",
+ Hotkey::new(alt, KeyCode::F2),
+ Message::MenuActivated(Entry::Two),
+ ),
+ menu::Entry::Separator,
+ menu::Entry::item(
+ "Three",
+ Hotkey::new(alt, KeyCode::F3),
+ Message::MenuActivated(Entry::Three),
+ ),
+ ]),
+ ),
+ menu::Entry::dropdown(
+ "Second",
+ Menu::with_entries(vec![
+ menu::Entry::item(
+ "A",
+ Hotkey::new(ctrl_shift, KeyCode::A),
+ Message::MenuActivated(Entry::A),
+ ),
+ menu::Entry::item(
+ "B",
+ Hotkey::new(ctrl_shift, KeyCode::B),
+ Message::MenuActivated(Entry::B),
+ ),
+ menu::Entry::Separator,
+ menu::Entry::item(
+ "C",
+ Hotkey::new(ctrl_shift, KeyCode::C),
+ Message::MenuActivated(Entry::C),
+ ),
+ ]),
+ ),
+ ])
+ }
+
+ fn update(
+ &mut self,
+ message: Message,
+ _clipboard: &mut Clipboard,
+ ) -> Command<Message> {
+ match message {
+ Message::MenuActivated(entry) => self.selected = Some(entry),
+ }
+
+ Command::none()
+ }
+
+ fn view(&mut self) -> Element<Message> {
+ Container::new(
+ Text::new(format!("Selected {:?}", self.selected)).size(48),
+ )
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+}
diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs
index 81cf1770..3bd8aa25 100644
--- a/examples/pane_grid/src/main.rs
+++ b/examples/pane_grid/src/main.rs
@@ -146,7 +146,7 @@ impl Application for Example {
Event::Keyboard(keyboard::Event::KeyPressed {
modifiers,
key_code,
- }) if modifiers.is_command_pressed() => handle_hotkey(key_code),
+ }) if modifiers.command() => handle_hotkey(key_code),
_ => None,
}
})
diff --git a/glutin/Cargo.toml b/glutin/Cargo.toml
index 78d5fe47..b2a7f307 100644
--- a/glutin/Cargo.toml
+++ b/glutin/Cargo.toml
@@ -16,7 +16,7 @@ debug = ["iced_winit/debug"]
[dependencies.glutin]
version = "0.27"
git = "https://github.com/iced-rs/glutin"
-rev = "2564d0ab87cf2ad824a2a58733aebe40dd2f29bb"
+rev = "03437d8a1826d83c62017b2bb7bf18bfc9e352cc"
[dependencies.iced_native]
version = "0.4"
diff --git a/glutin/src/application.rs b/glutin/src/application.rs
index a8e5dbf9..991c8705 100644
--- a/glutin/src/application.rs
+++ b/glutin/src/application.rs
@@ -52,11 +52,14 @@ where
runtime.track(subscription);
let context = {
- let builder = settings.window.into_builder(
- &application.title(),
- application.mode(),
- event_loop.primary_monitor(),
- );
+ let builder = settings
+ .window
+ .into_builder(
+ &application.title(),
+ application.mode(),
+ event_loop.primary_monitor(),
+ )
+ .with_menu(Some(conversion::menu(&application.menu())));
let context = ContextBuilder::new()
.with_vsync(true)
@@ -304,6 +307,16 @@ async fn run_instance<A, E, C>(
// Maybe we can use `ControlFlow::WaitUntil` for this.
}
event::Event::WindowEvent {
+ event: event::WindowEvent::MenuEntryActivated(entry_id),
+ ..
+ } => {
+ if let Some(message) =
+ conversion::menu_message(state.menu(), entry_id)
+ {
+ messages.push(message);
+ }
+ }
+ event::Event::WindowEvent {
event: window_event,
..
} => {
diff --git a/native/src/lib.rs b/native/src/lib.rs
index cd214e36..cbb02506 100644
--- a/native/src/lib.rs
+++ b/native/src/lib.rs
@@ -61,8 +61,8 @@ mod debug;
mod debug;
pub use iced_core::{
- Align, Background, Color, Font, HorizontalAlignment, Length, Padding,
- Point, Rectangle, Size, Vector, VerticalAlignment,
+ menu, Align, Background, Color, Font, HorizontalAlignment, Length, Menu,
+ Padding, Point, Rectangle, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
diff --git a/native/src/program.rs b/native/src/program.rs
index 066c29d8..75fab094 100644
--- a/native/src/program.rs
+++ b/native/src/program.rs
@@ -11,7 +11,7 @@ pub trait Program: Sized {
type Renderer: Renderer;
/// The type of __messages__ your [`Program`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
/// The type of [`Clipboard`] your [`Program`] will use.
type Clipboard: Clipboard;
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs
index 20117fa0..f06f057b 100644
--- a/native/src/widget/text_input.rs
+++ b/native/src/widget/text_input.rs
@@ -362,7 +362,7 @@ where
Event::Keyboard(keyboard::Event::CharacterReceived(c))
if self.state.is_focused
&& self.state.is_pasting.is_none()
- && !self.state.keyboard_modifiers.is_command_pressed()
+ && !self.state.keyboard_modifiers.command()
&& !c.is_control() =>
{
let mut editor =
@@ -450,7 +450,7 @@ where
if platform::is_jump_modifier_pressed(modifiers)
&& !self.is_secure
{
- if modifiers.shift {
+ if modifiers.shift() {
self.state
.cursor
.select_left_by_words(&self.value);
@@ -459,7 +459,7 @@ where
.cursor
.move_left_by_words(&self.value);
}
- } else if modifiers.shift {
+ } else if modifiers.shift() {
self.state.cursor.select_left(&self.value)
} else {
self.state.cursor.move_left(&self.value);
@@ -469,7 +469,7 @@ where
if platform::is_jump_modifier_pressed(modifiers)
&& !self.is_secure
{
- if modifiers.shift {
+ if modifiers.shift() {
self.state
.cursor
.select_right_by_words(&self.value);
@@ -478,14 +478,14 @@ where
.cursor
.move_right_by_words(&self.value);
}
- } else if modifiers.shift {
+ } else if modifiers.shift() {
self.state.cursor.select_right(&self.value)
} else {
self.state.cursor.move_right(&self.value);
}
}
keyboard::KeyCode::Home => {
- if modifiers.shift {
+ if modifiers.shift() {
self.state.cursor.select_range(
self.state.cursor.start(&self.value),
0,
@@ -495,7 +495,7 @@ where
}
}
keyboard::KeyCode::End => {
- if modifiers.shift {
+ if modifiers.shift() {
self.state.cursor.select_range(
self.state.cursor.start(&self.value),
self.value.len(),
@@ -505,10 +505,7 @@ where
}
}
keyboard::KeyCode::C
- if self
- .state
- .keyboard_modifiers
- .is_command_pressed() =>
+ if self.state.keyboard_modifiers.command() =>
{
match self.state.cursor.selection(&self.value) {
Some((start, end)) => {
@@ -520,10 +517,7 @@ where
}
}
keyboard::KeyCode::X
- if self
- .state
- .keyboard_modifiers
- .is_command_pressed() =>
+ if self.state.keyboard_modifiers.command() =>
{
match self.state.cursor.selection(&self.value) {
Some((start, end)) => {
@@ -545,7 +539,7 @@ where
messages.push(message);
}
keyboard::KeyCode::V => {
- if self.state.keyboard_modifiers.is_command_pressed() {
+ if self.state.keyboard_modifiers.command() {
let content = match self.state.is_pasting.take() {
Some(content) => content,
None => {
@@ -576,10 +570,7 @@ where
}
}
keyboard::KeyCode::A
- if self
- .state
- .keyboard_modifiers
- .is_command_pressed() =>
+ if self.state.keyboard_modifiers.command() =>
{
self.state.cursor.select_all(&self.value);
}
@@ -858,9 +849,9 @@ mod platform {
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
- modifiers.alt
+ modifiers.alt()
} else {
- modifiers.control
+ modifiers.control()
}
}
}
diff --git a/src/application.rs b/src/application.rs
index bda8558c..ae85c841 100644
--- a/src/application.rs
+++ b/src/application.rs
@@ -1,6 +1,6 @@
use crate::window;
use crate::{
- Clipboard, Color, Command, Element, Executor, Settings, Subscription,
+ Clipboard, Color, Command, Element, Executor, Menu, Settings, Subscription,
};
/// An interactive cross-platform application.
@@ -99,7 +99,7 @@ pub trait Application: Sized {
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
/// The data needed to initialize your [`Application`].
type Flags;
@@ -191,6 +191,13 @@ pub trait Application: Sized {
false
}
+ /// Returns the current system [`Menu`] of the [`Application`].
+ ///
+ /// By default, it returns an empty [`Menu`].
+ fn menu(&self) -> Menu<Self::Message> {
+ Menu::new()
+ }
+
/// Runs the [`Application`].
///
/// On native platforms, this method will take control of the current thread
@@ -296,6 +303,10 @@ where
fn should_exit(&self) -> bool {
self.0.should_exit()
}
+
+ fn menu(&self) -> Menu<Self::Message> {
+ self.0.menu()
+ }
}
#[cfg(target_arch = "wasm32")]
diff --git a/src/lib.rs b/src/lib.rs
index 6a029afd..8cd6aa70 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -245,7 +245,7 @@ pub use sandbox::Sandbox;
pub use settings::Settings;
pub use runtime::{
- futures, Align, Background, Clipboard, Color, Command, Font,
- HorizontalAlignment, Length, Point, Rectangle, Size, Subscription, Vector,
- VerticalAlignment,
+ futures, menu, Align, Background, Clipboard, Color, Command, Font,
+ HorizontalAlignment, Length, Menu, Point, Rectangle, Size, Subscription,
+ Vector, VerticalAlignment,
};
diff --git a/src/sandbox.rs b/src/sandbox.rs
index 10b05a92..cb3cf624 100644
--- a/src/sandbox.rs
+++ b/src/sandbox.rs
@@ -88,7 +88,7 @@ use crate::{
/// ```
pub trait Sandbox {
/// The type of __messages__ your [`Sandbox`] will produce.
- type Message: std::fmt::Debug + Send;
+ type Message: std::fmt::Debug + Clone + Send;
/// Initializes the [`Sandbox`].
///
diff --git a/web/src/lib.rs b/web/src/lib.rs
index 158416b9..6b7d0115 100644
--- a/web/src/lib.rs
+++ b/web/src/lib.rs
@@ -74,8 +74,8 @@ pub use dodrio;
pub use element::Element;
pub use hasher::Hasher;
pub use iced_core::{
- keyboard, mouse, Align, Background, Color, Font, HorizontalAlignment,
- Length, Padding, Point, Rectangle, Size, Vector, VerticalAlignment,
+ keyboard, menu, mouse, Align, Background, Color, Font, HorizontalAlignment,
+ Length, Menu, Padding, Point, Rectangle, Size, Vector, VerticalAlignment,
};
pub use iced_futures::{executor, futures, Command};
pub use subscription::Subscription;
diff --git a/winit/Cargo.toml b/winit/Cargo.toml
index c5c6ef70..b1192135 100644
--- a/winit/Cargo.toml
+++ b/winit/Cargo.toml
@@ -21,7 +21,7 @@ thiserror = "1.0"
[dependencies.winit]
version = "0.25"
git = "https://github.com/iced-rs/winit"
-rev = "44a9a6fc442fcfa3fa0dfc2d5a2f86fdf4aba10c"
+rev = "844485272a7412cb35cdbfac3524decdf59475ca"
[dependencies.iced_native]
version = "0.4"
diff --git a/winit/src/application.rs b/winit/src/application.rs
index 49f2f513..5d1aabf9 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -14,6 +14,7 @@ use iced_futures::futures;
use iced_futures::futures::channel::mpsc;
use iced_graphics::window;
use iced_native::program::Program;
+use iced_native::Menu;
use iced_native::{Cache, UserInterface};
use std::mem::ManuallyDrop;
@@ -98,6 +99,13 @@ pub trait Application: Program<Clipboard = Clipboard> {
fn should_exit(&self) -> bool {
false
}
+
+ /// Returns the current system [`Menu`] of the [`Application`].
+ ///
+ /// By default, it returns an empty [`Menu`].
+ fn menu(&self) -> Menu<Self::Message> {
+ Menu::new()
+ }
}
/// Runs an [`Application`] with an executor, compositor, and the provided
@@ -145,6 +153,7 @@ where
application.mode(),
event_loop.primary_monitor(),
)
+ .with_menu(Some(conversion::menu(&application.menu())))
.build(&event_loop)
.map_err(Error::WindowCreationFailed)?;
@@ -380,6 +389,16 @@ async fn run_instance<A, E, C>(
// Maybe we can use `ControlFlow::WaitUntil` for this.
}
event::Event::WindowEvent {
+ event: event::WindowEvent::MenuEntryActivated(entry_id),
+ ..
+ } => {
+ if let Some(message) =
+ conversion::menu_message(state.menu(), entry_id)
+ {
+ messages.push(message);
+ }
+ }
+ event::Event::WindowEvent {
event: window_event,
..
} => {
diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs
index b54d3aed..f60f09be 100644
--- a/winit/src/application/state.rs
+++ b/winit/src/application/state.rs
@@ -1,5 +1,5 @@
use crate::conversion;
-use crate::{Application, Color, Debug, Mode, Point, Size, Viewport};
+use crate::{Application, Color, Debug, Menu, Mode, Point, Size, Viewport};
use std::marker::PhantomData;
use winit::event::{Touch, WindowEvent};
@@ -9,6 +9,7 @@ use winit::window::Window;
#[derive(Debug, Clone)]
pub struct State<A: Application> {
title: String,
+ menu: Menu<A::Message>,
mode: Mode,
background_color: Color,
scale_factor: f64,
@@ -23,6 +24,7 @@ impl<A: Application> State<A> {
/// Creates a new [`State`] for the provided [`Application`] and window.
pub fn new(application: &A, window: &Window) -> Self {
let title = application.title();
+ let menu = application.menu();
let mode = application.mode();
let background_color = application.background_color();
let scale_factor = application.scale_factor();
@@ -38,6 +40,7 @@ impl<A: Application> State<A> {
Self {
title,
+ menu,
mode,
background_color,
scale_factor,
@@ -50,6 +53,11 @@ impl<A: Application> State<A> {
}
}
+ /// Returns the current [`Menu`] of the [`State`].
+ pub fn menu(&self) -> &Menu<A::Message> {
+ &self.menu
+ }
+
/// Returns the current background [`Color`] of the [`State`].
pub fn background_color(&self) -> Color {
self.background_color
@@ -203,5 +211,14 @@ impl<A: Application> State<A> {
self.scale_factor = new_scale_factor;
}
+
+ // Update menu
+ let new_menu = application.menu();
+
+ if self.menu != new_menu {
+ window.set_menu(Some(conversion::menu(&new_menu)));
+
+ self.menu = new_menu;
+ }
}
}
diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs
index b850a805..5a8b9fd8 100644
--- a/winit/src/conversion.rs
+++ b/winit/src/conversion.rs
@@ -3,6 +3,7 @@
//! [`winit`]: https://github.com/rust-windowing/winit
//! [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
use crate::keyboard;
+use crate::menu::{self, Menu};
use crate::mouse;
use crate::touch;
use crate::window;
@@ -156,6 +157,110 @@ pub fn visible(mode: Mode) -> bool {
}
}
+/// Converts a `Hotkey` from [`iced_native`] to a [`winit`] Hotkey.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+fn hotkey(hotkey: keyboard::Hotkey) -> winit::window::Hotkey {
+ use winit::event::ModifiersState;
+
+ let mut modifiers = ModifiersState::empty();
+ modifiers.set(ModifiersState::CTRL, hotkey.modifiers.control());
+ modifiers.set(ModifiersState::SHIFT, hotkey.modifiers.shift());
+ modifiers.set(ModifiersState::ALT, hotkey.modifiers.alt());
+ modifiers.set(ModifiersState::LOGO, hotkey.modifiers.logo());
+
+ winit::window::Hotkey::new(modifiers, to_virtual_keycode(hotkey.key))
+}
+
+/// Converts a `Menu` from [`iced_native`] to a [`winit`] menu.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+pub fn menu<Message>(menu: &Menu<Message>) -> winit::window::Menu {
+ fn menu_i<Message>(
+ converted: &mut winit::window::Menu,
+ starting_id: usize,
+ menu: &Menu<Message>,
+ ) -> usize {
+ let mut id = starting_id;
+
+ for item in menu.iter() {
+ match item {
+ menu::Entry::Item { title, hotkey, .. } => {
+ converted.add_item(id, title, hotkey.map(self::hotkey));
+
+ id += 1;
+ }
+ menu::Entry::Dropdown { title, submenu } => {
+ let mut converted_submenu = winit::window::Menu::new();
+ let n_children =
+ menu_i(&mut converted_submenu, id, submenu);
+
+ converted.add_dropdown(title, converted_submenu);
+
+ id += n_children;
+ }
+ menu::Entry::Separator => {
+ converted.add_separator();
+ }
+ }
+ }
+
+ id - starting_id
+ }
+
+ let mut converted = winit::window::Menu::default();
+ let _ = menu_i(&mut converted, 0, menu);
+
+ converted
+}
+
+/// Given a [`Menu`] and an identifier of a [`menu::Entry`], it returns the
+/// `Message` that should be produced when that entry is activated.
+pub fn menu_message<Message>(menu: &Menu<Message>, id: u32) -> Option<Message>
+where
+ Message: Clone,
+{
+ fn find_message<Message>(
+ target: u32,
+ starting_id: u32,
+ menu: &Menu<Message>,
+ ) -> Result<Message, u32>
+ where
+ Message: Clone,
+ {
+ let mut id = starting_id;
+
+ for entry in menu.iter() {
+ match entry {
+ menu::Entry::Item { on_activation, .. } => {
+ if id == target {
+ return Ok(on_activation.clone());
+ }
+
+ id += 1;
+ }
+ menu::Entry::Dropdown { submenu, .. } => {
+ match find_message(target, id, submenu) {
+ Ok(message) => {
+ return Ok(message);
+ }
+ Err(n_children) => {
+ id += n_children;
+ }
+ }
+ }
+ menu::Entry::Separator => {}
+ }
+ }
+
+ Err(id - starting_id)
+ }
+
+ find_message(id, 0, menu).ok()
+}
+
/// Converts a `MouseCursor` from [`iced_native`] to a [`winit`] cursor icon.
///
/// [`winit`]: https://github.com/rust-windowing/winit
@@ -203,12 +308,14 @@ pub fn mouse_button(mouse_button: winit::event::MouseButton) -> mouse::Button {
pub fn modifiers(
modifiers: winit::event::ModifiersState,
) -> keyboard::Modifiers {
- keyboard::Modifiers {
- shift: modifiers.shift(),
- control: modifiers.ctrl(),
- alt: modifiers.alt(),
- logo: modifiers.logo(),
- }
+ let mut result = keyboard::Modifiers::empty();
+
+ result.set(keyboard::Modifiers::SHIFT, modifiers.shift());
+ result.set(keyboard::Modifiers::CTRL, modifiers.ctrl());
+ result.set(keyboard::Modifiers::ALT, modifiers.alt());
+ result.set(keyboard::Modifiers::LOGO, modifiers.logo());
+
+ result
}
/// Converts a physical cursor position to a logical `Point`.
@@ -252,6 +359,183 @@ pub fn touch_event(
}
}
+/// Converts a `KeyCode` from [`iced_native`] to an [`winit`] key code.
+///
+/// [`winit`]: https://github.com/rust-windowing/winit
+/// [`iced_native`]: https://github.com/hecrj/iced/tree/master/native
+fn to_virtual_keycode(
+ keycode: keyboard::KeyCode,
+) -> winit::event::VirtualKeyCode {
+ use keyboard::KeyCode;
+ use winit::event::VirtualKeyCode;
+
+ match keycode {
+ KeyCode::Key1 => VirtualKeyCode::Key1,
+ KeyCode::Key2 => VirtualKeyCode::Key2,
+ KeyCode::Key3 => VirtualKeyCode::Key3,
+ KeyCode::Key4 => VirtualKeyCode::Key4,
+ KeyCode::Key5 => VirtualKeyCode::Key5,
+ KeyCode::Key6 => VirtualKeyCode::Key6,
+ KeyCode::Key7 => VirtualKeyCode::Key7,
+ KeyCode::Key8 => VirtualKeyCode::Key8,
+ KeyCode::Key9 => VirtualKeyCode::Key9,
+ KeyCode::Key0 => VirtualKeyCode::Key0,
+ KeyCode::A => VirtualKeyCode::A,
+ KeyCode::B => VirtualKeyCode::B,
+ KeyCode::C => VirtualKeyCode::C,
+ KeyCode::D => VirtualKeyCode::D,
+ KeyCode::E => VirtualKeyCode::E,
+ KeyCode::F => VirtualKeyCode::F,
+ KeyCode::G => VirtualKeyCode::G,
+ KeyCode::H => VirtualKeyCode::H,
+ KeyCode::I => VirtualKeyCode::I,
+ KeyCode::J => VirtualKeyCode::J,
+ KeyCode::K => VirtualKeyCode::K,
+ KeyCode::L => VirtualKeyCode::L,
+ KeyCode::M => VirtualKeyCode::M,
+ KeyCode::N => VirtualKeyCode::N,
+ KeyCode::O => VirtualKeyCode::O,
+ KeyCode::P => VirtualKeyCode::P,
+ KeyCode::Q => VirtualKeyCode::Q,
+ KeyCode::R => VirtualKeyCode::R,
+ KeyCode::S => VirtualKeyCode::S,
+ KeyCode::T => VirtualKeyCode::T,
+ KeyCode::U => VirtualKeyCode::U,
+ KeyCode::V => VirtualKeyCode::V,
+ KeyCode::W => VirtualKeyCode::W,
+ KeyCode::X => VirtualKeyCode::X,
+ KeyCode::Y => VirtualKeyCode::Y,
+ KeyCode::Z => VirtualKeyCode::Z,
+ KeyCode::Escape => VirtualKeyCode::Escape,
+ KeyCode::F1 => VirtualKeyCode::F1,
+ KeyCode::F2 => VirtualKeyCode::F2,
+ KeyCode::F3 => VirtualKeyCode::F3,
+ KeyCode::F4 => VirtualKeyCode::F4,
+ KeyCode::F5 => VirtualKeyCode::F5,
+ KeyCode::F6 => VirtualKeyCode::F6,
+ KeyCode::F7 => VirtualKeyCode::F7,
+ KeyCode::F8 => VirtualKeyCode::F8,
+ KeyCode::F9 => VirtualKeyCode::F9,
+ KeyCode::F10 => VirtualKeyCode::F10,
+ KeyCode::F11 => VirtualKeyCode::F11,
+ KeyCode::F12 => VirtualKeyCode::F12,
+ KeyCode::F13 => VirtualKeyCode::F13,
+ KeyCode::F14 => VirtualKeyCode::F14,
+ KeyCode::F15 => VirtualKeyCode::F15,
+ KeyCode::F16 => VirtualKeyCode::F16,
+ KeyCode::F17 => VirtualKeyCode::F17,
+ KeyCode::F18 => VirtualKeyCode::F18,
+ KeyCode::F19 => VirtualKeyCode::F19,
+ KeyCode::F20 => VirtualKeyCode::F20,
+ KeyCode::F21 => VirtualKeyCode::F21,
+ KeyCode::F22 => VirtualKeyCode::F22,
+ KeyCode::F23 => VirtualKeyCode::F23,
+ KeyCode::F24 => VirtualKeyCode::F24,
+ KeyCode::Snapshot => VirtualKeyCode::Snapshot,
+ KeyCode::Scroll => VirtualKeyCode::Scroll,
+ KeyCode::Pause => VirtualKeyCode::Pause,
+ KeyCode::Insert => VirtualKeyCode::Insert,
+ KeyCode::Home => VirtualKeyCode::Home,
+ KeyCode::Delete => VirtualKeyCode::Delete,
+ KeyCode::End => VirtualKeyCode::End,
+ KeyCode::PageDown => VirtualKeyCode::PageDown,
+ KeyCode::PageUp => VirtualKeyCode::PageUp,
+ KeyCode::Left => VirtualKeyCode::Left,
+ KeyCode::Up => VirtualKeyCode::Up,
+ KeyCode::Right => VirtualKeyCode::Right,
+ KeyCode::Down => VirtualKeyCode::Down,
+ KeyCode::Backspace => VirtualKeyCode::Back,
+ KeyCode::Enter => VirtualKeyCode::Return,
+ KeyCode::Space => VirtualKeyCode::Space,
+ KeyCode::Compose => VirtualKeyCode::Compose,
+ KeyCode::Caret => VirtualKeyCode::Caret,
+ KeyCode::Numlock => VirtualKeyCode::Numlock,
+ KeyCode::Numpad0 => VirtualKeyCode::Numpad0,
+ KeyCode::Numpad1 => VirtualKeyCode::Numpad1,
+ KeyCode::Numpad2 => VirtualKeyCode::Numpad2,
+ KeyCode::Numpad3 => VirtualKeyCode::Numpad3,
+ KeyCode::Numpad4 => VirtualKeyCode::Numpad4,
+ KeyCode::Numpad5 => VirtualKeyCode::Numpad5,
+ KeyCode::Numpad6 => VirtualKeyCode::Numpad6,
+ KeyCode::Numpad7 => VirtualKeyCode::Numpad7,
+ KeyCode::Numpad8 => VirtualKeyCode::Numpad8,
+ KeyCode::Numpad9 => VirtualKeyCode::Numpad9,
+ KeyCode::AbntC1 => VirtualKeyCode::AbntC1,
+ KeyCode::AbntC2 => VirtualKeyCode::AbntC2,
+ KeyCode::NumpadAdd => VirtualKeyCode::NumpadAdd,
+ KeyCode::Plus => VirtualKeyCode::Plus,
+ KeyCode::Apostrophe => VirtualKeyCode::Apostrophe,
+ KeyCode::Apps => VirtualKeyCode::Apps,
+ KeyCode::At => VirtualKeyCode::At,
+ KeyCode::Ax => VirtualKeyCode::Ax,
+ KeyCode::Backslash => VirtualKeyCode::Backslash,
+ KeyCode::Calculator => VirtualKeyCode::Calculator,
+ KeyCode::Capital => VirtualKeyCode::Capital,
+ KeyCode::Colon => VirtualKeyCode::Colon,
+ KeyCode::Comma => VirtualKeyCode::Comma,
+ KeyCode::Convert => VirtualKeyCode::Convert,
+ KeyCode::NumpadDecimal => VirtualKeyCode::NumpadDecimal,
+ KeyCode::NumpadDivide => VirtualKeyCode::NumpadDivide,
+ KeyCode::Equals => VirtualKeyCode::Equals,
+ KeyCode::Grave => VirtualKeyCode::Grave,
+ KeyCode::Kana => VirtualKeyCode::Kana,
+ KeyCode::Kanji => VirtualKeyCode::Kanji,
+ KeyCode::LAlt => VirtualKeyCode::LAlt,
+ KeyCode::LBracket => VirtualKeyCode::LBracket,
+ KeyCode::LControl => VirtualKeyCode::LControl,
+ KeyCode::LShift => VirtualKeyCode::LShift,
+ KeyCode::LWin => VirtualKeyCode::LWin,
+ KeyCode::Mail => VirtualKeyCode::Mail,
+ KeyCode::MediaSelect => VirtualKeyCode::MediaSelect,
+ KeyCode::MediaStop => VirtualKeyCode::MediaStop,
+ KeyCode::Minus => VirtualKeyCode::Minus,
+ KeyCode::NumpadMultiply => VirtualKeyCode::NumpadMultiply,
+ KeyCode::Mute => VirtualKeyCode::Mute,
+ KeyCode::MyComputer => VirtualKeyCode::MyComputer,
+ KeyCode::NavigateForward => VirtualKeyCode::NavigateForward,
+ KeyCode::NavigateBackward => VirtualKeyCode::NavigateBackward,
+ KeyCode::NextTrack => VirtualKeyCode::NextTrack,
+ KeyCode::NoConvert => VirtualKeyCode::NoConvert,
+ KeyCode::NumpadComma => VirtualKeyCode::NumpadComma,
+ KeyCode::NumpadEnter => VirtualKeyCode::NumpadEnter,
+ KeyCode::NumpadEquals => VirtualKeyCode::NumpadEquals,
+ KeyCode::OEM102 => VirtualKeyCode::OEM102,
+ KeyCode::Period => VirtualKeyCode::Period,
+ KeyCode::PlayPause => VirtualKeyCode::PlayPause,
+ KeyCode::Power => VirtualKeyCode::Power,
+ KeyCode::PrevTrack => VirtualKeyCode::PrevTrack,
+ KeyCode::RAlt => VirtualKeyCode::RAlt,
+ KeyCode::RBracket => VirtualKeyCode::RBracket,
+ KeyCode::RControl => VirtualKeyCode::RControl,
+ KeyCode::RShift => VirtualKeyCode::RShift,
+ KeyCode::RWin => VirtualKeyCode::RWin,
+ KeyCode::Semicolon => VirtualKeyCode::Semicolon,
+ KeyCode::Slash => VirtualKeyCode::Slash,
+ KeyCode::Sleep => VirtualKeyCode::Sleep,
+ KeyCode::Stop => VirtualKeyCode::Stop,
+ KeyCode::NumpadSubtract => VirtualKeyCode::NumpadSubtract,
+ KeyCode::Sysrq => VirtualKeyCode::Sysrq,
+ KeyCode::Tab => VirtualKeyCode::Tab,
+ KeyCode::Underline => VirtualKeyCode::Underline,
+ KeyCode::Unlabeled => VirtualKeyCode::Unlabeled,
+ KeyCode::VolumeDown => VirtualKeyCode::VolumeDown,
+ KeyCode::VolumeUp => VirtualKeyCode::VolumeUp,
+ KeyCode::Wake => VirtualKeyCode::Wake,
+ KeyCode::WebBack => VirtualKeyCode::WebBack,
+ KeyCode::WebFavorites => VirtualKeyCode::WebFavorites,
+ KeyCode::WebForward => VirtualKeyCode::WebForward,
+ KeyCode::WebHome => VirtualKeyCode::WebHome,
+ KeyCode::WebRefresh => VirtualKeyCode::WebRefresh,
+ KeyCode::WebSearch => VirtualKeyCode::WebSearch,
+ KeyCode::WebStop => VirtualKeyCode::WebStop,
+ KeyCode::Yen => VirtualKeyCode::Yen,
+ KeyCode::Copy => VirtualKeyCode::Copy,
+ KeyCode::Paste => VirtualKeyCode::Paste,
+ KeyCode::Cut => VirtualKeyCode::Cut,
+ KeyCode::Asterisk => VirtualKeyCode::Asterisk,
+ }
+}
+
/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced_native`] key code.
///
/// [`winit`]: https://github.com/rust-windowing/winit