diff --git a/.cargo/config.toml b/.cargo/config.toml index bac83fe..be614b0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,4 +3,4 @@ # check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility # we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93 [target.wasm32-unknown-unknown] -rustflags = ["--cfg=web_sys_unstable_apis"] \ No newline at end of file +rustflags = ["--cfg=web_sys_unstable_apis"] diff --git a/.cz.json b/.cz.json new file mode 100644 index 0000000..deb922d --- /dev/null +++ b/.cz.json @@ -0,0 +1,10 @@ +{ + "commitizen": { + "name": "cz_conventional_commits", + "tag_format": "$version", + "version_scheme": "semver", + "version_provider": "cargo", + "update_changelog_on_bump": true, + "major_version_zero": true + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..18a3239 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: +- hooks: + - id: commitizen + stages: + - commit-msg + repo: https://github.com/commitizen-tools/commitizen + rev: v3.6.0 +- hooks: + - id: fmt + - id: cargo-check + - id: clippy + repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 +- hooks: + - id: gitleaks + repo: https://github.com/gitleaks/gitleaks + rev: v8.17.0 +- hooks: + - id: end-of-file-fixer + - args: + - --markdown-linebreak-ext=md + id: trailing-whitespace + repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..a311b9d --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2021" +newline_style = "Unix" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..668aa49 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behaviour that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologising to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behaviour include: + +- The use of sexualised language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behaviour and will take appropriate and fair corrective action in +response to any behaviour that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +ask@rodneylab.com. All complaints will be reviewed and investigated promptly and +fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behaviour deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behaviour was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behaviour. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behaviour. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behaviour, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Cargo.lock b/Cargo.lock index 09d9b1f..8a901fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arboard" version = "3.2.0" @@ -499,6 +514,34 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.0", +] + +[[package]] +name = "cistercian_clock" +version = "0.1.0" +dependencies = [ + "chrono", + "eframe", + "egui", + "env_logger", + "image", + "log", + "serde", + "wasm-bindgen-futures", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -750,18 +793,6 @@ dependencies = [ "winit", ] -[[package]] -name = "eframe_template" -version = "0.1.0" -dependencies = [ - "eframe", - "egui", - "env_logger", - "log", - "serde", - "wasm-bindgen-futures", -] - [[package]] name = "egui" version = "0.24.1" @@ -1177,6 +1208,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.3.0" @@ -1189,9 +1243,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2480,6 +2534,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-implement" version = "0.48.0" @@ -2550,6 +2613,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2562,6 +2640,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2574,6 +2658,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2586,6 +2676,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2598,6 +2694,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2610,6 +2712,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2622,6 +2730,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2634,6 +2748,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winit" version = "0.28.6" diff --git a/Cargo.toml b/Cargo.toml index e0df3d7..f695cc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "eframe_template" +name = "cistercian_clock" version = "0.1.0" -authors = ["Emil Ernerfeldt "] +authors = ["Rodney Johnson "] edition = "2021" rust-version = "1.72" - [dependencies] -egui = "0.24.1" -eframe = { version = "0.24.1", default-features = false, features = [ - "accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. - "default_fonts", # Embed the default egui fonts. - "glow", # Use the glow rendering backend. Alternative: "wgpu". - "persistence", # Enable restoring app state when restarting the app. +chrono = "0.4.31" +eframe = { version = "0.24.1", default-features = false, features = ["accesskit", # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies. + "default_fonts", # Embed the default egui fonts. + "glow", # Use the glow rendering backend. Alternative: "wgpu". + "persistence", # Enable restoring app state when restarting the app. ] } +egui = "0.24.1" +image = { version = "0.24.7", default-features = false } log = "0.4" # You only need serde if you want app persistence: @@ -27,7 +27,6 @@ env_logger = "0.10" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" - [profile.release] opt-level = 2 # fast and small wasm @@ -35,9 +34,7 @@ opt-level = 2 # fast and small wasm [profile.dev.package."*"] opt-level = 2 - [patch.crates-io] - # If you want to use the bleeding edge version of egui and eframe: # egui = { git = "https://github.com/emilk/egui", branch = "master" } # eframe = { git = "https://github.com/emilk/egui", branch = "master" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..24e6529 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, Rodney Johnson +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 549b18b..397790f 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,30 @@ -# eframe template +Rodney Lab cistercian clock Github banner -[![dependency status](https://deps.rs/repo/github/emilk/eframe_template/status.svg)](https://deps.rs/repo/github/emilk/eframe_template) -[![Build Status](https://github.com/emilk/eframe_template/workflows/CI/badge.svg)](https://github.com/emilk/eframe_template/actions?workflow=CI) +

