1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 14:20:04 -04:00

Merge branch 'master' of https://github.com/emilk/egui into multiples_viewports

This commit is contained in:
Konkitoman
2023-08-15 02:17:12 +03:00
159 changed files with 2536 additions and 1163 deletions

View File

@@ -4,8 +4,8 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/
* Keep your PR:s small and focused. * Keep your PR:s small and focused.
* If applicable, add a screenshot or gif. * If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example. * If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example.
* Do not open PR:s from your `master` branch, as thart makes it difficult for maintainers to add commits to your PR. * Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`. * Remember to run `cargo fmt` and `cargo cranky`.
* Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`. * Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved. * When you have addressed a PR comment, mark it as resolved.

View File

@@ -29,4 +29,4 @@ jobs:
with: with:
mode: minimum mode: minimum
count: 1 count: 1
labels: "ecolor, eframe, egui_extras, egui_glow, egui-wgpu, egui-winit, egui, epaint" labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui-wgpu, egui-winit, egui, epaint, plot, typo"

View File

@@ -1,6 +1,6 @@
on: [push, pull_request] on: [push, pull_request]
name: CI name: Rust
env: env:
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses, # web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
@@ -15,13 +15,11 @@ jobs:
name: Format + check + test name: Format + check + test
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@master
with: with:
profile: default toolchain: 1.67.0
toolchain: 1.65.0
override: true
- name: Install packages (Linux) - name: Install packages (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
@@ -37,10 +35,10 @@ jobs:
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Rustfmt - name: Rustfmt
uses: actions-rs/cargo@v1 run: cargo fmt --all -- --check
with:
command: fmt - name: Lint vertical spacing
args: --all -- --check run: ./scripts/lint.py
- name: Install cargo-cranky - name: Install cargo-cranky
uses: baptiste0928/cargo-install@v1 uses: baptiste0928/cargo-install@v1
@@ -48,70 +46,37 @@ jobs:
crate: cargo-cranky crate: cargo-cranky
- name: check --all-features - name: check --all-features
uses: actions-rs/cargo@v1 run: cargo check --locked --all-features --all-targets
with:
command: check
args: --locked --all-features --all-targets
- name: check egui_extras --all-features - name: check egui_extras --all-features
uses: actions-rs/cargo@v1 run: cargo check --locked --all-features --all-targets -p egui_extras
with:
command: check
args: --locked --all-features --all-targets -p egui_extras
- name: check default features - name: check default features
uses: actions-rs/cargo@v1 run: cargo check --locked --all-targets
with:
command: check
args: --locked --all-targets
- name: check --no-default-features - name: check --no-default-features
uses: actions-rs/cargo@v1 run: cargo check --locked --no-default-features --lib --all-targets
with:
command: check
args: --locked --no-default-features --lib --all-targets
- name: check epaint --no-default-features - name: check epaint --no-default-features
uses: actions-rs/cargo@v1 run: cargo check --locked --no-default-features --lib --all-targets -p epaint
with:
command: check
args: --locked --no-default-features --lib --all-targets -p epaint
- name: check eframe --no-default-features - name: check eframe --no-default-features
uses: actions-rs/cargo@v1 run: cargo check --locked --no-default-features --features x11 --lib --all-targets -p eframe
with:
command: check
args: --locked --no-default-features --lib --all-targets -p eframe
- name: Test doc-tests - name: Test doc-tests
uses: actions-rs/cargo@v1 run: cargo test --doc --all-features
with:
command: test
args: --doc --all-features
- name: cargo doc --lib - name: cargo doc --lib
uses: actions-rs/cargo@v1 run: cargo doc --lib --no-deps --all-features
with:
command: doc
args: --lib --no-deps --all-features
- name: cargo doc --document-private-items - name: cargo doc --document-private-items
uses: actions-rs/cargo@v1 run: cargo doc --document-private-items --no-deps --all-features
with:
command: doc
args: --document-private-items --no-deps --all-features
- name: Test - name: Test
uses: actions-rs/cargo@v1 run: cargo test --all-features
with:
command: test
args: --all-features
- name: Cranky - name: Cranky
uses: actions-rs/cargo@v1 run: cargo cranky --all-targets --all-features -- -D warnings
with:
command: cranky
args: --all-targets --all-features -- -D warnings
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -119,13 +84,11 @@ jobs:
name: Check wasm32 + wasm-bindgen name: Check wasm32 + wasm-bindgen
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal toolchain: 1.67.0
toolchain: 1.65.0 targets: wasm32-unknown-unknown
target: wasm32-unknown-unknown
override: true
- run: sudo apt-get update && sudo apt-get install libgtk-3-dev - run: sudo apt-get update && sudo apt-get install libgtk-3-dev
@@ -138,27 +101,18 @@ jobs:
crate: cargo-cranky crate: cargo-cranky
- name: Check wasm32 egui_demo_app - name: Check wasm32 egui_demo_app
uses: actions-rs/cargo@v1 run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown
- name: Check wasm32 egui_demo_app --all-features - name: Check wasm32 egui_demo_app --all-features
uses: actions-rs/cargo@v1 run: cargo check -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
with:
command: check
args: -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features
- name: Check wasm32 eframe - name: Check wasm32 eframe
uses: actions-rs/cargo@v1 run: cargo check -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
with:
command: check
args: -p eframe --lib --no-default-features --features glow,persistence --target wasm32-unknown-unknown
- name: wasm-bindgen - name: wasm-bindgen
uses: jetli/wasm-bindgen-action@v0.1.0 uses: jetli/wasm-bindgen-action@v0.1.0
with: with:
version: "0.2.86" version: "0.2.87"
- run: ./scripts/wasm_bindgen_check.sh --skip-setup - run: ./scripts/wasm_bindgen_check.sh --skip-setup
@@ -188,13 +142,13 @@ jobs:
name: cargo-deny ${{ matrix.target }} name: cargo-deny ${{ matrix.target }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
rust-version: "1.65.0" rust-version: "1.67.0"
log-level: error log-level: error
command: check command: check
arguments: ${{ matrix.flags }} --target ${{ matrix.target }} arguments: --target ${{ matrix.target }}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -202,14 +156,12 @@ jobs:
name: android name: android
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal toolchain: 1.67.0
toolchain: 1.65.0 targets: aarch64-linux-android
target: aarch64-linux-android
override: true
- name: Set up cargo cache - name: Set up cargo cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@@ -223,18 +175,13 @@ jobs:
name: Check Windows name: Check Windows
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1 - uses: dtolnay/rust-toolchain@master
with: with:
profile: minimal toolchain: 1.67.0
toolchain: 1.65.0
override: true
- name: Set up cargo cache - name: Set up cargo cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Check - name: Check
uses: actions-rs/cargo@v1 run: cargo check --all-targets --all-features
with:
command: check
args: --all-targets --all-features

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Actions Repository - name: Checkout Actions Repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Check spelling of entire workspace - name: Check spelling of entire workspace
uses: crate-ci/typos@master uses: crate-ci/typos@master

View File

@@ -3,8 +3,8 @@ All notable changes to the `egui` crate will be documented in this file.
NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glium`](crates/egui_glium/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs!
This file is updated upon each release.
## Unreleased Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23 - A plethora of small improvements ## 0.22.0 - 2023-05-23 - A plethora of small improvements

View File

@@ -60,7 +60,7 @@ Read the section on integrations at <https://github.com/emilk/egui#integrations>
## Code Conventions ## Code Conventions
Conventions unless otherwise specified: Conventions unless otherwise specified:
* angles are in radians * angles are in radians and clock-wise
* `Vec2::X` is right and `Vec2::Y` is down. * `Vec2::X` is right and `Vec2::Y` is down.
* `Pos2::ZERO` is left top. * `Pos2::ZERO` is left top.

146
Cargo.lock generated
View File

@@ -215,9 +215,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]] [[package]]
name = "ash" name = "ash"
version = "0.37.2+1.3.238" version = "0.37.3+1.3.251"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28bf19c1f0a470be5fbf7522a308a05df06610252c5bcf5143e1b23f629a9a03" checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a"
dependencies = [ dependencies = [
"libloading 0.7.4", "libloading 0.7.4",
] ]
@@ -326,12 +326,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "atomic_refcell"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31"
[[package]] [[package]]
name = "atspi" name = "atspi"
version = "0.10.1" version = "0.10.1"
@@ -714,7 +708,7 @@ dependencies = [
"cocoa-foundation", "cocoa-foundation",
"core-foundation", "core-foundation",
"core-graphics", "core-graphics",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
"objc", "objc",
] ]
@@ -729,7 +723,7 @@ dependencies = [
"block", "block",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
"objc", "objc",
] ]
@@ -814,7 +808,7 @@ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
] ]
@@ -826,7 +820,7 @@ checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"foreign-types", "foreign-types 0.3.2",
"libc", "libc",
] ]
@@ -947,12 +941,12 @@ dependencies = [
[[package]] [[package]]
name = "d3d12" name = "d3d12"
version = "0.6.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f0de2f5a8e7bd4a9eec0e3c781992a4ce1724f68aec7d7a3715344de8b39da" checksum = "e16e44ab292b1dddfdaf7be62cfd8877df52f2f3fde5858d95bab606be259f20"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"libloading 0.7.4", "libloading 0.8.0",
"winapi", "winapi",
] ]
@@ -1395,7 +1389,6 @@ version = "0.22.0"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"ahash 0.8.3", "ahash 0.8.3",
"atomic_refcell",
"backtrace", "backtrace",
"bytemuck", "bytemuck",
"criterion", "criterion",
@@ -1501,7 +1494,28 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [ dependencies = [
"foreign-types-shared", "foreign-types-shared 0.1.1",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared 0.3.1",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
] ]
[[package]] [[package]]
@@ -1510,6 +1524,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.1.0" version = "1.1.0"
@@ -1683,9 +1703,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "glow" name = "glow"
version = "0.12.1" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e007a07a24de5ecae94160f141029e9a347282cfe25d1d58d85d845cf3130f1" checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"slotmap", "slotmap",
@@ -1770,21 +1790,21 @@ dependencies = [
[[package]] [[package]]
name = "gpu-alloc" name = "gpu-alloc"
version = "0.5.4" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22beaafc29b38204457ea030f6fb7a84c9e4dd1b86e311ba0542533453d87f62" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"gpu-alloc-types", "gpu-alloc-types",
] ]
[[package]] [[package]]
name = "gpu-alloc-types" name = "gpu-alloc-types"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
] ]
[[package]] [[package]]
@@ -2099,9 +2119,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.63" version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -2265,16 +2285,17 @@ dependencies = [
[[package]] [[package]]
name = "metal" name = "metal"
version = "0.24.0" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de11355d1f6781482d027a3b4d4de7825dcedb197bf573e0596d00008402d060" checksum = "623b5e6cefd76e58f774bd3cc0c6f5c7615c58c03a97815245a25c3c9bdee318"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 2.3.1",
"block", "block",
"core-graphics-types", "core-graphics-types",
"foreign-types", "foreign-types 0.5.0",
"log", "log",
"objc", "objc",
"paste",
] ]
[[package]] [[package]]
@@ -2312,12 +2333,12 @@ dependencies = [
[[package]] [[package]]
name = "naga" name = "naga"
version = "0.12.1" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d3edd593521f4a1dfd9b25193ed0224764572905f013d30ca5fbb85e010876" checksum = "c1ceaaa4eedaece7e4ec08c55c640ba03dbb73fb812a6570a59bcf1930d0f70e"
dependencies = [ dependencies = [
"bit-set", "bit-set",
"bitflags 1.3.2", "bitflags 2.3.1",
"codespan-reporting", "codespan-reporting",
"hexf-parse", "hexf-parse",
"indexmap", "indexmap",
@@ -3103,6 +3124,16 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "save_plot"
version = "0.1.0"
dependencies = [
"eframe",
"env_logger",
"image",
"rfd",
]
[[package]] [[package]]
name = "scoped-tls" name = "scoped-tls"
version = "1.0.1" version = "1.0.1"
@@ -3860,9 +3891,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.86" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"wasm-bindgen-macro", "wasm-bindgen-macro",
@@ -3870,9 +3901,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-backend" name = "wasm-bindgen-backend"
version = "0.2.86" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"log", "log",
@@ -3897,9 +3928,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.86" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -3907,9 +3938,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.86" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3920,9 +3951,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.86" version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
@@ -4011,9 +4042,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.63" version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4057,9 +4088,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu" name = "wgpu"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13edd72c7b08615b7179dd7e778ee3f0bdc870ef2de9019844ff2cceeee80b11" checksum = "7472f3b69449a8ae073f6ec41d05b6f846902d92a6c45313c50cb25857b736ce"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"cfg-if", "cfg-if",
@@ -4081,9 +4112,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-core" name = "wgpu-core"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "625bea30a0ba50d88025f95c80211d1a85c86901423647fb74f397f614abbd9a" checksum = "ecf7454d9386f602f7399225c92dd2fbdcde52c519bc8fb0bd6fbeb388075dc2"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-vec", "bit-vec",
@@ -4104,9 +4135,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-hal" name = "wgpu-hal"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41af2ea7d87bd41ad0a37146252d5f7c26490209f47f544b2ee3b3ff34c7732e" checksum = "6654a13885a17f475e8324efb46dc6986d7aaaa98353330f8de2077b153d0101"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"arrayvec", "arrayvec",
@@ -4116,7 +4147,6 @@ dependencies = [
"block", "block",
"core-graphics-types", "core-graphics-types",
"d3d12", "d3d12",
"foreign-types",
"glow", "glow",
"gpu-alloc", "gpu-alloc",
"gpu-allocator", "gpu-allocator",
@@ -4146,9 +4176,9 @@ dependencies = [
[[package]] [[package]]
name = "wgpu-types" name = "wgpu-types"
version = "0.16.0" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd33a976130f03dcdcd39b3810c0c3fc05daf86f0aaf867db14bfb7c4a9a32b" checksum = "ee64d7398d0c2f9ca48922c902ef69c42d000c759f3db41e355f4a570b052b67"
dependencies = [ dependencies = [
"bitflags 2.3.1", "bitflags 2.3.1",
"js-sys", "js-sys",
@@ -4474,9 +4504,9 @@ dependencies = [
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.13" version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d8f380ae16a37b30e6a2cf67040608071384b1450c189e61bea3ff57cde922d" checksum = "5a56c84a8ccd4258aed21c92f70c0f6dea75356b6892ae27c24139da456f9336"
[[package]] [[package]]
name = "xmlparser" name = "xmlparser"

View File

@@ -36,3 +36,4 @@ opt-level = 2
[workspace.dependencies] [workspace.dependencies]
thiserror = "1.0.37" thiserror = "1.0.37"
wgpu = { version = "0.17.0", features = ["fragile-send-sync-non-atomic-wasm"] }

View File

@@ -91,6 +91,7 @@ warn = [
"clippy::trailing_empty_array", "clippy::trailing_empty_array",
"clippy::trait_duplication_in_bounds", "clippy::trait_duplication_in_bounds",
"clippy::unimplemented", "clippy::unimplemented",
"clippy::uninlined_format_args",
"clippy::unnecessary_wraps", "clippy::unnecessary_wraps",
"clippy::unnested_or_patterns", "clippy::unnested_or_patterns",
"clippy::unused_peekable", "clippy::unused_peekable",

View File

@@ -1,6 +1,6 @@
# There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm. # There is also a scripts/clippy_wasm/clippy.toml which forbids some mthods that are not available in wasm.
msrv = "1.65" msrv = "1.67"
# Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown # Allow-list of words for markdown in dosctrings https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
doc-valid-idents = [ doc-valid-idents = [

View File

@@ -2,7 +2,8 @@
All notable changes to the `ecolor` crate will be noted in this file. All notable changes to the `ecolor` crate will be noted in this file.
## Unreleased This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23 ## 0.22.0 - 2023-05-23

View File

@@ -7,7 +7,7 @@ authors = [
] ]
description = "Color structs and color conversion utilities" description = "Color structs and color conversion utilities"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui" homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"

View File

@@ -3,9 +3,9 @@ All notable changes to the `eframe` crate.
NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs! NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/CHANGELOG.md), [`egui_glow`](../egui_glow/CHANGELOG.md),and [`egui-wgpu`](../egui-wgpu/CHANGELOG.md) have their own changelogs!
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## Unreleased
* Expose raw window and display handles in `CreationContext` and `Frame`
## 0.22.0 - 2023-05-23 ## 0.22.0 - 2023-05-23
* Fix: `request_repaint_after` works even when called from background thread [#2939](https://github.com/emilk/egui/pull/2939) * Fix: `request_repaint_after` works even when called from background thread [#2939](https://github.com/emilk/egui/pull/2939)

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively" description = "egui framework - write GUI apps that compiles to web and/or natively"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe" homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -27,7 +27,14 @@ targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[features] [features]
default = ["accesskit", "default_fonts", "glow"] default = [
"accesskit",
"default_fonts",
"glow",
"wayland",
"winit/default",
"x11",
]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"] accesskit = ["egui/accesskit", "egui-winit/accesskit"]
@@ -42,6 +49,9 @@ glow = ["dep:glow", "dep:egui_glow", "dep:glutin", "dep:glutin-winit"]
## Enables wayland support and fixes clipboard issue. ## Enables wayland support and fixes clipboard issue.
wayland = ["egui-winit/wayland"] wayland = ["egui-winit/wayland"]
## Enables compiling for x11.
x11 = ["egui-winit/x11"]
## Enable saving app state to disk. ## Enable saving app state to disk.
persistence = [ persistence = [
"directories-next", "directories-next",
@@ -109,7 +119,7 @@ image = { version = "0.24", default-features = false, features = [
"png", "png",
] } # Needed for app icon ] } # Needed for app icon
raw-window-handle = { version = "0.5.0" } raw-window-handle = { version = "0.5.0" }
winit = "0.28.1" winit = { version = "0.28.1", default-features = false }
# optional native: # optional native:
directories-next = { version = "2", optional = true } directories-next = { version = "2", optional = true }
@@ -123,7 +133,7 @@ pollster = { version = "0.3", optional = true } # needed for wgpu
glutin = { version = "0.30", optional = true } glutin = { version = "0.30", optional = true }
glutin-winit = { version = "0.3.0", optional = true } glutin-winit = { version = "0.3.0", optional = true }
puffin = { version = "0.16", optional = true } puffin = { version = "0.16", optional = true }
wgpu = { version = "0.16.0", optional = true } wgpu = { workspace = true, optional = true }
# mac: # mac:
[target.'cfg(any(target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -140,7 +150,7 @@ winapi = "0.3.9"
bytemuck = "1.7" bytemuck = "1.7"
js-sys = "0.3" js-sys = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
wasm-bindgen = "0.2.86" wasm-bindgen = "0.2.87"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.58", features = [ web-sys = { version = "0.3.58", features = [
"BinaryType", "BinaryType",
@@ -190,4 +200,4 @@ web-sys = { version = "0.3.58", features = [
egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit egui-wgpu = { version = "0.22.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit
raw-window-handle = { version = "0.5.2", optional = true } raw-window-handle = { version = "0.5.2", optional = true }
tts = { version = "0.25", optional = true, default-features = false } tts = { version = "0.25", optional = true, default-features = false }
wgpu = { version = "0.16.0", optional = true } wgpu = { workspace = true, optional = true }

View File

@@ -437,7 +437,7 @@ pub struct NativeOptions {
/// will be used instead. /// will be used instead.
/// ///
/// ### On Wayland /// ### On Wayland
/// On Wauland this sets the Application ID for the window. /// On Wayland this sets the Application ID for the window.
/// ///
/// The application ID is used in several places of the compositor, e.g. for /// The application ID is used in several places of the compositor, e.g. for
/// grouping windows of the same application. It is also important for /// grouping windows of the same application. It is also important for

View File

@@ -7,7 +7,7 @@
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there! //! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//! //!
//! In short, you implement [`App`] (especially [`App::update`]) and then //! In short, you implement [`App`] (especially [`App::update`]) and then
//! call [`crate::run_native`] from your `main.rs`, and/or call `eframe::start_web` from your `lib.rs`. //! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`.
//! //!
//! ## Usage, native: //! ## Usage, native:
//! ``` no_run //! ``` no_run
@@ -274,6 +274,7 @@ pub fn run_simple_native(
struct SimpleApp<U> { struct SimpleApp<U> {
update_fun: U, update_fun: U,
} }
impl<U: FnMut(&egui::Context, &mut Frame)> App for SimpleApp<U> { impl<U: FnMut(&egui::Context, &mut Frame)> App for SimpleApp<U> {
fn update( fn update(
&mut self, &mut self,

View File

@@ -9,7 +9,7 @@ use raw_window_handle::{HasRawDisplayHandle as _, HasRawWindowHandle as _};
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
use egui::accesskit; use egui::accesskit;
use egui::{NumExt as _, ViewportBuilder, ViewportId, ViewportRender}; use egui::{mutex::RwLock, NumExt as _, ViewportBuilder, ViewportId, ViewportRender};
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit; use egui_winit::accesskit_winit;
use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings}; use egui_winit::{native_pixels_per_point, EventResponse, WindowSettings};
@@ -125,9 +125,12 @@ pub fn window_builder<E>(
} }
#[cfg(all(feature = "wayland", target_os = "linux"))] #[cfg(all(feature = "wayland", target_os = "linux"))]
if let Some(app_id) = &native_options.app_id { {
use winit::platform::wayland::WindowBuilderExtWayland as _; use winit::platform::wayland::WindowBuilderExtWayland as _;
window_builder = window_builder.with_name(app_id, ""); match &native_options.app_id {
Some(app_id) => window_builder = window_builder.with_name(app_id, ""),
None => window_builder = window_builder.with_name(title, ""),
}
} }
if let Some(min_size) = *min_window_size { if let Some(min_size) = *min_window_size {
@@ -143,10 +146,11 @@ pub fn window_builder<E>(
let inner_size_points = if let Some(mut window_settings) = window_settings { let inner_size_points = if let Some(mut window_settings) = window_settings {
// Restore pos/size from previous session // Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)] window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop));
window_settings.clamp_window_to_sane_position(event_loop); window_settings.clamp_position_to_monitors(event_loop);
window_builder = window_settings.initialize_window(window_builder);
window_builder = window_settings.initialize_window_builder(window_builder);
window_settings.inner_size_points() window_settings.inner_size_points()
} else { } else {
if let Some(pos) = *initial_window_pos { if let Some(pos) = *initial_window_pos {
@@ -176,12 +180,14 @@ pub fn window_builder<E>(
} }
} }
} }
window_builder window_builder
} }
pub fn apply_native_options_to_window( pub fn apply_native_options_to_window(
window: &winit::window::Window, window: &winit::window::Window,
native_options: &crate::NativeOptions, native_options: &crate::NativeOptions,
window_settings: Option<WindowSettings>,
) { ) {
use winit::window::WindowLevel; use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top { window.set_window_level(if native_options.always_on_top {
@@ -189,6 +195,10 @@ pub fn apply_native_options_to_window(
} else { } else {
WindowLevel::Normal WindowLevel::Normal
}); });
if let Some(window_settings) = window_settings {
window_settings.initialize_window(window);
}
} }
fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 { fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
@@ -340,6 +350,7 @@ pub struct EpiIntegration {
pending_full_output: egui::FullOutput, pending_full_output: egui::FullOutput,
/// When set, it is time to close the native window. /// When set, it is time to close the native window.
close: bool, close: bool,
can_drag_window: bool, can_drag_window: bool,
window_state: WindowState, window_state: WindowState,
follow_system_theme: bool, follow_system_theme: bool,
@@ -588,27 +599,37 @@ impl EpiIntegration {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Persistence stuff: // Persistence stuff:
pub fn maybe_autosave(&mut self, app: &mut dyn epi::App, window: &winit::window::Window) { pub fn maybe_autosave(
&mut self,
app: &mut dyn epi::App,
window: Arc<RwLock<winit::window::Window>>,
) {
let now = std::time::Instant::now(); let now = std::time::Instant::now();
if now - self.last_auto_save > app.auto_save_interval() { if now - self.last_auto_save > app.auto_save_interval() {
self.save(app, window); self.save(app, Some(window));
self.last_auto_save = now; self.last_auto_save = now;
} }
} }
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: &winit::window::Window) { pub fn save(
&mut self,
_app: &mut dyn epi::App,
_window: Option<Arc<RwLock<winit::window::Window>>>,
) {
#[cfg(feature = "persistence")] #[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() { if let Some(storage) = self.frame.storage_mut() {
crate::profile_function!(); crate::profile_function!();
if _app.persist_native_window() { if let Some(window) = _window {
crate::profile_scope!("native_window"); if _app.persist_native_window() {
epi::set_value( crate::profile_scope!("native_window");
storage, epi::set_value(
STORAGE_WINDOW_KEY, storage,
&WindowSettings::from_display(_window), STORAGE_WINDOW_KEY,
); &WindowSettings::from_display(&window.read()),
);
}
} }
if _app.persist_egui_memory() { if _app.persist_egui_memory() {
crate::profile_scope!("egui_memory"); crate::profile_scope!("egui_memory");

View File

@@ -80,14 +80,45 @@ impl crate::Storage for FileStorage {
join_handle.join().ok(); join_handle.join().ok();
} }
let join_handle = std::thread::spawn(move || { match std::thread::Builder::new()
let file = std::fs::File::create(&file_path).unwrap(); .name("eframe_persist".to_owned())
let config = Default::default(); .spawn(move || {
ron::ser::to_writer_pretty(file, &kv, config).unwrap(); save_to_disk(&file_path, &kv);
log::trace!("Persisted to {:?}", file_path); }) {
}); Ok(join_handle) => {
self.last_save_join_handle = Some(join_handle);
}
Err(err) => {
log::warn!("Failed to spawn thread to save app state: {err}");
}
}
}
}
}
self.last_save_join_handle = Some(join_handle); fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
crate::profile_function!();
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
if let Err(err) = std::fs::create_dir_all(parent_dir) {
log::warn!("Failed to create directory {parent_dir:?}: {err}");
}
}
}
match std::fs::File::create(file_path) {
Ok(file) => {
let config = Default::default();
if let Err(err) = ron::ser::to_writer_pretty(file, &kv, config) {
log::warn!("Failed to serialize app state: {err}");
} else {
log::trace!("Persisted to {:?}", file_path);
}
}
Err(err) => {
log::warn!("Failed to create file {file_path:?}: {err}");
} }
} }
} }

View File

@@ -24,6 +24,7 @@ pub enum UserEvent {
RequestRepaint { RequestRepaint {
id: ViewportId, id: ViewportId,
when: Instant, when: Instant,
/// What the frame number was when the repaint was _requested_. /// What the frame number was when the repaint was _requested_.
frame_nr: u64, frame_nr: u64,
}, },
@@ -154,12 +155,14 @@ fn run_and_return(
// Platform-dependent event handlers to workaround a winit bug // Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619 // See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => { #[cfg(target_os = "windows")]
winit::event::Event::RedrawEventsCleared => {
// windows_next_repaint_times.clear(); // windows_next_repaint_times.clear();
// winit_app.run_ui_and_paint(None) // winit_app.run_ui_and_paint(None)
vec![EventResult::Wait] vec![EventResult::Wait]
} }
winit::event::Event::RedrawRequested(window_id) if !cfg!(windows) => { #[cfg(not(target_os = "windows"))]
winit::event::Event::RedrawRequested(window_id) => {
windows_next_repaint_times.remove(window_id); windows_next_repaint_times.remove(window_id);
winit_app.run_ui_and_paint(*window_id) winit_app.run_ui_and_paint(*window_id)
} }
@@ -214,7 +217,7 @@ fn run_and_return(
} }
EventResult::RepaintNow(window_id) => { EventResult::RepaintNow(window_id) => {
log::trace!("Repaint caused by winit::Event: {:?}", event); log::trace!("Repaint caused by winit::Event: {:?}", event);
if cfg!(windows) { if cfg!(target_os = "windows") {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
windows_next_repaint_times.remove(&window_id); windows_next_repaint_times.remove(&window_id);
@@ -280,7 +283,7 @@ fn run_and_return(
// //
// Note that this approach may cause issues on macOS (emilk/egui#2768); therefore, // Note that this approach may cause issues on macOS (emilk/egui#2768); therefore,
// we only apply this approach on Windows to minimize the affect. // we only apply this approach on Windows to minimize the affect.
#[cfg(windows)] #[cfg(target_os = "windows")]
{ {
event_loop.run_return(|_, _, control_flow| { event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit(); control_flow.set_exit();
@@ -307,12 +310,12 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
// Platform-dependent event handlers to workaround a winit bug // Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619 // See: https://github.com/rust-windowing/winit/issues/1619
winit::event::Event::RedrawEventsCleared if cfg!(windows) => { winit::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => {
// windows_next_repaint_times.clear(); // windows_next_repaint_times.clear();
// winit_app.run_ui_and_paint(None) // winit_app.run_ui_and_paint(None)
vec![] vec![]
} }
winit::event::Event::RedrawRequested(window_id) if !cfg!(windows) => { winit::event::Event::RedrawRequested(window_id) if !cfg!(target_os = "windows") => {
windows_next_repaint_times.remove(&window_id); windows_next_repaint_times.remove(&window_id);
winit_app.run_ui_and_paint(window_id) winit_app.run_ui_and_paint(window_id)
} }
@@ -349,7 +352,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
match event { match event {
EventResult::Wait => {} EventResult::Wait => {}
EventResult::RepaintNow(window_id) => { EventResult::RepaintNow(window_id) => {
if cfg!(windows) { if cfg!(target_os = "windows") {
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280 // Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
windows_next_repaint_times.remove(&window_id); windows_next_repaint_times.remove(&window_id);
@@ -828,7 +831,11 @@ mod glow_integration {
if let Some(window) = &glutin_window_context.windows.get(&ViewportId::MAIN) { if let Some(window) = &glutin_window_context.windows.get(&ViewportId::MAIN) {
let window = window.read(); let window = window.read();
if let Some(window) = &window.window { if let Some(window) = &window.window {
epi_integration::apply_native_options_to_window(&window.read(), native_options); epi_integration::apply_native_options_to_window(
&window.read(),
native_options,
window_settings,
);
} }
} }
@@ -862,7 +869,7 @@ mod glow_integration {
let painter = let painter =
egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version) egui_glow::Painter::new(gl.clone(), "", self.native_options.shader_version)
.unwrap_or_else(|error| panic!("some OpenGL error occurred {}\n", error)); .unwrap_or_else(|err| panic!("An OpenGL error occurred: {err}\n"));
let system_theme = system_theme( let system_theme = system_theme(
&gl_window &gl_window
@@ -1209,15 +1216,13 @@ mod glow_integration {
if let Some(running) = self.running.write().take() { if let Some(running) = self.running.write().take() {
running.integration.write().save( running.integration.write().save(
running.app.write().as_mut(), running.app.write().as_mut(),
&running running
.glutin_ctx .glutin_ctx
.read() .read()
.window(ViewportId::MAIN) .window(ViewportId::MAIN)
.read() .read()
.window .window
.as_ref() .clone(),
.unwrap()
.read(),
); );
running.app.write().on_exit(Some(&running.gl)); running.app.write().on_exit(Some(&running.gl));
running.painter.write().destroy(); running.painter.write().destroy();
@@ -1429,10 +1434,8 @@ mod glow_integration {
.collect::<Vec<EventResult>>() .collect::<Vec<EventResult>>()
}; };
integration.maybe_autosave( integration
app.write().as_mut(), .maybe_autosave(app.write().as_mut(), win.read().window.clone().unwrap());
&win.read().window.as_ref().unwrap().read(),
);
if win.read().window.as_ref().unwrap().read().is_minimized() == Some(true) { if win.read().window.as_ref().unwrap().read().is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU: // On Mac, a minimized Window uses up all CPU:
@@ -1809,7 +1812,11 @@ mod wgpu_integration {
let window_builder = let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings); epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = create_winit_window_builder(&window_builder).build(event_loop)?; let window = create_winit_window_builder(&window_builder).build(event_loop)?;
epi_integration::apply_native_options_to_window(&window, native_options); epi_integration::apply_native_options_to_window(
&window,
native_options,
window_settings,
);
Ok((window, window_builder)) Ok((window, window_builder))
} }
@@ -2104,13 +2111,11 @@ mod wgpu_integration {
fn save_and_destroy(&mut self) { fn save_and_destroy(&mut self) {
if let Some(mut running) = self.running.take() { if let Some(mut running) = self.running.take() {
if let Some((Some(window), _, _, _, _)) = if let Some((window, _, _, _, _)) = running.windows.read().get(&ViewportId::MAIN) {
running.windows.read().get(&ViewportId::MAIN)
{
running running
.integration .integration
.write() .write()
.save(running.app.as_mut(), &window.read()); .save(running.app.as_mut(), window.clone());
} }
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@@ -2276,7 +2281,7 @@ mod wgpu_integration {
let Some((_, (Some(window), _, _, _, _))) = windows_id.get(&window_id).and_then(|id|(windows.read().get(id).map(|w|(*id, w.clone())))) else{return vec![]}; let Some((_, (Some(window), _, _, _, _))) = windows_id.get(&window_id).and_then(|id|(windows.read().get(id).map(|w|(*id, w.clone())))) else{return vec![]};
integration integration
.write() .write()
.maybe_autosave(app.as_mut(), &window.read()); .maybe_autosave(app.as_mut(), window.clone());
if window.read().is_minimized() == Some(true) { if window.read().is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU: // On Mac, a minimized Window uses up all CPU:

View File

@@ -68,7 +68,7 @@ pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web
id: egui::TouchId::from(touch.identifier()), id: egui::TouchId::from(touch.identifier()),
phase, phase,
pos: pos_from_touch(canvas_origin, &touch), pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(), force: Some(touch.force()),
}); });
} }
} }

View File

@@ -104,7 +104,7 @@ pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement { pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
canvas_element(canvas_id) canvas_element(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id {:?}", canvas_id)) .unwrap_or_else(|| panic!("Failed to find canvas with id {canvas_id:?}"))
} }
fn canvas_origin(canvas_id: &str) -> egui::Pos2 { fn canvas_origin(canvas_id: &str) -> egui::Pos2 {

View File

@@ -104,8 +104,7 @@ pub fn install_text_agent(runner_ref: &WebRunner) -> Result<(), JsValue> {
runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| { runner_ref.add_event_listener(&input, "focusout", move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again. // Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!( let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()", "document.getElementById('{AGENT_ID}').focus()"
AGENT_ID
)); ));
window window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10) .set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
@@ -221,8 +220,8 @@ pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<(
let x = (x - canvas.offset_width() as f32 / 2.0) let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32); .min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?; style.set_property("position", "absolute").ok()?;
style.set_property("top", &format!("{}px", y)).ok()?; style.set_property("top", &format!("{y}px")).ok()?;
style.set_property("left", &format!("{}px", x)).ok() style.set_property("left", &format!("{x}px")).ok()
}) })
} else { } else {
style.set_property("position", "absolute").ok()?; style.set_property("position", "absolute").ok()?;

View File

@@ -27,7 +27,7 @@ impl WebPainterGlow {
let gl = std::sync::Arc::new(gl); let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None) let painter = egui_glow::Painter::new(gl, shader_prefix, None)
.map_err(|error| format!("Error starting glow painter: {}", error))?; .map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self { Ok(Self {
canvas, canvas,

View File

@@ -87,8 +87,7 @@ impl WebPainterWgpu {
} else { } else {
// Workaround for https://github.com/gfx-rs/wgpu/issues/3710: // Workaround for https://github.com/gfx-rs/wgpu/issues/3710:
// Don't use `create_surface_from_canvas`, but `create_surface` instead! // Don't use `create_surface_from_canvas`, but `create_surface` instead!
let raw_window = let raw_window = EguiWebWindow(egui::util::hash(("egui on wgpu", canvas_id)) as u32);
EguiWebWindow(egui::util::hash(&format!("egui on wgpu {canvas_id}")) as u32);
canvas.set_attribute("data-raw-handle", &raw_window.0.to_string()); canvas.set_attribute("data-raw-handle", &raw_window.0.to_string());
#[allow(unsafe_code)] #[allow(unsafe_code)]

View File

@@ -2,8 +2,8 @@
All notable changes to the `egui-wgpu` integration will be noted in this file. All notable changes to the `egui-wgpu` integration will be noted in this file.
## Unreleased This file is updated upon each release.
* Fix panic on wgpu GL backend due to new screenshot capability ([#3068](https://github.com/emilk/egui/issues/3068), [#3078](https://github.com/emilk/egui/pull/3078) Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23 ## 0.22.0 - 2023-05-23

View File

@@ -8,7 +8,7 @@ authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
] ]
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu" homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -45,13 +45,13 @@ bytemuck = "1.7"
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
thiserror.workspace = true thiserror.workspace = true
type-map = "0.5.0" type-map = "0.5.0"
wgpu = "0.16.0" wgpu.workspace = true
#! ### Optional dependencies #! ### Optional dependencies
## Enable this when generating docs. ## Enable this when generating docs.
document-features = { version = "0.2", optional = true } document-features = { version = "0.2", optional = true }
winit = { version = "0.28", optional = true } winit = { version = "0.28", default-features = false, optional = true }
# Native: # Native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -560,7 +560,7 @@ impl Renderer {
} else { } else {
// allocate a new texture // allocate a new texture
// Use same label for all resources associated with this texture id (no point in retyping the type) // Use same label for all resources associated with this texture id (no point in retyping the type)
let label_str = format!("egui_texid_{:?}", id); let label_str = format!("egui_texid_{id:?}");
let label = Some(label_str.as_str()); let label = Some(label_str.as_str());
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label, label,
@@ -904,8 +904,7 @@ fn create_sampler(
}; };
device.create_sampler(&wgpu::SamplerDescriptor { device.create_sampler(&wgpu::SamplerDescriptor {
label: Some(&format!( label: Some(&format!(
"egui sampler (mag: {:?}, min {:?})", "egui sampler (mag: {mag_filter:?}, min {min_filter:?})"
mag_filter, min_filter
)), )),
mag_filter, mag_filter,
min_filter, min_filter,

View File

@@ -298,7 +298,7 @@ impl Painter {
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: self.msaa_samples,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: depth_format, format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT usage: wgpu::TextureUsages::RENDER_ATTACHMENT

View File

@@ -1,8 +1,8 @@
# Changelog for egui-winit # Changelog for egui-winit
All notable changes to the `egui-winit` integration will be noted in this file. All notable changes to the `egui-winit` integration will be noted in this file.
This file is updated upon each release.
## Unreleased Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23 ## 0.22.0 - 2023-05-23

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit" description = "Bindings for using egui with winit"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit" homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -18,7 +18,7 @@ all-features = true
[features] [features]
default = ["clipboard", "links", "wayland", "winit/default"] default = ["clipboard", "links", "wayland", "winit/default", "x11"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/). ## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["accesskit_winit", "egui/accesskit"] accesskit = ["accesskit_winit", "egui/accesskit"]
@@ -42,6 +42,9 @@ serde = ["egui/serde", "dep:serde"]
## Enables Wayland support. ## Enables Wayland support.
wayland = ["winit/wayland"] wayland = ["winit/wayland"]
## Enables compiling for x11.
x11 = ["winit/x11"]
# Allow crates to choose an android-activity backend via Winit # Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can # - It's important that most applications should not have to depend on android-activity directly, and can
# rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link) # rely on Winit to pull in a suitable version (unlike most Rust crates, any version conflicts won't link)

View File

@@ -3,7 +3,7 @@ use raw_window_handle::HasRawDisplayHandle;
/// Handles interfacing with the OS clipboard. /// Handles interfacing with the OS clipboard.
/// ///
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard, /// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works works within the same app is used instead. /// then a fallback clipboard that just works within the same app is used instead.
pub struct Clipboard { pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))] #[cfg(all(feature = "arboard", not(target_os = "android")))]
arboard: Option<arboard::Clipboard>, arboard: Option<arboard::Clipboard>,

View File

@@ -313,6 +313,7 @@ impl State {
} }
WindowEvent::KeyboardInput { input, .. } => { WindowEvent::KeyboardInput { input, .. } => {
self.on_keyboard_input(input); self.on_keyboard_input(input);
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = egui_ctx.wants_keyboard_input() let consumed = egui_ctx.wants_keyboard_input()
|| input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab); || input.virtual_keycode == Some(winit::event::VirtualKeyCode::Tab);
EventResponse { EventResponse {
@@ -454,7 +455,7 @@ impl State {
id: egui::TouchId(0), id: egui::TouchId(0),
phase: egui::TouchPhase::Start, phase: egui::TouchPhase::Start,
pos, pos,
force: 0.0, force: None,
}); });
} else { } else {
self.any_pointer_button_down = false; self.any_pointer_button_down = false;
@@ -466,7 +467,7 @@ impl State {
id: egui::TouchId(0), id: egui::TouchId(0),
phase: egui::TouchPhase::End, phase: egui::TouchPhase::End,
pos, pos,
force: 0.0, force: None,
}); });
}; };
} }
@@ -492,7 +493,7 @@ impl State {
id: egui::TouchId(0), id: egui::TouchId(0),
phase: egui::TouchPhase::Move, phase: egui::TouchPhase::Move,
pos: pos_in_points, pos: pos_in_points,
force: 0.0, force: None,
}); });
} }
} else { } else {
@@ -518,13 +519,13 @@ impl State {
touch.location.y as f32 / self.pixels_per_point(), touch.location.y as f32 / self.pixels_per_point(),
), ),
force: match touch.force { force: match touch.force {
Some(winit::event::Force::Normalized(force)) => force as f32, Some(winit::event::Force::Normalized(force)) => Some(force as f32),
Some(winit::event::Force::Calibrated { Some(winit::event::Force::Calibrated {
force, force,
max_possible_force, max_possible_force,
.. ..
}) => (force / max_possible_force) as f32, }) => Some((force / max_possible_force) as f32),
None => 0_f32, None => None,
}, },
}); });
// If we're not yet translating a touch or we're translating this very // If we're not yet translating a touch or we're translating this very

View File

@@ -1,13 +1,15 @@
use egui::ViewportBuilder; use egui::ViewportBuilder;
/// Can be used to store native window settings (position and size). /// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct WindowSettings { pub struct WindowSettings {
/// Position of window in physical pixels. This is either /// Position of window content in physical pixels.
/// the inner or outer position depending on the platform. inner_position_pixels: Option<egui::Pos2>,
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>, /// Position of window frame/titlebar in physical pixels.
outer_position_pixels: Option<egui::Pos2>,
fullscreen: bool, fullscreen: bool,
@@ -18,22 +20,20 @@ pub struct WindowSettings {
impl WindowSettings { impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self { pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor()); let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows. let inner_position_pixels = window
window .inner_position()
.inner_position() .ok()
.ok() .map(|p| egui::pos2(p.x as f32, p.y as f32));
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else { let outer_position_pixels = window
// Other platforms use the outer position. .outer_position()
window .ok()
.outer_position() .map(|p| egui::pos2(p.x as f32, p.y as f32));
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};
Self { Self {
position, inner_position_pixels,
outer_position_pixels,
fullscreen: window.fullscreen().is_some(), fullscreen: window.fullscreen().is_some(),
@@ -48,13 +48,15 @@ impl WindowSettings {
self.inner_size_points self.inner_size_points
} }
pub fn initialize_window(&self, mut window: ViewportBuilder) -> ViewportBuilder { pub fn initialize_window_builder(&self, mut window: ViewportBuilder) -> ViewportBuilder {
// If the app last ran on two monitors and only one is now connected, then // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
// the given position is invalid. // See [`winit::window::WindowBuilder::with_position`] for details.
// If this happens on Mac, the window is clamped into valid area. let pos_px = if cfg!(target_os = "macos") {
// If this happens on Windows, the clamping behavior is managed by the function self.inner_position_pixels
// clamp_window_to_sane_position. } else {
if let Some(pos) = self.position { self.outer_position_pixels
};
if let Some(pos) = pos_px {
window = window.with_position(Some((pos.x as i32, pos.y as i32))); window = window.with_position(Some((pos.x as i32, pos.y as i32)));
} }
@@ -70,68 +72,103 @@ impl WindowSettings {
} }
} }
pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) { pub fn initialize_window(&self, window: &winit::window::Window) {
if cfg!(target_os = "macos") {
// Mac sometimes has problems restoring the window to secondary monitors
// using only `WindowBuilder::with_position`, so we need this extra step:
if let Some(pos) = self.outer_position_pixels {
window.set_outer_position(winit::dpi::PhysicalPosition { x: pos.x, y: pos.y });
}
}
}
pub fn clamp_size_to_sane_values(&mut self, largest_monitor_size_points: egui::Vec2) {
use egui::NumExt as _; use egui::NumExt as _;
if let Some(size) = &mut self.inner_size_points { if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows // Prevent ridiculously small windows:
let min_size = egui::Vec2::splat(64.0); let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size); *size = size.at_least(min_size);
*size = size.at_most(max_size);
// Make sure we don't try to create a window larger than the largest monitor
// because on Linux that can lead to a crash.
*size = size.at_most(largest_monitor_size_points);
} }
} }
pub fn clamp_window_to_sane_position<E>( pub fn clamp_position_to_monitors<E>(
&mut self, &mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>, event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) { ) {
if let (Some(position), Some(inner_size_points)) = // If the app last ran on two monitors and only one is now connected, then
(&mut self.position, &self.inner_size_points) // the given position is invalid.
{ // If this happens on Mac, the window is clamped into valid area.
let monitors = event_loop.available_monitors(); // If this happens on Windows, the window becomes invisible to the user 🤦‍♂️
// default to primary monitor, in case the correct monitor was disconnected. // So on Windows we clamp the position to the monitor it is on.
let mut active_monitor = if let Some(active_monitor) = event_loop if !cfg!(target_os = "windows") {
.primary_monitor() return;
.or_else(|| event_loop.available_monitors().next()) }
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position.x as i32)) let Some(inner_size_points) = self.inner_size_points else { return; };
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32); if let Some(pos_px) = &mut self.inner_position_pixels {
// Add size of title bar. This is 32 px by default in Win 10/11. clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
if cfg!(target_os = "windows") { }
inner_size_pixels += if let Some(pos_px) = &mut self.outer_position_pixels {
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32); clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
} }
} }
} }
fn clamp_pos_to_monitors<E>(
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
window_size_pts: egui::Vec2,
position_px: &mut egui::Pos2,
) {
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let window_size_px = window_size_pts * (monitor.scale_factor() as f32);
let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
..(monitor.position().y + monitor.size().height as i32);
if monitor_x_range.contains(&(position_px.x as i32))
&& monitor_y_range.contains(&(position_px.y as i32))
{
active_monitor = monitor;
}
}
let mut window_size_px = window_size_pts * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
window_size_px += egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size_px = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position_px = position_px.clamp(monitor_position, monitor_position + window_size);
}

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "An easy-to-use immediate mode GUI that runs on both web and native" description = "An easy-to-use immediate mode GUI that runs on both web and native"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui" homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "../../README.md" readme = "../../README.md"

