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

"RSX" Component builder with macros #56

Closed
Redrield opened this issue Nov 14, 2018 · 11 comments
Closed

"RSX" Component builder with macros #56

Redrield opened this issue Nov 14, 2018 · 11 comments

Comments

@Redrield
Copy link

Description

One thing that I feel as though Azul could benefit from is a way to create structured DOMs in a more declarative fashion, if we look to WASM interface frameworks, we can see this idea with how yew allows users to craft interfaces.

Describe your solution

A macro like dom! or html! that would allow people to make GUIs in a way that resembles JSX or TSX in frontend web development terms

Are there alternatives or drawbacks?

  • Sticking with the current system, despite it being less declarative
  • Maintaining the macro might be a pain, I'm not sure

Is this a breaking change

No, the old way of creating UIs could exist side by side with this

@fschutt
Copy link
Owner

fschutt commented Nov 14, 2018

No, I'm not very fond of this idea, I've thought about this already. The more general problem I see with JSX is that JSX is essentially just functions, wrapped in an XML-ish syntax. For example, take this JSX:

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is {isLoggedIn ? 'currently' : 'not'} logged in.
    </div>
  );
}

and compare it to this Rust / azul code:

fn render(&self) {
    Dom::new(NodeType::Div).with_child(
        Dom::new(NodeType::Label(
            format!("The user is {} logged in.", if self.logged_in { "currently" } else { "not" }))
        )
    )
}

See any similarities? All that JSX is doing is wrapping call_my_function() into <call_my_function /> and calling it "modern". The only reason I can see for this - besides hype and marketing - is performance, because in browsers, the size of the JS matters, in Rust it doesn't matter because Rust is compiled.

Other crates like conrod or yew - they can't expose a reasonable API without using macros, because they need boilerplate code - this essentially leaks implementation details into the public API, just that the macros cover it up a bit. I don't see macros as a "feature", I rather see them as a bug - the API has too much boilerplate to be written using functions, so macros are used to cover up things like Rust-String-to-JS-String conversions in yew, for example. One of the main appeals of azul is having a macro-free API and being able to define user interfaces purely by composing functions. Just to show how "declarative" a pure-functional UI can be, here is a function that I wrote for my application:

/// Render a key-value pair
fn render_config_field(key: &str, id: &str, value: String) -> Dom<AppData> {
    Dom::new(NodeType::Div).with_class("configuration_field").with_id(id)
            .with_child(Dom::new(NodeType::Label(key.into())).with_class("configuration_field_label"))
            .with_child(Dom::new(NodeType::Label(value)).with_class("configuration_field_value"))
}

/// Render the controls for editing the map
fn render_map_fields(map: &Map) -> Dom<AppData> {
    Dom::new(NodeType::Div).with_id("map_configuration_container")
        .with_child(render_config_section_header("Map Configuration"))
        .with_child(render_config_field("Width:", "map_width", format!("{}", map.width_mm)))
        .with_child(render_config_field("Height:", "map_height", format!("{}", map.height_mm)))
        .with_child(render_config_field("Center East:", "map_center_east", format!("{}", map.center_east)))
        .with_child(render_config_field("Center North:", "map_center_north", format!("{}", map.center_north)))
        .with_child(render_config_field("Scale:", "map_scale", format!("{}", map.scale)))
        .with_child(render_config_field("CRS:", "map_crs", format!("{}", map.target_crs)))
}

See how I didn't have to copy-paste the DOM creation of the key-value-pair everywhere? The question is, how is render_config_field("blah") less "declarative" than <ConfigField content="blah" />. It isn't, it's just a different format and instead of </>, there is .(), that's more or less the whole difference.

IMO, there is really no need for such a macro, because you can already efficiently compose the UI via functions (which is something that JS can't, since JS doesn't redraw the screen all the time and has to deal with the stateful browser, so they need React and JSX and all that stuff to deal with the statefulness).

Another reason against macros is that macros are always harder to debug than functions. Always. Every time I've used a macro in the past, I've been way better off with functions. Macros are fundamentally a hack around the type system, they should be used only when absolutely necessary and when functions can't be used.

Of course, you can make your own crate with extension macros that build on top of azul, you can use the public API for that. Kind of like the maplit crate (which exposes syntax sugar macros for std::collections::HashMap), but there is no need for it to be in the core library, you can use the public API for this.

Another possibility is to make "shorthands" for typing-intensive things like Dom::new(NodeType::Div) or something like that, i.e div(), p(), button(), etc. However, some users confused NodeType::label() with NodeType::Label() - the one being a function, the other being a enum variant - and having extremely short function names isn't such a great idea in general. However, even that doesn't have to exist in the crate itself either.

There is going to be a XML-based document loader (see #29), but that is only intended for prototyping and because of hot-reloading UI files while the app is running. So to finish this very long comment, my suggestion is that if you want this, make it as a separate crate and then I can see if parts of it may be used in the API. I currently have a lot of stuff to do, lots of other bugs to fix and so it would be better if it could be maintained separately, to split the work.

@erlend-sh
Copy link

erlend-sh commented Nov 20, 2018

Relevant: https://github.com/bodil/typed-html

@Yatekii
Copy link
Contributor

Yatekii commented Nov 20, 2018

Yes, typed HTML looks great but it has an entirely different purpose ;)
azul already provides type-safety ;)

@shulcsm
Copy link

shulcsm commented Nov 21, 2018

See any similarities? All that JSX is doing is wrapping call_my_function() into <call_my_function /> and calling it "modern". The only reason I can see for this - besides hype and marketing - is performance, because in browsers, the size of the JS matters, in Rust it doesn't matter because Rust is compiled.

Performance has nothing to do with it, you can use react just fine without the JSX.