+ + Rodney Lab logo + +

+

+ Cistercian Clock +

-This is a template repo for [eframe](https://github.com/emilk/egui/tree/master/crates/eframe), a framework for writing apps using [egui](https://github.com/emilk/egui/). +# cistercian-clock -The goal is for this to be the simplest way to get started writing a GUI app in Rust. +Trying out egui Rust immediate mode tooling by building a Cistercian clock. This +code accompanies the blog post on +trying egui. If you have any questions, please drop a comment at the bottom +of that page. -You can compile your app natively or for the web, and share it using Github Pages. +## Usage -## Getting started +Clone the repo then run: -Start by clicking "Use this template" at https://github.com/emilk/eframe_template/ or follow [these instructions](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template). +```shell +cargo run +``` -Change the name of the crate: Chose a good name for your project, and change the name to it in: -* `Cargo.toml` - * Change the `package.name` from `eframe_template` to `your_crate`. - * Change the `package.authors` -* `main.rs` - * Change `eframe_template::TemplateApp` to `your_crate::TemplateApp` -* `index.html` - * Change the `eframe template` to `your_crate`. optional. -* `assets/sw.js` - * Change the `'./eframe_template.js'` to `./your_crate.js` (in `filesToCache` array) - * Change the `'./eframe_template_bg.wasm'` to `./your_crate_bg.wasm` (in `filesToCache` array) +## Issues -### Learning about egui - -`src/app.rs` contains a simple example app. This is just to give some inspiration - most of it can be removed if you like. - -The official egui docs are at . If you prefer watching a video introduction, check out . For inspiration, check out the [the egui web demo](https://emilk.github.io/egui/index.html) and follow the links in it to its source code. - -### Testing locally - -Make sure you are using the latest version of stable rust by running `rustup update`. - -`cargo run --release` - -On Linux you need to first run: - -`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev` - -On Fedora Rawhide you need to run: - -`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel gtk3-devel atk fontconfig-devel` - -### Web Locally - -You can compile your app to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and publish it as a web page. - -We use [Trunk](https://trunkrs.dev/) to build for web target. -1. Install the required target with `rustup target add wasm32-unknown-unknown`. -2. Install Trunk with `cargo install --locked trunk`. -3. Run `trunk serve` to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project. -4. Open `http://127.0.0.1:8080/index.html#dev` in a browser. See the warning below. - -> `assets/sw.js` script will try to cache our app, and loads the cached version when it cannot connect to server allowing your app to work offline (like PWA). -> appending `#dev` to `index.html` will skip this caching, allowing us to load the latest builds during development. - -### Web Deploy -1. Just run `trunk build --release`. -2. It will generate a `dist` directory as a "static html" website -3. Upload the `dist` directory to any of the numerous free hosting websites including [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site). -4. we already provide a workflow that auto-deploys our app to GitHub pages if you enable it. -> To enable Github Pages, you need to go to Repository -> Settings -> Pages -> Source -> set to `gh-pages` branch and `/` (root). -> -> If `gh-pages` is not available in `Source`, just create and push a branch called `gh-pages` and it should be available. - -You can test the template app at . - -## Updating egui - -As of 2023, egui is in active development with frequent releases with breaking changes. [eframe_template](https://github.com/emilk/eframe_template/) will be updated in lock-step to always use the latest version of egui. - -When updating `egui` and `eframe` it is recommended you do so one version at the time, and read about the changes in [the egui changelog](https://github.com/emilk/egui/blob/master/CHANGELOG.md) and [eframe changelog](https://github.com/emilk/egui/blob/master/crates/eframe/CHANGELOG.md). +Feel free to jump into the +[Rodney Lab matrix chat room](https://matrix.to/#/%23rodney:matrix.org). diff --git a/assets/icon-256.png b/assets/icon-256.png index ae72287..c34f230 100644 Binary files a/assets/icon-256.png and b/assets/icon-256.png differ diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..c333fe4 --- /dev/null +++ b/dprint.json @@ -0,0 +1,18 @@ +{ + "json": { + }, + "markdown": { + }, + "toml": { + }, + "includes": ["**/*.{json,md,toml}"], + "excludes": [ + "**/*-lock.json", + "target" + ], + "plugins": [ + "https://plugins.dprint.dev/json-0.17.4.wasm", + "https://plugins.dprint.dev/markdown-0.16.1.wasm", + "https://plugins.dprint.dev/toml-0.5.4.wasm" + ] +} diff --git a/images/rodneylab-github-cistercian-clock.png b/images/rodneylab-github-cistercian-clock.png new file mode 100644 index 0000000..dfb1e1a Binary files /dev/null and b/images/rodneylab-github-cistercian-clock.png differ diff --git a/src/app.rs b/src/app.rs index 18dd259..3f852e1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,7 +1,92 @@ +use chrono::{Local, Timelike}; +use core::time::Duration; +use egui::{ + epaint::Shadow, + scroll_area::ScrollBarVisibility, + style::{HandleShape, Selection, Widgets}, + vec2, Color32, + FontFamily::Proportional, + FontId, Painter, Pos2, Rounding, ScrollArea, Sense, Stroke, + TextStyle::{self, Body, Button, Heading, Monospace, Name, Small}, + Ui, Vec2, Visuals, +}; + +fn dark_mode_override() -> Visuals { + Visuals { + dark_mode: true, + //override_text_color: None, + override_text_color: Some(Color32::from_gray(252)), + widgets: Widgets::default(), + selection: Selection::default(), + hyperlink_color: Color32::from_rgb(90, 170, 255), + faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so + extreme_bg_color: Color32::from_gray(10), // e.g. TextEdit background + code_bg_color: Color32::from_gray(64), + warn_fg_color: Color32::from_rgb(255, 143, 0), // orange + error_fg_color: Color32::from_rgb(255, 0, 0), // red + + window_rounding: Rounding::same(6.0), + window_shadow: Shadow::big_dark(), + //window_fill: Color32::from_gray(27), + window_fill: Color32::from_rgb(23, 18, 25), + window_stroke: Stroke::new(1.0, Color32::from_gray(60)), + + menu_rounding: Rounding::same(6.0), + + //panel_fill: Color32::from_gray(27), + panel_fill: Color32::from_rgb(23, 18, 25), + + popup_shadow: Shadow::small_dark(), + resize_corner_size: 12.0, + text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)), + text_cursor_preview: false, + clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion + button_frame: true, + collapsing_header_frame: false, + indent_has_left_vline: true, + + striped: false, + + slider_trailing_fill: false, + + handle_shape: HandleShape::Rect { aspect_ratio: 1.0 }, + + interact_cursor: None, + + image_loading_spinners: true, + } +} + +pub fn light_mode_override() -> Visuals { + Visuals { + dark_mode: false, + override_text_color: Some(Color32::from_rgb(4, 3, 15)), + widgets: Widgets::light(), + //selection: Selection::light(), + selection: Selection::default(), + hyperlink_color: Color32::from_rgb(0, 155, 255), + faint_bg_color: Color32::from_additive_luminance(5), // visible, but barely so + extreme_bg_color: Color32::from_gray(255), // e.g. TextEdit background + code_bg_color: Color32::from_gray(230), + warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background. + error_fg_color: Color32::from_rgb(255, 0, 0), // red + + window_shadow: Shadow::big_light(), + window_fill: Color32::from_gray(255), + window_stroke: Stroke::new(1.0, Color32::from_gray(190)), + + panel_fill: Color32::from_gray(255), + + popup_shadow: Shadow::small_light(), + text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)), + ..Visuals::dark() + } +} + /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] // if we add new fields, give them default values when deserializing old state -pub struct TemplateApp { +pub struct CistercianClockApp { // Example stuff: label: String, @@ -9,7 +94,7 @@ pub struct TemplateApp { value: f32, } -impl Default for TemplateApp { +impl Default for CistercianClockApp { fn default() -> Self { Self { // Example stuff: @@ -19,7 +104,7 @@ impl Default for TemplateApp { } } -impl TemplateApp { +impl CistercianClockApp { /// Called once before the first frame. pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // This is also where you can customize the look and feel of egui using @@ -35,7 +120,371 @@ impl TemplateApp { } } -impl eframe::App for TemplateApp { +struct Colours { + colour_0: Color32, + colour_1: Color32, + colour_2: Color32, + colour_3: Color32, + colour_4: Color32, + colour_6: Color32, +} + +fn paint_unit_number( + painter: &mut Painter, + centre: Pos2, + scale: f32, + colours: &Colours, + number: u32, +) { + let width = scale * (34.0 / 2.0 - 1.0); + let stroke_width = if scale < 2.0 { 2.0 } else { scale * 1.0 }; + let Colours { + colour_0, + colour_1, + colour_2, + colour_3, + colour_4, + colour_6, + } = colours; + let stroke = Stroke::new(stroke_width, *colour_0); + painter.line_segment( + [centre - vec2(0.0, width), centre + vec2(0.0, width)], + stroke, + ); + + if number == 1 || number == 5 || number == 7 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_1); + painter.line_segment( + [ + centre - vec2(0.0, width), + centre + vec2(scale * 10.0, -width), + ], + stroke, + ); + } + + if number == 2 || number == 8 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_2); + painter.line_segment( + [ + centre - vec2(0.0, width - scale * 10.0), + centre + vec2(scale * 10.0, -width + (scale * 10.0)), + ], + stroke, + ); + } + + if number == 3 { + let stroke = Stroke::new(stroke_width, *colour_3); + painter.line_segment( + [ + centre - vec2(0.0, width), + centre + vec2(scale * 10.0, -width + scale * 10.0), + ], + stroke, + ); + } + + if number == 4 || number == 5 { + let stroke = Stroke::new(stroke_width, *colour_4); + painter.line_segment( + [ + centre - vec2(0.0, width - scale * 10.0), + centre + vec2(scale * 10.0, -width), + ], + stroke, + ); + } + + if number > 5 { + let stroke = Stroke::new(stroke_width, *colour_6); + painter.line_segment( + [ + centre + vec2(scale * 10.0, -width), + centre + vec2(scale * 10.0, -width + scale * 10.0), + ], + stroke, + ); + } +} + +fn paint_tens_number( + painter: &mut Painter, + centre: Pos2, + scale: f32, + colours: &Colours, + number: u32, +) { + let width = scale * (34.0 / 2.0 - 1.0); + let stroke_width = if scale < 2.0 { 2.0 } else { scale * 1.0 }; + let Colours { + colour_1, + colour_2, + colour_3, + colour_4, + colour_6, + .. + } = colours; + + if number == 1 || number == 5 || number == 7 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_1); + painter.line_segment( + [ + centre - vec2(scale * 10.0, width), + centre + vec2(0.0, -width), + ], + stroke, + ); + } + if number == 2 || number == 8 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_2); + painter.line_segment( + [ + centre - vec2(scale * 10.0, width - scale * 10.0), + centre + vec2(0.0, -width + scale * 10.0), + ], + stroke, + ); + } + if number == 3 { + let stroke = Stroke::new(stroke_width, *colour_3); + painter.line_segment( + [ + centre - vec2(scale * 10.0, width - scale * 10.0), + centre + vec2(0.0, -width), + ], + stroke, + ); + } + if number == 4 || number == 5 { + let stroke = Stroke::new(stroke_width, *colour_4); + painter.line_segment( + [ + centre - vec2(scale * 10.0, width), + centre + vec2(0.0, -width + scale * 10.0), + ], + stroke, + ); + } + if number > 5 { + let stroke = Stroke::new(stroke_width, *colour_6); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, -width), + centre + vec2(-scale * 10.0, -width + scale * 10.0), + ], + stroke, + ); + } +} + +fn paint_hundreds_number( + painter: &mut Painter, + centre: Pos2, + scale: f32, + colours: &Colours, + number: u32, +) { + let width = scale * (34.0 / 2.0 - 1.0); + let stroke_width = if scale < 2.0 { 2.0 } else { scale * 1.0 }; + let Colours { + colour_1, + colour_2, + colour_3, + colour_4, + colour_6, + .. + } = colours; + + if number == 1 || number == 5 || number == 7 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_1); + painter.line_segment( + [ + centre + vec2(0.0, width), + centre + vec2(scale * 10.0, width), + ], + stroke, + ); + } + if number == 2 || number == 8 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_2); + painter.line_segment( + [ + centre + vec2(0.0, width - scale * 10.0), + centre + vec2(scale * 10.0, width - scale * 10.0), + ], + stroke, + ); + } + if number == 3 { + let stroke = Stroke::new(stroke_width, *colour_3); + painter.line_segment( + [ + centre + vec2(0.0, width), + centre + vec2(scale * 10.0, width - scale * 10.0), + ], + stroke, + ); + } + if number == 4 || number == 5 { + let stroke = Stroke::new(stroke_width, *colour_4); + painter.line_segment( + [ + centre + vec2(0.0, width - scale * 10.0), + centre + vec2(scale * 10.0, width), + ], + stroke, + ); + } + if number > 5 { + let stroke = Stroke::new(stroke_width, *colour_6); + painter.line_segment( + [ + centre + vec2(scale * 10.0, width - scale * 10.0), + centre + vec2(scale * 10.0, width), + ], + stroke, + ); + } +} + +fn paint_thousands_number( + painter: &mut Painter, + centre: Pos2, + scale: f32, + colours: &Colours, + number: u32, +) { + let width = scale * (34.0 / 2.0 - 1.0); + let stroke_width = if scale < 2.0 { 2.0 } else { scale * 1.0 }; + let Colours { + colour_1, + colour_2, + colour_3, + colour_4, + colour_6, + .. + } = colours; + + if number == 1 || number == 5 || number == 7 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_1); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, width), + centre + vec2(0.0, width), + ], + stroke, + ); + } + if number == 2 || number == 8 || number == 9 { + let stroke = Stroke::new(stroke_width, *colour_2); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, width - scale * 10.0), + centre + vec2(0.0, width - scale * 10.0), + ], + stroke, + ); + } + if number == 3 { + let stroke = Stroke::new(stroke_width, *colour_3); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, width - scale * 10.0), + centre + vec2(0.0, width), + ], + stroke, + ); + } + if number == 4 || number == 5 { + let stroke = Stroke::new(stroke_width, *colour_4); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, width), + centre + vec2(0.0, width - scale * 10.0), + ], + stroke, + ); + } + if number > 5 { + let stroke = Stroke::new(stroke_width, *colour_6); + painter.line_segment( + [ + centre + vec2(-scale * 10.0, width - scale * 10.0), + centre + vec2(-scale * 10.0, width), + ], + stroke, + ); + } +} + +fn paint_number( + ui: &mut Ui, + colours: &Colours, + number: u32, + scale: Option, + show_arabic_numeral: Option, +) { + let scale = if let Some(value) = scale { value } else { 1.0 }; + assert!((0..=9_999).contains(&number)); + match show_arabic_numeral { + Some(true) => { + match number { + 0..=999 => ui.label(number.to_string()), + _ => ui.label(format!("{},{:003}", number / 1000, number % 1000)), + }; + } + None | Some(false) => {} + } + + let size = Vec2::splat(scale * 34.0); + let (response, mut painter) = ui.allocate_painter(size, Sense::hover()); + let rect = response.rect; + let c = rect.center(); + + let unit = number % 10; + paint_unit_number(&mut painter, c, scale, colours, unit); + if number > 9 { + let tens = (number % 100) / 10; + paint_tens_number(&mut painter, c, scale, colours, tens); + } + if number > 99 { + let hundreds = (number % 1_000) / 100; + paint_hundreds_number(&mut painter, c, scale, colours, hundreds); + } + if number > 999 { + let thousands = (number % 10_000) / 1_000; + paint_thousands_number(&mut painter, c, scale, colours, thousands); + } +} + +const DARK_CISTERCIAN_NUMERAL_COLOURS: Colours = Colours { + colour_0: Color32::from_gray(242), + colour_1: Color32::from_rgb(58, 134, 255), + colour_2: Color32::from_rgb(251, 86, 7), + colour_3: Color32::from_rgb(162, 106, 241), + colour_4: Color32::from_rgb(255, 0, 110), + colour_6: Color32::from_rgb(255, 190, 11), +}; + +const LIGHT_CISTERCIAN_NUMERAL_COLOURS: Colours = Colours { + colour_0: Color32::from_rgb(4, 3, 15), + colour_1: Color32::from_rgb(93, 93, 91), + colour_2: Color32::from_rgb(0, 122, 94), + colour_3: Color32::from_rgb(27, 42, 65), + colour_4: Color32::from_rgb(150, 2, 0), + colour_6: Color32::from_rgb(0, 122, 163), +}; + +fn paint_number_row(ui: &mut Ui, colours: &Colours, start: u32, end: u32) { + ui.horizontal(|ui| { + for number in start..end { + ui.horizontal_top(|ui| paint_number(ui, colours, number, None, Some(true))); + } + }); +} + +impl eframe::App for CistercianClockApp { /// Called by the frame work to save state before shutdown. fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage, eframe::APP_KEY, self); @@ -43,10 +492,28 @@ impl eframe::App for TemplateApp { /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. + // Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. // For inspiration and more examples, go to https://emilk.github.io/egui + let mut style = (*ctx.style()).clone(); + style.text_styles = [ + (Heading, FontId::new(30.0, Proportional)), + (Name("clock".into()), FontId::new(64.0, Proportional)), + (Body, FontId::new(18.0, Proportional)), + (Monospace, FontId::new(14.0, Proportional)), + (Button, FontId::new(14.0, Proportional)), + (Small, FontId::new(10.0, Proportional)), + ] + .into(); + ctx.set_style(style); + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + if ui.visuals().dark_mode { + ctx.set_visuals(dark_mode_override()); + } else { + ctx.set_visuals(light_mode_override()); + }; + // The top panel is often a good place for a menu bar: egui::menu::bar(ui, |ui| { @@ -61,35 +528,78 @@ impl eframe::App for TemplateApp { ui.add_space(16.0); } - egui::widgets::global_dark_light_mode_buttons(ui); + egui::widgets::global_dark_light_mode_switch(ui); }); }); egui::CentralPanel::default().show(ctx, |ui| { + let colours = if ui.visuals().dark_mode { + DARK_CISTERCIAN_NUMERAL_COLOURS + } else { + LIGHT_CISTERCIAN_NUMERAL_COLOURS + }; + ui.ctx().request_repaint_after(Duration::new(1, 0)); // The central panel the region left after adding TopPanel's and SidePanel's - ui.heading("eframe template"); + ui.heading("Cistercian Time"); + ui.add_space(30.0); + let now = Local::now(); + let hours_minutes: u32 = now.hour() * 100 + now.minute(); + let seconds: u32 = now.second(); + let time = now.format("%H:%M %S").to_string(); ui.horizontal(|ui| { - ui.label("Write something: "); - ui.text_edit_singleline(&mut self.label); + paint_number(ui, &colours, hours_minutes, Some(4.0), None); + paint_number(ui, &colours, seconds, Some(4.0), None); }); - - ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value")); - if ui.button("Increment").clicked() { - self.value += 1.0; - } + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.style_mut().override_text_style = Some(TextStyle::Name("clock".into())); + ui.label(time) + }); + ui.add_space(20.0); ui.separator(); + ScrollArea::vertical() + .auto_shrink(false) + .scroll_bar_visibility(ScrollBarVisibility::default()) + .show(ui, |ui| { + ui.heading("Cistercian Numbers"); + ui.add_space(30.0); + paint_number_row(ui, &colours, 0, 10); + ui.add_space(30.0); + for tens in 1..10 { + paint_number_row(ui, &colours, 10 * tens, (tens + 1) * 10); + ui.add_space(15.0); + } - ui.add(egui::github_link_file!( - "https://github.com/emilk/eframe_template/blob/master/", - "Source code." - )); + ui.add_space(30.0); + ui.horizontal(|ui| { + for number in 1..5 { + ui.horizontal_top(|ui| { + paint_number(ui, &colours, number * 100, None, Some(true)); + }); + } + }); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - powered_by_egui_and_eframe(ui); - egui::warn_if_debug_build(ui); - }); + ui.add_space(30.0); + ui.horizontal(|ui| { + for number in 1..5 { + ui.horizontal_top(|ui| { + paint_number(ui, &colours, number * 1_000, None, Some(true)); + }); + } + }); + + ui.add_space(30.0); + ui.separator(); + + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + powered_by_egui_and_eframe(ui); + egui::warn_if_debug_build(ui); + }); + }); + }); }); } } diff --git a/src/lib.rs b/src/lib.rs index fbae77a..0b7e2f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ #![warn(clippy::all, rust_2018_idioms)] mod app; -pub use app::TemplateApp; +pub use app::CistercianClockApp; diff --git a/src/main.rs b/src/main.rs index 4d870aa..109e1e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,34 @@ #![warn(clippy::all, rust_2018_idioms)] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use egui::IconData; + // When compiling natively: #[cfg(not(target_arch = "wasm32"))] fn main() -> eframe::Result<()> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let icon_image = image::open("assets/icon-256.png").unwrap(); + let width = icon_image.width(); + let height = icon_image.height(); + let icon_rgba8 = icon_image.into_rgba8().to_vec(); + let icon_data = IconData { + rgba: icon_rgba8, + width, + height, + }; + let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([400.0, 300.0]) - .with_min_inner_size([300.0, 220.0]), + .with_inner_size([750.0, 600.0]) + .with_min_inner_size([750.0, 600.0]) + .with_icon(icon_data), ..Default::default() }; eframe::run_native( - "eframe template", + "Cistercian Clock", native_options, - Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))), + Box::new(|cc| Box::new(cistercian_clock::CistercianClockApp::new(cc))), ) } @@ -32,7 +45,7 @@ fn main() { .start( "the_canvas_id", // hardcode it web_options, - Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))), + Box::new(|cc| Box::new(cistercian_clock::CistercianClockApp::new(cc))), ) .await .expect("failed to start eframe");