From 368f3a4cead41004d76acc8775951288a64f080d Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Fri, 25 Sep 2020 12:57:35 +0100 Subject: [PATCH 1/9] Sub windows (squashed from previous branch) --- CHANGELOG.md | 1 + druid-shell/Cargo.toml | 0 druid/examples/sub_window.rs | 356 +++++++++++++++++++++++++++++++++ druid/examples/web/build.rs | 1 + druid/src/command.rs | 11 +- druid/src/contexts.rs | 14 +- druid/src/core.rs | 43 +++- druid/src/lib.rs | 2 + druid/src/sub_window.rs | 42 ++++ druid/src/widget/mod.rs | 2 + druid/src/widget/sub_window.rs | 114 +++++++++++ druid/src/win_handler.rs | 26 ++- 12 files changed, 605 insertions(+), 7 deletions(-) mode change 100644 => 100755 druid-shell/Cargo.toml create mode 100644 druid/examples/sub_window.rs create mode 100644 druid/src/sub_window.rs create mode 100644 druid/src/widget/sub_window.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 59da48aee7..2e54475819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ You can find its changes [documented below](#060---2020-06-01). - `WindowLevel` to control system window Z order, with Mac and GTK implementations ([#1231] by [@rjwittams]) - WIDGET_PADDING items added to theme and `Flex::with_default_spacer`/`Flex::add_default_spacer` ([#1220] by [@cmyr]) - CONFIGURE_WINDOW command to allow reconfiguration of an existing window. ([#1235] by [@rjwittams]) +- Sub windows: Allow opening windows that share state with arbitrary parts of the widget hierarchy ([#XXXX] by [@rjwittams]) ### Changed diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml old mode 100644 new mode 100755 diff --git a/druid/examples/sub_window.rs b/druid/examples/sub_window.rs new file mode 100644 index 0000000000..1bb4e146e4 --- /dev/null +++ b/druid/examples/sub_window.rs @@ -0,0 +1,356 @@ +// 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. + +use druid::commands::CLOSE_WINDOW; +use druid::lens::Unit; +use druid::widget::{ + Align, Button, Controller, ControllerHost, Flex, Label, SubWindowHost, TextBox, +}; +use druid::{ + theme, Affine, AppLauncher, BoxConstraints, Color, Data, Env, Event, EventCtx, LayoutCtx, Lens, + LensExt, LifeCycle, LifeCycleCtx, LocalizedString, PaintCtx, Point, Rect, RenderContext, Size, + TimerToken, UpdateCtx, Widget, WidgetExt, WindowConfig, WindowDesc, WindowId, +}; +use druid_shell::piet::Text; +use druid_shell::{Screen, WindowLevel}; +use instant::{Duration, Instant}; +use piet_common::{TextLayout, TextLayoutBuilder}; + +const VERTICAL_WIDGET_SPACING: f64 = 20.0; +const TEXT_BOX_WIDTH: f64 = 200.0; +const WINDOW_TITLE: LocalizedString = LocalizedString::new("Hello World!"); + +#[derive(Clone, Data, Lens)] +struct SubState { + my_stuff: String, +} + +#[derive(Clone, Data, Lens)] +struct HelloState { + name: String, + sub: SubState, +} + +pub fn main() { + // describe the main window + let main_window = WindowDesc::new(build_root_widget) + .title(WINDOW_TITLE) + .window_size((400.0, 400.0)); + + // create the initial app state + let initial_state = HelloState { + name: "World".into(), + sub: SubState { + my_stuff: "It's mine!".into(), + }, + }; + + // start the application + AppLauncher::with_window(main_window) + .use_simple_logger() + .launch(initial_state) + .expect("Failed to launch application"); +} + +enum TooltipState { + Showing(WindowId), + Waiting { + last_move: Instant, + timer_expire: Instant, + token: TimerToken, + window_pos: Point, + }, + Fresh, +} + +struct TooltipController { + tip: String, + state: TooltipState, +} + +impl TooltipController { + pub fn new(tip: impl Into) -> Self { + TooltipController { + tip: tip.into(), + state: TooltipState::Fresh, + } + } +} + +impl> Controller for TooltipController { + fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + let wait_duration = Duration::from_millis(500); + let resched_dur = Duration::from_millis(50); + let cursor_size = Size::new(15., 15.); + let now = Instant::now(); + let new_state = match &self.state { + TooltipState::Fresh => match event { + Event::MouseMove(me) if ctx.is_hot() => Some(TooltipState::Waiting { + last_move: now, + timer_expire: now + wait_duration, + token: ctx.request_timer(wait_duration), + window_pos: me.window_pos, + }), + _ => None, + }, + TooltipState::Waiting { + last_move, + timer_expire, + token, + window_pos, + } => match event { + Event::MouseMove(me) if ctx.is_hot() => { + let (cur_token, cur_expire) = if *timer_expire - now < resched_dur { + (ctx.request_timer(wait_duration), now + wait_duration) + } else { + (*token, *timer_expire) + }; + Some(TooltipState::Waiting { + last_move: now, + timer_expire: cur_expire, + token: cur_token, + window_pos: me.window_pos, + }) + } + Event::Timer(tok) if tok == token => { + let deadline = *last_move + wait_duration; + ctx.set_handled(); + if deadline > now { + let wait_for = deadline - now; + log::info!("Waiting another {:?}", wait_for); + Some(TooltipState::Waiting { + last_move: *last_move, + timer_expire: deadline, + token: ctx.request_timer(wait_for), + window_pos: *window_pos, + }) + } else { + let req = SubWindowHost::make_requirement( + ctx.widget_id(), + WindowConfig::default() + .show_titlebar(false) + .window_size(Size::new(100.0, 23.0)) + .set_level(WindowLevel::Tooltip) + .set_position( + ctx.window().get_position() + + window_pos.to_vec2() + + cursor_size.to_vec2(), + ), + false, + Label::<()>::new(self.tip.clone()), + (), + ); + let win_id = req.window_id; + ctx.new_sub_window(req); + Some(TooltipState::Showing(win_id)) + } + } + _ => None, + }, + TooltipState::Showing(win_id) => { + match event { + Event::MouseMove(me) if !ctx.is_hot() => { + // TODO another timer on leaving + log::info!("Sending close window for {:?}", win_id); + ctx.submit_command(CLOSE_WINDOW.to(*win_id)); + Some(TooltipState::Waiting { + last_move: now, + timer_expire: now + wait_duration, + token: ctx.request_timer(wait_duration), + window_pos: me.window_pos, + }) + } + _ => None, + } + } + }; + + if let Some(state) = new_state { + self.state = state; + } + + if !ctx.is_handled() { + child.event(ctx, event, data, env); + } + } + + fn lifecycle( + &mut self, + child: &mut W, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &T, + env: &Env, + ) { + if let LifeCycle::HotChanged(false) = event { + if let TooltipState::Showing(win_id) = self.state { + ctx.submit_command(CLOSE_WINDOW.to(win_id)); + } + self.state = TooltipState::Fresh; + } + child.lifecycle(ctx, event, data, env) + } +} + +struct DragWindowController { + init_pos: Option, + //dragging: bool +} + +impl DragWindowController { + pub fn new() -> Self { + DragWindowController { init_pos: None } + } +} + +impl> Controller for DragWindowController { + fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + match event { + Event::MouseDown(me) if me.buttons.has_left() => { + ctx.set_active(true); + self.init_pos = Some(me.window_pos) + } + Event::MouseMove(me) if ctx.is_active() && me.buttons.has_left() => { + if let Some(init_pos) = self.init_pos { + let within_window_change = me.window_pos.to_vec2() - init_pos.to_vec2(); + let old_pos = ctx.window().get_position(); + let new_pos = old_pos + within_window_change; + log::info!( + "Drag {:?} ", + ( + init_pos, + me.window_pos, + within_window_change, + old_pos, + new_pos + ) + ); + ctx.window().set_position(new_pos) + } + } + Event::MouseUp(_me) if ctx.is_active() => { + self.init_pos = None; + ctx.set_active(false) + } + _ => (), + } + child.event(ctx, event, data, env) + } +} + +struct ScreenThing; + +impl Widget<()> for ScreenThing { + fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut (), _env: &Env) {} + + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &(), _env: &Env) {} + + fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &(), _data: &(), _env: &Env) {} + + fn layout( + &mut self, + _ctx: &mut LayoutCtx, + bc: &BoxConstraints, + _data: &(), + _env: &Env, + ) -> Size { + bc.constrain(Size::new(800.0, 600.0)) + } + + fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) { + let sz = ctx.size(); + + let monitors = Screen::get_monitors(); + let all = monitors + .iter() + .map(|x| x.virtual_rect()) + .fold(Rect::ZERO, |s, r| r.union(s)); + if all.width() > 0. && all.height() > 0. { + let trans = Affine::scale(f64::min(sz.width / all.width(), sz.height / all.height())) + * Affine::translate(all.origin().to_vec2()).inverse(); + let font = env.get(theme::UI_FONT).family; + + for (i, mon) in monitors.iter().enumerate() { + let vr = mon.virtual_rect(); + let tr = trans.transform_rect_bbox(vr); + ctx.stroke(tr, &Color::WHITE, 1.0); + + if let Ok(tl) = ctx + .text() + .new_text_layout(format!( + "{}:{}x{}@{},{}", + i, + vr.width(), + vr.height(), + vr.x0, + vr.y0 + )) + .max_width(tr.width() - 5.) + .font(font.clone(), env.get(theme::TEXT_SIZE_NORMAL)) + .text_color(Color::WHITE) + .build() + { + ctx.draw_text(&tl, tr.center() - tl.size().to_vec2() * 0.5); + } + } + } + } +} + +fn build_root_widget() -> impl Widget { + let label = ControllerHost::new( + Label::new(|data: &HelloState, _env: &Env| { + format!("Hello {}! {} ", data.name, data.sub.my_stuff) + }), + TooltipController::new("Tips! Are good"), + ); + // a textbox that modifies `name`. + let textbox = TextBox::new() + .with_placeholder("Who are we greeting?") + .fix_width(TEXT_BOX_WIDTH) + .lens(HelloState::sub.then(SubState::my_stuff)); + + let button = Button::new("Make sub window") + .on_click(|ctx, data: &mut SubState, _env| { + let tb = TextBox::new().lens(SubState::my_stuff); + let drag_thing = Label::new("Drag me").controller(DragWindowController::new()); + let col = Flex::column().with_child(drag_thing).with_child(tb); + let req = SubWindowHost::make_requirement( + ctx.widget_id(), + WindowConfig::default() + .show_titlebar(false) + .window_size(Size::new(100., 100.)) + .set_position(Point::new(1000.0, 500.0)) + .set_level(WindowLevel::AppWindow), + true, + col, + data.clone(), + ); + + ctx.new_sub_window(req) + }) + .center() + .lens(HelloState::sub); + + // arrange the two widgets vertically, with some padding + let layout = Flex::column() + .with_child(label) + .with_flex_child(ScreenThing.lens(Unit::default()).padding(5.), 1.) + .with_spacer(VERTICAL_WIDGET_SPACING) + .with_child(textbox) + .with_child(button); + + // center the two widgets in the available space + Align::centered(layout) +} diff --git a/druid/examples/web/build.rs b/druid/examples/web/build.rs index 887a5c599c..2ed9c80c29 100644 --- a/druid/examples/web/build.rs +++ b/druid/examples/web/build.rs @@ -22,6 +22,7 @@ const EXCEPTIONS: &[&str] = &[ "svg", // usvg doesn't currently build as Wasm. "ext_event", // the web backend doesn't currently support spawning threads. "blocking_function", // the web backend doesn't currently support spawning threads. + "sub_window", ]; /// Create a platform specific link from `src` to the `dst` directory. diff --git a/druid/src/command.rs b/druid/src/command.rs index 157cb4536a..a20e4e164d 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -146,7 +146,7 @@ pub mod sys { use std::any::Any; use super::Selector; - use crate::{FileDialogOptions, FileInfo, SingleUse, WindowConfig}; + use crate::{FileDialogOptions, FileInfo, SingleUse, SubWindowRequirement, WindowConfig}; /// Quit the running application. This command is handled by the druid library. pub const QUIT_APP: Selector = Selector::new("druid-builtin.quit-app"); @@ -189,6 +189,15 @@ pub mod sys { pub(crate) const SHOW_CONTEXT_MENU: Selector> = Selector::new("druid-builtin.show-context-menu"); + pub(crate) const NEW_SUB_WINDOW: Selector> = + Selector::new("druid-builtin.new-sub-window"); + + pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector> = + Selector::new("druid-builtin.parent_to_host"); + + pub(crate) const SUB_WINDOW_HOST_TO_PARENT: Selector> = + Selector::new("druid-builtin.host_to_parent"); + /// The selector for a command to set the window's menu. The payload should /// be a [`MenuDesc`] object. /// diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 0331a1f394..5d830f31d2 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -26,7 +26,8 @@ use crate::piet::{Piet, PietText, RenderContext}; use crate::shell::Region; use crate::{ commands, Affine, Command, ContextMenu, Cursor, Env, ExtEventSink, Insets, MenuDesc, Point, - Rect, SingleUse, Size, Target, TimerToken, WidgetId, WindowDesc, WindowHandle, WindowId, + Rect, SingleUse, Size, SubWindowRequirement, Target, TimerToken, WidgetId, WindowDesc, + WindowHandle, WindowId, }; /// A macro for implementing methods on multiple contexts. @@ -315,6 +316,17 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, pub fn set_menu(&mut self, menu: MenuDesc) { self.state.set_menu(menu); } + + /// Create a new sub window that will have its app data synchronised with the nearest surrounding widget pod. + // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the + // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. + pub fn new_sub_window(&mut self, requirement: SubWindowRequirement) { + if let Some(id) = requirement.host_id { + self.widget_state.add_sub_window_host(id); + } + + self.submit_command(commands::NEW_SUB_WINDOW.with(SingleUse::new(requirement))); + } }); // methods on event, update, and lifecycle diff --git a/druid/src/core.rs b/druid/src/core.rs index 84176ccf3e..2326d38bdf 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -17,6 +17,8 @@ use std::collections::{HashMap, VecDeque}; use crate::bloom::Bloom; +use crate::command::sys::{SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; + use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; use crate::util::ExtendDrain; @@ -114,6 +116,9 @@ pub(crate) struct WidgetState { pub(crate) children_changed: bool, /// Associate timers with widgets that requested them. pub(crate) timers: HashMap, + + // Port -> Host + pub(crate) sub_window_hosts: Option>, } /// Methods by which a widget can attempt to change focus state. @@ -700,10 +705,22 @@ impl> WidgetPod { let inner_event = modified_event.as_ref().unwrap_or(event); inner_ctx.widget_state.has_active = false; - self.inner.event(&mut inner_ctx, &inner_event, data, env); - - inner_ctx.widget_state.has_active |= inner_ctx.widget_state.is_active; - ctx.is_handled |= inner_ctx.is_handled; + match inner_event { + Event::Command(cmd) if cmd.is(SUB_WINDOW_HOST_TO_PARENT) => { + if let Some(update) = cmd + .get_unchecked(SUB_WINDOW_HOST_TO_PARENT) + .downcast_ref::() + { + *data = (*update).clone(); + } + ctx.is_handled = true + } + _ => { + self.inner.event(&mut inner_ctx, &inner_event, data, env); + inner_ctx.widget_state.has_active |= inner_ctx.widget_state.is_active; + ctx.is_handled |= inner_ctx.is_handled; + } + } } // Always merge even if not needed, because merging is idempotent and gives us simpler code. @@ -847,6 +864,15 @@ impl> WidgetPod { } } + if let Some(hosts) = &self.state.sub_window_hosts { + for host in hosts { + let cloned: T = (*data).clone(); + let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, Box::new(cloned), *host); + + ctx.submit_command(command) + } + } + let prev_env = self.env.as_ref().filter(|p| !p.same(env)); let mut child_ctx = UpdateCtx { @@ -896,6 +922,7 @@ impl WidgetState { children: Bloom::new(), children_changed: false, timers: HashMap::new(), + sub_window_hosts: None, } } @@ -953,6 +980,14 @@ impl WidgetState { pub(crate) fn layout_rect(&self) -> Rect { self.layout_rect.unwrap_or_default() } + + pub(crate) fn add_sub_window_host(&mut self, host_id: WidgetId) { + if let Some(ports) = &mut self.sub_window_hosts { + ports.push(host_id); + } else { + self.sub_window_hosts = Some(vec![host_id]) + } + } } #[cfg(test)] diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 41835f67a8..d9fc3a943c 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -158,6 +158,7 @@ mod localization; mod menu; mod mouse; pub mod scroll_component; +mod sub_window; #[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod tests; @@ -197,6 +198,7 @@ pub use lens::{Lens, LensExt}; pub use localization::LocalizedString; pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem}; pub use mouse::MouseEvent; +pub use sub_window::SubWindowRequirement; pub use text::{FontDescriptor, TextLayout}; pub use widget::{Widget, WidgetExt, WidgetId}; pub use win_handler::DruidHandler; diff --git a/druid/src/sub_window.rs b/druid/src/sub_window.rs new file mode 100644 index 0000000000..85554a7fcc --- /dev/null +++ b/druid/src/sub_window.rs @@ -0,0 +1,42 @@ +use crate::app::{PendingWindow, WindowConfig}; +use crate::lens::Unit; +use crate::win_handler::AppState; +use crate::{Data, Widget, WidgetExt, WidgetId, WindowHandle, WindowId}; +use druid_shell::Error; + +// We can't have any type arguments here, as both ends would need to know them +// ahead of time in order to instantiate correctly. +// So we erase everything to () +/// The required information to create a sub window, including the widget it should host, and the +pub struct SubWindowRequirement { + pub(crate) host_id: Option, // Present if updates should be sent from the pod to the sub window. + pub(crate) sub_window_root: Box>, + pub(crate) window_config: WindowConfig, + /// The window id that the sub window will have once it is created. Can be used to send commands to. + pub window_id: WindowId, +} + +impl SubWindowRequirement { + pub(crate) fn new( + host_id: Option, + sub_window_root: Box>, + window_config: WindowConfig, + window_id: WindowId, + ) -> Self { + SubWindowRequirement { + host_id, + sub_window_root, + window_config, + window_id, + } + } + + pub(crate) fn make_sub_window( + self, + app_state: &mut AppState, + ) -> Result { + let sub_window_root = self.sub_window_root; + let pending = PendingWindow::new(|| sub_window_root.lens(Unit::default())); + app_state.build_native_window(self.window_id, pending, self.window_config) + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 7383fb19f8..012746ed2a 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -42,6 +42,7 @@ mod slider; mod spinner; mod split; mod stepper; +mod sub_window; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] mod svg; @@ -79,6 +80,7 @@ pub use slider::Slider; pub use spinner::Spinner; pub use split::Split; pub use stepper::Stepper; +pub use sub_window::SubWindowHost; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; diff --git a/druid/src/widget/sub_window.rs b/druid/src/widget/sub_window.rs new file mode 100644 index 0000000000..5c69611532 --- /dev/null +++ b/druid/src/widget/sub_window.rs @@ -0,0 +1,114 @@ +use crate::app::WindowConfig; +use crate::command::sys::SUB_WINDOW_PARENT_TO_HOST; +use crate::commands::SUB_WINDOW_HOST_TO_PARENT; +use crate::{ + BoxConstraints, Command, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, + PaintCtx, Point, Rect, Size, SubWindowRequirement, UpdateCtx, Widget, WidgetExt, WidgetId, + WidgetPod, WindowId, +}; +use std::ops::Deref; + +/// A widget currently meant to be used as the root of a sub window. +/// It ignores its own AppData, but provides its child with data synchronised +/// from the WidgetPod that created it. +pub struct SubWindowHost> { + id: WidgetId, + parent_id: WidgetId, + sync: bool, + data: U, + child: WidgetPod, +} + +impl> SubWindowHost { + fn new(id: WidgetId, parent_id: WidgetId, sync: bool, data: U, widget: W) -> Self { + SubWindowHost { + id, + parent_id, + sync, + data, + child: WidgetPod::new(widget), + } + } + + /// Creates a subwindow requirement that hosts the provided widget within a sub window host. + /// It will synchronise data updates with the provided parent_id if "sync" is true, and it will expect to be sent + /// SUB_WINDOW_PARENT_TO_HOST commands to update the provided data for the widget. + pub fn make_requirement( + parent_id: WidgetId, + window_config: WindowConfig, + sync: bool, + widget: W, + data: U, + ) -> SubWindowRequirement + where + W: 'static, + U: Data, + { + let host_id = WidgetId::next(); + let sub_window_host = SubWindowHost::new(host_id, parent_id, sync, data, widget).boxed(); + let host_id = if sync { Some(host_id) } else { None }; + SubWindowRequirement::new(host_id, sub_window_host, window_config, WindowId::next()) + } +} + +impl> Widget<()> for SubWindowHost { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), env: &Env) { + match event { + Event::Command(cmd) if self.sync && cmd.is(SUB_WINDOW_PARENT_TO_HOST) => { + if let Some(update) = cmd + .get_unchecked(SUB_WINDOW_PARENT_TO_HOST) + .downcast_ref::() + { + self.data = update.deref().clone(); + ctx.request_update(); + } else { + log::warn!("Received a sub window parent to host command that could not be unwrapped. \ + This could mean that the sub window you requested and the enclosing widget pod that you opened it from do not share a common data type. \ + Make sure you have a widget pod between your requesting widget and any lenses." ) + } + ctx.set_handled(); + } + _ => { + let old = self.data.clone(); // Could avoid this by keeping two bit of data or if we could ask widget pod? + self.child.event(ctx, event, &mut self.data, env); + if self.sync && !old.same(&self.data) { + ctx.submit_command(Command::new( + SUB_WINDOW_HOST_TO_PARENT, + Box::new(self.data.clone()), + self.parent_id, + )) + } + } + } + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &(), env: &Env) { + self.child.lifecycle(ctx, event, &self.data, env) + } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &(), _data: &(), env: &Env) { + if ctx.has_requested_update() { + // Should env be copied from the parent too? Possibly + self.child.update(ctx, &self.data, env); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &(), env: &Env) -> Size { + let size = self.child.layout(ctx, bc, &self.data, env); + self.child.set_layout_rect( + ctx, + &self.data, + env, + Rect::from_origin_size(Point::ORIGIN, size), + ); + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) { + self.child.paint_raw(ctx, &self.data, env); + } + + fn id(&self) -> Option { + Some(self.id) + } +} diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 7b003e3aba..ab80cc995f 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -574,6 +574,11 @@ impl AppState { log::error!("failed to create window: '{}'", e); } } + _ if cmd.is(sys_cmd::NEW_SUB_WINDOW) => { + if let Err(e) = self.new_sub_window(cmd) { + log::error!("failed to create sub window: '{}'", e); + } + } _ if cmd.is(sys_cmd::CLOSE_ALL_WINDOWS) => self.request_close_all_windows(), // these should come from a window // FIXME: we need to be able to open a file without a window handle @@ -629,7 +634,6 @@ impl AppState { .windows .get_mut(window_id) .map(|w| w.handle.clone()); - let result = handle.and_then(|mut handle| handle.save_as_sync(options)); self.inner.borrow_mut().dispatch_cmd({ if let Some(info) = result { @@ -650,6 +654,26 @@ impl AppState { Ok(()) } + fn new_sub_window(&mut self, cmd: Command) -> Result<(), Box> { + if let Some(transfer) = cmd.get(sys_cmd::NEW_SUB_WINDOW) { + if let Some(swr) = transfer.take() { + let window = swr.make_sub_window(self)?; + window.show(); + Ok(()) + } else { + panic!( + "{} command must carry a SubWindowRequirement internally", + sys_cmd::NEW_SUB_WINDOW + ) + } + } else { + panic!( + "{} command must carry a SingleUse", + sys_cmd::NEW_SUB_WINDOW + ) + } + } + fn request_close_window(&mut self, id: WindowId) { self.inner.borrow_mut().request_close_window(id); } From 28b0617154609404ae718248017267b976cec959 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Mon, 4 Jan 2021 00:20:43 +0000 Subject: [PATCH 2/9] Update druid version. Address review comments --- druid/examples/sub_window.rs | 17 ++--- druid/src/command.rs | 4 +- druid/src/contexts.rs | 23 ++++--- druid/src/core.rs | 27 +++----- druid/src/lib.rs | 1 - druid/src/sub_window.rs | 115 ++++++++++++++++++++++++++++++--- druid/src/widget/mod.rs | 2 - druid/src/widget/sub_window.rs | 114 -------------------------------- 8 files changed, 136 insertions(+), 167 deletions(-) delete mode 100644 druid/src/widget/sub_window.rs diff --git a/druid/examples/sub_window.rs b/druid/examples/sub_window.rs index 1bb4e146e4..f3983e9e5d 100644 --- a/druid/examples/sub_window.rs +++ b/druid/examples/sub_window.rs @@ -14,9 +14,7 @@ use druid::commands::CLOSE_WINDOW; use druid::lens::Unit; -use druid::widget::{ - Align, Button, Controller, ControllerHost, Flex, Label, SubWindowHost, TextBox, -}; +use druid::widget::{Align, Button, Controller, ControllerHost, Flex, Label, TextBox}; use druid::{ theme, Affine, AppLauncher, BoxConstraints, Color, Data, Env, Event, EventCtx, LayoutCtx, Lens, LensExt, LifeCycle, LifeCycleCtx, LocalizedString, PaintCtx, Point, Rect, RenderContext, Size, @@ -136,8 +134,7 @@ impl> Controller for TooltipController { window_pos: *window_pos, }) } else { - let req = SubWindowHost::make_requirement( - ctx.widget_id(), + let win_id = ctx.new_sub_window( WindowConfig::default() .show_titlebar(false) .window_size(Size::new(100.0, 23.0)) @@ -147,12 +144,9 @@ impl> Controller for TooltipController { + window_pos.to_vec2() + cursor_size.to_vec2(), ), - false, Label::<()>::new(self.tip.clone()), (), ); - let win_id = req.window_id; - ctx.new_sub_window(req); Some(TooltipState::Showing(win_id)) } } @@ -326,19 +320,16 @@ fn build_root_widget() -> impl Widget { let tb = TextBox::new().lens(SubState::my_stuff); let drag_thing = Label::new("Drag me").controller(DragWindowController::new()); let col = Flex::column().with_child(drag_thing).with_child(tb); - let req = SubWindowHost::make_requirement( - ctx.widget_id(), + + ctx.new_sub_window( WindowConfig::default() .show_titlebar(false) .window_size(Size::new(100., 100.)) .set_position(Point::new(1000.0, 500.0)) .set_level(WindowLevel::AppWindow), - true, col, data.clone(), ); - - ctx.new_sub_window(req) }) .center() .lens(HelloState::sub); diff --git a/druid/src/command.rs b/druid/src/command.rs index 765a635b9c..41350c054b 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -170,7 +170,9 @@ pub mod sys { use std::any::Any; use super::Selector; - use crate::{FileDialogOptions, FileInfo, SingleUse, SubWindowRequirement, WindowConfig}; + use crate::{ + sub_window::SubWindowRequirement, FileDialogOptions, FileInfo, SingleUse, WindowConfig, + }; /// Quit the running application. This command is handled by the druid library. pub const QUIT_APP: Selector = Selector::new("druid-builtin.quit-app"); diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 50912a73d9..1965239dca 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -26,9 +26,9 @@ use crate::env::KeyLike; use crate::piet::{Piet, PietText, RenderContext}; use crate::shell::Region; use crate::{ - commands, Affine, Command, ContextMenu, Cursor, Env, ExtEventSink, Insets, MenuDesc, - Notification, Point, Rect, SingleUse, Size, SubWindowRequirement, Target, TimerToken, WidgetId, WindowDesc, - WindowHandle, WindowId, + commands, sub_window::SubWindowRequirement, widget::Widget, Affine, Command, ContextMenu, + Cursor, Data, Env, ExtEventSink, Insets, MenuDesc, Notification, Point, Rect, SingleUse, Size, + Target, TimerToken, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, }; /// A macro for implementing methods on multiple contexts. @@ -354,12 +354,17 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// Create a new sub window that will have its app data synchronised with the nearest surrounding widget pod. // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. - pub fn new_sub_window(&mut self, requirement: SubWindowRequirement) { - if let Some(id) = requirement.host_id { - self.widget_state.add_sub_window_host(id); - } - - self.submit_command(commands::NEW_SUB_WINDOW.with(SingleUse::new(requirement))); + pub fn new_sub_window + 'static, U: Data>( + &mut self, + window_config: WindowConfig, + widget: W, + data: U, + ) -> WindowId { + let req = SubWindowRequirement::new(self.widget_id(), window_config, widget, data); + let window_id = req.window_id; + self.widget_state.add_sub_window_host(req.host_id); + self.submit_command(commands::NEW_SUB_WINDOW.with(SingleUse::new(req))); + window_id } }); diff --git a/druid/src/core.rs b/druid/src/core.rs index fd2faa0857..316b04ef1e 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -133,7 +133,7 @@ pub(crate) struct WidgetState { pub(crate) cursor: Option, // Port -> Host - pub(crate) sub_window_hosts: Option>, + pub(crate) sub_window_hosts: Vec, } /// Methods by which a widget can attempt to change focus state. @@ -1009,13 +1009,10 @@ impl> WidgetPod { } } - if let Some(hosts) = &self.state.sub_window_hosts { - for host in hosts { - let cloned: T = (*data).clone(); - let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, Box::new(cloned), *host); - - ctx.submit_command(command) - } + for host in &self.state.sub_window_hosts { + let cloned: T = (*data).clone(); + let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, Box::new(cloned), *host); + ctx.submit_command(command); } let prev_env = self.env.as_ref().filter(|p| !p.same(env)); @@ -1072,7 +1069,7 @@ impl WidgetState { timers: HashMap::new(), cursor_change: CursorChange::Default, cursor: None, - sub_window_hosts: None, + sub_window_hosts: Vec::new(), } } @@ -1152,6 +1149,10 @@ impl WidgetState { pub(crate) fn layout_rect(&self) -> Rect { Rect::from_origin_size(self.origin, self.size) } + + pub(crate) fn add_sub_window_host(&mut self, host_id: WidgetId) { + self.sub_window_hosts.push(host_id) + } } impl CursorChange { @@ -1161,14 +1162,6 @@ impl CursorChange { CursorChange::Default => None, } } - - pub(crate) fn add_sub_window_host(&mut self, host_id: WidgetId) { - if let Some(ports) = &mut self.sub_window_hosts { - ports.push(host_id); - } else { - self.sub_window_hosts = Some(vec![host_id]) - } - } } #[cfg(test)] diff --git a/druid/src/lib.rs b/druid/src/lib.rs index bd0d568759..d30c60ead8 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -206,7 +206,6 @@ pub use lens::{Lens, LensExt}; pub use localization::LocalizedString; pub use menu::{sys as platform_menus, ContextMenu, MenuDesc, MenuItem}; pub use mouse::MouseEvent; -pub use sub_window::SubWindowRequirement; pub use text::{ArcStr, FontDescriptor, TextLayout}; pub use util::Handled; pub use widget::{Widget, WidgetExt, WidgetId}; diff --git a/druid/src/sub_window.rs b/druid/src/sub_window.rs index 85554a7fcc..9f155ccbc1 100644 --- a/druid/src/sub_window.rs +++ b/druid/src/sub_window.rs @@ -1,15 +1,21 @@ use crate::app::{PendingWindow, WindowConfig}; +use crate::command::sys::SUB_WINDOW_HOST_TO_PARENT; +use crate::commands::SUB_WINDOW_PARENT_TO_HOST; use crate::lens::Unit; +use crate::widget::prelude::*; use crate::win_handler::AppState; -use crate::{Data, Widget, WidgetExt, WidgetId, WindowHandle, WindowId}; +use crate::{ + Command, Data, Point, Rect, Widget, WidgetExt, WidgetId, WidgetPod, WindowHandle, WindowId, +}; use druid_shell::Error; +use std::ops::Deref; // We can't have any type arguments here, as both ends would need to know them // ahead of time in order to instantiate correctly. // So we erase everything to () /// The required information to create a sub window, including the widget it should host, and the -pub struct SubWindowRequirement { - pub(crate) host_id: Option, // Present if updates should be sent from the pod to the sub window. +pub(crate) struct SubWindowRequirement { + pub(crate) host_id: WidgetId, pub(crate) sub_window_root: Box>, pub(crate) window_config: WindowConfig, /// The window id that the sub window will have once it is created. Can be used to send commands to. @@ -17,17 +23,26 @@ pub struct SubWindowRequirement { } impl SubWindowRequirement { - pub(crate) fn new( - host_id: Option, - sub_window_root: Box>, + /// Creates a subwindow requirement that hosts the provided widget within a sub window host. + /// It will synchronise data updates with the provided parent_id if "sync" is true, and it will expect to be sent + /// SUB_WINDOW_PARENT_TO_HOST commands to update the provided data for the widget. + pub fn new>( + parent_id: WidgetId, window_config: WindowConfig, - window_id: WindowId, - ) -> Self { + widget: W, + data: U, + ) -> SubWindowRequirement + where + W: 'static, + U: Data, + { + let host_id = WidgetId::next(); + let sub_window_host = SubWindowHost::new(host_id, parent_id, data, widget).boxed(); SubWindowRequirement { host_id, - sub_window_root, + sub_window_root: sub_window_host, window_config, - window_id, + window_id: WindowId::next(), } } @@ -40,3 +55,83 @@ impl SubWindowRequirement { app_state.build_native_window(self.window_id, pending, self.window_config) } } + +struct SubWindowHost> { + id: WidgetId, + parent_id: WidgetId, + data: U, + child: WidgetPod, +} + +impl> SubWindowHost { + pub(crate) fn new(id: WidgetId, parent_id: WidgetId, data: U, widget: W) -> Self { + SubWindowHost { + id, + parent_id, + data, + child: WidgetPod::new(widget), + } + } +} + +impl> Widget<()> for SubWindowHost { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), env: &Env) { + match event { + Event::Command(cmd) if cmd.is(SUB_WINDOW_PARENT_TO_HOST) => { + if let Some(update) = cmd + .get_unchecked(SUB_WINDOW_PARENT_TO_HOST) + .downcast_ref::() + { + self.data = update.deref().clone(); + ctx.request_update(); + } else { + log::warn!("Received a sub window parent to host command that could not be unwrapped. \ + This could mean that the sub window you requested and the enclosing widget pod that you opened it from do not share a common data type. \ + Make sure you have a widget pod between your requesting widget and any lenses." ) + } + ctx.set_handled(); + } + _ => { + let old = self.data.clone(); // Could avoid this by keeping two bit of data or if we could ask widget pod? + self.child.event(ctx, event, &mut self.data, env); + if !old.same(&self.data) { + ctx.submit_command(Command::new( + SUB_WINDOW_HOST_TO_PARENT, + Box::new(self.data.clone()), + self.parent_id, + )) + } + } + } + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &(), env: &Env) { + self.child.lifecycle(ctx, event, &self.data, env) + } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &(), _data: &(), env: &Env) { + if ctx.has_requested_update() { + // Should env be copied from the parent too? Possibly + self.child.update(ctx, &self.data, env); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &(), env: &Env) -> Size { + let size = self.child.layout(ctx, bc, &self.data, env); + self.child.set_layout_rect( + ctx, + &self.data, + env, + Rect::from_origin_size(Point::ORIGIN, size), + ); + size + } + + fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) { + self.child.paint_raw(ctx, &self.data, env); + } + + fn id(&self) -> Option { + Some(self.id) + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 728b0bd3e2..d5a96fb720 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -43,7 +43,6 @@ mod slider; mod spinner; mod split; mod stepper; -mod sub_window; #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] mod svg; @@ -83,7 +82,6 @@ pub use slider::Slider; pub use spinner::Spinner; pub use split::Split; pub use stepper::Stepper; -pub use sub_window::SubWindowHost; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; diff --git a/druid/src/widget/sub_window.rs b/druid/src/widget/sub_window.rs deleted file mode 100644 index 5c69611532..0000000000 --- a/druid/src/widget/sub_window.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::app::WindowConfig; -use crate::command::sys::SUB_WINDOW_PARENT_TO_HOST; -use crate::commands::SUB_WINDOW_HOST_TO_PARENT; -use crate::{ - BoxConstraints, Command, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, - PaintCtx, Point, Rect, Size, SubWindowRequirement, UpdateCtx, Widget, WidgetExt, WidgetId, - WidgetPod, WindowId, -}; -use std::ops::Deref; - -/// A widget currently meant to be used as the root of a sub window. -/// It ignores its own AppData, but provides its child with data synchronised -/// from the WidgetPod that created it. -pub struct SubWindowHost> { - id: WidgetId, - parent_id: WidgetId, - sync: bool, - data: U, - child: WidgetPod, -} - -impl> SubWindowHost { - fn new(id: WidgetId, parent_id: WidgetId, sync: bool, data: U, widget: W) -> Self { - SubWindowHost { - id, - parent_id, - sync, - data, - child: WidgetPod::new(widget), - } - } - - /// Creates a subwindow requirement that hosts the provided widget within a sub window host. - /// It will synchronise data updates with the provided parent_id if "sync" is true, and it will expect to be sent - /// SUB_WINDOW_PARENT_TO_HOST commands to update the provided data for the widget. - pub fn make_requirement( - parent_id: WidgetId, - window_config: WindowConfig, - sync: bool, - widget: W, - data: U, - ) -> SubWindowRequirement - where - W: 'static, - U: Data, - { - let host_id = WidgetId::next(); - let sub_window_host = SubWindowHost::new(host_id, parent_id, sync, data, widget).boxed(); - let host_id = if sync { Some(host_id) } else { None }; - SubWindowRequirement::new(host_id, sub_window_host, window_config, WindowId::next()) - } -} - -impl> Widget<()> for SubWindowHost { - fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), env: &Env) { - match event { - Event::Command(cmd) if self.sync && cmd.is(SUB_WINDOW_PARENT_TO_HOST) => { - if let Some(update) = cmd - .get_unchecked(SUB_WINDOW_PARENT_TO_HOST) - .downcast_ref::() - { - self.data = update.deref().clone(); - ctx.request_update(); - } else { - log::warn!("Received a sub window parent to host command that could not be unwrapped. \ - This could mean that the sub window you requested and the enclosing widget pod that you opened it from do not share a common data type. \ - Make sure you have a widget pod between your requesting widget and any lenses." ) - } - ctx.set_handled(); - } - _ => { - let old = self.data.clone(); // Could avoid this by keeping two bit of data or if we could ask widget pod? - self.child.event(ctx, event, &mut self.data, env); - if self.sync && !old.same(&self.data) { - ctx.submit_command(Command::new( - SUB_WINDOW_HOST_TO_PARENT, - Box::new(self.data.clone()), - self.parent_id, - )) - } - } - } - } - - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &(), env: &Env) { - self.child.lifecycle(ctx, event, &self.data, env) - } - - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &(), _data: &(), env: &Env) { - if ctx.has_requested_update() { - // Should env be copied from the parent too? Possibly - self.child.update(ctx, &self.data, env); - } - } - - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &(), env: &Env) -> Size { - let size = self.child.layout(ctx, bc, &self.data, env); - self.child.set_layout_rect( - ctx, - &self.data, - env, - Rect::from_origin_size(Point::ORIGIN, size), - ); - size - } - - fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) { - self.child.paint_raw(ctx, &self.data, env); - } - - fn id(&self) -> Option { - Some(self.id) - } -} From d962c74886b84f2d1f9247bfb59fe304020fa4ab Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Tue, 5 Jan 2021 17:40:15 +0000 Subject: [PATCH 3/9] Fix to mac window close handling. Add WindowDisconnected event. Close sub windows when their owning WidgetPod gets a WindowDisconnected event. --- CHANGELOG.md | 1 + druid-shell/src/platform/mac/window.rs | 14 +++++++++++++- druid/src/contexts.rs | 5 ++++- druid/src/core.rs | 18 ++++++++++++------ druid/src/event.rs | 3 +++ druid/src/win_handler.rs | 8 ++++++++ 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82449baa4b..7798ed6886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ You can find its changes [documented below](#070---2021-01-01). ### Added - Sub windows: Allow opening windows that share state with arbitrary parts of the widget hierarchy ([#1254] by [@rjwittams]) +- WindowDisconnected event when a window is closed ([#1254] by [@rjwittams]) ### Changed diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 8f43d80732..5763a487b7 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -419,6 +419,10 @@ lazy_static! { sel!(showContextMenu:), show_context_menu as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern "C" fn(&mut Object, Sel, id), + ); decl.add_method( sel!(windowWillClose:), window_will_close as extern "C" fn(&mut Object, Sel, id), @@ -827,7 +831,15 @@ extern "C" fn window_did_resign_key(this: &mut Object, _: Sel, _notification: id } } -extern "C" fn window_will_close(this: &mut Object, _: Sel, _window: id) { +extern "C" fn window_should_close(this: &mut Object, _: Sel, _window: id) { + unsafe { + let view_state: *mut c_void = *this.get_ivar("viewState"); + let view_state = &mut *(view_state as *mut ViewState); + (*view_state).handler.request_close(); + } +} + +extern "C" fn window_will_close(this: &mut Object, _: Sel, _notification: id) { unsafe { let view_state: *mut c_void = *this.get_ivar("viewState"); let view_state = &mut *(view_state as *mut ViewState); diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 1965239dca..994d7ad47d 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -352,6 +352,8 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, } /// Create a new sub window that will have its app data synchronised with the nearest surrounding widget pod. + /// 'U' must be the type of the nearest surrounding widget pod. The 'data' argument should be the current value of data + /// for that widget. // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. pub fn new_sub_window + 'static, U: Data>( @@ -362,7 +364,8 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, ) -> WindowId { let req = SubWindowRequirement::new(self.widget_id(), window_config, widget, data); let window_id = req.window_id; - self.widget_state.add_sub_window_host(req.host_id); + self.widget_state + .add_sub_window_host(window_id, req.host_id); self.submit_command(commands::NEW_SUB_WINDOW.with(SingleUse::new(req))); window_id } diff --git a/druid/src/core.rs b/druid/src/core.rs index 316b04ef1e..f0c13bfcda 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -17,14 +17,14 @@ use std::collections::{HashMap, VecDeque}; use crate::bloom::Bloom; -use crate::command::sys::{SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; +use crate::command::sys::{CLOSE_WINDOW, SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; use crate::util::ExtendDrain; use crate::{ ArcStr, BoxConstraints, Color, Command, Cursor, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, Notification, PaintCtx, Region, - RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, + RenderContext, Target, TextLayout, TimerToken, UpdateCtx, Widget, WidgetId, WindowId, }; /// Our queue type @@ -133,7 +133,7 @@ pub(crate) struct WidgetState { pub(crate) cursor: Option, // Port -> Host - pub(crate) sub_window_hosts: Vec, + pub(crate) sub_window_hosts: Vec<(WindowId, WidgetId)>, } /// Methods by which a widget can attempt to change focus state. @@ -679,6 +679,12 @@ impl> WidgetPod { } }, Event::WindowConnected => true, + Event::WindowDisconnected => { + for (window_id, _) in &self.state.sub_window_hosts { + ctx.submit_command(CLOSE_WINDOW.to(*window_id)) + } + true + } Event::WindowSize(_) => { self.state.needs_layout = true; ctx.is_root @@ -1009,7 +1015,7 @@ impl> WidgetPod { } } - for host in &self.state.sub_window_hosts { + for (_, host) in &self.state.sub_window_hosts { let cloned: T = (*data).clone(); let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, Box::new(cloned), *host); ctx.submit_command(command); @@ -1150,8 +1156,8 @@ impl WidgetState { Rect::from_origin_size(self.origin, self.size) } - pub(crate) fn add_sub_window_host(&mut self, host_id: WidgetId) { - self.sub_window_hosts.push(host_id) + pub(crate) fn add_sub_window_host(&mut self, window_id: WindowId, host_id: WidgetId) { + self.sub_window_hosts.push((window_id, host_id)) } } diff --git a/druid/src/event.rs b/druid/src/event.rs index e4c8ff97c1..de2d6d6070 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -60,6 +60,8 @@ pub enum Event { /// /// [`LifeCycle::WidgetAdded`]: enum.LifeCycle.html#variant.WidgetAdded WindowConnected, + /// Sent to all widgets in a given window when that window is closed. + WindowDisconnected, /// Called on the root widget when the window size changes. /// /// Discussion: it's not obvious this should be propagated to user @@ -353,6 +355,7 @@ impl Event { pub fn should_propagate_to_hidden(&self) -> bool { match self { Event::WindowConnected + | Event::WindowDisconnected | Event::WindowSize(_) | Event::Timer(_) | Event::AnimFrame(_) diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index 2a145fed62..b01db7a7df 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -352,6 +352,13 @@ impl Inner { self.show_context_menu(id, &cmd); return Handled::Yes; } + if cmd.is(sys_cmd::CLOSE_WINDOW) { + let event = Event::WindowDisconnected; + if let Some(w) = self.windows.get_mut(id) { + w.event(&mut self.command_queue, event, &mut self.data, &self.env); + } + return Handled::No; // We return no because we haven't actually closed the window. + } if let Some(w) = self.windows.get_mut(id) { let event = Event::Command(cmd); return w.event(&mut self.command_queue, event, &mut self.data, &self.env); @@ -897,6 +904,7 @@ impl WinHandler for DruidHandler { fn request_close(&mut self) { self.app_state .handle_cmd(sys_cmd::CLOSE_WINDOW.to(self.window_id)); + self.app_state.process_commands(); self.app_state.inner.borrow_mut().do_update(); } From 510032b84cff982f845a55f4e87704afd689295f Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Tue, 5 Jan 2021 19:34:45 +0000 Subject: [PATCH 4/9] Add WindowCloseRequested event and make window close cancellation work on mac. --- CHANGELOG.md | 2 +- druid-shell/src/platform/mac/window.rs | 5 ++-- druid/examples/sub_window.rs | 27 +++++++++++++++++++-- druid/src/core.rs | 2 +- druid/src/event.rs | 9 ++++++- druid/src/win_handler.rs | 33 +++++++++++++++++++------- 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7798ed6886..f0ea1eea41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ You can find its changes [documented below](#070---2021-01-01). ### Added - Sub windows: Allow opening windows that share state with arbitrary parts of the widget hierarchy ([#1254] by [@rjwittams]) -- WindowDisconnected event when a window is closed ([#1254] by [@rjwittams]) +- WindowCloseRequested/WindowDisconnected event when a window is closing ([#1254] by [@rjwittams]) ### Changed diff --git a/druid-shell/src/platform/mac/window.rs b/druid-shell/src/platform/mac/window.rs index 5763a487b7..2acb6f789b 100644 --- a/druid-shell/src/platform/mac/window.rs +++ b/druid-shell/src/platform/mac/window.rs @@ -421,7 +421,7 @@ lazy_static! { ); decl.add_method( sel!(windowShouldClose:), - window_should_close as extern "C" fn(&mut Object, Sel, id), + window_should_close as extern "C" fn(&mut Object, Sel, id)->BOOL, ); decl.add_method( sel!(windowWillClose:), @@ -831,11 +831,12 @@ extern "C" fn window_did_resign_key(this: &mut Object, _: Sel, _notification: id } } -extern "C" fn window_should_close(this: &mut Object, _: Sel, _window: id) { +extern "C" fn window_should_close(this: &mut Object, _: Sel, _window: id) -> BOOL { unsafe { let view_state: *mut c_void = *this.get_ivar("viewState"); let view_state = &mut *(view_state as *mut ViewState); (*view_state).handler.request_close(); + NO } } diff --git a/druid/examples/sub_window.rs b/druid/examples/sub_window.rs index f3983e9e5d..a0ff43f74f 100644 --- a/druid/examples/sub_window.rs +++ b/druid/examples/sub_window.rs @@ -14,7 +14,7 @@ use druid::commands::CLOSE_WINDOW; use druid::lens::Unit; -use druid::widget::{Align, Button, Controller, ControllerHost, Flex, Label, TextBox}; +use druid::widget::{Align, Button, Checkbox, Controller, ControllerHost, Flex, Label, TextBox}; use druid::{ theme, Affine, AppLauncher, BoxConstraints, Color, Data, Env, Event, EventCtx, LayoutCtx, Lens, LensExt, LifeCycle, LifeCycleCtx, LocalizedString, PaintCtx, Point, Rect, RenderContext, Size, @@ -38,6 +38,7 @@ struct SubState { struct HelloState { name: String, sub: SubState, + closeable: bool, } pub fn main() { @@ -52,6 +53,7 @@ pub fn main() { sub: SubState { my_stuff: "It's mine!".into(), }, + closeable: true, }; // start the application @@ -302,6 +304,24 @@ impl Widget<()> for ScreenThing { } } +struct CancelClose; + +impl> Controller for CancelClose { + fn event( + &mut self, + w: &mut W, + ctx: &mut EventCtx<'_, '_>, + event: &Event, + data: &mut bool, + env: &Env, + ) { + match (&data, event) { + (false, Event::WindowCloseRequested) => ctx.set_handled(), + _ => w.event(ctx, event, data, env), + } + } +} + fn build_root_widget() -> impl Widget { let label = ControllerHost::new( Label::new(|data: &HelloState, _env: &Env| { @@ -334,13 +354,16 @@ fn build_root_widget() -> impl Widget { .center() .lens(HelloState::sub); + let check_box = + ControllerHost::new(Checkbox::new("Closeable?"), CancelClose).lens(HelloState::closeable); // arrange the two widgets vertically, with some padding let layout = Flex::column() .with_child(label) .with_flex_child(ScreenThing.lens(Unit::default()).padding(5.), 1.) .with_spacer(VERTICAL_WIDGET_SPACING) .with_child(textbox) - .with_child(button); + .with_child(button) + .with_child(check_box); // center the two widgets in the available space Align::centered(layout) diff --git a/druid/src/core.rs b/druid/src/core.rs index f0c13bfcda..d914e98cf5 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -678,7 +678,7 @@ impl> WidgetPod { } } }, - Event::WindowConnected => true, + Event::WindowConnected | Event::WindowCloseRequested => true, Event::WindowDisconnected => { for (window_id, _) in &self.state.sub_window_hosts { ctx.submit_command(CLOSE_WINDOW.to(*window_id)) diff --git a/druid/src/event.rs b/druid/src/event.rs index de2d6d6070..2189eb985f 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -60,7 +60,13 @@ pub enum Event { /// /// [`LifeCycle::WidgetAdded`]: enum.LifeCycle.html#variant.WidgetAdded WindowConnected, - /// Sent to all widgets in a given window when that window is closed. + /// Sent to all widgets in a given window when the system requests a closed. + /// If the event is handled, the window will not be closed. + /// It could be cancelled by another widget, so no destructive side effects are advised. + WindowCloseRequested, + /// Sent to all widgets in a given window when the system is going to close a window. + /// It can't be cancelled at this point, so its safe to dispose of resources that should go away + /// when the window closes. WindowDisconnected, /// Called on the root widget when the window size changes. /// @@ -355,6 +361,7 @@ impl Event { pub fn should_propagate_to_hidden(&self) -> bool { match self { Event::WindowConnected + | Event::WindowCloseRequested | Event::WindowDisconnected | Event::WindowSize(_) | Event::Timer(_) diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index b01db7a7df..a4d0f19878 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -352,16 +352,31 @@ impl Inner { self.show_context_menu(id, &cmd); return Handled::Yes; } - if cmd.is(sys_cmd::CLOSE_WINDOW) { - let event = Event::WindowDisconnected; - if let Some(w) = self.windows.get_mut(id) { - w.event(&mut self.command_queue, event, &mut self.data, &self.env); - } - return Handled::No; // We return no because we haven't actually closed the window. - } if let Some(w) = self.windows.get_mut(id) { - let event = Event::Command(cmd); - return w.event(&mut self.command_queue, event, &mut self.data, &self.env); + return if cmd.is(sys_cmd::CLOSE_WINDOW) { + let handled = w.event( + &mut self.command_queue, + Event::WindowCloseRequested, + &mut self.data, + &self.env, + ); + if !handled.is_handled() { + w.event( + &mut self.command_queue, + Event::WindowDisconnected, + &mut self.data, + &self.env, + ); + } + handled + } else { + w.event( + &mut self.command_queue, + Event::Command(cmd), + &mut self.data, + &self.env, + ) + }; } } // in this case we send it to every window that might contain From ae8732b45c850d7625ce83c20bb0538e89b5f70b Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 6 Jan 2021 16:09:25 +0000 Subject: [PATCH 5/9] Update druid/src/event.rs Co-authored-by: Colin Rofls --- druid/src/event.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/druid/src/event.rs b/druid/src/event.rs index 2189eb985f..4ab0781767 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -60,9 +60,12 @@ pub enum Event { /// /// [`LifeCycle::WidgetAdded`]: enum.LifeCycle.html#variant.WidgetAdded WindowConnected, - /// Sent to all widgets in a given window when the system requests a closed. - /// If the event is handled, the window will not be closed. - /// It could be cancelled by another widget, so no destructive side effects are advised. + /// Sent to all widgets in a given window when the system requests to close the window. + /// + /// If the event is handled (with [`EventCtx::set_handle`]), the window will not be closed. + /// All widgets are given an opportunity to handle this event; your widget should not assume + /// that the window *will* close just because this event is received; for instance, you should + /// avoid destructive side effects such as cleaning up resources. WindowCloseRequested, /// Sent to all widgets in a given window when the system is going to close a window. /// It can't be cancelled at this point, so its safe to dispose of resources that should go away From f3793f225bc048b7bd63c90fd5c0bc4784f33271 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 6 Jan 2021 16:14:07 +0000 Subject: [PATCH 6/9] Update druid/src/event.rs Co-authored-by: Colin Rofls --- druid/src/event.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/druid/src/event.rs b/druid/src/event.rs index 4ab0781767..35f85c25b9 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -67,9 +67,10 @@ pub enum Event { /// that the window *will* close just because this event is received; for instance, you should /// avoid destructive side effects such as cleaning up resources. WindowCloseRequested, - /// Sent to all widgets in a given window when the system is going to close a window. - /// It can't be cancelled at this point, so its safe to dispose of resources that should go away - /// when the window closes. + /// Sent to all widgets in a given window when the system is going to close that window. + /// + /// This event means the window *will* go away; it is safe to dispose of resources and + /// do any other cleanup. WindowDisconnected, /// Called on the root widget when the window size changes. /// From 913261e588c4b6d65dce1a73407c0045a5e2bf93 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 6 Jan 2021 18:49:54 +0000 Subject: [PATCH 7/9] Changes to allow env updates to flow down to sub windows --- druid/examples/sub_window.rs | 23 ++++++---- druid/src/command.rs | 11 +++-- druid/src/contexts.rs | 19 ++++---- druid/src/core.rs | 25 ++++++++--- druid/src/event.rs | 4 +- druid/src/sub_window.rs | 87 +++++++++++++++++++++++------------- druid/src/win_handler.rs | 8 ++-- 7 files changed, 115 insertions(+), 62 deletions(-) diff --git a/druid/examples/sub_window.rs b/druid/examples/sub_window.rs index a0ff43f74f..19afbf9a14 100644 --- a/druid/examples/sub_window.rs +++ b/druid/examples/sub_window.rs @@ -1,4 +1,4 @@ -// Copyright 2019 The Druid Authors. +// Copyright 2021 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. @@ -14,7 +14,9 @@ use druid::commands::CLOSE_WINDOW; use druid::lens::Unit; -use druid::widget::{Align, Button, Checkbox, Controller, ControllerHost, Flex, Label, TextBox}; +use druid::widget::{ + Align, Button, Checkbox, Controller, ControllerHost, EnvScope, Flex, Label, TextBox, +}; use druid::{ theme, Affine, AppLauncher, BoxConstraints, Color, Data, Env, Event, EventCtx, LayoutCtx, Lens, LensExt, LifeCycle, LifeCycleCtx, LocalizedString, PaintCtx, Point, Rect, RenderContext, Size, @@ -148,6 +150,7 @@ impl> Controller for TooltipController { ), Label::<()>::new(self.tip.clone()), (), + env.clone(), ); Some(TooltipState::Showing(win_id)) } @@ -323,11 +326,14 @@ impl> Controller for CancelClose { } fn build_root_widget() -> impl Widget { - let label = ControllerHost::new( - Label::new(|data: &HelloState, _env: &Env| { - format!("Hello {}! {} ", data.name, data.sub.my_stuff) - }), - TooltipController::new("Tips! Are good"), + let label = EnvScope::new( + |env, _t| env.set(theme::LABEL_COLOR, env.get(theme::PRIMARY_LIGHT)), + ControllerHost::new( + Label::new(|data: &HelloState, _env: &Env| { + format!("Hello {}! {} ", data.name, data.sub.my_stuff) + }), + TooltipController::new("Tips! Are good"), + ), ); // a textbox that modifies `name`. let textbox = TextBox::new() @@ -336,7 +342,7 @@ fn build_root_widget() -> impl Widget { .lens(HelloState::sub.then(SubState::my_stuff)); let button = Button::new("Make sub window") - .on_click(|ctx, data: &mut SubState, _env| { + .on_click(|ctx, data: &mut SubState, env| { let tb = TextBox::new().lens(SubState::my_stuff); let drag_thing = Label::new("Drag me").controller(DragWindowController::new()); let col = Flex::column().with_child(drag_thing).with_child(tb); @@ -349,6 +355,7 @@ fn build_root_widget() -> impl Widget { .set_level(WindowLevel::AppWindow), col, data.clone(), + env.clone(), ); }) .center() diff --git a/druid/src/command.rs b/druid/src/command.rs index 41350c054b..16ee68b416 100644 --- a/druid/src/command.rs +++ b/druid/src/command.rs @@ -171,7 +171,8 @@ pub mod sys { use super::Selector; use crate::{ - sub_window::SubWindowRequirement, FileDialogOptions, FileInfo, SingleUse, WindowConfig, + sub_window::{SubWindowDesc, SubWindowUpdate}, + FileDialogOptions, FileInfo, SingleUse, WindowConfig, }; /// Quit the running application. This command is handled by the druid library. @@ -215,12 +216,16 @@ pub mod sys { pub(crate) const SHOW_CONTEXT_MENU: Selector> = Selector::new("druid-builtin.show-context-menu"); - pub(crate) const NEW_SUB_WINDOW: Selector> = + /// This is sent to the window handler to create a new sub window. + pub(crate) const NEW_SUB_WINDOW: Selector> = Selector::new("druid-builtin.new-sub-window"); - pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector> = + /// This is sent from a WidgetPod to any attached SubWindowHosts when a data update occurs + pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector = Selector::new("druid-builtin.parent_to_host"); + /// This is sent from a SubWindowHost to its parent WidgetPod after it has processed events, + /// if that processing changed the data value. pub(crate) const SUB_WINDOW_HOST_TO_PARENT: Selector> = Selector::new("druid-builtin.host_to_parent"); diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 994d7ad47d..72fbb78fc3 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -26,9 +26,9 @@ use crate::env::KeyLike; use crate::piet::{Piet, PietText, RenderContext}; use crate::shell::Region; use crate::{ - commands, sub_window::SubWindowRequirement, widget::Widget, Affine, Command, ContextMenu, - Cursor, Data, Env, ExtEventSink, Insets, MenuDesc, Notification, Point, Rect, SingleUse, Size, - Target, TimerToken, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, + commands, sub_window::SubWindowDesc, widget::Widget, Affine, Command, ContextMenu, Cursor, + Data, Env, ExtEventSink, Insets, MenuDesc, Notification, Point, Rect, SingleUse, Size, Target, + TimerToken, WidgetId, WindowConfig, WindowDesc, WindowHandle, WindowId, }; /// A macro for implementing methods on multiple contexts. @@ -351,18 +351,21 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, self.state.set_menu(menu); } - /// Create a new sub window that will have its app data synchronised with the nearest surrounding widget pod. - /// 'U' must be the type of the nearest surrounding widget pod. The 'data' argument should be the current value of data - /// for that widget. + /// Create a new sub-window. + /// + /// The sub-window will have its app data synchronised with caller's nearest ancestor [`WidgetPod`]. + /// 'U' must be the type of the nearest surrounding [`WidgetPod`]. The 'data' argument should be + /// the current value of data for that widget. // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the - // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. + // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. pub fn new_sub_window + 'static, U: Data>( pub fn new_sub_window + 'static, U: Data>( &mut self, window_config: WindowConfig, widget: W, data: U, + env: Env, ) -> WindowId { - let req = SubWindowRequirement::new(self.widget_id(), window_config, widget, data); + let req = SubWindowDesc::new(self.widget_id(), window_config, widget, data, env); let window_id = req.window_id; self.widget_state .add_sub_window_host(window_id, req.host_id); diff --git a/druid/src/core.rs b/druid/src/core.rs index 7cc6384b27..23cfdd56a9 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -20,6 +20,7 @@ use crate::bloom::Bloom; use crate::command::sys::{CLOSE_WINDOW, SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; use crate::contexts::ContextState; use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2}; +use crate::sub_window::SubWindowUpdate; use crate::util::ExtendDrain; use crate::{ ArcStr, BoxConstraints, Color, Command, Cursor, Data, Env, Event, EventCtx, InternalEvent, @@ -1014,14 +1015,24 @@ impl> WidgetPod { (Some(_), Some(_)) => {} } } - - for (_, host) in &self.state.sub_window_hosts { - let cloned: T = (*data).clone(); - let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, Box::new(cloned), *host); - ctx.submit_command(command); - } - let prev_env = self.env.as_ref().filter(|p| !p.same(env)); + let env_changed = prev_env.is_some(); + let data_changed = self.old_data.as_ref().filter(|p| !p.same(data)).is_some(); + + if env_changed || data_changed { + for (_, host) in &self.state.sub_window_hosts { + let update = SubWindowUpdate { + data: if data_changed { + Some(Box::new((*data).clone())) + } else { + None + }, + env: if env_changed { Some(env.clone()) } else { None }, + }; + let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, update, *host); + ctx.submit_command(command); + } + } let mut child_ctx = UpdateCtx { state: ctx.state, diff --git a/druid/src/event.rs b/druid/src/event.rs index 35f85c25b9..2736c071ff 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -62,10 +62,12 @@ pub enum Event { WindowConnected, /// Sent to all widgets in a given window when the system requests to close the window. /// - /// If the event is handled (with [`EventCtx::set_handle`]), the window will not be closed. + /// If the event is handled (with [`set_handled`]), the window will not be closed. /// All widgets are given an opportunity to handle this event; your widget should not assume /// that the window *will* close just because this event is received; for instance, you should /// avoid destructive side effects such as cleaning up resources. + /// + /// [`set_handled`]: struct.EventCtx.html#method.set_handled WindowCloseRequested, /// Sent to all widgets in a given window when the system is going to close that window. /// diff --git a/druid/src/sub_window.rs b/druid/src/sub_window.rs index 9f155ccbc1..f73099739e 100644 --- a/druid/src/sub_window.rs +++ b/druid/src/sub_window.rs @@ -1,6 +1,19 @@ +// Copyright 2021 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. + use crate::app::{PendingWindow, WindowConfig}; -use crate::command::sys::SUB_WINDOW_HOST_TO_PARENT; -use crate::commands::SUB_WINDOW_PARENT_TO_HOST; +use crate::commands::{SUB_WINDOW_HOST_TO_PARENT, SUB_WINDOW_PARENT_TO_HOST}; use crate::lens::Unit; use crate::widget::prelude::*; use crate::win_handler::AppState; @@ -8,13 +21,15 @@ use crate::{ Command, Data, Point, Rect, Widget, WidgetExt, WidgetId, WidgetPod, WindowHandle, WindowId, }; use druid_shell::Error; +use std::any::Any; use std::ops::Deref; // We can't have any type arguments here, as both ends would need to know them // ahead of time in order to instantiate correctly. // So we erase everything to () /// The required information to create a sub window, including the widget it should host, and the -pub(crate) struct SubWindowRequirement { +/// config of the window to be created. +pub(crate) struct SubWindowDesc { pub(crate) host_id: WidgetId, pub(crate) sub_window_root: Box>, pub(crate) window_config: WindowConfig, @@ -22,7 +37,12 @@ pub(crate) struct SubWindowRequirement { pub window_id: WindowId, } -impl SubWindowRequirement { +pub(crate) struct SubWindowUpdate { + pub(crate) data: Option>, + pub(crate) env: Option, +} + +impl SubWindowDesc { /// Creates a subwindow requirement that hosts the provided widget within a sub window host. /// It will synchronise data updates with the provided parent_id if "sync" is true, and it will expect to be sent /// SUB_WINDOW_PARENT_TO_HOST commands to update the provided data for the widget. @@ -31,14 +51,15 @@ impl SubWindowRequirement { window_config: WindowConfig, widget: W, data: U, - ) -> SubWindowRequirement + env: Env, + ) -> SubWindowDesc where W: 'static, U: Data, { let host_id = WidgetId::next(); - let sub_window_host = SubWindowHost::new(host_id, parent_id, data, widget).boxed(); - SubWindowRequirement { + let sub_window_host = SubWindowHost::new(host_id, parent_id, widget, data, env).boxed(); + SubWindowDesc { host_id, sub_window_root: sub_window_host, window_config, @@ -59,41 +80,46 @@ impl SubWindowRequirement { struct SubWindowHost> { id: WidgetId, parent_id: WidgetId, - data: U, child: WidgetPod, + data: U, + env: Env, } impl> SubWindowHost { - pub(crate) fn new(id: WidgetId, parent_id: WidgetId, data: U, widget: W) -> Self { + pub(crate) fn new(id: WidgetId, parent_id: WidgetId, widget: W, data: U, env: Env) -> Self { SubWindowHost { id, parent_id, data, + env, child: WidgetPod::new(widget), } } } impl> Widget<()> for SubWindowHost { - fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), env: &Env) { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), _env: &Env) { match event { Event::Command(cmd) if cmd.is(SUB_WINDOW_PARENT_TO_HOST) => { - if let Some(update) = cmd - .get_unchecked(SUB_WINDOW_PARENT_TO_HOST) - .downcast_ref::() - { - self.data = update.deref().clone(); - ctx.request_update(); - } else { - log::warn!("Received a sub window parent to host command that could not be unwrapped. \ - This could mean that the sub window you requested and the enclosing widget pod that you opened it from do not share a common data type. \ - Make sure you have a widget pod between your requesting widget and any lenses." ) + let update = cmd.get_unchecked(SUB_WINDOW_PARENT_TO_HOST); + if let Some(data_update) = &update.data { + if let Some(dc) = data_update.downcast_ref::() { + self.data = dc.deref().clone(); + ctx.request_update(); + } else { + log::warn!("Received a sub window parent to host command that could not be unwrapped. \ + This could mean that the sub window you requested and the enclosing widget pod that you opened it from do not share a common data type. \ + Make sure you have a widget pod between your requesting widget and any lenses." ) + } + } + if let Some(env_update) = &update.env { + self.env = env_update.clone() } ctx.set_handled(); } _ => { let old = self.data.clone(); // Could avoid this by keeping two bit of data or if we could ask widget pod? - self.child.event(ctx, event, &mut self.data, env); + self.child.event(ctx, event, &mut self.data, &self.env); if !old.same(&self.data) { ctx.submit_command(Command::new( SUB_WINDOW_HOST_TO_PARENT, @@ -105,30 +131,29 @@ impl> Widget<()> for SubWindowHost { } } - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &(), env: &Env) { - self.child.lifecycle(ctx, event, &self.data, env) + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _data: &(), _env: &Env) { + self.child.lifecycle(ctx, event, &self.data, &self.env) } - fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &(), _data: &(), env: &Env) { + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &(), _data: &(), _env: &Env) { if ctx.has_requested_update() { - // Should env be copied from the parent too? Possibly - self.child.update(ctx, &self.data, env); + self.child.update(ctx, &self.data, &self.env); } } - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &(), env: &Env) -> Size { - let size = self.child.layout(ctx, bc, &self.data, env); + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &(), _env: &Env) -> Size { + let size = self.child.layout(ctx, bc, &self.data, &self.env); self.child.set_layout_rect( ctx, &self.data, - env, + &self.env, Rect::from_origin_size(Point::ORIGIN, size), ); size } - fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), env: &Env) { - self.child.paint_raw(ctx, &self.data, env); + fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), _env: &Env) { + self.child.paint_raw(ctx, &self.data, &self.env); } fn id(&self) -> Option { diff --git a/druid/src/win_handler.rs b/druid/src/win_handler.rs index a4d0f19878..db9476b082 100644 --- a/druid/src/win_handler.rs +++ b/druid/src/win_handler.rs @@ -727,19 +727,19 @@ impl AppState { fn new_sub_window(&mut self, cmd: Command) -> Result<(), Box> { if let Some(transfer) = cmd.get(sys_cmd::NEW_SUB_WINDOW) { - if let Some(swr) = transfer.take() { - let window = swr.make_sub_window(self)?; + if let Some(sub_window_desc) = transfer.take() { + let window = sub_window_desc.make_sub_window(self)?; window.show(); Ok(()) } else { panic!( - "{} command must carry a SubWindowRequirement internally", + "{} command must carry a SubWindowDesc internally", sys_cmd::NEW_SUB_WINDOW ) } } else { panic!( - "{} command must carry a SingleUse", + "{} command must carry a SingleUse", sys_cmd::NEW_SUB_WINDOW ) } From d5b394224446b1eb253af98cdb7b75a758c494b2 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 6 Jan 2021 19:20:37 +0000 Subject: [PATCH 8/9] Doc fix --- druid/src/contexts.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 72fbb78fc3..8e3ae75ada 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -356,6 +356,8 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// The sub-window will have its app data synchronised with caller's nearest ancestor [`WidgetPod`]. /// 'U' must be the type of the nearest surrounding [`WidgetPod`]. The 'data' argument should be /// the current value of data for that widget. + /// + /// [`WidgetPod`]: struct.WidgetPod.html // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. pub fn new_sub_window + 'static, U: Data>( pub fn new_sub_window + 'static, U: Data>( From e4a1b1702093d9ab70f91c1f9c7b238bac1c664f Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Thu, 7 Jan 2021 01:55:07 +0000 Subject: [PATCH 9/9] Review fixes --- druid/src/contexts.rs | 2 +- druid/src/core.rs | 15 ++++++++++----- druid/src/event.rs | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 8e3ae75ada..8690703fc9 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -359,7 +359,7 @@ impl_context_method!(EventCtx<'_, '_>, UpdateCtx<'_, '_>, LifeCycleCtx<'_, '_>, /// /// [`WidgetPod`]: struct.WidgetPod.html // TODO - dynamically check that the type of the pod we are registering this on is the same as the type of the - // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. pub fn new_sub_window + 'static, U: Data>( + // requirement. Needs type ids recorded. This goes wrong if you don't have a pod between you and a lens. pub fn new_sub_window + 'static, U: Data>( &mut self, window_config: WindowConfig, diff --git a/druid/src/core.rs b/druid/src/core.rs index 23cfdd56a9..c67e8e8d10 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -1015,11 +1015,11 @@ impl> WidgetPod { (Some(_), Some(_)) => {} } } - let prev_env = self.env.as_ref().filter(|p| !p.same(env)); - let env_changed = prev_env.is_some(); - let data_changed = self.old_data.as_ref().filter(|p| !p.same(data)).is_some(); - if env_changed || data_changed { + let data_changed = + self.old_data.is_none() || self.old_data.as_ref().filter(|p| !p.same(data)).is_some(); + + if ctx.env_changed() || data_changed { for (_, host) in &self.state.sub_window_hosts { let update = SubWindowUpdate { data: if data_changed { @@ -1027,13 +1027,18 @@ impl> WidgetPod { } else { None }, - env: if env_changed { Some(env.clone()) } else { None }, + env: if ctx.env_changed() { + Some(env.clone()) + } else { + None + }, }; let command = Command::new(SUB_WINDOW_PARENT_TO_HOST, update, *host); ctx.submit_command(command); } } + let prev_env = self.env.as_ref().filter(|p| !p.same(env)); let mut child_ctx = UpdateCtx { state: ctx.state, widget_state: &mut self.state, diff --git a/druid/src/event.rs b/druid/src/event.rs index 2736c071ff..e044cd5f86 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -67,7 +67,7 @@ pub enum Event { /// that the window *will* close just because this event is received; for instance, you should /// avoid destructive side effects such as cleaning up resources. /// - /// [`set_handled`]: struct.EventCtx.html#method.set_handled + /// [`set_handled`]: crate::EventCtx::set_handled WindowCloseRequested, /// Sent to all widgets in a given window when the system is going to close that window. ///