Sorry but I don't see any real arguments except for implementation and debugging complexity.
So you dislike JSX, fine. What about QML? It sounds like you are dismissing DSLs as such just because you don't like them.

@fschutt
Copy link
Owner

fschutt commented Nov 21, 2018

Performance has nothing to do with it, you can use react just fine without the JSX.

I meant the code size (since JS files have to be delivered over the network), JSX is more "compact" than regular JS code, so that's an argument for better performance.

I'm not dismissing DSLs, but I am dismissing specializing on a specific DSL in the core library or building the core model of azul around a DSL. You should be able to build a DSL on top of the existing Rust API, not the other way around. DSLs should be syntax sugar, not the core foundation. So, since it's only syntax sugar, there is no reason for a DSL to be inside of azul.

Eventually, I will re-export things like the Dom, the CSS parser and all important traits into seperate crates, so that people can use them without depending on Azul itself. This way you could parse QSS (the QML CSS equivalent) without having to re-implement your custom CSS parser.

The thing is, DSL flavors come and go, today JSX is the best thing, other people like QML a lot, tomorrow there's something else. And well, "implementation and debugging complexity" is sadly a real argument - I don't have unlimited time. For my own projects that I use Azul for, I am able build UIs without a DSL, so I don't see not having a DSL as such a big problem as people make it out to be.

If you are interested in making something QML or JSX - be the change you want to see, that's really all I'm saying. Make a qml_to_azul_css crate or something. Someone else can make a jsx_to_azul crate and both can be happy. It's just that I don't want to maintain a DSL as part of azul itself or build the core architecture around a DSL.

@OtaK
Copy link

OtaK commented Nov 21, 2018

@fschutt I think you're a bit mistaken here: JSX compiles to pure JS function calls at build time.

See here: https://reactjs.org/docs/introducing-jsx.html#jsx-represents-objects

But I agree 100% with you that it should be either a separate crate or something feature-gated! It has no purpose in the core of a UI library (much like JSX is opt-in, you need a babel/webpack transformer to make it into JS React calls).

@fschutt
Copy link
Owner

fschutt commented Jan 18, 2019

@diegor8 I don't use yew or draco, this is the azul repository. I don't know if yew needs macros, ask at the yew maintainers, not me.

@fschutt
Copy link
Owner

fschutt commented Mar 20, 2019

Closing this because this is more or less what the XML system is. Take a look at the /examples/ui.xml for a syntax example:

<component name="Invoice" args="dueDate: String">
    <p>Your invoice is due at {dueDate}</p>
</component>

<app>
    <Invoice dueDate="02.03.2019" />
</app>

In the future, you will be able to compile that XML code to Rust functions (to have both a performant and type-safe prototyping API), but for now you can use the XML loader as a way to construct RSX-like macros.

@fschutt fschutt closed this as completed Mar 20, 2019
@richardanaya
Copy link

@fschutt
The logic in this thread seems flawed if the ultimate conclusion is "In the future, you will be able to compile that XML code to Rust functions". Nobody wants to prototype with XML and then throw away their work to make something real. It seems like that is the concept of what is realized with the "in the future" comment.

So really this feature seems to boil down to is it better to have:

  1. an external XML conversion process that turns symbols into rust symbols
  2. use a Rust macro to turn XML like syntax that tuns symbols into rust symbols

It seems questionable you think that people would rather look at a mountain of nested function calls vs an XML like language as their primary source. Claiming that functional composition makes this all easier reads suspiciously like someone who has never used this tech like this to make a complex UI page. There's a reason why React people don't write h(...) all over the place even though it's an option to them.

@fschutt
Copy link
Owner

fschutt commented May 14, 2019

@richardanaya

Nobody wants to prototype with XML and then throw away their work to make something real.

Well, I do, you're just supposed to compile the XML to Rust code at some point. Azul isn't a web browser, the point isn't to make the XML something like HTML, it's only there to enable hot-reloading, (so compile-time macros are out, because they don't run at runtime).

an external XML conversion process that turns symbols into rust symbols

Yes, that's what I meant by "XML-to-Rust compiler". There is already a stub API for that, even though it doesn't work yet. You can either compile your UI files once manually or put this function in your build script.

There's a reason why React people don't write h(...) all over the place even though it's an option to them.

Yeah because </> looks more familiar to web developers than .(), that's about it. I'd rather debug if thing { Button() } than <if thing="true"><Button /></if> and reinvent my own custom XML-based programming language (I have flashbacks to Angular and ng-if). How else would you do control flow and loops in XML - you'd just reinvent an XML-ified programming language, that's what I'm trying to avoid.

React is functional composition of UI elements, mapping (or "rendering") a model onto the screen, (model -> view). React just does a bit of smart caching and state management. You can see this mapping as a pure function, then it might be easier to understand what I mean by "composing functions". Using functions makes it both faster (since the code is compiled) and less proprietary (so you're not locked into my XML implementation if you don't like it).

Again:

  • The purpose of XML is hot-reloading, not to make the code easier to read (because of Rusts long compile times). I'll probably disable the XML loader in release mode, so that people don't accidentally use XML in release mode and then complain about bad performance.
  • no macro system, macros way harder to debug than regular Rust code. They look nice at first glance, but that's about it, working with macros is horrible, they should be used as a last resort
  • You can compile XML to Rust source code at build time if you want.

@richardanaya
Copy link

richardanaya commented May 14, 2019

I think I understand, I feel a bit silly now, but I do see your point about hot reloading at runtime being the only real way to do that fast in Rust. Thanks for clarifying. I do hope the XML side of this project grows! As someone who has worked with HTML and XAML before, I hope webrender is the future of GUI in Rust :)

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

7 participants