View File

@@ -426,7 +426,7 @@ impl Prepared {
temporarily_invisible: _, temporarily_invisible: _,
} = self; } = self;
state.size = content_ui.min_rect().size(); state.size = content_ui.min_size();
ctx.memory_mut(|m| m.areas.set_state(layer_id, state)); ctx.memory_mut(|m| m.areas.set_state(layer_id, state));

View File

@@ -36,6 +36,10 @@ impl CollapsingState {
ctx.data_mut(|d| d.insert_persisted(self.id, self.state)); ctx.data_mut(|d| d.insert_persisted(self.id, self.state));
} }
pub fn remove(&self, ctx: &Context) {
ctx.data_mut(|d| d.remove::<InnerState>(self.id));
}
pub fn id(&self) -> Id { pub fn id(&self) -> Id {
self.id self.id
} }

View File

@@ -193,9 +193,7 @@ impl Frame {
let where_to_put_background = ui.painter().add(Shape::Noop); let where_to_put_background = ui.painter().add(Shape::Noop);
let outer_rect_bounds = ui.available_rect_before_wrap(); let outer_rect_bounds = ui.available_rect_before_wrap();
let mut inner_rect = outer_rect_bounds; let mut inner_rect = (self.inner_margin + self.outer_margin).shrink_rect(outer_rect_bounds);
inner_rect.min += self.outer_margin.left_top() + self.inner_margin.left_top();
inner_rect.max -= self.outer_margin.right_bottom() + self.inner_margin.right_bottom();
// Make sure we don't shrink to the negative: // Make sure we don't shrink to the negative:
inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x);
@@ -256,17 +254,13 @@ impl Frame {
impl Prepared { impl Prepared {
fn paint_rect(&self) -> Rect { fn paint_rect(&self) -> Rect {
let mut rect = self.content_ui.min_rect(); self.frame
rect.min -= self.frame.inner_margin.left_top(); .inner_margin
rect.max += self.frame.inner_margin.right_bottom(); .expand_rect(self.content_ui.min_rect())
rect
} }
fn content_with_margin(&self) -> Rect { fn content_with_margin(&self) -> Rect {
let mut rect = self.content_ui.min_rect(); (self.frame.inner_margin + self.frame.outer_margin).expand_rect(self.content_ui.min_rect())
rect.min -= self.frame.inner_margin.left_top() + self.frame.outer_margin.left_top();
rect.max += self.frame.inner_margin.right_bottom() + self.frame.outer_margin.right_bottom();
rect
} }
pub fn end(self, ui: &mut Ui) -> Response { pub fn end(self, ui: &mut Ui) -> Response {

View File

@@ -15,8 +15,6 @@
//! //!
//! Add your [`Window`]:s after any top-level panels. //! Add your [`Window`]:s after any top-level panels.
use std::ops::RangeInclusive;
use crate::*; use crate::*;
/// State regarding panels. /// State regarding panels.
@@ -99,7 +97,7 @@ pub struct SidePanel {
resizable: bool, resizable: bool,
show_separator_line: bool, show_separator_line: bool,
default_width: f32, default_width: f32,
width_range: RangeInclusive<f32>, width_range: Rangef,
} }
impl SidePanel { impl SidePanel {
@@ -122,7 +120,7 @@ impl SidePanel {
resizable: true, resizable: true,
show_separator_line: true, show_separator_line: true,
default_width: 200.0, default_width: 200.0,
width_range: 96.0..=f32::INFINITY, width_range: Rangef::new(96.0, f32::INFINITY),
} }
} }
@@ -153,26 +151,29 @@ impl SidePanel {
/// The initial wrapping width of the [`SidePanel`]. /// The initial wrapping width of the [`SidePanel`].
pub fn default_width(mut self, default_width: f32) -> Self { pub fn default_width(mut self, default_width: f32) -> Self {
self.default_width = default_width; self.default_width = default_width;
self.width_range = self.width_range.start().at_most(default_width) self.width_range = Rangef::new(
..=self.width_range.end().at_least(default_width); self.width_range.min.at_most(default_width),
self.width_range.max.at_least(default_width),
);
self self
} }
/// Minimum width of the panel. /// Minimum width of the panel.
pub fn min_width(mut self, min_width: f32) -> Self { pub fn min_width(mut self, min_width: f32) -> Self {
self.width_range = min_width..=self.width_range.end().at_least(min_width); self.width_range = Rangef::new(min_width, self.width_range.max.at_least(min_width));
self self
} }
/// Maximum width of the panel. /// Maximum width of the panel.
pub fn max_width(mut self, max_width: f32) -> Self { pub fn max_width(mut self, max_width: f32) -> Self {
self.width_range = self.width_range.start().at_most(max_width)..=max_width; self.width_range = Rangef::new(self.width_range.min.at_most(max_width), max_width);
self self
} }
/// The allowable width range for the panel. /// The allowable width range for the panel.
pub fn width_range(mut self, width_range: RangeInclusive<f32>) -> Self { pub fn width_range(mut self, width_range: impl Into<Rangef>) -> Self {
self.default_width = clamp_to_range(self.default_width, width_range.clone()); let width_range = width_range.into();
self.default_width = clamp_to_range(self.default_width, width_range);
self.width_range = width_range; self.width_range = width_range;
self self
} }
@@ -180,7 +181,7 @@ impl SidePanel {
/// Enforce this exact width. /// Enforce this exact width.
pub fn exact_width(mut self, width: f32) -> Self { pub fn exact_width(mut self, width: f32) -> Self {
self.default_width = width; self.default_width = width;
self.width_range = width..=width; self.width_range = Rangef::point(width);
self self
} }
@@ -224,7 +225,7 @@ impl SidePanel {
if let Some(state) = PanelState::load(ui.ctx(), id) { if let Some(state) = PanelState::load(ui.ctx(), id) {
width = state.rect.width(); width = state.rect.width();
} }
width = clamp_to_range(width, width_range.clone()).at_most(available_rect.width()); width = clamp_to_range(width, width_range).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width); side.set_rect_width(&mut panel_rect, width);
ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel"); ui.ctx().check_for_id_clash(id, panel_rect, "SidePanel");
} }
@@ -241,7 +242,7 @@ impl SidePanel {
let resize_x = side.opposite().side_x(panel_rect); let resize_x = side.opposite().side_x(panel_rect);
let mouse_over_resize_line = we_are_on_top let mouse_over_resize_line = we_are_on_top
&& panel_rect.y_range().contains(&pointer.y) && panel_rect.y_range().contains(pointer.y)
&& (resize_x - pointer.x).abs() && (resize_x - pointer.x).abs()
<= ui.style().interaction.resize_grab_radius_side; <= ui.style().interaction.resize_grab_radius_side;
@@ -253,8 +254,7 @@ impl SidePanel {
is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id)); is_resizing = ui.memory(|mem| mem.is_being_dragged(resize_id));
if is_resizing { if is_resizing {
let width = (pointer.x - side.side_x(panel_rect)).abs(); let width = (pointer.x - side.side_x(panel_rect)).abs();
let width = let width = clamp_to_range(width, width_range).at_most(available_rect.width());
clamp_to_range(width, width_range.clone()).at_most(available_rect.width());
side.set_rect_width(&mut panel_rect, width); side.set_rect_width(&mut panel_rect, width);
} }
@@ -273,7 +273,7 @@ impl SidePanel {
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| { let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height ui.set_min_height(ui.max_rect().height()); // Make sure the frame fills the full height
ui.set_min_width(*width_range.start()); ui.set_min_width(width_range.min);
add_contents(ui) add_contents(ui)
}); });
@@ -544,7 +544,7 @@ pub struct TopBottomPanel {
resizable: bool, resizable: bool,
show_separator_line: bool, show_separator_line: bool,
default_height: Option<f32>, default_height: Option<f32>,
height_range: RangeInclusive<f32>, height_range: Rangef,
} }
impl TopBottomPanel { impl TopBottomPanel {
@@ -567,7 +567,7 @@ impl TopBottomPanel {
resizable: false, resizable: false,
show_separator_line: true, show_separator_line: true,
default_height: None, default_height: None,
height_range: 20.0..=f32::INFINITY, height_range: Rangef::new(20.0, f32::INFINITY),
} }
} }
@@ -599,28 +599,31 @@ impl TopBottomPanel {
/// Defaults to [`style::Spacing::interact_size`].y. /// Defaults to [`style::Spacing::interact_size`].y.
pub fn default_height(mut self, default_height: f32) -> Self { pub fn default_height(mut self, default_height: f32) -> Self {
self.default_height = Some(default_height); self.default_height = Some(default_height);
self.height_range = self.height_range.start().at_most(default_height) self.height_range = Rangef::new(
..=self.height_range.end().at_least(default_height); self.height_range.min.at_most(default_height),
self.height_range.max.at_least(default_height),
);
self self
} }
/// Minimum height of the panel. /// Minimum height of the panel.
pub fn min_height(mut self, min_height: f32) -> Self { pub fn min_height(mut self, min_height: f32) -> Self {
self.height_range = min_height..=self.height_range.end().at_least(min_height); self.height_range = Rangef::new(min_height, self.height_range.max.at_least(min_height));
self self
} }
/// Maximum height of the panel. /// Maximum height of the panel.
pub fn max_height(mut self, max_height: f32) -> Self { pub fn max_height(mut self, max_height: f32) -> Self {
self.height_range = self.height_range.start().at_most(max_height)..=max_height; self.height_range = Rangef::new(self.height_range.min.at_most(max_height), max_height);
self self
} }
/// The allowable height range for the panel. /// The allowable height range for the panel.
pub fn height_range(mut self, height_range: RangeInclusive<f32>) -> Self { pub fn height_range(mut self, height_range: impl Into<Rangef>) -> Self {
let height_range = height_range.into();
self.default_height = self self.default_height = self
.default_height .default_height
.map(|default_height| clamp_to_range(default_height, height_range.clone())); .map(|default_height| clamp_to_range(default_height, height_range));
self.height_range = height_range; self.height_range = height_range;
self self
} }
@@ -628,7 +631,7 @@ impl TopBottomPanel {
/// Enforce this exact height. /// Enforce this exact height.
pub fn exact_height(mut self, height: f32) -> Self { pub fn exact_height(mut self, height: f32) -> Self {
self.default_height = Some(height); self.default_height = Some(height);
self.height_range = height..=height; self.height_range = Rangef::point(height);
self self
} }
@@ -673,7 +676,7 @@ impl TopBottomPanel {
} else { } else {
default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y) default_height.unwrap_or_else(|| ui.style().spacing.interact_size.y)
}; };
height = clamp_to_range(height, height_range.clone()).at_most(available_rect.height()); height = clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height); side.set_rect_height(&mut panel_rect, height);
ui.ctx() ui.ctx()
.check_for_id_clash(id, panel_rect, "TopBottomPanel"); .check_for_id_clash(id, panel_rect, "TopBottomPanel");
@@ -692,7 +695,7 @@ impl TopBottomPanel {
let resize_y = side.opposite().side_y(panel_rect); let resize_y = side.opposite().side_y(panel_rect);
let mouse_over_resize_line = we_are_on_top let mouse_over_resize_line = we_are_on_top
&& panel_rect.x_range().contains(&pointer.x) && panel_rect.x_range().contains(pointer.x)
&& (resize_y - pointer.y).abs() && (resize_y - pointer.y).abs()
<= ui.style().interaction.resize_grab_radius_side; <= ui.style().interaction.resize_grab_radius_side;
@@ -704,8 +707,8 @@ impl TopBottomPanel {
is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id)); is_resizing = ui.memory(|mem| mem.interaction.drag_id == Some(resize_id));
if is_resizing { if is_resizing {
let height = (pointer.y - side.side_y(panel_rect)).abs(); let height = (pointer.y - side.side_y(panel_rect)).abs();
let height = clamp_to_range(height, height_range.clone()) let height =
.at_most(available_rect.height()); clamp_to_range(height, height_range).at_most(available_rect.height());
side.set_rect_height(&mut panel_rect, height); side.set_rect_height(&mut panel_rect, height);
} }
@@ -724,7 +727,7 @@ impl TopBottomPanel {
let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
let inner_response = frame.show(&mut panel_ui, |ui| { let inner_response = frame.show(&mut panel_ui, |ui| {
ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width ui.set_min_width(ui.max_rect().width()); // Make the frame fill full width
ui.set_min_height(*height_range.start()); ui.set_min_height(height_range.min);
add_contents(ui) add_contents(ui)
}); });
@@ -1056,9 +1059,7 @@ impl CentralPanel {
} }
} }
fn clamp_to_range(x: f32, range: RangeInclusive<f32>) -> f32 { fn clamp_to_range(x: f32, range: Rangef) -> f32 {
x.clamp( let range = range.as_positive();
range.start().min(*range.end()), x.clamp(range.min, range.max)
range.start().max(*range.end()),
)
} }

