multiwin/
multiwin.rs

1// Copyright 2019 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Opening and closing windows and using window and context menus.
16
17// On Windows platform, don't show a console when opening the app.
18#![windows_subsystem = "windows"]
19
20use druid::widget::prelude::*;
21use druid::widget::{
22    Align, BackgroundBrush, Button, Controller, ControllerHost, Flex, Label, Padding,
23};
24use druid::Target::Global;
25use druid::{
26    commands as sys_cmds, AppDelegate, AppLauncher, Application, Color, Command, Data, DelegateCtx,
27    Handled, LocalizedString, Menu, MenuItem, Target, WindowDesc, WindowHandle, WindowId,
28};
29use tracing::info;
30
31#[derive(Debug, Clone, Default, Data)]
32struct State {
33    menu_count: usize,
34    selected: usize,
35    glow_hot: bool,
36}
37
38pub fn main() {
39    let main_window = WindowDesc::new(ui_builder()).menu(make_menu).title(
40        LocalizedString::new("multiwin-demo-window-title").with_placeholder("Many windows!"),
41    );
42    AppLauncher::with_window(main_window)
43        .delegate(Delegate {
44            windows: Vec::new(),
45        })
46        .log_to_console()
47        .launch(State::default())
48        .expect("launch failed");
49}
50
51fn ui_builder() -> impl Widget<State> {
52    let text = LocalizedString::new("hello-counter")
53        .with_arg("count", |data: &State, _env| data.menu_count.into());
54    let label = Label::new(text);
55    let inc_button =
56        Button::<State>::new("Add menu item").on_click(|_ctx, data, _env| data.menu_count += 1);
57    let dec_button = Button::<State>::new("Remove menu item")
58        .on_click(|_ctx, data, _env| data.menu_count = data.menu_count.saturating_sub(1));
59    let new_button = Button::<State>::new("New window").on_click(|ctx, _data, _env| {
60        ctx.submit_command(sys_cmds::NEW_FILE.to(Global));
61    });
62    let quit_button = Button::<State>::new("Quit app").on_click(|_ctx, _data, _env| {
63        Application::global().quit();
64    });
65
66    let mut col = Flex::column();
67    col.add_flex_child(Align::centered(Padding::new(5.0, label)), 1.0);
68    let mut row = Flex::row();
69    row.add_child(Padding::new(5.0, inc_button));
70    row.add_child(Padding::new(5.0, dec_button));
71    col.add_flex_child(Align::centered(row), 1.0);
72    let mut row = Flex::row();
73    row.add_child(Padding::new(5.0, new_button));
74    row.add_child(Padding::new(5.0, quit_button));
75    col.add_flex_child(Align::centered(row), 1.0);
76    let content = ControllerHost::new(col, ContextMenuController);
77    Glow::new(content)
78}
79
80struct Glow<W> {
81    inner: W,
82}
83
84impl<W> Glow<W> {
85    pub fn new(inner: W) -> Glow<W> {
86        Glow { inner }
87    }
88}
89
90impl<W: Widget<State>> Widget<State> for Glow<W> {
91    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut State, env: &Env) {
92        self.inner.event(ctx, event, data, env);
93    }
94
95    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &State, env: &Env) {
96        if let LifeCycle::HotChanged(_) = event {
97            ctx.request_paint();
98        }
99        self.inner.lifecycle(ctx, event, data, env);
100    }
101
102    fn update(&mut self, ctx: &mut UpdateCtx, old_data: &State, data: &State, env: &Env) {
103        if old_data.glow_hot != data.glow_hot {
104            ctx.request_paint();
105        }
106        self.inner.update(ctx, old_data, data, env);
107    }
108
109    fn layout(
110        &mut self,
111        ctx: &mut LayoutCtx,
112        bc: &BoxConstraints,
113        data: &State,
114        env: &Env,
115    ) -> Size {
116        self.inner.layout(ctx, bc, data, env)
117    }
118
119    fn paint(&mut self, ctx: &mut PaintCtx, data: &State, env: &Env) {
120        if data.glow_hot && ctx.is_hot() {
121            BackgroundBrush::Color(Color::rgb8(200, 55, 55)).paint(ctx, data, env);
122        }
123        self.inner.paint(ctx, data, env);
124    }
125}
126
127struct ContextMenuController;
128struct Delegate {
129    windows: Vec<WindowId>,
130}
131
132impl<W: Widget<State>> Controller<State, W> for ContextMenuController {
133    fn event(
134        &mut self,
135        child: &mut W,
136        ctx: &mut EventCtx,
137        event: &Event,
138        data: &mut State,
139        env: &Env,
140    ) {
141        match event {
142            Event::MouseDown(ref mouse) if mouse.button.is_right() => {
143                ctx.show_context_menu(make_context_menu(), mouse.pos);
144            }
145            _ => child.event(ctx, event, data, env),
146        }
147    }
148}
149
150impl AppDelegate<State> for Delegate {
151    fn command(
152        &mut self,
153        ctx: &mut DelegateCtx,
154        _target: Target,
155        cmd: &Command,
156        data: &mut State,
157        _env: &Env,
158    ) -> Handled {
159        if cmd.is(sys_cmds::NEW_FILE) {
160            let new_win = WindowDesc::new(ui_builder())
161                .menu(make_menu)
162                .window_size((data.selected as f64 * 100.0 + 300.0, 500.0));
163            ctx.new_window(new_win);
164            Handled::Yes
165        } else {
166            Handled::No
167        }
168    }
169
170    fn window_added(
171        &mut self,
172        id: WindowId,
173        _handle: WindowHandle,
174        _data: &mut State,
175        _env: &Env,
176        _ctx: &mut DelegateCtx,
177    ) {
178        info!("Window added, id: {:?}", id);
179        self.windows.push(id);
180    }
181
182    fn window_removed(
183        &mut self,
184        id: WindowId,
185        _data: &mut State,
186        _env: &Env,
187        _ctx: &mut DelegateCtx,
188    ) {
189        info!("Window removed, id: {:?}", id);
190        if let Some(pos) = self.windows.iter().position(|x| *x == id) {
191            self.windows.remove(pos);
192        }
193    }
194}
195
196#[allow(unused_assignments)]
197fn make_menu(_: Option<WindowId>, state: &State, _: &Env) -> Menu<State> {
198    let mut base = Menu::empty();
199    #[cfg(target_os = "macos")]
200    {
201        base = druid::platform_menus::mac::menu_bar();
202    }
203    #[cfg(any(
204        target_os = "windows",
205        target_os = "freebsd",
206        target_os = "linux",
207        target_os = "openbsd"
208    ))]
209    {
210        base = base.entry(druid::platform_menus::win::file::default());
211    }
212    if state.menu_count != 0 {
213        let mut custom = Menu::new(LocalizedString::new("Custom"));
214
215        for i in 1..=state.menu_count {
216            custom = custom.entry(
217                MenuItem::new(
218                    LocalizedString::new("hello-counter")
219                        .with_arg("count", move |_: &State, _| i.into()),
220                )
221                .on_activate(move |_ctx, data, _env| data.selected = i)
222                .enabled_if(move |_data, _env| i % 3 != 0)
223                .selected_if(move |data, _env| i == data.selected),
224            );
225        }
226        base = base.entry(custom);
227    }
228    base.rebuild_on(|old_data, data, _env| old_data.menu_count != data.menu_count)
229}
230
231fn make_context_menu() -> Menu<State> {
232    Menu::empty()
233        .entry(
234            MenuItem::new(LocalizedString::new("Increment"))
235                .on_activate(|_ctx, data: &mut State, _env| data.menu_count += 1),
236        )
237        .entry(
238            MenuItem::new(LocalizedString::new("Decrement")).on_activate(
239                |_ctx, data: &mut State, _env| data.menu_count = data.menu_count.saturating_sub(1),
240            ),
241        )
242        .entry(
243            MenuItem::new(LocalizedString::new("Glow when hot"))
244                .on_activate(|_ctx, data: &mut State, _env| data.glow_hot = !data.glow_hot),
245        )
246}