widgetkit 0.2.0

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

use widgetkit::prelude::*;

fn main() -> widgetkit::Result<()> {
    WidgetApp::new()
        .widget("pulse", PulseWidget)
        .host(WindowsHost::new().with_size(Size::new(320.0, 140.0)))
        .renderer(SoftwareRenderer::new())
        .run()
}

struct PulseWidget;

#[derive(Default)]
struct PulseState {
    phase: f32,
    status: String,
}

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

impl Widget for PulseWidget {
    type State = PulseState;
    type Message = Msg;

    fn mount(&mut self, _ctx: &mut MountCtx<Self>) -> Self::State {
        PulseState {
            phase: 0.0,
            status: "booting module graph...".into(),
        }
    }

    fn start(&mut self, _state: &mut Self::State, ctx: &mut StartCtx<Self>) {
        ctx.scheduler()
            .every(std::time::Duration::from_millis(120), Msg::Tick);
        ctx.tasks()
            .spawn_named("load-status", async { Msg::Loaded });
    }

    fn update(
        &mut self,
        state: &mut Self::State,
        event: Event<Self::Message>,
        ctx: &mut UpdateCtx<Self>,
    ) {
        match event {
            Event::Message(Msg::Tick) => {
                state.phase += 0.1;
                if state.phase > 1.0 {
                    state.phase = 0.0;
                }
                ctx.request_render();
            }
            Event::Message(Msg::Loaded) => {
                state.status = "runtime stable".into();
                ctx.request_render();
            }
            Event::Host(_) => {}
        }
    }

    fn render(&self, state: &Self::State, canvas: &mut Canvas, _ctx: &RenderCtx<Self>) {
        let bg = Color::rgb(12, 14, 18);
        let panel = Color::rgb(24, 28, 36);
        let accent = Color::rgb(90, 180, 255);
        let muted = Color::rgb(180, 188, 204);

        canvas.clear(bg);
        canvas.save();
        canvas.clip_rect(Rect::xywh(12.0, 12.0, 296.0, 116.0));
        canvas.round_rect(Rect::xywh(12.0, 12.0, 296.0, 116.0), 16.0, panel);

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

        canvas.line(
            Point::new(24.0, 45.0),
            Point::new(284.0, 45.0),
            Stroke::new(1.0),
            Color::rgba(255, 255, 255, 24),
        );

        let base_x = 28.0;
        let y = 74.0;
        let step = 28.0;

        for i in 0..5 {
            let t = ((state.phase * 10.0) + i as f32) % 5.0;
            let radius = 6.0 + t;
            let alpha = 60 + (i as u8 * 30);

            canvas.circle(
                Point::new(base_x + i as f32 * step, y),
                radius,
                Color::rgba(90, 180, 255, alpha),
            );
        }

        canvas.text(
            Point::new(24.0, 108.0),
            &state.status,
            TextStyle::new().size(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();
    }
}