View File

@@ -124,7 +124,10 @@ impl Resize {
} }
/// Can you resize it with the mouse? /// Can you resize it with the mouse?
/// Note that a window can still auto-resize ///
/// Note that a window can still auto-resize.
///
/// Default is `true`.
pub fn resizable(mut self, resizable: bool) -> Self { pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable; self.resizable = resizable;
self self

View File

@@ -334,16 +334,22 @@ struct Prepared {
state: State, state: State,
has_bar: [bool; 2], has_bar: [bool; 2],
auto_shrink: [bool; 2], auto_shrink: [bool; 2],
/// How much horizontal and vertical space are used up by the /// How much horizontal and vertical space are used up by the
/// width of the vertical bar, and the height of the horizontal bar? /// width of the vertical bar, and the height of the horizontal bar?
current_bar_use: Vec2, current_bar_use: Vec2,
scroll_bar_visibility: ScrollBarVisibility, scroll_bar_visibility: ScrollBarVisibility,
/// Where on the screen the content is (excludes scroll bars). /// Where on the screen the content is (excludes scroll bars).
inner_rect: Rect, inner_rect: Rect,
content_ui: Ui, content_ui: Ui,
/// Relative coordinates: the offset and size of the view of the inner UI. /// Relative coordinates: the offset and size of the view of the inner UI.
/// `viewport.min == ZERO` means we scrolled to the top. /// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect, viewport: Rect,
scrolling_enabled: bool, scrolling_enabled: bool,
stick_to_end: [bool; 2], stick_to_end: [bool; 2],
} }
@@ -459,7 +465,7 @@ impl ScrollArea {
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d]; content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
} }
} }
// Make sure we din't accidentally expand the clip rect // Make sure we didn't accidentally expand the clip rect
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); content_clip_rect = content_clip_rect.intersect(ui.clip_rect());
content_ui.set_clip_rect(content_clip_rect); content_ui.set_clip_rect(content_clip_rect);
} }
@@ -640,8 +646,7 @@ impl Prepared {
let min = content_ui.min_rect().min[d]; let min = content_ui.min_rect().min[d];
let clip_rect = content_ui.clip_rect(); let clip_rect = content_ui.clip_rect();
let visible_range = min..=min + clip_rect.size()[d]; let visible_range = min..=min + clip_rect.size()[d];
let start = *scroll.start(); let (start, end) = (scroll.min, scroll.max);
let end = *scroll.end();
let clip_start = clip_rect.min[d]; let clip_start = clip_rect.min[d];
let clip_end = clip_rect.max[d]; let clip_end = clip_rect.max[d];
let mut spacing = ui.spacing().item_spacing[d]; let mut spacing = ui.spacing().item_spacing[d];

View File

@@ -232,7 +232,10 @@ impl<'open> Window<'open> {
} }
/// Can the user resize the window by dragging its edges? /// Can the user resize the window by dragging its edges?
///
/// Note that even if you set this to `false` the window may still auto-resize. /// Note that even if you set this to `false` the window may still auto-resize.
///
/// Default is `true`.
pub fn resizable(mut self, resizable: bool) -> Self { pub fn resizable(mut self, resizable: bool) -> Self {
self.resize = self.resize.resizable(resizable); self.resize = self.resize.resizable(resizable);
self self
@@ -278,6 +281,14 @@ impl<'open> Window<'open> {
self self
} }
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
///
/// See [`ScrollArea::drag_to_scroll`] for more.
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self
}
/// Constrain the area up to which the window can be dragged. /// Constrain the area up to which the window can be dragged.
pub fn drag_bounds(mut self, bounds: Rect) -> Self { pub fn drag_bounds(mut self, bounds: Rect) -> Self {
self.area = self.area.drag_bounds(bounds); self.area = self.area.drag_bounds(bounds);
@@ -1701,7 +1712,7 @@ impl TitleBar {
// Don't cover the close- and collapse buttons: // Don't cover the close- and collapse buttons:
// After 32 is used for a temporary embedd button! // After 32 is used for a temporary embedd button!
let double_click_rect = self.rect.shrink2(vec2( let double_click_rect = self.rect.shrink2(vec2(
32.0 + ui.style().visuals.text_cursor_width + ui.style().spacing.icon_width, 32.0 + ui.style().visuals.text_cursor.width + ui.style().spacing.icon_width,
0.0, 0.0,
)); ));

View File

@@ -744,7 +744,7 @@ impl Context {
} }
let show_error = |widget_rect: Rect, text: String| { let show_error = |widget_rect: Rect, text: String| {
let text = format!("🔥 {}", text); let text = format!("🔥 {text}");
let color = self.style().visuals.error_fg_color; let color = self.style().visuals.error_fg_color;
let painter = self.debug_painter(); let painter = self.debug_painter();
painter.rect_stroke(widget_rect, 0.0, (1.0, color)); painter.rect_stroke(widget_rect, 0.0, (1.0, color));
@@ -790,10 +790,10 @@ impl Context {
let id_str = id.short_debug_format(); let id_str = id.short_debug_format();
if prev_rect.min.distance(new_rect.min) < 4.0 { if prev_rect.min.distance(new_rect.min) < 4.0 {
show_error(new_rect, format!("Double use of {} ID {}", what, id_str)); show_error(new_rect, format!("Double use of {what} ID {id_str}"));
} else { } else {
show_error(prev_rect, format!("First use of {} ID {}", what, id_str)); show_error(prev_rect, format!("First use of {what} ID {id_str}"));
show_error(new_rect, format!("Second use of {} ID {}", what, id_str)); show_error(new_rect, format!("Second use of {what} ID {id_str}"));
} }
} }
@@ -1868,14 +1868,14 @@ impl Context {
let pointer_pos = self let pointer_pos = self
.pointer_hover_pos() .pointer_hover_pos()
.map_or_else(String::new, |pos| format!("{:?}", pos)); .map_or_else(String::new, |pos| format!("{pos:?}"));
ui.label(format!("Pointer pos: {}", pointer_pos)); ui.label(format!("Pointer pos: {pointer_pos}"));
let top_layer = self let top_layer = self
.pointer_hover_pos() .pointer_hover_pos()
.and_then(|pos| self.layer_id_at(pos)) .and_then(|pos| self.layer_id_at(pos))
.map_or_else(String::new, |layer| layer.short_debug_format()); .map_or_else(String::new, |layer| layer.short_debug_format());
ui.label(format!("Top layer under mouse: {}", top_layer)); ui.label(format!("Top layer under mouse: {top_layer}"));
ui.add_space(16.0); ui.add_space(16.0);
@@ -1961,7 +1961,7 @@ impl Context {
ui.image(texture_id, size); ui.image(texture_id, size);
}); });
ui.label(format!("{} x {}", w, h)); ui.label(format!("{w} x {h}"));
ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6)); ui.label(format!("{:.3} MB", meta.bytes_used() as f64 * 1e-6));
ui.label(format!("{:?}", meta.name)); ui.label(format!("{:?}", meta.name));
ui.end_row(); ui.end_row();
@@ -1982,8 +1982,7 @@ impl Context {
let (num_state, num_serialized) = self.data(|d| (d.len(), d.count_serialized())); let (num_state, num_serialized) = self.data(|d| (d.len(), d.count_serialized()));
ui.label(format!( ui.label(format!(
"{} widget states stored (of which {} are serialized).", "{num_state} widget states stored (of which {num_serialized} are serialized)."
num_state, num_serialized
)); ));
ui.horizontal(|ui| { ui.horizontal(|ui| {

View File

@@ -274,10 +274,10 @@ pub enum Event {
/// Position of the touch (or where the touch was last detected) /// Position of the touch (or where the touch was last detected)
pos: Pos2, pos: Pos2,
/// Describes how hard the touch device was pressed. May always be `0` if the platform does /// Describes how hard the touch device was pressed. May always be `None` if the platform does
/// not support pressure sensitivity. /// not support pressure sensitivity.
/// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure).
force: f32, force: Option<f32>,
}, },
/// A raw mouse wheel event as sent by the backend (minus the z coordinate), /// A raw mouse wheel event as sent by the backend (minus the z coordinate),
@@ -618,11 +618,11 @@ pub struct ModifierNames<'a> {
} }
impl ModifierNames<'static> { impl ModifierNames<'static> {
/// ⌥ ^ ⇧ ⌘ - NOTE: not supported by the default egui font. /// ⌥ ⇧ ⌘ - NOTE: not supported by the default egui font.
pub const SYMBOLS: Self = Self { pub const SYMBOLS: Self = Self {
is_short: true, is_short: true,
alt: "", alt: "",
ctrl: "^", ctrl: "",
shift: "", shift: "",
mac_cmd: "", mac_cmd: "",
mac_alt: "", mac_alt: "",
@@ -701,27 +701,37 @@ pub enum Key {
/// The virtual keycode for the Minus key. /// The virtual keycode for the Minus key.
Minus, Minus,
/// The virtual keycode for the Plus/Equals key. /// The virtual keycode for the Plus/Equals key.
PlusEquals, PlusEquals,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num0, Num0,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num1, Num1,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num2, Num2,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num3, Num3,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num4, Num4,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num5, Num5,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num6, Num6,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num7, Num7,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num8, Num8,
/// Either from the main row or from the numpad. /// Either from the main row or from the numpad.
Num9, Num9,
@@ -914,7 +924,7 @@ fn format_kb_shortcut() {
cmd_shift_f.format(&ModifierNames::NAMES, true), cmd_shift_f.format(&ModifierNames::NAMES, true),
"Shift+Cmd+F" "Shift+Cmd+F"
); );
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "^⇧F"); assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⇧F");
assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F"); assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F");
} }
@@ -935,25 +945,25 @@ impl RawInput {
focused, focused,
} = self; } = self;
ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("screen_rect: {screen_rect:?} points"));
ui.label(format!("pixels_per_point: {:?}", pixels_per_point)) ui.label(format!("pixels_per_point: {pixels_per_point:?}"))
.on_hover_text( .on_hover_text(
"Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.",
); );
ui.label(format!("max_texture_side: {:?}", max_texture_side)); ui.label(format!("max_texture_side: {max_texture_side:?}"));
if let Some(time) = time { if let Some(time) = time {
ui.label(format!("time: {:.3} s", time)); ui.label(format!("time: {time:.3} s"));
} else { } else {
ui.label("time: None"); ui.label("time: None");
} }
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("modifiers: {modifiers:#?}"));
ui.label(format!("hovered_files: {}", hovered_files.len())); ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len())); ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.label(format!("focused: {}", focused)); ui.label(format!("focused: {focused}"));
ui.scope(|ui| { ui.scope(|ui| {
ui.set_min_height(150.0); ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events)) ui.label(format!("events: {events:#?}"))
.on_hover_text("key presses etc"); .on_hover_text("key presses etc");
}); });
} }

View File

@@ -116,7 +116,7 @@ impl PlatformOutput {
/// This can be used by a text-to-speech system to describe the events (if any). /// This can be used by a text-to-speech system to describe the events (if any).
pub fn events_description(&self) -> String { pub fn events_description(&self) -> String {
// only describe last event: // only describe last event:
if let Some(event) = self.events.iter().rev().next() { if let Some(event) = self.events.iter().next_back() {
match event { match event {
OutputEvent::Clicked(widget_info) OutputEvent::Clicked(widget_info)
| OutputEvent::DoubleClicked(widget_info) | OutputEvent::DoubleClicked(widget_info)
@@ -433,12 +433,12 @@ impl OutputEvent {
impl std::fmt::Debug for OutputEvent { impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Clicked(wi) => write!(f, "Clicked({:?})", wi), Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi), Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi), Self::TripleClicked(wi) => write!(f, "TripleClicked({wi:?})"),
Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi), Self::FocusGained(wi) => write!(f, "FocusGained({wi:?})"),
Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi), Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({wi:?})"),
Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi), Self::ValueChanged(wi) => write!(f, "ValueChanged({wi:?})"),
} }
} }
} }
@@ -625,14 +625,14 @@ impl WidgetInfo {
if let Some(selected) = selected { if let Some(selected) = selected {
if *typ == WidgetType::Checkbox { if *typ == WidgetType::Checkbox {
let state = if *selected { "checked" } else { "unchecked" }; let state = if *selected { "checked" } else { "unchecked" };
description = format!("{} {}", state, description); description = format!("{state} {description}");
} else { } else {
description += if *selected { "selected" } else { "" }; description += if *selected { "selected" } else { "" };
}; };
} }
if let Some(label) = label { if let Some(label) = label {
description = format!("{}: {}", label, description); description = format!("{label}: {description}");
} }
if typ == &WidgetType::TextEdit { if typ == &WidgetType::TextEdit {
@@ -646,7 +646,7 @@ impl WidgetInfo {
} else { } else {
text = "blank".into(); text = "blank".into();
} }
description = format!("{}: {}", text, description); description = format!("{text}: {description}");
} }
if let Some(value) = value { if let Some(value) = value {

View File

@@ -1,5 +1,3 @@
use std::ops::RangeInclusive;
use crate::{id::IdSet, *}; use crate::{id::IdSet, *};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@@ -46,7 +44,7 @@ pub(crate) struct FrameState {
pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ? pub(crate) scroll_delta: Vec2, // TODO(emilk): move to `InputState` ?
/// horizontal, vertical /// horizontal, vertical
pub(crate) scroll_target: [Option<(RangeInclusive<f32>, Option<Align>)>; 2], pub(crate) scroll_target: [Option<(Rangef, Option<Align>)>; 2],
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
pub(crate) accesskit_state: Option<AccessKitFrameState>, pub(crate) accesskit_state: Option<AccessKitFrameState>,

View File

@@ -47,7 +47,7 @@ impl State {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// type alias for boxed function to determine row color during grid generation // type alias for boxed function to determine row color during grid generation
type ColorPickerFn = Box<dyn Fn(usize, &Style) -> Option<Color32>>; type ColorPickerFn = Box<dyn Send + Sync + Fn(usize, &Style) -> Option<Color32>>;
pub(crate) struct GridLayout { pub(crate) struct GridLayout {
ctx: Context, ctx: Context,
@@ -60,6 +60,7 @@ pub(crate) struct GridLayout {
/// State previous frame (if any). /// State previous frame (if any).
/// This can be used to predict future sizes of cells. /// This can be used to predict future sizes of cells.
prev_state: State, prev_state: State,
/// State accumulated during the current frame. /// State accumulated during the current frame.
curr_state: State, curr_state: State,
initial_available: Rect, initial_available: Rect,
@@ -311,7 +312,7 @@ impl Grid {
/// Setting this will allow for dynamic coloring of rows of the grid object /// Setting this will allow for dynamic coloring of rows of the grid object
pub fn with_row_color<F>(mut self, color_picker: F) -> Self pub fn with_row_color<F>(mut self, color_picker: F) -> Self
where where
F: Fn(usize, &Style) -> Option<Color32> + 'static, F: Send + Sync + Fn(usize, &Style) -> Option<Color32> + 'static,
{ {
self.color_picker = Some(Box::new(color_picker)); self.color_picker = Some(Box::new(color_picker));
self self

View File

@@ -992,30 +992,28 @@ impl InputState {
}); });
} }
ui.label(format!("scroll_delta: {:?} points", scroll_delta)); ui.label(format!("scroll_delta: {scroll_delta:?} points"));
ui.label(format!("zoom_factor_delta: {:4.2}x", zoom_factor_delta)); ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x"));
ui.label(format!("screen_rect: {:?} points", screen_rect)); ui.label(format!("screen_rect: {screen_rect:?} points"));
ui.label(format!( ui.label(format!(
"{} physical pixels for each logical point", "{pixels_per_point} physical pixels for each logical point"
pixels_per_point
)); ));
ui.label(format!( ui.label(format!(
"max texture size (on each side): {}", "max texture size (on each side): {max_texture_side}"
max_texture_side
)); ));
ui.label(format!("time: {:.3} s", time)); ui.label(format!("time: {time:.3} s"));
ui.label(format!( ui.label(format!(
"time since previous frame: {:.1} ms", "time since previous frame: {:.1} ms",
1e3 * unstable_dt 1e3 * unstable_dt
)); ));
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("stable_dt: {:.1} ms", 1e3 * stable_dt)); ui.label(format!("stable_dt: {:.1} ms", 1e3 * stable_dt));
ui.label(format!("focused: {}", focused)); ui.label(format!("focused: {focused}"));
ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("modifiers: {modifiers:#?}"));
ui.label(format!("keys_down: {:?}", keys_down)); ui.label(format!("keys_down: {keys_down:?}"));
ui.scope(|ui| { ui.scope(|ui| {
ui.set_min_height(150.0); ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events)) ui.label(format!("events: {events:#?}"))
.on_hover_text("key presses etc"); .on_hover_text("key presses etc");
}); });
} }
@@ -1039,22 +1037,21 @@ impl PointerState {
pointer_events, pointer_events,
} = self; } = self;
ui.label(format!("latest_pos: {:?}", latest_pos)); ui.label(format!("latest_pos: {latest_pos:?}"));
ui.label(format!("interact_pos: {:?}", interact_pos)); ui.label(format!("interact_pos: {interact_pos:?}"));
ui.label(format!("delta: {:?}", delta)); ui.label(format!("delta: {delta:?}"));
ui.label(format!( ui.label(format!(
"velocity: [{:3.0} {:3.0}] points/sec", "velocity: [{:3.0} {:3.0}] points/sec",
velocity.x, velocity.y velocity.x, velocity.y
)); ));
ui.label(format!("down: {:#?}", down)); ui.label(format!("down: {down:#?}"));
ui.label(format!("press_origin: {:?}", press_origin)); ui.label(format!("press_origin: {press_origin:?}"));
ui.label(format!("press_start_time: {:?} s", press_start_time)); ui.label(format!("press_start_time: {press_start_time:?} s"));
ui.label(format!( ui.label(format!(
"has_moved_too_much_for_a_click: {}", "has_moved_too_much_for_a_click: {has_moved_too_much_for_a_click}"
has_moved_too_much_for_a_click
)); ));
ui.label(format!("last_click_time: {:#?}", last_click_time)); ui.label(format!("last_click_time: {last_click_time:#?}"));
ui.label(format!("last_last_click_time: {:#?}", last_last_click_time)); ui.label(format!("last_last_click_time: {last_last_click_time:#?}"));
ui.label(format!("pointer_events: {:?}", pointer_events)); ui.label(format!("pointer_events: {pointer_events:?}"));
} }
} }

View File

