#![windows_subsystem = "windows"]
use druid::commands::CLOSE_WINDOW;
use druid::lens::Unit;
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,
TimerToken, UpdateCtx, Widget, WidgetExt, WindowConfig, WindowDesc, WindowId, WindowSizePolicy,
};
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,
closeable: bool,
}
pub fn main() {
let main_window = WindowDesc::new(build_root_widget())
.title(WINDOW_TITLE)
.window_size((400.0, 400.0));
let initial_state = HelloState {
name: "World".into(),
sub: SubState {
my_stuff: "It's mine!".into(),
},
closeable: true,
};
AppLauncher::with_window(main_window)
.log_to_console()
.launch(initial_state)
.expect("Failed to launch application");
}
enum TooltipState {
Showing(WindowId),
Waiting {
last_move: Instant,
timer_expire: Instant,
token: TimerToken,
position_in_window_coordinates: 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),
position_in_window_coordinates: me.window_pos,
}),
_ => None,
},
TooltipState::Waiting {
last_move,
timer_expire,
token,
position_in_window_coordinates,
} => 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,
position_in_window_coordinates: 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;
tracing::info!("Waiting another {:?}", wait_for);
Some(TooltipState::Waiting {
last_move: *last_move,
timer_expire: deadline,
token: ctx.request_timer(wait_for),
position_in_window_coordinates: *position_in_window_coordinates,
})
} else {
let tooltip_position_in_window_coordinates =
(position_in_window_coordinates.to_vec2() + cursor_size.to_vec2())
.to_point();
let win_id = ctx.new_sub_window(
WindowConfig::default()
.show_titlebar(false)
.window_size_policy(WindowSizePolicy::Content)
.set_level(WindowLevel::Tooltip(ctx.window().clone()))
.set_position(tooltip_position_in_window_coordinates),
Label::<()>::new(self.tip.clone()),
(),
env.clone(),
);
Some(TooltipState::Showing(win_id))
}
}
_ => None,
},
TooltipState::Showing(win_id) => {
match event {
Event::MouseMove(me) if !ctx.is_hot() => {
tracing::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),
position_in_window_coordinates: 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>,
}
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;
tracing::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);
}
}
}
}
}
struct CancelClose;
impl<W: Widget<bool>> Controller<bool, W> 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<HelloState> {
let label = EnvScope::new(
|env, _t| env.set(theme::TEXT_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"),
),
);
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);
ctx.new_sub_window(
WindowConfig::default()
.show_titlebar(false)
.window_size(Size::new(100., 100.))
.set_level(WindowLevel::AppWindow),
col,
data.clone(),
env.clone(),
);
})
.center()
.lens(HelloState::sub);
let check_box =
ControllerHost::new(Checkbox::new("Closeable?"), CancelClose).lens(HelloState::closeable);
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(check_box);
Align::centered(layout)
}