godoru 0.1.0

UI Framework for Rust using Godot
#![allow(non_snake_case)]

use godoru::{
    Align, AppTheme, Color, Component, Container, ContainerDirection, ContainerWrap, CursorIcon,
    GodoruApp, GodoruResult, ImageFit, Justify, Shader, SizeMode, WindowOptions,
};

#[derive(Clone, Debug)]
enum Action {
    ButtonClick,
    ButtonHover,
    ButtonUnhover,
    ButtonFocus,
    ButtonBlur,
    ButtonDown,
    ButtonUp,
    PanelClick,
    TextHover,
    ImageClick,
    NameChanged(String),
    NameSubmitted(String),
    FeatureToggled(bool),
}

fn main() -> GodoruResult<()> {
    let assets = godoru::Ui::<Action>::new().assets();
    let logo = assets.image("godoru.png")?;

    let neonShader = Shader::fromCode(
        r#"
shader_type canvas_item;

vec3 hsv2rgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void fragment() {
	COLOR = texture(TEXTURE, UV);
	float hue = (UV.x+UV.y) * 0.25 + mod(TIME * 0.5, 1.0);
	COLOR.rgb *= hsv2rgb(vec3(hue,0.6, 1.0));
}

"#,
    );

    let theme = AppTheme::new()
        .backgroundColor(Color::rgb(0.0, 0.0, 0.0))
        .class(Component::Container, "screen", |style| {
            style.background(Color::rgb(0.02, 0.02, 0.08));
            style.padding(24);
        })
        .class(Component::Container, "hero", |style| {
            style.background(Color::rgb(1.0, 0.95, 0.0));
            style.color(Color::rgb(0.0, 0.0, 0.0));
            style.radius(18);
            style.padding((18, 24, 18, 24));
            style.border(Color::rgb(1.0, 0.0, 0.0), 6);
            style.shadow(Color::rgba(1.0, 0.0, 1.0, 0.65), 20, 0.0, 10.0);
        })
        .class(Component::Container, "strip", |style| {
            style.background(Color::rgb(0.0, 0.9, 1.0));
            style.radius(14);
            style.padding(14);
            style.border(Color::rgb(0.0, 0.0, 1.0), 4);
        })
        .class(Component::Container, "card", |style| {
            style.background(Color::rgb(1.0, 0.0, 0.0));
            style.radius(16);
            style.padding(18);
            style.border(Color::rgb(0.0, 0.0, 1.0), 5);
            style.shadow(Color::rgba(0.0, 1.0, 0.0, 0.75), 14, 6.0, 6.0);
        })
        .class(Component::Container, "shaderCard", |style| {
            style.background(Color::rgb(0.0, 0.0, 1.0));
            style.radius(18);
            style.padding(18);
            style.border(Color::rgb(1.0, 1.0, 1.0), 4);
            style.shader(neonShader.clone());
        })
        .class(Component::Text, "title", |style| {
            style.color(Color::rgb(0.0, 0.0, 0.0));
            style.fontSize(36);
        })
        .class(Component::Text, "label", |style| {
            style.color(Color::rgb(1.0, 1.0, 1.0));
            style.fontSize(18);
        })
        .class(Component::Text, "blueText", |style| {
            style.color(Color::rgb(0.0, 0.0, 1.0));
            style.fontSize(24);
        })
        .class(Component::Image, "assetImage", |style| {
            style.background(Color::rgb(0.0, 1.0, 0.0));
            style.radius(18);
            style.border(Color::rgb(1.0, 1.0, 0.0), 5);
        })
        .class(Component::Button, "primary", |style| {
            style.background(Color::rgb(0.0, 0.0, 1.0));
            style.color(Color::rgb(1.0, 1.0, 1.0));
            style.radius(12);
            style.padding((12, 22, 12, 22));
            style.border(Color::rgb(1.0, 1.0, 1.0), 4);
        })
        .class(Component::Button, "primary:hover", |style| {
            style.background(Color::rgb(0.0, 1.0, 0.0));
            style.color(Color::rgb(0.0, 0.0, 0.0));
            style.border(Color::rgb(1.0, 0.0, 1.0), 5);
        })
        .class(Component::Button, "primary:pressed", |style| {
            style.background(Color::rgb(1.0, 0.0, 0.0));
            style.color(Color::rgb(1.0, 1.0, 0.0));
            style.border(Color::rgb(1.0, 1.0, 0.0), 6);
        })
        .class(Component::Button, "primary:focus", |style| {
            style.background(Color::rgb(1.0, 0.0, 1.0));
            style.color(Color::rgb(1.0, 1.0, 1.0));
            style.border(Color::rgb(0.0, 1.0, 1.0), 6);
        })
        .class(Component::TextInput, "input", |style| {
            style.background(Color::rgb(1.0, 1.0, 1.0));
            style.color(Color::rgb(0.0, 0.0, 0.0));
            style.radius(10);
            style.padding((10, 14, 10, 14));
            style.border(Color::rgb(0.0, 0.0, 1.0), 4);
            style.fontSize(18);
        })
        .class(Component::TextInput, "input:focus", |style| {
            style.background(Color::rgb(1.0, 1.0, 0.0));
            style.border(Color::rgb(1.0, 0.0, 0.0), 6);
        })
        .class(Component::Checkbox, "check", |style| {
            style.background(Color::rgb(0.0, 1.0, 1.0));
            style.color(Color::rgb(0.0, 0.0, 0.0));
            style.radius(10);
            style.padding((10, 14, 10, 14));
            style.border(Color::rgb(0.0, 0.0, 0.0), 4);
            style.fontSize(18);
        })
        .class(Component::Checkbox, "check:focus", |style| {
            style.background(Color::rgb(1.0, 0.0, 1.0));
            style.color(Color::rgb(1.0, 1.0, 1.0));
        })
        .class(Component::Scroll, "scroll", |style| {
            style.background(Color::rgb(0.2, 0.0, 0.55));
            style.radius(14);
            style.padding(12);
            style.border(Color::rgb(0.0, 1.0, 0.0), 4);
        });

    let root = Container::<Action>::new()
        .id("ComponentShowcaseRoot")
        .class("screen")
        .direction(ContainerDirection::Vertical)
        .gap(16)
        .children(|ui| {
            ui.container()
                .id("Hero")
                .class("hero")
                .direction(ContainerDirection::Vertical)
                .gap(8)
                .onClick(Action::PanelClick)
                .cursor(CursorIcon::Pointer)
                .children(|ui| {
                    ui.text("Godoru Component Showcase").class("title");
                    ui.text(
                        "Strong colors expose style mapping, decoration, state and layout issues.",
                    )
                    .class("blueText")
                    .onHover(Action::TextHover)
                    .cursor(CursorIcon::Pointer);
                });

            ui.container()
                .id("DecorationsRow")
                .class("strip")
                .direction(ContainerDirection::Horizontal)
                .wrap(ContainerWrap::Wrap)
                .align(Align::Stretch)
                .justify(Justify::SpaceBetween)
                .gap(16)
                .children(|ui| {
                    ui.container()
                        .id("RedCard")
                        .class("card")
                        .size(SizeMode::Fixed(320), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(10)
                        .children(|ui| {
                            ui.text("Container").class("label");
                            ui.text("radius, border, shadow, padding, margin and click")
                                .class("label");
                        });

                    ui.container()
                        .id("ShaderCard")
                        .class("shaderCard")
                        .size(SizeMode::Fixed(320), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(10)
                        .children(|ui| {
                            ui.text("Shader").class("label");
                            ui.text("CanvasItem shader applied to a container")
                                .class("label");
                        });
                });

            ui.container()
                .id("ComponentsGrid")
                .direction(ContainerDirection::Horizontal)
                .wrap(ContainerWrap::Wrap)
                .gap(16)
                .children(|ui| {
                    ui.container()
                        .class("card")
                        .size(SizeMode::Fixed(360), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(12)
                        .children(|ui| {
                            ui.text("Text").class("label").size(26);
                            ui.text("White label text").class("label");
                            ui.text("Blue text with hover event")
                                .class("blueText")
                                .onHover(Action::TextHover)
                                .cursor(CursorIcon::Pointer);
                        });

                    ui.container()
                        .class("card")
                        .size(SizeMode::Fixed(360), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(12)
                        .children(|ui| {
                            ui.text("Image").class("label").size(26);
                            ui.image(logo)
                                .class("assetImage")
                                .fit(ImageFit::Contain)
                                .customMinimumSize(220, 140)
                                .onClick(Action::ImageClick)
                                .cursor(CursorIcon::Pointer);
                        });

                    ui.container()
                        .class("card")
                        .size(SizeMode::Fixed(360), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(12)
                        .children(|ui| {
                            ui.text("Button").class("label").size(26);
                            ui.button("Hover, focus, press")
                                .class("primary")
                                .customMinimumSize(260, 56)
                                .cursor(CursorIcon::Pointer)
                                .onClick(Action::ButtonClick)
                                .onHover(Action::ButtonHover)
                                .onUnhover(Action::ButtonUnhover)
                                .onFocus(Action::ButtonFocus)
                                .onBlur(Action::ButtonBlur)
                                .onPointerDown(Action::ButtonDown)
                                .onPointerUp(Action::ButtonUp);
                        });

                    ui.container()
                        .class("card")
                        .size(SizeMode::Fixed(360), SizeMode::Fit)
                        .direction(ContainerDirection::Vertical)
                        .gap(12)
                        .children(|ui| {
                            ui.text("TextInput + Checkbox").class("label").size(26);
                            ui.textInput("")
                                .class("input")
                                .placeholder("Type here")
                                .customMinimumSize(280, 48)
                                .onChange(Action::NameChanged)
                                .onSubmit(Action::NameSubmitted)
                                .onFocus(Action::ButtonFocus)
                                .onBlur(Action::ButtonBlur);
                            ui.checkbox(false)
                                .class("check")
                                .text("Enable strong mode")
                                .customMinimumSize(280, 48)
                                .onToggle(Action::FeatureToggled);
                        });
                });

            ui.scroll()
                .id("ScrollPreview")
                .class("scroll")
                .customMinimumSize(760, 190)
                .children(|ui| {
                    for index in 1..=10 {
                        ui.container()
                            .class(if index % 2 == 0 { "strip" } else { "card" })
                            .margin((4, 0, 4, 0))
                            .padding(10)
                            .children(|ui| {
                                ui.text(format!(
                                    "Scroll row {index}: Container + Text inside Scroll"
                                ))
                                .class("label");
                            });
                    }
                });
        });

    GodoruApp::<(), Action>::withState(())
        .assetsRoot("assets")
        .onAction(|_, action| match action {
            Action::NameChanged(value) => println!("Name changed: {value}"),
            Action::NameSubmitted(value) => println!("Name submitted: {value}"),
            Action::FeatureToggled(value) => println!("Feature toggled: {value}"),
            other => println!("Godoru showcase action: {other:?}"),
        })
        .createWindow(
            WindowOptions::new("component_showcase")
                .title("Godoru Component Showcase")
                .size(800, 800)
                .theme(theme)
                .root(root),
        )?
        .run()
}