@@ -118,7 +118,7 @@ struct ActiveTouch {
/// ///
/// Note that a value of 0.0 either indicates a very light touch, or it means that the device /// Note that a value of 0.0 either indicates a very light touch, or it means that the device
/// is not capable of measuring the touch force. /// is not capable of measuring the touch force.
force: f32, force: Option<f32>,
} }
impl TouchState { impl TouchState {
@@ -249,7 +249,7 @@ impl TouchState {
// first pass: calculate force and center of touch positions: // first pass: calculate force and center of touch positions:
for touch in self.active_touches.values() { for touch in self.active_touches.values() {
state.avg_force += touch.force; state.avg_force += touch.force.unwrap_or(0.0);
state.avg_pos.x += touch.pos.x; state.avg_pos.x += touch.pos.x;
state.avg_pos.y += touch.pos.y; state.avg_pos.y += touch.pos.y;
} }
@@ -286,7 +286,7 @@ impl TouchState {
impl TouchState { impl TouchState {
pub fn ui(&self, ui: &mut crate::Ui) { pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(format!("{:?}", self)); ui.label(format!("{self:?}"));
} }
} }
@@ -294,7 +294,7 @@ impl Debug for TouchState {
// This outputs less clutter than `#[derive(Debug)]`: // This outputs less clutter than `#[derive(Debug)]`:
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (id, touch) in &self.active_touches { for (id, touch) in &self.active_touches {
f.write_fmt(format_args!("#{:?}: {:#?}\n", id, touch))?; f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
} }
f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?; f.write_fmt(format_args!("gesture: {:#?}\n", self.gesture_state))?;
Ok(()) Ok(())

View File

@@ -31,10 +31,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo
Color32::BLACK Color32::BLACK
}; };
ui.label(format!( ui.label(format!("Texture size: {width} x {height} (hover to zoom)"));
"Texture size: {} x {} (hover to zoom)",
width, height
));
if width <= 1 || height <= 1 { if width <= 1 || height <= 1 {
return; return;
} }
@@ -108,7 +105,7 @@ impl Widget for &epaint::stats::PaintStats {
label(ui, shape_path, "paths"); label(ui, shape_path, "paths");
label(ui, shape_mesh, "nested meshes"); label(ui, shape_mesh, "nested meshes");
label(ui, shape_vec, "nested shapes"); label(ui, shape_vec, "nested shapes");
ui.label(format!("{:6} callbacks", num_callbacks)); ui.label(format!("{num_callbacks:6} callbacks"));
ui.add_space(10.0); ui.add_space(10.0);
ui.label("Text shapes:"); ui.label("Text shapes:");

View File

@@ -127,7 +127,7 @@ impl PaintList {
#[inline(always)] #[inline(always)]
pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx { pub fn add(&mut self, clip_rect: Rect, shape: Shape) -> ShapeIdx {
let idx = ShapeIdx(self.0.len()); let idx = ShapeIdx(self.0.len());
self.0.push(ClippedShape(clip_rect, shape)); self.0.push(ClippedShape { clip_rect, shape });
idx idx
} }
@@ -135,7 +135,7 @@ impl PaintList {
self.0.extend( self.0.extend(
shapes shapes
.into_iter() .into_iter()
.map(|shape| ClippedShape(clip_rect, shape)), .map(|shape| ClippedShape { clip_rect, shape }),
); );
} }
@@ -148,12 +148,12 @@ impl PaintList {
/// and then later setting it using `paint_list.set(idx, cr, frame);`. /// and then later setting it using `paint_list.set(idx, cr, frame);`.
#[inline(always)] #[inline(always)]
pub fn set(&mut self, idx: ShapeIdx, clip_rect: Rect, shape: Shape) { pub fn set(&mut self, idx: ShapeIdx, clip_rect: Rect, shape: Shape) {
self.0[idx.0] = ClippedShape(clip_rect, shape); self.0[idx.0] = ClippedShape { clip_rect, shape };
} }
/// Translate each [`Shape`] and clip rectangle by this much, in-place /// Translate each [`Shape`] and clip rectangle by this much, in-place
pub fn translate(&mut self, delta: Vec2) { pub fn translate(&mut self, delta: Vec2) {
for ClippedShape(clip_rect, shape) in &mut self.0 { for ClippedShape { clip_rect, shape } in &mut self.0 {
*clip_rect = clip_rect.translate(delta); *clip_rect = clip_rect.translate(delta);
shape.translate(delta); shape.translate(delta);
} }

View File

@@ -336,7 +336,9 @@ pub use epaint::emath;
#[cfg(feature = "color-hex")] #[cfg(feature = "color-hex")]
pub use ecolor::hex_color; pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba}; pub use ecolor::{Color32, Rgba};
pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2}; pub use emath::{
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, Vec2,
};
pub use epaint::{ pub use epaint::{
mutex, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak}, text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},

View File

@@ -604,8 +604,10 @@ impl Memory {
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct Areas { pub struct Areas {
areas: IdMap<area::State>, areas: IdMap<area::State>,
/// Back-to-front. Top is last. /// Back-to-front. Top is last.
order: Vec<LayerId>, order: Vec<LayerId>,
visible_last_frame: ahash::HashSet<LayerId>, visible_last_frame: ahash::HashSet<LayerId>,
visible_current_frame: ahash::HashSet<LayerId>, visible_current_frame: ahash::HashSet<LayerId>,

View File

@@ -1,8 +1,7 @@
use std::ops::RangeInclusive;
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
emath::{Align2, Pos2, Rect, Vec2}, emath::{Align2, Pos2, Rangef, Rect, Vec2},
layers::{LayerId, PaintList, ShapeIdx}, layers::{LayerId, PaintList, ShapeIdx},
Color32, Context, FontId, Color32, Context, FontId,
}; };
@@ -227,7 +226,7 @@ impl Painter {
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect { pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
let color = self.ctx.style().visuals.error_fg_color; let color = self.ctx.style().visuals.error_fg_color;
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {}", text)) self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
} }
/// text with a background /// text with a background
@@ -263,12 +262,12 @@ impl Painter {
} }
/// Paints a horizontal line. /// Paints a horizontal line.
pub fn hline(&self, x: RangeInclusive<f32>, y: f32, stroke: impl Into<Stroke>) { pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) {
self.add(Shape::hline(x, y, stroke)); self.add(Shape::hline(x, y, stroke));
} }
/// Paints a vertical line. /// Paints a vertical line.
pub fn vline(&self, x: f32, y: RangeInclusive<f32>, stroke: impl Into<Stroke>) { pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) {
self.add(Shape::vline(x, y, stroke)); self.add(Shape::vline(x, y, stroke));
} }

View File

@@ -338,6 +338,13 @@ pub struct Margin {
} }
impl Margin { impl Margin {
pub const ZERO: Self = Self {
left: 0.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
};
#[inline] #[inline]
pub fn same(margin: f32) -> Self { pub fn same(margin: f32) -> Self {
Self { Self {
@@ -360,30 +367,46 @@ impl Margin {
} }
/// Total margins on both sides /// Total margins on both sides
#[inline]
pub fn sum(&self) -> Vec2 { pub fn sum(&self) -> Vec2 {
vec2(self.left + self.right, self.top + self.bottom) vec2(self.left + self.right, self.top + self.bottom)
} }
#[inline]
pub fn left_top(&self) -> Vec2 { pub fn left_top(&self) -> Vec2 {
vec2(self.left, self.top) vec2(self.left, self.top)
} }
#[inline]
pub fn right_bottom(&self) -> Vec2 { pub fn right_bottom(&self) -> Vec2 {
vec2(self.right, self.bottom) vec2(self.right, self.bottom)
} }
#[inline]
pub fn is_same(&self) -> bool { pub fn is_same(&self) -> bool {
self.left == self.right && self.left == self.top && self.left == self.bottom self.left == self.right && self.left == self.top && self.left == self.bottom
} }
#[inline]
pub fn expand_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min - self.left_top(), rect.max + self.right_bottom())
}
#[inline]
pub fn shrink_rect(&self, rect: Rect) -> Rect {
Rect::from_min_max(rect.min + self.left_top(), rect.max - self.right_bottom())
}
} }
impl From<f32> for Margin { impl From<f32> for Margin {
#[inline]
fn from(v: f32) -> Self { fn from(v: f32) -> Self {
Self::same(v) Self::same(v)
} }
} }
impl From<Vec2> for Margin { impl From<Vec2> for Margin {
#[inline]
fn from(v: Vec2) -> Self { fn from(v: Vec2) -> Self {
Self::symmetric(v.x, v.y) Self::symmetric(v.x, v.y)
} }
@@ -392,6 +415,7 @@ impl From<Vec2> for Margin {
impl std::ops::Add for Margin { impl std::ops::Add for Margin {
type Output = Self; type Output = Self;
#[inline]
fn add(self, other: Self) -> Self { fn add(self, other: Self) -> Self {
Self { Self {
left: self.left + other.left, left: self.left + other.left,
@@ -491,7 +515,8 @@ pub struct Visuals {
pub resize_corner_size: f32, pub resize_corner_size: f32,
pub text_cursor_width: f32, /// The color and width of the text cursor
pub text_cursor: Stroke,
/// show where the text cursor would be if you clicked /// show where the text cursor would be if you clicked
pub text_cursor_preview: bool, pub text_cursor_preview: bool,
@@ -767,7 +792,7 @@ impl Visuals {
popup_shadow: Shadow::small_dark(), popup_shadow: Shadow::small_dark(),
resize_corner_size: 12.0, resize_corner_size: 12.0,
text_cursor_width: 2.0, text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
text_cursor_preview: false, text_cursor_preview: false,
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
button_frame: true, button_frame: true,
@@ -800,6 +825,7 @@ impl Visuals {
panel_fill: Color32::from_gray(248), panel_fill: Color32::from_gray(248),
popup_shadow: Shadow::small_light(), popup_shadow: Shadow::small_light(),
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
..Self::dark() ..Self::dark()
} }
} }
@@ -1334,7 +1360,7 @@ impl Visuals {
popup_shadow, popup_shadow,
resize_corner_size, resize_corner_size,
text_cursor_width, text_cursor,
text_cursor_preview, text_cursor_preview,
clip_rect_margin, clip_rect_margin,
button_frame, button_frame,
@@ -1392,8 +1418,9 @@ impl Visuals {
}); });
ui_color(ui, hyperlink_color, "hyperlink_color"); ui_color(ui, hyperlink_color, "hyperlink_color");
stroke_ui(ui, text_cursor, "Text Cursor");
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
ui.add(Slider::new(text_cursor_width, 0.0..=4.0).text("text_cursor_width"));
ui.checkbox(text_cursor_preview, "Preview text cursor on hover"); ui.checkbox(text_cursor_preview, "Preview text cursor on hover");
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin")); ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));

View File

@@ -517,15 +517,17 @@ impl Ui {
} }
/// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`. /// `ui.set_width_range(min..=max);` is equivalent to `ui.set_min_width(min); ui.set_max_width(max);`.
pub fn set_width_range(&mut self, width: std::ops::RangeInclusive<f32>) { pub fn set_width_range(&mut self, width: impl Into<Rangef>) {
self.set_min_width(*width.start()); let width = width.into();
self.set_max_width(*width.end()); self.set_min_width(width.min);
self.set_max_width(width.max);
} }
/// `ui.set_height_range(min..=max);` is equivalent to `ui.set_min_height(min); ui.set_max_height(max);`. /// `ui.set_height_range(min..=max);` is equivalent to `ui.set_min_height(min); ui.set_max_height(max);`.
pub fn set_height_range(&mut self, height: std::ops::RangeInclusive<f32>) { pub fn set_height_range(&mut self, height: impl Into<Rangef>) {
self.set_min_height(*height.start()); let height = height.into();
self.set_max_height(*height.end()); self.set_min_height(height.min);
self.set_max_height(height.max);
} }
/// Set both the minimum and maximum width. /// Set both the minimum and maximum width.
@@ -556,6 +558,7 @@ impl Ui {
// Layout related measures: // Layout related measures:
/// The available space at the moment, given the current cursor. /// The available space at the moment, given the current cursor.
///
/// This how much more space we can take up without overflowing our parent. /// This how much more space we can take up without overflowing our parent.
/// Shrinks as widgets allocate space and the cursor moves. /// Shrinks as widgets allocate space and the cursor moves.
/// A small size should be interpreted as "as little as possible". /// A small size should be interpreted as "as little as possible".
@@ -564,19 +567,30 @@ impl Ui {
self.placer.available_size() self.placer.available_size()
} }
/// The available width at the moment, given the current cursor.
///
/// See [`Self::available_size`] for more information.
pub fn available_width(&self) -> f32 { pub fn available_width(&self) -> f32 {
self.available_size().x self.available_size().x
} }
/// The available height at the moment, given the current cursor.
///
/// See [`Self::available_size`] for more information.
pub fn available_height(&self) -> f32 { pub fn available_height(&self) -> f32 {
self.available_size().y self.available_size().y
} }
/// In case of a wrapping layout, how much space is left on this row/column? /// In case of a wrapping layout, how much space is left on this row/column?
///
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
pub fn available_size_before_wrap(&self) -> Vec2 { pub fn available_size_before_wrap(&self) -> Vec2 {
self.placer.available_rect_before_wrap().size() self.placer.available_rect_before_wrap().size()
} }
/// In case of a wrapping layout, how much space is left on this row/column?
///
/// If the layout does not wrap, this will return the same value as [`Self::available_size`].
pub fn available_rect_before_wrap(&self) -> Rect { pub fn available_rect_before_wrap(&self) -> Rect {
self.placer.available_rect_before_wrap() self.placer.available_rect_before_wrap()
} }
@@ -966,7 +980,7 @@ impl Ui {
/// ``` /// ```
pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) { pub fn scroll_to_rect(&self, rect: Rect, align: Option<Align>) {
for d in 0..2 { for d in 0..2 {
let range = rect.min[d]..=rect.max[d]; let range = Rangef::new(rect.min[d], rect.max[d]);
self.ctx() self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((range, align))); .frame_state_mut(|state| state.scroll_target[d] = Some((range, align)));
} }
@@ -996,9 +1010,9 @@ impl Ui {
pub fn scroll_to_cursor(&self, align: Option<Align>) { pub fn scroll_to_cursor(&self, align: Option<Align>) {
let target = self.next_widget_position(); let target = self.next_widget_position();
for d in 0..2 { for d in 0..2 {
let target = target[d]; let target = Rangef::point(target[d]);
self.ctx() self.ctx()
.frame_state_mut(|state| state.scroll_target[d] = Some((target..=target, align))); .frame_state_mut(|state| state.scroll_target[d] = Some((target, align)));
} }
} }
@@ -2231,3 +2245,9 @@ impl Ui {
} }
} }
} }
#[test]
fn ui_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Ui>();
}

View File

@@ -27,7 +27,7 @@ pub type ViewportRender = dyn Fn(&Context) + Sync + Send;
#[derive(Hash, PartialEq, Eq, Clone)] #[derive(Hash, PartialEq, Eq, Clone)]
pub struct ViewportBuilder { pub struct ViewportBuilder {
pub title: String, pub title: String,
pub name: Option<String>, pub name: Option<(String, String)>,
pub position: Option<Option<(i32, i32)>>, pub position: Option<Option<(i32, i32)>>,
pub inner_size: Option<Option<(u32, u32)>>, pub inner_size: Option<Option<(u32, u32)>>,
pub fullscreen: Option<bool>, pub fullscreen: Option<bool>,
@@ -217,6 +217,11 @@ impl ViewportBuilder {
self self
} }
pub fn with_name(mut self, id: impl Into<String>, instance: impl Into<String>) -> Self {
self.name = Some((id.into(), instance.into()));
self
}
/// Is not implemented for winit /// Is not implemented for winit
/// You should use `ViewportCommand::CursorHitTest` if you want to set this! /// You should use `ViewportCommand::CursorHitTest` if you want to set this!
pub fn with_hittest(mut self, value: bool) -> Self { pub fn with_hittest(mut self, value: bool) -> Self {

View File

@@ -676,7 +676,7 @@ impl WidgetTextGalley {
self.galley.size() self.galley.size()
} }
/// Size of the laid out text. /// The full, non-elided text of the input job.
#[inline] #[inline]
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
self.galley.text() self.galley.text()

View File

@@ -23,6 +23,7 @@ pub struct Button {
text: WidgetText, text: WidgetText,
shortcut_text: WidgetText, shortcut_text: WidgetText,
wrap: Option<bool>, wrap: Option<bool>,
/// None means default for interact /// None means default for interact
fill: Option<Color32>, fill: Option<Color32>,
stroke: Option<Stroke>, stroke: Option<Stroke>,

View File

@@ -234,17 +234,17 @@ fn color_text_ui(ui: &mut Ui, color: impl Into<Color32>, alpha: Alpha) {
if ui.button("📋").on_hover_text("Click to copy").clicked() { if ui.button("📋").on_hover_text("Click to copy").clicked() {
if alpha == Alpha::Opaque { if alpha == Alpha::Opaque {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}", r, g, b)); ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}"));
} else { } else {
ui.output_mut(|o| o.copied_text = format!("{}, {}, {}, {}", r, g, b, a)); ui.output_mut(|o| o.copied_text = format!("{r}, {g}, {b}, {a}"));
} }
} }
if alpha == Alpha::Opaque { if alpha == Alpha::Opaque {
ui.label(format!("rgb({}, {}, {})", r, g, b)) ui.label(format!("rgb({r}, {g}, {b})"))
.on_hover_text("Red Green Blue"); .on_hover_text("Red Green Blue");
} else { } else {
ui.label(format!("rgba({}, {}, {}, {})", r, g, b, a)) ui.label(format!("rgba({r}, {g}, {b}, {a})"))
.on_hover_text("Red Green Blue with premultiplied Alpha"); .on_hover_text("Red Green Blue with premultiplied Alpha");
} }
}); });

View File

