Ply 1.0: A dead-simple, cross-platform app engine (with live WASM docs)

Hello everyone!

I’m excited to share a project I’ve been working on: Ply 1.0.

It started because I wanted to build a multiplayer board game in Rust. While there are some amazing UI frameworks out there, I didn't want to fight with boilerplate, deep ECS queries, or complex layout verbosity just to get basic screens running. I wanted an API that felt natural, so I ended up writing my own pure Rust layout engine and building an app engine around it.

Ply is designed to make building cross-platform UIs dead simple. It uses a builder pattern with closures, requires just one import, and runs on six platforms from a single codebase.

Here is what the API looks like:

use ply_engine::prelude::*;

// inside your app loop
ui.element().width(grow!()).height(grow!())
    .background_color(0x262220)
    .layout(|l| l.align(CenterX, CenterY).padding(24))
    .children(|ui| {
        ui.text("Hello, Ply!", |t| t.font_size(32).color(0xFFFFFF));
    });

What's inside:

Instead of just being a rendering wrapper, Ply is a full-featured engine:

  • Six Platforms: Linux, macOS, Windows, Android, iOS, and Web via the plyx CLI.
  • Advanced Graphics: TinyVG vector support with on-demand rasterization and GLSL shaders applicable to elements.
  • Full Text Input: Support for selection, undo/redo, multiline, and standard keyboard shortcuts.
  • Accessibility: Deep integration with AccessKit on desktop and a JS bridge for web screen readers.
  • Networking & Audio: Built-in non-blocking HTTP/WebSockets and WAV/OGG playback.
  • Debug Inspector: A built-in, Chrome DevTools-style inspector to debug layouts and states in real-time.
  • Rich Text Styling: Inline gradients, per-character animations (wave, pulse), and typewriter effects.

Interactive Documentation

I was tired of reading static docs, so I built the documentation as a series of live WASM playgrounds. You can modify the Rust code directly in your browser and see the UI update instantly. There is also a small live interpreter on the homepage to experiment with the syntax.

Website: https://plyx.iz.rs

Interactive Docs: Getting Started | Ply

GitHub: TheRedDeveloper/ply-engine

Get Started

Ply is licensed under 0BSD (no attribution required). You can try it right now with:

cargo install plyx
plyx init

I'd love to hear your thoughts! Does this API style resonate with you? What kind of apps would you want to build with something like this? I'll be hanging around to answer any questions!

Interesting and commendable attempt but 'dead simple' might be a tad optimistic. I tried a simple 'increment button' based on app state but couldn't make it to work. Actually the whole concept of app state (for data heavy app) eludes me so far.

On the whole I would say it is in a quite rough and unusable state at the moment (reminds me of Xilem although that is probably far worse) so beats me why you would go for version '1.0'.

So what are your ambitions with this? Do you have a roadmap?

Thanks for the feedback! Here's a counter button:

let count = Rc::new(Cell::new(0));

// ... in your main loop
ui.element().width(grow!()).height(grow!())
  .layout(|l| l.align(CenterX, CenterY).gap(10).direction(TopToBottom))
  .children(|ui| {
    ui.text(&format!("Count: {}", count.get()), |t| t.font_size(32).color(0xFFFFFF));

    ui.element()
      .children(|ui| {
        let bg = if ui.pressed() {
            0xB91414
        } else if ui.hovered() {
            0xFF654D
        } else {
            0x3A3533
        };

        let c = count.clone();
        ui.element()
          .on_press(move |_| { c.set(c.get() + 1); })
          .background_color(bg)
          .corner_radius(6.0)
          .layout(|l| l.padding(5))
          .children(|ui| {
            ui.text("Increment", |t| t.font_size(24).color(0xFFFFFF));
          });
      });
  });

Your app state is just regular Rust variables. .on_press() fires once per click, and pressed()/hovered() work inline in the children closure for visual feedback. The Interactivity docs cover all of this along with a reusable button helper.

There's also just_pressed() (#24) feature planned which will make this even simpler by letting you check for single-frame presses inline without needing Rc<Cell<>>. #24 is actually a pretty good first issue if you want to contribute!

As for the roadmap, planned features are tracked as issues. Two have already been completed by others just today.