diff --git a/mangades-plain/Cargo.toml b/mangades-plain/Cargo.toml new file mode 100644 index 0000000..95ca698 --- /dev/null +++ b/mangades-plain/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "mangades-plain" +version = "0.1.0" +edition = "2021" +authors.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mangui = { path = "../ui" } +uno-gen = { path = "../uno-gen" } + +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +tokio = { version = "1.36.0", features = ["full"] } +futures = "0.3.30" +reqwest = "0.11.24" +image = "0.24.9" +bytes = "*" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/mangades-plain/src/main.rs b/mangades-plain/src/main.rs new file mode 100644 index 0000000..3e39895 --- /dev/null +++ b/mangades-plain/src/main.rs @@ -0,0 +1,84 @@ +use std::sync::{Arc, mpsc, Mutex}; +use mangui::nodes::layout::Layout; +use mangui::{MainEntry, SharedNode}; +use mangui::femtovg::Paint; +use mangui::nodes::text::Text; +use mangui::nodes::{Style, TaffyStyle, ToShared}; +use mangui::taffy::{AlignItems, FlexDirection, JustifyContent, LengthPercentage, Rect}; +use uno_gen::uno; +use crate::anilist::load_demo_async; +use crate::tokens::TEXT_LARGE; +use crate::utils::{append, detach}; + +mod anilist; +mod utils; +mod tokens; + +#[tokio::main] +async fn main() { + let (tx, rx) = mpsc::channel(); + let tx = Arc::new(tx); + let root = Layout::default(); + let groot: SharedNode = Arc::new(Mutex::new(root)); + + let loading_container = Layout::default() + .style(Style { + layout: TaffyStyle { + flex_grow: 1., + justify_content: JustifyContent::Center.into(), + align_items: AlignItems::Center.into(), + ..Default::default() + }, + background: Some(Paint::color(*tokens::BACKGROUND)), + ..Default::default() + }) + .to_shared(); + let loading_text = Text::new("Loading...".to_owned(), TEXT_LARGE) + .style(Style { + text_fill: Some(Paint::color(*tokens::WHITE)), + ..Default::default() + }) + .to_shared(); + append(&groot, &{ loading_container.clone() }); + append(&loading_container, &{ loading_text.clone() }); + + let groot_clone = groot.clone(); + tokio::spawn(async move { + let data = load_demo_async().await; + + let mainview_container = Layout::default() + .style(Style { + layout: TaffyStyle { + flex_grow: 1., + flex_direction: FlexDirection::Column, + ..Default::default() + }, + background: Some(Paint::color(*tokens::BACKGROUND)), + ..Default::default() + }) + .to_shared(); + let i = 2; + uno!(gap-1 flex p-5px mt-1 mb-2 ml-[i] overflow-hidden); + let title = Text::new("Mangades".to_owned(), TEXT_LARGE) + .style(Style { + layout: TaffyStyle { + padding: Rect { left: LengthPercentage::Length(10.), right: LengthPercentage::Length(10.), top: LengthPercentage::Length(10.), bottom: LengthPercentage::Length(10.) }, + ..Default::default() + }, + text_fill: Some(Paint::color(*tokens::WHITE)), + ..Default::default() + }) + .to_shared(); + + append(&mainview_container, &title); + detach(&loading_container); + append(&groot_clone, &mainview_container); + + tx.send(()).unwrap(); + }); + + mangui::run_event_loop(MainEntry { + root: groot.clone(), + render: rx + }); +} diff --git a/uno-gen/Cargo.toml b/uno-gen/Cargo.toml new file mode 100644 index 0000000..eb8a0e8 --- /dev/null +++ b/uno-gen/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "uno-gen" +version = "0.1.0" +edition = "2021" +authors.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quote = "1.0" +proc-macro2 = "1.0.78" + +[lib] +proc-macro = true \ No newline at end of file diff --git a/uno-gen/src/lib.rs b/uno-gen/src/lib.rs new file mode 100644 index 0000000..1517667 --- /dev/null +++ b/uno-gen/src/lib.rs @@ -0,0 +1,722 @@ +use proc_macro2::{Span, TokenStream, TokenTree}; +use quote::{IdentFragment, quote_spanned}; +use quote::spanned::Spanned; + +#[derive(Clone, Default, Debug)] +enum UserSettable { + Value(T), + Arbitrary(TokenStream), + #[default] + None +} + +impl UserSettable { + fn require_value(&mut self) -> Result<&mut T, RuleParseError> { + match self { + UserSettable::Value(value) => Ok(value), + UserSettable::Arbitrary(stream) => { + Err(RuleParseError { + span: stream.__span(), + message: "Expected a value".to_owned() + }) + }, + UserSettable::None => { + Err(RuleParseError { + span: Span::call_site(), + message: "Expected a value".to_owned() + }) + } + } + } +} + +impl UserSettable { + fn require_non_arbitrary(&mut self) -> Result<&mut T, RuleParseError> { + match self { + UserSettable::Value(value) => Ok(value), + UserSettable::None => { + *self = UserSettable::Value(Default::default()); + Ok(self.require_value()?) + }, + UserSettable::Arbitrary(stream) => { + Err(RuleParseError { + span: stream.__span(), + message: "Expected a value".to_owned() + }) + } + } + } +} + +#[derive(Clone, Default, Debug)] +struct Point { + x: UserSettable, + y: UserSettable +} + +#[derive(Clone, Default, Debug)] +struct Size { + width: UserSettable, + height: UserSettable +} + +#[derive(Clone, Default, Debug)] +struct Color { + r: f32, + g: f32, + b: f32, + a: f32 +} + +#[derive(Clone, Debug)] +enum Paint { + Color(Color) +} + +impl Default for Paint { + fn default() -> Self { + Paint::Color(Color { + r: 0., + g: 0., + b: 0., + a: 0. + }) + } +} + +#[derive(Copy, Clone, Default, Debug)] +enum Cursor { + #[default] + Default +} + +#[derive(Clone, Default, Debug)] +struct Transform { + pub position: UserSettable>, + pub scale: UserSettable>, + pub rotation: UserSettable +} + +#[derive(Clone, Default, Debug)] +struct Style { + pub layout: UserSettable, + pub cursor: UserSettable, + pub background: UserSettable, + pub text_fill: UserSettable, + pub font_size: UserSettable, + pub line_height: UserSettable, + pub border_radius: UserSettable, + pub transform: UserSettable +} + +#[derive(Clone, Default, Debug)] +struct Rect { + pub left: UserSettable, + pub right: UserSettable, + pub top: UserSettable, + pub bottom: UserSettable, +} + +#[derive(Clone, Default, Debug)] +enum Position { + #[default] + Relative, + Absolute, +} + +#[derive(Clone, Default, Debug)] +enum Display { + Block, + #[default] + Flex, + Grid, + None, +} + +#[derive(Clone, Default, Debug)] +enum Overflow { + #[default] + Visible, + Clip, + Hidden, + Scroll, +} + +#[derive(Clone, Default, Debug)] +enum LengthPercentageAuto { + Length(f32), + Percent(f32), + #[default] + Auto, +} + +#[derive(Clone, Default, Debug)] +enum Dimension { + Length(f32), + Percent(f32), + #[default] + Auto, +} + +#[derive(Clone, Debug)] +enum LengthPercentage { + Length(f32), + Percent(f32), +} + +impl Default for LengthPercentage { + fn default() -> Self { + LengthPercentage::Length(0.) + } +} + +#[derive(Clone, Debug)] +enum AlignItems { + Start, + End, + FlexStart, + FlexEnd, + Center, + Baseline, + Stretch, +} + +type AlignSelf = AlignItems; + +#[derive(Clone, Debug)] +enum AlignContent { + Start, + End, + FlexStart, + FlexEnd, + Center, + Stretch, + SpaceBetween, + SpaceEvenly, + SpaceAround, +} +type JustifyContent = AlignContent; + +#[derive(Clone, Default, Debug)] +enum FlexDirection { + #[default] + Row, + Column, + RowReverse, + ColumnReverse, +} + +#[derive(Clone, Default, Debug)] +enum FlexWrap { + #[default] + NoWrap, + Wrap, + WrapReverse, +} + + +#[derive(Clone, Default, Debug)] +struct Line { + pub start: T, + pub end: T, +} + +#[derive(Clone, Debug)] +struct MinMax { + pub min: Min, + pub max: Max, +} +#[derive(Clone, Debug)] +enum MinTrackSizingFunction { + Fixed(LengthPercentage), + MinContent, + MaxContent, + Auto, +} +#[derive(Clone, Debug)] +enum MaxTrackSizingFunction { + Fixed(LengthPercentage), + MinContent, + MaxContent, + FitContent(LengthPercentage), + Auto, + Fraction(f32), +} +type NonRepeatedTrackSizingFunction = MinMax; +#[derive(Clone, Debug)] +enum GridTrackRepetition { + AutoFill, + AutoFit, + Count(u16), +} + +#[derive(Clone, Debug)] +enum TrackSizingFunction { + Single(NonRepeatedTrackSizingFunction), + Repeat(GridTrackRepetition, Vec), +} + +#[derive(Clone, Default, Debug)] +enum GridAutoFlow { + #[default] + Row, + Column, + RowDense, + ColumnDense, +} + +#[derive(Clone, Debug)] +struct GridLine(i16); +#[derive(Clone, Default, Debug)] +enum GridPlacement { + #[default] + Auto, + Line(GridLine), + Span(u16), +} +#[derive(Clone, Default, Debug)] +struct TaffyStyle { + pub display: UserSettable, + pub overflow: UserSettable>, + pub scrollbar_width: UserSettable, + pub position: UserSettable, + pub inset: UserSettable>, + pub size: UserSettable>, + pub min_size: UserSettable>, + pub max_size: UserSettable>, + pub aspect_ratio: UserSettable, + pub margin: UserSettable>, + pub padding: UserSettable>, + pub border: UserSettable>, + pub align_items: UserSettable, + pub align_self: UserSettable, + pub justify_items: UserSettable, + pub justify_self: UserSettable, + pub align_content: UserSettable, + pub justify_content: UserSettable, + pub gap: UserSettable>, + pub flex_direction: UserSettable, + pub flex_wrap: UserSettable, + pub flex_basis: UserSettable, + pub flex_grow: UserSettable, + pub flex_shrink: UserSettable, + pub grid_template_rows: UserSettable>, + pub grid_template_columns: UserSettable>, + pub grid_auto_rows: UserSettable>, + pub grid_auto_columns: UserSettable>, + pub grid_auto_flow: UserSettable, + pub grid_row: UserSettable>, + pub grid_column: UserSettable>, +} + +trait ValueToUserSettable { + fn to_user_settable(self, span: Span, inverse: bool) -> Result, RuleParseError>; +} + +impl ValueToUserSettable for TokenTree { + fn to_user_settable(self, _span: Span, inverse: bool) -> Result, RuleParseError> { + match self { + TokenTree::Literal(lit) => { + let text = lit.to_string(); + let mut lit = text.parse::().map_err(|_| RuleParseError { + span: lit.span(), + message: "Expected a float".to_owned() + })?; + if inverse { + lit = -lit; + } + Ok(UserSettable::Value(lit)) + }, + TokenTree::Group(group) => { + if inverse { + return Err(RuleParseError { + span: group.span(), + message: "Groups are not invertible".to_owned() + }) + } + Ok(UserSettable::Arbitrary(group.stream())) + }, + _ => { + Err(RuleParseError { + span: self.span(), + message: "Expected a literal or a group".to_owned() + }) + } + } + } +} + +impl ValueToUserSettable for TokenTree { + fn to_user_settable(self, span: Span, inverse: bool) -> Result, RuleParseError> { + match self.to_user_settable(span, inverse)? { + UserSettable::Value(value) => Ok(UserSettable::Value(LengthPercentage::Length(value))), + UserSettable::Arbitrary(stream) => { + Ok(UserSettable::Arbitrary(stream)) + }, + UserSettable::None => { + Err(RuleParseError { + span, + message: "Expected a value".to_owned() + }) + } + + } + } +} + +impl ValueToUserSettable for TokenTree { + fn to_user_settable(self, span: Span, inverse: bool) -> Result, RuleParseError> { + match &self { + TokenTree::Literal(lit) if lit.to_string() == "auto" => { + if inverse { + return Err(RuleParseError { + span: lit.span(), + message: "auto is not invertible".to_owned() + }) + } + return Ok(UserSettable::Value(LengthPercentageAuto::Auto)) + }, + _ => {} + } + match self.to_user_settable(span, inverse)? { + UserSettable::Value(value) => Ok(UserSettable::Value(LengthPercentageAuto::Length(value))), + UserSettable::Arbitrary(stream) => { + Ok(UserSettable::Arbitrary(stream)) + }, + UserSettable::None => { + Err(RuleParseError { + span, + message: "Expected a value".to_owned() + }) + } + + } + } +} + +impl ValueToUserSettable for TokenTree { + fn to_user_settable(self, span: Span, inverse: bool) -> Result, RuleParseError> { + if inverse { + return Err(RuleParseError { + span, + message: "overflow is not invertible".to_owned() + }) + } + match self { + TokenTree::Ident(ident) => { + match ident.to_string().as_str() { + "visible" => Ok(UserSettable::Value(Overflow::Visible)), + "hidden" => Ok(UserSettable::Value(Overflow::Hidden)), + "scroll" => Ok(UserSettable::Value(Overflow::Scroll)), + "clip" => Ok(UserSettable::Value(Overflow::Clip)), + _ => { + Err(RuleParseError { + span: ident.span(), + message: "Expected a valid value (one of visible, hidden, scroll or clip)".to_owned() + }) + } + } + }, + _ => { + Err(RuleParseError { + span: self.span(), + message: "Expected a valid value (one of visible, hidden, scroll or clip)".to_owned() + }) + } + } + } +} + +impl ValueToUserSettable for TokenTree { + fn to_user_settable(self, _span: Span, inverse: bool) -> Result, RuleParseError> { + match self { + TokenTree::Group(group) => { + if inverse { + return Err(RuleParseError { + span: group.span(), + message: "Groups are not invertible".to_owned() + }) + } + Ok(UserSettable::Arbitrary(group.stream())) + }, + _ => { + Err(RuleParseError { + span: self.span(), + message: "Expected a group - layout doesn't support literal values".to_owned() + }) + } + } + } +} + +impl, Y> ValueToUserSettable for Option { + fn to_user_settable(self, span: Span, inverse: bool) -> Result, RuleParseError> { + match self { + Some(value) => value.to_user_settable(span, inverse), + None => Err(RuleParseError { + span, + message: "Expected a value".to_owned() + }) + } + } +} + +#[derive(Clone, Debug)] +struct Rule { + prefix: Option, + inverse: bool, + prefix_span: Span, + name: String, + name_span: Span, + value: Option +} + +struct RuleParseError { + span: Span, + message: String +} + +#[proc_macro] +pub fn uno(item: proc_macro::TokenStream) -> proc_macro::TokenStream { + let item = TokenStream::from(item); + let mut output = TokenStream::new(); + dbg!(&item); + let rules = parse_rules(item); + + let rules = match rules { + Ok(rules) => rules, + Err(err) => { + let RuleParseError { span, message } = err; + return quote_spanned!(span => { + compile_error!(#message); + }).into(); + } + }; + + let style = match process_rules(rules) { + Ok(style) => style, + Err(err) => { + let RuleParseError { span, message } = err; + return quote_spanned!(span => { + compile_error!(#message); + }).into(); + } + }; + + dbg!(style); + + output.into() +} + +fn process_rules(rules: Vec) -> Result { + let mut style = Style::default(); + dbg!(&rules); + + for rule in rules { + let Rule { + prefix, + prefix_span, + name, + name_span, + value, + inverse + } = rule; + + // todo: handle passing single objects to certain attributes (p, m, gap, overflow, etc) + match name.as_str() { + "flex" => { + // todo: support certain identifiers like col, row, etc + if let Some(value) = value { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.flex_grow = value.clone(); + style.layout.require_non_arbitrary()?.flex_shrink = value; + } else { + style.layout.require_non_arbitrary()?.display = UserSettable::Value(Display::Flex); + } + }, + "p" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.top = value.clone(); + style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.right = value.clone(); + style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.left = value.clone(); + style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.bottom = value; + }, + "m" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.top = value.clone(); + style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.right = value.clone(); + style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.left = value.clone(); + style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.bottom = value; + }, + "pt" | "pr" | "pl" | "pb" => { + let value = value.to_user_settable(name_span, inverse)?; + match name.as_str() { + "pt" => style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.top = value, + "pr" => style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.right = value, + "pl" => style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.left = value, + "pb" => style.layout.require_non_arbitrary()?.padding.require_non_arbitrary()?.bottom = value, + _ => {} + } + }, + "mt" | "ml" | "mr" | "mb" => { + let value = value.to_user_settable(name_span, inverse)?; + match name.as_str() { + "mt" => style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.top = value, + "mr" => style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.right = value, + "ml" => style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.left = value, + "mb" => style.layout.require_non_arbitrary()?.margin.require_non_arbitrary()?.bottom = value, + _ => {} + } + }, + "gap" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.gap.require_non_arbitrary()?.width = value.clone(); + style.layout.require_non_arbitrary()?.gap.require_non_arbitrary()?.height = value; + }, + "overflow" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.overflow.require_non_arbitrary()?.x = value.clone(); + style.layout.require_non_arbitrary()?.overflow.require_non_arbitrary()?.y = value; + }, + "overflow_x" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.overflow.require_non_arbitrary()?.x = value; + }, + "overflow_y" => { + let value = value.to_user_settable(name_span, inverse)?; + style.layout.require_non_arbitrary()?.overflow.require_non_arbitrary()?.y = value; + }, + "layout" => { + if let Some(value) = value { + let value = value.to_user_settable(name_span, inverse)?; + style.layout = value; + } else { + return Err(RuleParseError { + span: name_span, + message: "Expected a value as a reference to TaffyStyle object".to_owned() + }); + } + }, + _ => { + return Err(RuleParseError { + span: name_span, + message: "Unknown rule".to_owned() + }); + } + } + } + + Ok(style) +} + +/// Parse the rules from the input +/// +/// Example rules: +/// +/// flex p-5px mt-1 mb-2 ml-[i] hover:test overflow-hidden flex-col mb-1/2 +fn parse_rules(item: TokenStream) -> Result, RuleParseError> { + let mut rules = vec![]; + let mut prefix = None; + let mut prefix_span = Span::call_site(); + let mut name = String::new(); + let mut name_span = Span::call_site(); + let mut value = None; + let mut should_parse_value = false; + let mut inverse = false; + + for token in item { + match token { + TokenTree::Ident(ident) => { + if should_parse_value { + value = Some(TokenTree::Ident(ident)); + should_parse_value = false; + + rules.push(Rule { + prefix: prefix.take(), + prefix_span, + name: std::mem::take(&mut name), + name_span, + value: value.take(), + inverse + }); + } else if prefix.is_some() && name.is_empty() { + name = ident.to_string(); + name_span = ident.span(); + } else { + if !name.is_empty() { + rules.push(Rule { + prefix: prefix.take(), + prefix_span, + name, + name_span, + value: None, + inverse + }); + } + name = ident.to_string(); + name_span = ident.span(); + } + inverse = false; + prefix_span = Span::call_site(); + }, + TokenTree::Punct(punct) => { + if punct.as_char() == '-' { + if name.is_empty() { + inverse = true; + } else { + should_parse_value = true; + } + } else if punct.as_char() == ':' { + prefix = Some(name); + prefix_span = name_span; + name = String::new(); + } else if punct.as_char() == '/' { + return Err(RuleParseError { + span: punct.span(), + message: "Fractions are not supported yet".to_owned() + }); + } else { + return Err(RuleParseError { + span: punct.span(), + message: "Unexpected punctuation".to_owned() + }); + } + }, + TokenTree::Literal(_) | TokenTree::Group(_) => { + if !should_parse_value { + // error + return Err(RuleParseError { + span: token.span(), + message: "Unexpected value, expected literal (rule start)".to_owned() + }); + } + value = Some(token); + + rules.push(Rule { + prefix: prefix.take(), + prefix_span, + name: std::mem::take(&mut name), + name_span, + value: value.take(), + inverse + }); + should_parse_value = false; + inverse = false; + prefix_span = Span::call_site(); + }, + } + } + + if !name.is_empty() { + rules.push(Rule { + prefix: prefix.take(), + prefix_span, + name, + name_span, + value: None, + inverse + }); + } + + Ok(rules) +} \ No newline at end of file