init - initial linux support

This commit is contained in:
amrbashir 2022-05-05 13:50:22 +02:00
commit a46c53dbe9
No known key found for this signature in database
GPG key ID: BBD7A47A2003FF33
9 changed files with 573 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

19
Cargo.toml Normal file
View file

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

51
examples/tao.rs Normal file
View file

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

43
examples/winit.rs Normal file
View file

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

63
src/lib.rs Normal file
View file

@ -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<str>, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled))
}
pub fn add_text_item(&mut self, label: impl AsRef<str>, enabled: bool) -> TextMenuItem {
TextMenuItem(self.0.add_text_item(label, enabled))
}
#[cfg(target_os = "linux")]
pub fn init_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::Container>,
{
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<str>) {
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<str>, enabled: bool) -> Submenu {
Submenu(self.0.add_submenu(label, enabled))
}
pub fn add_text_item(&mut self, label: impl AsRef<str>, 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<str>) {
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()
}
}

230
src/platform_impl/linux.rs Normal file
View file

@ -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<Vec<Arc<Mutex<MenuEntry>>>>,
etype: MenuEntryType,
menu_gtk_items: Option<Arc<Mutex<Vec<(gtk::MenuItem, gtk::Menu)>>>>,
item_gtk_items: Option<Arc<Mutex<Vec<gtk::MenuItem>>>>,
}
struct InnerMenu {
entries: Vec<Arc<Mutex<MenuEntry>>>,
gtk_items: Vec<(gtk::MenuBar, gtk::Box)>,
}
pub struct Menu(Arc<Mutex<InnerMenu>>);
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<str>, 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<str>, 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<W>(&self, w: &W)
where
W: IsA<gtk::Container>,
{
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<M: IsA<gtk::MenuShell>>(gtk_menu: &M, entries: &Vec<Arc<Mutex<MenuEntry>>>) {
for entry in entries {
let mut entry = entry.lock();
let gtk_item = gtk::MenuItem::with_label(&entry.label);
gtk_menu.append(&gtk_item);
gtk_item.set_sensitive(entry.enabled);
if let MenuEntryType::Submenu = entry.etype {
let gtk_menu = gtk::Menu::new();
gtk_item.set_submenu(Some(&gtk_menu));
add_entries_to_menu(&gtk_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<Mutex<MenuEntry>>,
gtk_items: Arc<Mutex<Vec<(gtk::MenuItem, gtk::Menu)>>>,
}
impl Submenu {
pub fn set_label(&mut self, label: impl AsRef<str>) {
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<str>, 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<str>, 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<Mutex<MenuEntry>>,
gtk_items: Arc<Mutex<Vec<gtk::MenuItem>>>,
id: u64,
}
impl TextMenuItem {
pub fn set_label(&mut self, label: impl AsRef<str>) {
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
}
}

8
src/platform_impl/mod.rs Normal file
View file

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

View file

@ -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<M: MenuEntry>(&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<String>) -> 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<String>) {
let mut item_info: MENUITEMINFOW = unsafe { std::mem::zeroed() };
item_info.cbSize = std::mem::size_of::<MENUITEMINFOW>() 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<M: MenuEntry>(&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<String>) -> Self {
Self {
id: IDS_COUNTER.next(),
parent: 0,
}
}
pub fn id(&self) -> u64 {
self.id
}
pub fn set_title(&self, title: impl Into<String>) {
let mut item_info: MENUITEMINFOW = unsafe { std::mem::zeroed() };
item_info.cbSize = std::mem::size_of::<MENUITEMINFOW>() 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) };
}
}

20
src/util.rs Normal file
View file

@ -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<std::ffi::OsStr>) -> Vec<u16> {
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
.chain(std::iter::once(0))
.collect()
}