widgetkit 0.3.0

Modular Rust framework for building desktop widgets.
Documentation
#![windows_subsystem = "windows"]

use chrono::Local;
use widgetkit::prelude::*;

fn main() -> widgetkit::Result<()> {
    WidgetApp::new()
        .widget("clock", ClockWidget)
        .host(
            WindowsHost::new()
                .size_policy(SizePolicy::ContentWithLimits {
                    min: Some(Size::new(280.0, 112.0)),
                    max: Some(Size::new(520.0, 180.0)),
                })
                .frameless(true)
                .transparent(true)
                .always_on_top(true),
        )
        .renderer(SoftwareRenderer::new())
        .run()
}

struct ClockWidget;

#[derive(Default)]
struct ClockState {
    time_text: String,
    status: String,
    accent: Color,
}

#[derive(Clone, Debug)]
enum Msg {
    Tick,
    StatusLoaded(String),
}

impl Widget for ClockWidget {
    type State = ClockState;
    type Message = Msg;

    fn mount(&mut self, _ctx: &mut MountCtx<Self>) -> Self::State {
        ClockState {
            time_text: current_time_string(),
            status: "starting runtime...".into(),
            accent: Color::rgb(127, 160, 255),
        }
    }

    fn start(&mut self, _state: &mut Self::State, ctx: &mut StartCtx<Self>) {
        ctx.scheduler()
            .every(Duration::from_secs(1), Msg::Tick);
        ctx.tasks().spawn_named("load-status", async {
            Msg::StatusLoaded("software renderer + render frame ready".to_string())
        });
        ctx.post(Msg::Tick);
    }

    fn update(
        &mut self,
        state: &mut Self::State,
        event: Event<Self::Message>,
        ctx: &mut UpdateCtx<Self>,
    ) {
        match event {
            Event::Message(Msg::Tick) => {
                state.time_text = current_time_string();
                ctx.request_render();
            }
            Event::Message(Msg::StatusLoaded(status)) => {
                state.status = status;
                ctx.request_render();
            }
            Event::Host(_) => {}
        }
    }

    fn preferred_size(&self, state: &Self::State, ctx: &LayoutCtx<Self>) -> Size {
        let title = ctx.measure_text("WidgetKit", TextStyle::new().size(16.0));
        let time = ctx.measure_text(&state.time_text, TextStyle::new().size(24.0));
        let status = ctx.measure_text(
            &state.status,
            TextStyle::new()
                .size(10.0)
                .line_height(12.0)
                .baseline(TextBaseline::Bottom),
        );

        let width = title.width.max(time.width).max(status.width) + 64.0;
        let height = title.height + time.height + status.height + 64.0;

        ctx.constrain(Size::new(width, height))
    }

    fn render(&self, state: &Self::State, canvas: &mut Canvas, ctx: &RenderCtx<Self>) {
        let bg = Color::rgb(14, 17, 22);
        let card = Color::rgb(30, 36, 48);
        let muted = Color::rgb(190, 198, 212);
        let divider = Color::rgba(255, 255, 255, 32);
        let size = ctx.surface_size();
        let card_rect = Rect::xywh(12.0, 12.0, size.width - 24.0, size.height - 24.0);
        let divider_y = 45.0;

        canvas.clear(bg);

        canvas.save();
        canvas.clip_rect(card_rect);
        canvas.round_rect(card_rect, 18.0, card);

        canvas.text(
            Point::new(28.0, 28.0),
            "WidgetKit",
            TextStyle::new().size(16.0),
            state.accent,
        );

        canvas.line(
            Point::new(24.0, divider_y),
            Point::new(size.width - 36.0, divider_y),
            Stroke::new(1.0),
            divider,
        );

        canvas.text(
            Point::new(28.0, 50.0),
            &state.time_text,
            TextStyle::new().size(24.0),
            Color::WHITE,
        );

        canvas.circle(Point::new(size.width - 48.0, 30.0), 6.0, state.accent);

        canvas.text(
            Point::new(28.0, size.height - 25.0),
            &state.status,
            TextStyle::new()
                .size(10.0)
                .line_height(12.0)
                .baseline(TextBaseline::Bottom),
            muted,
        );

        canvas.restore();
    }

    fn stop(&mut self, _state: &mut Self::State, ctx: &mut StopCtx<Self>) {
        ctx.tasks().cancel_all();
        ctx.scheduler().clear();
    }
}

fn current_time_string() -> String {
    Local::now().format("%H:%M:%S").to_string()
}