@@ -11,6 +11,7 @@ use crate::*;
pub(crate) struct MonoState { pub(crate) struct MonoState {
last_dragged_id: Option<Id>, last_dragged_id: Option<Id>,
last_dragged_value: Option<f64>, last_dragged_value: Option<f64>,
/// For temporary edit of a [`DragValue`] value. /// For temporary edit of a [`DragValue`] value.
/// Couples with the current focus id. /// Couples with the current focus id.
edit_string: Option<String>, edit_string: Option<String>,
@@ -63,6 +64,7 @@ pub struct DragValue<'a> {
max_decimals: Option<usize>, max_decimals: Option<usize>,
custom_formatter: Option<NumFormatter<'a>>, custom_formatter: Option<NumFormatter<'a>>,
custom_parser: Option<NumParser<'a>>, custom_parser: Option<NumParser<'a>>,
update_while_editing: bool,
} }
impl<'a> DragValue<'a> { impl<'a> DragValue<'a> {
@@ -94,6 +96,7 @@ impl<'a> DragValue<'a> {
max_decimals: None, max_decimals: None,
custom_formatter: None, custom_formatter: None,
custom_parser: None, custom_parser: None,
update_while_editing: true,
} }
} }
@@ -352,6 +355,15 @@ impl<'a> DragValue<'a> {
} }
.custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok()) .custom_parser(|s| i64::from_str_radix(s, 16).map(|n| n as f64).ok())
} }
/// Update the value on each key press when text-editing the value.
///
/// Default: `true`.
/// If `false`, the value will only be updated when user presses enter or deselects the value.
pub fn update_while_editing(mut self, update: bool) -> Self {
self.update_while_editing = update;
self
}
} }
impl<'a> Widget for DragValue<'a> { impl<'a> Widget for DragValue<'a> {
@@ -366,6 +378,7 @@ impl<'a> Widget for DragValue<'a> {
max_decimals, max_decimals,
custom_formatter, custom_formatter,
custom_parser, custom_parser,
update_while_editing,
} = self; } = self;
let shift = ui.input(|i| i.modifiers.shift_only()); let shift = ui.input(|i| i.modifiers.shift_only());
@@ -392,7 +405,9 @@ impl<'a> Widget for DragValue<'a> {
let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize; let auto_decimals = (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize;
let auto_decimals = auto_decimals + is_slow_speed as usize; let auto_decimals = auto_decimals + is_slow_speed as usize;
let max_decimals = max_decimals.unwrap_or(auto_decimals + 2); let max_decimals = max_decimals
.unwrap_or(auto_decimals + 2)
.at_least(min_decimals);
let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals); let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);
let change = ui.input_mut(|input| { let change = ui.input_mut(|input| {
@@ -475,9 +490,15 @@ impl<'a> Widget for DragValue<'a> {
.desired_width(ui.spacing().interact_size.x) .desired_width(ui.spacing().interact_size.x)
.font(text_style), .font(text_style),
); );
// Only update the value when the user presses enter, or clicks elsewhere. NOT every frame.
// See https://github.com/emilk/egui/issues/2687 let update = if update_while_editing {
if response.lost_focus() { // Update when the edit content has changed.
response.changed()
} else {
// Update only when the edit has lost focus.
response.lost_focus()
};
if update {
let parsed_value = match custom_parser { let parsed_value = match custom_parser {
Some(parser) => parser(&value_text), Some(parser) => parser(&value_text),
None => value_text.parse().ok(), None => value_text.parse().ok(),
@@ -606,7 +627,7 @@ impl<'a> Widget for DragValue<'a> {
// The value is exposed as a string by the text edit widget // The value is exposed as a string by the text edit widget
// when in edit mode. // when in edit mode.
if !is_kb_editing { if !is_kb_editing {
let value_text = format!("{}{}{}", prefix, value_text, suffix); let value_text = format!("{prefix}{value_text}{suffix}");
builder.set_value(value_text); builder.set_value(value_text);
} }
}); });

View File

@@ -83,6 +83,7 @@ impl Widget for Link {
pub struct Hyperlink { pub struct Hyperlink {
url: String, url: String,
text: WidgetText, text: WidgetText,
new_tab: bool,
} }
impl Hyperlink { impl Hyperlink {
@@ -92,6 +93,7 @@ impl Hyperlink {
Self { Self {
url: url.clone(), url: url.clone(),
text: url.into(), text: url.into(),
new_tab: false,
} }
} }
@@ -100,13 +102,20 @@ impl Hyperlink {
Self { Self {
url: url.to_string(), url: url.to_string(),
text: text.into(), text: text.into(),
new_tab: false,
} }
} }
/// Always open this hyperlink in a new browser tab.
pub fn open_in_new_tab(mut self, new_tab: bool) -> Self {
self.new_tab = new_tab;
self
}
} }
impl Widget for Hyperlink { impl Widget for Hyperlink {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let Self { url, text } = self; let Self { url, text, new_tab } = self;
let response = ui.add(Link::new(text)); let response = ui.add(Link::new(text));
if response.clicked() { if response.clicked() {
@@ -114,7 +123,7 @@ impl Widget for Hyperlink {
ui.ctx().output_mut(|o| { ui.ctx().output_mut(|o| {
o.open_url = Some(crate::output::OpenUrl { o.open_url = Some(crate::output::OpenUrl {
url: url.clone(), url: url.clone(),
new_tab: modifiers.any(), new_tab: new_tab || modifiers.any(),
}); });
}); });
} }

View File

@@ -12,10 +12,14 @@ use crate::{widget_text::WidgetTextGalley, *};
/// ui.label(egui::RichText::new("With formatting").underline()); /// ui.label(egui::RichText::new("With formatting").underline());
/// # }); /// # });
/// ``` /// ```
///
/// For full control of the text you can use [`crate::text::LayoutJob`]
/// as argument to [`Self::new`].
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
pub struct Label { pub struct Label {
text: WidgetText, text: WidgetText,
wrap: Option<bool>, wrap: Option<bool>,
truncate: bool,
sense: Option<Sense>, sense: Option<Sense>,
} }
@@ -24,6 +28,7 @@ impl Label {
Self { Self {
text: text.into(), text: text.into(),
wrap: None, wrap: None,
truncate: false,
sense: None, sense: None,
} }
} }
@@ -34,6 +39,8 @@ impl Label {
/// If `true`, the text will wrap to stay within the max width of the [`Ui`]. /// If `true`, the text will wrap to stay within the max width of the [`Ui`].
/// ///
/// Calling `wrap` will override [`Self::truncate`].
///
/// By default [`Self::wrap`] will be `true` in vertical layouts /// By default [`Self::wrap`] will be `true` in vertical layouts
/// and horizontal layouts with wrapping, /// and horizontal layouts with wrapping,
/// and `false` on non-wrapping horizontal layouts. /// and `false` on non-wrapping horizontal layouts.
@@ -44,6 +51,23 @@ impl Label {
#[inline] #[inline]
pub fn wrap(mut self, wrap: bool) -> Self { pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = Some(wrap); self.wrap = Some(wrap);
self.truncate = false;
self
}
/// If `true`, the text will stop at the max width of the [`Ui`],
/// and what doesn't fit will be elided, replaced with `…`.
///
/// If the text is truncated, the full text will be shown on hover as a tool-tip.
///
/// Default is `false`, which means the text will expand the parent [`Ui`],
/// or wrap if [`Self::wrap`] is set.
///
/// Calling `truncate` will override [`Self::wrap`].
#[inline]
pub fn truncate(mut self, truncate: bool) -> Self {
self.wrap = None;
self.truncate = truncate;
self self
} }
@@ -98,10 +122,11 @@ impl Label {
.text .text
.into_text_job(ui.style(), FontSelection::Default, valign); .into_text_job(ui.style(), FontSelection::Default, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text()); let truncate = self.truncate;
let wrap = !truncate && self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width(); let available_width = ui.available_width();
if should_wrap if wrap
&& ui.layout().main_dir() == Direction::LeftToRight && ui.layout().main_dir() == Direction::LeftToRight
&& ui.layout().main_wrap() && ui.layout().main_wrap()
&& available_width.is_finite() && available_width.is_finite()
@@ -138,7 +163,11 @@ impl Label {
} }
(pos, text_galley, response) (pos, text_galley, response)
} else { } else {
if should_wrap { if truncate {
text_job.job.wrap.max_width = available_width;
text_job.job.wrap.max_rows = 1;
text_job.job.wrap.break_anywhere = true;
} else if wrap {
text_job.job.wrap.max_width = available_width; text_job.job.wrap.max_width = available_width;
} else { } else {
text_job.job.wrap.max_width = f32::INFINITY; text_job.job.wrap.max_width = f32::INFINITY;
@@ -167,9 +196,14 @@ impl Label {
impl Widget for Label { impl Widget for Label {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
let (pos, text_galley, response) = self.layout_in_ui(ui); let (pos, text_galley, mut response) = self.layout_in_ui(ui);
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text())); response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, text_galley.text()));
if text_galley.galley.elided {
// Show the full (non-elided) text on hover:
response = response.on_hover_text(text_galley.text());
}
if ui.is_rect_visible(response.rect) { if ui.is_rect_visible(response.rect) {
let response_color = ui.style().interact(&response).text_color(); let response_color = ui.style().interact(&response).text_color();

View File

@@ -38,6 +38,8 @@ pub use text_edit::{TextBuffer, TextEdit};
/// ///
/// [`Button`], [`Label`], [`Slider`], etc all implement the [`Widget`] trait. /// [`Button`], [`Label`], [`Slider`], etc all implement the [`Widget`] trait.
/// ///
/// You only need to implement `Widget` if you care about being able to do `ui.add(your_widget);`.
///
/// Note that the widgets ([`Button`], [`TextEdit`] etc) are /// Note that the widgets ([`Button`], [`TextEdit`] etc) are
/// [builders](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html), /// [builders](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html),
/// and not objects that hold state. /// and not objects that hold state.

View File

@@ -0,0 +1,318 @@
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
use epaint::{
emath::{remap_clamp, round_to_decimals},
Pos2, Rect, Shape, Stroke, TextShape,
};
use crate::{Response, Sense, TextStyle, Ui, WidgetText};
use super::{transform::PlotTransform, GridMark};
pub(super) type AxisFormatterFn = fn(f64, usize, &RangeInclusive<f64>) -> String;
/// X or Y axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
/// Horizontal X-Axis
X,
/// Vertical Y-axis
Y,
}
impl From<Axis> for usize {
#[inline]
fn from(value: Axis) -> Self {
match value {
Axis::X => 0,
Axis::Y => 1,
}
}
}
/// Placement of the horizontal X-Axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VPlacement {
Top,
Bottom,
}
/// Placement of the vertical Y-Axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HPlacement {
Left,
Right,
}
/// Placement of an axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Placement {
/// Bottom for X-axis, or left for Y-axis.
LeftBottom,
/// Top for x-axis and right for y-axis.
RightTop,
}
impl From<HPlacement> for Placement {
#[inline]
fn from(placement: HPlacement) -> Self {
match placement {
HPlacement::Left => Placement::LeftBottom,
HPlacement::Right => Placement::RightTop,
}
}
}
impl From<VPlacement> for Placement {
#[inline]
fn from(placement: VPlacement) -> Self {
match placement {
VPlacement::Top => Placement::RightTop,
VPlacement::Bottom => Placement::LeftBottom,
}
}
}
/// Axis configuration.
///
/// Used to configure axis label and ticks.
#[derive(Clone)]
pub struct AxisHints {
pub(super) label: WidgetText,
pub(super) formatter: AxisFormatterFn,
pub(super) digits: usize,
pub(super) placement: Placement,
}
// TODO: this just a guess. It might cease to work if a user changes font size.
const LINE_HEIGHT: f32 = 12.0;
impl Default for AxisHints {
/// Initializes a default axis configuration for the specified axis.
///
/// `label` is empty.
/// `formatter` is default float to string formatter.
/// maximum `digits` on tick label is 5.
fn default() -> Self {
Self {
label: Default::default(),
formatter: Self::default_formatter,
digits: 5,
placement: Placement::LeftBottom,
}
}
}
impl AxisHints {
/// Specify custom formatter for ticks.
///
/// The first parameter of `formatter` is the raw tick value as `f64`.
/// The second parameter is the maximum number of characters that fit into y-labels.
/// The second parameter of `formatter` is the currently shown range on this axis.
pub fn formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
self.formatter = fmt;
self
}
fn default_formatter(tick: f64, max_digits: usize, _range: &RangeInclusive<f64>) -> String {
if tick.abs() > 10.0_f64.powf(max_digits as f64) {
let tick_rounded = tick as isize;
return format!("{tick_rounded:+e}");
}
let tick_rounded = round_to_decimals(tick, max_digits);
if tick.abs() < 10.0_f64.powf(-(max_digits as f64)) && tick != 0.0 {
return format!("{tick_rounded:+e}");
}
tick_rounded.to_string()
}
/// Specify axis label.
///
/// The default is 'x' for x-axes and 'y' for y-axes.
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
self.label = label.into();
self
}
/// Specify maximum number of digits for ticks.
///
/// This is considered by the default tick formatter and affects the width of the y-axis
pub fn max_digits(mut self, digits: usize) -> Self {
self.digits = digits;
self
}
/// Specify the placement of the axis.
///
/// For X-axis, use [`VPlacement`].
/// For Y-axis, use [`HPlacement`].
pub fn placement(mut self, placement: impl Into<Placement>) -> Self {
self.placement = placement.into();
self
}
pub(super) fn thickness(&self, axis: Axis) -> f32 {
match axis {
Axis::X => {
if self.label.is_empty() {
1.0 * LINE_HEIGHT
} else {
3.0 * LINE_HEIGHT
}
}
Axis::Y => {
if self.label.is_empty() {
(self.digits as f32) * LINE_HEIGHT
} else {
(self.digits as f32 + 1.0) * LINE_HEIGHT
}
}
}
}
}
#[derive(Clone)]
pub(super) struct AxisWidget {
pub(super) range: RangeInclusive<f64>,
pub(super) hints: AxisHints,
pub(super) rect: Rect,
pub(super) transform: Option<PlotTransform>,
pub(super) steps: Arc<Vec<GridMark>>,
}
impl AxisWidget {
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
pub(super) fn new(hints: AxisHints, rect: Rect) -> Self {
Self {
range: (0.0..=0.0),
hints,
rect,
transform: None,
steps: Default::default(),
}
}
pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response {
let response = ui.allocate_rect(self.rect, Sense::hover());
if ui.is_rect_visible(response.rect) {
let visuals = ui.style().visuals.clone();
let text = self.hints.label;
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
let text_color = visuals
.override_text_color
.unwrap_or_else(|| ui.visuals().text_color());
let angle: f32 = match axis {
Axis::X => 0.0,
Axis::Y => -std::f32::consts::TAU * 0.25,
};
// select text_pos and angle depending on placement and orientation of widget
let text_pos = match self.hints.placement {
Placement::LeftBottom => match axis {
Axis::X => {
let pos = response.rect.center_bottom();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y - galley.size().y * 1.25,
}
}
Axis::Y => {
let pos = response.rect.left_center();
Pos2 {
x: pos.x,
y: pos.y + galley.size().x / 2.0,
}
}
},
Placement::RightTop => match axis {
Axis::X => {
let pos = response.rect.center_top();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y + galley.size().y * 0.25,
}
}
Axis::Y => {
let pos = response.rect.right_center();
Pos2 {
x: pos.x - galley.size().y * 1.5,
y: pos.y + galley.size().x / 2.0,
}
}
},
};
let shape = TextShape {
pos: text_pos,
galley: galley.galley,
underline: Stroke::NONE,
override_text_color: Some(text_color),
angle,
};
ui.painter().add(shape);
// --- add ticks ---
let font_id = TextStyle::Body.resolve(ui.style());
let transform = match self.transform {
Some(t) => t,
None => return response,
};
for step in self.steps.iter() {
let text = (self.hints.formatter)(step.value, self.hints.digits, &self.range);
if !text.is_empty() {
const MIN_TEXT_SPACING: f32 = 20.0;
const FULL_CONTRAST_SPACING: f32 = 40.0;
let spacing_in_points =
(transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32;
if spacing_in_points <= MIN_TEXT_SPACING {
continue;
}
let line_strength = remap_clamp(
spacing_in_points,
MIN_TEXT_SPACING..=FULL_CONTRAST_SPACING,
0.0..=1.0,
);
let line_color = super::color_from_strength(ui, line_strength);
let galley = ui
.painter()
.layout_no_wrap(text, font_id.clone(), line_color);
let text_pos = match axis {
Axis::X => {
let y = match self.hints.placement {
Placement::LeftBottom => self.rect.min.y,
Placement::RightTop => self.rect.max.y - galley.size().y,
};
let projected_point = super::PlotPoint::new(step.value, 0.0);
Pos2 {
x: transform.position_from_point(&projected_point).x
- galley.size().x / 2.0,
y,
}
}
Axis::Y => {
let x = match self.hints.placement {
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.x,
};
let projected_point = super::PlotPoint::new(0.0, step.value);
Pos2 {
x,
y: transform.position_from_point(&projected_point).y
- galley.size().y / 2.0,
}
}
};
ui.painter().add(Shape::galley(text_pos, galley));
}
}
}
response
}
}

View File

@@ -760,15 +760,22 @@ impl PlotItem for Text {
/// A set of points. /// A set of points.
pub struct Points { pub struct Points {
pub(super) series: PlotPoints, pub(super) series: PlotPoints,
pub(super) shape: MarkerShape, pub(super) shape: MarkerShape,
/// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically.
pub(super) color: Color32, pub(super) color: Color32,
/// Whether to fill the marker. Does not apply to all types. /// Whether to fill the marker. Does not apply to all types.
pub(super) filled: bool, pub(super) filled: bool,
/// The maximum extent of the marker from its center. /// The maximum extent of the marker from its center.
pub(super) radius: f32, pub(super) radius: f32,
pub(super) name: String, pub(super) name: String,
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) stems: Option<f32>, pub(super) stems: Option<f32>,
} }
@@ -997,6 +1004,7 @@ impl PlotItem for Points {
pub struct Arrows { pub struct Arrows {
pub(super) origins: PlotPoints, pub(super) origins: PlotPoints,
pub(super) tips: PlotPoints, pub(super) tips: PlotPoints,
pub(super) tip_length: Option<f32>,
pub(super) color: Color32, pub(super) color: Color32,
pub(super) name: String, pub(super) name: String,
pub(super) highlight: bool, pub(super) highlight: bool,
@@ -1007,6 +1015,7 @@ impl Arrows {
Self { Self {
origins: origins.into(), origins: origins.into(),
tips: tips.into(), tips: tips.into(),
tip_length: None,
color: Color32::TRANSPARENT, color: Color32::TRANSPARENT,
name: Default::default(), name: Default::default(),
highlight: false, highlight: false,
@@ -1019,6 +1028,12 @@ impl Arrows {
self self
} }
/// Set the length of the arrow tips
pub fn tip_length(mut self, tip_length: f32) -> Self {
self.tip_length = Some(tip_length);
self
}
/// Set the arrows' color. /// Set the arrows' color.
pub fn color(mut self, color: impl Into<Color32>) -> Self { pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = color.into(); self.color = color.into();
@@ -1044,6 +1059,7 @@ impl PlotItem for Arrows {
let Self { let Self {
origins, origins,
tips, tips,
tip_length,
color, color,
highlight, highlight,
.. ..
@@ -1062,7 +1078,11 @@ impl PlotItem for Arrows {
.for_each(|(origin, tip)| { .for_each(|(origin, tip)| {
let vector = tip - origin; let vector = tip - origin;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let tip_length = vector.length() / 4.0; let tip_length = if let Some(tip_length) = tip_length {
*tip_length
} else {
vector.length() / 4.0
};
let tip = origin + vector; let tip = origin + vector;
let dir = vector.normalized(); let dir = vector.normalized();
shapes.push(Shape::line_segment([origin, tip], stroke)); shapes.push(Shape::line_segment([origin, tip], stroke));
@@ -1119,6 +1139,7 @@ pub struct PlotImage {
pub(super) tint: Color32, pub(super) tint: Color32,
pub(super) highlight: bool, pub(super) highlight: bool,
pub(super) name: String, pub(super) name: String,
pub(crate) rotation: Option<(f32, Vec2)>,
} }
impl PlotImage { impl PlotImage {
@@ -1137,6 +1158,7 @@ impl PlotImage {
size: size.into(), size: size.into(),
bg_fill: Default::default(), bg_fill: Default::default(),
tint: Color32::WHITE, tint: Color32::WHITE,
rotation: None,
} }
} }
@@ -1175,6 +1197,17 @@ impl PlotImage {
self.name = name.to_string(); self.name = name.to_string();
self self
} }
/// Rotate the image about an origin by some angle
///
/// Positive angle is clockwise.
/// Origin is a vector in normalized UV space ((0,0) in top-left, (1,1) bottom right).
///
/// To rotate about the center you can pass `Vec2::splat(0.5)` as the origin.
pub fn rotate(mut self, angle: f32, origin: Vec2) -> Self {
self.rotation = Some((angle, origin));
self
}
} }
impl PlotItem for PlotImage { impl PlotItem for PlotImage {
@@ -1202,11 +1235,14 @@ impl PlotItem for PlotImage {
let right_bottom_tf = transform.position_from_point(&right_bottom); let right_bottom_tf = transform.position_from_point(&right_bottom);
Rect::from_two_pos(left_top_tf, right_bottom_tf) Rect::from_two_pos(left_top_tf, right_bottom_tf)
}; };
Image::new(*texture_id, *size) let mut image = Image::new(*texture_id, *size)
.bg_fill(*bg_fill) .bg_fill(*bg_fill)
.tint(*tint) .tint(*tint)
.uv(*uv) .uv(*uv);
.paint_at(ui, rect); if let Some((angle, origin)) = self.rotation {
image = image.rotate(angle, origin);
}
image.paint_at(ui, rect);
if *highlight { if *highlight {
shapes.push(Shape::rect_stroke( shapes.push(Shape::rect_stroke(
rect, rect,
@@ -1261,8 +1297,10 @@ pub struct BarChart {
pub(super) bars: Vec<Bar>, pub(super) bars: Vec<Bar>,
pub(super) default_color: Color32, pub(super) default_color: Color32,
pub(super) name: String, pub(super) name: String,
/// A custom element formatter /// A custom element formatter
pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>, pub(super) element_formatter: Option<Box<dyn Fn(&Bar, &BarChart) -> String>>,
highlight: bool, highlight: bool,
} }
@@ -1431,8 +1469,10 @@ pub struct BoxPlot {
pub(super) boxes: Vec<BoxElem>, pub(super) boxes: Vec<BoxElem>,
pub(super) default_color: Color32, pub(super) default_color: Color32,
pub(super) name: String, pub(super) name: String,
/// A custom element formatter /// A custom element formatter
pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>, pub(super) element_formatter: Option<Box<dyn Fn(&BoxElem, &BoxPlot) -> String>>,
highlight: bool, highlight: bool,
} }
@@ -1692,7 +1732,7 @@ pub(super) fn rulers_at_value(
let mut prefix = String::new(); let mut prefix = String::new();
if !name.is_empty() { if !name.is_empty() {
prefix = format!("{}\n", name); prefix = format!("{name}\n");
} }
let text = { let text = {

View File

@@ -125,8 +125,8 @@ impl ToString for LineStyle {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
LineStyle::Solid => "Solid".into(), LineStyle::Solid => "Solid".into(),
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing), LineStyle::Dotted { spacing } => format!("Dotted{spacing}Px"),
LineStyle::Dashed { length } => format!("Dashed{}Px", length), LineStyle::Dashed { length } => format!("Dashed{length}Px"),
} }
} }
} }

View File

@@ -0,0 +1,33 @@
use epaint::Pos2;
use crate::{Context, Id};
use super::{transform::ScreenTransform, AxisBools};
/// Information about the plot that has to persist between frames.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone)]
pub(super) struct PlotMemory {
/// Indicates if the user has modified the bounds, for example by moving or zooming,
/// or if the bounds should be calculated based by included point or auto bounds.
pub(super) bounds_modified: AxisBools,
pub(super) hovered_entry: Option<String>,
pub(super) hidden_items: ahash::HashSet<String>,
pub(super) last_screen_transform: ScreenTransform,
/// Allows to remember the first click position when performing a boxed zoom
pub(super) last_click_pos_for_zoom: Option<Pos2>,
}
impl PlotMemory {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data().get_persisted(id)
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data().insert_persisted(id, self);
}
}

View File

@@ -1,15 +1,17 @@
//! Simple plotting library. //! Simple plotting library.
use ahash::HashMap; use std::{ops::RangeInclusive, sync::Arc};
use std::ops::RangeInclusive;
use crate::*; use ahash::HashMap;
use epaint::util::FloatOrd; use epaint::util::FloatOrd;
use epaint::Hsva; use epaint::Hsva;
use axis::AxisWidget;
use items::PlotItem; use items::PlotItem;
use legend::LegendWidget; use legend::LegendWidget;
use crate::*;
pub use items::{ pub use items::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape, Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
@@ -17,16 +19,17 @@ pub use items::{
pub use legend::{Corner, Legend}; pub use legend::{Corner, Legend};
pub use transform::{PlotBounds, PlotTransform}; pub use transform::{PlotBounds, PlotTransform};
use self::items::{horizontal_line, rulers_color, vertical_line}; use items::{horizontal_line, rulers_color, vertical_line};
pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};
mod axis;
mod items; mod items;
mod legend; mod legend;
mod transform; mod transform;
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String; type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
type LabelFormatter = Option<Box<LabelFormatterFn>>; type LabelFormatter = Option<Box<LabelFormatterFn>>;
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
type AxisFormatter = Option<Box<AxisFormatterFn>>;
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>; type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
type GridSpacer = Box<GridSpacerFn>; type GridSpacer = Box<GridSpacerFn>;
@@ -78,6 +81,7 @@ pub struct AxisBools {
} }
impl AxisBools { impl AxisBools {
#[inline]
pub fn new(x: bool, y: bool) -> Self { pub fn new(x: bool, y: bool) -> Self {
Self { x, y } Self { x, y }
} }
@@ -89,11 +93,19 @@ impl AxisBools {
} }
impl From<bool> for AxisBools { impl From<bool> for AxisBools {
#[inline]
fn from(val: bool) -> Self { fn from(val: bool) -> Self {
AxisBools { x: val, y: val } AxisBools { x: val, y: val }
} }
} }
impl From<[bool; 2]> for AxisBools {
#[inline]
fn from([x, y]: [bool; 2]) -> Self {
AxisBools { x, y }
}
}
/// Information about the plot that has to persist between frames. /// Information about the plot that has to persist between frames.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone)] #[derive(Clone)]
@@ -101,9 +113,11 @@ struct PlotMemory {
/// Indicates if the user has modified the bounds, for example by moving or zooming, /// Indicates if the user has modified the bounds, for example by moving or zooming,
/// or if the bounds should be calculated based by included point or auto bounds. /// or if the bounds should be calculated based by included point or auto bounds.
bounds_modified: AxisBools, bounds_modified: AxisBools,
hovered_entry: Option<String>, hovered_entry: Option<String>,
hidden_items: ahash::HashSet<String>, hidden_items: ahash::HashSet<String>,
last_plot_transform: PlotTransform, last_plot_transform: PlotTransform,
/// Allows to remember the first click position when performing a boxed zoom /// Allows to remember the first click position when performing a boxed zoom
last_click_pos_for_zoom: Option<Pos2>, last_click_pos_for_zoom: Option<Pos2>,
} }
@@ -180,8 +194,7 @@ pub struct PlotResponse<R> {
pub struct Plot { pub struct Plot {
id_source: Id, id_source: Id,
center_x_axis: bool, center_axis: AxisBools,
center_y_axis: bool,
allow_zoom: AxisBools, allow_zoom: AxisBools,
allow_drag: AxisBools, allow_drag: AxisBools,
allow_scroll: bool, allow_scroll: bool,
@@ -206,11 +219,12 @@ pub struct Plot {
show_y: bool, show_y: bool,
label_formatter: LabelFormatter, label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>, coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2], x_axes: Vec<AxisHints>, // default x axes
y_axes: Vec<AxisHints>, // default y axes
legend_config: Option<Legend>, legend_config: Option<Legend>,
show_background: bool, show_background: bool,
show_axes: [bool; 2], show_axes: AxisBools,
show_grid: AxisBools,
grid_spacers: [GridSpacer; 2], grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool, sharp_grid_lines: bool,
clamp_grid: bool, clamp_grid: bool,
@@ -222,8 +236,7 @@ impl Plot {
Self { Self {
id_source: Id::new(id_source), id_source: Id::new(id_source),
center_x_axis: false, center_axis: false.into(),
center_y_axis: false,
allow_zoom: true.into(), allow_zoom: true.into(),
allow_drag: true.into(), allow_drag: true.into(),
allow_scroll: true, allow_scroll: true,
@@ -248,11 +261,12 @@ impl Plot {
show_y: true, show_y: true,
label_formatter: None, label_formatter: None,
coordinates_formatter: None, coordinates_formatter: None,
axis_formatters: [None, None], // [None; 2] requires Copy x_axes: vec![Default::default()],
y_axes: vec![Default::default()],
legend_config: None, legend_config: None,
show_background: true, show_background: true,
show_axes: [true; 2], show_axes: true.into(),
show_grid: true.into(),
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true, sharp_grid_lines: true,
clamp_grid: false, clamp_grid: false,
@@ -309,15 +323,15 @@ impl Plot {
self self
} }
/// Always keep the x-axis centered. Default: `false`. /// Always keep the X-axis centered. Default: `false`.
pub fn center_x_axis(mut self, on: bool) -> Self { pub fn center_x_axis(mut self, on: bool) -> Self {
self.center_x_axis = on; self.center_axis.x = on;
self self
} }
/// Always keep the y-axis centered. Default: `false`. /// Always keep the Y-axis centered. Default: `false`.
pub fn center_y_axis(mut self, on: bool) -> Self { pub fn center_y_axis(mut self, on: bool) -> Self {
self.center_y_axis = on; self.center_axis.y = on;
self self
} }
@@ -415,36 +429,6 @@ impl Plot {
self self
} }
/// Provide a function to customize the labels for the X axis based on the current visible value range.
///
/// This is useful for custom input domains, e.g. date/time.
///
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
/// the formatter function can return empty strings. This is also useful if your domain is
/// discrete (e.g. only full days in a calendar).
pub fn x_axis_formatter(
mut self,
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
self.axis_formatters[0] = Some(Box::new(func));
self
}
/// Provide a function to customize the labels for the Y axis based on the current value range.
///
/// This is useful for custom value representation, e.g. percentage or units.
///
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
/// the formatter function can return empty strings. This is also useful if your Y values are
/// discrete (e.g. only integers).
pub fn y_axis_formatter(
mut self,
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
self.axis_formatters[1] = Some(Box::new(func));
self
}
/// Configure how the grid in the background is spaced apart along the X axis. /// Configure how the grid in the background is spaced apart along the X axis.
/// ///
/// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
@@ -536,11 +520,19 @@ impl Plot {
self self
} }
/// Show the axes. /// Show axis labels and grid tick values on the side of the plot.
/// Can be useful to disable if the plot is overlaid over an existing grid or content. ///
/// Default: `[true; 2]`. /// Default: `[true; 2]`.
pub fn show_axes(mut self, show: [bool; 2]) -> Self { pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
self.show_axes = show; self.show_axes = show.into();
self
}
/// Show a grid overlay on the plot.
///
/// Default: `[true; 2]`.
pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
self.show_grid = show.into();
self self
} }
@@ -583,6 +575,94 @@ impl Plot {
self self
} }
/// Set the x axis label of the main X-axis.
///
/// Default: no label.
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.label = label.into();
}
self
}
/// Set the y axis label of the main Y-axis.
///
/// Default: no label.
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.label = label.into();
}
self
}
/// Set the position of the main X-axis.
pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.placement = placement.into();
}
self
}
/// Set the position of the main Y-axis.
pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.placement = placement.into();
}
self
}
/// Specify custom formatter for ticks on the main X-axis.
///
/// The first parameter of `fmt` is the raw tick value as `f64`.
/// The second parameter is the maximum requested number of characters per tick label.
/// The second parameter of `fmt` is the currently shown range on this axis.
pub fn x_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.formatter = fmt;
}
self
}
/// Specify custom formatter for ticks on the main Y-axis.
///
/// The first parameter of `formatter` is the raw tick value as `f64`.
/// The second parameter is the maximum requested number of characters per tick label.
/// The second parameter of `formatter` is the currently shown range on this axis.
pub fn y_axis_formatter(mut self, fmt: fn(f64, usize, &RangeInclusive<f64>) -> String) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.formatter = fmt;
}
self
}
/// Set the main Y-axis-width by number of digits
///
/// The default is 5 digits.
///
/// > Todo: This is experimental. Changing the font size might break this.
pub fn y_axis_width(mut self, digits: usize) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.digits = digits;
}
self
}
/// Set custom configuration for X-axis
///
/// More than one axis may be specified. The first specified axis is considered the main axis.
pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
self.x_axes = hints;
self
}
/// Set custom configuration for left Y-axis
///
/// More than one axis may be specified. The first specified axis is considered the main axis.
pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
self.y_axes = hints;
self
}
/// Interact with and add items to the plot and finally draw it. /// Interact with and add items to the plot and finally draw it.
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> { pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
self.show_dyn(ui, Box::new(build_fn)) self.show_dyn(ui, Box::new(build_fn))
@@ -595,8 +675,7 @@ impl Plot {
) -> PlotResponse<R> { ) -> PlotResponse<R> {
let Self { let Self {
id_source, id_source,
center_x_axis, center_axis,
center_y_axis,
allow_zoom, allow_zoom,
allow_drag, allow_drag,
allow_scroll, allow_scroll,
@@ -615,11 +694,13 @@ impl Plot {
mut show_y, mut show_y,
label_formatter, label_formatter,
coordinates_formatter, coordinates_formatter,
axis_formatters, x_axes,
y_axes,
legend_config, legend_config,
reset, reset,
show_background, show_background,
show_axes, show_axes,
show_grid,
linked_axes, linked_axes,
linked_cursors, linked_cursors,
@@ -628,7 +709,9 @@ impl Plot {
sharp_grid_lines, sharp_grid_lines,
} = self; } = self;
// Determine the size of the plot in the UI // Determine position of widget.
let pos = ui.available_rect_before_wrap().min;
// Determine size of widget.
let size = { let size = {
let width = width let width = width
.unwrap_or_else(|| { .unwrap_or_else(|| {
@@ -651,9 +734,79 @@ impl Plot {
.at_least(min_size.y); .at_least(min_size.y);
vec2(width, height) vec2(width, height)
}; };
// Determine complete rect of widget.
let complete_rect = Rect {
min: pos,
max: pos + size,
};
// Next we want to create this layout.
// Incides are only examples.
//
// left right
// +---+---------x----------+ +
// | | X-axis 3 |
// | +--------------------+ top
// | | X-axis 2 |
// +-+-+--------------------+-+-+
// |y|y| |y|y|
// |-|-| |-|-|
// |A|A| |A|A|
// y|x|x| Plot Window |x|x|
// |i|i| |i|i|
// |s|s| |s|s|
// |1|0| |2|3|
// +-+-+--------------------+-+-+
// | X-axis 0 | |
// +--------------------+ | bottom
// | X-axis 1 | |
// + +--------------------+---+
//
// Allocate the space. let mut plot_rect: Rect = {
let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); // Calcuclate the space needed for each axis labels.
let mut margin = Margin::ZERO;
if show_axes.x {
for cfg in &x_axes {
match cfg.placement {
axis::Placement::LeftBottom => {
margin.bottom += cfg.thickness(Axis::X);
}
axis::Placement::RightTop => {
margin.top += cfg.thickness(Axis::X);
}
}
}
}
if show_axes.y {
for cfg in &y_axes {
match cfg.placement {
axis::Placement::LeftBottom => {
margin.left += cfg.thickness(Axis::Y);
}
axis::Placement::RightTop => {
margin.right += cfg.thickness(Axis::Y);
}
}
}
}
// determine plot rectangle
margin.shrink_rect(complete_rect)
};
let [mut x_axis_widgets, mut y_axis_widgets] =
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
// If too little space, remove axis widgets
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
y_axis_widgets.clear();
x_axis_widgets.clear();
plot_rect = complete_rect;
}
// Allocate the plot window.
let response = ui.allocate_rect(plot_rect, Sense::drag());
let rect = plot_rect;
// Load or initialize the memory. // Load or initialize the memory.
let plot_id = ui.make_persistent_id(id_source); let plot_id = ui.make_persistent_id(id_source);
@@ -677,8 +830,8 @@ impl Plot {
last_plot_transform: PlotTransform::new( last_plot_transform: PlotTransform::new(
rect, rect,
min_auto_bounds, min_auto_bounds,
center_x_axis, center_axis.x,
center_y_axis, center_axis.y,
), ),
last_click_pos_for_zoom: None, last_click_pos_for_zoom: None,
}); });
@@ -839,7 +992,7 @@ impl Plot {
} }
} }
let mut transform = PlotTransform::new(rect, bounds, center_x_axis, center_y_axis); let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);
// Enforce aspect ratio // Enforce aspect ratio
if let Some(data_aspect) = data_aspect { if let Some(data_aspect) = data_aspect {
@@ -864,7 +1017,7 @@ impl Plot {
delta.y = 0.0; delta.y = 0.0;
} }
transform.translate_bounds(delta); transform.translate_bounds(delta);
bounds_modified = true.into(); bounds_modified = allow_drag;
} }
// Zooming // Zooming
@@ -935,7 +1088,7 @@ impl Plot {
} }
if zoom_factor != Vec2::splat(1.0) { if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos); transform.zoom(zoom_factor, hover_pos);
bounds_modified = true.into(); bounds_modified = allow_zoom;
} }
} }
if allow_scroll { if allow_scroll {
@@ -947,6 +1100,39 @@ impl Plot {
} }
} }
// --- transform initialized
// Add legend widgets to plot
let bounds = transform.bounds();
let x_axis_range = bounds.range_x();
let x_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[0], bounds.max[0]),
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
};
(grid_spacers[0])(input)
});
let y_axis_range = bounds.range_y();
let y_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[1], bounds.max[1]),
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
};
(grid_spacers[1])(input)
});
for mut widget in x_axis_widgets {
widget.range = x_axis_range.clone();
widget.transform = Some(transform);
widget.steps = x_steps.clone();
widget.ui(ui, Axis::X);
}
for mut widget in y_axis_widgets {
widget.range = y_axis_range.clone();
widget.transform = Some(transform);
widget.steps = y_steps.clone();
widget.ui(ui, Axis::Y);
}
// Initialize values from functions. // Initialize values from functions.
for item in &mut items { for item in &mut items {
item.initialize(transform.bounds().range_x()); item.initialize(transform.bounds().range_x());
@@ -958,16 +1144,16 @@ impl Plot {
show_y, show_y,
label_formatter, label_formatter,
coordinates_formatter, coordinates_formatter,
axis_formatters, show_grid,
show_axes,
transform, transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |(_, group)| group.x), draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |(_, group)| group.y), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
draw_cursors, draw_cursors,
grid_spacers, grid_spacers,
sharp_grid_lines, sharp_grid_lines,
clamp_grid, clamp_grid,
}; };
let plot_cursors = prepared.ui(ui, &response); let plot_cursors = prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect { if let Some(boxed_zoom_rect) = boxed_zoom_rect {
@@ -1022,7 +1208,7 @@ impl Plot {
} else { } else {
response response
}; };
ui.advance_cursor_after_rect(complete_rect);
PlotResponse { PlotResponse {
inner, inner,
response, response,
@@ -1031,6 +1217,79 @@ impl Plot {
} }
} }
fn axis_widgets(
show_axes: AxisBools,
plot_rect: Rect,
[x_axes, y_axes]: [&[AxisHints]; 2],
) -> [Vec<AxisWidget>; 2] {
let mut x_axis_widgets = Vec::<AxisWidget>::new();
let mut y_axis_widgets = Vec::<AxisWidget>::new();
// Widget count per border of plot in order left, top, right, bottom
struct NumWidgets {
left: usize,
top: usize,
right: usize,
bottom: usize,
}
let mut num_widgets = NumWidgets {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
if show_axes.x {
for cfg in x_axes {
let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
let rect = match cfg.placement {
axis::Placement::LeftBottom => {
let off = num_widgets.bottom as f32;
num_widgets.bottom += 1;
Rect {
min: plot_rect.left_bottom() + size_y * off,
max: plot_rect.right_bottom() + size_y * (off + 1.0),
}
}
axis::Placement::RightTop => {
let off = num_widgets.top as f32;
num_widgets.top += 1;
Rect {
min: plot_rect.left_top() - size_y * (off + 1.0),
max: plot_rect.right_top() - size_y * off,
}
}
};
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
if show_axes.y {
for cfg in y_axes {
let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
let rect = match cfg.placement {
axis::Placement::LeftBottom => {
let off = num_widgets.left as f32;
num_widgets.left += 1;
Rect {
min: plot_rect.left_top() - size_x * (off + 1.0),
max: plot_rect.left_bottom() - size_x * off,
}
}
axis::Placement::RightTop => {
let off = num_widgets.right as f32;
num_widgets.right += 1;
Rect {
min: plot_rect.right_top() + size_x * off,
max: plot_rect.right_bottom() + size_x * (off + 1.0),
}
}
};
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
[x_axis_widgets, y_axis_widgets]
}
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply /// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply
/// them at the right time, as other modifications need to happen first. /// them at the right time, as other modifications need to happen first.
enum BoundsModification { enum BoundsModification {
@@ -1081,17 +1340,25 @@ impl PlotUi {
.push(BoundsModification::Translate(delta_pos)); .push(BoundsModification::Translate(delta_pos));
} }
/// Can be used to check if the plot was hovered or clicked.
pub fn response(&self) -> &Response {
&self.response
}
/// Returns `true` if the plot area is currently hovered. /// Returns `true` if the plot area is currently hovered.
#[deprecated = "Use plot_ui.response().hovered()"]
pub fn plot_hovered(&self) -> bool { pub fn plot_hovered(&self) -> bool {
self.response.hovered() self.response.hovered()
} }
/// Returns `true` if the plot was clicked by the primary button. /// Returns `true` if the plot was clicked by the primary button.
#[deprecated = "Use plot_ui.response().clicked()"]
pub fn plot_clicked(&self) -> bool { pub fn plot_clicked(&self) -> bool {
self.response.clicked() self.response.clicked()
} }
/// Returns `true` if the plot was clicked by the secondary button. /// Returns `true` if the plot was clicked by the secondary button.
#[deprecated = "Use plot_ui.response().secondary_clicked()"]
pub fn plot_secondary_clicked(&self) -> bool { pub fn plot_secondary_clicked(&self) -> bool {
self.response.secondary_clicked() self.response.secondary_clicked()
} }
@@ -1258,6 +1525,7 @@ pub struct GridInput {
} }
/// One mark (horizontal or vertical line) in the background grid of a plot. /// One mark (horizontal or vertical line) in the background grid of a plot.
#[derive(Debug, Clone, Copy)]
pub struct GridMark { pub struct GridMark {
/// X or Y value in the plot. /// X or Y value in the plot.
pub value: f64, pub value: f64,
@@ -1319,14 +1587,14 @@ struct PreparedPlot {
show_y: bool, show_y: bool,
label_formatter: LabelFormatter, label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>, coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
axis_formatters: [AxisFormatter; 2], // axis_formatters: [AxisFormatter; 2],
show_axes: [bool; 2],
transform: PlotTransform, transform: PlotTransform,
show_grid: AxisBools,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool, draw_cursor_x: bool,
draw_cursor_y: bool, draw_cursor_y: bool,
draw_cursors: Vec<Cursor>, draw_cursors: Vec<Cursor>,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool, sharp_grid_lines: bool,
clamp_grid: bool, clamp_grid: bool,
} }
@@ -1335,16 +1603,11 @@ impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> { fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
let mut axes_shapes = Vec::new(); let mut axes_shapes = Vec::new();
for d in 0..2 { if self.show_grid.x {
if self.show_axes[d] { self.paint_grid(ui, &mut axes_shapes, Axis::X);
self.paint_axis( }
ui, if self.show_grid.y {
d, self.paint_grid(ui, &mut axes_shapes, Axis::Y);
self.show_axes[1 - d],
&mut axes_shapes,
self.sharp_grid_lines,
);
}
} }
// Sort the axes by strength so that those with higher strength are drawn in front. // Sort the axes by strength so that those with higher strength are drawn in front.
@@ -1421,41 +1684,27 @@ impl PreparedPlot {
cursors cursors
} }
fn paint_axis( fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
&self,
ui: &Ui,
axis: usize,
other_axis_shown: bool,
shapes: &mut Vec<(Shape, f32)>,
sharp_grid_lines: bool,
) {
#![allow(clippy::collapsible_else_if)] #![allow(clippy::collapsible_else_if)]
let Self { let Self {
transform, transform,
axis_formatters, // axis_formatters,
grid_spacers, grid_spacers,
clamp_grid, clamp_grid,
.. ..
} = self; } = self;
let bounds = transform.bounds(); let iaxis = usize::from(axis);
let axis_range = match axis {
0 => bounds.range_x(),
1 => bounds.range_y(),
_ => panic!("Axis {} does not exist.", axis),
};
let font_id = TextStyle::Body.resolve(ui.style());
// Where on the cross-dimension to show the label values // Where on the cross-dimension to show the label values
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); let bounds = transform.bounds();
let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
let input = GridInput { let input = GridInput {
bounds: (bounds.min[axis], bounds.max[axis]), bounds: (bounds.min[iaxis], bounds.max[iaxis]),
base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS, base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
}; };
let steps = (grid_spacers[axis])(input); let steps = (grid_spacers[iaxis])(input);
let clamp_range = clamp_grid.then(|| { let clamp_range = clamp_grid.then(|| {
let mut tight_bounds = PlotBounds::NOTHING; let mut tight_bounds = PlotBounds::NOTHING;
@@ -1471,25 +1720,27 @@ impl PreparedPlot {
let value_main = step.value; let value_main = step.value;
if let Some(clamp_range) = clamp_range { if let Some(clamp_range) = clamp_range {
if axis == 0 { match axis {
if !clamp_range.range_x().contains(&value_main) { Axis::X => {
continue; if !clamp_range.range_x().contains(&value_main) {
}; continue;
} else { };
if !clamp_range.range_y().contains(&value_main) { }
continue; Axis::Y => {
}; if !clamp_range.range_y().contains(&value_main) {
continue;
};
}
} }
} }
let value = if axis == 0 { let value = match axis {
PlotPoint::new(value_main, value_cross) Axis::X => PlotPoint::new(value_main, value_cross),
} else { Axis::Y => PlotPoint::new(value_cross, value_main),
PlotPoint::new(value_cross, value_main)
}; };
let pos_in_gui = transform.position_from_point(&value); let pos_in_gui = transform.position_from_point(&value);
let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 { if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
let line_strength = remap_clamp( let line_strength = remap_clamp(
@@ -1498,24 +1749,27 @@ impl PreparedPlot {
0.0..=1.0, 0.0..=1.0,
); );
let line_color = color_from_contrast(ui, line_strength); let line_color = color_from_strength(ui, line_strength);
let mut p0 = pos_in_gui; let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui; let mut p1 = pos_in_gui;
p0[1 - axis] = transform.frame().min[1 - axis]; p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p1[1 - axis] = transform.frame().max[1 - axis]; p1[1 - iaxis] = transform.frame().max[1 - iaxis];
if let Some(clamp_range) = clamp_range { if let Some(clamp_range) = clamp_range {
if axis == 0 { match axis {
p0.y = transform.position_from_point_y(clamp_range.min[1]); Axis::X => {
p1.y = transform.position_from_point_y(clamp_range.max[1]); p0.y = transform.position_from_point_y(clamp_range.min[1]);
} else { p1.y = transform.position_from_point_y(clamp_range.max[1]);
p0.x = transform.position_from_point_x(clamp_range.min[0]); }
p1.x = transform.position_from_point_x(clamp_range.max[0]); Axis::Y => {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
} }
} }
if sharp_grid_lines { if self.sharp_grid_lines {
// Round to avoid aliasing // Round to avoid aliasing
p0 = ui.ctx().round_pos_to_pixels(p0); p0 = ui.ctx().round_pos_to_pixels(p0);
p1 = ui.ctx().round_pos_to_pixels(p1); p1 = ui.ctx().round_pos_to_pixels(p1);
@@ -1526,47 +1780,6 @@ impl PreparedPlot {
line_strength, line_strength,
)); ));
} }
const MIN_TEXT_SPACING: f32 = 40.0;
if spacing_in_points > MIN_TEXT_SPACING {
let text_strength =
remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0);
let color = color_from_contrast(ui, text_strength);
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main, &axis_range)
} else {
emath::round_to_decimals(value_main, 5).to_string() // hack
};
// Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice)
let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0;
// Custom formatters can return empty string to signal "no label at this resolution"
if !text.is_empty() && !skip_origin_y {
let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
// Make sure we see the labels, even if the axis is off-screen:
text_pos[1 - axis] = text_pos[1 - axis]
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
.at_least(transform.frame().min[1 - axis] + 1.0);
shapes.push((Shape::galley(text_pos, galley), text_strength));
}
}
}
fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color;
let fg = ui.visuals().widgets.open.fg_stroke.color;
let mix = 0.5 * contrast.sqrt();
Color32::from_rgb(
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
)
} }
} }
@@ -1666,9 +1879,21 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
let is_integral = number as i64 as f64 == number; let is_integral = number as i64 as f64 == number;
if is_integral { if is_integral {
// perfect integer - show it as such: // perfect integer - show it as such:
format!("{:.0}", number) format!("{number:.0}")
} else { } else {
// make sure we tell the user it is not an integer by always showing a decimal or two: // make sure we tell the user it is not an integer by always showing a decimal or two:
format!("{:.*}", num_decimals.at_least(1), number) format!("{:.*}", num_decimals.at_least(1), number)
} }
} }
/// Determine a color from a 0-1 strength value.
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color;
let fg = ui.visuals().widgets.open.fg_stroke.color;
let mix = 0.5 * strength.sqrt();
Color32::from_rgb(
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
)
}

