mirror of
https://github.com/italicsjenga/muda.git
synced 2024-12-23 20:11:29 +11:00
init - initial linux support
This commit is contained in:
commit
a46c53dbe9
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
19
Cargo.toml
Normal file
19
Cargo.toml
Normal 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
51
examples/tao.rs
Normal 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
43
examples/winit.rs
Normal 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
63
src/lib.rs
Normal 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
230
src/platform_impl/linux.rs
Normal 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(>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<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
8
src/platform_impl/mod.rs
Normal 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;
|
137
src/platform_impl/windows.rs
Normal file
137
src/platform_impl/windows.rs
Normal 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
20
src/util.rs
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue