commit a46c53dbe9001e081262072946bc79beb527bb92 Author: amrbashir Date: Thu May 5 13:50:22 2022 +0200 init - initial linux support diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c73b8b7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "menu-rs" +version = "0.0.0" +edition = "2021" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.34", features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_Graphics_Gdi" +] } + +[target.'cfg(target_os = "linux")'.dependencies] +parking_lot = "0.12.0" +gtk = "0.15" + +[dev-dependencies] +winit = "0.26" +tao = { path = "../tao", default-features = false } diff --git a/examples/tao.rs b/examples/tao.rs new file mode 100644 index 0000000..949759e --- /dev/null +++ b/examples/tao.rs @@ -0,0 +1,51 @@ +use menu_rs::Menu; +#[cfg(target_os = "linux")] +use tao::platform::unix::WindowExtUnix; +#[cfg(target_os = "windows")] +use tao::platform::windows::WindowExtWindows; +use tao::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +fn main() { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + let window2 = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut menu_bar = Menu::new(); + let mut file_menu = menu_bar.add_submenu("File", true); + let mut edit_menu = menu_bar.add_submenu("Edit", true); + + let _open_item = file_menu.add_text_item("Open", true); + let mut save_item = file_menu.add_text_item("Save", true); + let _quit_item = file_menu.add_text_item("Quit", true); + + let _copy_item = edit_menu.add_text_item("Copy", true); + let _cut_item = edit_menu.add_text_item("Cut", true); + + #[cfg(target_os = "windows")] + menu_bar.init_for_hwnd(window.hwnd() as _); + #[cfg(target_os = "linux")] + menu_bar.init_for_gtk_window(window.gtk_window()); + #[cfg(target_os = "linux")] + menu_bar.init_for_gtk_window(window2.gtk_window()); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + save_item.set_enabled(false); + save_item.set_label("Save disabled"); + } + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }) +} diff --git a/examples/winit.rs b/examples/winit.rs new file mode 100644 index 0000000..d3ed363 --- /dev/null +++ b/examples/winit.rs @@ -0,0 +1,43 @@ +use menu_rs::Menu; +#[cfg(target_os = "windows")] +use winit::platform::windows::WindowExtWindows; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +fn main() { + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut menu_bar = Menu::new(); + let mut file_menu = menu_bar.add_submenu("File", true); + let mut edit_menu = menu_bar.add_submenu("Edit", true); + + let _open_item = file_menu.add_text_item("Open", true); + let mut save_item = file_menu.add_text_item("Save", true); + let _quit_item = file_menu.add_text_item("Quit", true); + + let _copy_item = edit_menu.add_text_item("Copy", true); + let _cut_item = edit_menu.add_text_item("Cut", true); + + #[cfg(target_os = "windows")] + menu_bar.init_for_hwnd(window.hwnd() as _); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + save_item.set_enabled(false); + } + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d93f8d5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,63 @@ +mod platform_impl; +mod util; + +pub struct Menu(platform_impl::Menu); + +impl Menu { + pub fn new() -> Self { + Self(platform_impl::Menu::new()) + } + + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + Submenu(self.0.add_submenu(label, enabled)) + } + + pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + TextMenuItem(self.0.add_text_item(label, enabled)) + } + + #[cfg(target_os = "linux")] + pub fn init_for_gtk_window(&self, w: &W) + where + W: gtk::prelude::IsA, + { + self.0.init_for_gtk_window(w) + } +} + +#[derive(Clone)] +pub struct Submenu(platform_impl::Submenu); + +impl Submenu { + pub fn set_label(&mut self, label: impl AsRef) { + self.0.set_label(label) + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.0.set_enabled(enabled) + } + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + Submenu(self.0.add_submenu(label, enabled)) + } + + pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + TextMenuItem(self.0.add_text_item(label, enabled)) + } +} + +#[derive(Clone)] +pub struct TextMenuItem(platform_impl::TextMenuItem); + +impl TextMenuItem { + pub fn set_label(&mut self, label: impl AsRef) { + self.0.set_label(label) + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.0.set_enabled(enabled) + } + + pub fn id(&self) -> u64 { + self.0.id() + } +} diff --git a/src/platform_impl/linux.rs b/src/platform_impl/linux.rs new file mode 100644 index 0000000..ec8c514 --- /dev/null +++ b/src/platform_impl/linux.rs @@ -0,0 +1,230 @@ +use parking_lot::Mutex; +use std::sync::Arc; + +use gtk::{prelude::*, Orientation}; + +use crate::util::Counter; + +const COUNTER: Counter = Counter::new(); + +enum MenuEntryType { + Submenu, + Text, +} + +struct MenuEntry { + label: String, + enabled: bool, + entries: Option>>>, + etype: MenuEntryType, + menu_gtk_items: Option>>>, + item_gtk_items: Option>>>, +} + +struct InnerMenu { + entries: Vec>>, + gtk_items: Vec<(gtk::MenuBar, gtk::Box)>, +} + +pub struct Menu(Arc>); + +impl Menu { + pub fn new() -> Self { + Self(Arc::new(Mutex::new(InnerMenu { + entries: Vec::new(), + gtk_items: Vec::new(), + }))) + } + + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + let label = label.as_ref().to_string(); + let gtk_items = Arc::new(Mutex::new(Vec::new())); + let entry = Arc::new(Mutex::new(MenuEntry { + label: label.clone(), + enabled, + entries: Some(Vec::new()), + etype: MenuEntryType::Submenu, + menu_gtk_items: Some(gtk_items.clone()), + item_gtk_items: None, + })); + self.0.lock().entries.push(entry.clone()); + Submenu { + label, + enabled, + entry, + gtk_items, + } + } + + pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + let label = label.as_ref().to_string(); + let gtk_items = Arc::new(Mutex::new(Vec::new())); + let entry = Arc::new(Mutex::new(MenuEntry { + label: label.clone(), + enabled, + entries: None, + etype: MenuEntryType::Text, + menu_gtk_items: None, + item_gtk_items: Some(gtk_items.clone()), + })); + self.0.lock().entries.push(entry.clone()); + TextMenuItem { + label, + enabled, + entry, + gtk_items, + id: COUNTER.next(), + } + } + + pub fn init_for_gtk_window(&self, w: &W) + where + W: IsA, + { + let menu_bar = gtk::MenuBar::new(); + add_entries_to_menu(&menu_bar, &self.0.lock().entries); + + let vbox = gtk::Box::new(Orientation::Vertical, 0); + vbox.pack_start(&menu_bar, false, false, 0); + w.add(&vbox); + vbox.show_all(); + + self.0.lock().gtk_items.push((menu_bar, vbox)); + } +} + +fn add_entries_to_menu>(gtk_menu: &M, entries: &Vec>>) { + for entry in entries { + let mut entry = entry.lock(); + let gtk_item = gtk::MenuItem::with_label(&entry.label); + gtk_menu.append(>k_item); + gtk_item.set_sensitive(entry.enabled); + if let MenuEntryType::Submenu = entry.etype { + let gtk_menu = gtk::Menu::new(); + gtk_item.set_submenu(Some(>k_menu)); + add_entries_to_menu(>k_menu, entry.entries.as_ref().unwrap()); + entry + .menu_gtk_items + .as_mut() + .unwrap() + .lock() + .push((gtk_item, gtk_menu)); + } else { + entry.item_gtk_items.as_mut().unwrap().lock().push(gtk_item); + } + } +} + +#[derive(Clone)] +pub struct Submenu { + label: String, + enabled: bool, + entry: Arc>, + gtk_items: Arc>>, +} + +impl Submenu { + pub fn set_label(&mut self, label: impl AsRef) { + let label = label.as_ref().to_string(); + for (item, _) in self.gtk_items.lock().iter() { + item.set_label(&label); + } + + self.label = label.clone(); + self.entry.lock().label = label; + } + + pub fn set_enabled(&mut self, enabled: bool) { + for (item, _) in self.gtk_items.lock().iter() { + item.set_sensitive(enabled); + } + + self.enabled = enabled; + self.entry.lock().enabled = enabled; + } + + pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { + let label = label.as_ref().to_string(); + let gtk_items = Arc::new(Mutex::new(Vec::new())); + let entry = Arc::new(Mutex::new(MenuEntry { + label: label.clone(), + enabled, + entries: Some(Vec::new()), + etype: MenuEntryType::Submenu, + menu_gtk_items: Some(gtk_items.clone()), + item_gtk_items: None, + })); + self.entry + .lock() + .entries + .as_mut() + .unwrap() + .push(entry.clone()); + Submenu { + label, + enabled, + entry, + gtk_items, + } + } + + pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { + let label = label.as_ref().to_string(); + let gtk_items = Arc::new(Mutex::new(Vec::new())); + let entry = Arc::new(Mutex::new(MenuEntry { + label: label.clone(), + enabled, + entries: None, + etype: MenuEntryType::Text, + menu_gtk_items: None, + item_gtk_items: Some(gtk_items.clone()), + })); + self.entry + .lock() + .entries + .as_mut() + .unwrap() + .push(entry.clone()); + TextMenuItem { + label, + enabled, + entry, + gtk_items, + id: COUNTER.next(), + } + } +} + +#[derive(Clone)] +pub struct TextMenuItem { + label: String, + enabled: bool, + entry: Arc>, + gtk_items: Arc>>, + id: u64, +} + +impl TextMenuItem { + pub fn set_label(&mut self, label: impl AsRef) { + let label = label.as_ref().to_string(); + for item in self.gtk_items.lock().iter() { + item.set_label(&label); + } + + self.label = label.clone(); + self.entry.lock().label = label; + } + + pub fn set_enabled(&mut self, enabled: bool) { + for item in self.gtk_items.lock().iter() { + item.set_sensitive(enabled); + } + + self.enabled = enabled; + self.entry.lock().enabled = enabled; + } + + pub fn id(&self) -> u64 { + self.id + } +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs new file mode 100644 index 0000000..82eb2d1 --- /dev/null +++ b/src/platform_impl/mod.rs @@ -0,0 +1,8 @@ +pub use self::platform_impl::*; + +#[cfg(target_os = "windows")] +#[path = "windows.rs"] +mod platform_impl; +#[cfg(target_os = "linux")] +#[path = "linux.rs"] +mod platform_impl; diff --git a/src/platform_impl/windows.rs b/src/platform_impl/windows.rs new file mode 100644 index 0000000..942e241 --- /dev/null +++ b/src/platform_impl/windows.rs @@ -0,0 +1,137 @@ +use windows_sys::Win32::UI::WindowsAndMessaging::{ + AppendMenuW, CreateMenu, EnableMenuItem, SetMenu, SetMenuItemInfoW, HMENU, MENUITEMINFOW, + MF_BYCOMMAND, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MF_STRING, MIIM_STRING, +}; + +use crate::{util::encode_wide, MenuEntry, IDS_COUNTER}; + +pub struct MenuBar(HMENU); + +impl MenuBar { + pub fn new() -> Self { + Self(unsafe { CreateMenu() }) + } + + pub fn add_entry(&mut self, entry: &mut M) { + let mut flags = 0; + let id; + + if entry.is_menu() { + flags |= MF_POPUP; + let menu = entry.platform_menu().unwrap(); + id = menu.hmenu as _; + menu.parent = self.0; + } else { + flags |= MF_STRING; + let item = entry.platform_item().unwrap(); + id = item.id as _; + item.parent = self.0; + }; + + if !entry.enabled() { + flags |= MF_GRAYED; + } + + unsafe { + AppendMenuW(self.0, flags, id, encode_wide(entry.title()).as_mut_ptr()); + } + } + + pub fn init_for_hwnd(&self, hwnd: isize) { + unsafe { SetMenu(hwnd, self.0) }; + } +} + +pub struct Menu { + hmenu: HMENU, + parent: HMENU, +} + +impl Menu { + pub fn new(_title: impl Into) -> Self { + Self { + hmenu: unsafe { CreateMenu() }, + parent: 0, + } + } + + pub fn id(&self) -> u64 { + self.hmenu as u64 + } + + pub fn set_title(&mut self, title: impl Into) { + let mut item_info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + item_info.cbSize = std::mem::size_of::() as _; + item_info.fMask = MIIM_STRING; + item_info.dwTypeData = encode_wide(title.into()).as_mut_ptr(); + + unsafe { SetMenuItemInfoW(self.parent, self.hmenu as u32, false.into(), &item_info) }; + } + + pub fn set_enabled(&mut self, enabled: bool) { + let enabled = if enabled { MF_ENABLED } else { MF_DISABLED }; + unsafe { EnableMenuItem(self.parent, self.hmenu as u32, MF_BYCOMMAND | enabled) }; + } + + pub fn add_entry(&mut self, entry: &mut M) { + let mut flags = 0; + let id; + + if entry.is_menu() { + flags |= MF_POPUP; + let menu = entry.platform_menu().unwrap(); + id = menu.hmenu as _; + menu.parent = self.hmenu; + } else { + flags |= MF_STRING; + let item = entry.platform_item().unwrap(); + id = item.id as _; + item.parent = self.hmenu; + }; + + if !entry.enabled() { + flags |= MF_GRAYED; + } + + unsafe { + AppendMenuW( + self.hmenu, + flags, + id, + encode_wide(entry.title()).as_mut_ptr(), + ); + } + } +} + +pub struct MenuItem { + id: u64, + parent: HMENU, +} + +impl MenuItem { + pub fn new(_title: impl Into) -> Self { + Self { + id: IDS_COUNTER.next(), + parent: 0, + } + } + + pub fn id(&self) -> u64 { + self.id + } + + pub fn set_title(&self, title: impl Into) { + let mut item_info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + item_info.cbSize = std::mem::size_of::() as _; + item_info.fMask = MIIM_STRING; + item_info.dwTypeData = encode_wide(title.into()).as_mut_ptr(); + + unsafe { SetMenuItemInfoW(self.parent, self.id as u32, false.into(), &item_info) }; + } + + pub fn set_enabled(&self, enabled: bool) { + let enabled = if enabled { MF_ENABLED } else { MF_DISABLED }; + unsafe { EnableMenuItem(self.parent, self.id as u32, MF_BYCOMMAND | enabled) }; + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..13a36ac --- /dev/null +++ b/src/util.rs @@ -0,0 +1,20 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + +pub(crate) struct Counter(AtomicU64); + +impl Counter { + pub(crate) const fn new() -> Self { + Self(AtomicU64::new(1)) + } + + pub(crate) fn next(&self) -> u64 { + self.0.fetch_add(1, Ordering::Release) + } +} + +#[cfg(target_os = "windows")] +pub fn encode_wide(string: impl AsRef) -> Vec { + std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) + .chain(std::iter::once(0)) + .collect() +}