View File

@@ -77,8 +77,10 @@ pub struct Slider<'a> {
prefix: String, prefix: String,
suffix: String, suffix: String,
text: WidgetText, text: WidgetText,
/// Sets the minimal step of the widget value /// Sets the minimal step of the widget value
step: Option<f64>, step: Option<f64>,
drag_value_speed: Option<f64>, drag_value_speed: Option<f64>,
min_decimals: usize, min_decimals: usize,
max_decimals: Option<usize>, max_decimals: Option<usize>,
@@ -524,12 +526,12 @@ impl<'a> Slider<'a> {
} }
/// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen. /// For instance, `position` is the mouse position and `position_range` is the physical location of the slider on the screen.
fn value_from_position(&self, position: f32, position_range: RangeInclusive<f32>) -> f64 { fn value_from_position(&self, position: f32, position_range: Rangef) -> f64 {
let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64; let normalized = remap_clamp(position, position_range, 0.0..=1.0) as f64;
value_from_normalized(normalized, self.range(), &self.spec) value_from_normalized(normalized, self.range(), &self.spec)
} }
fn position_from_value(&self, value: f64, position_range: RangeInclusive<f32>) -> f32 { fn position_from_value(&self, value: f64, position_range: Rangef) -> f32 {
let normalized = normalized_from_value(value, self.range(), &self.spec); let normalized = normalized_from_value(value, self.range(), &self.spec);
lerp(position_range, normalized as f32) lerp(position_range, normalized as f32)
} }
@@ -555,11 +557,11 @@ impl<'a> Slider<'a> {
let new_value = if self.smart_aim { let new_value = if self.smart_aim {
let aim_radius = ui.input(|i| i.aim_radius()); let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64( emath::smart_aim::best_in_range_f64(
self.value_from_position(position - aim_radius, position_range.clone()), self.value_from_position(position - aim_radius, position_range),
self.value_from_position(position + aim_radius, position_range.clone()), self.value_from_position(position + aim_radius, position_range),
) )
} else { } else {
self.value_from_position(position, position_range.clone()) self.value_from_position(position, position_range)
}; };
self.set_value(new_value); self.set_value(new_value);
} }
@@ -594,18 +596,18 @@ impl<'a> Slider<'a> {
if kb_step != 0.0 { if kb_step != 0.0 {
let prev_value = self.get_value(); let prev_value = self.get_value();
let prev_position = self.position_from_value(prev_value, position_range.clone()); let prev_position = self.position_from_value(prev_value, position_range);
let new_position = prev_position + kb_step; let new_position = prev_position + kb_step;
let new_value = match self.step { let new_value = match self.step {
Some(step) => prev_value + (kb_step as f64 * step), Some(step) => prev_value + (kb_step as f64 * step),
None if self.smart_aim => { None if self.smart_aim => {
let aim_radius = ui.input(|i| i.aim_radius()); let aim_radius = ui.input(|i| i.aim_radius());
emath::smart_aim::best_in_range_f64( emath::smart_aim::best_in_range_f64(
self.value_from_position(new_position - aim_radius, position_range.clone()), self.value_from_position(new_position - aim_radius, position_range),
self.value_from_position(new_position + aim_radius, position_range.clone()), self.value_from_position(new_position + aim_radius, position_range),
) )
} }
_ => self.value_from_position(new_position, position_range.clone()), _ => self.value_from_position(new_position, position_range),
}; };
self.set_value(new_value); self.set_value(new_value);
} }
@@ -686,15 +688,11 @@ impl<'a> Slider<'a> {
} }
} }
fn position_range(&self, rect: &Rect) -> RangeInclusive<f32> { fn position_range(&self, rect: &Rect) -> Rangef {
let handle_radius = self.handle_radius(rect); let handle_radius = self.handle_radius(rect);
match self.orientation { match self.orientation {
SliderOrientation::Horizontal => { SliderOrientation::Horizontal => rect.x_range().shrink(handle_radius),
(rect.left() + handle_radius)..=(rect.right() - handle_radius) SliderOrientation::Vertical => rect.y_range().shrink(handle_radius),
}
SliderOrientation::Vertical => {
(rect.bottom() - handle_radius)..=(rect.top() + handle_radius)
}
} }
} }
@@ -726,7 +724,7 @@ impl<'a> Slider<'a> {
} }
} }
fn value_ui(&mut self, ui: &mut Ui, position_range: RangeInclusive<f32>) -> Response { fn value_ui(&mut self, ui: &mut Ui, position_range: Rangef) -> Response {
// If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step` // If [`DragValue`] is controlled from the keyboard and `step` is defined, set speed to `step`
let change = ui.input(|input| { let change = ui.input(|input| {
input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32 input.num_presses(Key::ArrowUp) as i32 + input.num_presses(Key::ArrowRight) as i32
@@ -740,7 +738,7 @@ impl<'a> Slider<'a> {
step step
} else { } else {
self.drag_value_speed self.drag_value_speed
.unwrap_or_else(|| self.current_gradient(&position_range)) .unwrap_or_else(|| self.current_gradient(position_range))
}; };
let mut value = self.get_value(); let mut value = self.get_value();
@@ -767,12 +765,11 @@ impl<'a> Slider<'a> {
} }
/// delta(value) / delta(points) /// delta(value) / delta(points)
fn current_gradient(&mut self, position_range: &RangeInclusive<f32>) -> f64 { fn current_gradient(&mut self, position_range: Rangef) -> f64 {
// TODO(emilk): handle clamping // TODO(emilk): handle clamping
let value = self.get_value(); let value = self.get_value();
let value_from_pos = let value_from_pos = |position: f32| self.value_from_position(position, position_range);
|position: f32| self.value_from_position(position, position_range.clone()); let pos_from_value = |value: f64| self.position_from_value(value, position_range);
let pos_from_value = |value: f64| self.position_from_value(value, position_range.clone());
let left_value = value_from_pos(pos_from_value(value) - 0.5); let left_value = value_from_pos(pos_from_value(value) - 0.5);
let right_value = value_from_pos(pos_from_value(value) + 0.5); let right_value = value_from_pos(pos_from_value(value) + 0.5);
right_value - left_value right_value - left_value

View File

@@ -1138,7 +1138,7 @@ fn paint_cursor_end(
galley: &Galley, galley: &Galley,
cursor: &Cursor, cursor: &Cursor,
) -> Rect { ) -> Rect {
let stroke = ui.visuals().selection.stroke; let stroke = ui.visuals().text_cursor;
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2()); let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys
@@ -1147,10 +1147,7 @@ fn paint_cursor_end(
let top = cursor_pos.center_top(); let top = cursor_pos.center_top();
let bottom = cursor_pos.center_bottom(); let bottom = cursor_pos.center_bottom();
painter.line_segment( painter.line_segment([top, bottom], (stroke.width, stroke.color));
[top, bottom],
(ui.visuals().text_cursor_width, stroke.color),
);
if false { if false {
// Roof/floor: // Roof/floor:
@@ -1185,7 +1182,7 @@ fn insert_text(
if char_limit < usize::MAX { if char_limit < usize::MAX {
let mut new_string = text_to_insert; let mut new_string = text_to_insert;
// Avoid subtract with overflow panic // Avoid subtract with overflow panic
let cutoff = char_limit.saturating_sub(text.as_str().len()); let cutoff = char_limit.saturating_sub(text.as_str().chars().count());
new_string = match new_string.char_indices().nth(cutoff) { new_string = match new_string.char_indices().nth(cutoff) {
None => new_string, None => new_string,

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
publish = false publish = false
default-run = "egui_demo_app" default-run = "egui_demo_app"
@@ -65,6 +65,6 @@ env_logger = "0.10"
# web: # web:
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "=0.2.86" wasm-bindgen = "=0.2.87"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-sys = "0.3" web-sys = "0.3"

View File

@@ -139,7 +139,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> boo
if ui.button("Random image").clicked() { if ui.button("Random image").clicked() {
let seed = ui.input(|i| i.time); let seed = ui.input(|i| i.time);
let side = 640; let side = 640;
*url = format!("https://picsum.photos/seed/{}/{}", seed, side); *url = format!("https://picsum.photos/seed/{seed}/{side}");
trigger_fetch = true; trigger_fetch = true;
} }
}); });

View File

@@ -239,8 +239,7 @@ impl BackendPanel {
if ui if ui
.add_enabled(enabled, egui::Button::new("Reset")) .add_enabled(enabled, egui::Button::new("Reset"))
.on_hover_text(format!( .on_hover_text(format!(
"Reset scale to native value ({:.1})", "Reset scale to native value ({native_pixels_per_point:.1})"
native_pixels_per_point
)) ))
.clicked() .clicked()
{ {
@@ -454,8 +453,8 @@ impl EguiWindows {
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.stick_to_bottom(true) .stick_to_bottom(true)
.show(ui, |ui| { .show(ui, |ui| {
for event in &*tmp_output_event_history.read().unwrap() { for event in output_event_history.read() {
ui.label(format!("{:?}", event)); ui.label(format!("{event:?}"));
} }
}); });
}); });

View File

@@ -161,6 +161,15 @@ impl Default for Anchor {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(Clone, Copy, Debug)]
#[must_use]
enum Command {
Nothing,
ResetEverything,
}
// ----------------------------------------------------------------------------
/// The state that we persist (serialize). /// The state that we persist (serialize).
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -286,18 +295,19 @@ impl eframe::App for WrapApp {
frame.set_fullscreen(!frame.info().window_info.fullscreen); frame.set_fullscreen(!frame.info().window_info.fullscreen);
} }
let mut cmd = Command::Nothing;
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
egui::trace!(ui); egui::trace!(ui);
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.visuals_mut().button_frame = false; ui.visuals_mut().button_frame = false;
self.bar_contents(ui, frame); self.bar_contents(ui, frame, &mut cmd);
}); });
}); });
self.state.backend_panel.update(ctx, frame); self.state.backend_panel.update(ctx, frame);
if !is_mobile(ctx) { if !is_mobile(ctx) {
self.backend_panel(ctx, frame); cmd = self.backend_panel(ctx, frame);
} }
self.show_selected_app(ctx, frame, render); self.show_selected_app(ctx, frame, render);
@@ -310,6 +320,8 @@ impl eframe::App for WrapApp {
if !frame.is_web() { if !frame.is_web() {
egui::gui_zoom::zoom_with_keyboard_shortcuts(ctx, frame.info().native_pixels_per_point); egui::gui_zoom::zoom_with_keyboard_shortcuts(ctx, frame.info().native_pixels_per_point);
} }
self.run_cmd(ctx, cmd);
} }
#[cfg(feature = "glow")] #[cfg(feature = "glow")]
@@ -326,12 +338,14 @@ impl eframe::App for WrapApp {
} }
impl WrapApp { impl WrapApp {
fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { fn backend_panel(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) -> Command {
// The backend-panel can be toggled on/off. // The backend-panel can be toggled on/off.
// We show a little animation when the user switches it. // We show a little animation when the user switches it.
let is_open = let is_open =
self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible()); self.state.backend_panel.open || ctx.memory(|mem| mem.everything_is_visible());
let mut cmd = Command::Nothing;
egui::SidePanel::left("backend_panel") egui::SidePanel::left("backend_panel")
.resizable(false) .resizable(false)
.show_animated(ctx, is_open, |ui| { .show_animated(ctx, is_open, |ui| {
@@ -340,11 +354,28 @@ impl WrapApp {
}); });
ui.separator(); ui.separator();
self.backend_panel_contents(ui, frame); self.backend_panel_contents(ui, frame, &mut cmd);
}); });
cmd
} }
fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn run_cmd(&mut self, ctx: &egui::Context, cmd: Command) {
match cmd {
Command::Nothing => {}
Command::ResetEverything => {
self.state = Default::default();
ctx.memory_mut(|mem| *mem = Default::default());
}
}
}
fn backend_panel_contents(
&mut self,
ui: &mut egui::Ui,
frame: &mut eframe::Frame,
cmd: &mut Command,
) {
self.state.backend_panel.ui(ui, frame); self.state.backend_panel.ui(ui, frame);
ui.separator(); ui.separator();
@@ -360,8 +391,7 @@ impl WrapApp {
} }
if ui.button("Reset everything").clicked() { if ui.button("Reset everything").clicked() {
self.state = Default::default(); *cmd = Command::ResetEverything;
ui.ctx().memory_mut(|mem| *mem = Default::default());
ui.close_menu(); ui.close_menu();
} }
}); });
@@ -381,7 +411,7 @@ impl WrapApp {
} }
} }
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cmd: &mut Command) {
egui::widgets::global_dark_light_mode_switch(ui); egui::widgets::global_dark_light_mode_switch(ui);
ui.separator(); ui.separator();
@@ -389,7 +419,7 @@ impl WrapApp {
if is_mobile(ui.ctx()) { if is_mobile(ui.ctx()) {
ui.menu_button("💻 Backend", |ui| { ui.menu_button("💻 Backend", |ui| {
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`. ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
self.backend_panel_contents(ui, frame); self.backend_panel_contents(ui, frame, cmd);
}); });
} else { } else {
ui.toggle_value(&mut self.state.backend_panel.open, "💻 Backend"); ui.toggle_value(&mut self.state.backend_panel.open, "💻 Backend");
@@ -405,7 +435,7 @@ impl WrapApp {
{ {
selected_anchor = anchor; selected_anchor = anchor;
if frame.is_web() { if frame.is_web() {
ui.output_mut(|o| o.open_url(format!("#{}", anchor))); ui.output_mut(|o| o.open_url(format!("#{anchor}")));
} }
} }
} }

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Example library for egui" description = "Example library for egui"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib" homepage = "https://github.com/emilk/egui/tree/master/crates/egui_demo_lib"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"

View File

@@ -83,11 +83,11 @@ fn about_immediate_mode(ui: &mut egui::Ui) {
fn links(ui: &mut egui::Ui) { fn links(ui: &mut egui::Ui) {
use egui::special_emojis::{GITHUB, TWITTER}; use egui::special_emojis::{GITHUB, TWITTER};
ui.hyperlink_to( ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB), format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui", "https://github.com/emilk/egui",
); );
ui.hyperlink_to( ui.hyperlink_to(
format!("{} @ernerfeldt", TWITTER), format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt", "https://twitter.com/ernerfeldt",
); );
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/"); ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");

View File

@@ -121,7 +121,7 @@ impl CodeExample {
ui.separator(); ui.separator();
code_view_ui(ui, &format!("{:#?}", self)); code_view_ui(ui, &format!("{self:#?}"));
ui.separator(); ui.separator();

View File

@@ -252,11 +252,11 @@ impl DemoWindows {
use egui::special_emojis::{GITHUB, TWITTER}; use egui::special_emojis::{GITHUB, TWITTER};
ui.hyperlink_to( ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB), format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui", "https://github.com/emilk/egui",
); );
ui.hyperlink_to( ui.hyperlink_to(
format!("{} @ernerfeldt", TWITTER), format!("{TWITTER} @ernerfeldt"),
"https://twitter.com/ernerfeldt", "https://twitter.com/ernerfeldt",
); );

View File

@@ -140,7 +140,7 @@ impl LayoutTest {
Direction::TopDown, Direction::TopDown,
Direction::BottomUp, Direction::BottomUp,
] { ] {
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir)); ui.radio_value(&mut self.layout.main_dir, dir, format!("{dir:?}"));
} }
}); });
@@ -162,7 +162,7 @@ impl LayoutTest {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Cross Align:"); ui.label("Cross Align:");
for &align in &[Align::Min, Align::Center, Align::Max] { for &align in &[Align::Min, Align::Center, Align::Max] {
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align)); ui.radio_value(&mut self.layout.cross_align, align, format!("{align:?}"));
} }
}); });

View File

@@ -1,5 +1,4 @@
use super::*; use super::*;
use crate::LOREM_IPSUM;
use egui::{epaint::text::TextWrapping, *}; use egui::{epaint::text::TextWrapping, *};
/// Showcase some ui code /// Showcase some ui code
@@ -8,9 +7,7 @@ use egui::{epaint::text::TextWrapping, *};
pub struct MiscDemoWindow { pub struct MiscDemoWindow {
num_columns: usize, num_columns: usize,
break_anywhere: bool, text_break: TextBreakDemo,
max_rows: usize,
overflow_character: Option<char>,
widgets: Widgets, widgets: Widgets,
colors: ColorWidgets, colors: ColorWidgets,
@@ -27,9 +24,7 @@ impl Default for MiscDemoWindow {
MiscDemoWindow { MiscDemoWindow {
num_columns: 2, num_columns: 2,
max_rows: 2, text_break: Default::default(),
break_anywhere: false,
overflow_character: Some('…'),
widgets: Default::default(), widgets: Default::default(),
colors: Default::default(), colors: Default::default(),
@@ -61,8 +56,14 @@ impl View for MiscDemoWindow {
fn ui(&mut self, ui: &mut Ui) { fn ui(&mut self, ui: &mut Ui) {
ui.set_min_width(250.0); ui.set_min_width(250.0);
CollapsingHeader::new("Widgets") CollapsingHeader::new("Label")
.default_open(true) .default_open(true)
.show(ui, |ui| {
label_ui(ui);
});
CollapsingHeader::new("Misc widgets")
.default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
self.widgets.ui(ui); self.widgets.ui(ui);
}); });
@@ -70,12 +71,12 @@ impl View for MiscDemoWindow {
CollapsingHeader::new("Text layout") CollapsingHeader::new("Text layout")
.default_open(false) .default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
text_layout_ui( text_layout_demo(ui);
ui, ui.separator();
&mut self.max_rows, self.text_break.ui(ui);
&mut self.break_anywhere, ui.vertical_centered(|ui| {
&mut self.overflow_character, ui.add(crate::egui_github_link_file_line!());
); });
}); });
CollapsingHeader::new("Colors") CollapsingHeader::new("Colors")
@@ -177,6 +178,43 @@ impl View for MiscDemoWindow {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
fn label_ui(ui: &mut egui::Ui) {
ui.vertical_centered(|ui| {
ui.add(crate::egui_github_link_file_line!());
});
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);
ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");
ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");
ui.monospace("There is also a monospace font.");
});
ui.add(
egui::Label::new(
"Labels containing long text can be set to elide the text that doesn't fit on a single line using `Label::elide`. When hovered, the label will show the full text.",
)
.truncate(true),
);
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct Widgets { pub struct Widgets {
@@ -200,28 +238,6 @@ impl Widgets {
ui.add(crate::egui_github_link_file_line!()); ui.add(crate::egui_github_link_file_line!());
}); });
ui.horizontal_wrapped(|ui| {
// Trick so we don't have to add spaces in the text below:
let width = ui.fonts(|f|f.glyph_width(&TextStyle::Body.resolve(ui.style()), ' '));
ui.spacing_mut().item_spacing.x = width;
ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110)));
ui.colored_label(Color32::from_rgb(128, 140, 255), "color"); // Shortcut version
ui.label("and tooltips.").on_hover_text(
"This is a multiline tooltip that demonstrates that you can easily add tooltips to any element.\nThis is the second line.\nThis is the third.",
);
ui.label("You can mix in other widgets into text, like");
let _ = ui.small_button("this button");
ui.label(".");
ui.label("The default font supports all latin and cyrillic characters (ИÅđ…), common math symbols (∫√∞²⅓…), and many emojis (💓🌟🖩…).")
.on_hover_text("There is currently no support for right-to-left languages.");
ui.label("See the 🔤 Font Book for more!");
ui.monospace("There is also a monospace font.");
});
let tooltip_ui = |ui: &mut Ui| { let tooltip_ui = |ui: &mut Ui| {
ui.heading("The name of the tooltip"); ui.heading("The name of the tooltip");
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -455,7 +471,7 @@ impl Tree {
.into_iter() .into_iter()
.enumerate() .enumerate()
.filter_map(|(i, mut tree)| { .filter_map(|(i, mut tree)| {
if tree.ui_impl(ui, depth + 1, &format!("child #{}", i)) == Action::Keep { if tree.ui_impl(ui, depth + 1, &format!("child #{i}")) == Action::Keep {
Some(tree) Some(tree)
} else { } else {
None None
@@ -473,12 +489,7 @@ impl Tree {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
fn text_layout_ui( fn text_layout_demo(ui: &mut Ui) {
ui: &mut egui::Ui,
max_rows: &mut usize,
break_anywhere: &mut bool,
overflow_character: &mut Option<char>,
) {
use egui::text::LayoutJob; use egui::text::LayoutJob;
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -632,32 +643,64 @@ fn text_layout_ui(
); );
ui.label(job); ui.label(job);
}
ui.separator();
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
ui.horizontal(|ui| { #[cfg_attr(feature = "serde", serde(default))]
ui.add(DragValue::new(max_rows)); struct TextBreakDemo {
ui.label("Max rows"); break_anywhere: bool,
}); max_rows: usize,
ui.checkbox(break_anywhere, "Break anywhere"); overflow_character: Option<char>,
ui.horizontal(|ui| { }
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), ""); impl Default for TextBreakDemo {
ui.selectable_value(overflow_character, Some('—'), ""); fn default() -> Self {
ui.selectable_value(overflow_character, Some('-'), " - "); Self {
ui.label("Overflow character"); max_rows: 1,
}); break_anywhere: true,
overflow_character: Some('…'),
let mut job = LayoutJob::single_section(LOREM_IPSUM.to_owned(), TextFormat::default()); }
job.wrap = TextWrapping { }
max_rows: *max_rows, }
break_anywhere: *break_anywhere,
overflow_character: *overflow_character, impl TextBreakDemo {
..Default::default() pub fn ui(&mut self, ui: &mut Ui) {
}; let Self {
ui.label(job); break_anywhere,
max_rows,
ui.vertical_centered(|ui| { overflow_character,
ui.add(crate::egui_github_link_file_line!()); } = self;
});
use egui::text::LayoutJob;
ui.horizontal(|ui| {
ui.add(DragValue::new(max_rows));
ui.label("Max rows");
});
ui.horizontal(|ui| {
ui.label("Line-break:");
ui.radio_value(break_anywhere, false, "word boundaries");
ui.radio_value(break_anywhere, true, "anywhere");
});
ui.horizontal(|ui| {
ui.selectable_value(overflow_character, None, "None");
ui.selectable_value(overflow_character, Some('…'), "");
ui.selectable_value(overflow_character, Some('—'), "");
ui.selectable_value(overflow_character, Some('-'), " - ");
ui.label("Overflow character");
});
let mut job =
LayoutJob::single_section(crate::LOREM_IPSUM_LONG.to_owned(), TextFormat::default());
job.wrap = TextWrapping {
max_rows: *max_rows,
break_anywhere: *break_anywhere,
overflow_character: *overflow_character,
..Default::default()
};
ui.label(job); // `Label` overrides some of the wrapping settings, e.g. wrap width
}
} }

View File

@@ -50,7 +50,7 @@ impl super::View for MultiTouch {
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers."); ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches)); let num_touches = ui.input(|i| i.multi_touch().map_or(0, |mt| mt.num_touches));
ui.label(format!("Current touches: {}", num_touches)); ui.label(format!("Current touches: {num_touches}"));
let color = if ui.visuals().dark_mode { let color = if ui.visuals().dark_mode {
Color32::WHITE Color32::WHITE

View File

@@ -1,12 +1,12 @@
use std::f64::consts::TAU; use std::f64::consts::TAU;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use egui::plot::{AxisBools, GridInput, GridMark, PlotResponse};
use egui::*; use egui::*;
use plot::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine, use egui::plot::{
Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Arrows, AxisBools, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter,
Text, VLine, Corner, GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage,
PlotPoint, PlotPoints, PlotResponse, Points, Polygon, Text, VLine,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -39,8 +39,8 @@ pub struct PlotDemo {
charts_demo: ChartsDemo, charts_demo: ChartsDemo,
items_demo: ItemsDemo, items_demo: ItemsDemo,
interaction_demo: InteractionDemo, interaction_demo: InteractionDemo,
custom_axes_demo: CustomAxisDemo, custom_axes_demo: CustomAxesDemo,
linked_axes_demo: LinkedAxisDemo, linked_axes_demo: LinkedAxesDemo,
open_panel: Panel, open_panel: Panel,
} }
@@ -119,17 +119,9 @@ impl super::View for PlotDemo {
} }
} }
fn is_approx_zero(val: f64) -> bool {
val.abs() < 1e-6
}
fn is_approx_integer(val: f64) -> bool {
val.fract().abs() < 1e-6
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(PartialEq)] #[derive(Copy, Clone, PartialEq)]
struct LineDemo { struct LineDemo {
animate: bool, animate: bool,
time: f64, time: f64,
@@ -138,6 +130,8 @@ struct LineDemo {
square: bool, square: bool,
proportional: bool, proportional: bool,
coordinates: bool, coordinates: bool,
show_axes: bool,
show_grid: bool,
line_style: LineStyle, line_style: LineStyle,
} }
@@ -151,6 +145,8 @@ impl Default for LineDemo {
square: false, square: false,
proportional: true, proportional: true,
coordinates: true, coordinates: true,
show_axes: true,
show_grid: true,
line_style: LineStyle::Solid, line_style: LineStyle::Solid,
} }
} }
@@ -165,9 +161,10 @@ impl LineDemo {
circle_center, circle_center,
square, square,
proportional, proportional,
line_style,
coordinates, coordinates,
.. show_axes,
show_grid,
line_style,
} = self; } = self;
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -195,6 +192,13 @@ impl LineDemo {
}); });
}); });
ui.vertical(|ui| {
ui.checkbox(show_axes, "Show axes");
ui.checkbox(show_grid, "Show grid");
ui.checkbox(coordinates, "Show coordinates on hover")
.on_hover_text("Can take a custom formatting function.");
});
ui.vertical(|ui| { ui.vertical(|ui| {
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
ui.checkbox(animate, "Animate"); ui.checkbox(animate, "Animate");
@@ -202,8 +206,6 @@ impl LineDemo {
.on_hover_text("Always keep the viewport square."); .on_hover_text("Always keep the viewport square.");
ui.checkbox(proportional, "Proportional data axes") ui.checkbox(proportional, "Proportional data axes")
.on_hover_text("Tick are the same size on both axes."); .on_hover_text("Tick are the same size on both axes.");
ui.checkbox(coordinates, "Show coordinates")
.on_hover_text("Can take a custom formatting function.");
ComboBox::from_label("Line style") ComboBox::from_label("Line style")
.selected_text(line_style.to_string()) .selected_text(line_style.to_string())
@@ -268,11 +270,16 @@ impl LineDemo {
impl LineDemo { impl LineDemo {
fn ui(&mut self, ui: &mut Ui) -> Response { fn ui(&mut self, ui: &mut Ui) -> Response {
self.options_ui(ui); self.options_ui(ui);
if self.animate { if self.animate {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64; self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64;
}; };
let mut plot = Plot::new("lines_demo").legend(Legend::default()); let mut plot = Plot::new("lines_demo")
.legend(Legend::default())
.y_axis_width(4)
.show_axes(self.show_axes)
.show_grid(self.show_grid);
if self.square { if self.square {
plot = plot.view_aspect(1.0); plot = plot.view_aspect(1.0);
} }
@@ -326,7 +333,7 @@ impl MarkerDemo {
[5.0, 0.0 + y_offset], [5.0, 0.0 + y_offset],
[6.0, 0.5 + y_offset], [6.0, 0.5 + y_offset],
]) ])
.name(format!("{:?}", marker)) .name(format!("{marker:?}"))
.filled(self.fill_markers) .filled(self.fill_markers)
.radius(self.marker_radius) .radius(self.marker_radius)
.shape(marker); .shape(marker);
@@ -416,7 +423,7 @@ impl LegendDemo {
ui.label("Position:"); ui.label("Position:");
ui.horizontal(|ui| { ui.horizontal(|ui| {
Corner::all().for_each(|position| { Corner::all().for_each(|position| {
ui.selectable_value(&mut config.position, position, format!("{:?}", position)); ui.selectable_value(&mut config.position, position, format!("{position:?}"));
}); });
}); });
ui.end_row(); ui.end_row();
@@ -429,8 +436,8 @@ impl LegendDemo {
); );
ui.end_row(); ui.end_row();
}); });
let legend_plot = Plot::new("legend_demo") let legend_plot = Plot::new("legend_demo")
.y_axis_width(2)
.legend(config.clone()) .legend(config.clone())
.data_aspect(1.0); .data_aspect(1.0);
legend_plot legend_plot
@@ -448,19 +455,19 @@ impl LegendDemo {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(PartialEq, Default)] #[derive(PartialEq, Default)]
struct CustomAxisDemo {} struct CustomAxesDemo {}
impl CustomAxisDemo { impl CustomAxesDemo {
const MINS_PER_DAY: f64 = 24.0 * 60.0; const MINS_PER_DAY: f64 = 24.0 * 60.0;
const MINS_PER_H: f64 = 60.0; const MINS_PER_H: f64 = 60.0;
fn logistic_fn() -> Line { fn logistic_fn() -> Line {
fn days(min: f64) -> f64 { fn days(min: f64) -> f64 {
CustomAxisDemo::MINS_PER_DAY * min CustomAxesDemo::MINS_PER_DAY * min
} }
let values = PlotPoints::from_explicit_callback( let values = PlotPoints::from_explicit_callback(
move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()), move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxesDemo::MINS_PER_DAY - 2.0)).exp()),
days(0.0)..days(5.0), days(0.0)..days(5.0),
100, 100,
); );
@@ -504,8 +511,8 @@ impl CustomAxisDemo {
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
fn ui(&mut self, ui: &mut Ui) -> Response { fn ui(&mut self, ui: &mut Ui) -> Response {
const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY; const MINS_PER_DAY: f64 = CustomAxesDemo::MINS_PER_DAY;
const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H; const MINS_PER_H: f64 = CustomAxesDemo::MINS_PER_H;
fn day(x: f64) -> f64 { fn day(x: f64) -> f64 {
(x / MINS_PER_DAY).floor() (x / MINS_PER_DAY).floor()
@@ -523,7 +530,7 @@ impl CustomAxisDemo {
100.0 * y 100.0 * y
} }
let x_fmt = |x, _range: &RangeInclusive<f64>| { let x_fmt = |x, _digits, _range: &RangeInclusive<f64>| {
if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY { if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY {
// No labels outside value bounds // No labels outside value bounds
String::new() String::new()
@@ -536,7 +543,7 @@ impl CustomAxisDemo {
} }
}; };
let y_fmt = |y, _range: &RangeInclusive<f64>| { let y_fmt = |y, _digits, _range: &RangeInclusive<f64>| {
// Display only integer percentages // Display only integer percentages
if !is_approx_zero(y) && is_approx_integer(100.0 * y) { if !is_approx_zero(y) && is_approx_integer(100.0 * y) {
format!("{:.0}%", percent(y)) format!("{:.0}%", percent(y))
@@ -557,14 +564,27 @@ impl CustomAxisDemo {
ui.label("Zoom in on the X-axis to see hours and minutes"); ui.label("Zoom in on the X-axis to see hours and minutes");
let x_axes = vec![
AxisHints::default().label("Time").formatter(x_fmt),
AxisHints::default().label("Value"),
];
let y_axes = vec![
AxisHints::default()
.label("Percent")
.formatter(y_fmt)
.max_digits(4),
AxisHints::default()
.label("Absolute")
.placement(plot::HPlacement::Right),
];
Plot::new("custom_axes") Plot::new("custom_axes")
.data_aspect(2.0 * MINS_PER_DAY as f32) .data_aspect(2.0 * MINS_PER_DAY as f32)
.x_axis_formatter(x_fmt) .custom_x_axes(x_axes)
.y_axis_formatter(y_fmt) .custom_y_axes(y_axes)
.x_grid_spacer(CustomAxisDemo::x_grid) .x_grid_spacer(CustomAxesDemo::x_grid)
.label_formatter(label_fmt) .label_formatter(label_fmt)
.show(ui, |plot_ui| { .show(ui, |plot_ui| {
plot_ui.line(CustomAxisDemo::logistic_fn()); plot_ui.line(CustomAxesDemo::logistic_fn());
}) })
.response .response
} }
@@ -573,29 +593,25 @@ impl CustomAxisDemo {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
#[derive(PartialEq)] #[derive(PartialEq)]
struct LinkedAxisDemo { struct LinkedAxesDemo {
link_x: bool, link_x: bool,
link_y: bool, link_y: bool,
link_cursor_x: bool, link_cursor_x: bool,
link_cursor_y: bool, link_cursor_y: bool,
} }
impl Default for LinkedAxisDemo { impl Default for LinkedAxesDemo {
fn default() -> Self { fn default() -> Self {
let link_x = true;
let link_y = false;
let link_cursor_x = true;
let link_cursor_y = false;
Self { Self {
link_x, link_x: true,
link_y, link_y: true,
link_cursor_x, link_cursor_x: true,
link_cursor_y, link_cursor_y: true,
} }
} }
} }
impl LinkedAxisDemo { impl LinkedAxesDemo {
fn line_with_slope(slope: f64) -> Line { fn line_with_slope(slope: f64) -> Line {
Line::new(PlotPoints::from_explicit_callback( Line::new(PlotPoints::from_explicit_callback(
move |x| slope * x, move |x| slope * x,
@@ -621,11 +637,11 @@ impl LinkedAxisDemo {
} }
fn configure_plot(plot_ui: &mut plot::PlotUi) { fn configure_plot(plot_ui: &mut plot::PlotUi) {
plot_ui.line(LinkedAxisDemo::line_with_slope(0.5)); plot_ui.line(LinkedAxesDemo::line_with_slope(0.5));
plot_ui.line(LinkedAxisDemo::line_with_slope(1.0)); plot_ui.line(LinkedAxesDemo::line_with_slope(1.0));
plot_ui.line(LinkedAxisDemo::line_with_slope(2.0)); plot_ui.line(LinkedAxesDemo::line_with_slope(2.0));
plot_ui.line(LinkedAxisDemo::sin()); plot_ui.line(LinkedAxesDemo::sin());
plot_ui.line(LinkedAxisDemo::cos()); plot_ui.line(LinkedAxesDemo::cos());
} }
fn ui(&mut self, ui: &mut Ui) -> Response { fn ui(&mut self, ui: &mut Ui) -> Response {
@@ -642,28 +658,32 @@ impl LinkedAxisDemo {
let link_group_id = ui.id().with("linked_demo"); let link_group_id = ui.id().with("linked_demo");
ui.horizontal(|ui| { ui.horizontal(|ui| {
Plot::new("linked_axis_1") Plot::new("left-top")
.data_aspect(1.0) .data_aspect(1.0)
.width(250.0) .width(250.0)
.height(250.0) .height(250.0)
.link_axis(link_group_id, self.link_x, self.link_y) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot); .show(ui, LinkedAxesDemo::configure_plot);
Plot::new("linked_axis_2") Plot::new("right-top")
.data_aspect(2.0) .data_aspect(2.0)
.width(150.0) .width(150.0)
.height(250.0) .height(250.0)
.y_axis_width(3)
.y_axis_label("y")
.y_axis_position(plot::HPlacement::Right)
.link_axis(link_group_id, self.link_x, self.link_y) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot); .show(ui, LinkedAxesDemo::configure_plot);
}); });
Plot::new("linked_axis_3") Plot::new("left-bottom")
.data_aspect(0.5) .data_aspect(0.5)
.width(250.0) .width(250.0)
.height(150.0) .height(150.0)
.x_axis_label("x")
.link_axis(link_group_id, self.link_x, self.link_y) .link_axis(link_group_id, self.link_x, self.link_y)
.link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y)
.show(ui, LinkedAxisDemo::configure_plot) .show(ui, LinkedAxesDemo::configure_plot)
.response .response
} }
} }
@@ -761,7 +781,7 @@ impl InteractionDemo {
plot_ui.pointer_coordinate(), plot_ui.pointer_coordinate(),
plot_ui.pointer_coordinate_drag_delta(), plot_ui.pointer_coordinate_drag_delta(),
plot_ui.plot_bounds(), plot_ui.plot_bounds(),
plot_ui.plot_hovered(), plot_ui.response().hovered(),
) )
}); });
@@ -774,21 +794,18 @@ impl InteractionDemo {
"origin in screen coordinates: x: {:.02}, y: {:.02}", "origin in screen coordinates: x: {:.02}, y: {:.02}",
screen_pos.x, screen_pos.y screen_pos.x, screen_pos.y
)); ));
ui.label(format!("plot hovered: {}", hovered)); ui.label(format!("plot hovered: {hovered}"));
let coordinate_text = if let Some(coordinate) = pointer_coordinate { let coordinate_text = if let Some(coordinate) = pointer_coordinate {
format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y) format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y)
} else { } else {
"None".to_owned() "None".to_owned()
}; };
ui.label(format!("pointer coordinate: {}", coordinate_text)); ui.label(format!("pointer coordinate: {coordinate_text}"));
let coordinate_text = format!( let coordinate_text = format!(
"x: {:.02}, y: {:.02}", "x: {:.02}, y: {:.02}",
pointer_coordinate_drag_delta.x, pointer_coordinate_drag_delta.y pointer_coordinate_drag_delta.x, pointer_coordinate_drag_delta.y
); );
ui.label(format!( ui.label(format!("pointer coordinate drag delta: {coordinate_text}"));
"pointer coordinate drag delta: {}",
coordinate_text
));
response response
} }
@@ -892,6 +909,7 @@ impl ChartsDemo {
Plot::new("Normal Distribution Demo") Plot::new("Normal Distribution Demo")
.legend(Legend::default()) .legend(Legend::default())
.clamp_grid(true) .clamp_grid(true)
.y_axis_width(3)
.allow_zoom(self.allow_zoom) .allow_zoom(self.allow_zoom)
.allow_drag(self.allow_drag) .allow_drag(self.allow_drag)
.show(ui, |plot_ui| plot_ui.bar_chart(chart)) .show(ui, |plot_ui| plot_ui.bar_chart(chart))
@@ -1006,3 +1024,11 @@ impl ChartsDemo {
.response .response
} }
} }
fn is_approx_zero(val: f64) -> bool {
val.abs() < 1e-6
}
fn is_approx_integer(val: f64) -> bool {
val.fract().abs() < 1e-6
}

View File

@@ -232,10 +232,10 @@ impl super::View for ScrollTo {
for item in 1..=50 { for item in 1..=50 {
if track_item && item == self.track_item { if track_item && item == self.track_item {
let response = let response =
ui.colored_label(Color32::YELLOW, format!("This is item {}", item)); ui.colored_label(Color32::YELLOW, format!("This is item {item}"));
response.scroll_to_me(self.tack_item_align); response.scroll_to_me(self.tack_item_align);
} else { } else {
ui.label(format!("This is item {}", item)); ui.label(format!("This is item {item}"));
} }
} }
}); });
@@ -254,8 +254,7 @@ impl super::View for ScrollTo {
ui.separator(); ui.separator();
ui.label(format!( ui.label(format!(
"Scroll offset: {:.0}/{:.0} px", "Scroll offset: {current_scroll:.0}/{max_scroll:.0} px"
current_scroll, max_scroll
)); ));
ui.separator(); ui.separator();

View File

@@ -37,7 +37,7 @@ impl super::View for StripDemo {
.size(Size::exact(50.0)) .size(Size::exact(50.0))
.size(Size::remainder()) .size(Size::remainder())
.size(Size::relative(0.5).at_least(60.0)) .size(Size::relative(0.5).at_least(60.0))
.size(Size::exact(10.0)) .size(Size::exact(10.5))
.vertical(|mut strip| { .vertical(|mut strip| {
strip.cell(|ui| { strip.cell(|ui| {
ui.painter().rect_filled( ui.painter().rect_filled(

View File

@@ -38,7 +38,6 @@ impl super::Demo for TableDemo {
fn show(&mut self, ctx: &egui::Context, open: &mut bool) { fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name()) egui::Window::new(self.name())
.open(open) .open(open)
.resizable(true)
.default_width(400.0) .default_width(400.0)
.show(ctx, |ui| { .show(ctx, |ui| {
use super::View as _; use super::View as _;
@@ -102,7 +101,7 @@ impl super::View for TableDemo {
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
StripBuilder::new(ui) StripBuilder::new(ui)
.size(Size::remainder().at_least(100.0)) // for the table .size(Size::remainder().at_least(100.0)) // for the table
.size(Size::exact(10.0)) // for the source code link .size(Size::exact(10.5)) // for the source code link
.vertical(|mut strip| { .vertical(|mut strip| {
strip.cell(|ui| { strip.cell(|ui| {
egui::ScrollArea::horizontal().show(ui, |ui| { egui::ScrollArea::horizontal().show(ui, |ui| {

View File

@@ -20,7 +20,7 @@ impl super::View for CursorTest {
ui.heading("Hover to switch cursor icon:"); ui.heading("Hover to switch cursor icon:");
for &cursor_icon in &egui::CursorIcon::ALL { for &cursor_icon in &egui::CursorIcon::ALL {
let _ = ui let _ = ui
.button(format!("{:?}", cursor_icon)) .button(format!("{cursor_icon:?}"))
.on_hover_cursor(cursor_icon); .on_hover_cursor(cursor_icon);
} }
ui.add(crate::egui_github_link_file!()); ui.add(crate::egui_github_link_file!());
@@ -239,7 +239,7 @@ impl super::View for TableTest {
for row in 0..self.num_rows { for row in 0..self.num_rows {
for col in 0..self.num_cols { for col in 0..self.num_cols {
if col == 0 { if col == 0 {
ui.label(format!("row {}", row)); ui.label(format!("row {row}"));
} else { } else {
let word_idx = row * 3 + col * 5; let word_idx = row * 3 + col * 5;
let word_count = (row * 5 + col * 75) % 13; let word_count = (row * 5 + col * 75) % 13;
@@ -350,13 +350,13 @@ impl super::View for InputTest {
use std::fmt::Write as _; use std::fmt::Write as _;
if response.clicked_by(button) { if response.clicked_by(button) {
writeln!(new_info, "Clicked by {:?} button", button).ok(); writeln!(new_info, "Clicked by {button:?} button").ok();
} }
if response.double_clicked_by(button) { if response.double_clicked_by(button) {
writeln!(new_info, "Double-clicked by {:?} button", button).ok(); writeln!(new_info, "Double-clicked by {button:?} button").ok();
} }
if response.triple_clicked_by(button) { if response.triple_clicked_by(button) {
writeln!(new_info, "Triple-clicked by {:?} button", button).ok(); writeln!(new_info, "Triple-clicked by {button:?} button").ok();
} }
if response.dragged_by(button) { if response.dragged_by(button) {
writeln!( writeln!(

View File

@@ -125,7 +125,7 @@ impl WidgetGallery {
ui.add(doc_link_label("Hyperlink", "Hyperlink")); ui.add(doc_link_label("Hyperlink", "Hyperlink"));
use egui::special_emojis::GITHUB; use egui::special_emojis::GITHUB;
ui.hyperlink_to( ui.hyperlink_to(
format!("{} egui on GitHub", GITHUB), format!("{GITHUB} egui on GitHub"),
"https://github.com/emilk/egui", "https://github.com/emilk/egui",
); );
ui.end_row(); ui.end_row();
@@ -172,7 +172,7 @@ impl WidgetGallery {
ui.add(doc_link_label("ComboBox", "ComboBox")); ui.add(doc_link_label("ComboBox", "ComboBox"));
egui::ComboBox::from_label("Take your pick") egui::ComboBox::from_label("Take your pick")
.selected_text(format!("{:?}", radio)) .selected_text(format!("{radio:?}"))
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
ui.style_mut().wrap = Some(false); ui.style_mut().wrap = Some(false);
ui.set_min_width(60.0); ui.set_min_width(60.0);
@@ -270,14 +270,15 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response {
let line = Line::new(line_points); let line = Line::new(line_points);
egui::plot::Plot::new("example_plot") egui::plot::Plot::new("example_plot")
.height(32.0) .height(32.0)
.show_axes(false)
.data_aspect(1.0) .data_aspect(1.0)
.show(ui, |plot_ui| plot_ui.line(line)) .show(ui, |plot_ui| plot_ui.line(line))
.response .response
} }
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a { fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
let label = format!("{}:", title); let label = format!("{title}:");
let url = format!("https://docs.rs/egui?search={}", search_term); let url = format!("https://docs.rs/egui?search={search_term}");
move |ui: &mut egui::Ui| { move |ui: &mut egui::Ui| {
ui.hyperlink_to(label, url).on_hover_ui(|ui| { ui.hyperlink_to(label, url).on_hover_ui(|ui| {
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {

View File

@@ -161,7 +161,7 @@ fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
let font_id = TextStyle::Body.resolve(ui.style()); let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id)); let row_height = ui.fonts(|f| f.row_height(&font_id));
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
let text = format!("{}.", number); let text = format!("{number}.");
let text_color = ui.visuals().strong_text_color(); let text_color = ui.visuals().strong_text_color();
ui.painter().text( ui.painter().text(
rect.right_center(), rect.right_center(),

View File

@@ -1,8 +1,8 @@
# Changelog for egui_extras # Changelog for egui_extras
All notable changes to the `egui_extras` integration will be noted in this file. All notable changes to the `egui_extras` integration will be noted in this file.
This file is updated upon each release.
## Unreleased Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## 0.22.0 - 2023-05-23 ## 0.22.0 - 2023-05-23

View File

@@ -8,7 +8,7 @@ authors = [
] ]
description = "Extra functionality and widgets for the egui GUI library" description = "Extra functionality and widgets for the egui GUI library"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui" homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"

View File

@@ -428,6 +428,6 @@ fn month_name(i: u32) -> &'static str {
10 => "October", 10 => "October",
11 => "November", 11 => "November",
12 => "December", 12 => "December",
_ => panic!("Unknown month: {}", i), _ => panic!("Unknown month: {i}"),
} }
} }

View File

@@ -10,11 +10,15 @@ pub use usvg::FitTo;
/// Use the `svg` and `image` features to enable more constructors. /// Use the `svg` and `image` features to enable more constructors.
pub struct RetainedImage { pub struct RetainedImage {
debug_name: String, debug_name: String,
size: [usize; 2], size: [usize; 2],
/// Cleared once [`Self::texture`] has been loaded. /// Cleared once [`Self::texture`] has been loaded.
image: Mutex<egui::ColorImage>, image: Mutex<egui::ColorImage>,
/// Lazily loaded when we have an egui context. /// Lazily loaded when we have an egui context.
texture: Mutex<Option<egui::TextureHandle>>, texture: Mutex<Option<egui::TextureHandle>>,
options: TextureOptions, options: TextureOptions,
} }
@@ -254,7 +258,7 @@ pub fn load_svg_bytes_with_size(
}; };
let mut pixmap = tiny_skia::Pixmap::new(w, h) let mut pixmap = tiny_skia::Pixmap::new(w, h)
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?; .ok_or_else(|| format!("Failed to create SVG Pixmap of size {w}x{h}"))?;
resvg::render(&rtree, fit_to, Default::default(), pixmap.as_mut()) resvg::render(&rtree, fit_to, Default::default(), pixmap.as_mut())
.ok_or_else(|| "Failed to render SVG".to_owned())?; .ok_or_else(|| "Failed to render SVG".to_owned())?;

View File

@@ -32,9 +32,11 @@ pub struct StripLayout<'l> {
direction: CellDirection, direction: CellDirection,
pub(crate) rect: Rect, pub(crate) rect: Rect,
pub(crate) cursor: Pos2, pub(crate) cursor: Pos2,
/// Keeps track of the max used position, /// Keeps track of the max used position,
/// so we know how much space we used. /// so we know how much space we used.
max: Pos2, max: Pos2,
cell_layout: egui::Layout, cell_layout: egui::Layout,
} }

View File

@@ -1,14 +1,16 @@
use egui::Rangef;
/// Size hint for table column/strip cell. /// Size hint for table column/strip cell.
#[derive(Clone, Debug, Copy)] #[derive(Clone, Debug, Copy)]
pub enum Size { pub enum Size {
/// Absolute size in points, with a given range of allowed sizes to resize within. /// Absolute size in points, with a given range of allowed sizes to resize within.
Absolute { initial: f32, range: (f32, f32) }, Absolute { initial: f32, range: Rangef },
/// Relative size relative to all available space. /// Relative size relative to all available space.
Relative { fraction: f32, range: (f32, f32) }, Relative { fraction: f32, range: Rangef },
/// Multiple remainders each get the same space. /// Multiple remainders each get the same space.
Remainder { range: (f32, f32) }, Remainder { range: Rangef },
} }
impl Size { impl Size {
@@ -16,7 +18,7 @@ impl Size {
pub fn exact(points: f32) -> Self { pub fn exact(points: f32) -> Self {
Self::Absolute { Self::Absolute {
initial: points, initial: points,
range: (points, points), range: Rangef::new(points, points),
} }
} }
@@ -24,7 +26,7 @@ impl Size {
pub fn initial(points: f32) -> Self { pub fn initial(points: f32) -> Self {
Self::Absolute { Self::Absolute {
initial: points, initial: points,
range: (0.0, f32::INFINITY), range: Rangef::new(0.0, f32::INFINITY),
} }
} }
@@ -33,14 +35,14 @@ impl Size {
egui::egui_assert!(0.0 <= fraction && fraction <= 1.0); egui::egui_assert!(0.0 <= fraction && fraction <= 1.0);
Self::Relative { Self::Relative {
fraction, fraction,
range: (0.0, f32::INFINITY), range: Rangef::new(0.0, f32::INFINITY),
} }
} }
/// Multiple remainders each get the same space. /// Multiple remainders each get the same space.
pub fn remainder() -> Self { pub fn remainder() -> Self {
Self::Remainder { Self::Remainder {
range: (0.0, f32::INFINITY), range: Rangef::new(0.0, f32::INFINITY),
} }
} }
@@ -50,7 +52,7 @@ impl Size {
Self::Absolute { range, .. } Self::Absolute { range, .. }
| Self::Relative { range, .. } | Self::Relative { range, .. }
| Self::Remainder { range, .. } => { | Self::Remainder { range, .. } => {
range.0 = minimum; range.min = minimum;
} }
} }
self self
@@ -62,14 +64,14 @@ impl Size {
Self::Absolute { range, .. } Self::Absolute { range, .. }
| Self::Relative { range, .. } | Self::Relative { range, .. }
| Self::Remainder { range, .. } => { | Self::Remainder { range, .. } => {
range.1 = maximum; range.max = maximum;
} }
} }
self self
} }
/// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table). /// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table).
pub fn range(self) -> (f32, f32) { pub fn range(self) -> Rangef {
match self { match self {
Self::Absolute { range, .. } Self::Absolute { range, .. }
| Self::Relative { range, .. } | Self::Relative { range, .. }
@@ -99,12 +101,9 @@ impl Sizing {
.iter() .iter()
.map(|&size| match size { .map(|&size| match size {
Size::Absolute { initial, .. } => initial, Size::Absolute { initial, .. } => initial,
Size::Relative { Size::Relative { fraction, range } => {
fraction,
range: (min, max),
} => {
assert!(0.0 <= fraction && fraction <= 1.0); assert!(0.0 <= fraction && fraction <= 1.0);
(length * fraction).clamp(min, max) range.clamp(length * fraction)
} }
Size::Remainder { .. } => { Size::Remainder { .. } => {
remainders += 1; remainders += 1;
@@ -120,9 +119,9 @@ impl Sizing {
let mut remainder_length = length - sum_non_remainder; let mut remainder_length = length - sum_non_remainder;
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor(); let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
self.sizes.iter().for_each(|&size| { self.sizes.iter().for_each(|&size| {
if let Size::Remainder { range: (min, _max) } = size { if let Size::Remainder { range } = size {
if avg_remainder_length < min { if avg_remainder_length < range.min {
remainder_length -= min; remainder_length -= range.min;
remainders -= 1; remainders -= 1;
} }
} }
@@ -138,11 +137,8 @@ impl Sizing {
.iter() .iter()
.map(|&size| match size { .map(|&size| match size {
Size::Absolute { initial, .. } => initial, Size::Absolute { initial, .. } => initial,
Size::Relative { Size::Relative { fraction, range } => range.clamp(length * fraction),
fraction, Size::Remainder { range } => range.clamp(avg_remainder_length),
range: (min, max),
} => (length * fraction).clamp(min, max),
Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max),
}) })
.collect() .collect()
} }

View File

@@ -72,13 +72,13 @@ impl<'a> StripBuilder<'a> {
self self
} }
/// Allocate space for for one column/row. /// Allocate space for one column/row.
pub fn size(mut self, size: Size) -> Self { pub fn size(mut self, size: Size) -> Self {
self.sizing.add(size); self.sizing.add(size);
self self
} }
/// Allocate space for for several columns/rows at once. /// Allocate space for several columns/rows at once.
pub fn sizes(mut self, size: Size, count: usize) -> Self { pub fn sizes(mut self, size: Size, count: usize) -> Self {
for _ in 0..count { for _ in 0..count {
self.sizing.add(size); self.sizing.add(size);

View File

@@ -3,7 +3,7 @@
//! | fixed size | all available space/minimum | 30% of available width | fixed size | //! | fixed size | all available space/minimum | 30% of available width | fixed size |
//! Takes all available height, so if you want something below the table, put it in a strip. //! Takes all available height, so if you want something below the table, put it in a strip.
use egui::{Align, NumExt as _, Rect, Response, ScrollArea, Ui, Vec2}; use egui::{Align, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2};
use crate::{ use crate::{
layout::{CellDirection, CellSize}, layout::{CellDirection, CellSize},
@@ -28,7 +28,9 @@ enum InitialColumnSize {
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Column { pub struct Column {
initial_width: InitialColumnSize, initial_width: InitialColumnSize,
width_range: (f32, f32),
width_range: Rangef,
/// Clip contents if too narrow? /// Clip contents if too narrow?
clip: bool, clip: bool,
@@ -78,7 +80,7 @@ impl Column {
fn new(initial_width: InitialColumnSize) -> Self { fn new(initial_width: InitialColumnSize) -> Self {
Self { Self {
initial_width, initial_width,
width_range: (0.0, f32::INFINITY), width_range: Rangef::new(0.0, f32::INFINITY),
resizable: None, resizable: None,
clip: false, clip: false,
} }
@@ -110,7 +112,7 @@ impl Column {
/// ///
/// Default: 0.0 /// Default: 0.0
pub fn at_least(mut self, minimum: f32) -> Self { pub fn at_least(mut self, minimum: f32) -> Self {
self.width_range.0 = minimum; self.width_range.min = minimum;
self self
} }
@@ -118,13 +120,13 @@ impl Column {
/// ///
/// Default: [`f32::INFINITY`] /// Default: [`f32::INFINITY`]
pub fn at_most(mut self, maximum: f32) -> Self { pub fn at_most(mut self, maximum: f32) -> Self {
self.width_range.1 = maximum; self.width_range.max = maximum;
self self
} }
/// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table). /// Allowed range of movement (in points), if in a resizable [`Table`](crate::table::Table).
pub fn range(mut self, range: std::ops::RangeInclusive<f32>) -> Self { pub fn range(mut self, range: impl Into<Rangef>) -> Self {
self.width_range = (*range.start(), *range.end()); self.width_range = range.into();
self self
} }
@@ -146,8 +148,8 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width), InitialColumnSize::Automatic(suggested_width) => Size::initial(suggested_width),
InitialColumnSize::Remainder => Size::remainder(), InitialColumnSize::Remainder => Size::remainder(),
} }
.at_least(column.width_range.0) .at_least(column.width_range.min)
.at_most(column.width_range.1); .at_most(column.width_range.max);
sizing.add(size); sizing.add(size);
} }
sizing sizing
@@ -157,6 +159,7 @@ fn to_sizing(columns: &[Column]) -> crate::sizing::Sizing {
struct TableScrollOptions { struct TableScrollOptions {
vscroll: bool, vscroll: bool,
drag_to_scroll: bool,
stick_to_bottom: bool, stick_to_bottom: bool,
scroll_to_row: Option<(usize, Option<Align>)>, scroll_to_row: Option<(usize, Option<Align>)>,
scroll_offset_y: Option<f32>, scroll_offset_y: Option<f32>,
@@ -169,6 +172,7 @@ impl Default for TableScrollOptions {
fn default() -> Self { fn default() -> Self {
Self { Self {
vscroll: true, vscroll: true,
drag_to_scroll: true,
stick_to_bottom: false, stick_to_bottom: false,
scroll_to_row: None, scroll_to_row: None,
scroll_offset_y: None, scroll_offset_y: None,
@@ -271,6 +275,14 @@ impl<'a> TableBuilder<'a> {
self.vscroll(vscroll) self.vscroll(vscroll)
} }
/// Enables scrolling the table's contents using mouse drag (default: `true`).
///
/// See [`ScrollArea::drag_to_scroll`] for more.
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll_options.drag_to_scroll = drag_to_scroll;
self
}
/// Should the scroll handle stick to the bottom position even as the content size changes /// Should the scroll handle stick to the bottom position even as the content size changes
/// dynamically? The scroll handle remains stuck until manually changed, and will become stuck /// dynamically? The scroll handle remains stuck until manually changed, and will become stuck
/// once again when repositioned to the bottom. Default: `false`. /// once again when repositioned to the bottom. Default: `false`.
@@ -511,8 +523,10 @@ pub struct Table<'a> {
columns: Vec<Column>, columns: Vec<Column>,
available_width: f32, available_width: f32,
state: TableState, state: TableState,
/// Accumulated maximum used widths for each column. /// Accumulated maximum used widths for each column.
max_used_widths: Vec<f32>, max_used_widths: Vec<f32>,
first_frame_auto_size_columns: bool, first_frame_auto_size_columns: bool,
resizable: bool, resizable: bool,
striped: bool, striped: bool,
@@ -551,6 +565,7 @@ impl<'a> Table<'a> {
let TableScrollOptions { let TableScrollOptions {
vscroll, vscroll,
drag_to_scroll,
stick_to_bottom, stick_to_bottom,
scroll_to_row, scroll_to_row,
scroll_offset_y, scroll_offset_y,
@@ -563,6 +578,7 @@ impl<'a> Table<'a> {
let mut scroll_area = ScrollArea::new([false, vscroll]) let mut scroll_area = ScrollArea::new([false, vscroll])
.auto_shrink([true; 2]) .auto_shrink([true; 2])
.drag_to_scroll(drag_to_scroll)
.stick_to_bottom(stick_to_bottom) .stick_to_bottom(stick_to_bottom)
.min_scrolled_height(min_scrolled_height) .min_scrolled_height(min_scrolled_height)
.max_height(max_scroll_height) .max_height(max_scroll_height)
@@ -598,13 +614,13 @@ impl<'a> Table<'a> {
if scroll_to_row.is_some() && scroll_to_y_range.is_none() { if scroll_to_row.is_some() && scroll_to_y_range.is_none() {
// TableBody::row didn't find the right row, so scroll to the bottom: // TableBody::row didn't find the right row, so scroll to the bottom:
scroll_to_y_range = Some((f32::INFINITY, f32::INFINITY)); scroll_to_y_range = Some(Rangef::new(f32::INFINITY, f32::INFINITY));
} }
}); });
if let Some((min_y, max_y)) = scroll_to_y_range { if let Some(y_range) = scroll_to_y_range {
let x = 0.0; // ignored, we only have vertical scrolling let x = 0.0; // ignored, we only have vertical scrolling
let rect = egui::Rect::from_min_max(egui::pos2(x, min_y), egui::pos2(x, max_y)); let rect = egui::Rect::from_x_y_ranges(x..=x, y_range);
let align = scroll_to_row.and_then(|(_, a)| a); let align = scroll_to_row.and_then(|(_, a)| a);
ui.scroll_to_rect(rect, align); ui.scroll_to_rect(rect, align);
} }
@@ -617,14 +633,14 @@ impl<'a> Table<'a> {
for (i, column_width) in state.column_widths.iter_mut().enumerate() { for (i, column_width) in state.column_widths.iter_mut().enumerate() {
let column = &columns[i]; let column = &columns[i];
let column_is_resizable = column.resizable.unwrap_or(resizable); let column_is_resizable = column.resizable.unwrap_or(resizable);
let (min_width, max_width) = column.width_range; let width_range = column.width_range;
if !column.clip { if !column.clip {
// Unless we clip we don't want to shrink below the // Unless we clip we don't want to shrink below the
// size that was actually used: // size that was actually used:
*column_width = column_width.at_least(max_used_widths[i]); *column_width = column_width.at_least(max_used_widths[i]);
} }
*column_width = column_width.clamp(min_width, max_width); *column_width = width_range.clamp(*column_width);
let is_last_column = i + 1 == columns.len(); let is_last_column = i + 1 == columns.len();
@@ -633,7 +649,7 @@ impl<'a> Table<'a> {
let eps = 0.1; // just to avoid some rounding errors. let eps = 0.1; // just to avoid some rounding errors.
*column_width = available_width - eps; *column_width = available_width - eps;
*column_width = column_width.at_least(max_used_widths[i]); *column_width = column_width.at_least(max_used_widths[i]);
*column_width = column_width.clamp(min_width, max_width); *column_width = width_range.clamp(*column_width);
break; break;
} }
@@ -641,7 +657,7 @@ impl<'a> Table<'a> {
if column.is_auto() && (first_frame_auto_size_columns || !column_is_resizable) { if column.is_auto() && (first_frame_auto_size_columns || !column_is_resizable) {
*column_width = max_used_widths[i]; *column_width = max_used_widths[i];
*column_width = column_width.clamp(min_width, max_width); *column_width = width_range.clamp(*column_width);
} else if column_is_resizable { } else if column_is_resizable {
let column_resize_id = ui.id().with("resize_column").with(i); let column_resize_id = ui.id().with("resize_column").with(i);
@@ -656,7 +672,7 @@ impl<'a> Table<'a> {
if resize_response.double_clicked() { if resize_response.double_clicked() {
// Resize to the minimum of what is needed. // Resize to the minimum of what is needed.
*column_width = max_used_widths[i].clamp(min_width, max_width); *column_width = width_range.clamp(max_used_widths[i]);
} else if resize_response.dragged() { } else if resize_response.dragged() {
if let Some(pointer) = ui.ctx().pointer_latest_pos() { if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let mut new_width = *column_width + pointer.x - x; let mut new_width = *column_width + pointer.x - x;
@@ -671,7 +687,7 @@ impl<'a> Table<'a> {
new_width = new_width =
new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame); new_width.at_least(max_used_widths[i] - max_shrinkage_per_frame);
} }
new_width = new_width.clamp(min_width, max_width); new_width = width_range.clamp(new_width);
let x = x - *column_width + new_width; let x = x - *column_width + new_width;
(p0.x, p1.x) = (x, x); (p0.x, p1.x) = (x, x);
@@ -731,7 +747,7 @@ pub struct TableBody<'a> {
/// If we find the correct row to scroll to, /// If we find the correct row to scroll to,
/// this is set to the y-range of the row. /// this is set to the y-range of the row.
scroll_to_y_range: &'a mut Option<(f32, f32)>, scroll_to_y_range: &'a mut Option<Rangef>,
} }
impl<'a> TableBody<'a> { impl<'a> TableBody<'a> {
@@ -779,7 +795,7 @@ impl<'a> TableBody<'a> {
let bottom_y = self.layout.cursor.y; let bottom_y = self.layout.cursor.y;
if Some(self.row_nr) == self.scroll_to_row { if Some(self.row_nr) == self.scroll_to_row {
*self.scroll_to_y_range = Some((top_y, bottom_y)); *self.scroll_to_y_range = Some(Rangef::new(top_y, bottom_y));
} }
self.row_nr += 1; self.row_nr += 1;
@@ -819,7 +835,7 @@ impl<'a> TableBody<'a> {
if let Some(scroll_to_row) = self.scroll_to_row { if let Some(scroll_to_row) = self.scroll_to_row {
let scroll_to_row = scroll_to_row.at_most(total_rows.saturating_sub(1)) as f32; let scroll_to_row = scroll_to_row.at_most(total_rows.saturating_sub(1)) as f32;
*self.scroll_to_y_range = Some(( *self.scroll_to_y_range = Some(Rangef::new(
self.layout.cursor.y + scroll_to_row * row_height_with_spacing, self.layout.cursor.y + scroll_to_row * row_height_with_spacing,
self.layout.cursor.y + (scroll_to_row + 1.0) * row_height_with_spacing, self.layout.cursor.y + (scroll_to_row + 1.0) * row_height_with_spacing,
)); ));
@@ -909,7 +925,7 @@ impl<'a> TableBody<'a> {
cursor_y += (row_height + spacing.y) as f64; cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row { if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some(( *self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + old_cursor_y) as f32, (scroll_to_y_range_offset + old_cursor_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32, (scroll_to_y_range_offset + cursor_y) as f32,
)); ));
@@ -953,7 +969,7 @@ impl<'a> TableBody<'a> {
cursor_y += (row_height + spacing.y) as f64; cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row { if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some(( *self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + top_y) as f32, (scroll_to_y_range_offset + top_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32, (scroll_to_y_range_offset + cursor_y) as f32,
)); ));
@@ -972,7 +988,7 @@ impl<'a> TableBody<'a> {
let top_y = cursor_y; let top_y = cursor_y;
cursor_y += (row_height + spacing.y) as f64; cursor_y += (row_height + spacing.y) as f64;
if Some(row_index) == self.scroll_to_row { if Some(row_index) == self.scroll_to_row {
*self.scroll_to_y_range = Some(( *self.scroll_to_y_range = Some(Rangef::new(
(scroll_to_y_range_offset + top_y) as f32, (scroll_to_y_range_offset + top_y) as f32,
(scroll_to_y_range_offset + cursor_y) as f32, (scroll_to_y_range_offset + cursor_y) as f32,
)); ));
@@ -981,10 +997,8 @@ impl<'a> TableBody<'a> {
if self.scroll_to_row.is_some() && self.scroll_to_y_range.is_none() { if self.scroll_to_row.is_some() && self.scroll_to_y_range.is_none() {
// Catch desire to scroll past the end: // Catch desire to scroll past the end:
*self.scroll_to_y_range = Some(( *self.scroll_to_y_range =
(scroll_to_y_range_offset + cursor_y) as f32, Some(Rangef::point((scroll_to_y_range_offset + cursor_y) as f32));
(scroll_to_y_range_offset + cursor_y) as f32,
));
} }
if height_below_visible > 0.0 { if height_below_visible > 0.0 {
@@ -1013,8 +1027,10 @@ pub struct TableRow<'a, 'b> {
layout: &'b mut StripLayout<'a>, layout: &'b mut StripLayout<'a>,
columns: &'b [Column], columns: &'b [Column],
widths: &'b [f32], widths: &'b [f32],
/// grows during building with the maximum widths /// grows during building with the maximum widths
max_used_widths: &'b mut [f32], max_used_widths: &'b mut [f32],
col_index: usize, col_index: usize,
striped: bool, striped: bool,
height: f32, height: f32,

View File

@@ -1,6 +1,9 @@
# Changelog for egui_glium # Changelog for egui_glium
All notable changes to the `egui_glium` integration will be noted in this file. All notable changes to the `egui_glium` integration will be noted in this file.
This file is updated upon each release.
Changes since the last release can be found by running the `scripts/generate_changelog.py` script.
## Unreleased ## Unreleased
* Remove the `screen_reader` feature ([#2669](https://github.com/emilk/egui/pull/2669)). * Remove the `screen_reader` feature ([#2669](https://github.com/emilk/egui/pull/2669)).

View File

@@ -4,7 +4,7 @@ version = "0.22.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"] authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui natively using the glium library" description = "Bindings for using egui natively using the glium library"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.67"
homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glium" homepage = "https://github.com/emilk/egui/tree/master/crates/egui_glium"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
readme = "README.md" readme = "README.md"

View File

@@ -78,8 +78,8 @@ fn main() {
// Platform-dependent event handlers to workaround a winit bug // Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987 // See: https://github.com/rust-windowing/winit/issues/987
// See: https://github.com/rust-windowing/winit/issues/1619 // See: https://github.com/rust-windowing/winit/issues/1619
glutin::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(), glutin::event::Event::RedrawEventsCleared if cfg!(target_os = "windows") => redraw(),
glutin::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(), glutin::event::Event::RedrawRequested(_) if !cfg!(target_os = "windows") => redraw(),
glutin::event::Event::WindowEvent { event, .. } => { glutin::event::Event::WindowEvent { event, .. } => {
use glutin::event::WindowEvent; use glutin::event::WindowEvent;

Some files were not shown because too many files have changed in this diff Show More