pixel-widgets 0.4.0

Elm inspired UI library styled using images
Documentation
use pixel_widgets::loader::Loader;
use pixel_widgets::prelude::*;
use pixel_widgets::widget::drag_drop::DragDropContext;
use pixel_widgets::widget::panel::Anchor;
use pixel_widgets::Command;
use serde::Deserialize;
use std::collections::HashMap;
use winit::window::WindowBuilder;

enum Alchemy {
    Loading {
        progress: usize,
        total: usize,
    },
    Game {
        state: ManagedState<Id>,
        context: DragDropContext<DragItem>,
        items: Vec<Item>,
        next_item_id: usize,
        playground: Vec<(Item, usize, (f32, f32))>,
    },
}

#[derive(Clone, Copy)]
enum DragItem {
    FromInventory(usize),
    FromPlayground(usize),
}

#[derive(PartialEq, Eq, Clone)]
enum Id {
    Inventory,
    InventoryItem(usize),
    Playground,
    PlaygroundItem(usize),
}

#[derive(Clone)]
struct Item {
    id: usize,
    image: Image,
    name: String,
    discovered: bool,
    combinations: HashMap<usize, usize>,
}

#[derive(Clone)]
enum Message {
    Void,
    LoadItems(usize),
    LoadedItem,
    Loaded(Vec<Item>),
    Place(Item, (f32, f32)),
    MovePlaygroundItem(usize, (f32, f32)),
    CombineInventory(usize, usize),
    CombinePlayground(usize, usize),
}

impl Model for Alchemy {
    type Message = Message;

    fn update(&mut self, message: Self::Message) -> Vec<Command<Message>> {
        match message {
            Message::Void => (),
            Message::LoadItems(total) => {
                *self = Self::Loading { progress: 0, total };
            }
            Message::LoadedItem => {
                if let Self::Loading { ref mut progress, .. } = *self {
                    *progress += 1;
                }
            }
            Message::Loaded(items) => {
                *self = Self::Game {
                    state: Default::default(),
                    context: Default::default(),
                    items,
                    next_item_id: 0,
                    playground: Default::default(),
                };
            }
            Message::Place(item, pos) => {
                if let Self::Game {
                    ref mut playground,
                    ref mut next_item_id,
                    ..
                } = *self
                {
                    playground.push((item, *next_item_id, pos));
                    *next_item_id += 1;
                }
            }
            Message::MovePlaygroundItem(move_id, new_pos) => {
                if let Self::Game { ref mut playground, .. } = *self {
                    for &mut (_, ref id, ref mut pos) in playground.iter_mut() {
                        if *id == move_id {
                            *pos = new_pos;
                        }
                    }
                }
            }
            Message::CombineInventory(target, item_id) => {
                if let Self::Game {
                    ref mut playground,
                    ref mut items,
                    ..
                } = *self
                {
                    for &mut (ref mut item, ref id, _) in playground.iter_mut() {
                        if *id == target {
                            if let Some(&output) = item.combinations.get(&item_id) {
                                items[output].discovered = true;
                                *item = items[output].clone();
                            }
                        }
                    }
                }
            }
            Message::CombinePlayground(target, source) => {
                if let Self::Game {
                    ref mut playground,
                    ref mut items,
                    ..
                } = *self
                {
                    if target != source {
                        let mut success = false;
                        let mut item_id = None;
                        for &(ref item, ref id, _) in playground.iter() {
                            if *id == source {
                                item_id.replace(item.id);
                            }
                        }
                        if let Some(item_id) = item_id {
                            for &mut (ref mut item, ref id, _) in playground.iter_mut() {
                                if *id == target {
                                    if let Some(&output) = item.combinations.get(&item_id) {
                                        items[output].discovered = true;
                                        *item = items[output].clone();
                                        success = true;
                                    }
                                }
                            }
                            if success {
                                playground.retain(|(_, id, _)| *id != source);
                            }
                        }
                    }
                }
            }
        }

        Vec::new()
    }

