├── .github ├── ISSUE_TEMPLATE │ ├── add-widget.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── contributors.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── assets ├── logo-128x128.png ├── logo-256x256.png ├── logo-512x512.png └── logo.svg ├── clippy.toml ├── examples ├── animation │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── canvas │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── checkbox │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── component │ ├── Cargo.toml │ └── src │ │ ├── counter.rs │ │ └── main.rs ├── counter │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── fetcher │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── gesture_detector │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hello_world │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── icon │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── image │ ├── Cargo.toml │ ├── pelican.jpg │ └── src │ │ └── main.rs ├── plugin │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── plugin.rs └── slider │ ├── Cargo.toml │ └── src │ └── main.rs ├── maycoon-core ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── NotoSans.ttf │ ├── app │ ├── context.rs │ ├── diagnostics.rs │ ├── font_ctx.rs │ ├── handler.rs │ ├── info.rs │ ├── mod.rs │ ├── runner.rs │ └── update.rs │ ├── component.rs │ ├── config.rs │ ├── layout.rs │ ├── lib.rs │ ├── plugin.rs │ ├── reference.rs │ ├── signal │ ├── actor.rs │ ├── eval.rs │ ├── fixed.rs │ ├── map.rs │ ├── memoized.rs │ ├── mod.rs │ ├── rw.rs │ └── state.rs │ ├── tasks │ ├── mod.rs │ └── runner.rs │ └── widget.rs ├── maycoon-macros ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── assets.rs │ ├── lib.rs │ └── svg_icon.rs ├── maycoon-theme ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── globals.rs │ ├── id.rs │ ├── lib.rs │ ├── style.rs │ └── theme │ ├── celeste.rs │ ├── celeste.rs~ │ ├── mod.rs │ └── mod.rs~ ├── maycoon-widgets ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── animator.rs │ ├── button.rs │ ├── canvas.rs │ ├── checkbox.rs │ ├── container.rs │ ├── fetcher.rs │ ├── gesture_detector.rs │ ├── icon │ ├── mod.rs │ └── svg.rs │ ├── image.rs │ ├── lib.rs │ ├── slider.rs │ └── text.rs ├── release-plz.toml ├── rustfmt.toml └── src └── lib.rs /.github/ISSUE_TEMPLATE/add-widget.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Add Widget 3 | about: Suggest a Widget to add to our collection 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What Widget should we add?** 11 | The name of the widget and possible examples of its implementation in other frameworks. 12 | 13 | **What purpose should it fulfill?** 14 | Describe the feature/function the widget should have and why we need it. 15 | 16 | **Alternatives** 17 | Are there any alternative approaches/widgets that would also fulfill this widgets purpose? 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, requires-review 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Platform:** 27 | - OS: [e.g. Windows 11] 28 | - Cargo (`cargo --version`): [e.g. `cargo 1.81.0-nightly`] 29 | - Crate Version: [e.g. `maycoon 0.1.0`] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, requires-review 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout Repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Install Rust toolchain 22 | uses: dtolnay/rust-toolchain@stable 23 | 24 | - name: Update apt-get 25 | run: sudo apt-get update 26 | 27 | - name: Install fontconfig 28 | run: sudo apt install pkg-config libfreetype6-dev libfontconfig1-dev 29 | 30 | - name: Build 31 | run: cargo build --verbose --workspace 32 | 33 | - name: Run tests 34 | run: cargo test --verbose --workspace 35 | -------------------------------------------------------------------------------- /.github/workflows/contributors.yml: -------------------------------------------------------------------------------- 1 | name: Contributors 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | contrib-readme-job: 7 | runs-on: ubuntu-latest 8 | name: Generates contributors list for the README 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | steps: 13 | - name: Contribute List 14 | uses: akhilmhdh/contributors-readme-action@v2.3.10 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install Rust toolchain 23 | uses: dtolnay/rust-toolchain@stable 24 | 25 | - name: Update apt-get 26 | run: sudo apt-get update 27 | 28 | - name: Install fontconfig 29 | run: sudo apt install pkg-config libfreetype6-dev libfontconfig1-dev 30 | 31 | - name: Run release-plz 32 | uses: MarcoIeni/release-plz-action@v0.5 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | Cargo.lock 4 | Maycoon.iml 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-v0.3.2...maycoon-v0.4.0) - 2025-04-29 11 | 12 | ### Added 13 | 14 | - Global State Management 15 | 16 | ### Fixed 17 | 18 | - Typo in README 19 | 20 | ### Other 21 | 22 | - Update syn to 2.0.101 23 | - Remove parking_lot dependency 24 | - Update log to 0.4.27 25 | - Update bytemuck to 1.23.0 26 | - Update Cargo.toml 27 | - Update taffy to 0.8.1 28 | 29 | ## [0.3.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-v0.1.0...maycoon-v0.3.0) - 2025-01-26 30 | 31 | ### Other 32 | 33 | - Fix release-plz 34 | - Configure release-plz 35 | - Fix clippy lints 36 | - Update build.yml 37 | - Update build.yml 38 | - Update README.md 39 | - Update build.yml 40 | - Update build.yml 41 | - Update release.yml 42 | - Update dependencies 43 | - Add canvas widget 44 | - Merge pull request [#28](https://github.com/maycoon-ui/maycoon/pull/28) from waywardmonkeys/update-to-vello-0.3 45 | - Replace dashmap with indexmap 46 | - Update issue templates 47 | - Update README.md 48 | 49 | ## [0.1.0](https://github.com/maycoon-ui/maycoon/releases/tag/maycoon-v0.1.0) - 2024-10-04 50 | 51 | ### Fixed 52 | 53 | - fixed state issue 54 | 55 | ### Other 56 | 57 | - Update release.yml 58 | - Update README.md 59 | - Create release.yml 60 | - Update build.yml 61 | - Create build.yml 62 | - Add workspace keys 63 | - Update SECURITY.md 64 | - Restyling 65 | - Rename crates and rework state value 66 | - Make may-macro optional 67 | - Add macros feature 68 | - Moved examples to separate workspace 69 | - Format 70 | - Format 71 | - Update rustfmt.toml 72 | - Add READMEs 73 | - Update info 74 | - Fixed merge conflicts 75 | - Reworked state machine 76 | - Update hello-world.rs 77 | - Update counter.rs 78 | - Update dependencies 79 | - Update README.md 80 | - Update hello-world.rs 81 | - Create counter.rs 82 | - Added counter example and macro crate 83 | - Update lib.rs 84 | - Update issue templates 85 | - Update issue templates 86 | - Create CONTRIBUTING.md 87 | - Create SECURITY.md 88 | - Create CODE_OF_CONDUCT.md 89 | - Change Text to Button 90 | - Create rustfmt.toml 91 | - Create clippy.toml 92 | - Hide vello export behind feature gate 93 | - Added Documentation and fixes 94 | - Added Text Rendering 95 | - Update README.md 96 | - Remake (again) 97 | - Remake 98 | - Widgets & Fixes 99 | - Reworked Themes and Styles 100 | - Complete Re-Work ( Again ) 101 | - Renderer Revamp 102 | - Added widget lifetimes 103 | - New State Machine 104 | - Update hello-world.rs 105 | - Rename faq.md to FAQ.md 106 | - Fix fmt & theme'ing 107 | - Fix URLs 108 | - Update faq.md 109 | - Update hello-world.rs 110 | - Update Cargo.toml 111 | - Add themes 112 | - Update Cargo.toml 113 | - Update config.toml 114 | - Added themes 115 | - fmt 116 | - Init 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mp@ypon.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Maycoon 2 | 3 | First off, thanks for taking the time to contribute! We appreciate your support. 4 | 5 | The following is a set of guidelines for contributing to Maycoon. These guidelines are intended to make it easy for you to get involved with us. 6 | 7 | ## Table of Contents 8 | 9 | 1. [How Can I Contribute?](#how-can-i-contribute) 10 | - [Reporting Bugs](#reporting-bugs) 11 | - [Suggesting Enhancements](#suggesting-enhancements) 12 | - [Submitting Pull Requests](#submitting-pull-requests) 13 | 2. [Code of Conduct](#code-of-conduct) 14 | 3. [Style Guides](#style-guides) 15 | - [Coding Standards](#coding-standards) 16 | - [Commit Messages](#commit-messages) 17 | 4. [Contact](#contact) 18 | 19 | ## How Can I Contribute? 20 | 21 | ### Reporting Bugs 22 | 23 | If you find a bug, please report it by opening an issue in the [issue tracker](https://github.com/maycoon-ui/maycoon/issues). Make sure to include: 24 | 25 | - A clear and descriptive title. 26 | - A detailed description of the steps to reproduce the issue. 27 | - The expected and actual results. 28 | - Any relevant logs, screenshots, or other context. 29 | 30 | ### Suggesting Enhancements 31 | 32 | If you have an idea to improve Maycoon, we would love to hear about it! To suggest an enhancement: 33 | 34 | - Open an issue in the [issue tracker](https://github.com/maycoon-ui/maycoon/issues) with the label "enhancement". 35 | - Describe your idea and explain why it would be useful. 36 | 37 | ### Submitting Pull Requests 38 | 39 | 1. Fork the repository. 40 | 2. Create a new branch. 41 | 3. Make your changes. 42 | 4. Commit your changes. 43 | 5. Push to the branch. 44 | 6. Open a Pull Request. 45 | 46 | Please ensure that your pull request adheres to the following guidelines: 47 | 48 | - Include a clear description of the changes and why they are being made. 49 | - Follow the project's coding standards. 50 | - Ensure that your changes pass the existing tests and add new tests if necessary. 51 | 52 | ## Code of Conduct 53 | 54 | We are committed to maintaining a welcoming and respectful community. By participating, you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md). 55 | 56 | ## Style Guides 57 | 58 | ### Coding Standards 59 | 60 | - Make sure to use `rustfmt`, `cargo fix` and `clippy` for formatting and fixing code. 61 | - Your code must be 100% documented. Every crate should have `#![warn(missing_docs)]` in the root file to warn about missing documentation. 62 | - Try to be safe and use only `unsafe` code if really necessary. 63 | - Before adding any dependencies, make sure you really need the crate functionality and cannot implement features yourself. 64 | - Keep your code and dependencies up to date. 65 | - Comment complex code for future contributors. 66 | 67 | ### Commit Messages 68 | 69 | - Only give essential information about the commit. 70 | - Reference issues and pull requests related to the commit. 71 | - Don't leave out important information. 72 | 73 | ## Contact 74 | 75 | If you have any questions or need further assistance, please reach out to the Author Mikail Plotzky via mp@ypon.com. 76 | 77 | Thank you for contributing to Maycoon! 78 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["maycoon-core", "maycoon-macros", "maycoon-theme", "maycoon-widgets", "examples/*"] 3 | 4 | [workspace.package] 5 | version = "0.4.0" 6 | edition = "2021" 7 | authors = ["Mikail Plotzky ", "Maycoon Contributors"] 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/maycoon-ui/maycoon" 10 | homepage = "https://maycoon-ui.github.io" 11 | categories = ["gui", "graphics", "rendering", "virtualization", "rendering::engine"] 12 | keywords = ["ui", "gui", "interface", "graphics", "user-interface"] 13 | 14 | [workspace.dependencies] 15 | bytemuck = "1.23.0" 16 | peniko = "0.3.1" 17 | nalgebra = { version = "0.33.2", default-features = false, features = ["std"] } 18 | indexmap = "2.9.0" 19 | log = "0.4.27" 20 | maycoon-core = { version = "0.4.0", path = "maycoon-core" } 21 | maycoon-macros = { version = "0.4.0", path = "maycoon-macros" } 22 | maycoon-theme = { version = "0.4.0", path = "maycoon-theme" } 23 | maycoon-widgets = { version = "0.4.0", path = "maycoon-widgets" } 24 | 25 | [package] 26 | name = "maycoon" 27 | description = "Lightning fast and powerful UI Framework for Rust." 28 | version.workspace = true 29 | edition.workspace = true 30 | license.workspace = true 31 | authors.workspace = true 32 | categories.workspace = true 33 | keywords.workspace = true 34 | repository.workspace = true 35 | homepage.workspace = true 36 | 37 | [dependencies] 38 | maycoon-core = { workspace = true } 39 | maycoon-theme = { workspace = true } 40 | maycoon-widgets = { workspace = true } 41 | maycoon-macros = { workspace = true, optional = true } 42 | peniko = { workspace = true } 43 | nalgebra = { workspace = true } 44 | 45 | [features] 46 | default = ["macros", "include-noto-sans"] 47 | 48 | # Exports useful macros for working with maycoon. 49 | macros = ["maycoon-macros"] 50 | 51 | # Exports `vello` and `skrifa` for drawing vector graphics. Useful if you want to create custom widgets. 52 | vg = ["maycoon-core/vg"] 53 | 54 | # Enables the `Canvas` widget for drawing to the screen. 55 | canvas = ["maycoon-widgets/canvas"] 56 | 57 | # Include the `Noto Sans` font as a default font. If disabled, you must specify a default font using the `FontContext`. 58 | include-noto-sans = ["maycoon-core/include-noto-sans"] 59 | 60 | [lib] 61 | name = "maycoon" 62 | path = "src/lib.rs" 63 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mikail Plotzky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Maycoon Logo](assets/logo-256x256.png) 4 | 5 | # Maycoon 6 | 7 | ![Crates.io Version](https://img.shields.io/crates/v/maycoon) 8 | ![License](https://img.shields.io/crates/l/maycoon) 9 | ![docs.rs](https://img.shields.io/docsrs/maycoon) 10 | 11 | **Modern and Innovative UI Framework written in Rust** 12 | 13 |
14 | 15 | ## Features 16 | 17 | - ⚙️ **Made in [Rust](https://www.rust-lang.org)** 18 | - We believe Rust is perfect for performance critical applications and efficient memory management. 19 | - 🛠️ **Cross-platform** 20 | - As a desktop-focused UI framework, Maycoon is compatible with Windows, Linux, macOS and other Unix-like operating 21 | systems. 22 | - *Mobile support planned but not yet implemented* 23 | - *Web support planned but not yet implemented* 24 | - 🎨 **Customizable** 25 | - Maycoon provides a variety of widgets and themes to customize the look and feel of your application with ease. 26 | - 🚀 **Lightning Fast** 27 | - Your application will start up fast and run even faster. 28 | - The UI uses [vello](https://github.com/linebender/vello) for GPU-accelerated rendering and top performance. 29 | - 📦 **Modular** 30 | - Every crate and feature is optional, so you can only enable what you really need and avoid unnecessary 31 | dependencies or features. 32 | - Build widgets using components or using raw vector graphics. 33 | 34 | ## Getting Started 35 | 36 | If you are new to [Rust](https://www.rust-lang.org), we recommend learning the [basics](https://www.rust-lang.org/learn) 37 | first. 38 | 39 | To start using Maycoon, you can follow 40 | the official [Installation Guide](https://maycoon-ui.github.io/guide/qick-start/installation.html). 41 | 42 | For a little tutorial, see the [Counter Example](https://maycoon-ui.github.io/guide/qick-start/basic-app.html). 43 | 44 | ## License 45 | 46 | This project is dual licensed under the [MIT license](LICENSE-MIT) and the [Apache License 2.0](LICENSE-APACHE). 47 | 48 | Any contributions are, unless otherwise stated, licensed under the same terms. 49 | 50 | **NOTE:** The [Noto Sans](https://fonts.google.com/noto/specimen/Noto+Sans) font, that is embedded by default with the 51 | `include-noto-sans` feature is licensed under 52 | the [SIL Open Font License 1.1](https://openfontlicense.org/open-font-license-official-text/). 53 | 54 | ## Contributors 55 | 56 | Thanks to everyone who has contributed to Maycoon! 57 | 58 | 59 | 60 | 61 | 62 | 69 | 76 | 77 | 78 |
63 | 64 | DraftedDev 65 |
66 | Mikail Plotzky 67 |
68 |
70 | 71 | waywardmonkeys 72 |
73 | Bruce Mitchener 74 |
75 |
79 | 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Latest (master branch) 6 | 7 | ## Reporting a Vulnerability 8 | 9 | We currently don't have a real security vulnerability reporting technique, but you are welcome 10 | to [open an issue](https://github.com/maycoon-ui/maycoon/issues). 11 | -------------------------------------------------------------------------------- /assets/logo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maycoon-ui/maycoon/355e160d09525d64dacd4d1504b4db68f398ede1/assets/logo-128x128.png -------------------------------------------------------------------------------- /assets/logo-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maycoon-ui/maycoon/355e160d09525d64dacd4d1504b4db68f398ede1/assets/logo-256x256.png -------------------------------------------------------------------------------- /assets/logo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maycoon-ui/maycoon/355e160d09525d64dacd4d1504b4db68f398ede1/assets/logo-512x512.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | type-complexity-threshold = 300 2 | too-many-arguments-threshold = 15 3 | -------------------------------------------------------------------------------- /examples/animation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "animation" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/animation/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::update::Update; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::signal::Signal; 6 | use maycoon::core::widget::Widget; 7 | use maycoon::theme::theme::celeste::CelesteTheme; 8 | use maycoon::widgets::animator::Animator; 9 | use maycoon::widgets::text::Text; 10 | use std::time::Duration; 11 | 12 | struct MyApp; 13 | 14 | impl Application for MyApp { 15 | type Theme = CelesteTheme; 16 | type State = (); 17 | 18 | fn build(context: AppContext, _: Self::State) -> impl Widget { 19 | let font_size = context.use_state(0.0); 20 | 21 | Animator::new( 22 | Duration::from_millis(2000), 23 | Text::new("Hello World!".to_string()).with_font_size(font_size.maybe()), 24 | move |_, f| { 25 | font_size.set(f * 30.0); 26 | 27 | Update::DRAW 28 | }, 29 | ) 30 | } 31 | 32 | fn config(&self) -> MayConfig { 33 | MayConfig::default() 34 | } 35 | } 36 | 37 | fn main() { 38 | MyApp.run(()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/canvas/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "canvas" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../..", features = ["vg", "canvas"] } 9 | -------------------------------------------------------------------------------- /examples/canvas/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::color::color::palette; 2 | use maycoon::color::kurbo::{Affine, Circle, Point, Stroke}; 3 | use maycoon::color::Brush; 4 | use maycoon::core::app::context::AppContext; 5 | use maycoon::core::app::Application; 6 | use maycoon::core::config::MayConfig; 7 | use maycoon::core::widget::Widget; 8 | use maycoon::theme::theme::celeste::CelesteTheme; 9 | use maycoon::widgets::canvas::Canvas; 10 | 11 | struct MyApp; 12 | 13 | impl Application for MyApp { 14 | type Theme = CelesteTheme; 15 | type State = (); 16 | 17 | fn build(_: AppContext, _: Self::State) -> impl Widget { 18 | Canvas::new(|scene, _| { 19 | scene.stroke( 20 | &Stroke::new(10.0), 21 | Affine::default(), 22 | &Brush::Solid(palette::css::GREEN), 23 | None, 24 | &Circle::new(Point::new(100.0, 100.0), 50.0), 25 | ); 26 | }) 27 | } 28 | 29 | fn config(&self) -> MayConfig { 30 | MayConfig::default() 31 | } 32 | } 33 | 34 | fn main() { 35 | MyApp.run(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/checkbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "checkbox" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/checkbox/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::Application; 3 | use maycoon::core::config::MayConfig; 4 | use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; 5 | use maycoon::core::reference::Ref; 6 | use maycoon::core::signal::state::StateSignal; 7 | use maycoon::core::signal::{MaybeSignal, Signal}; 8 | use maycoon::core::widget::{Widget, WidgetLayoutExt}; 9 | use maycoon::math::Vector2; 10 | use maycoon::theme::theme::celeste::CelesteTheme; 11 | use maycoon::widgets::checkbox::Checkbox; 12 | use maycoon::widgets::container::Container; 13 | use maycoon::widgets::text::Text; 14 | 15 | struct MyApp; 16 | 17 | impl Application for MyApp { 18 | type Theme = CelesteTheme; 19 | type State = (); 20 | 21 | fn build(context: AppContext, _: Self::State) -> impl Widget { 22 | let checked = context.use_signal(StateSignal::new(false)); 23 | 24 | Container::new(vec![ 25 | { 26 | let checked = checked.clone(); 27 | 28 | Box::new(Checkbox::new(MaybeSignal::signal(checked))) 29 | }, 30 | { 31 | let checked = checked.clone(); 32 | 33 | Box::new(Text::new(checked.map(|val| Ref::Owned(val.to_string())))) 34 | }, 35 | ]) 36 | .with_layout_style(LayoutStyle { 37 | size: Vector2::::new(Dimension::percent(1.0), Dimension::percent(1.0)), 38 | flex_direction: FlexDirection::Column, 39 | align_items: Some(AlignItems::Center), 40 | ..Default::default() 41 | }) 42 | } 43 | 44 | fn config(&self) -> MayConfig { 45 | MayConfig::default() 46 | } 47 | } 48 | 49 | fn main() { 50 | MyApp.run(()) 51 | } 52 | -------------------------------------------------------------------------------- /examples/component/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "component" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/component/src/counter.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::update::Update; 3 | use maycoon::core::component::{Component, Composed}; 4 | use maycoon::core::layout::LayoutStyle; 5 | use maycoon::core::reference::Ref; 6 | use maycoon::core::signal::eval::EvalSignal; 7 | use maycoon::core::signal::{ArcSignal, MaybeSignal, Signal}; 8 | use maycoon::core::widget::{Widget, WidgetLayoutExt}; 9 | use maycoon::theme::id::WidgetId; 10 | use maycoon::widgets::button::Button; 11 | use maycoon::widgets::container::Container; 12 | use maycoon::widgets::text::Text; 13 | 14 | pub struct Counter { 15 | counter: ArcSignal, 16 | layout: MaybeSignal, 17 | } 18 | 19 | impl Counter { 20 | pub fn new(counter: ArcSignal) -> Composed { 21 | Counter { 22 | counter, 23 | layout: LayoutStyle::default().into(), 24 | } 25 | .compose() 26 | } 27 | } 28 | 29 | impl Component for Counter { 30 | fn build(&self, context: AppContext) -> impl Widget + 'static { 31 | let counter = self.counter.clone(); 32 | 33 | Container::new(vec![ 34 | { 35 | let counter = counter.clone(); 36 | 37 | Box::new( 38 | Button::new(Text::new("Increase".to_string())).with_on_pressed( 39 | EvalSignal::new(move || { 40 | counter.set(*counter.get() + 1); 41 | Update::DRAW 42 | }) 43 | .hook(&context) 44 | .maybe(), 45 | ), 46 | ) 47 | }, 48 | { 49 | let counter = counter.clone(); 50 | 51 | Box::new( 52 | Button::new(Text::new("Decrease".to_string())).with_on_pressed( 53 | EvalSignal::new(move || { 54 | counter.set(*counter.get() - 1); 55 | Update::DRAW 56 | }) 57 | .hook(&context) 58 | .maybe(), 59 | ), 60 | ) 61 | }, 62 | Box::new(Text::new( 63 | MaybeSignal::signal(counter).map(|i| Ref::Owned(i.to_string())), 64 | )), 65 | ]) 66 | .with_layout_style(self.layout.get().clone()) 67 | } 68 | 69 | fn widget_id(&self) -> WidgetId { 70 | WidgetId::new("my-example", "Counter") 71 | } 72 | } 73 | 74 | impl WidgetLayoutExt for Counter { 75 | fn set_layout_style(&mut self, layout_style: impl Into>) { 76 | self.layout = layout_style.into(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/component/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::counter::Counter; 2 | use maycoon::core::app::context::AppContext; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; 6 | use maycoon::core::signal::state::StateSignal; 7 | use maycoon::core::widget::{Widget, WidgetLayoutExt}; 8 | use maycoon::math::Vector2; 9 | use maycoon::theme::theme::celeste::CelesteTheme; 10 | 11 | mod counter; 12 | 13 | struct MyApp; 14 | 15 | impl Application for MyApp { 16 | type Theme = CelesteTheme; 17 | type State = (); 18 | 19 | fn build(context: AppContext, _: Self::State) -> impl Widget { 20 | let counter = context.use_signal(StateSignal::new(0)); 21 | 22 | Counter::new(counter).with_layout_style(LayoutStyle { 23 | size: Vector2::::new(Dimension::percent(1.0), Dimension::percent(1.0)), 24 | flex_direction: FlexDirection::Column, 25 | align_items: Some(AlignItems::Center), 26 | ..Default::default() 27 | }) 28 | } 29 | 30 | fn config(&self) -> MayConfig { 31 | MayConfig::default() 32 | } 33 | } 34 | 35 | fn main() { 36 | MyApp.run(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::update::Update; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; 6 | use maycoon::core::reference::Ref; 7 | use maycoon::core::signal::eval::EvalSignal; 8 | use maycoon::core::signal::state::StateSignal; 9 | use maycoon::core::signal::Signal; 10 | use maycoon::core::widget::{Widget, WidgetLayoutExt}; 11 | use maycoon::math::Vector2; 12 | use maycoon::theme::theme::celeste::CelesteTheme; 13 | use maycoon::widgets::button::Button; 14 | use maycoon::widgets::container::Container; 15 | use maycoon::widgets::text::Text; 16 | 17 | struct MyApp; 18 | 19 | impl Application for MyApp { 20 | type Theme = CelesteTheme; 21 | type State = (); 22 | 23 | fn build(context: AppContext, _: Self::State) -> impl Widget { 24 | let counter = context.use_signal(StateSignal::new(0)); 25 | 26 | Container::new(vec![ 27 | { 28 | let counter = counter.clone(); 29 | 30 | Box::new( 31 | Button::new(Text::new("Increase".to_string())).with_on_pressed( 32 | EvalSignal::new(move || { 33 | counter.set(*counter.get() + 1); 34 | 35 | Update::DRAW 36 | }) 37 | .hook(&context) 38 | .maybe(), 39 | ), 40 | ) 41 | }, 42 | { 43 | let counter = counter.clone(); 44 | 45 | Box::new( 46 | Button::new(Text::new("Decrease".to_string())).with_on_pressed( 47 | EvalSignal::new(move || { 48 | counter.set(*counter.get() - 1); 49 | 50 | Update::DRAW 51 | }) 52 | .hook(&context) 53 | .maybe(), 54 | ), 55 | ) 56 | }, 57 | Box::new(Text::new(counter.map(|i| Ref::Owned(i.to_string())))), 58 | ]) 59 | .with_layout_style(LayoutStyle { 60 | size: Vector2::::new(Dimension::percent(1.0), Dimension::percent(1.0)), 61 | flex_direction: FlexDirection::Column, 62 | align_items: Some(AlignItems::Center), 63 | ..Default::default() 64 | }) 65 | } 66 | 67 | fn config(&self) -> MayConfig { 68 | MayConfig::default() 69 | } 70 | } 71 | 72 | fn main() { 73 | MyApp.run(()) 74 | } 75 | -------------------------------------------------------------------------------- /examples/fetcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fetcher" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | surf = "2.3.2" 10 | serde = { version = "1", features = ["derive"] } 11 | -------------------------------------------------------------------------------- /examples/fetcher/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::update::Update; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::{MayConfig, TasksConfig}; 5 | use maycoon::core::widget::Widget; 6 | use maycoon::theme::theme::celeste::CelesteTheme; 7 | use maycoon::widgets::fetcher::WidgetFetcher; 8 | use maycoon::widgets::text::Text; 9 | use serde::Deserialize; 10 | 11 | struct MyApp; 12 | 13 | impl Application for MyApp { 14 | type Theme = CelesteTheme; 15 | type State = (); 16 | 17 | fn build(_: AppContext, _: Self::State) -> impl Widget { 18 | WidgetFetcher::new(get_random_quote(), Update::DRAW, |data| { 19 | if let Some(data) = data { 20 | Text::new(format!(" \"{}\" \n - {}", data.quote, data.author)) 21 | } else { 22 | Text::new(" Loading Quote...".to_string()) 23 | } 24 | }) 25 | } 26 | 27 | fn config(&self) -> MayConfig { 28 | MayConfig { 29 | tasks: Some(TasksConfig::default()), 30 | ..Default::default() 31 | } 32 | } 33 | } 34 | 35 | fn main() { 36 | MyApp.run(()) 37 | } 38 | 39 | #[derive(Deserialize)] 40 | struct Quote { 41 | quote: String, 42 | author: String, 43 | } 44 | 45 | async fn get_random_quote() -> Quote { 46 | surf::get("https://dummyjson.com/quotes/random") 47 | .await 48 | .expect("Failed to fetch quote") 49 | .body_json::() 50 | .await 51 | .expect("Failed to parse quote") 52 | } 53 | -------------------------------------------------------------------------------- /examples/gesture_detector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gesture_detector" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/gesture_detector/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::update::Update; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::signal::eval::EvalSignal; 6 | use maycoon::core::signal::Signal; 7 | use maycoon::core::widget::Widget; 8 | use maycoon::theme::theme::celeste::CelesteTheme; 9 | use maycoon::widgets::gesture_detector::GestureDetector; 10 | use maycoon::widgets::text::Text; 11 | 12 | struct MyApp; 13 | 14 | impl Application for MyApp { 15 | type Theme = CelesteTheme; 16 | type State = (); 17 | 18 | fn build(context: AppContext, _: Self::State) -> impl Widget { 19 | GestureDetector::new(Text::new("Gesture Detector".to_string())) 20 | .with_on_hover( 21 | EvalSignal::new(move || { 22 | println!("Hovered"); 23 | Update::DRAW 24 | }) 25 | .hook(&context) 26 | .maybe(), 27 | ) 28 | .with_on_release( 29 | EvalSignal::new(move || { 30 | println!("Release"); 31 | Update::DRAW 32 | }) 33 | .hook(&context) 34 | .maybe(), 35 | ) 36 | .with_on_press( 37 | EvalSignal::new(move || { 38 | println!("Press"); 39 | Update::DRAW 40 | }) 41 | .hook(&context) 42 | .maybe(), 43 | ) 44 | } 45 | 46 | fn config(&self) -> MayConfig { 47 | MayConfig::default() 48 | } 49 | } 50 | 51 | fn main() { 52 | MyApp.run(()) 53 | } 54 | -------------------------------------------------------------------------------- /examples/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/hello_world/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::Application; 3 | use maycoon::core::config::MayConfig; 4 | use maycoon::core::widget::Widget; 5 | use maycoon::theme::theme::celeste::CelesteTheme; 6 | use maycoon::widgets::text::Text; 7 | 8 | struct MyApp; 9 | 10 | impl Application for MyApp { 11 | type Theme = CelesteTheme; 12 | type State = (); 13 | 14 | fn build(_: AppContext, _: Self::State) -> impl Widget { 15 | Text::new("Hello World".to_string()) 16 | } 17 | 18 | fn config(&self) -> MayConfig { 19 | MayConfig::default() 20 | } 21 | } 22 | 23 | fn main() { 24 | MyApp.run(()) 25 | } 26 | -------------------------------------------------------------------------------- /examples/icon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "icon" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/icon/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::Application; 3 | use maycoon::core::config::MayConfig; 4 | use maycoon::core::widget::Widget; 5 | use maycoon::macros::svg_icon; 6 | use maycoon::theme::theme::celeste::CelesteTheme; 7 | use maycoon::widgets::icon::svg::SvgIcon; 8 | use maycoon::widgets::icon::Icon; 9 | 10 | struct MyApp; 11 | 12 | impl Application for MyApp { 13 | type Theme = CelesteTheme; 14 | type State = (); 15 | 16 | fn build(_: AppContext, _: Self::State) -> impl Widget { 17 | let icon: SvgIcon = svg_icon!("./assets/logo.svg"); 18 | 19 | Icon::new(icon) 20 | } 21 | 22 | fn config(&self) -> MayConfig { 23 | MayConfig::default() 24 | } 25 | } 26 | 27 | fn main() { 28 | MyApp.run(()) 29 | } 30 | -------------------------------------------------------------------------------- /examples/image/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "image" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | image = "0.25" 9 | maycoon = { path = "../.." } 10 | -------------------------------------------------------------------------------- /examples/image/pelican.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maycoon-ui/maycoon/355e160d09525d64dacd4d1504b4db68f398ede1/examples/image/pelican.jpg -------------------------------------------------------------------------------- /examples/image/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::color::{Blob, ImageFormat}; 2 | use maycoon::core::app::context::AppContext; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::signal::fixed::FixedSignal; 6 | use maycoon::core::signal::Signal; 7 | use maycoon::core::widget::Widget; 8 | use maycoon::theme::theme::celeste::CelesteTheme; 9 | use maycoon::widgets::image::{Image, ImageData}; 10 | 11 | const IMAGE_DATA: &[u8] = include_bytes!("../pelican.jpg"); 12 | 13 | struct MyApp; 14 | 15 | impl Application for MyApp { 16 | type Theme = CelesteTheme; 17 | type State = (); 18 | 19 | fn build(context: AppContext, _: Self::State) -> impl Widget { 20 | let image = FixedSignal::new(ImageData::new( 21 | Blob::from( 22 | image::load_from_memory(IMAGE_DATA) 23 | .unwrap() 24 | .into_rgba8() 25 | .to_vec(), 26 | ), 27 | ImageFormat::Rgba8, 28 | 427, 29 | 640, 30 | )) 31 | .hook(&context); 32 | 33 | Image::new(image.maybe()) 34 | } 35 | 36 | fn config(&self) -> MayConfig { 37 | MayConfig::default() 38 | } 39 | } 40 | 41 | fn main() { 42 | MyApp.run(()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/plugin/src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::plugin::MyPlugin; 2 | use maycoon::core::app::context::AppContext; 3 | use maycoon::core::app::Application; 4 | use maycoon::core::config::MayConfig; 5 | use maycoon::core::plugin::PluginManager; 6 | use maycoon::core::widget::Widget; 7 | use maycoon::theme::theme::celeste::CelesteTheme; 8 | use maycoon::widgets::text::Text; 9 | 10 | pub mod plugin; 11 | 12 | struct MyApp; 13 | 14 | impl Application for MyApp { 15 | type Theme = CelesteTheme; 16 | type State = (); 17 | 18 | fn build(_: AppContext, _: Self::State) -> impl Widget { 19 | Text::new("Drop a file!".to_string()) 20 | } 21 | 22 | fn config(&self) -> MayConfig { 23 | MayConfig::default() 24 | } 25 | 26 | fn plugins(&self) -> PluginManager { 27 | let mut plugins = PluginManager::new(); 28 | 29 | plugins.register(MyPlugin); 30 | 31 | plugins 32 | } 33 | } 34 | 35 | fn main() { 36 | MyApp.run(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/plugin/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::info::AppInfo; 2 | use maycoon::core::app::update::UpdateManager; 3 | use maycoon::core::config::MayConfig; 4 | use maycoon::core::layout::{NodeId, TaffyTree}; 5 | use maycoon::core::plugin::{Plugin, PluginManager}; 6 | use maycoon::core::vg::util::{RenderContext, RenderSurface}; 7 | use maycoon::core::vg::{Renderer, Scene}; 8 | use maycoon::core::window::{ActiveEventLoop, Window, WindowEvent}; 9 | use maycoon::theme::theme::Theme; 10 | use std::sync::Arc; 11 | use std::time::Instant; 12 | 13 | pub struct MyPlugin; 14 | 15 | impl Plugin for MyPlugin { 16 | fn name(&self) -> &'static str { 17 | "my_plugin" 18 | } 19 | 20 | fn on_register(&mut self, _manager: &mut PluginManager) { 21 | println!("Hello World!"); 22 | } 23 | 24 | fn on_unregister(&mut self, _manager: &mut PluginManager) { 25 | println!("Bye World!"); 26 | } 27 | 28 | fn on_window_event( 29 | &mut self, 30 | event: &mut WindowEvent, 31 | _config: &mut MayConfig, 32 | _window: &Arc, 33 | _renderer: &mut Renderer, 34 | _scene: &mut Scene, 35 | _surface: &mut RenderSurface<'_>, 36 | _taffy: &mut TaffyTree, 37 | _window_node: NodeId, 38 | _info: &mut AppInfo, 39 | _render_ctx: &mut RenderContext, 40 | _update: &UpdateManager, 41 | _last_update: &mut Instant, 42 | _event_loop: &ActiveEventLoop, 43 | ) { 44 | if let WindowEvent::DroppedFile(path) = event { 45 | println!("Dropped file: {}", path.to_string_lossy()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/slider/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slider" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | maycoon = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/slider/src/main.rs: -------------------------------------------------------------------------------- 1 | use maycoon::core::app::context::AppContext; 2 | use maycoon::core::app::Application; 3 | use maycoon::core::config::MayConfig; 4 | use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; 5 | use maycoon::core::reference::Ref; 6 | use maycoon::core::signal::state::StateSignal; 7 | use maycoon::core::signal::Signal; 8 | use maycoon::core::widget::{Widget, WidgetLayoutExt}; 9 | use maycoon::math::Vector2; 10 | use maycoon::theme::theme::celeste::CelesteTheme; 11 | use maycoon::widgets::container::Container; 12 | use maycoon::widgets::slider::Slider; 13 | use maycoon::widgets::text::Text; 14 | 15 | struct MyApp; 16 | 17 | impl Application for MyApp { 18 | type Theme = CelesteTheme; 19 | type State = (); 20 | 21 | fn build(context: AppContext, _: Self::State) -> impl Widget { 22 | let value = context.use_signal(StateSignal::new(0.0f32)); 23 | 24 | Container::new(vec![ 25 | Box::new(Slider::new(value.maybe())), 26 | Box::new(Text::new(value.map(|i| Ref::Owned(i.to_string())))), 27 | ]) 28 | .with_layout_style(LayoutStyle { 29 | size: Vector2::::new(Dimension::percent(1.0), Dimension::percent(1.0)), 30 | flex_direction: FlexDirection::Column, 31 | align_items: Some(AlignItems::Center), 32 | ..Default::default() 33 | }) 34 | } 35 | 36 | fn config(&self) -> MayConfig { 37 | MayConfig::default() 38 | } 39 | } 40 | 41 | fn main() { 42 | MyApp.run(()) 43 | } 44 | -------------------------------------------------------------------------------- /maycoon-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-core-v0.3.2...maycoon-core-v0.4.0) - 2025-04-29 11 | 12 | ### Added 13 | 14 | - Global State Management 15 | 16 | ### Fixed 17 | 18 | - Default Font Selection 19 | 20 | ### Other 21 | 22 | - Remove parking_lot dependency 23 | - Update taffy to 0.8.1 24 | 25 | ## [0.3.1](https://github.com/maycoon-ui/maycoon/compare/maycoon-core-v0.3.0...maycoon-core-v0.3.1) - 2025-04-19 26 | 27 | ### Other 28 | 29 | - Temporarily fix font issues 30 | - Fix cargo asset packaging 31 | 32 | ## [0.3.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-core-v0.1.0...maycoon-core-v0.3.0) - 2025-01-26 33 | 34 | ### Other 35 | 36 | - Fix typo 37 | - Fix clippy lints 38 | - Fix `clippy::doc_markdown` lints 39 | - Fix updating vello 40 | - Update taffy and winit 41 | - Implement component architecture 42 | - Add size info 43 | - Add Task Runner 44 | - Make self in widget_id immutable 45 | - Add init_threads config parameter 46 | - Update dependencies 47 | - Merge pull request [#28](https://github.com/maycoon-ui/maycoon/pull/28) from waywardmonkeys/update-to-vello-0.3 48 | - Add way to load system fonts 49 | - Replace dashmap with indexmap 50 | 51 | ## [0.1.0](https://github.com/maycoon-ui/maycoon/releases/tag/maycoon-core-v0.1.0) - 2024-10-04 52 | 53 | ### Other 54 | 55 | - Update config.rs 56 | - Fix non-windows compat 57 | - Add workspace keys 58 | - Add EmptyState 59 | - Rename crates and rework state value 60 | -------------------------------------------------------------------------------- /maycoon-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maycoon-core" 3 | description = "Core Functionality for Maycoon UI => See the `maycoon` crate for more." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | categories.workspace = true 9 | keywords.workspace = true 10 | repository.workspace = true 11 | homepage.workspace = true 12 | 13 | [dependencies] 14 | winit = "0.30.9" 15 | wgpu-types = "23.0.0" 16 | vello = "0.4.1" 17 | taffy = "0.8.1" 18 | bitflags = "2.9.0" 19 | font-kit = "0.14.2" 20 | futures = { version = "0.3.31", features = ["thread-pool"] } 21 | skrifa = { version = "0.30.0", optional = true } 22 | arc-swap = "1.7.1" 23 | parking_lot = "0.12.3" 24 | maycoon-theme = { workspace = true } 25 | nalgebra = { workspace = true } 26 | indexmap = { workspace = true } 27 | peniko = { workspace = true } 28 | log = { workspace = true } 29 | 30 | [features] 31 | default = [] 32 | include-noto-sans = [] 33 | vg = ["skrifa"] 34 | -------------------------------------------------------------------------------- /maycoon-core/README.md: -------------------------------------------------------------------------------- 1 | # Maycoon Core Library 2 | 3 | This library contains core structures and functionality for the `maycoon` crate. 4 | 5 | See the [official Website](https://maycoon-ui.github.io) for more information about Maycoon. 6 | -------------------------------------------------------------------------------- /maycoon-core/src/NotoSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maycoon-ui/maycoon/355e160d09525d64dacd4d1504b4db68f398ede1/maycoon-core/src/NotoSans.ttf -------------------------------------------------------------------------------- /maycoon-core/src/app/context.rs: -------------------------------------------------------------------------------- 1 | use crate::app::diagnostics::Diagnostics; 2 | use crate::app::update::{Update, UpdateManager}; 3 | use crate::signal::actor::ActorSignal; 4 | use crate::signal::eval::EvalSignal; 5 | use crate::signal::fixed::FixedSignal; 6 | use crate::signal::memoized::MemoizedSignal; 7 | use crate::signal::rw::RwSignal; 8 | use crate::signal::state::StateSignal; 9 | use crate::signal::Signal; 10 | use std::sync::Arc; 11 | use vello::util::RenderContext; 12 | 13 | /// The application context for managing the application lifecycle. 14 | #[derive(Clone)] 15 | pub struct AppContext { 16 | update: UpdateManager, 17 | diagnostics: Diagnostics, 18 | render: Arc, 19 | } 20 | 21 | impl AppContext { 22 | /// Create a new application context using the given [UpdateManager]. 23 | pub fn new( 24 | update: UpdateManager, 25 | diagnostics: Diagnostics, 26 | render: Arc, 27 | ) -> Self { 28 | Self { 29 | update, 30 | diagnostics, 31 | render, 32 | } 33 | } 34 | 35 | /// Get the [Diagnostics] of the application. 36 | pub fn diagnostics(&self) -> Diagnostics { 37 | self.diagnostics.clone() 38 | } 39 | 40 | /// Get the [RenderContext] of the application. 41 | pub fn render_ctx(&self) -> Arc { 42 | self.render.clone() 43 | } 44 | 45 | /// Get the [UpdateManager] of the application. 46 | pub fn update(&self) -> UpdateManager { 47 | self.update.clone() 48 | } 49 | 50 | /// Hook the given [Signal] to the [UpdateManager] of this application. 51 | /// 52 | /// This makes the signal reactive, so it will notify the renderer when the inner value changes. 53 | pub fn hook_signal>(&self, signal: &mut S) { 54 | let update = self.update(); 55 | 56 | signal.listen(Box::new(move |_| { 57 | update.insert(Update::EVAL); 58 | })); 59 | } 60 | 61 | /// Hook the given [Signal] to the [UpdateManager] of this application and return it inside an [Arc]. 62 | /// 63 | /// See [AppContext::hook_signal] for more. 64 | pub fn use_signal>(&self, mut signal: S) -> Arc { 65 | self.hook_signal(&mut signal); 66 | 67 | Arc::new(signal) 68 | } 69 | 70 | /// Shortcut for creating and hooking a [StateSignal] into the application lifecycle. 71 | pub fn use_state(&self, value: T) -> Arc> { 72 | self.use_signal(StateSignal::new(value)) 73 | } 74 | 75 | /// Shortcut for creating and hooking a [MemoizedSignal] into the application lifecycle. 76 | pub fn use_memoized( 77 | &self, 78 | value: impl Fn() -> T + 'static, 79 | ) -> Arc> { 80 | self.use_signal(MemoizedSignal::new(value)) 81 | } 82 | 83 | /// Shortcut for creating and hooking a [FixedSignal] into the application lifecycle. 84 | pub fn use_fixed(&self, value: T) -> Arc> { 85 | self.use_signal(FixedSignal::new(value)) 86 | } 87 | 88 | /// Shortcut for creating and hooking an [EvalSignal] into the application lifecycle. 89 | pub fn use_eval(&self, eval: impl Fn() -> T + 'static) -> Arc> { 90 | self.use_signal(EvalSignal::new(eval)) 91 | } 92 | 93 | /// Shortcut for creating and hooking a [RwSignal] into the application lifecycle. 94 | pub fn use_rw(&self, value: T) -> Arc> { 95 | self.use_signal(RwSignal::new(value)) 96 | } 97 | 98 | /// Shortcut for creating and hooking an [ActorSignal] into the application lifecycle. 99 | pub fn use_actor(&self, value: T) -> Arc> { 100 | self.use_signal(ActorSignal::new(value)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /maycoon-core/src/app/diagnostics.rs: -------------------------------------------------------------------------------- 1 | /// Contains diagnostics data for the application. 2 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] 3 | pub struct Diagnostics { 4 | /// The updates since the last second. Use `updates_per_sec` for the average updates per second. 5 | pub updates: usize, 6 | /// The average updates per second. 7 | pub updates_per_sec: usize, 8 | } 9 | -------------------------------------------------------------------------------- /maycoon-core/src/app/font_ctx.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use peniko::{Blob, Font}; 3 | use std::sync::Arc; 4 | 5 | /// A font manager for maycoon applications. 6 | /// 7 | /// Can be used to load and access in-memory fonts or by system source. 8 | /// 9 | /// If the default `include-noto-sans` feature is enabled, the default font is set to [Noto Sans](https://fonts.google.com/specimen/Noto+Sans). 10 | #[derive(Clone, Debug)] 11 | pub struct FontContext { 12 | default: String, 13 | fonts: IndexMap, 14 | } 15 | 16 | impl FontContext { 17 | /// Create a new font context with the given default font name. 18 | /// 19 | /// Make sure to load the default font via [FontContext::load], 20 | /// before passing this context to the application runner. 21 | pub fn new(default: String) -> Self { 22 | Self { 23 | default, 24 | fonts: IndexMap::new(), 25 | } 26 | } 27 | 28 | /// Loads a font with a custom name into the font context. 29 | /// 30 | /// If the font with the same name already exists, it will be overwritten and the old font will be returned. 31 | pub fn load(&mut self, name: impl ToString, font: Font) -> Option { 32 | self.fonts.insert(name.to_string(), font) 33 | } 34 | 35 | /// Loads a system font into the font context. 36 | /// The provided name must match the postscript name of the font. 37 | /// 38 | /// If a font with the same name is already loaded, it will be overwritten and the old font will be returned. 39 | /// 40 | /// Returns `None` if the font could not be loaded. 41 | /// 42 | /// **NOTE:** Not every postscript font is available on every system. 43 | pub fn load_system( 44 | &mut self, 45 | name: impl ToString, 46 | postscript_name: impl ToString, 47 | ) -> Option<()> { 48 | log::debug!("Loading system font: {}", postscript_name.to_string()); 49 | 50 | let font = font_kit::source::SystemSource::new() 51 | .select_by_postscript_name(postscript_name.to_string().as_str()) 52 | .ok()? 53 | .load() 54 | .ok()? 55 | .copy_font_data()?; 56 | 57 | self.load(name, Font::new(Blob::new(font), 0)); 58 | 59 | Some(()) 60 | } 61 | 62 | /// Set the default font. 63 | /// 64 | /// **NOTE:** The font must be loaded before usage with [FontContext::load]. 65 | pub fn set_default_font(&mut self, name: impl ToString) { 66 | self.default = name.to_string(); 67 | } 68 | 69 | /// Get a font by a specified name. Returns [None] if the font could not be found. 70 | pub fn get(&self, name: impl ToString) -> Option { 71 | self.fonts.get(&name.to_string()).cloned() 72 | } 73 | 74 | /// Removes a font by the given name and returns it or [None] if the font could not be found. 75 | pub fn remove(&mut self, name: impl ToString) -> Option { 76 | self.fonts.swap_remove(&name.to_string()) 77 | } 78 | 79 | /// Returns the default font. [Roboto](https://fonts.google.com/specimen/Roboto) by default. 80 | pub fn default_font(&self) -> &Font { 81 | self.fonts 82 | .get(&self.default) 83 | .expect("Default font not found. Please load one via `FontContext::load`.") 84 | } 85 | } 86 | 87 | #[cfg(feature = "include-noto-sans")] 88 | impl Default for FontContext { 89 | fn default() -> Self { 90 | let mut ctx = FontContext::new("Noto Sans".to_string()); 91 | 92 | ctx.load( 93 | "Noto Sans", 94 | Font::new(Blob::new(Arc::new(crate::DEFAULT_FONT)), 0), 95 | ); 96 | 97 | ctx 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /maycoon-core/src/app/info.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Vector2; 2 | use winit::event::{DeviceId, ElementState, KeyEvent, MouseButton, MouseScrollDelta}; 3 | 4 | use crate::app::diagnostics::Diagnostics; 5 | use crate::app::font_ctx::FontContext; 6 | 7 | /// The application information container. 8 | pub struct AppInfo { 9 | /// The position of the cursor. If [None], the cursor left the window. 10 | pub cursor_pos: Option>, 11 | /// The fired key events. 12 | pub keys: Vec<(DeviceId, KeyEvent)>, 13 | /// The fired mouse button events. 14 | pub buttons: Vec<(DeviceId, MouseButton, ElementState)>, 15 | /// The mouse scroll delta, if a [winit::event::WindowEvent::MouseWheel] event was fired. 16 | pub mouse_scroll_delta: Option, 17 | /// App Diagnostics. 18 | pub diagnostics: Diagnostics, 19 | /// The current font context. 20 | pub font_context: FontContext, 21 | /// The size of the window. 22 | pub size: Vector2, 23 | } 24 | 25 | impl AppInfo { 26 | /// Reset the application information for a new frame. 27 | pub fn reset(&mut self) { 28 | self.buttons.clear(); 29 | self.keys.clear(); 30 | self.mouse_scroll_delta = None; 31 | } 32 | } 33 | 34 | impl Default for AppInfo { 35 | fn default() -> Self { 36 | Self { 37 | cursor_pos: None, 38 | keys: Vec::with_capacity(4), 39 | buttons: Vec::with_capacity(2), 40 | mouse_scroll_delta: None, 41 | diagnostics: Diagnostics::default(), 42 | font_context: FontContext::default(), 43 | size: Vector2::new(0.0, 0.0), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /maycoon-core/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::context::AppContext; 2 | use crate::app::runner::MayRunner; 3 | use crate::config::MayConfig; 4 | use crate::plugin::PluginManager; 5 | use crate::widget::Widget; 6 | use maycoon_theme::theme::Theme; 7 | 8 | /// Contains diagnostics data for the application. 9 | pub mod diagnostics; 10 | 11 | /// Contains the font context structure. 12 | pub mod font_ctx; 13 | 14 | /// Contains the application handler. 15 | pub mod handler; 16 | 17 | /// Contains the application information structure. 18 | pub mod info; 19 | 20 | /// Contains the update mode bitflag. 21 | pub mod update; 22 | 23 | /// Contains the [AppContext] structure for access to the application lifecycle. 24 | pub mod context; 25 | 26 | /// Contains the [MayRunner] structure to create and run an application using `winit`. 27 | pub mod runner; 28 | 29 | /// The main application interface. 30 | /// 31 | /// Contains basic functions for the [MayRunner] to create and run an application. 32 | pub trait Application: Sized { 33 | /// The theme of the application and its widgets. 34 | /// 35 | /// See [maycoon_theme::theme] for built-in themes. 36 | type Theme: Theme; 37 | 38 | /// The global state of the application. 39 | type State; 40 | 41 | /// Renders/builds the application's widgets. 42 | /// 43 | /// This function will be passed to the [MayRunner] to create and run the application. 44 | fn build(context: AppContext, state: Self::State) -> impl Widget; 45 | 46 | /// Returns the [MayConfig] for the application. 47 | fn config(&self) -> MayConfig; 48 | 49 | /// Builds and returns the [PluginManager] for the application. 50 | fn plugins(&self) -> PluginManager { 51 | PluginManager::new() 52 | } 53 | 54 | /// Runs the application using the [MayRunner]. 55 | /// 56 | /// Override this method if you want to use a custom event loop. 57 | fn run(self, state: Self::State) { 58 | MayRunner::::new(self.config()).run(state, Self::build, self.plugins()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /maycoon-core/src/app/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::app::context::AppContext; 2 | use crate::app::font_ctx::FontContext; 3 | use crate::app::handler::AppHandler; 4 | use crate::app::update::UpdateManager; 5 | use crate::config::MayConfig; 6 | use crate::plugin::PluginManager; 7 | use crate::widget::Widget; 8 | use maycoon_theme::theme::Theme; 9 | use peniko::Font; 10 | use winit::dpi::{LogicalPosition, LogicalSize, Position, Size}; 11 | use winit::event_loop::EventLoopBuilder; 12 | use winit::window::WindowAttributes; 13 | 14 | /// The core Application structure. 15 | pub struct MayRunner { 16 | config: MayConfig, 17 | font_ctx: FontContext, 18 | } 19 | 20 | impl MayRunner { 21 | /// Create a new App with the given [MayConfig]. 22 | pub fn new(config: MayConfig) -> Self { 23 | // init task runner 24 | if let Some(config) = &config.tasks { 25 | log::info!("Initializing task runner."); 26 | 27 | crate::tasks::runner::TaskRunner::new(config.stack_size, config.workers) 28 | .expect("Failed to create task runner") 29 | .init() 30 | .expect("Failed to init task runner"); 31 | } 32 | 33 | Self { 34 | config, 35 | font_ctx: FontContext::default(), 36 | } 37 | } 38 | 39 | /// Loads a new font into the font context. 40 | /// 41 | /// See [FontContext::load] for more. 42 | pub fn with_font(mut self, name: impl ToString, font: Font) -> Self { 43 | self.font_ctx.load(name, font); 44 | self 45 | } 46 | 47 | /// Loads a new system font into the font context. 48 | /// 49 | /// See [FontContext::load_system] for more. 50 | pub fn with_system_font(mut self, name: impl ToString, postscript_name: impl ToString) -> Self { 51 | self.font_ctx.load_system(name, postscript_name); 52 | self 53 | } 54 | 55 | /// Set the font context. Can be used to configure fonts. 56 | pub fn with_font_context(mut self, font_ctx: FontContext) -> Self { 57 | self.font_ctx = font_ctx; 58 | self 59 | } 60 | 61 | /// Run the application with given widget and state. 62 | pub fn run(mut self, state: S, builder: F, mut plugins: PluginManager) 63 | where 64 | W: Widget, 65 | F: Fn(AppContext, S) -> W, 66 | { 67 | let mut event_loop = EventLoopBuilder::default() 68 | .build() 69 | .expect("Failed to create event loop"); 70 | 71 | let mut attrs = WindowAttributes::default() 72 | .with_inner_size(LogicalSize::new( 73 | self.config.window.size.x, 74 | self.config.window.size.y, 75 | )) 76 | .with_resizable(self.config.window.resizable) 77 | .with_enabled_buttons(self.config.window.buttons) 78 | .with_title(self.config.window.title.clone()) 79 | .with_maximized(self.config.window.maximized) 80 | .with_visible(self.config.window.visible) 81 | .with_transparent(self.config.window.transparent) 82 | .with_blur(self.config.window.blur) 83 | .with_decorations(self.config.window.decorations) 84 | .with_window_icon(self.config.window.icon.clone()) 85 | .with_content_protected(self.config.window.content_protected) 86 | .with_window_level(self.config.window.level) 87 | .with_active(self.config.window.active) 88 | .with_cursor(self.config.window.cursor.clone()); 89 | 90 | // since `with_max_inner_size()` doesn't support `Option` values, we need to manually set it 91 | attrs.max_inner_size = self 92 | .config 93 | .window 94 | .max_size 95 | .map(|v| Size::Logical(LogicalSize::new(v.x, v.y))); 96 | 97 | // since `with_min_inner_size()` doesn't support `Option` values, we need to manually set it 98 | attrs.min_inner_size = self 99 | .config 100 | .window 101 | .min_size 102 | .map(|v| Size::Logical(LogicalSize::new(v.x, v.y))); 103 | 104 | // since `with_position()` doesn't support `Option` values, we need to manually set it 105 | attrs.position = self 106 | .config 107 | .window 108 | .position 109 | .map(|v| Position::Logical(LogicalPosition::new(v.x, v.y))); 110 | 111 | // since `with_resize_increments()` doesn't support `Option` values, we need to manually set it 112 | attrs.resize_increments = self 113 | .config 114 | .window 115 | .resize_increments 116 | .map(|v| Size::Logical(LogicalSize::new(v.x, v.y))); 117 | 118 | log::info!("Launching Application..."); 119 | 120 | let update = UpdateManager::new(); 121 | 122 | plugins.run(|pl| pl.init(&mut event_loop, &update, &mut attrs, &mut self.config)); 123 | 124 | event_loop 125 | .run_app(&mut AppHandler::new( 126 | attrs, 127 | self.config, 128 | builder, 129 | state, 130 | self.font_ctx, 131 | update, 132 | plugins, 133 | )) 134 | .expect("Failed to run event loop"); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /maycoon-core/src/app/update.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | use std::sync::atomic::{AtomicU8, Ordering}; 3 | use std::sync::Arc; 4 | 5 | bitflags! { 6 | /// Update bitflags to define which part of the App should Update. 7 | /// 8 | /// Possible values: 9 | /// - **EVAL** - Re-evaluate the widget tree. 10 | /// - **DRAW** - Re-draw the widget tree. 11 | /// - **LAYOUT** - Re-layout the widget tree. 12 | /// - **FORCE** - Force the App to re-evaluate, re-draw and re-layout the widget tree. 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 14 | pub struct Update: u8 { 15 | /// Re-evaluate the widget tree. 16 | const EVAL = 0b00000001; 17 | /// Re-draw the widget tree. 18 | const DRAW = 0b00000010; 19 | /// Re-layout the widget tree. 20 | const LAYOUT = 0b00000100; 21 | /// Force the App to re-evaluate, re-draw and re-layout the widget tree. 22 | const FORCE = 0b00001000; 23 | } 24 | } 25 | 26 | /// Manages updates for the application lifecycle. 27 | /// 28 | /// It's using atomic operations to ensure lockless thread-safety. 29 | #[derive(Clone, Debug)] 30 | pub struct UpdateManager { 31 | update: Arc, 32 | } 33 | 34 | impl UpdateManager { 35 | /// Creates a new `UpdateManager`. 36 | pub fn new() -> Self { 37 | Self { 38 | update: Arc::new(AtomicU8::new(0)), 39 | } 40 | } 41 | 42 | /// Inserts the given `Update` into the `UpdateManager` using bitwise OR. 43 | pub fn insert(&self, update: Update) { 44 | self.update.fetch_or(update.bits(), Ordering::AcqRel); 45 | } 46 | 47 | /// Removes the given `Update` from the `UpdateManager` using bitwise AND. 48 | pub fn remove(&self, update: Update) { 49 | self.update.fetch_and(!update.bits(), Ordering::AcqRel); 50 | } 51 | 52 | /// Returns the current `Update` of the `UpdateManager`. 53 | pub fn get(&self) -> Update { 54 | Update::from_bits(self.update.load(Ordering::Acquire)) 55 | .expect("failed to decode update bits") 56 | } 57 | 58 | /// Sets the current `Update` of the `UpdateManager`. 59 | pub fn set(&self, update: Update) { 60 | self.update.store(update.bits(), Ordering::Release); 61 | } 62 | 63 | /// Clears the current `Update` flags of the `UpdateManager`. 64 | pub fn clear(&self) { 65 | self.update.store(0, Ordering::Release); 66 | } 67 | } 68 | 69 | impl Default for UpdateManager { 70 | fn default() -> Self { 71 | Self::new() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /maycoon-core/src/component.rs: -------------------------------------------------------------------------------- 1 | use crate::app::context::AppContext; 2 | use crate::app::info::AppInfo; 3 | use crate::app::update::Update; 4 | use crate::layout::{LayoutNode, LayoutStyle, StyleNode}; 5 | use crate::signal::MaybeSignal; 6 | use crate::widget::{BoxedWidget, Widget, WidgetChildExt, WidgetChildrenExt, WidgetLayoutExt}; 7 | use maycoon_theme::id::WidgetId; 8 | use maycoon_theme::theme::Theme; 9 | use std::fmt::Debug; 10 | use std::ops::{Deref, DerefMut}; 11 | use vello::Scene; 12 | 13 | /// A trait for creating a [Widget] from simple functions. 14 | /// 15 | /// Simplifies the creation of your own widgets. 16 | pub trait Component { 17 | /// Builds the inner widget. 18 | fn build(&self, context: AppContext) -> impl Widget + 'static; 19 | 20 | /// The id of this widget/component. 21 | fn widget_id(&self) -> WidgetId; 22 | 23 | /// Composes this component into a [Widget] using [ComposedWidget]. 24 | fn compose(self) -> Composed 25 | where 26 | Self: Sized, 27 | { 28 | Composed { 29 | component: self, 30 | widget: None, 31 | } 32 | } 33 | } 34 | 35 | /// A [Widget] created from a [Component]. 36 | pub struct Composed { 37 | component: C, 38 | widget: Option, 39 | } 40 | 41 | impl Composed { 42 | /// Creates a new [Composed] widget from a [Component]. 43 | pub fn new(component: C) -> Self { 44 | Self { 45 | component, 46 | widget: None, 47 | } 48 | } 49 | } 50 | 51 | impl Widget for Composed { 52 | fn render( 53 | &mut self, 54 | scene: &mut Scene, 55 | theme: &mut dyn Theme, 56 | layout_node: &LayoutNode, 57 | info: &AppInfo, 58 | context: AppContext, 59 | ) { 60 | if let Some(widget) = &mut self.widget { 61 | widget.render(scene, theme, layout_node, info, context) 62 | } else { 63 | self.widget = Some(Box::new(self.component.build(context.clone()))); 64 | } 65 | } 66 | 67 | fn layout_style(&self) -> StyleNode { 68 | if let Some(widget) = &self.widget { 69 | widget.layout_style() 70 | } else { 71 | StyleNode { 72 | style: LayoutStyle::default(), 73 | children: Vec::new(), 74 | } 75 | } 76 | } 77 | 78 | fn update(&mut self, layout: &LayoutNode, context: AppContext, info: &AppInfo) -> Update { 79 | if let Some(widget) = &mut self.widget { 80 | widget.update(layout, context, info) 81 | } else { 82 | self.widget = Some(Box::new(self.component.build(context.clone()))); 83 | Update::FORCE 84 | } 85 | } 86 | 87 | fn widget_id(&self) -> WidgetId { 88 | self.component.widget_id() 89 | } 90 | } 91 | 92 | impl WidgetChildrenExt for Composed { 93 | fn set_children(&mut self, children: Vec) { 94 | self.component.set_children(children) 95 | } 96 | 97 | fn with_children(self, children: Vec) -> Self 98 | where 99 | Self: Sized, 100 | { 101 | self.component.with_children(children).compose() 102 | } 103 | 104 | fn add_child(&mut self, child: impl Widget + 'static) { 105 | self.component.add_child(child) 106 | } 107 | 108 | fn with_child(self, child: impl Widget + 'static) -> Self 109 | where 110 | Self: Sized, 111 | { 112 | self.component.with_child(child).compose() 113 | } 114 | } 115 | 116 | impl WidgetChildExt for Composed { 117 | fn set_child(&mut self, child: impl Widget + 'static) { 118 | self.component.set_child(child) 119 | } 120 | 121 | fn with_child(self, child: impl Widget + 'static) -> Self 122 | where 123 | Self: Sized, 124 | { 125 | self.component.with_child(child).compose() 126 | } 127 | } 128 | 129 | impl WidgetLayoutExt for Composed { 130 | fn set_layout_style(&mut self, layout_style: impl Into>) { 131 | self.component.set_layout_style(layout_style) 132 | } 133 | 134 | fn with_layout_style(self, layout_style: impl Into>) -> Self 135 | where 136 | Self: Sized, 137 | { 138 | self.component.with_layout_style(layout_style).compose() 139 | } 140 | } 141 | 142 | impl Clone for Composed { 143 | fn clone(&self) -> Self { 144 | Self { 145 | component: self.component.clone(), 146 | widget: None, 147 | } 148 | } 149 | } 150 | 151 | impl Debug for Composed { 152 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 153 | f.debug_struct("ComposedWidget") 154 | .field("component", &self.component) 155 | .field("widget", &self.widget.as_ref().map(|_| "?".to_string())) 156 | .finish() 157 | } 158 | } 159 | 160 | impl Deref for Composed { 161 | type Target = C; 162 | 163 | fn deref(&self) -> &Self::Target { 164 | &self.component 165 | } 166 | } 167 | 168 | impl DerefMut for Composed { 169 | fn deref_mut(&mut self) -> &mut Self::Target { 170 | &mut self.component 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /maycoon-core/src/config.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::{Point2, Vector2}; 2 | use std::num::NonZeroUsize; 3 | use vello::util::DeviceHandle; 4 | pub use vello::AaConfig; 5 | pub use wgpu_types::PresentMode; 6 | pub use winit::window::{ 7 | BadIcon, Cursor, CursorIcon, CustomCursor, Icon as WindowIcon, WindowButtons, WindowLevel, 8 | }; 9 | 10 | use maycoon_theme::theme::Theme; 11 | 12 | /// Maycoon Application Configuration Structure. 13 | #[derive(Clone)] 14 | pub struct MayConfig { 15 | /// Window Configuration 16 | pub window: WindowConfig, 17 | /// Renderer Configuration. 18 | pub render: RenderConfig, 19 | /// Task Runner Configuration. If [None] (default), the task runner won't be enabled. 20 | pub tasks: Option, 21 | /// Theme of the Application. 22 | pub theme: T, 23 | } 24 | 25 | impl Default for MayConfig { 26 | fn default() -> Self { 27 | Self { 28 | window: WindowConfig::default(), 29 | render: RenderConfig::default(), 30 | tasks: None, 31 | theme: T::default(), 32 | } 33 | } 34 | } 35 | 36 | /// Window configuration. 37 | #[derive(Clone)] 38 | pub struct WindowConfig { 39 | /// The title of the window. 40 | pub title: String, 41 | /// The inner size of the window. 42 | pub size: Vector2, 43 | /// The minimum size of the window. 44 | pub min_size: Option>, 45 | /// The maximum size of the window. 46 | pub max_size: Option>, 47 | /// If the window should be resizeable. 48 | pub resizable: bool, 49 | /// If the window should be maximized on startup. 50 | pub maximized: bool, 51 | /// The window mode. 52 | pub mode: WindowMode, 53 | /// The window level. 54 | pub level: WindowLevel, 55 | /// If the window should be visible on startup. 56 | pub visible: bool, 57 | /// If the window background should be blurred. 58 | pub blur: bool, 59 | /// If the window background should be transparent. May not be compatible on all system. 60 | pub transparent: bool, 61 | /// The desired initial position for the window. 62 | pub position: Option>, 63 | /// If the window should be active/focused on startup. 64 | pub active: bool, 65 | /// The enabled window buttons. 66 | pub buttons: WindowButtons, 67 | /// If the window should be decorated (have borders). 68 | pub decorations: bool, 69 | /// The resize increments of the window. Not supported everywhere. 70 | pub resize_increments: Option>, 71 | /// Prevents window capturing by some apps (not all though). 72 | pub content_protected: bool, 73 | /// The window icon. 74 | pub icon: Option, 75 | /// The window cursor. 76 | pub cursor: Cursor, 77 | /// If the window should exit/close on close request (pressing the close window button). 78 | pub close_on_request: bool, 79 | } 80 | 81 | impl Default for WindowConfig { 82 | fn default() -> Self { 83 | Self { 84 | title: "New App".to_string(), 85 | size: Vector2::new(800.0, 600.0), 86 | min_size: None, 87 | max_size: None, 88 | resizable: true, 89 | maximized: false, 90 | mode: WindowMode::default(), 91 | level: Default::default(), 92 | visible: true, 93 | blur: false, 94 | transparent: false, 95 | position: None, 96 | active: true, 97 | buttons: WindowButtons::all(), 98 | decorations: true, 99 | resize_increments: None, 100 | content_protected: false, 101 | icon: None, 102 | cursor: Cursor::default(), 103 | close_on_request: true, 104 | } 105 | } 106 | } 107 | 108 | /// Renderer configuration. 109 | #[derive(Clone)] 110 | pub struct RenderConfig { 111 | /// The antialiasing config 112 | pub antialiasing: AaConfig, 113 | /// If the backend should use the CPU for most drawing operations. 114 | /// 115 | /// **NOTE:** The GPU is still used during rasterization. 116 | pub cpu: bool, 117 | /// The presentation mode of the window/surface. 118 | pub present_mode: PresentMode, 119 | /// The number of threads to use for initialization in [vello]. 120 | pub init_threads: Option, 121 | /// The selector function to determine which device to use for rendering. Defaults to using the first device found. 122 | pub device_selector: fn(&Vec) -> &DeviceHandle, 123 | } 124 | 125 | impl Default for RenderConfig { 126 | fn default() -> Self { 127 | Self { 128 | antialiasing: AaConfig::Area, 129 | cpu: false, 130 | present_mode: PresentMode::AutoNoVsync, 131 | init_threads: None, 132 | device_selector: |devices| devices.first().expect("No devices found"), 133 | } 134 | } 135 | } 136 | 137 | /// The window mode. 138 | #[derive(Clone, Debug, Default)] 139 | pub enum WindowMode { 140 | /// The default windowed mode. 141 | #[default] 142 | Windowed, 143 | /// Size the window to fill the screen and remove borders. This is more modern, than default Fullscreen. 144 | Borderless, 145 | /// Legacy Fullscreen mode. 146 | Fullscreen, 147 | } 148 | 149 | /// Configuration structure for the integrated [TaskRunner](crate::tasks::TaskRunner). 150 | /// 151 | /// The task runner isn't used by maycoon internally, but can be used to spawn asynchronous tasks and integrate them with the UI. 152 | #[derive(Clone, Debug, Eq, PartialEq)] 153 | pub struct TasksConfig { 154 | /// The stack size of each thread of the task runner thread pool. Defaults to 1 MB. 155 | pub stack_size: usize, 156 | /// The amount of worker threads of the task runner thread pool. Defaults to half of the available threads. 157 | pub workers: NonZeroUsize, 158 | } 159 | 160 | impl Default for TasksConfig { 161 | fn default() -> Self { 162 | Self { 163 | stack_size: 1024 * 1024, // 1 MB 164 | workers: NonZeroUsize::new( 165 | std::thread::available_parallelism() 166 | .expect("Failed to get available threads") 167 | .get() 168 | / 2, 169 | ) 170 | .unwrap(), 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /maycoon-core/src/layout.rs: -------------------------------------------------------------------------------- 1 | use nalgebra::Vector2; 2 | pub use taffy::{ 3 | AlignContent, AlignItems, AlignSelf, Dimension, Display, FlexDirection, FlexWrap, GridAutoFlow, 4 | GridPlacement, JustifyContent, JustifyItems, JustifySelf, Layout, LengthPercentage, 5 | LengthPercentageAuto, Line, NodeId, Overflow, Position, Rect, TaffyError, TaffyResult, 6 | TaffyTree, 7 | }; 8 | 9 | /// Defines different aspects and properties of a widget layout. 10 | #[derive(Clone, PartialEq, Debug)] 11 | pub struct LayoutStyle { 12 | /// What layout strategy should be used? 13 | pub display: Display, 14 | 15 | /// How children overflowing their container should affect layout. 16 | pub overflow: (Overflow, Overflow), 17 | 18 | /// How much space (in points) should be reserved for scrollbars. 19 | pub scrollbar_width: f32, 20 | 21 | /// What should the position value of this struct use as a base offset? 22 | pub position: Position, 23 | 24 | /// How should the position of this element be tweaked relative to the layout defined? 25 | pub inset: Rect, 26 | 27 | /// Sets the initial size of the item. 28 | pub size: Vector2, 29 | 30 | /// Controls the minimum size of the item. 31 | pub min_size: Vector2, 32 | 33 | /// Controls the maximum size of the item. 34 | pub max_size: Vector2, 35 | 36 | /// Sets the preferred aspect ratio for the item 37 | /// 38 | /// The ratio is calculated as width divided by height. 39 | pub aspect_ratio: Option, 40 | 41 | /// How large should the margin be on each side? 42 | pub margin: Rect, 43 | 44 | /// How large should the padding be on each side? 45 | pub padding: Rect, 46 | 47 | /// How large should the border be on each side? 48 | pub border: Rect, 49 | 50 | /// How this node's children aligned in the cross/block axis? 51 | pub align_items: Option, 52 | 53 | /// How this node should be aligned in the cross/block axis 54 | /// Falls back to the parents [AlignItems] if not set. 55 | pub align_self: Option, 56 | 57 | /// How this node's children should be aligned in the inline axis. 58 | pub justify_items: Option, 59 | 60 | /// How this node should be aligned in the inline axis 61 | /// Falls back to the parents [JustifyItems] if not set. 62 | pub justify_self: Option, 63 | 64 | /// How should content contained within this item be aligned in the cross/block axis? 65 | pub align_content: Option, 66 | 67 | /// How should content contained within this item be aligned in the main/inline axis? 68 | pub justify_content: Option, 69 | 70 | /// How large should the gaps between items in a grid or flex container be? 71 | pub gap: Vector2, 72 | 73 | /// Which direction does the main axis flow in? 74 | pub flex_direction: FlexDirection, 75 | 76 | /// Should elements wrap, or stay in a single line? 77 | pub flex_wrap: FlexWrap, 78 | 79 | /// Sets the initial main axis size of the item. 80 | pub flex_basis: Dimension, 81 | 82 | /// The relative rate at which this item grows when it is expanding to fill space. 83 | /// 84 | /// 0.0 is the default value, and this value must be positive. 85 | pub flex_grow: f32, 86 | 87 | /// The relative rate at which this item shrinks when it is contracting to fit into space. 88 | /// 89 | /// 1.0 is the default value, and this value must be positive. 90 | pub flex_shrink: f32, 91 | 92 | /// Controls how items get placed into the grid for auto-placed items. 93 | pub grid_auto_flow: GridAutoFlow, 94 | 95 | /// Defines which row in the grid the item should start and end at. 96 | pub grid_row: Line, 97 | 98 | /// Defines which column in the grid the item should start and end at. 99 | pub grid_column: Line, 100 | } 101 | 102 | impl Default for LayoutStyle { 103 | fn default() -> Self { 104 | LayoutStyle { 105 | display: Display::default(), 106 | overflow: (Overflow::Visible, Overflow::Visible), 107 | scrollbar_width: 0.0, 108 | position: Position::Relative, 109 | inset: Rect::auto(), 110 | margin: Rect::zero(), 111 | padding: Rect::zero(), 112 | border: Rect::zero(), 113 | size: Vector2::new(Dimension::auto(), Dimension::auto()), 114 | min_size: Vector2::new(Dimension::auto(), Dimension::auto()), 115 | max_size: Vector2::new(Dimension::auto(), Dimension::auto()), 116 | aspect_ratio: None, 117 | gap: Vector2::new(LengthPercentage::length(0.0), LengthPercentage::length(0.0)), 118 | align_items: None, 119 | align_self: None, 120 | justify_items: None, 121 | justify_self: None, 122 | align_content: None, 123 | justify_content: None, 124 | flex_direction: FlexDirection::Row, 125 | flex_wrap: FlexWrap::NoWrap, 126 | flex_grow: 0.0, 127 | flex_shrink: 1.0, 128 | flex_basis: Dimension::auto(), 129 | grid_auto_flow: GridAutoFlow::Row, 130 | grid_row: Line { 131 | start: GridPlacement::Auto, 132 | end: GridPlacement::Auto, 133 | }, 134 | grid_column: Line { 135 | start: GridPlacement::Auto, 136 | end: GridPlacement::Auto, 137 | }, 138 | } 139 | } 140 | } 141 | 142 | impl From for taffy::Style { 143 | fn from(value: LayoutStyle) -> Self { 144 | taffy::Style { 145 | display: value.display, 146 | overflow: taffy::Point { 147 | x: value.overflow.0, 148 | y: value.overflow.1, 149 | }, 150 | scrollbar_width: value.scrollbar_width, 151 | position: value.position, 152 | inset: value.inset, 153 | margin: value.margin, 154 | padding: value.padding, 155 | border: value.border, 156 | size: taffy::Size { 157 | width: value.size.x, 158 | height: value.size.y, 159 | }, 160 | min_size: taffy::Size { 161 | width: value.min_size.x, 162 | height: value.min_size.y, 163 | }, 164 | max_size: taffy::Size { 165 | width: value.max_size.x, 166 | height: value.max_size.y, 167 | }, 168 | aspect_ratio: value.aspect_ratio, 169 | gap: taffy::Size { 170 | width: value.gap.x, 171 | height: value.gap.y, 172 | }, 173 | align_items: value.align_items, 174 | align_self: value.align_self, 175 | justify_items: value.justify_items, 176 | justify_self: value.justify_self, 177 | align_content: value.align_content, 178 | justify_content: value.justify_content, 179 | flex_direction: value.flex_direction, 180 | flex_wrap: value.flex_wrap, 181 | flex_grow: value.flex_grow, 182 | flex_shrink: value.flex_shrink, 183 | flex_basis: value.flex_basis, 184 | grid_auto_flow: value.grid_auto_flow, 185 | grid_row: value.grid_row, 186 | grid_column: value.grid_column, 187 | ..Default::default() 188 | } 189 | } 190 | } 191 | 192 | /// The computed layout with children nodes. 193 | #[derive(Debug)] 194 | pub struct LayoutNode { 195 | /// The computed layout of this node. 196 | pub layout: Layout, 197 | /// The children of this node. 198 | pub children: Vec, 199 | } 200 | 201 | /// The raw layout styles with children nodes. 202 | pub struct StyleNode { 203 | /// The layout style of this node. 204 | pub style: LayoutStyle, 205 | /// The children of this node. 206 | pub children: Vec, 207 | } 208 | -------------------------------------------------------------------------------- /maycoon-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! Core library for Maycoon => See `maycoon` crate. 4 | //! 5 | //! Contains core app logic and widget types. 6 | 7 | #[cfg(feature = "vg")] 8 | pub use vello as vg; 9 | 10 | #[cfg(feature = "vg")] 11 | pub use skrifa; 12 | 13 | /// Contains useful types for interacting with winit. 14 | pub mod window { 15 | pub use winit::event::*; 16 | pub use winit::event_loop::*; 17 | pub use winit::keyboard::*; 18 | pub use winit::window::*; 19 | } 20 | 21 | /// Contains app functionality. 22 | pub mod app; 23 | 24 | /// Contains the [MayConfig](config::MayConfig) struct. 25 | pub mod config; 26 | 27 | /// Contains useful types and functions for layout interaction. 28 | pub mod layout; 29 | 30 | /// Contains the signal system for reactive programming 31 | pub mod signal; 32 | 33 | /// Contains the core widget functionalities 34 | pub mod widget; 35 | 36 | /// Contains structures to work with the component architecture 37 | pub mod component; 38 | 39 | /// Contains the task runner and utilities for running async 40 | pub mod tasks; 41 | 42 | /// Contains the [reference::Ref] for representing a reference to a value. 43 | pub mod reference; 44 | 45 | /// Contains the plugin system. 46 | pub mod plugin; 47 | 48 | #[cfg(feature = "include-noto-sans")] 49 | pub(crate) const DEFAULT_FONT: &[u8] = include_bytes!("NotoSans.ttf"); 50 | -------------------------------------------------------------------------------- /maycoon-core/src/plugin.rs: -------------------------------------------------------------------------------- 1 | use crate::app::info::AppInfo; 2 | use crate::app::update::UpdateManager; 3 | use crate::config::MayConfig; 4 | use indexmap::IndexMap; 5 | use maycoon_theme::theme::Theme; 6 | use std::sync::Arc; 7 | use std::time::Instant; 8 | use taffy::{NodeId, TaffyTree}; 9 | use vello::util::{RenderContext, RenderSurface}; 10 | use vello::{Renderer, Scene}; 11 | use winit::event::WindowEvent; 12 | use winit::event_loop::{ActiveEventLoop, EventLoop}; 13 | use winit::window::{Window, WindowAttributes}; 14 | 15 | /// A plugin interface for maycoon applications. 16 | /// 17 | /// Plugins are used to extend functionality and manipulate the inner state of applications. 18 | /// Beware that tampering with the application state may cause crashes or other issues if not done correctly. 19 | /// 20 | /// All functions defined in this trait get called before the app handler logic and therefore can control the application flow. 21 | pub trait Plugin { 22 | /// The plugin name. 23 | /// 24 | /// Should be unique among the ecosystem. 25 | fn name(&self) -> &'static str; 26 | 27 | /// Called when the plugin is registered using [PluginManager::register]. 28 | fn on_register(&mut self, _manager: &mut PluginManager) {} 29 | 30 | /// Called when the plugin is unregistered using [PluginManager::unregister]. 31 | fn on_unregister(&mut self, _manager: &mut PluginManager) {} 32 | 33 | /// Called right before initializing the [AppHandler](crate::app::handler::AppHandler) and running the event loop. 34 | fn init( 35 | &mut self, 36 | _event_loop: &mut EventLoop<()>, 37 | _update: &UpdateManager, 38 | _window: &mut WindowAttributes, 39 | _config: &mut MayConfig, 40 | ) { 41 | } 42 | 43 | /// Called when the application is resumed after being suspended or when it's first started. 44 | /// 45 | /// Desktop applications typically don't get suspended and this function is only called once, 46 | /// while mobile apps can be suspended and resumed. 47 | fn on_resume( 48 | &mut self, 49 | _config: &mut MayConfig, 50 | _scene: &mut Scene, 51 | _taffy: &mut TaffyTree, 52 | _window_node: NodeId, 53 | _info: &mut AppInfo, 54 | _update: &UpdateManager, 55 | _last_update: &mut Instant, 56 | _event_loop: &ActiveEventLoop, 57 | ) { 58 | } 59 | 60 | /// Called right before the application handler tries to update the application 61 | /// and figure out what updates to apply. 62 | fn on_update( 63 | &mut self, 64 | _config: &mut MayConfig, 65 | _window: &Arc, 66 | _renderer: &mut Renderer, 67 | _scene: &mut Scene, 68 | _surface: &mut RenderSurface<'_>, 69 | _taffy: &mut TaffyTree, 70 | _window_node: NodeId, 71 | _info: &mut AppInfo, 72 | _render_ctx: &RenderContext, 73 | _update: &UpdateManager, 74 | _last_update: &mut Instant, 75 | _event_loop: &ActiveEventLoop, 76 | ) { 77 | } 78 | 79 | /// Called when a window event is received. 80 | fn on_window_event( 81 | &mut self, 82 | _event: &mut WindowEvent, 83 | _config: &mut MayConfig, 84 | _window: &Arc, 85 | _renderer: &mut Renderer, 86 | _scene: &mut Scene, 87 | _surface: &mut RenderSurface<'_>, 88 | _taffy: &mut TaffyTree, 89 | _window_node: NodeId, 90 | _info: &mut AppInfo, 91 | _render_ctx: &RenderContext, 92 | _update: &UpdateManager, 93 | _last_update: &mut Instant, 94 | _event_loop: &ActiveEventLoop, 95 | ) { 96 | } 97 | 98 | /// Called when the application is suspended. 99 | fn on_suspended( 100 | &mut self, 101 | _config: &mut MayConfig, 102 | _scene: &mut Scene, 103 | _taffy: &mut TaffyTree, 104 | _window_node: NodeId, 105 | _info: &mut AppInfo, 106 | _update: &UpdateManager, 107 | _last_update: &mut Instant, 108 | _event_loop: &ActiveEventLoop, 109 | ) { 110 | } 111 | } 112 | 113 | /// A plugin manager for maycoon applications. 114 | pub struct PluginManager { 115 | plugins: IndexMap<&'static str, Box>>, 116 | } 117 | 118 | impl PluginManager { 119 | /// Creates a new empty plugin manager. 120 | pub fn new() -> Self { 121 | Self { 122 | plugins: IndexMap::new(), 123 | } 124 | } 125 | 126 | /// Registers a new plugin. 127 | pub fn register(&mut self, mut plugin: impl Plugin + 'static) { 128 | plugin.on_register(self); 129 | 130 | self.plugins.insert(plugin.name(), Box::new(plugin)); 131 | } 132 | 133 | /// Unregisters a plugin. 134 | pub fn unregister(&mut self, name: &'static str) { 135 | let mut pl = self.plugins.swap_remove(name).expect("Plugin not found"); 136 | 137 | pl.on_unregister(self); 138 | } 139 | 140 | /// Unregisters all plugins. 141 | pub fn clear(&mut self) { 142 | let plugins = self.plugins.keys().cloned().collect::>(); 143 | 144 | for name in plugins { 145 | self.unregister(name); 146 | } 147 | } 148 | 149 | /// Runs a closure on all plugins. 150 | pub fn run(&mut self, mut op: F) 151 | where 152 | F: FnMut(&mut Box>), 153 | { 154 | for pl in self.plugins.values_mut() { 155 | op(pl); 156 | } 157 | } 158 | } 159 | 160 | impl Default for PluginManager { 161 | fn default() -> Self { 162 | Self::new() 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /maycoon-core/src/reference.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | use std::sync::{Arc, RwLockReadGuard, RwLockWriteGuard}; 3 | 4 | /// Represents a reference to a value of type `T`. 5 | /// 6 | /// Due to Rust's temporal borrowing rules, 7 | /// returning a reference to a value may not be always possible, 8 | /// so this enum is used to represent one by having multiple variants for multiple types of references. 9 | pub enum Ref<'a, T> { 10 | /// An owned value. Useful for [Copy] types. 11 | Owned(T), 12 | /// A borrowed reference. 13 | Borrow(&'a T), 14 | /// An [Arc] reference. 15 | Arc(Arc), 16 | /// A [RwLockReadGuard] reference. 17 | ReadGuard(RwLockReadGuard<'a, T>), 18 | /// A [parking_lot::RwLockReadGuard] reference. 19 | ParkingLotReadGuard(parking_lot::RwLockReadGuard<'a, T>), 20 | } 21 | 22 | impl<'a, T> Deref for Ref<'a, T> { 23 | type Target = T; 24 | 25 | fn deref(&self) -> &Self::Target { 26 | match self { 27 | Ref::Owned(value) => value, 28 | Ref::Borrow(value) => value, 29 | Ref::Arc(value) => value, 30 | Ref::ReadGuard(value) => value, 31 | Ref::ParkingLotReadGuard(value) => value, 32 | } 33 | } 34 | } 35 | 36 | impl<'a, T> From for Ref<'a, T> { 37 | fn from(value: T) -> Self { 38 | Ref::Owned(value) 39 | } 40 | } 41 | 42 | impl<'a, T> From<&'a T> for Ref<'a, T> { 43 | fn from(value: &'a T) -> Self { 44 | Ref::Borrow(value) 45 | } 46 | } 47 | 48 | impl<'a, T> From> for Ref<'a, T> { 49 | fn from(value: Arc) -> Self { 50 | Ref::Arc(value) 51 | } 52 | } 53 | 54 | impl<'a, T> From> for Ref<'a, T> { 55 | fn from(value: RwLockReadGuard<'a, T>) -> Self { 56 | Ref::ReadGuard(value) 57 | } 58 | } 59 | 60 | impl<'a, T> From> for Ref<'a, T> { 61 | fn from(value: parking_lot::RwLockReadGuard<'a, T>) -> Self { 62 | Ref::ParkingLotReadGuard(value) 63 | } 64 | } 65 | 66 | /// Represents a mutable reference to a value of type `T`. 67 | /// 68 | /// Due to Rust's temporal borrowing rules, 69 | /// returning a reference to a value may not be always possible, 70 | /// so this enum is used to represent one by having multiple variants for multiple types of mutable references. 71 | pub enum MutRef<'a, T> { 72 | /// A borrowed mutable reference. 73 | Borrow(&'a mut T), 74 | /// A [RwLockWriteGuard] mutable reference. 75 | WriteGuard(RwLockWriteGuard<'a, T>), 76 | /// A [parking_lot::RwLockWriteGuard] mutable reference. 77 | ParkingLotWriteGuard(parking_lot::RwLockWriteGuard<'a, T>), 78 | } 79 | 80 | impl<'a, T> Deref for MutRef<'a, T> { 81 | type Target = T; 82 | 83 | fn deref(&self) -> &Self::Target { 84 | match self { 85 | MutRef::Borrow(value) => value, 86 | MutRef::WriteGuard(value) => value, 87 | MutRef::ParkingLotWriteGuard(value) => value, 88 | } 89 | } 90 | } 91 | 92 | impl<'a, T> DerefMut for MutRef<'a, T> { 93 | fn deref_mut(&mut self) -> &mut Self::Target { 94 | match self { 95 | MutRef::Borrow(value) => value, 96 | MutRef::WriteGuard(value) => value, 97 | MutRef::ParkingLotWriteGuard(value) => value, 98 | } 99 | } 100 | } 101 | 102 | impl<'a, T> From<&'a mut T> for MutRef<'a, T> { 103 | fn from(value: &'a mut T) -> Self { 104 | MutRef::Borrow(value) 105 | } 106 | } 107 | 108 | impl<'a, T> From> for MutRef<'a, T> { 109 | fn from(value: RwLockWriteGuard<'a, T>) -> Self { 110 | MutRef::WriteGuard(value) 111 | } 112 | } 113 | 114 | impl<'a, T> From> for MutRef<'a, T> { 115 | fn from(value: parking_lot::RwLockWriteGuard<'a, T>) -> Self { 116 | MutRef::ParkingLotWriteGuard(value) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/actor.rs: -------------------------------------------------------------------------------- 1 | use crate::reference::{MutRef, Ref}; 2 | use crate::signal::rw::RwSignal; 3 | use crate::signal::{Listener, Signal}; 4 | use parking_lot::Mutex; 5 | 6 | /// An action to run on the inner value. 7 | pub type Action = Box)>; 8 | 9 | /// A signal to run actions on the inner value if unlocked. 10 | /// 11 | /// The issue with [RwSignal] is that it may cause deadlocks if not used properly. 12 | /// [ActorSignal] tries to fix this by running actions you push beforehand as soon as the signal is unlocked and the value requires an update. 13 | /// This avoids deadlocks, but can still cause the pushed actions to not run at all, if the signal is constantly locked. 14 | pub struct ActorSignal { 15 | rw: RwSignal, 16 | actions: Mutex>>, 17 | } 18 | 19 | impl ActorSignal { 20 | /// Creates a new [ActorSignal] with the given value. 21 | pub fn new(value: T) -> Self { 22 | Self { 23 | rw: RwSignal::new(value), 24 | actions: Mutex::new(Vec::new()), 25 | } 26 | } 27 | 28 | /// Get a mutable reference to the value. 29 | /// 30 | /// This calls [ActorSignal::run] if the inner value is not locked yet and returns the final value. 31 | pub fn get_mut(&self) -> MutRef { 32 | let value = self.rw.get_mut(); 33 | 34 | if !self.is_locked() { 35 | self.run(); 36 | } 37 | 38 | value 39 | } 40 | 41 | /// Check if the inner value is locked. 42 | pub fn is_locked(&self) -> bool { 43 | self.rw.is_locked() 44 | } 45 | 46 | /// Push an action to run on the inner value. 47 | /// 48 | /// The action will be run as soon as the inner value is unlocked and requires an update. 49 | pub fn act(&self, action: Action) { 50 | self.actions.lock().push(action); 51 | } 52 | 53 | /// Run all pushed actions. 54 | /// 55 | /// This can cause deadlocks if [Self::is_locked] returns true. 56 | pub fn run(&self) { 57 | for action in self.actions.lock().drain(..) { 58 | action(self.get_mut()); 59 | } 60 | } 61 | } 62 | 63 | impl Signal for ActorSignal { 64 | fn get(&self) -> Ref { 65 | if !self.is_locked() { 66 | self.run(); 67 | } 68 | 69 | self.rw.get() 70 | } 71 | 72 | fn set_value(&self, value: T) { 73 | self.rw.set_value(value); 74 | } 75 | 76 | fn listen(&mut self, listener: Listener) { 77 | self.rw.listen(listener); 78 | } 79 | 80 | fn notify(&self) { 81 | self.rw.notify(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/eval.rs: -------------------------------------------------------------------------------- 1 | use crate::reference::Ref; 2 | use crate::signal::{Listener, Signal}; 3 | 4 | /// A signal that evaluates a function to get the value. 5 | /// 6 | /// The evaluation function will be called every time the value is requested via [Signal::get], 7 | /// so it's recommended to avoid expensive operations. 8 | pub struct EvalSignal { 9 | eval: Box T>, 10 | } 11 | 12 | impl EvalSignal { 13 | /// Create a new eval signal using the given evaluation function. 14 | pub fn new(eval: impl Fn() -> T + 'static) -> Self { 15 | Self { 16 | eval: Box::new(eval), 17 | } 18 | } 19 | } 20 | 21 | impl Signal for EvalSignal { 22 | fn get(&self) -> Ref { 23 | Ref::Owned((self.eval)()) 24 | } 25 | 26 | fn set_value(&self, _: T) {} 27 | 28 | fn listen(&mut self, _: Listener) {} 29 | 30 | fn notify(&self) {} 31 | } 32 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/fixed.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{Listener, Ref, Signal}; 2 | 3 | /// A signal with a fixed value. 4 | /// 5 | /// The inner value cannot be changed and listeners do not exist. 6 | /// Useful for testing purposes. 7 | #[derive(Clone)] 8 | pub struct FixedSignal { 9 | value: T, 10 | } 11 | 12 | impl FixedSignal { 13 | /// Creates a new fixed signal. 14 | pub fn new(value: T) -> Self { 15 | Self { value } 16 | } 17 | } 18 | 19 | impl Signal for FixedSignal { 20 | fn get(&self) -> Ref { 21 | Ref::Borrow(&self.value) 22 | } 23 | 24 | fn set_value(&self, _: T) {} 25 | 26 | fn listen(&mut self, _: Listener) {} 27 | 28 | fn notify(&self) {} 29 | } 30 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/map.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{ArcSignal, Listener, Ref, Signal}; 2 | 3 | /// A signal wrapping another signal and applying a mapping function, when the inner value is requested. 4 | /// The mapping function will be called every time the inner value is requested via [Signal::get]. 5 | /// This signal cannot be directly mutated. Use [MapSignal::signal] to get the inner signal. 6 | /// 7 | /// Calling [Signal::set], [Signal::set_value], [Signal::listen] or [Signal::notify] has no effect. 8 | pub struct MapSignal { 9 | signal: ArcSignal, 10 | map: Box) -> Ref>, 11 | } 12 | 13 | impl MapSignal { 14 | /// Create a new map signal using the given inner signal and mapping function. 15 | pub fn new(signal: ArcSignal, map: impl Fn(Ref) -> Ref + 'static) -> Self { 16 | Self { 17 | signal, 18 | map: Box::new(map), 19 | } 20 | } 21 | 22 | /// Get the inner signal. 23 | /// 24 | /// Can be used to mutate the inner value. 25 | pub fn signal(&self) -> ArcSignal { 26 | self.signal.clone() 27 | } 28 | 29 | /// Get the inner signal's value, without applying the mapping function. 30 | pub fn get_unmapped(&self) -> Ref { 31 | self.signal.get() 32 | } 33 | } 34 | 35 | impl Signal for MapSignal { 36 | fn get(&self) -> Ref { 37 | (self.map)(self.get_unmapped()) 38 | } 39 | 40 | fn set_value(&self, _: U) {} 41 | 42 | fn listen(&mut self, _: Listener) {} 43 | 44 | fn notify(&self) { 45 | self.signal.notify(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/memoized.rs: -------------------------------------------------------------------------------- 1 | use crate::reference::Ref; 2 | use crate::signal::{Listener, Signal}; 3 | use std::sync::OnceLock; 4 | 5 | /// A signal for creating a value once, when requested. 6 | /// The value is immutable after creation. 7 | /// Calling [Signal::set], [Signal::set_value], [Signal::listen] or [Signal::notify] has no effect. 8 | /// 9 | /// **NOTE:** The inner factory function will only be called once the value is requested via [Signal::get]. 10 | pub struct MemoizedSignal { 11 | inner: OnceLock, 12 | factory: Box T>, 13 | } 14 | 15 | impl MemoizedSignal { 16 | /// Create a new memoized signal using the given factory function. 17 | pub fn new(factory: impl Fn() -> T + 'static) -> Self { 18 | Self { 19 | inner: OnceLock::new(), 20 | factory: Box::new(factory), 21 | } 22 | } 23 | } 24 | 25 | impl Signal for MemoizedSignal { 26 | fn get(&self) -> Ref { 27 | Ref::Borrow(self.inner.get_or_init(&self.factory)) 28 | } 29 | 30 | fn set_value(&self, _: T) {} 31 | 32 | fn listen(&mut self, _: Listener) {} 33 | 34 | fn notify(&self) {} 35 | } 36 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::context::AppContext; 2 | use crate::reference::Ref; 3 | use crate::signal::fixed::FixedSignal; 4 | use crate::signal::map::MapSignal; 5 | use std::sync::Arc; 6 | 7 | /// Contains the [FixedSignal] signal. 8 | pub mod fixed; 9 | 10 | /// Contains the [memoized::MemoizedSignal] signal. 11 | pub mod memoized; 12 | 13 | /// Contains the [state::StateSignal] signal. 14 | pub mod state; 15 | 16 | /// Contains the [MapSignal] signal. 17 | pub mod map; 18 | 19 | /// Contains the [eval::EvalSignal] signal. 20 | pub mod eval; 21 | 22 | /// Contains the [rw::RwSignal] signal. 23 | pub mod rw; 24 | 25 | /// Contains the [actor::ActorSignal] signal. 26 | pub mod actor; 27 | 28 | /// Listener function for [Signal]. 29 | pub type Listener = Box)>; 30 | 31 | /// [Arc] reference to a [Signal]. 32 | pub type ArcSignal = Arc>; 33 | 34 | /// Base signal trait. 35 | /// 36 | /// Signals store values of type `T` and notify listeners when they change. 37 | /// 38 | /// **NOTE:** By default, signals don't have any listeners. To "hook" a signal into the application cycle, call [use_signal]. 39 | /// 40 | /// [use_signal]: AppContext::use_signal 41 | pub trait Signal: 'static { 42 | /// Get a reference to the current value of the signal. 43 | fn get(&self) -> Ref; 44 | 45 | /// Set the value of the signal. 46 | /// 47 | /// **NOTE:** This does not notify listeners, use [set] instead. 48 | fn set_value(&self, value: T); 49 | 50 | /// Add a listener to the signal, which will be called when the signal's value changes. 51 | fn listen(&mut self, listener: Listener); 52 | 53 | /// Notify listeners that the signal's value has changed. 54 | /// May also be called manually to update listeners. 55 | fn notify(&self); 56 | 57 | /// Set the value of the signal and notify listeners. 58 | fn set(&self, value: T) { 59 | self.set_value(value); 60 | self.notify(); 61 | } 62 | 63 | /// Converts the signal into a [MaybeSignal]. 64 | fn maybe(self: &Arc) -> MaybeSignal 65 | where 66 | Self: Sized, 67 | { 68 | MaybeSignal::signal(self.clone()) 69 | } 70 | 71 | /// Converts this signal into a [MaybeSignal] and applies the given mapping function. 72 | /// 73 | /// See [MaybeSignal::map] for more. 74 | fn map(self: Arc, map: impl Fn(Ref) -> Ref + 'static) -> MaybeSignal 75 | where 76 | Self: Sized, 77 | { 78 | self.maybe().map(map) 79 | } 80 | 81 | /// Hooks the signal into the given [AppContext]. 82 | /// 83 | /// Required for the signal to become reactive with the app lifecycle. 84 | fn hook(self, context: &AppContext) -> Arc 85 | where 86 | Self: Sized, 87 | { 88 | context.use_signal(self) 89 | } 90 | } 91 | 92 | /// A value which may be a signal or a fixed value. 93 | #[derive(Clone)] 94 | pub enum MaybeSignal { 95 | /// A signal. 96 | Signal(ArcSignal), 97 | /// A fixed value wrapped inside an [Arc]. 98 | Value(Arc), 99 | } 100 | 101 | impl MaybeSignal { 102 | /// Wrap a [Signal] inside a [MaybeSignal]. 103 | pub fn signal(signal: ArcSignal) -> Self { 104 | Self::Signal(signal) 105 | } 106 | 107 | /// Wrap a value inside a [MaybeSignal]. 108 | pub fn value(value: T) -> Self { 109 | Self::Value(Arc::new(value)) 110 | } 111 | 112 | /// Get a reference to the current value. 113 | /// 114 | /// If the value is a signal, the signal's current value is returned, 115 | /// otherwise a [Ref::Arc] of the value is returned. 116 | pub fn get(&self) -> Ref { 117 | match self { 118 | MaybeSignal::Signal(signal) => signal.get(), 119 | MaybeSignal::Value(value) => Ref::Arc(value.clone()), 120 | } 121 | } 122 | 123 | /// Converts the [MaybeSignal] into a [ArcSignal]. 124 | /// 125 | /// If the value is a [MaybeSignal::Value], a [FixedSignal] is created. 126 | pub fn into_signal(self) -> ArcSignal { 127 | match self { 128 | MaybeSignal::Signal(signal) => signal, 129 | MaybeSignal::Value(value) => { 130 | Arc::new(FixedSignal::new(Arc::into_inner(value).unwrap())) 131 | }, 132 | } 133 | } 134 | 135 | /// Converts the [MaybeSignal] into an [ArcSignal] if it is a [MaybeSignal::Signal]. 136 | pub fn as_signal(&self) -> Option> { 137 | match self { 138 | MaybeSignal::Signal(signal) => Some(signal.clone()), 139 | _ => None, 140 | } 141 | } 142 | 143 | /// Applies the given mapping function to the signal. 144 | /// 145 | /// Returns a [MaybeSignal] containing a [MapSignal] which maps the inner value of the signal. 146 | pub fn map(self, map: impl Fn(Ref) -> Ref + 'static) -> MaybeSignal { 147 | let signal = self.into_signal(); 148 | 149 | MaybeSignal::signal(Arc::new(MapSignal::new(signal, map))) 150 | } 151 | } 152 | 153 | impl Default for MaybeSignal { 154 | fn default() -> Self { 155 | Self::value(T::default()) 156 | } 157 | } 158 | 159 | impl From for MaybeSignal { 160 | fn from(value: T) -> Self { 161 | Self::value(value) 162 | } 163 | } 164 | 165 | impl From> for MaybeSignal { 166 | fn from(signal: ArcSignal) -> Self { 167 | Self::signal(signal) 168 | } 169 | } 170 | 171 | impl<'a, T: 'static> From<&'a ArcSignal> for MaybeSignal { 172 | fn from(value: &'a ArcSignal) -> Self { 173 | Self::signal(value.clone()) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/rw.rs: -------------------------------------------------------------------------------- 1 | use crate::reference::{MutRef, Ref}; 2 | use crate::signal::{Listener, Signal}; 3 | use parking_lot::RwLock; 4 | 5 | /// A signal based on [RwLock] to load/store a value and notify listeners when it changes. 6 | /// 7 | /// The difference between this signal and [StateSignal] is that this signal can be directly mutated using [get_mut]. 8 | /// However, this introduces extra overhead and the same risks as [RwLock]. 9 | pub struct RwSignal { 10 | lock: RwLock, 11 | listeners: Vec>, 12 | } 13 | 14 | impl RwSignal { 15 | /// Create a new [RwSignal] with the given value. 16 | pub fn new(value: T) -> Self { 17 | Self { 18 | lock: RwLock::new(value), 19 | listeners: Vec::with_capacity(1), 20 | } 21 | } 22 | 23 | /// Get a mutable reference to the value. 24 | /// 25 | /// **NOTE:** Multiple mutable reference cannot co-exist and will lead to deadlocks. 26 | pub fn get_mut(&self) -> MutRef { 27 | MutRef::ParkingLotWriteGuard(self.lock.write()) 28 | } 29 | 30 | /// Check if the signal is locked. 31 | pub fn is_locked(&self) -> bool { 32 | self.lock.is_locked() 33 | } 34 | } 35 | 36 | impl Signal for RwSignal { 37 | fn get(&self) -> Ref { 38 | Ref::ParkingLotReadGuard(self.lock.read()) 39 | } 40 | 41 | fn set_value(&self, value: T) { 42 | *self.get_mut() = value; 43 | } 44 | 45 | fn listen(&mut self, listener: Listener) { 46 | self.listeners.push(listener); 47 | } 48 | 49 | fn notify(&self) { 50 | for listener in &self.listeners { 51 | listener(self.get()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /maycoon-core/src/signal/state.rs: -------------------------------------------------------------------------------- 1 | use crate::signal::{Listener, Ref, Signal}; 2 | use arc_swap::ArcSwap; 3 | use std::sync::Arc; 4 | 5 | /// Simple signal implementation based on [ArcSwap] to load/store a value and notify listeners when it changes. 6 | pub struct StateSignal { 7 | value: ArcSwap, 8 | listeners: Vec>, 9 | } 10 | 11 | impl StateSignal { 12 | /// Creates a new signal with the given value. 13 | pub fn new(value: T) -> Self { 14 | Self { 15 | value: ArcSwap::new(Arc::new(value)), 16 | listeners: Vec::with_capacity(1), 17 | } 18 | } 19 | } 20 | 21 | impl Signal for StateSignal { 22 | fn get(&self) -> Ref { 23 | Ref::Arc(self.value.load().clone()) 24 | } 25 | 26 | fn set_value(&self, value: T) { 27 | self.value.store(Arc::new(value)); 28 | } 29 | 30 | fn listen(&mut self, listener: Listener) { 31 | self.listeners.push(listener); 32 | } 33 | 34 | fn notify(&self) { 35 | for listener in &self.listeners { 36 | listener(self.get()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /maycoon-core/src/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks::runner::TaskRunner; 2 | use std::future::Future; 3 | use std::sync::OnceLock; 4 | 5 | pub use futures::future; 6 | 7 | /// Contains the [TaskRunner] struct. 8 | pub mod runner; 9 | 10 | /// A handle to a running task which can be awaited in order to get its result. 11 | pub type TaskHandle = future::RemoteHandle; 12 | 13 | static RUNNER: OnceLock = OnceLock::new(); 14 | 15 | /// Returns the global [TaskRunner] or panics if it hasn't been initialized yet. 16 | pub fn runner<'a>() -> &'a TaskRunner { 17 | try_runner().expect("Task runner not initialized yet") 18 | } 19 | 20 | /// Returns the global [TaskRunner] or [None] if it hasn't been initialized yet. 21 | pub fn try_runner<'a>() -> Option<&'a TaskRunner> { 22 | RUNNER.get() 23 | } 24 | 25 | /// Spawns the given [Future] on the task runner thread pool and returns a [TaskHandle] to await for the result. 26 | /// 27 | /// Panics if the task runner hasn't been initialized yet. 28 | pub fn spawn(fut: Fut) -> TaskHandle 29 | where 30 | Fut: Future + Send + 'static, 31 | Fut::Output: Send, 32 | { 33 | runner().run(fut) 34 | } 35 | 36 | /// Blocks the current thread until the given [Future] completes. 37 | pub fn block_on(fut: Fut) -> Fut::Output 38 | where 39 | Fut: Future, 40 | { 41 | futures::executor::block_on(fut) 42 | } 43 | -------------------------------------------------------------------------------- /maycoon-core/src/tasks/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::tasks::{TaskHandle, RUNNER}; 2 | use futures::executor::{ThreadPool, ThreadPoolBuilder}; 3 | use futures::task::SpawnExt; 4 | use std::future::Future; 5 | use std::num::NonZeroUsize; 6 | 7 | /// The Maycoon Task Runner for running asynchronous tasks using a thread pool from the [futures] crate. 8 | /// 9 | /// Initialize it using the [TaskConfig](crate::config::TasksConfig) inside the [MayConfig](crate::config::MayConfig). 10 | pub struct TaskRunner { 11 | pool: ThreadPool, 12 | } 13 | 14 | impl TaskRunner { 15 | /// Creates a new [TaskRunner] with the given thread stack size and worker/thread count. 16 | pub fn new(stack_size: usize, workers: NonZeroUsize) -> Result { 17 | Ok(Self { 18 | pool: ThreadPoolBuilder::new() 19 | .stack_size(stack_size) 20 | .pool_size(workers.get()) 21 | .name_prefix("maycoon-worker-") 22 | .create()?, 23 | }) 24 | } 25 | 26 | /// Runs the given [Future] on the task runner thread pool and returns the output. 27 | pub fn run(&self, fut: Fut) -> TaskHandle 28 | where 29 | Fut: Future + Send + 'static, 30 | Fut::Output: Send, 31 | { 32 | self.pool 33 | .spawn_with_handle(fut) 34 | .expect("Failed to spawn task") 35 | } 36 | 37 | /// Initialize the global task runner. This cannot be called twice. Will return [None] if the task runner is already initialized. 38 | pub fn init(self) -> Option<()> { 39 | RUNNER.set(self).ok() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /maycoon-core/src/widget.rs: -------------------------------------------------------------------------------- 1 | use vello::Scene; 2 | 3 | use crate::app::context::AppContext; 4 | use crate::app::info::AppInfo; 5 | use crate::app::update::Update; 6 | use crate::layout::{LayoutNode, LayoutStyle, StyleNode}; 7 | use crate::signal::MaybeSignal; 8 | use maycoon_theme::id::WidgetId; 9 | use maycoon_theme::theme::Theme; 10 | 11 | /// A boxed widget. 12 | pub type BoxedWidget = Box; 13 | 14 | /// The base trait for all widgets. 15 | pub trait Widget { 16 | /// Render the widget to the canvas. 17 | fn render( 18 | &mut self, 19 | scene: &mut Scene, 20 | theme: &mut dyn Theme, 21 | layout_node: &LayoutNode, 22 | info: &AppInfo, 23 | context: AppContext, 24 | ); 25 | 26 | /// Return the layout style node for layout computation. 27 | fn layout_style(&self) -> StyleNode; 28 | 29 | /// Update the widget state with given info and layout. Returns if the app should be updated. 30 | fn update(&mut self, layout: &LayoutNode, context: AppContext, info: &AppInfo) -> Update; 31 | 32 | /// Return the widget id. 33 | fn widget_id(&self) -> WidgetId; 34 | } 35 | 36 | /// An extension trait for widgets with a single child widget. 37 | pub trait WidgetChildExt { 38 | /// Sets the child widget of the widget. 39 | fn set_child(&mut self, child: impl Widget + 'static); 40 | 41 | /// Sets the child widget of the widget and returns self. 42 | fn with_child(mut self, child: impl Widget + 'static) -> Self 43 | where 44 | Self: Sized, 45 | { 46 | self.set_child(child); 47 | self 48 | } 49 | } 50 | 51 | /// An extension trait for widgets with multiple child widgets. 52 | pub trait WidgetChildrenExt { 53 | /// Sets the child widgets of the widget. 54 | fn set_children(&mut self, children: Vec); 55 | 56 | /// Sets the child widgets of the widget and returns self. 57 | fn with_children(mut self, children: Vec) -> Self 58 | where 59 | Self: Sized, 60 | { 61 | self.set_children(children); 62 | self 63 | } 64 | 65 | /// Adds a child widget to the widget. 66 | fn add_child(&mut self, child: impl Widget + 'static); 67 | 68 | /// Adds a child widget to the widget and returns self. 69 | fn with_child(mut self, child: impl Widget + 'static) -> Self 70 | where 71 | Self: Sized, 72 | { 73 | self.add_child(child); 74 | self 75 | } 76 | } 77 | 78 | /// An extension trait for widgets with a layout style. 79 | pub trait WidgetLayoutExt { 80 | /// Sets the layout style of the widget. 81 | fn set_layout_style(&mut self, layout_style: impl Into>); 82 | 83 | /// Sets the layout style of the widget and returns self. 84 | fn with_layout_style(mut self, layout_style: impl Into>) -> Self 85 | where 86 | Self: Sized, 87 | { 88 | self.set_layout_style(layout_style); 89 | self 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /maycoon-macros/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.4.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-macros-v0.3.2...maycoon-macros-v0.4.0) - 2025-04-29 11 | 12 | ### Other 13 | 14 | - Update syn to 2.0.101 15 | 16 | ## [0.3.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-macros-v0.1.0...maycoon-macros-v0.3.0) - 2025-01-26 17 | 18 | ### Other 19 | 20 | - Fix `clippy::doc_markdown` lints 21 | - Update syn 22 | - Update dependencies 23 | 24 | ## [0.1.0](https://github.com/maycoon-ui/maycoon/releases/tag/maycoon-macros-v0.1.0) - 2024-10-04 25 | 26 | ### Other 27 | 28 | - Add workspace keys 29 | - Rename crates and rework state value 30 | -------------------------------------------------------------------------------- /maycoon-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maycoon-macros" 3 | description = "Macros for Maycoon UI => See the `maycoon` crate for more." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | categories.workspace = true 9 | keywords.workspace = true 10 | repository.workspace = true 11 | homepage.workspace = true 12 | 13 | [dependencies] 14 | syn = "2.0.101" 15 | quote = "1.0.40" 16 | proc-macro2 = "1.0.95" 17 | ureq = "3.0.11" 18 | 19 | [lib] 20 | proc-macro = true 21 | -------------------------------------------------------------------------------- /maycoon-macros/README.md: -------------------------------------------------------------------------------- 1 | # Maycoon Macro Library 2 | 3 | This library contains macros for the `maycoon` crate. 4 | 5 | See the [official Website](https://maycoon-ui.github.io) for more information about Maycoon. 6 | -------------------------------------------------------------------------------- /maycoon-macros/src/assets.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use std::path::{Path, PathBuf}; 4 | use syn::{Expr, Lit}; 5 | 6 | #[inline] 7 | pub fn static_asset(input: TokenStream) -> TokenStream { 8 | let input: Expr = syn::parse2(input).expect("failed to parse input"); 9 | 10 | match input { 11 | Expr::Lit(expr_lit) => match expr_lit.lit { 12 | Lit::Str(lit) => { 13 | let lit = lit.value(); 14 | let source = Path::new(&lit); 15 | 16 | let file_name = source 17 | .file_name() 18 | .expect("Failed to get file name") 19 | .to_str() 20 | .unwrap(); 21 | 22 | let data = if source.starts_with("http") || source.starts_with("") { 23 | std::fs::read_to_string(source).expect("Failed to read file") 24 | } else { 25 | get_or_create_asset(Path::new("icons"), file_name, || { 26 | ureq::get(source.to_str().unwrap()) 27 | .call() 28 | .expect("Failed to download file") 29 | .into_body() 30 | .read_to_string() 31 | .expect("Failed to read file") 32 | }) 33 | }; 34 | 35 | let data = data.as_str(); 36 | 37 | quote! { 38 | #data 39 | } 40 | }, 41 | 42 | _ => panic!("Expected string literal"), 43 | }, 44 | 45 | _ => panic!("Expected literal"), 46 | } 47 | } 48 | 49 | pub fn temp_assets_folder() -> PathBuf { 50 | let path = std::env::temp_dir().join("maycoon-compilation-assets"); 51 | 52 | if !path.exists() { 53 | std::fs::create_dir_all(&path).expect("failed to create static assets directory"); 54 | } 55 | 56 | path 57 | } 58 | 59 | pub fn get_or_create_asset(path: &Path, name: &str, or_create: impl FnOnce() -> String) -> String { 60 | let asset_folder = temp_assets_folder().join(path); 61 | 62 | if !asset_folder.exists() { 63 | std::fs::create_dir_all(&asset_folder).expect("failed to create static assets directory"); 64 | } 65 | 66 | let asset_path = asset_folder.join(name); 67 | 68 | if asset_path.exists() { 69 | std::fs::read_to_string(&asset_path).expect("failed to read static asset") 70 | } else { 71 | let data = or_create(); 72 | 73 | std::fs::write(&asset_path, &data).expect("failed to write static asset"); 74 | 75 | data 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /maycoon-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! Macros for Maycoon => See `maycoon` crate. 4 | //! 5 | //! Contains procedural macros. 6 | 7 | mod assets; 8 | mod svg_icon; 9 | 10 | /// Create a new `SvgIcon` from the given SVG source. 11 | /// 12 | /// This is equivalent to `SvgIcon::new(static_asset!(url))` and works as a convenience macro. 13 | #[proc_macro] 14 | pub fn svg_icon(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 15 | proc_macro::TokenStream::from(svg_icon::svg_icon(proc_macro2::TokenStream::from(input))) 16 | } 17 | 18 | /// Creates a static asset from the given path/url and caches the file for faster compilation times. 19 | /// 20 | /// This will either read a file path or download the file from the given URL using [ureq]. 21 | /// After the data has been retrieved, it will be saved as a static asset file in a temporary directory (e.g. `%temp%` on windows). 22 | /// When re-executing this macro, the file can be re-loaded for faster compilation times. 23 | #[proc_macro] 24 | pub fn static_asset(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 25 | proc_macro::TokenStream::from(assets::static_asset(proc_macro2::TokenStream::from(input))) 26 | } 27 | -------------------------------------------------------------------------------- /maycoon-macros/src/svg_icon.rs: -------------------------------------------------------------------------------- 1 | use crate::assets; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use syn::LitStr; 5 | 6 | #[inline] 7 | pub fn svg_icon(input: TokenStream) -> TokenStream { 8 | let data = syn::parse2::(assets::static_asset(input)) 9 | .expect("failed to parse input") 10 | .value(); 11 | 12 | quote! { 13 | maycoon::widgets::icon::svg::SvgIcon::new(#data).expect("failed to create SVG icon") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /maycoon-theme/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.1](https://github.com/maycoon-ui/maycoon/compare/maycoon-theme-v0.3.0...maycoon-theme-v0.3.1) - 2025-04-19 11 | 12 | ### Other 13 | 14 | - Update Cargo.toml 15 | - Hopefully fix cargo issues 16 | 17 | ## [0.2.0](https://github.com/maycoon-ui/maycoon/compare/maycoon-theme-v0.1.0...maycoon-theme-v0.2.0) - 2025-01-26 18 | 19 | ### Other 20 | 21 | - Fix clippy lints 22 | - Fix `clippy::doc_markdown` lints 23 | - Update to Vello 0.4 24 | - Add GestureDetector widget and example 25 | - Add invalid ID 26 | - Replace dashmap with indexmap 27 | - release 28 | 29 | ## [0.1.0](https://github.com/maycoon-ui/maycoon/releases/tag/maycoon-theme-v0.1.0) - 2024-10-04 30 | 31 | ### Other 32 | 33 | - Update id.rs 34 | - Add workspace keys 35 | - Add slider widget 36 | - Add checkbox widget and example 37 | - New Themes 38 | - Rename crates and rework state value 39 | -------------------------------------------------------------------------------- /maycoon-theme/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maycoon-theme" 3 | description = "Themes & Styling for Maycoon UI => See the `maycoon` crate for more." 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | authors.workspace = true 8 | categories.workspace = true 9 | keywords.workspace = true 10 | repository.workspace = true 11 | homepage.workspace = true 12 | 13 | [dependencies] 14 | indexmap = { workspace = true } 15 | peniko = { workspace = true } 16 | -------------------------------------------------------------------------------- /maycoon-theme/README.md: -------------------------------------------------------------------------------- 1 | # Maycoon Theme Library 2 | 3 | This library contains themes & styling utilities for the `maycoon` crate. 4 | 5 | See the [official Website](https://maycoon-ui.github.io) for more information about Maycoon. 6 | -------------------------------------------------------------------------------- /maycoon-theme/src/globals.rs: -------------------------------------------------------------------------------- 1 | /// Global theme values for all widgets to use. 2 | /// 3 | /// This may be used to invert the text color of widgets that are inside a container. 4 | #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] 5 | pub struct Globals { 6 | /// Invert text color. 7 | pub invert_text_color: bool, 8 | } 9 | -------------------------------------------------------------------------------- /maycoon-theme/src/id.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | 3 | /// An identifier for a widget. This is not for instantiated widgets, but for the widget types in general. 4 | /// It contains a namespace, which should be the crate name and the id of the widget. 5 | /// 6 | /// ``` 7 | /// use maycoon_theme::id::WidgetId; 8 | /// 9 | /// WidgetId::new("fancy_text_widget", "FancyText"); 10 | /// ``` 11 | #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] 12 | pub struct WidgetId { 13 | namespace: String, 14 | id: String, 15 | } 16 | 17 | impl WidgetId { 18 | /// Create a new widget id by a namespace and custom id. 19 | /// The namespace should be the crate name and the id should be the widget type name. 20 | /// 21 | /// Example: 22 | /// ``` 23 | /// let id = maycoon_theme::id::WidgetId::new("my_crate", "MyWidget"); 24 | /// ``` 25 | pub fn new(namespace: impl ToString, id: impl ToString) -> Self { 26 | Self { 27 | namespace: namespace.to_string(), 28 | id: id.to_string(), 29 | } 30 | } 31 | 32 | /// Returns the namespace of the widget id. 33 | pub fn namespace(&self) -> &str { 34 | &self.namespace 35 | } 36 | 37 | /// Returns the actual widget id. 38 | pub fn id(&self) -> &str { 39 | &self.id 40 | } 41 | } 42 | 43 | impl Display for WidgetId { 44 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 45 | write!(f, "{}:{}", self.namespace, self.id) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /maycoon-theme/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! Theme/Styling library for Maycoon => See `maycoon` crate. 4 | //! 5 | //! Contains themes and widget styles. 6 | 7 | /// Contains the [globals::Globals] struct. 8 | pub mod globals; 9 | /// Contains the [id::WidgetId] struct. 10 | pub mod id; 11 | /// Contains styling structures. 12 | pub mod style; 13 | /// Contains the [theme::Theme] trait and built-in themes. 14 | pub mod theme; 15 | -------------------------------------------------------------------------------- /maycoon-theme/src/style.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use peniko::{Brush, Color, Gradient}; 3 | 4 | /// Styling map for defining widget appearance. 5 | #[derive(Clone, Debug)] 6 | pub struct Style { 7 | map: IndexMap, 8 | } 9 | 10 | impl Style { 11 | /// Create a new empty style. 12 | pub fn new() -> Self { 13 | Self { 14 | map: IndexMap::with_capacity(16), 15 | } 16 | } 17 | 18 | /// Create a style from an array of strings and style values. 19 | pub fn from_values(values: impl IntoIterator) -> Self { 20 | Self { 21 | map: IndexMap::from_iter(values), 22 | } 23 | } 24 | 25 | /// Removes the style value from the map with the give name. 26 | pub fn remove(&mut self, name: impl ToString) { 27 | self.map.swap_remove(&name.to_string()); 28 | } 29 | 30 | /// Insert a style value with the given name into the style map. 31 | pub fn with_value(mut self, name: impl ToString, value: StyleVal) -> Self { 32 | self.map.insert(name.to_string(), value); 33 | self 34 | } 35 | 36 | /// Set a style value by name. 37 | pub fn set(&mut self, name: impl ToString, value: StyleVal) { 38 | self.map.insert(name.to_string(), value); 39 | } 40 | 41 | /// Set a color style value by name. 42 | pub fn set_color(&mut self, name: impl ToString, color: Color) { 43 | self.map.insert(name.to_string(), StyleVal::Color(color)); 44 | } 45 | 46 | /// Set a gradient style value by name. 47 | pub fn set_gradient(&mut self, name: impl ToString, gradient: Gradient) { 48 | self.map 49 | .insert(name.to_string(), StyleVal::Gradient(gradient)); 50 | } 51 | 52 | /// Set a bool style value by name. 53 | pub fn set_bool(&mut self, name: impl ToString, value: bool) { 54 | self.map.insert(name.to_string(), StyleVal::Bool(value)); 55 | } 56 | 57 | /// Set a brush style value by name. 58 | pub fn set_brush(&mut self, name: impl ToString, brush: Brush) { 59 | self.map.insert(name.to_string(), StyleVal::Brush(brush)); 60 | } 61 | 62 | /// Set a float style value by name. 63 | pub fn set_float(&mut self, name: impl ToString, value: f32) { 64 | self.map.insert(name.to_string(), StyleVal::Float(value)); 65 | } 66 | 67 | /// Set an int style value by name. 68 | pub fn set_int(&mut self, name: impl ToString, value: i32) { 69 | self.map.insert(name.to_string(), StyleVal::Int(value)); 70 | } 71 | 72 | /// Set an unsized int style value by name. 73 | pub fn set_uint(&mut self, name: impl ToString, value: u32) { 74 | self.map.insert(name.to_string(), StyleVal::UInt(value)); 75 | } 76 | 77 | /// Get a style value by name. Returns [None] if the value name does not exist. 78 | pub fn get(&self, name: impl ToString) -> Option { 79 | self.map.get(&name.to_string()).cloned() 80 | } 81 | 82 | /// Get a color style value by name. Returns [None] if the value name does not exist. 83 | pub fn get_color(&self, name: impl ToString) -> Option { 84 | if let Some(val) = self.map.get(&name.to_string()) { 85 | match val { 86 | StyleVal::Color(color) => Some(*color), 87 | _ => None, 88 | } 89 | } else { 90 | None 91 | } 92 | } 93 | 94 | /// Get a gradient style value by name. Returns [None] if the value name does not exist. 95 | pub fn get_gradient(&self, name: impl ToString) -> Option { 96 | if let Some(val) = self.map.get(&name.to_string()) { 97 | match val { 98 | StyleVal::Gradient(gradient) => Some(gradient.clone()), 99 | _ => None, 100 | } 101 | } else { 102 | None 103 | } 104 | } 105 | 106 | /// Get a brush style value by name. Returns [None] if the value name does not exist. 107 | pub fn get_brush(&self, name: impl ToString) -> Option { 108 | if let Some(val) = self.map.get(&name.to_string()) { 109 | match val { 110 | StyleVal::Brush(brush) => Some(brush.clone()), 111 | _ => None, 112 | } 113 | } else { 114 | None 115 | } 116 | } 117 | 118 | /// Get a float style value by name. Returns [None] if the value name does not exist. 119 | pub fn get_float(&self, name: impl ToString) -> Option { 120 | if let Some(val) = self.map.get(&name.to_string()) { 121 | match val { 122 | StyleVal::Float(float) => Some(*float), 123 | _ => None, 124 | } 125 | } else { 126 | None 127 | } 128 | } 129 | 130 | /// Get an int style value by name. Returns [None] if the value name does not exist. 131 | pub fn get_int(&self, name: impl ToString) -> Option { 132 | if let Some(val) = self.map.get(&name.to_string()) { 133 | match val { 134 | StyleVal::Int(int) => Some(*int), 135 | _ => None, 136 | } 137 | } else { 138 | None 139 | } 140 | } 141 | 142 | /// Get an unsized int style value by name. Returns [None] if the value name does not exist. 143 | pub fn get_uint(&self, name: impl ToString) -> Option { 144 | if let Some(val) = self.map.get(&name.to_string()) { 145 | match val { 146 | StyleVal::UInt(uint) => Some(*uint), 147 | _ => None, 148 | } 149 | } else { 150 | None 151 | } 152 | } 153 | 154 | /// Get a bool style value by name. Returns [None] if the value name does not exist. 155 | pub fn get_bool(&self, name: impl ToString) -> Option { 156 | if let Some(val) = self.map.get(&name.to_string()) { 157 | match val { 158 | StyleVal::Bool(bool) => Some(*bool), 159 | _ => None, 160 | } 161 | } else { 162 | None 163 | } 164 | } 165 | } 166 | 167 | impl Default for Style { 168 | fn default() -> Self { 169 | Self::new() 170 | } 171 | } 172 | 173 | /// Default widget styles. 174 | #[derive(Clone, PartialEq, Debug)] 175 | pub struct DefaultStyles { 176 | text: DefaultTextStyles, 177 | container: DefaultContainerStyles, 178 | interactive: DefaultInteractiveStyles, 179 | } 180 | 181 | impl DefaultStyles { 182 | /// Create new default styles with given styles. 183 | pub fn new( 184 | text: DefaultTextStyles, 185 | container: DefaultContainerStyles, 186 | interactive: DefaultInteractiveStyles, 187 | ) -> Self { 188 | Self { 189 | text, 190 | container, 191 | interactive, 192 | } 193 | } 194 | 195 | /// Get the default styles for text widgets. 196 | pub fn text(&self) -> &DefaultTextStyles { 197 | &self.text 198 | } 199 | 200 | /// Get the default styles for container widgets. 201 | pub fn container(&self) -> &DefaultContainerStyles { 202 | &self.container 203 | } 204 | 205 | /// Get the default styles for interactive widgets. 206 | pub fn interactive(&self) -> &DefaultInteractiveStyles { 207 | &self.interactive 208 | } 209 | } 210 | 211 | /// The default text widget styles. 212 | #[derive(Clone, PartialEq, Debug)] 213 | pub struct DefaultTextStyles { 214 | foreground: Color, 215 | background: Color, 216 | } 217 | 218 | impl DefaultTextStyles { 219 | /// Create new default text styles with given colors. 220 | pub fn new(foreground: Color, background: Color) -> Self { 221 | Self { 222 | foreground, 223 | background, 224 | } 225 | } 226 | 227 | /// Get the default foreground color. 228 | pub fn foreground(&self) -> Color { 229 | self.foreground 230 | } 231 | 232 | /// Get the default background color. 233 | pub fn background(&self) -> Color { 234 | self.background 235 | } 236 | } 237 | 238 | /// The default container widget styles. 239 | #[derive(Clone, PartialEq, Debug)] 240 | pub struct DefaultContainerStyles { 241 | foreground: Color, 242 | background: Color, 243 | } 244 | 245 | impl DefaultContainerStyles { 246 | /// Create new default container styles with given colors. 247 | pub fn new(foreground: Color, background: Color) -> Self { 248 | Self { 249 | foreground, 250 | background, 251 | } 252 | } 253 | 254 | /// Get the default foreground color. 255 | pub fn foreground(&self) -> Color { 256 | self.foreground 257 | } 258 | 259 | /// Get the default background color. 260 | pub fn background(&self) -> Color { 261 | self.background 262 | } 263 | } 264 | 265 | /// The default interactive widget styles. 266 | #[derive(Clone, PartialEq, Debug)] 267 | pub struct DefaultInteractiveStyles { 268 | active: Color, 269 | inactive: Color, 270 | hover: Color, 271 | disabled: Color, 272 | } 273 | 274 | impl DefaultInteractiveStyles { 275 | /// Create new default interactive styles with given colors. 276 | pub fn new(active: Color, inactive: Color, hover: Color, disabled: Color) -> Self { 277 | Self { 278 | active, 279 | inactive, 280 | hover, 281 | disabled, 282 | } 283 | } 284 | 285 | /// Get the default active widget color. 286 | pub fn active(&self) -> Color { 287 | self.active 288 | } 289 | 290 | /// Get the default inactive widget color. 291 | pub fn inactive(&self) -> Color { 292 | self.inactive 293 | } 294 | 295 | /// Get the default on-hover widget color. 296 | pub fn hover(&self) -> Color { 297 | self.hover 298 | } 299 | 300 | /// Get the default disabled widget color. 301 | pub fn disabled(&self) -> Color { 302 | self.disabled 303 | } 304 | } 305 | 306 | /// A style value. 307 | #[derive(Clone, Debug)] 308 | pub enum StyleVal { 309 | /// A color style value. 310 | Color(Color), 311 | /// A gradient style value. 312 | Gradient(Gradient), 313 | /// A brush style value. 314 | Brush(Brush), 315 | /// A float style value. 316 | Float(f32), 317 | /// An int style value. 318 | Int(i32), 319 | /// An unsized int style value. 320 | UInt(u32), 321 | /// A bool style value. 322 | Bool(bool), 323 | } 324 | -------------------------------------------------------------------------------- /maycoon-theme/src/theme/celeste.rs: -------------------------------------------------------------------------------- 1 | use peniko::{color::palette, Color}; 2 | 3 | use crate::globals::Globals; 4 | use crate::id::WidgetId; 5 | use crate::style::{ 6 | DefaultContainerStyles, DefaultInteractiveStyles, DefaultStyles, DefaultTextStyles, Style, 7 | StyleVal, 8 | }; 9 | use crate::theme::Theme; 10 | 11 | /// A smooth and minimalistic theme with a cold blue and purple touch. 12 | #[derive(Debug, Clone)] 13 | pub enum CelesteTheme { 14 | /// Use [CelesteTheme::light] to use the light Celeste theme. 15 | Light(Globals), 16 | } 17 | 18 | impl CelesteTheme { 19 | /// The Light Celeste Theme. 20 | pub fn light() -> Self { 21 | Self::Light(Globals::default()) 22 | } 23 | } 24 | 25 | impl Default for CelesteTheme { 26 | fn default() -> Self { 27 | Self::light() 28 | } 29 | } 30 | 31 | impl Theme for CelesteTheme { 32 | fn of(&self, id: WidgetId) -> Option