{"id":13683548,"url":"https://github.com/jazzfool/reclutch","last_synced_at":"2025-07-15T20:30:33.494Z","repository":{"id":137122495,"uuid":"219296603","full_name":"jazzfool/reclutch","owner":"jazzfool","description":"Rust UI Core","archived":false,"fork":false,"pushed_at":"2020-09-05T12:07:37.000Z","size":1688,"stargazers_count":157,"open_issues_count":0,"forks_count":4,"subscribers_count":9,"default_branch":"master","last_synced_at":"2024-04-24T07:36:38.128Z","etag":null,"topics":["gui","rust","ui"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jazzfool.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-11-03T12:21:20.000Z","updated_at":"2024-04-12T07:47:02.000Z","dependencies_parsed_at":"2024-01-13T16:47:58.739Z","dependency_job_id":"52b4015f-cfcc-4984-bbe8-b1d2002f62af","html_url":"https://github.com/jazzfool/reclutch","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzfool%2Freclutch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzfool%2Freclutch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzfool%2Freclutch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jazzfool%2Freclutch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jazzfool","download_url":"https://codeload.github.com/jazzfool/reclutch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226065992,"owners_count":17568302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gui","rust","ui"],"created_at":"2024-08-02T13:02:15.206Z","updated_at":"2024-11-23T16:23:12.259Z","avatar_url":"https://github.com/jazzfool.png","language":"Rust","funding_links":[],"categories":["Rust","UI Framework"],"sub_categories":["Native"],"readme":"# Reclutch\n\n[![Build Status](https://travis-ci.com/jazzfool/reclutch.svg?branch=master)](https://travis-ci.com/jazzfool/reclutch)\n\nA strong foundation for building predictable and straight-forward Rust UI toolkits. Reclutch is:\n\n- **Bare:** Very little UI code is included. In practice it's a utility library which makes very little assumptions about the toolkit or UI.\n- **Platform-agnostic:** Although a default display object is provided, the type of display object is generic, meaning you can build for platforms other than desktop. For example you can create web applications simply by using DOM nodes as display objects while still being efficient, given the retained-mode design.\n- **Reusable:** Provided structures such as unbound queue handlers allow for the reuse of common logical components across widgets.\n\n## Overview\n\nReclutch implements the well-known retained-mode widget ownership design within safe Rust, following along the footsteps of popular desktop frameworks. To implement this behavior, three core ideas are implemented:\n\n- A widget ownership model with no middleman, allowing widgets to mutate children at any time, but also collect children as a whole to make traversing the widget tree a trivial task.\n- A robust event queue system with support for `futures`, `crossbeam` and `winit` event loop integration, plus a multitude of queue utilities and queue variations for support in any environment.\n- An event queue abstraction to facilitate just-in-time event coordination between widgets, filling any pitfalls that may arise when using event queues. Beyond this, it also moves the code to handle queues to the constructor, presenting an opportunity to modularize and reuse logic across widgets.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\".media/showcase.png\" width=\"90%\"/\u003e\n\u003c/p\u003e\n\n### Note for MacOS\n\nThere appears to be a bug with shared OpenGL textures on MacOS. As a result, the `opengl` example won't work correctly. For applications that require rendering from multiple contexts into a single texture, consider using Vulkan or similar.\n\n### _Also see:_\n\n- [Events and event queues](event/README.md)\n- [Otway](https://github.com/jazzfool/otway)\n\n## Example\n\nAll rendering details have been excluded for simplicity.\n\n```rust\n#[derive(WidgetChildren)]\nstruct Button {\n    pub button_press: RcEventQueue\u003c()\u003e,\n    graph: VerbGraph\u003cButton, ()\u003e,\n}\n\nimpl Button {\n    pub fn new(global: \u0026mut RcEventQueue\u003cWindowEvent\u003e) -\u003e Self {\n        Button {\n            button_press: RcEventQueue::new(),\n            global_listener: VerbGraph::new().add(\n                \"global\",\n                QueueHandler::new(global).on(\"click\", |button, _aux, _event: WindowEvent| {\n                    button.button_press.emit_owned(());\n                }),\n            ),\n        }\n    }\n}\n\nimpl Widget for Button {\n    type UpdateAux = ();\n    type GraphicalAux = ();\n    type DisplayObject = DisplayCommand;\n\n    fn bounds(\u0026self) -\u003e Rect { /* --snip-- */ }\n\n    fn update(\u0026mut self, aux: \u0026mut ()) {\n        // Note: this helper function requires that `HasVerbGraph` be implemented on `Self`.\n        reclutch_verbgraph::update_all(self, aux);\n        // The equivalent version which doesn't require `HasVerbGraph` is;\n        let mut graph = self.graph.take().unwrap();\n        graph.update_all(self, aux);\n        self.graph = Some(graph);\n    }\n\n    fn draw(\u0026mut self, display: \u0026mut dyn GraphicsDisplay, _aux: \u0026mut ()) { /* --snip-- */ }\n}\n```\n\nThe classic counter example can be found in examples/overview.\n\n---\n\n## Children\n\nChildren are stored manually by the implementing widget type.\n\n```rust\n#[derive(WidgetChildren)]\nstruct ExampleWidget {\n    #[widget_child]\n    child: AnotherWidget,\n    #[vec_widget_child]\n    children: Vec\u003cAnotherWidget\u003e,\n}\n```\n\nWhich expands to exactly...\n\n```rust\nimpl reclutch::widget::WidgetChildren for ExampleWidget {\n    fn children(\n        \u0026self,\n    ) -\u003e Vec\u003c\n        \u0026dyn reclutch::widget::WidgetChildren\u003c\n            UpdateAux = Self::UpdateAux,\n            GraphicalAux = Self::GraphicalAux,\n            DisplayObject = Self::DisplayObject,\n        \u003e,\n    \u003e {\n        let mut children = Vec::with_capacity(1 + self.children.len());\n        children.push(\u0026self.child as _);\n        for child in \u0026self.children {\n            children.push(child as _);\n        }\n        children\n    }\n\n    fn children_mut(\n        \u0026mut self,\n    ) -\u003e Vec\u003c\n        \u0026mut dyn reclutch::widget::WidgetChildren\u003c\n            UpdateAux = Self::UpdateAux,\n            GraphicalAux = Self::GraphicalAux,\n            DisplayObject = Self::DisplayObject,\n        \u003e,\n    \u003e {\n        let mut children = Vec::with_capacity(1 + self.children.len());\n        children.push(\u0026mut self.child as _);\n        for child in \u0026mut self.children {\n            children.push(child as _);\n        }\n        children\n    }\n}\n```\n\n(Note: you can switch out the `reclutch::widget::WidgetChildren`s above with your own trait using `#[widget_children_trait(...)]`)\n\nThen all the other functions (`draw`, `update`, maybe even `bounds` for parent clipping) are propagated manually (or your API can have a function which automatically and recursively invokes for both parent and child);\n\n```rust\nfn draw(\u0026mut self, display: \u0026mut dyn GraphicsDisplay) {\n    // do our own rendering here...\n\n    // ...then propagate to children\n    for child in self.children_mut() {\n        child.draw(display);\n    }\n}\n```\n\n**Note:** `WidgetChildren` requires that `Widget` is implemented.\n\nThe derive functionality is a feature, enabled by default.\n\n## Rendering\n\nRendering is done through \"command groups\". It's designed in a way that both a retained-mode renderer (e.g. WebRender) and an immediate-mode renderer (Direct2D, Skia, Cairo) can be implemented.\nThe API also supports Z-Order.\n\n```rust\nstruct VisualWidget {\n    command_group: CommandGroup,\n}\n\nimpl Widget for VisualWidget {\n    // --snip--\n\n    fn update(\u0026mut self, _aux: \u0026mut ()) {\n        if self.changed {\n            // This simply sets an internal boolean to \"true\", so don't be afraid to call it multiple times during updating.\n            self.command_group.repaint();\n        }\n    }\n\n    // Draws a nice red rectangle.\n    fn draw(\u0026mut self, display: \u0026mut dyn GraphicsDisplay, _aux: \u0026mut ()) {\n        let mut builder = DisplayListBuilder::new();\n        builder.push_rectangle(\n            Rect::new(Point::new(10.0, 10.0), Size::new(30.0, 50.0)),\n            GraphicsDisplayPaint::Fill(Color::new(1.0, 0.0, 0.0, 1.0).into()),\n            None);\n\n        // Only pushes/modifies the command group if a repaint is needed.\n        self.command_group.push(display, \u0026builder.build(), Default::default(), None, true);\n\n        draw_children();\n    }\n\n    // --snip--\n}\n```\n\n## Updating\n\nThe `update` method on widgets is an opportunity for widgets to update layout, animations, etc. and more importantly handle events that have been emitted since the last `update`.\n\nWidgets have an associated type; `UpdateAux` which allows for a global object to be passed around during updating. This is useful for things like updating a layout.\n\nHere's a simple example;\n\n```rust\ntype UpdateAux = Globals;\n\nfn update(\u0026mut self, aux: \u0026mut Globals) {\n    if aux.layout.node_is_dirty(self.layout_node) {\n        self.bounds = aux.layout.get_node(self.layout_node);\n        self.command_group.repaint();\n    }\n\n    self.update_animations(aux.delta_time());\n\n    // propagation is done manually\n    for child in self.children_mut() {\n        child.update(aux);\n    }\n\n    // If your UI doesn't update constantly, then you must check child events *after* propagation,\n    // but if it does update constantly, then it's more of a micro-optimization, since any missed events\n    // will come back around next update.\n    //\n    // This kind of consideration can be avoided by using the more \"modern\" updating API; `verbgraph`,\n    // which is discussed in the \"Updating correctly\" section.\n    for press_event in self.button_press_listener.peek() {\n        self.on_button_press(press_event);\n    }\n}\n```\n\n## Updating correctly\n\nThe above code is fine, but for more a complex UI then there is the possibility of events being processed out-of-order.\nTo fix this, Reclutch has the `verbgraph` module; a facility to jump between widgets and into their specific queue handlers.\nIn essence, it breaks the linear execution of update procedures so that dependent events can be handled even if the primary `update` function has already be executed.\n\nThis is best shown through example;\n\n```rust\nfn new() -\u003e Self {\n    let graph = verbgraph! {\n        Self as obj,\n        Aux as aux,\n\n        // the string \"count_up\" is the tag used to identify procedures.\n        // they can also overlap.\n        \"count_up\" =\u003e event in \u0026count_up.event =\u003e {\n            click =\u003e {\n                // here we mutate a variable that `obj.template_label` implicitly/indirectly depends on.\n                obj.count += 1;\n                // Here template_label is assumed to be a label whose text uses a template engine\n                // that needs to be explicitly rendered.\n                obj.template_label.values[0] = obj.count.to_string();\n                // If we don't call this then `obj.dynamic_label` doesn't\n                // get a chance to respond to our changes in this update pass.\n                // This doesn't invoke the entire update cycle for `template_label`, only the specific part we care about; `\"update_template\"`.\n                reclutch_verbgraph::require_update(\u0026mut obj.template_label, aux, \"update_template\");\n                // \"update_template\" refers to the tag.\n            }\n        }\n    };\n    // ...\n}\n\nfn update(\u0026mut self, aux: \u0026mut Aux) {\n    for child in self.children_mut() {\n        child.update(aux);\n    }\n\n    reclutch_verbgraph::update_all(self, aux);\n}\n```\n\nIn the `verbgraph` module is also the `Event` trait, which is required to support the syntax seen in `verbgraph!`.\n\n```rust\n#[derive(Event, Clone)]\nenum AnEvent {\n    #[event_key(pop)]\n    Pop,\n    #[event_key(squeeze)]\n    Squeeze(f32),\n    #[event_key(smash)]\n    Smash {\n        force: f64,\n        hulk: bool,\n    },\n}\n```\n\nGenerates exactly;\n\n```rust\nimpl reclutch::verbgraph::Event for AnEvent {\n    fn get_key(\u0026self) -\u003e \u0026'static str {\n        match self {\n            AnEvent::Pop =\u003e \"pop\",\n            AnEvent::Squeeze(..) =\u003e \"squeeze\",\n            AnEvent::Smash{..} =\u003e \"smash\",\n        }\n    }\n}\n\nimpl AnEvent {\n    pub fn unwrap_as_pop(self) -\u003e Option\u003c()\u003e {\n        if let AnEvent::Pop = self {\n            Some(())\n        } else {\n            None\n        }\n    }\n\n    pub fn unwrap_as_squeeze(self) -\u003e Option\u003c(f32)\u003e {\n        if let AnEvent::Squeeze(x0) = self {\n            Some((x0))\n        } else {\n            None\n        }\n    }\n\n    pub fn unwrap_as_smash(self) -\u003e Option\u003c(f64, bool)\u003e {\n        if let AnEvent::Smash{force, hulk} = self {\n            Some((force, hulk))\n        } else {\n            None\n        }\n    }\n}\n```\n\n`get_key` is used to find the correct closure to execute given an event\nand `unwrap_as_` is used to extract the inner information from within the\ngiven closure (because once `get_key` is matched then we can be certain it\nis of a certain variant).\n\n## License\n\nReclutch is licensed under either\n\n- [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)\n- [MIT](http://opensource.org/licenses/MIT)\n\nat your choosing.\n\nThis license also applies to all \"sub-projects\" (`event`, `derive` and `verbgraph`).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjazzfool%2Freclutch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjazzfool%2Freclutch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjazzfool%2Freclutch/lists"}