Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Please add ability to serialize/deserialize from structured file format such as JSON or YAML #170

Open
kristianmandrup opened this issue Nov 22, 2022 · 9 comments

Comments

@kristianmandrup
Copy link

It would be really cool to be able to write the Kayak UI in an external file such as a JSON file, similar to a CSS file and have it loaded and the Kayak UI built (builder pattern), similar to how the rsx! macro works.

I would love to help with this effort if you can help get me started and point me in the right direction. I first thought there was a general Widget struct that could be returned for each Widget parser, but it looks like I would need to resort to Box<dyn Any> for a more generic approach?

@heavyrain266
Copy link
Contributor

How about modern/readable format like KDL? It fits pretty well suited for structured UI with e.g. CSS styling.

@kristianmandrup
Copy link
Author

Sure, I will look into it. This is what I've got so far:

Parser

use serde_json;
use std::{result::Result};
use serde::{Deserialize, Serialize};

use crate::morph::{build_text_widget, UiNode};

// use crate::morph::build_world;

type OptStr = Option<String>;
// type OptNum = Option<u32>;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UiParseNode {
   #[serde(skip_serializing_if = "Option::is_none")]
   pub text: OptStr,
   pub width: OptStr,
   pub height: OptStr,
   pub child_space: OptStr,
   pub position_type: OptStr,
} 

pub fn parse_ui() -> Result<UiNode, &'static str> {

    let ui_json = r#"
    {
        "width": "2 px",
        "height": "5 px"
    }
    "#;    

    let ui = serde_json::from_str(ui_json).unwrap();
    println!("ui {:#?}", ui);
    build_text_widget(ui)
}

Builder

use std::{any::Any};
use kayak_ui::{widgets::{TextWidgetBundle, TextProps}};
use regex::Regex;
use crate::morph_serde::{UiParseNode, OptStr}; // use morphorm::Cache;

trait UiParser {
    fn parse(&self) -> Result<Box<dyn Any>, &'static str>;
}

struct UiTextProps {
    node: UiParseNode
}
impl UiTextProps {
    fn new(node: UiParseNode) -> Self {
        Self {
            node
        }
    }
}

impl UiParser for UiTextProps {
    fn parse(&self) -> Result<Box<dyn Any>, &'static str> {
        let text = &self.node.text.clone();
        let content = text.to_owned().unwrap_or("".to_string());
        let widget = TextProps {
            content: content.to_string(),
            ..Default::default()
        };
        Ok(Box::new(widget))            
    }
}

 struct UiTextWidget {
    node: UiParseNode
}
impl UiParser for UiTextWidget {
    fn parse(&self) -> Result<Box<dyn Any>, &'static str> {
        let text = UiTextProps::new(self.node.to_owned()).parse()?;
        if let Ok(content) = text.downcast::<TextProps>() {
            let widget = TextWidgetBundle {
                text: *content,
                ..Default::default()
            };        
            Ok(Box::new(widget))
        } else {
            Err("bad TextProps")
        }
    }
}

pub struct UiNode {
    unit: UiNodeUnit
}
impl UiNode {
    fn new(unit: UiNodeUnit) -> Self {
        Self {
            unit
        }
    }
}

pub enum UiNodeUnit {
    Pixels(f32),    
}


pub fn build_text_widget(ui: UiParseNode) -> Result<UiNode, &'static str>  {
    if let Ok(unit) = parse_unit(ui.width) {
        Ok(UiNode::new(unit))
    } else {
        Err("bad Text Widget")        
    }
    
}

pub fn parse_unit(optstr: OptStr) -> Result<UiNodeUnit, &'static str>  {
    if let Some(str) = optstr {
        let re = Regex::new(r"px\s*$").unwrap();
        let str = re.replace(&str, "");
        let number = &str[..str.len() - 2];
        let pixels = number.parse::<f32>().unwrap();
        Ok(UiNodeUnit::Pixels(pixels))
    } else {
        Err("bad unit")        
    }
}

@kristianmandrup
Copy link
Author

Note: I'm only starting to learn Rust so I'm sure the above code could be much improved. You're welcome to make suggestions :)

@heavyrain266
Copy link
Contributor

kayak_ui uses nanoserde instead of serde internally for msdf data and fast compile times.

@StarArawn
Copy link
Owner

I think that some internal structs should likely implement nanoserde traits or regular serde traits hidden behind feature flags. As for building UI's using JSON, YAML, or KDL I'm not convinced that those formats would be beneficial. @MrGVSV and I had an idea where we could write widgets as scripts in a scripting language so that widgets could be modified at runtime. I think is probably an ideal path forward here to having asset loaded widgets. As an intermediate step I do want to be able to define styles as assets so look for that change in the near future. :)

@kristianmandrup
Copy link
Author

@StarArawn Thanks for your suggestions and feedback.

I'm just playing around and experimenting for now (leveling up on Rust). My initial idea is be to load certain Widget structs from a structured file format (such as JSON) into a HasMap. Then any Kayak UI builder can reference entries in this hashmap, sort of like "reusable classes" at a very primitive level. Then we can build from there...

@kristianmandrup
Copy link
Author

kristianmandrup commented Nov 23, 2022

@kristianmandrup
Copy link
Author

kristianmandrup commented Nov 23, 2022

Looking at nanoserde as suggested.

JSON proposal below.

extends can be used as a simple class-like mechanism, ie. some base styling that can be extended and combined.
The my-image image bundle at the bottom references the profile-image asset for the image.

{
  "assets": {
    "images": [
      {
        "name": "profile-image",
        "type": "image",
        "path": "path/to/profile.png"
      }
    ],
    "fonts": [
      {
        "name": "roboto",
        "type": "font",
        "path": "path/to/roboto.tff"
      }
    ]
  },
  "styles": [
    {
      "name": "base",
      "color": "white",
      "background-color": "darkgray"
    },
    {
      "name": "base-image",
      "border-radius": "500",
      "position-type": "self-directed"
    }
  ],
  "widgets": {
    "buttons": [
      {
        "name": "menu-button",
        "type": "button",
        "style": {
          "extends": "base",
          "bottom": "20 px",
          "cursor": "hand"
        }
      }
    ],
    "text-widgets": [
      {
        "name": "game-title",
        "type": "text-widget",
        "text": {
          "extends": "base",
          "content": "hello",
          "size": 20,
          "font-ref": "roboto"
        }
      }
    ],
    "image-bundles": [
      {
        "name": "my-image",
        "type": "image-bundle",
        "image-ref": "profile-image",
        "styles": {
          "extends": "base-image",
          "left": "10 px",
          "top": "10 px",
          "width": "200 px",
          "height": "182 px"
        }
      }
    ]
  }
}

@kristianmandrup
Copy link
Author

I've now completed first rough implementation here: https://github.com/kristianmandrup/kayak_ui_deserializer

Only just started on my Rust journey a couple of months ago in order to get into Game Dev, so I'd love for anyone to make suggestions for improvements or try it out. Cheers.

PS: This issue can be either closed or marked as feature suggestion (for progress tracking) as you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants