diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bac83fe --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work +# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html +# 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 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 0000000..2c67f7b --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,34 @@ +# Copied from https://github.com/rerun-io/rerun_template + +# https://github.com/marketplace/actions/require-labels +# Check for existence of labels +# See all our labels at https://github.com/rerun-io/rerun/issues/labels + +name: PR Labels + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Check for a "do-not-merge" label + uses: mheap/github-action-required-labels@v3 + with: + mode: exactly + count: 0 + labels: "do-not-merge" + + - name: Require label "include in changelog" or "exclude from changelog" + uses: mheap/github-action-required-labels@v3 + with: + mode: minimum + count: 1 + labels: "exclude from changelog, include in changelog" diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000..8bb984c --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,29 @@ +# Copied from https://github.com/rerun-io/rerun_template +on: [push, pull_request] + +name: Link checker + +jobs: + link-checker: + name: Check links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Restore link checker cache + uses: actions/cache@v3 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + # Check https://github.com/lycheeverse/lychee on how to run locally. + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v1.9.0 + with: + fail: true + lycheeVersion: "0.14.3" + # When given a directory, lychee checks only markdown, html and text files, everything else we have to glob in manually. + args: | + --base . --cache --max-cache-age 1d . "**/*.rs" "**/*.toml" "**/*.hpp" "**/*.cpp" "**/CMakeLists.txt" "**/*.py" "**/*.yml" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..3878bdc --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,48 @@ +name: Github Pages + +# By default, runs if you push to main. keeps your deployed app in sync with main branch. +on: + push: + branches: + - main +# to only run when you do a new github release, comment out above part and uncomment the below trigger. +# on: +# release: +# types: +# - published + +permissions: + contents: write # for committing to gh-pages branch. + +jobs: + build-github-pages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 # repo checkout + - name: Setup toolchain for wasm + run: | + rustup update stable + rustup default stable + rustup set profile minimal + rustup target add wasm32-unknown-unknown + - name: Rust Cache # cache the rust build artefacts + uses: Swatinem/rust-cache@v2 + - name: Download and install Trunk binary + run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- + - name: Build # build + # Environment $public_url resolves to the github project page. + # If using a user/organization page, remove the `${{ github.event.repository.name }}` part. + # using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico . + # this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested + # relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which + # will obviously return error 404 not found. + run: ./trunk build --release --public-url $public_url + env: + public_url: "https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: dist + # this option will not maintain any history of your previous pages deployment + # set to false if you want all page build to be committed to your gh-pages branch history + single-commit: true diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..e9f9cec --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,154 @@ +# Copied from https://github.com/rerun-io/rerun_template +on: [push, pull_request] + +name: Rust + +env: + RUSTFLAGS: -D warnings + RUSTDOCFLAGS: -D warnings + +jobs: + rust-check: + name: Rust + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions-rs/toolchain@v1 + with: + profile: default + toolchain: 1.76.0 + override: true + + - name: Install packages (Linux) + if: runner.os == 'Linux' + uses: awalsh128/cache-apt-pkgs-action@v1.3.0 + with: + packages: libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev + version: 1.0 + execute_install_scripts: true + + - name: Set up cargo cache + uses: Swatinem/rust-cache@v2 + + - name: Rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Lint vertical spacing + run: ./scripts/lint.py + + - name: check --all-features + uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features --all-targets + + - name: check default features + uses: actions-rs/cargo@v1 + with: + command: check + args: --all-targets + + - name: check --no-default-features + uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features --lib --all-targets + + - name: Test doc-tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --doc --all-features + + - name: cargo doc --lib + uses: actions-rs/cargo@v1 + with: + command: doc + args: --lib --no-deps --all-features + + - name: cargo doc --document-private-items + uses: actions-rs/cargo@v1 + with: + command: doc + args: --document-private-items --no-deps --all-features + + - name: Build tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --no-run + + - name: Run test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features + + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-targets --all-features -- -D warnings + + # --------------------------------------------------------------------------- + + check_wasm: + name: Check wasm32 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.76.0 + target: wasm32-unknown-unknown + override: true + + - name: Set up cargo cache + uses: Swatinem/rust-cache@v2 + + - name: Check wasm32 + uses: actions-rs/cargo@v1 + with: + command: check + args: --target wasm32-unknown-unknown --lib + + - name: Clippy wasm32 + env: + CLIPPY_CONF_DIR: "scripts/clippy_wasm" # Use scripts/clippy_wasm/clippy.toml + run: cargo clippy --target wasm32-unknown-unknown --lib -- -D warnings + + # --------------------------------------------------------------------------- + + cargo-deny: + name: Check Rust dependencies (cargo-deny) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + rust-version: "1.76.0" + log-level: warn + command: check + + # --------------------------------------------------------------------------- + + trunk: + name: trunk + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.76.0 + target: wasm32-unknown-unknown + override: true + - name: Download and install Trunk binary + run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- + - name: Build + run: ./trunk build diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 0000000..3055f87 --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,19 @@ +# Copied from https://github.com/rerun-io/rerun_template + +# https://github.com/crate-ci/typos +# Add exceptions to `.typos.toml` +# install and run locally: cargo install typos-cli && typos + +name: Spell Check +on: [pull_request] + +jobs: + run: + name: Spell Check + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + + - name: Check spelling of entire workspace + uses: crate-ci/typos@master diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54ace48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Mac stuff: +.DS_Store + +# trunk output folder +dist + +# Rust compile target directories: +target +target_ra +target_wasm + +# https://github.com/lycheeverse/lychee +.lycheecache diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..0e66a4e --- /dev/null +++ b/.typos.toml @@ -0,0 +1,6 @@ +# https://github.com/crate-ci/typos +# install: cargo install typos-cli +# run: typos + +[default.extend-words] +egui = "egui" # Example for how to ignore a false positive diff --git a/CHANGELOG.md b/CHANGELOG.md index d94eb79..553e011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the `egui_plot` integration will be noted in this file. This file is updated upon each release. -Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. ## 0.28.1 - 2024-07-05 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5640e4f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# 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, caste, color, 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 behavior 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 apologizing 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 behavior include: + +* The use of sexualized 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 behavior and will take appropriate and fair corrective action in +response to any behavior 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 +[the egui discord](https://discord.gg/JFcEma9bJq). +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 behavior 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 +behavior 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 behavior. 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 behavior. + +**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 behavior, 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.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5cc9ac1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,123 @@ +# Contribution Guidelines + + +## Discussion + +You can ask questions, share screenshots and more at [GitHub Discussions](https://github.com/emilk/egui_plot/discussions). + +There is an `egui` discord at . + + +## Filing an issue + +[Issues](https://github.com/emilk/egui_plot/issues) are for bug reports and feature requests. Issues are not for asking questions (use [Discussions](https://github.com/emilk/egui_plot/discussions) or [Discord](https://discord.gg/vbuv9Xan65) for that). + +Always make sure there is not already a similar issue to the one you are creating. + +If you are filing a bug, please provide a way to reproduce it. + + +## Making a PR + +For small things, just go ahead an open a PR. For bigger things, please file an issue first (or find an existing one) and announce that you plan to work on something. That way we will avoid having several people doing double work, and you might get useful feedback on the issue before you start working. + +You can test your code locally by running `./scripts/check.sh`. + +When you have something that works, open a draft PR. You may get some helpful feedback early! +When you feel the PR is ready to go, do a self-review of the code, and then open it for review. + +Don't worry about having many small commits in the PR - they will be squashed to one commit once merged. + +Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged. + +## PR review +It is very easy to add complexity to a project, but remember that each line of code added is code that needs to be maintained in perpetuity, so we have a high bar on what get merged! + +When reviewing, we look for: +* The PR title and description should be helpful +* Breaking changes are documented in the PR description +* The code should be readable +* The code should have helpful docstrings +* The code should follow the [Code Style](CONTRIBUTING.md#code-style) + +Note that each new egui_plot release have some breaking changes, so we don't mind having a few of those in a PR. +f course, we still try to avoid them if we can, and if we can't we try to first deprecate old code using the `#[deprecated]` attribute. + + +## Code Style +* Read some code before writing your own +* Leave the code cleaner than how you found it +* Write idiomatic rust +* Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +* Add blank lines around all `fn`, `struct`, `enum`, etc +* `// Comment like this.` and not `//like this` +* Use `TODO` instead of `FIXME` +* Add your github handle to the `TODO`:s you write, e.g: `TODO(emilk): clean this up` +* Avoid `unsafe` +* Avoid `unwrap` and any other code that can cause panics +* Use good names for everything +* Add docstrings to types, `struct` fields and all `pub fn` +* Add some example code (doc-tests) +* Before making a function longer, consider adding a helper function +* If you are only using it in one function, put the `use` statement in that function. This improves locality, making it easier to read and move the code +* When importing a `trait` to use it's trait methods, do this: `use Trait as _;`. That lets the reader know why you imported it, even though it seems unused +* Avoid double negatives +* Flip `if !condition {} else {}` +* Sets of things should be lexicographically sorted (e.g. crate dependencies in `Cargo.toml`) +* Break the above rules when it makes sense + + +### Good: +``` rust +/// The name of the thing. +pub fn name(&self) -> &str { + &self.name +} + +fn foo(&self) { + // TODO(emilk): this can be optimized +} +``` + +### Bad: +``` rust +//gets the name +pub fn get_name(&self) -> &str { + &self.name +} +fn foo(&self) { + //FIXME: this can be optimized +} +``` + +### Coordinate system +The left-top corner of the screen is `(0.0, 0.0)`, +with `Vec2::X` increasing to the right and `Vec2::Y` increasing downwards. + +`egui` uses logical _points_ as its coordinate system. +Those related to physical _pixels_ by the `pixels_per_point` scale factor. +For example, a high-dpi screen can have `pixels_per_point = 2.0`, +meaning there are two physical screen pixels for each logical point. + +Angles are in radians, and are measured clockwise from the X-axis, which has angle=0. + + +### Avoid `unwrap`, `expect` etc. +The code should never panic or crash, which means that any instance of `unwrap` or `expect` is a potential time-bomb. Even if you structured your code to make them impossible, any reader will have to read the code very carefully to prove to themselves that an `unwrap` won't panic. Often you can instead rewrite your code so as to avoid it. The same goes for indexing into a slice (which will panic on out-of-bounds) - it is often preferable to use `.get()`. + +For instance: + +``` rust +let first = if vec.is_empty() { + return; +} else { + vec[0] +}; +``` +can be better written as: + +``` rust +let Some(first) = vec.first() else { + return; +}; +``` diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..713a5a7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,377 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "accesskit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "ecolor" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6b451ff1143f6de0f33fc7f1b68fecfd2c7de06e104de96c4514de3f5396f8" +dependencies = [ + "emath", + "serde", +] + +[[package]] +name = "egui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c97e70a2768de630f161bb5392cbd3874fcf72868f14df0e002e82e06cb798" +dependencies = [ + "accesskit", + "ahash", + "emath", + "epaint", + "nohash-hasher", + "serde", +] + +[[package]] +name = "egui_plot" +version = "0.28.1" +dependencies = [ + "ahash", + "document-features", + "egui", + "emath", + "serde", +] + +[[package]] +name = "emath" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6a21708405ea88f63d8309650b4d77431f4bc28fb9d8e6f77d3963b51249e6" +dependencies = [ + "serde", +] + +[[package]] +name = "enumn" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd000fd6988e73bbe993ea3db9b1aa64906ab88766d654973924340c8cddb42" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "epaint" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f0dcc0a0771e7500e94cd1cb797bd13c9f23b9409bdc3c824e2cbc562b7fa01" +dependencies = [ + "ab_glyph", + "ahash", + "ecolor", + "emath", + "nohash-hasher", + "parking_lot", + "serde", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "owned_ttf_parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "ttf-parser" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8686b91785aff82828ed725225925b33b4fde44c4bb15876e5f7c832724c420a" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4efdd38 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,188 @@ +[workspace] +resolver = "2" +members = ["crates/egui_plot"] + + +[workspace.package] +edition = "2021" +license = "MIT OR Apache-2.0" +rust-version = "1.76" +version = "0.28.1" + + +[profile.dev.package."*"] +# Optimize all dependencies even in debug builds (does not affect workspace packages): +opt-level = 2 + + +[workspace.dependencies] +ahash = { version = "0.8.11", default-features = false, features = [ + "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead + "std", +] } +document-features = " 0.2.8" +egui = { version = "0.28.1", default-features = false } +emath = { version = "0.28.1", default-features = false } +serde = { version = "1", features = ["derive"] } + + +[workspace.lints.rust] +unsafe_code = "deny" + +elided_lifetimes_in_paths = "warn" +future_incompatible = "warn" +nonstandard_style = "warn" +rust_2018_idioms = "warn" +rust_2021_prelude_collisions = "warn" +semicolon_in_expressions_from_macros = "warn" +trivial_numeric_casts = "warn" +unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 +unused_extern_crates = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" + +trivial_casts = "allow" +unused_qualifications = "allow" + +[workspace.lints.rustdoc] +all = "warn" +missing_crate_level_docs = "warn" + +# See also clippy.toml +[workspace.lints.clippy] +as_ptr_cast_mut = "warn" +await_holding_lock = "warn" +bool_to_int_with_if = "warn" +char_lit_as_u8 = "warn" +checked_conversions = "warn" +clear_with_drain = "warn" +cloned_instead_of_copied = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +derive_partial_eq_without_eq = "warn" +disallowed_macros = "warn" # See clippy.toml +disallowed_methods = "warn" # See clippy.toml +disallowed_names = "warn" # See clippy.toml +disallowed_script_idents = "warn" # See clippy.toml +disallowed_types = "warn" # See clippy.toml +doc_link_with_quotes = "warn" +doc_markdown = "warn" +empty_enum = "warn" +enum_glob_use = "warn" +equatable_if_let = "warn" +exit = "warn" +expl_impl_clone_on_copy = "warn" +explicit_deref_methods = "warn" +explicit_into_iter_loop = "warn" +explicit_iter_loop = "warn" +fallible_impl_from = "warn" +filter_map_next = "warn" +flat_map_option = "warn" +float_cmp_const = "warn" +fn_params_excessive_bools = "warn" +fn_to_numeric_cast_any = "warn" +from_iter_instead_of_collect = "warn" +get_unwrap = "warn" +if_let_mutex = "warn" +implicit_clone = "warn" +imprecise_flops = "warn" +index_refutable_slice = "warn" +inefficient_to_string = "warn" +infinite_loop = "warn" +into_iter_without_iter = "warn" +invalid_upcast_comparisons = "warn" +iter_not_returning_iterator = "warn" +iter_on_empty_collections = "warn" +iter_on_single_items = "warn" +iter_over_hash_type = "warn" +iter_without_into_iter = "warn" +large_digit_groups = "warn" +large_include_file = "warn" +large_stack_arrays = "warn" +large_stack_frames = "warn" +large_types_passed_by_value = "warn" +let_underscore_untyped = "warn" +let_unit_value = "warn" +linkedlist = "warn" +lossy_float_literal = "warn" +macro_use_imports = "warn" +manual_assert = "warn" +manual_clamp = "warn" +manual_instant_elapsed = "warn" +manual_let_else = "warn" +manual_ok_or = "warn" +manual_string_new = "warn" +map_err_ignore = "warn" +map_flatten = "warn" +map_unwrap_or = "warn" +match_on_vec_items = "warn" +match_same_arms = "warn" +match_wild_err_arm = "warn" +match_wildcard_for_single_variants = "warn" +mem_forget = "warn" +mismatched_target_os = "warn" +mismatching_type_param_order = "warn" +missing_assert_message = "warn" +missing_enforced_import_renames = "warn" +missing_errors_doc = "warn" +missing_safety_doc = "warn" +mut_mut = "warn" +mutex_integer = "warn" +needless_borrow = "warn" +needless_continue = "warn" +needless_for_each = "warn" +needless_pass_by_ref_mut = "warn" +needless_pass_by_value = "warn" +negative_feature_names = "warn" +nonstandard_macro_braces = "warn" +option_option = "warn" +path_buf_push_overwrite = "warn" +ptr_as_ptr = "warn" +ptr_cast_constness = "warn" +pub_without_shorthand = "warn" +rc_mutex = "warn" +readonly_write_lock = "warn" +redundant_type_annotations = "warn" +ref_option_ref = "warn" +ref_patterns = "warn" +rest_pat_in_fully_bound_structs = "warn" +same_functions_in_if_condition = "warn" +semicolon_if_nothing_returned = "warn" +should_panic_without_expect = "warn" +significant_drop_tightening = "warn" +single_match_else = "warn" +str_to_string = "warn" +string_add = "warn" +string_add_assign = "warn" +string_lit_as_bytes = "warn" +string_lit_chars_any = "warn" +string_to_string = "warn" +suspicious_command_arg_space = "warn" +suspicious_xor_used_as_pow = "warn" +todo = "warn" +too_many_lines = "warn" +trailing_empty_array = "warn" +trait_duplication_in_bounds = "warn" +tuple_array_conversions = "warn" +unchecked_duration_subtraction = "warn" +undocumented_unsafe_blocks = "warn" +unimplemented = "warn" +uninhabited_references = "warn" +uninlined_format_args = "warn" +unnecessary_box_returns = "warn" +unnecessary_safety_doc = "warn" +unnecessary_struct_initialization = "warn" +unnecessary_wraps = "warn" +unnested_or_patterns = "warn" +unused_peekable = "warn" +unused_rounding = "warn" +unused_self = "warn" +unwrap_used = "warn" +use_self = "warn" +useless_transmute = "warn" +verbose_file_reads = "warn" +wildcard_dependencies = "warn" +wildcard_imports = "warn" +zero_sized_map_values = "warn" + +manual_range_contains = "allow" # this one is just worse imho diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index ab342bd..cdecf67 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # egui_plot +[github](https://github.com/emilk/egui_plot) [![Latest version](https://img.shields.io/crates/v/egui_plot.svg)](https://crates.io/crates/egui_plot) [![Documentation](https://docs.rs/egui_plot/badge.svg)](https://docs.rs/egui_plot) [![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) +[![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) Immediate mode plotting for [`egui`](https://github.com/emilk/egui). diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..fb67af0 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,61 @@ +# Releases +## Cadence +We release a new major `egui_plot` whenever there is a new major `egui` release. + + +## Versioning +For the moment the `egui_plot` version follows that of the `egui` crates. +That may change in the future. + +The version in `main` is always the version of the last published crate. +This is so that users can easily patch their `egui_plot` to the version on `main` if they want to. + + +## Governance +Releases are generally done by [emilk](https://github.com/emilk/), but the [rerun-io](https://github.com/rerun-io/) organization (where emilk is CTO) also has publish rights to all the crates. + + +## Rust version policy +Our Minimum Supported Rust Version (MSRV) is always _at least_ two minor release behind the latest Rust version. This means users of egui aren't forced to update to the very latest Rust version. + +We don't update the MSRV in a patch release, unless we really, really need to. + + +# Release process +## Patch release +* [ ] Make a branch off of the latest release +* [ ] cherry-pick what you want to release +* [ ] run `cargo semver-checks` + +## Optional polish before a major release +* [ ] improve the demo a bit +* [ ] `cargo update` +* [ ] `cargo outdated` (or manually look for outdated crates in each `Cargo.toml`) +* [ ] `cargo machete` + +## Release testing +* [ ] test the demo app +* [ ] test the web demo + - test on mobile + - test on chromium +* [ ] `./scripts/check.sh` +* [ ] check that CI is green + +## Preparation +* [ ] optionally record gif or take a screenshot for `CHANGELOG.md` release note (and later twitter post) +* [ ] update changelogs using `scripts/generate_changelog.py` + - For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.27.0..HEAD` +* [ ] bump version numbers in workspace `Cargo.toml` + +## Actual release +I usually do this all on the `main` branch, but doing it in a release branch is also fine, as long as you remember to merge it into `main` later. + +* [ ] `git commit -m 'Release 0.x.0 - summary'` +* [ ] `cargo publish` +* [ ] `git tag -a 0.x.0 -m 'Release 0.x.0 - summary'` +* [ ] `git pull --tags ; git tag -d latest && git tag -a latest -m 'Latest release' && git push --tags origin latest --force ; git push --tags` +* [ ] merge release PR or push to `main` +* [ ] check that CI is green +* [ ] do a GitHub release: https://github.com/emilk/egui/releases/new + * Follow the format of the last release +* [ ] wait for documentation to build: https://docs.rs/releases/queue diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..a7b19c9 --- /dev/null +++ b/Trunk.toml @@ -0,0 +1 @@ +[build] diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100755 index 0000000..61ad031 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/icon-1024.png b/assets/icon-1024.png new file mode 100644 index 0000000..1b5868a Binary files /dev/null and b/assets/icon-1024.png differ diff --git a/assets/icon-256.png b/assets/icon-256.png new file mode 100644 index 0000000..ae72287 Binary files /dev/null and b/assets/icon-256.png differ diff --git a/assets/icon_ios_touch_192.png b/assets/icon_ios_touch_192.png new file mode 100644 index 0000000..8472802 Binary files /dev/null and b/assets/icon_ios_touch_192.png differ diff --git a/assets/manifest.json b/assets/manifest.json new file mode 100644 index 0000000..2a137fb --- /dev/null +++ b/assets/manifest.json @@ -0,0 +1,28 @@ +{ + "name": "egui Template PWA", + "short_name": "egui-template-pwa", + "icons": [ + { + "src": "./icon-256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "./maskable_icon_x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "./icon-1024.png", + "sizes": "1024x1024", + "type": "image/png" + } + ], + "lang": "en-US", + "id": "/index.html", + "start_url": "./index.html", + "display": "standalone", + "background_color": "white", + "theme_color": "white" +} diff --git a/assets/maskable_icon_x512.png b/assets/maskable_icon_x512.png new file mode 100644 index 0000000..db8df3e Binary files /dev/null and b/assets/maskable_icon_x512.png differ diff --git a/assets/sw.js b/assets/sw.js new file mode 100644 index 0000000..7ecd229 --- /dev/null +++ b/assets/sw.js @@ -0,0 +1,25 @@ +var cacheName = 'egui-template-pwa'; +var filesToCache = [ + './', + './index.html', + './eframe_template.js', + './eframe_template_bg.wasm', +]; + +/* Start the service worker and cache all of the app's content */ +self.addEventListener('install', function (e) { + e.waitUntil( + caches.open(cacheName).then(function (cache) { + return cache.addAll(filesToCache); + }) + ); +}); + +/* Serve cached content when offline */ +self.addEventListener('fetch', function (e) { + e.respondWith( + caches.match(e.request).then(function (response) { + return response || fetch(e.request); + }) + ); +}); diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..e1c2dbc --- /dev/null +++ b/clippy.toml @@ -0,0 +1,77 @@ +# There is also a scripts/clippy_wasm/clippy.toml which forbids some methods that are not available in wasm. + +# ----------------------------------------------------------------------------- +# Section identical to scripts/clippy_wasm/clippy.toml: + +msrv = "1.76" + +allow-unwrap-in-tests = true + +# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api +# We want suggestions, even if it changes public API. +avoid-breaking-exported-api = false + +max-fn-params-bools = 1 + +# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file +max-include-file-size = 1000000 + +# https://rust-lang.github.io/rust-clippy/master/index.html#/type_complexity +type-complexity-threshold = 200 + +# ----------------------------------------------------------------------------- + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_macros +disallowed-macros = [ + 'dbg', + 'std::unimplemented', + + # TODO(emilk): consider forbidding these to encourage the use of proper log stream, and then explicitly allow legitimate uses + # 'std::eprint', + # 'std::eprintln', + # 'std::print', + # 'std::println', +] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods +disallowed-methods = [ + "std::env::temp_dir", # Use the tempdir crate instead + + # There are many things that aren't allowed on wasm, + # but we cannot disable them all here (because of e.g. https://github.com/rust-lang/rust-clippy/issues/10406) + # so we do that in `clipppy_wasm.toml` instead. + + "std::thread::spawn", # Use `std::thread::Builder` and name the thread + + "sha1::Digest::new", # SHA1 is cryptographically broken + + "std::panic::catch_unwind", # We compile with `panic = "abort"` +] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_names +disallowed-names = [] + +# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types +disallowed-types = [ + # Use the faster & simpler non-poisonable primitives in `parking_lot` instead + "std::sync::Mutex", + "std::sync::RwLock", + "std::sync::Condvar", + # "std::sync::Once", # enabled for now as the `log_once` macro uses it internally + + "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken + + "winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account + "winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account +] + +# ----------------------------------------------------------------------------- + +# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown +doc-valid-idents = [ + # You must also update the same list in the root `clippy.toml`! + "AccessKit", + "WebGL", + "WebGPU", + "..", +] diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..7f92023 --- /dev/null +++ b/deny.toml @@ -0,0 +1,85 @@ +# Copied from https://github.com/rerun-io/rerun_template +# +# https://github.com/EmbarkStudios/cargo-deny +# +# cargo-deny checks our dependency tree for copy-left licenses, +# duplicate dependencies, and rustsec advisories (https://rustsec.org/advisories). +# +# Install: `cargo install cargo-deny` +# Check: `cargo deny check`. + + +# Note: running just `cargo deny check` without a `--target` can result in +# false positives due to https://github.com/EmbarkStudios/cargo-deny/issues/324 +[graph] +targets = [ + { triple = "aarch64-apple-darwin" }, + { triple = "i686-pc-windows-gnu" }, + { triple = "i686-pc-windows-msvc" }, + { triple = "i686-unknown-linux-gnu" }, + { triple = "wasm32-unknown-unknown" }, + { triple = "x86_64-apple-darwin" }, + { triple = "x86_64-pc-windows-gnu" }, + { triple = "x86_64-pc-windows-msvc" }, + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-unknown-linux-musl" }, + { triple = "x86_64-unknown-redox" }, +] +all-features = true + + +[advisories] +version = 2 +ignore = [] + + +[bans] +multiple-versions = "deny" +wildcards = "deny" +deny = [ + { name = "cmake", reason = "It has hurt me too much" }, + { name = "openssl-sys", reason = "Use rustls" }, + { name = "openssl", reason = "Use rustls" }, +] + +skip = [] +skip-tree = [] + + +[licenses] +version = 2 +private = { ignore = true } +confidence-threshold = 0.93 # We want really high confidence when inferring licenses from text +allow = [ + "Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html + "Apache-2.0", # https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) + "BSD-2-Clause", # https://tldrlegal.com/license/bsd-2-clause-license-(freebsd) + "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) + "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained + "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ + "ISC", # https://www.tldrlegal.com/license/isc-license + "LicenseRef-UFL-1.0", # no official SPDX, see https://github.com/emilk/egui/issues/2321 + "MIT-0", # https://choosealicense.com/licenses/mit-0/ + "MIT", # https://tldrlegal.com/license/mit-license + "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. + "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html + "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html + "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) +] +exceptions = [] + +[[licenses.clarify]] +name = "webpki" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + + +[sources] +unknown-registry = "deny" +unknown-git = "deny" diff --git a/index.html b/index.html new file mode 100644 index 0000000..5d57a4a --- /dev/null +++ b/index.html @@ -0,0 +1,148 @@ + + + + + + + + + + eframe template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Loading… +