    fn view(&mut self) -> Node<Message> {
        match self {
            &mut Self::Loading { progress, total } => Column::new()
                .push(Space)
                .push(Progress::new(progress as f32 / total as f32))
                .class("loading"),
            &mut Self::Game {
                ref mut state,
                ref context,
                ref items,
                ref playground,
                ..
            } => {
                let mut state = state.tracker();

                let playground = Layers::with_background(
                    state.get(&Id::Playground),
                    Drop::new(
                        state.get(&Id::Playground),
                        context,
                        |_| true,
                        move |drag_item, pos| match drag_item {
                            DragItem::FromInventory(i) => Message::Place(items[i].clone(), pos),
                            DragItem::FromPlayground(i) => Message::MovePlaygroundItem(i, pos),
                        },
                        Space,
                    ),
                )
                .extend(playground.iter().map(|(item, id, pos)| {
                    let drag = Drag::new(
                        state.get(&Id::PlaygroundItem(*id)),
                        context,
                        DragItem::FromPlayground(*id),
                        &item.image,
                    );
                    let drop = Drop::new(
                        state.get(&Id::PlaygroundItem(*id)),
                        context,
                        |_| true,
                        move |drag_item, _| match drag_item {
                            DragItem::FromInventory(other_id) => Message::CombineInventory(*id, other_id),
                            DragItem::FromPlayground(other_id) => Message::CombinePlayground(*id, other_id),
                        },
                        drag,
                    );
                    let widget = Panel::new(*pos, Anchor::TopLeft, drop);
                    (*id, widget)
                }));

                let filtered: Vec<(usize, &Item)> =
                    items.iter().enumerate().filter(|(_, item)| item.discovered).collect();

                let inventory = Column::new().extend(filtered.chunks(4).map(|row| {
                    Row::new().extend(row.iter().map(|&(i, item)| {
                        Drag::new(
                            state.get(&Id::InventoryItem(i)),
                            context,
                            DragItem::FromInventory(i),
                            &item.image,
                        )
                    }))
                }));

                Row::new()
                    .push(playground)
                    .push(Scroll::new(state.get(&Id::Inventory), inventory))
                    .class("game")
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let model = Alchemy::Loading { progress: 0, total: 1 };

    let window = WindowBuilder::new()
        .with_title("Alchemy Game")
        .with_inner_size(winit::dpi::LogicalSize::new(800, 360));

    let loader = pixel_widgets::loader::FsLoader::new("./examples/alchemy".into()).unwrap();

    let mut sandbox = Sandbox::new(model, loader, window).await;
    sandbox.ui.set_stylesheet("alchemy.pwss").await.unwrap();

    let graphics = sandbox.ui.graphics();
    let (tx, rx) = futures::channel::mpsc::unbounded();
    sandbox.ui.command(Command::from_stream(rx));
    sandbox.ui.command(Command::from_future_message(async move {
        let graphics = graphics.clone();
        tx.unbounded_send(Message::LoadItems(1)).ok();

        #[derive(Deserialize)]
        struct Definition {
            image: String,
            name: String,
            unlocked: bool,
            recipe: Option<(String, String)>,
        }

        let bytes = graphics.loader().load("recipes.ron").await.unwrap();
        let defs: Vec<Definition> = ron::de::from_bytes(bytes.as_slice()).unwrap();
        tx.unbounded_send(Message::LoadItems(defs.len() + 1)).ok();
        tx.unbounded_send(Message::LoadedItem).ok();

        let mut items = futures::future::join_all(defs.iter().enumerate().map(|(id, def)| {
            let graphics = graphics.clone();
            let tx = tx.clone();
            async move {
                let image = graphics.load_image(def.image.as_str()).await.unwrap();
                tx.unbounded_send(Message::LoadedItem).ok();
                Item {
                    id,
                    image,
                    name: def.name.clone(),
                    discovered: def.unlocked,
                    combinations: HashMap::new(),
                }
            }
        }))
        .await;

        for (index, def) in defs.into_iter().enumerate() {
            if let Some((a, b)) = def.recipe {
                let a = items.iter().position(|i| i.name == a);
                let b = items.iter().position(|i| i.name == b);
                if let (Some(a), Some(b)) = (a, b) {
                    items[a].combinations.insert(b, index);
                    items[b].combinations.insert(a, index);
                }
            }
        }

        tx.unbounded_send(Message::Loaded(items)).ok();
        Message::Void
    }));

    sandbox.run().await;
}