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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Empty file modified druid-shell/Cargo.toml
100644 → 100755
Empty file.
356 changes: 356 additions & 0 deletions druid/examples/sub_window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
// Copyright 2019 The Druid Authors.
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
//
// 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<HelloState> = 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<String>) -> Self {
TooltipController {
tip: tip.into(),
state: TooltipState::Fresh,
}
}
}

impl<T, W: Widget<T>> Controller<T, W> 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<Point>,
//dragging: bool
}

impl DragWindowController {
pub fn new() -> Self {
DragWindowController { init_pos: None }
}
}

impl<T, W: Widget<T>> Controller<T, W> 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<HelloState> {
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)
}
1 change: 1 addition & 0 deletions druid/examples/web/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -189,6 +189,15 @@ 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>> =
Selector::new("druid-builtin.new-sub-window");

pub(crate) const SUB_WINDOW_PARENT_TO_HOST: Selector<Box<dyn Any>> =
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
Selector::new("druid-builtin.parent_to_host");

pub(crate) const SUB_WINDOW_HOST_TO_PARENT: Selector<Box<dyn Any>> =
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.
///
Expand Down
Loading