Skip to content

Commit

Permalink
Menus that use data (#1625)
Browse files Browse the repository at this point in the history
Menus are now updated semi-automatically from the app data.

The public menu API has been substantially changed.

Fixes #965
  • Loading branch information
jneem authored Mar 19, 2021
1 parent 39f4855 commit 8e09a6d
Show file tree
Hide file tree
Showing 18 changed files with 1,395 additions and 1,063 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ You can find its changes [documented below](#070---2021-01-01).
- Switch to trace-based logging ([#1562] by [@PoignardAzur])
- Spacers in `Flex` are now implemented by calculating the space in `Flex` instead of creating a widget for it ([#1584] by [@JAicewizard])
- Padding is generic over child widget, impls WidgetWrapper ([#1634] by [@cmyr])
- Menu support was rewritten with support for `Data` ([#1625] by [@jneem])

### Deprecated

Expand Down Expand Up @@ -638,6 +639,7 @@ Last release without a changelog :(
[#1600]: https://github.com/linebender/druid/pull/1600
[#1606]: https://github.com/linebender/druid/pull/1606
[#1619]: https://github.com/linebender/druid/pull/1619
[#1625]: https://github.com/linebender/druid/pull/1625
[#1634]: https://github.com/linebender/druid/pull/1634
[#1635]: https://github.com/linebender/druid/pull/1635
[#1636]: https://github.com/linebender/druid/pull/1636
Expand Down
11 changes: 10 additions & 1 deletion druid-shell/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ impl Application {
self.platform_app.quit()
}

// TODO: do these two go in some kind of PlatformExt trait?
// TODO: do these three go in some kind of PlatformExt trait?
/// Hide the application this window belongs to. (cmd+H)
pub fn hide(&self) {
#[cfg(target_os = "macos")]
Expand All @@ -182,6 +182,15 @@ impl Application {
self.platform_app.hide_others()
}

/// Sets the global application menu, on platforms where there is one.
///
/// On platforms with no global application menu, this has no effect.
#[allow(unused_variables)]
pub fn set_menu(&self, menu: crate::Menu) {
#[cfg(target_os = "macos")]
self.platform_app.set_menu(menu.into_inner());
}

/// Returns a handle to the system clipboard.
pub fn clipboard(&self) -> Clipboard {
self.platform_app.clipboard().into()
Expand Down
4 changes: 2 additions & 2 deletions druid-shell/src/hotkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use crate::{IntoKey, KbKey, KeyEvent, Modifiers};
/// ```
///
/// [`SysMods`]: enum.SysMods.html
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct HotKey {
pub(crate) mods: RawMods,
pub(crate) key: KbKey,
Expand Down Expand Up @@ -140,7 +140,7 @@ pub enum SysMods {
/// A representation of the active modifier keys.
///
/// This is intended to be clearer than `Modifiers`, when describing hotkeys.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RawMods {
None,
Alt,
Expand Down
3 changes: 2 additions & 1 deletion druid-shell/src/platform/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,8 @@ impl WindowHandle {
.unwrap();

let first_child = &vbox.get_children()[0];
if first_child.is::<gtk::MenuBar>() {
if let Some(old_menubar) = first_child.downcast_ref::<gtk::MenuBar>() {
old_menubar.deactivate();
vbox.remove(first_child);
}
let menubar = menu.into_gtk_menubar(&self, &accel_group);
Expand Down
7 changes: 7 additions & 0 deletions druid-shell/src/platform/mac/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::application::AppHandler;

use super::clipboard::Clipboard;
use super::error::Error;
use super::menu::Menu;
use super::util;

static APP_HANDLER_IVAR: &str = "druidAppHandler";
Expand Down Expand Up @@ -117,6 +118,12 @@ impl Application {
}
}

pub fn set_menu(&self, menu: Menu) {
unsafe {
NSApp().setMainMenu_(menu.menu);
}
}

pub fn clipboard(&self) -> Clipboard {
Clipboard
}
Expand Down
29 changes: 15 additions & 14 deletions druid/examples/markdown_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use druid::widget::prelude::*;
use druid::widget::{Controller, LineBreaking, RawLabel, Scroll, Split, TextBox};
use druid::{
AppDelegate, AppLauncher, Color, Command, Data, DelegateCtx, FontFamily, FontStyle, FontWeight,
Handled, Lens, LocalizedString, MenuDesc, Selector, Target, Widget, WidgetExt, WindowDesc,
Handled, Lens, LocalizedString, Menu, Selector, Target, Widget, WidgetExt, WindowDesc,
WindowId,
};

const WINDOW_TITLE: LocalizedString<AppState> = LocalizedString::new("Minimal Markdown");
Expand Down Expand Up @@ -93,7 +94,7 @@ pub fn main() {
// describe the main window
let main_window = WindowDesc::new(build_root_widget())
.title(WINDOW_TITLE)
.menu(make_menu())
.menu(make_menu)
.window_size((700.0, 600.0));

// create the initial app state
Expand Down Expand Up @@ -228,23 +229,23 @@ fn add_attribute_for_tag(tag: &Tag, mut attrs: AttributesAdder) {
}

#[allow(unused_assignments, unused_mut)]
fn make_menu<T: Data>() -> MenuDesc<T> {
let mut base = MenuDesc::empty();
fn make_menu<T: Data>(_window_id: Option<WindowId>, _app_state: &AppState, _env: &Env) -> Menu<T> {
let mut base = Menu::empty();
#[cfg(target_os = "macos")]
{
base = base.append(druid::platform_menus::mac::application::default())
base = base.entry(druid::platform_menus::mac::application::default())
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
base = base.append(druid::platform_menus::win::file::default());
base = base.entry(druid::platform_menus::win::file::default());
}
base.append(
MenuDesc::new(LocalizedString::new("common-menu-edit-menu"))
.append(druid::platform_menus::common::undo())
.append(druid::platform_menus::common::redo())
.append_separator()
.append(druid::platform_menus::common::cut().disabled())
.append(druid::platform_menus::common::copy())
.append(druid::platform_menus::common::paste()),
base.entry(
Menu::new(LocalizedString::new("common-menu-edit-menu"))
.entry(druid::platform_menus::common::undo())
.entry(druid::platform_menus::common::redo())
.separator()
.entry(druid::platform_menus::common::cut().enabled(false))
.entry(druid::platform_menus::common::copy())
.entry(druid::platform_menus::common::paste()),
)
}
149 changes: 59 additions & 90 deletions druid/examples/multiwin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,11 @@ use druid::widget::{
};
use druid::Target::Global;
use druid::{
commands as sys_cmds, AppDelegate, AppLauncher, Application, Color, Command, ContextMenu, Data,
DelegateCtx, Handled, LocalizedString, MenuDesc, MenuItem, Selector, Target, WindowDesc,
WindowId,
commands as sys_cmds, AppDelegate, AppLauncher, Application, Color, Command, Data, DelegateCtx,
Handled, LocalizedString, Menu, MenuItem, Target, WindowDesc, WindowId,
};
use tracing::info;

const MENU_COUNT_ACTION: Selector<usize> = Selector::new("menu-count-action");
const MENU_INCREMENT_ACTION: Selector = Selector::new("menu-increment-action");
const MENU_DECREMENT_ACTION: Selector = Selector::new("menu-decrement-action");
const MENU_SWITCH_GLOW_ACTION: Selector = Selector::new("menu-switch-glow");

#[derive(Debug, Clone, Default, Data)]
struct State {
menu_count: usize,
Expand All @@ -39,11 +33,9 @@ struct State {
}

pub fn main() {
let main_window = WindowDesc::new(ui_builder())
.menu(make_menu(&State::default()))
.title(
LocalizedString::new("multiwin-demo-window-title").with_placeholder("Many windows!"),
);
let main_window = WindowDesc::new(ui_builder()).menu(make_menu).title(
LocalizedString::new("multiwin-demo-window-title").with_placeholder("Many windows!"),
);
AppLauncher::with_window(main_window)
.delegate(Delegate {
windows: Vec::new(),
Expand All @@ -57,10 +49,10 @@ fn ui_builder() -> impl Widget<State> {
let text = LocalizedString::new("hello-counter")
.with_arg("count", |data: &State, _env| data.menu_count.into());
let label = Label::new(text);
let inc_button = Button::<State>::new("Add menu item")
.on_click(|ctx, _data, _env| ctx.submit_command(MENU_INCREMENT_ACTION.to(Global)));
let inc_button =
Button::<State>::new("Add menu item").on_click(|_ctx, data, _env| data.menu_count += 1);
let dec_button = Button::<State>::new("Remove menu item")
.on_click(|ctx, _data, _env| ctx.submit_command(MENU_DECREMENT_ACTION.to(Global)));
.on_click(|_ctx, data, _env| data.menu_count = data.menu_count.saturating_sub(1));
let new_button = Button::<State>::new("New window").on_click(|ctx, _data, _env| {
ctx.submit_command(sys_cmds::NEW_FILE.to(Global));
});
Expand Down Expand Up @@ -134,12 +126,18 @@ struct Delegate {
windows: Vec<WindowId>,
}

impl<T, W: Widget<T>> Controller<T, W> for ContextMenuController {
fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
impl<W: Widget<State>> Controller<State, W> for ContextMenuController {
fn event(
&mut self,
child: &mut W,
ctx: &mut EventCtx,
event: &Event,
data: &mut State,
env: &Env,
) {
match event {
Event::MouseDown(ref mouse) if mouse.button.is_right() => {
let menu = ContextMenu::new(make_context_menu::<State>(), mouse.pos);
ctx.show_context_menu(menu);
ctx.show_context_menu(make_context_menu(), mouse.pos);
}
_ => child.event(ctx, event, data, env),
}
Expand All @@ -155,45 +153,14 @@ impl AppDelegate<State> for Delegate {
data: &mut State,
_env: &Env,
) -> Handled {
match cmd {
_ if cmd.is(sys_cmds::NEW_FILE) => {
let new_win = WindowDesc::new(ui_builder())
.menu(make_menu(data))
.window_size((data.selected as f64 * 100.0 + 300.0, 500.0));
ctx.new_window(new_win);
Handled::Yes
}
_ if cmd.is(MENU_COUNT_ACTION) => {
data.selected = *cmd.get_unchecked(MENU_COUNT_ACTION);
let menu = make_menu::<State>(data);
for id in &self.windows {
ctx.set_menu(menu.clone(), *id);
}
Handled::Yes
}
// wouldn't it be nice if a menu (like a button) could just mutate state
// directly if desired?
_ if cmd.is(MENU_INCREMENT_ACTION) => {
data.menu_count += 1;
let menu = make_menu::<State>(data);
for id in &self.windows {
ctx.set_menu(menu.clone(), *id);
}
Handled::Yes
}
_ if cmd.is(MENU_DECREMENT_ACTION) => {
data.menu_count = data.menu_count.saturating_sub(1);
let menu = make_menu::<State>(data);
for id in &self.windows {
ctx.set_menu(menu.clone(), *id);
}
Handled::Yes
}
_ if cmd.is(MENU_SWITCH_GLOW_ACTION) => {
data.glow_hot = !data.glow_hot;
Handled::Yes
}
_ => Handled::No,
if cmd.is(sys_cmds::NEW_FILE) {
let new_win = WindowDesc::new(ui_builder())
.menu(make_menu)
.window_size((data.selected as f64 * 100.0 + 300.0, 500.0));
ctx.new_window(new_win);
Handled::Yes
} else {
Handled::No
}
}

Expand Down Expand Up @@ -223,46 +190,48 @@ impl AppDelegate<State> for Delegate {
}

#[allow(unused_assignments)]
fn make_menu<T: Data>(state: &State) -> MenuDesc<T> {
let mut base = MenuDesc::empty();
fn make_menu(_: Option<WindowId>, state: &State, _: &Env) -> Menu<State> {
let mut base = Menu::empty();
#[cfg(target_os = "macos")]
{
base = druid::platform_menus::mac::menu_bar();
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
base = base.append(druid::platform_menus::win::file::default());
base = base.entry(druid::platform_menus::win::file::default());
}
if state.menu_count != 0 {
base = base.append(
MenuDesc::new(LocalizedString::new("Custom")).append_iter(|| {
(1..state.menu_count + 1).map(|i| {
MenuItem::new(
LocalizedString::new("hello-counter")
.with_arg("count", move |_, _| i.into()),
MENU_COUNT_ACTION.with(i),
)
.disabled_if(|| i % 3 == 0)
.selected_if(|| i == state.selected)
})
}),
);
let mut custom = Menu::new(LocalizedString::new("Custom"));

for i in 1..=state.menu_count {
custom = custom.entry(
MenuItem::new(
LocalizedString::new("hello-counter")
.with_arg("count", move |_: &State, _| i.into()),
)
.on_activate(move |_ctx, data, _env| data.selected = i)
.enabled_if(move |_data, _env| i % 3 != 0)
.selected_if(move |data, _env| i == data.selected),
);
}
base = base.entry(custom);
}
base
base.rebuild_on(|old_data, data, _env| old_data.menu_count != data.menu_count)
}

fn make_context_menu<T: Data>() -> MenuDesc<T> {
MenuDesc::empty()
.append(MenuItem::new(
LocalizedString::new("Increment"),
MENU_INCREMENT_ACTION,
))
.append(MenuItem::new(
LocalizedString::new("Decrement"),
MENU_DECREMENT_ACTION,
))
.append(MenuItem::new(
LocalizedString::new("Glow when hot"),
MENU_SWITCH_GLOW_ACTION,
))
fn make_context_menu() -> Menu<State> {
Menu::empty()
.entry(
MenuItem::new(LocalizedString::new("Increment"))
.on_activate(|_ctx, data: &mut State, _env| data.menu_count += 1),
)
.entry(
MenuItem::new(LocalizedString::new("Decrement")).on_activate(
|_ctx, data: &mut State, _env| data.menu_count = data.menu_count.saturating_sub(1),
),
)
.entry(
MenuItem::new(LocalizedString::new("Glow when hot"))
.on_activate(|_ctx, data: &mut State, _env| data.glow_hot = !data.glow_hot),
)
}
Loading

0 comments on commit 8e09a6d

Please sign in to comment.