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

Sub windows (squashed from previous branch) #1254

Merged
merged 11 commits into from
Jan 7, 2021
23 changes: 15 additions & 8 deletions druid/examples/sub_window.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -148,6 +150,7 @@ impl<T, W: Widget<T>> Controller<T, W> for TooltipController {
),
Label::<()>::new(self.tip.clone()),
(),
env.clone(),
);
Some(TooltipState::Showing(win_id))
}
Expand Down Expand Up @@ -323,11 +326,14 @@ impl<W: Widget<bool>> Controller<bool, W> for CancelClose {
}

fn build_root_widget() -> impl Widget<HelloState> {
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()
Expand All @@ -336,7 +342,7 @@ fn build_root_widget() -> impl Widget<HelloState> {
.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);
Expand All @@ -349,6 +355,7 @@ fn build_root_widget() -> impl Widget<HelloState> {
.set_level(WindowLevel::AppWindow),
col,
data.clone(),
env.clone(),
);
})
.center()
Expand Down
11 changes: 8 additions & 3 deletions druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -215,12 +216,16 @@ pub mod sys {
pub(crate) const SHOW_CONTEXT_MENU: Selector<Box<dyn Any>> =
Selector::new("druid-builtin.show-context-menu");

pub(crate) const NEW_SUB_WINDOW: Selector<SingleUse<SubWindowRequirement>> =
/// This is sent to the window handler to create a new sub window.
pub(crate) const NEW_SUB_WINDOW: Selector<SingleUse<SubWindowDesc>> =
Selector::new("druid-builtin.new-sub-window");

pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector<Box<dyn Any>> =
/// This is sent from a WidgetPod to any attached SubWindowHosts when a data update occurs
pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector<SubWindowUpdate> =
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<Box<dyn Any>> =
Selector::new("druid-builtin.host_to_parent");

Expand Down
21 changes: 13 additions & 8 deletions druid/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -351,18 +351,23 @@ 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.
///
/// [`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.
// 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<W: Widget<U> + 'static, U: Data>(
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
pub fn new_sub_window<W: Widget<U> + '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);
Expand Down
25 changes: 18 additions & 7 deletions druid/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1014,14 +1015,24 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
(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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's an env_changed method available on UpdateCtx.

let data_changed = self.old_data.as_ref().filter(|p| !p.same(data)).is_some();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this be incorrect in the case where there is no old data?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I thought that couldn't happen but not sure why...


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,
Expand Down
18 changes: 12 additions & 6 deletions druid/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,19 @@ 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 [`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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should now be able to use the intra-doc-link syntax, like:

Suggested change
/// [`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 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.
///
Expand Down
87 changes: 56 additions & 31 deletions druid/src/sub_window.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
// 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};
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
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;
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
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
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<dyn Widget<()>>,
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) struct SubWindowUpdate {
pub(crate) data: Option<Box<dyn Any>>,
pub(crate) env: Option<Env>,
}

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.
Expand All @@ -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,
Expand All @@ -59,41 +80,46 @@ impl SubWindowRequirement {
struct SubWindowHost<U, W: Widget<U>> {
id: WidgetId,
parent_id: WidgetId,
data: U,
child: WidgetPod<U, W>,
data: U,
env: Env,
}

impl<U, W: Widget<U>> SubWindowHost<U, W> {
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<U: Data, W: Widget<U>> Widget<()> for SubWindowHost<U, W> {
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::<U>()
{
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::<U>() {
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?
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -105,30 +131,29 @@ impl<U: Data, W: Widget<U>> Widget<()> for SubWindowHost<U, W> {
}
}

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<WidgetId> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might be worth documenting somewhere that a a widget in a subwindow will get a new WidgetId.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The child will still have its own id, if it already had one? This is the widgetId of the SubWindowHost. Not sure what the public use of that info would be ... (ie how could someone make use of that information)

Expand Down
8 changes: 4 additions & 4 deletions druid/src/win_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,19 +727,19 @@ impl<T: Data> AppState<T> {

fn new_sub_window(&mut self, cmd: Command) -> Result<(), Box<dyn std::error::Error>> {
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<SubWindowRequirement>",
"{} command must carry a SingleUse<SubWindowDesc>",
sys_cmd::NEW_SUB_WINDOW
)
}
Expand Down