druid 0.8.2

Data-oriented Rust UI design toolkit.
Documentation
// 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.

//! Example of sub windows.

// On Windows platform, don't show a console when opening the app.
#![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() {
    // 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(),
        },
        closeable: true,
    };

    // start the application
    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() => {
                        // TODO another timer on leaving
                        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>,
    //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;
                    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"),
        ),
    );
    // 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);

            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);
    // 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(check_box);

    // center the two widgets in the available space
    Align::centered(layout)
}