+
+
+ + + + + + + + + diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..d248815 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,76 @@ +# Copied from https://github.com/rerun-io/rerun_template + +################################################################################ +# Config for the link checker lychee. +# +# Download & learn more at: +# https://github.com/lycheeverse/lychee +# +# Example config: +# https://github.com/lycheeverse/lychee/blob/master/lychee.example.toml +# +# Run `lychee . --dump` to list all found links that are being checked. +# +# Note that by default lychee will only check markdown and html files, +# to check any other files you have to point to them explicitly, e.g.: +# `lychee **/*.rs` +# To make things worse, `exclude_path` is ignored for these globs, +# so local runs with lots of gitignored files will be slow. +# (https://github.com/lycheeverse/lychee/issues/1405) +# +# This unfortunately doesn't list anything for non-glob checks. +################################################################################ + +# Maximum number of concurrent link checks. +# Workaround for "too many open files" error on MacOS, see https://github.com/lycheeverse/lychee/issues/1248 +max_concurrency = 32 + +# Check links inside `` and `
` blocks as well as Markdown code blocks.
+include_verbatim = true
+
+# Proceed for server connections considered insecure (invalid TLS).
+insecure = true
+
+# Exclude these filesystem paths from getting checked.
+exclude_path = [
+  # Unfortunately lychee doesn't yet read .gitignore https://github.com/lycheeverse/lychee/issues/1331
+  # The following entries are there because of that:
+  ".git",
+  "__pycache__",
+  "_deps/",
+  ".pixi",
+  "build",
+  "target_ra",
+  "target_wasm",
+  "target",
+  "venv",
+]
+
+# Exclude URLs and mail addresses from checking (supports regex).
+exclude = [
+  # Strings with replacements.
+  '/__VIEWER_VERSION__/', # Replacement variable __VIEWER_VERSION__.
+  '/\$',                  # Replacement variable $.
+  '/GIT_HASH/',           # Replacement variable GIT_HASH.
+  '\{\}',                 # Ignore links with string interpolation.
+  '\$relpath\^',          # Relative paths as used by rerun_cpp's doc header.
+  '%7B.+%7D',             # Ignore strings that look like ready to use links but contain a replacement strings. The URL escaping is for '{.+}' (this seems to be needed for html embedded urls since lychee assumes they use this encoding).
+  '%7B%7D',               # Ignore links with string interpolation, escaped variant.
+
+  # Local links that require further setup.
+  'http://127.0.0.1',
+  'http://localhost',
+  'recording:/',      # rrd recording link.
+  'ws:/',
+  're_viewer.js',     # Build artifact that html is linking to.
+
+  # Api endpoints.
+  'https://fonts.googleapis.com/', # Font API entrypoint, not a link.
+  'https://fonts.gstatic.com/',    # Font API entrypoint, not a link.
+  'https://tel.rerun.io/',         # Analytics endpoint.
+
+  # Avoid rate limiting.
+  'https://crates.io/crates/.*',                   # Avoid crates.io rate-limiting
+  'https://github.com/emilk/egui_plot/commit/\.*', # Ignore links to our own commits (typically in changelog).
+  'https://github.com/emilk/egui_plot/pull/\.*',   # Ignore links to our own pull requests (typically in changelog).
+]
diff --git a/rust-toolchain b/rust-toolchain
new file mode 100644
index 0000000..3fb0f58
--- /dev/null
+++ b/rust-toolchain
@@ -0,0 +1,10 @@
+# If you see this, run "rustup self update" to get rustup 1.23 or newer.
+
+# NOTE: above comment is for older `rustup` (before TOML support was added),
+# which will treat the first line as the toolchain name, and therefore show it
+# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
+
+[toolchain]
+channel = "1.76"  # Avoid specifying a patch version here; see https://github.com/emilk/eframe_template/issues/145
+components = [ "rustfmt", "clippy" ]
+targets = [ "wasm32-unknown-unknown" ]
diff --git a/scripts/check.sh b/scripts/check.sh
new file mode 100755
index 0000000..2344528
--- /dev/null
+++ b/scripts/check.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# This scripts runs various CI-like checks in a convenient way.
+
+set -eu
+script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+cd "$script_path/.."
+set -x
+
+# Checks all tests, lints etc.
+# Basically does what the CI does.
+
+cargo +1.75.0 install --quiet typos-cli
+
+export RUSTFLAGS="-D warnings"
+export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454
+
+# Fast checks first:
+typos
+./scripts/lint.py
+cargo fmt --all -- --check
+cargo deny check
+cargo doc --quiet --lib --no-deps --all-features
+cargo doc --quiet --document-private-items --no-deps --all-features
+
+cargo clippy --quiet --all-targets --all-features -- -D warnings
+
+CLIPPY_CONF_DIR="scripts/clippy_wasm" cargo clippy --quiet --target wasm32-unknown-unknown --lib -- -D warnings
+
+cargo check --quiet  --all-targets
+cargo check --quiet  --all-targets --no-default-features
+cargo check --quiet  --all-targets --all-features
+cargo test  --quiet --all-targets --all-features
+cargo test  --quiet --doc # slow - checks all doc-tests
+
+echo "All checks passed."
diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml
new file mode 100644
index 0000000..5b7bf9d
--- /dev/null
+++ b/scripts/clippy_wasm/clippy.toml
@@ -0,0 +1,75 @@
+# Copied from https://github.com/rerun-io/rerun_template
+
+# This is used by the CI so we can forbid some methods that are not available in wasm.
+#
+# We cannot forbid all these methods in the main `clippy.toml` because of
+# https://github.com/rust-lang/rust-clippy/issues/10406
+
+# -----------------------------------------------------------------------------
+# Section identical to the main clippy.toml:
+
+msrv = "1.76"
+
+allow-unwrap-in-tests = true
+
+# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html#avoid-breaking-exported-api
+# We want suggestions, even if it changes public API.
+avoid-breaking-exported-api = false
+
+excessive-nesting-threshold = 8
+
+max-fn-params-bools = 1
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#/large_include_file
+max-include-file-size = 1000000
+
+too-many-lines-threshold = 200
+
+# -----------------------------------------------------------------------------
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_methods
+disallowed-methods = [
+  { path = "crossbeam::channel::Receiver::into_iter", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::iter", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::recv_timeout", reason = "Cannot block on Web" },
+  { path = "crossbeam::channel::Receiver::recv", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_and_take", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_until_ready_mut", reason = "Cannot block on Web" },
+  { path = "poll_promise::Promise::block_until_ready", reason = "Cannot block on Web" },
+  { path = "rayon::spawn", reason = "Cannot spawn threads on wasm" },
+  { path = "std::sync::mpsc::Receiver::into_iter", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::iter", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::recv_timeout", reason = "Cannot block on Web" },
+  { path = "std::sync::mpsc::Receiver::recv", reason = "Cannot block on Web" },
+  { path = "std::thread::spawn", reason = "Cannot spawn threads on wasm" },
+  { path = "std::time::Duration::elapsed", reason = "use `web-time` crate instead for wasm/web compatibility" },
+  { path = "std::time::Instant::now", reason = "use `web-time` crate instead for wasm/web compatibility" },
+  { path = "std::time::SystemTime::now", reason = "use `web-time` or `time` crates instead for wasm/web compatibility" },
+]
+
+# https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
+disallowed-types = [
+  { path = "instant::SystemTime", reason = "Known bugs. Use web-time." },
+  { path = "std::thread::Builder", reason = "Cannot spawn threads on wasm" },
+  # { path = "std::path::PathBuf", reason = "Can't read/write files on web" }, // Used in build.rs files (which is fine).
+]
+
+# Allow-list of words for markdown in docstrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
+doc-valid-idents = [
+  # You must also update the same list in the root `clippy.toml`!
+  "..",
+  "GitHub",
+  "GLB",
+  "GLTF",
+  "iOS",
+  "macOS",
+  "NaN",
+  "OBJ",
+  "OpenGL",
+  "PyPI",
+  "sRGB",
+  "sRGBA",
+  "WebGL",
+  "WebSocket",
+  "WebSockets",
+]
diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py
new file mode 100755
index 0000000..b76d8cc
--- /dev/null
+++ b/scripts/generate_changelog.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+# Copied from https://github.com/rerun-io/rerun_template
+
+"""
+Summarizes recent PRs based on their GitHub labels.
+
+The result can be copy-pasted into CHANGELOG.md,
+though it often needs some manual editing too.
+"""
+
+from __future__ import annotations
+
+import argparse
+import multiprocessing
+import os
+import re
+import sys
+from dataclasses import dataclass
+from typing import Any, Optional
+
+import requests
+from git import Repo  # pip install GitPython
+from tqdm import tqdm
+
+OWNER = "emilk"
+REPO = "egui_plot"
+INCLUDE_LABELS = False  # It adds quite a bit of visual noise
+
+
+@dataclass
+class PrInfo:
+    gh_user_name: str
+    pr_title: str
+    labels: list[str]
+
+
+@dataclass
+class CommitInfo:
+    hexsha: str
+    title: str
+    pr_number: Optional[int]
+
+
+def get_github_token() -> str:
+    token = os.environ.get("GH_ACCESS_TOKEN", "")
+    if token != "":
+        return token
+
+    home_dir = os.path.expanduser("~")
+    token_file = os.path.join(home_dir, ".githubtoken")
+
+    try:
+        with open(token_file, encoding="utf8") as f:
+            token = f.read().strip()
+        return token
+    except Exception:
+        pass
+
+    print("ERROR: expected a GitHub token in the environment variable GH_ACCESS_TOKEN or in ~/.githubtoken")
+    sys.exit(1)
+
+
+# Slow
+def fetch_pr_info_from_commit_info(commit_info: CommitInfo) -> Optional[PrInfo]:
+    if commit_info.pr_number is None:
+        return None
+    else:
+        return fetch_pr_info(commit_info.pr_number)
+
+
+# Slow
+def fetch_pr_info(pr_number: int) -> Optional[PrInfo]:
+    url = f"https://api.github.com/repos/{OWNER}/{REPO}/pulls/{pr_number}"
+    gh_access_token = get_github_token()
+    headers = {"Authorization": f"Token {gh_access_token}"}
+    response = requests.get(url, headers=headers)
+    json = response.json()
+
+    # Check if the request was successful (status code 200)
+    if response.status_code == 200:
+        labels = [label["name"] for label in json["labels"]]
+        gh_user_name = json["user"]["login"]
+        return PrInfo(gh_user_name=gh_user_name, pr_title=json["title"], labels=labels)
+    else:
+        print(f"ERROR {url}: {response.status_code} - {json['message']}")
+        return None
+
+
+def get_commit_info(commit: Any) -> CommitInfo:
+    match = re.match(r"(.*) \(#(\d+)\)", commit.summary)
+    if match:
+        title = str(match.group(1))
+        pr_number = int(match.group(2))
+        return CommitInfo(hexsha=commit.hexsha, title=title, pr_number=pr_number)
+    else:
+        return CommitInfo(hexsha=commit.hexsha, title=commit.summary, pr_number=None)
+
+
+def remove_prefix(text: str, prefix: str) -> str:
+    if text.startswith(prefix):
+        return text[len(prefix) :]
+    return text  # or whatever
+
+
+def print_section(crate: str, items: list[str]) -> None:
+    if 0 < len(items):
+        print(f"#### {crate}")
+        for line in items:
+            print(f"* {line}")
+    print()
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="Generate a changelog.")
+    parser.add_argument("--commit-range", help="e.g. 0.1.0..HEAD", required=True)
+    args = parser.parse_args()
+
+    repo = Repo(".")
+    commits = list(repo.iter_commits(args.commit_range))
+    commits.reverse()  # Most recent last
+    commit_infos = list(map(get_commit_info, commits))
+
+    pool = multiprocessing.Pool()
+    pr_infos = list(
+        tqdm(
+            pool.imap(fetch_pr_info_from_commit_info, commit_infos),
+            total=len(commit_infos),
+            desc="Fetch PR info commits",
+        )
+    )
+
+    prs = []
+    unsorted_commits = []
+
+    for commit_info, pr_info in zip(commit_infos, pr_infos):
+        hexsha = commit_info.hexsha
+        title = commit_info.title
+        title = title.rstrip(".").strip()  # Some PR end with an unnecessary period
+        pr_number = commit_info.pr_number
+
+        if pr_number is None:
+            # Someone committed straight to main:
+            summary = f"{title} [{hexsha[:7]}](https://github.com/{OWNER}/{REPO}/commit/{hexsha})"
+            unsorted_commits.append(summary)
+        else:
+            # We prefer the PR title if available
+            title = pr_info.pr_title if pr_info else title
+            labels = pr_info.labels if pr_info else []
+
+            if "exclude from changelog" in labels:
+                continue
+            if "typo" in labels:
+                # We get so many typo PRs. Let's not flood the changelog with them.
+                continue
+
+            summary = f"{title} [#{pr_number}](https://github.com/{OWNER}/{REPO}/pull/{pr_number})"
+
+            if INCLUDE_LABELS and 0 < len(labels):
+                summary += f" ({', '.join(labels)})"
+
+            if pr_info is not None:
+                gh_user_name = pr_info.gh_user_name
+                summary += f" by [@{gh_user_name}](https://github.com/{gh_user_name})"
+
+            prs.append(summary)
+
+    # Clean up:
+    for i in range(len(prs)):
+        line = prs[i]
+        line = line[0].upper() + line[1:]  # Upper-case first letter
+        prs[i] = line
+
+    print()
+    print(f"Full diff at https://github.com/{OWNER}/{REPO}/compare/{args.commit_range}")
+    print()
+    print_section("PRs", prs)
+    print_section("Unsorted commits", unsorted_commits)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/lint.py b/scripts/lint.py
new file mode 100755
index 0000000..221d980
--- /dev/null
+++ b/scripts/lint.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+"""
+Runs custom linting on Rust code.
+"""
+
+import argparse
+import os
+import re
+import sys
+
+
+def lint_file_path(filepath, args) -> int:
+    with open(filepath) as f:
+        lines_in = f.readlines()
+
+    errors, lines_out = lint_lines(filepath, lines_in)
+
+    for error in errors:
+        print(error)
+
+    if args.fix and lines_in != lines_out:
+        with open(filepath, "w") as f:
+            f.writelines(lines_out)
+        print(f"{filepath} fixed.")
+
+    return len(errors)
+
+
+def lint_lines(filepath, lines_in):
+    last_line_was_empty = True
+
+    errors = []
+    lines_out = []
+    prev_line = ""
+
+    for line_nr, line in enumerate(lines_in):
+        line_nr = line_nr + 1
+
+        # TODO(emilk): only # and /// on lines before a keyword
+
+        pattern = (
+            r"^\s*((///)|((pub(\(\w*\))? )?((impl|fn|struct|enum|union|trait)\b))).*$"
+        )
+        if re.match(pattern, line):
+            stripped = prev_line.strip()
+            last_line_was_empty = (
+                stripped == ""
+                or stripped.startswith("#")
+                or stripped.startswith("//")
+                or stripped.endswith("{")
+                or stripped.endswith("(")
+                or stripped.endswith("\\")
+                or stripped.endswith('r"')
+                or stripped.endswith("]")
+            )
+            if not last_line_was_empty:
+                errors.append(
+                    f"{filepath}:{line_nr}: for readability, add newline before `{line.strip()}`"
+                )
+                lines_out.append("\n")
+
+        if re.search(r"\(mut self.*-> Self", line) and "pub(crate)" not in line:
+            if prev_line.strip() != "#[inline]":
+                errors.append(
+                    f"{filepath}:{line_nr}: builder methods should be marked #[inline]"
+                )
+                lines_out.append("#[inline]")
+
+
+        if re.search(r"TODO[^(]", line):
+            errors.append(
+                f"{filepath}:{line_nr}: write 'TODO(username):' instead"
+            )
+
+        if (
+            "(target_os" in line
+            and filepath.startswith("./crates/egui/")
+            and filepath != "./crates/egui/src/os.rs"
+        ):
+            errors.append(
+                f"{filepath}:{line_nr}: Don't use `target_os` - use ctx.os() instead."
+            )
+
+        lines_out.append(line)
+
+        prev_line = line
+
+    return errors, lines_out
+
+
+def test_lint():
+    should_pass = [
+        "hello world",
+        """
+        /// docstring
+        foo
+
+        /// docstring
+        bar
+        """,
+        """
+        #[inline]
+        pub fn with_color(mut self, color: Color32) -> Self {
+            self.color = color;
+            self
+        }
+        """,
+    ]
+
+    should_fail = [
+        """
+        /// docstring
+        foo
+        /// docstring
+        bar
+        """,
+        """
+        // not inlined
+        pub fn with_color(mut self, color: Color32) -> Self {
+            self.color = color;
+            self
+        }
+        """,
+    ]
+
+    for code in should_pass:
+        errors, _ = lint_lines("test.py", code.split("\n"))
+        assert len(errors) == 0, f"expected this to pass:\n{code}\ngot: {errors}"
+
+    for code in should_fail:
+        errors, _ = lint_lines("test.py", code.split("\n"))
+        assert len(errors) > 0, f"expected this to fail:\n{code}"
+
+    pass
+
+
+def main():
+    test_lint()  # Make sure we are bug free before we run!
+
+    parser = argparse.ArgumentParser(description="Lint Rust code with custom linter.")
+    parser.add_argument(
+        "files",
+        metavar="file",
+        type=str,
+        nargs="*",
+        help="File paths. Empty = all files, recursively.",
+    )
+    parser.add_argument(
+        "--fix", dest="fix", action="store_true", help="Automatically fix the files"
+    )
+
+    args = parser.parse_args()
+
+    num_errors = 0
+
+    if args.files:
+        for filepath in args.files:
+            num_errors += lint_file_path(filepath, args)
+    else:
+        script_dirpath = os.path.dirname(os.path.realpath(__file__))
+        root_dirpath = os.path.abspath(f"{script_dirpath}/..")
+        os.chdir(root_dirpath)
+
+        exclude = set(["target", "target_ra", "target_wasm"])
+        for root, dirs, files in os.walk(".", topdown=True):
+            dirs[:] = [d for d in dirs if d not in exclude]
+            for filename in files:
+                if filename.endswith(".rs"):
+                    filepath = os.path.join(root, filename)
+                    num_errors += lint_file_path(filepath, args)
+
+    if num_errors == 0:
+        print(f"{sys.argv[0]} finished without error")
+        sys.exit(0)
+    else:
+        print(f"{sys.argv[0]} found {num_errors} errors.")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()