Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example and new convenience methods for druid menus #2260

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions druid/examples/menu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2019 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! This is a very small example of how to use menus.
//! It does the almost bare minimum while still being useful.

// On Windows platform, don't show a console when opening the app.
#![windows_subsystem = "windows"]

use druid::widget::prelude::*;
use druid::widget::{Flex, Label};
use druid::{
AppDelegate, AppLauncher, Command, Data, DelegateCtx, Handled, Lens, Menu, MenuItem, Selector,
Target, WidgetExt, WindowDesc,
};

const COMMAND: Selector = Selector::new("custom_Selector");

#[derive(Clone, Data, Lens)]
struct AppState {
option: bool,
value: usize,
}

pub fn main() {
// describe the main window
let main_window = WindowDesc::new(build_root_widget())
.title("Hello World!")
.window_size((400.0, 400.0))
.menu(|_, _, _| build_menu());

// create the initial app state
let initial_state: AppState = AppState {
option: false,
value: 0,
};

// start the application. Here we pass in the application state.
AppLauncher::with_window(main_window)
.log_to_console()
.delegate(Delegate)
.launch(initial_state)
.expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<AppState> {
Flex::column()
.with_child(Label::new(|data: &AppState, _: &_| {
format!("Current value: {}", data.value)
}))
.with_default_spacer()
.with_child(Label::new(|data: &AppState, _: &_| {
format!("IS selected: {}", data.option)
}))
.center()
}

fn build_menu() -> Menu<AppState> {
let menu = Menu::new("Druid Menu")
.entry(MenuItem::new("Send Command").command(COMMAND))
.separator()
.entry(
MenuItem::new("Change value")
.on_activate(|_, data: &mut AppState, _| data.value = (data.value + 1) % 4),
)
.entry(
MenuItem::new("1 Selected")
.radio_item(1, Some(0))
.lens(AppState::value),
)
.entry(
MenuItem::new("2 Selected")
.radio_item(2, Some(0))
.lens(AppState::value),
)
.entry(
// Implementing the radio item from hand
MenuItem::new("3 Selected")
.on_activate(|_, data: &mut AppState, _| {
if data.value == 3 {
data.value = 0
} else {
data.value = 3
}
})
.selected_if(|data: &AppState, _| data.value == 3),
)
.separator()
.entry(
MenuItem::new("CheckBox")
.toggle_data()
.lens(AppState::option),
)
.entry(
// Implementing the CheckBox from hand
MenuItem::new("Manual CheckBox")
.on_activate(|_, data: &mut AppState, _| data.option = !data.option)
.selected_if(|data: &AppState, _| data.option),
)
.entry(
MenuItem::new("Disabled")
.on_activate(|_, _, _| panic!("disabled Menu Item was activated!"))
.enabled(false),
)
.entry(
MenuItem::new("Disabled Selectable")
.on_activate(|_, _, _| panic!("disabled Menu Item was activated!"))
.selected(false)
.enabled(false),
)
//we dont add new menu items based on data!
.rebuild_on(|_, _, _| false);

Menu::empty().entry(menu)
}

struct Delegate;

impl AppDelegate<AppState> for Delegate {
fn command(
&mut self,
_: &mut DelegateCtx,
_: Target,
cmd: &Command,
_: &mut AppState,
_: &Env,
) -> Handled {
if cmd.is(COMMAND) {
println!("Clicked \"Send Command\"!");
Handled::Yes
} else {
Handled::No
}
}
}
2 changes: 2 additions & 0 deletions druid/examples/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl_example!(invalidation);
impl_example!(layout);
impl_example!(lens);
impl_example!(list);
impl_example!(menu);
impl_example!(multiwin);
impl_example!(open_save);
impl_example!(panels.unwrap());
Expand All @@ -86,3 +87,4 @@ impl_example!(transparency);
impl_example!(view_switcher);
impl_example!(widget_gallery);
impl_example!(text);
impl_example!(z_stack);
4 changes: 2 additions & 2 deletions druid/examples/widget_gallery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ impl<T: Data> Widget<T> for SquaresGrid<T> {
// The space needed to lay all elements out on a single line.
let ideal_width = (self.cell_size.width + self.spacing + 1.0) * count;
// Constrain the width.
let width = ideal_width.min(bc.max().width).max(bc.min().width);
let width = ideal_width.clamp(bc.min().width, bc.max().width);
// Given the width, the space needed to lay out all elements (as many as possible on each
// line).
let cells_in_row =
Expand All @@ -345,7 +345,7 @@ impl<T: Data> Widget<T> for SquaresGrid<T> {
let ideal_height = height_from_rows(rows);

// Constrain the height
let height = ideal_height.max(bc.min().height).min(bc.max().height);
let height = ideal_height.clamp(bc.min().height, bc.max().height);
// Now calculate how many rows we can actually fit in
while height_from_rows(rows) > height && rows > 0 {
rows -= 1;
Expand Down
30 changes: 30 additions & 0 deletions druid/src/menu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,25 @@ impl<T: Data> MenuItem<T> {
self.on_activate(move |ctx, _data, _env| ctx.submit_command(cmd.clone()))
}

/// Turns this `MenuItem` into a `RadioButton`
///
/// When selected this MenuItem will set the provided value as data.
/// If data is equal to the provided value the Item is selected otherwise not.
pub fn radio_item(self, value: T, unselect: Option<T>) -> Self
where
T: PartialEq,
{
let value2 = value.clone();
self.on_activate(move |_, data: &mut T, _| {
if *data != value {
*data = value.clone();
} else if let Some(value) = unselect.clone() {
*data = value;
}
})
.selected_if(move |data, _| *data == value2)
}

/// Provide a hotkey for activating this menu item.
///
/// This is equivalent to
Expand Down Expand Up @@ -724,6 +743,17 @@ impl<T: Data> MenuItem<T> {
}
}

impl MenuItem<bool> {
/// Turns the MenuItem into a CheckBox.
///
/// this is a convenience method which sets the `on_activate` and `selected_if` callbacks
/// to behave like a `CheckBox`.
pub fn toggle_data(self) -> Self {
self.on_activate(|_, data, _| *data = !*data)
.selected_if(|data, _| *data)
}
}

impl<T: Data> MenuVisitor<T> for Menu<T> {
fn activate(&mut self, ctx: &mut MenuEventCtx, id: MenuItemId, data: &mut T, env: &Env) {
for child in &mut self.children {
Expand Down