mirror of
https://github.com/tauri-apps/tauri-egui.git
synced 2026-02-04 02:41:19 +01:00
Merge pull request #1 from tauri-apps/feat/egui-tao-0.20
This commit is contained in:
75
.changes/config.json
Executable file
75
.changes/config.json
Executable file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"gitSiteUrl": "https://www.github.com/tauri-apps/tauri-egui/",
|
||||
"pkgManagers": {
|
||||
"rust": {
|
||||
"version": true,
|
||||
"getPublishedVersion": "cargo search ${ pkgFile.pkg.package.name } --limit 1 | sed -nE \"s/^[^\\\"]*\\\"//; s/\\\".*//1p\"",
|
||||
"prepublish": [
|
||||
"sudo apt-get update",
|
||||
"sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libayatana-appindicator3-dev librsvg2-dev patchelf",
|
||||
"cargo install cargo-audit --features=fix",
|
||||
{
|
||||
"command": "cargo generate-lockfile",
|
||||
"dryRunCommand": true,
|
||||
"runFromRoot": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "echo '<details>\n<summary><em><h4>Cargo Audit</h4></em></summary>\n\n```'",
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }",
|
||||
"dryRunCommand": true,
|
||||
"runFromRoot": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "echo '```\n\n</details>\n'",
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
}
|
||||
],
|
||||
"publish": [
|
||||
"sleep 15s",
|
||||
{
|
||||
"command": "cargo package --no-verify",
|
||||
"dryRunCommand": true
|
||||
},
|
||||
{
|
||||
"command": "echo '<details>\n<summary><em><h4>Cargo Publish</h4></em></summary>\n\n```'",
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo publish",
|
||||
"dryRunCommand": "cargo publish --dry-run",
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "echo '```\n\n</details>\n'",
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
}
|
||||
],
|
||||
"postpublish": [
|
||||
"git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f",
|
||||
"git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f",
|
||||
"git push --tags -f"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"path": "${ pkg.path }/target/package/${ pkg.pkg }-${ pkgFile.version }.crate",
|
||||
"name": "${ pkg.pkg }-${ pkgFile.version }.crate"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"tauri-egui": {
|
||||
"path": ".",
|
||||
"manager": "rust"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
.changes/initial-release.md
Normal file
5
.changes/initial-release.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-egui": minor
|
||||
---
|
||||
|
||||
Initial release.
|
||||
16
.changes/readme.md
Executable file
16
.changes/readme.md
Executable file
@@ -0,0 +1,16 @@
|
||||
# Changes
|
||||
##### via https://github.com/jbolda/covector
|
||||
|
||||
As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version *number*, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend it represents the overall change for our sanity.
|
||||
|
||||
When you select the version bump required, you do *not* need to consider depedencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process.
|
||||
|
||||
Use the following format:
|
||||
```md
|
||||
---
|
||||
"tauri-log-plugin": minor
|
||||
---
|
||||
|
||||
Change summary goes here
|
||||
|
||||
```
|
||||
26
.github/workflows/audit.yml
vendored
Normal file
26
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**/Cargo.lock"
|
||||
- "**/Cargo.toml"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**/Cargo.lock"
|
||||
- "**/Cargo.toml"
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
15
.github/workflows/covector-status.yml
vendored
Executable file
15
.github/workflows/covector-status.yml
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
name: covector status
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
covector:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: covector status
|
||||
uses: jbolda/covector/packages/action@covector-v0
|
||||
with:
|
||||
command: 'status'
|
||||
37
.github/workflows/covector-version-or-publish.yml
vendored
Executable file
37
.github/workflows/covector-version-or-publish.yml
vendored
Executable file
@@ -0,0 +1,37 @@
|
||||
name: covector version or publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
covector:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: git config
|
||||
run: |
|
||||
git config --global user.name "${{ github.event.pusher.name }}"
|
||||
git config --global user.email "${{ github.event.pusher.email }}"
|
||||
- name: covector version-or-publish
|
||||
uses: jbolda/covector/packages/action@covector-v0
|
||||
id: covector
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
command: 'version-or-publish'
|
||||
createRelease: true
|
||||
- name: create pull request
|
||||
id: cpr
|
||||
uses: tauri-apps/create-pull-request@v2.8.0
|
||||
with:
|
||||
title: "Publish New Versions"
|
||||
labels: "version updates"
|
||||
branch: "release"
|
||||
body: ${{ steps.covector.outputs.change }}
|
||||
30
.github/workflows/format.yml
vendored
Normal file
30
.github/workflows/format.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install rustfmt with nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --manifest-path=Cargo.toml --all -- --check
|
||||
35
.github/workflows/lint.yml
vendored
Normal file
35
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Clippy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install webkit2gtk
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
- name: Install clippy with stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: clippy
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --manifest-path=Cargo.toml --all-targets --all-features -- -D warnings
|
||||
name: clippy
|
||||
77
.github/workflows/test.yml
vendored
Executable file
77
.github/workflows/test.yml
vendored
Executable file
@@ -0,0 +1,77 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths-ignore:
|
||||
- "examples/**"
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: install Linux dependencies
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Get current date
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest'
|
||||
run: echo "CURRENT_DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
|
||||
|
||||
- name: Get current date
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: echo "CURRENT_DATE=$(Get-Date -Format "yyyy-MM-dd")" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
# Add date to the cache to keep it up to date
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}-${{ env.CURRENT_DATE }}
|
||||
# Restore from outdated cache for speed
|
||||
restore-keys: |
|
||||
${{ matrix.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
# Add date to the cache to keep it up to date
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}-${{ env.CURRENT_DATE }}
|
||||
# Restore from outdated cache for speed
|
||||
restore-keys: |
|
||||
${{ matrix.os }}-${{ matrix.rust }}-cargo-index-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Cache cargo target
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ matrix.project}}/target
|
||||
# Add date to the cache to keep it up to date
|
||||
key: ${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}-${{ env.CURRENT_DATE }}
|
||||
# Restore from outdated cache for speed
|
||||
restore-keys: |
|
||||
${{ matrix.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.toml') }}
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -1,24 +1,24 @@
|
||||
[package]
|
||||
name = "tauri-egui"
|
||||
version = "0.1.0"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
tauri = { version = "1.0", git = "https://github.com/tauri-apps/tauri" }
|
||||
tauri-runtime = { version = "0.10", git = "https://github.com/tauri-apps/tauri"}
|
||||
tauri-runtime-wry = { version = "0.10", git = "https://github.com/tauri-apps/tauri"}
|
||||
tauri = "1.1"
|
||||
tauri-runtime = "0.11"
|
||||
tauri-runtime-wry = "0.11"
|
||||
egui = "0.19"
|
||||
eframe = { package = "eframe_tao", version = "0.19.0" }
|
||||
egui-winit = { package = "egui-tao", version = "0.19.0" }
|
||||
egui_glow = { package = "egui_glow_tao", version = "0.19.0" }
|
||||
glutin = { package = "glutin_tao", version = "0.29.1" }
|
||||
eframe = { package = "eframe_tao", version = "0.20.0" }
|
||||
egui-winit = { package = "egui-tao", version = "0.20.0" }
|
||||
egui_glow = { package = "egui_glow_tao", version = "0.20.0" }
|
||||
glutin = { package = "glutin_tao", version = "0.30" }
|
||||
glow = "0.11"
|
||||
rand = "0.8"
|
||||
raw-window-handle = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tauri = { version = "1.0", git = "https://github.com/tauri-apps/tauri", features = ["dialog-message"] }
|
||||
tauri = { version = "1.1", features = ["dialog-message"] }
|
||||
egui_demo_app = { path = "examples/egui_demo_app" }
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
@@ -32,4 +32,4 @@ path = "examples/demo/main.rs"
|
||||
members = [
|
||||
"examples/egui_demo_app",
|
||||
"examples/egui_demo_lib",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
)]
|
||||
|
||||
use tauri::{RunEvent, State};
|
||||
use tauri_egui::{eframe, egui};
|
||||
use tauri_egui::eframe;
|
||||
|
||||
#[tauri::command]
|
||||
async fn open_native_window(
|
||||
@@ -24,9 +24,7 @@ async fn open_native_window(
|
||||
let _window = egui_handle
|
||||
.create_window(
|
||||
"native-window".to_string(),
|
||||
Box::new(|cc| {
|
||||
Box::new(egui_demo_app::WrapApp::new(cc))
|
||||
}),
|
||||
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
|
||||
"Login".into(),
|
||||
native_options,
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ wgpu = ["eframe/wgpu", "bytemuck", "pollster", "dep:wgpu"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["js-sys", "wasmbind"] }
|
||||
eframe = { package = "eframe_tao", version = "0.19.0", default-features = false }
|
||||
eframe = { package = "eframe_tao", version = "0.20.0", default-features = false }
|
||||
egui = { version = "0.19.0", features = ["extra_debug_asserts"] }
|
||||
egui_demo_lib = { version = "0.19.0", path = "../egui_demo_lib", features = ["chrono"] }
|
||||
|
||||
|
||||
@@ -5,96 +5,97 @@ use egui::mutex::Mutex;
|
||||
use egui_glow::glow;
|
||||
|
||||
pub struct Custom3d {
|
||||
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
|
||||
rotating_triangle: Arc<Mutex<RotatingTriangle>>,
|
||||
angle: f32,
|
||||
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
|
||||
rotating_triangle: Arc<Mutex<RotatingTriangle>>,
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
impl Custom3d {
|
||||
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
|
||||
Self {
|
||||
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(
|
||||
cc.gl.as_ref().expect("GL Enabled"),
|
||||
))),
|
||||
angle: 0.0,
|
||||
}
|
||||
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
|
||||
Self {
|
||||
rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(
|
||||
cc.gl.as_ref().expect("GL Enabled"),
|
||||
))),
|
||||
angle: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Custom3d {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
||||
ui.label(" (OpenGL).");
|
||||
});
|
||||
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
||||
ui.label(" (OpenGL).");
|
||||
});
|
||||
ui.label(
|
||||
"It's not a very impressive demo, but it shows you can embed 3D inside of egui.",
|
||||
);
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn on_exit(&mut self, gl: Option<&glow::Context>) {
|
||||
if let Some(gl) = gl {
|
||||
self.rotating_triangle.lock().destroy(gl);
|
||||
}
|
||||
fn on_exit(&mut self, gl: Option<&glow::Context>) {
|
||||
if let Some(gl) = gl {
|
||||
self.rotating_triangle.lock().destroy(gl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Custom3d {
|
||||
fn custom_painting(&mut self, ui: &mut egui::Ui) {
|
||||
let (rect, response) =
|
||||
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
|
||||
fn custom_painting(&mut self, ui: &mut egui::Ui) {
|
||||
let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
|
||||
|
||||
self.angle += response.drag_delta().x * 0.01;
|
||||
self.angle += response.drag_delta().x * 0.01;
|
||||
|
||||
// Clone locals so we can move them into the paint callback:
|
||||
let angle = self.angle;
|
||||
let rotating_triangle = self.rotating_triangle.clone();
|
||||
// Clone locals so we can move them into the paint callback:
|
||||
let angle = self.angle;
|
||||
let rotating_triangle = self.rotating_triangle.clone();
|
||||
|
||||
let cb = egui_glow::CallbackFn::new(move |_info, painter| {
|
||||
rotating_triangle.lock().paint(painter.gl(), angle);
|
||||
});
|
||||
let cb = egui_glow::CallbackFn::new(move |_info, painter| {
|
||||
rotating_triangle.lock().paint(painter.gl(), angle);
|
||||
});
|
||||
|
||||
let callback = egui::PaintCallback {
|
||||
rect,
|
||||
callback: Arc::new(cb),
|
||||
};
|
||||
ui.painter().add(callback);
|
||||
}
|
||||
let callback = egui::PaintCallback {
|
||||
rect,
|
||||
callback: Arc::new(cb),
|
||||
};
|
||||
ui.painter().add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
struct RotatingTriangle {
|
||||
program: glow::Program,
|
||||
vertex_array: glow::VertexArray,
|
||||
program: glow::Program,
|
||||
vertex_array: glow::VertexArray,
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)] // we need unsafe code to use glow
|
||||
impl RotatingTriangle {
|
||||
fn new(gl: &glow::Context) -> Self {
|
||||
use glow::HasContext as _;
|
||||
fn new(gl: &glow::Context) -> Self {
|
||||
use glow::HasContext as _;
|
||||
|
||||
let shader_version = if cfg!(target_arch = "wasm32") {
|
||||
"#version 300 es"
|
||||
} else {
|
||||
"#version 330"
|
||||
};
|
||||
let shader_version = if cfg!(target_arch = "wasm32") {
|
||||
"#version 300 es"
|
||||
} else {
|
||||
"#version 330"
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let program = gl.create_program().expect("Cannot create program");
|
||||
unsafe {
|
||||
let program = gl.create_program().expect("Cannot create program");
|
||||
|
||||
let (vertex_shader_source, fragment_shader_source) = (
|
||||
r#"
|
||||
let (vertex_shader_source, fragment_shader_source) = (
|
||||
r#"
|
||||
const vec2 verts[3] = vec2[3](
|
||||
vec2(0.0, 1.0),
|
||||
vec2(-1.0, -1.0),
|
||||
@@ -113,7 +114,7 @@ impl RotatingTriangle {
|
||||
gl_Position.x *= cos(u_angle);
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
r#"
|
||||
precision mediump float;
|
||||
in vec4 v_color;
|
||||
out vec4 out_color;
|
||||
@@ -121,68 +122,68 @@ impl RotatingTriangle {
|
||||
out_color = v_color;
|
||||
}
|
||||
"#,
|
||||
);
|
||||
);
|
||||
|
||||
let shader_sources = [
|
||||
(glow::VERTEX_SHADER, vertex_shader_source),
|
||||
(glow::FRAGMENT_SHADER, fragment_shader_source),
|
||||
];
|
||||
let shader_sources = [
|
||||
(glow::VERTEX_SHADER, vertex_shader_source),
|
||||
(glow::FRAGMENT_SHADER, fragment_shader_source),
|
||||
];
|
||||
|
||||
let shaders: Vec<_> = shader_sources
|
||||
.iter()
|
||||
.map(|(shader_type, shader_source)| {
|
||||
let shader = gl
|
||||
.create_shader(*shader_type)
|
||||
.expect("Cannot create shader");
|
||||
gl.shader_source(shader, &format!("{}\n{}", shader_version, shader_source));
|
||||
gl.compile_shader(shader);
|
||||
if !gl.get_shader_compile_status(shader) {
|
||||
panic!("{}", gl.get_shader_info_log(shader));
|
||||
}
|
||||
gl.attach_shader(program, shader);
|
||||
shader
|
||||
})
|
||||
.collect();
|
||||
let shaders: Vec<_> = shader_sources
|
||||
.iter()
|
||||
.map(|(shader_type, shader_source)| {
|
||||
let shader = gl
|
||||
.create_shader(*shader_type)
|
||||
.expect("Cannot create shader");
|
||||
gl.shader_source(shader, &format!("{}\n{}", shader_version, shader_source));
|
||||
gl.compile_shader(shader);
|
||||
if !gl.get_shader_compile_status(shader) {
|
||||
panic!("{}", gl.get_shader_info_log(shader));
|
||||
}
|
||||
gl.attach_shader(program, shader);
|
||||
shader
|
||||
})
|
||||
.collect();
|
||||
|
||||
gl.link_program(program);
|
||||
if !gl.get_program_link_status(program) {
|
||||
panic!("{}", gl.get_program_info_log(program));
|
||||
}
|
||||
gl.link_program(program);
|
||||
if !gl.get_program_link_status(program) {
|
||||
panic!("{}", gl.get_program_info_log(program));
|
||||
}
|
||||
|
||||
for shader in shaders {
|
||||
gl.detach_shader(program, shader);
|
||||
gl.delete_shader(shader);
|
||||
}
|
||||
for shader in shaders {
|
||||
gl.detach_shader(program, shader);
|
||||
gl.delete_shader(shader);
|
||||
}
|
||||
|
||||
let vertex_array = gl
|
||||
.create_vertex_array()
|
||||
.expect("Cannot create vertex array");
|
||||
let vertex_array = gl
|
||||
.create_vertex_array()
|
||||
.expect("Cannot create vertex array");
|
||||
|
||||
Self {
|
||||
program,
|
||||
vertex_array,
|
||||
}
|
||||
}
|
||||
Self {
|
||||
program,
|
||||
vertex_array,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy(&self, gl: &glow::Context) {
|
||||
use glow::HasContext as _;
|
||||
unsafe {
|
||||
gl.delete_program(self.program);
|
||||
gl.delete_vertex_array(self.vertex_array);
|
||||
}
|
||||
fn destroy(&self, gl: &glow::Context) {
|
||||
use glow::HasContext as _;
|
||||
unsafe {
|
||||
gl.delete_program(self.program);
|
||||
gl.delete_vertex_array(self.vertex_array);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, gl: &glow::Context, angle: f32) {
|
||||
use glow::HasContext as _;
|
||||
unsafe {
|
||||
gl.use_program(Some(self.program));
|
||||
gl.uniform_1_f32(
|
||||
gl.get_uniform_location(self.program, "u_angle").as_ref(),
|
||||
angle,
|
||||
);
|
||||
gl.bind_vertex_array(Some(self.vertex_array));
|
||||
gl.draw_arrays(glow::TRIANGLES, 0, 3);
|
||||
}
|
||||
fn paint(&self, gl: &glow::Context, angle: f32) {
|
||||
use glow::HasContext as _;
|
||||
unsafe {
|
||||
gl.use_program(Some(self.program));
|
||||
gl.uniform_1_f32(
|
||||
gl.get_uniform_location(self.program, "u_angle").as_ref(),
|
||||
angle,
|
||||
);
|
||||
gl.bind_vertex_array(Some(self.vertex_array));
|
||||
gl.draw_arrays(glow::TRIANGLES, 0, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,177 +1,178 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use eframe::{
|
||||
egui_wgpu::{self, wgpu},
|
||||
wgpu::util::DeviceExt,
|
||||
egui_wgpu::{self, wgpu},
|
||||
wgpu::util::DeviceExt,
|
||||
};
|
||||
|
||||
pub struct Custom3d {
|
||||
angle: f32,
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
impl Custom3d {
|
||||
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
|
||||
// Get the WGPU render state from the eframe creation context. This can also be retrieved
|
||||
// from `eframe::Frame` when you don't have a `CreationContext` available.
|
||||
let wgpu_render_state = cc.wgpu_render_state.as_ref().expect("WGPU enabled");
|
||||
pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self {
|
||||
// Get the WGPU render state from the eframe creation context. This can also be retrieved
|
||||
// from `eframe::Frame` when you don't have a `CreationContext` available.
|
||||
let wgpu_render_state = cc.wgpu_render_state.as_ref().expect("WGPU enabled");
|
||||
|
||||
let device = &wgpu_render_state.device;
|
||||
let device = &wgpu_render_state.device;
|
||||
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()),
|
||||
});
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: None,
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()),
|
||||
});
|
||||
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: None,
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: None,
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu_render_state.target_format.into())],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: None,
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: "vs_main",
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: "fs_main",
|
||||
targets: &[Some(wgpu_render_state.target_format.into())],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview: None,
|
||||
});
|
||||
|
||||
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&[0.0]),
|
||||
usage: wgpu::BufferUsages::COPY_DST
|
||||
| wgpu::BufferUsages::MAP_WRITE
|
||||
| wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: None,
|
||||
contents: bytemuck::cast_slice(&[0.0]),
|
||||
usage: wgpu::BufferUsages::COPY_DST
|
||||
| wgpu::BufferUsages::MAP_WRITE
|
||||
| wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: None,
|
||||
layout: &bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
}],
|
||||
});
|
||||
|
||||
// Because the graphics pipeline must have the same lifetime as the egui render pass,
|
||||
// instead of storing the pipeline in our `Custom3D` struct, we insert it into the
|
||||
// `paint_callback_resources` type map, which is stored alongside the render pass.
|
||||
wgpu_render_state
|
||||
.egui_rpass
|
||||
.write()
|
||||
.paint_callback_resources
|
||||
.insert(TriangleRenderResources {
|
||||
pipeline,
|
||||
bind_group,
|
||||
uniform_buffer,
|
||||
});
|
||||
// Because the graphics pipeline must have the same lifetime as the egui render pass,
|
||||
// instead of storing the pipeline in our `Custom3D` struct, we insert it into the
|
||||
// `paint_callback_resources` type map, which is stored alongside the render pass.
|
||||
wgpu_render_state
|
||||
.egui_rpass
|
||||
.write()
|
||||
.paint_callback_resources
|
||||
.insert(TriangleRenderResources {
|
||||
pipeline,
|
||||
bind_group,
|
||||
uniform_buffer,
|
||||
});
|
||||
|
||||
Self { angle: 0.0 }
|
||||
}
|
||||
Self { angle: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for Custom3d {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("WGPU", "https://wgpu.rs");
|
||||
ui.label(" (Portable Rust graphics API awesomeness)");
|
||||
});
|
||||
ui.label("It's not a very impressive demo, but it shows you can embed 3D inside of egui.");
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::ScrollArea::both()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("The triangle is being painted using ");
|
||||
ui.hyperlink_to("WGPU", "https://wgpu.rs");
|
||||
ui.label(" (Portable Rust graphics API awesomeness)");
|
||||
});
|
||||
ui.label(
|
||||
"It's not a very impressive demo, but it shows you can embed 3D inside of egui.",
|
||||
);
|
||||
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.custom_painting(ui);
|
||||
});
|
||||
ui.label("Drag to rotate!");
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Custom3d {
|
||||
fn custom_painting(&mut self, ui: &mut egui::Ui) {
|
||||
let (rect, response) =
|
||||
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
|
||||
fn custom_painting(&mut self, ui: &mut egui::Ui) {
|
||||
let (rect, response) = ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
|
||||
|
||||
self.angle += response.drag_delta().x * 0.01;
|
||||
self.angle += response.drag_delta().x * 0.01;
|
||||
|
||||
// Clone locals so we can move them into the paint callback:
|
||||
let angle = self.angle;
|
||||
// Clone locals so we can move them into the paint callback:
|
||||
let angle = self.angle;
|
||||
|
||||
// The callback function for WGPU is in two stages: prepare, and paint.
|
||||
//
|
||||
// The prepare callback is called every frame before paint and is given access to the wgpu
|
||||
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
|
||||
// rendering.
|
||||
//
|
||||
// The paint callback is called after prepare and is given access to the render pass, which
|
||||
// can be used to issue draw commands.
|
||||
let cb = egui_wgpu::CallbackFn::new()
|
||||
.prepare(move |device, queue, paint_callback_resources| {
|
||||
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
|
||||
resources.prepare(device, queue, angle);
|
||||
})
|
||||
.paint(move |_info, rpass, paint_callback_resources| {
|
||||
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
|
||||
resources.paint(rpass);
|
||||
});
|
||||
// The callback function for WGPU is in two stages: prepare, and paint.
|
||||
//
|
||||
// The prepare callback is called every frame before paint and is given access to the wgpu
|
||||
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
|
||||
// rendering.
|
||||
//
|
||||
// The paint callback is called after prepare and is given access to the render pass, which
|
||||
// can be used to issue draw commands.
|
||||
let cb = egui_wgpu::CallbackFn::new()
|
||||
.prepare(move |device, queue, paint_callback_resources| {
|
||||
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
|
||||
resources.prepare(device, queue, angle);
|
||||
})
|
||||
.paint(move |_info, rpass, paint_callback_resources| {
|
||||
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
|
||||
resources.paint(rpass);
|
||||
});
|
||||
|
||||
let callback = egui::PaintCallback {
|
||||
rect,
|
||||
callback: Arc::new(cb),
|
||||
};
|
||||
let callback = egui::PaintCallback {
|
||||
rect,
|
||||
callback: Arc::new(cb),
|
||||
};
|
||||
|
||||
ui.painter().add(callback);
|
||||
}
|
||||
ui.painter().add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
struct TriangleRenderResources {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
uniform_buffer: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl TriangleRenderResources {
|
||||
fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) {
|
||||
// Update our uniform buffer with the angle from the UI
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle]));
|
||||
}
|
||||
fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) {
|
||||
// Update our uniform buffer with the angle from the UI
|
||||
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle]));
|
||||
}
|
||||
|
||||
fn paint<'rpass>(&'rpass self, rpass: &mut wgpu::RenderPass<'rpass>) {
|
||||
// Draw our triangle!
|
||||
rpass.set_pipeline(&self.pipeline);
|
||||
rpass.set_bind_group(0, &self.bind_group, &[]);
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
fn paint<'rpass>(&'rpass self, rpass: &mut wgpu::RenderPass<'rpass>) {
|
||||
// Draw our triangle!
|
||||
rpass.set_pipeline(&self.pipeline);
|
||||
rpass.set_bind_group(0, &self.bind_group, &[]);
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,201 +5,201 @@ use std::f32::consts::TAU;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct FractalClock {
|
||||
paused: bool,
|
||||
time: f64,
|
||||
zoom: f32,
|
||||
start_line_width: f32,
|
||||
depth: usize,
|
||||
length_factor: f32,
|
||||
luminance_factor: f32,
|
||||
width_factor: f32,
|
||||
line_count: usize,
|
||||
paused: bool,
|
||||
time: f64,
|
||||
zoom: f32,
|
||||
start_line_width: f32,
|
||||
depth: usize,
|
||||
length_factor: f32,
|
||||
luminance_factor: f32,
|
||||
width_factor: f32,
|
||||
line_count: usize,
|
||||
}
|
||||
|
||||
impl Default for FractalClock {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
paused: false,
|
||||
time: 0.0,
|
||||
zoom: 0.25,
|
||||
start_line_width: 2.5,
|
||||
depth: 9,
|
||||
length_factor: 0.8,
|
||||
luminance_factor: 0.8,
|
||||
width_factor: 0.9,
|
||||
line_count: 0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
paused: false,
|
||||
time: 0.0,
|
||||
zoom: 0.25,
|
||||
start_line_width: 2.5,
|
||||
depth: 9,
|
||||
length_factor: 0.8,
|
||||
luminance_factor: 0.8,
|
||||
width_factor: 0.9,
|
||||
line_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FractalClock {
|
||||
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||
if !self.paused {
|
||||
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
let painter = Painter::new(
|
||||
ui.ctx().clone(),
|
||||
ui.layer_id(),
|
||||
ui.available_rect_before_wrap(),
|
||||
);
|
||||
self.paint(&painter);
|
||||
// Make sure we allocate what we used (everything)
|
||||
ui.expand_to_include_rect(painter.clip_rect());
|
||||
|
||||
Frame::popup(ui.style())
|
||||
.stroke(Stroke::none())
|
||||
.show(ui, |ui| {
|
||||
ui.set_max_width(270.0);
|
||||
CollapsingHeader::new("Settings")
|
||||
.show(ui, |ui| self.options_ui(ui, seconds_since_midnight));
|
||||
});
|
||||
pub fn ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||
if !self.paused {
|
||||
self.time = seconds_since_midnight.unwrap_or_else(|| ui.input().time);
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
fn options_ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||
if seconds_since_midnight.is_some() {
|
||||
ui.label(format!(
|
||||
"Local time: {:02}:{:02}:{:02}.{:03}",
|
||||
(self.time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(self.time % (60.0 * 60.0) / 60.0).floor(),
|
||||
(self.time % 60.0).floor(),
|
||||
(self.time % 1.0 * 100.0).floor()
|
||||
));
|
||||
} else {
|
||||
ui.label("The fractal_clock clock is not showing the correct time");
|
||||
};
|
||||
ui.label(format!("Painted line count: {}", self.line_count));
|
||||
let painter = Painter::new(
|
||||
ui.ctx().clone(),
|
||||
ui.layer_id(),
|
||||
ui.available_rect_before_wrap(),
|
||||
);
|
||||
self.paint(&painter);
|
||||
// Make sure we allocate what we used (everything)
|
||||
ui.expand_to_include_rect(painter.clip_rect());
|
||||
|
||||
ui.checkbox(&mut self.paused, "Paused");
|
||||
ui.add(Slider::new(&mut self.zoom, 0.0..=1.0).text("zoom"));
|
||||
ui.add(Slider::new(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
|
||||
ui.add(Slider::new(&mut self.depth, 0..=14).text("depth"));
|
||||
ui.add(Slider::new(&mut self.length_factor, 0.0..=1.0).text("length factor"));
|
||||
ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
|
||||
ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor"));
|
||||
Frame::popup(ui.style())
|
||||
.stroke(Stroke::none())
|
||||
.show(ui, |ui| {
|
||||
ui.set_max_width(270.0);
|
||||
CollapsingHeader::new("Settings")
|
||||
.show(ui, |ui| self.options_ui(ui, seconds_since_midnight));
|
||||
});
|
||||
}
|
||||
|
||||
egui::reset_button(ui, self);
|
||||
fn options_ui(&mut self, ui: &mut Ui, seconds_since_midnight: Option<f64>) {
|
||||
if seconds_since_midnight.is_some() {
|
||||
ui.label(format!(
|
||||
"Local time: {:02}:{:02}:{:02}.{:03}",
|
||||
(self.time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(self.time % (60.0 * 60.0) / 60.0).floor(),
|
||||
(self.time % 60.0).floor(),
|
||||
(self.time % 1.0 * 100.0).floor()
|
||||
));
|
||||
} else {
|
||||
ui.label("The fractal_clock clock is not showing the correct time");
|
||||
};
|
||||
ui.label(format!("Painted line count: {}", self.line_count));
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Inspired by a screensaver by Rob Mayoff",
|
||||
"http://www.dqd.com/~mayoff/programs/FractalClock/",
|
||||
);
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
ui.checkbox(&mut self.paused, "Paused");
|
||||
ui.add(Slider::new(&mut self.zoom, 0.0..=1.0).text("zoom"));
|
||||
ui.add(Slider::new(&mut self.start_line_width, 0.0..=5.0).text("Start line width"));
|
||||
ui.add(Slider::new(&mut self.depth, 0..=14).text("depth"));
|
||||
ui.add(Slider::new(&mut self.length_factor, 0.0..=1.0).text("length factor"));
|
||||
ui.add(Slider::new(&mut self.luminance_factor, 0.0..=1.0).text("luminance factor"));
|
||||
ui.add(Slider::new(&mut self.width_factor, 0.0..=1.0).text("width factor"));
|
||||
|
||||
egui::reset_button(ui, self);
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Inspired by a screensaver by Rob Mayoff",
|
||||
"http://www.dqd.com/~mayoff/programs/FractalClock/",
|
||||
);
|
||||
ui.add(egui_demo_lib::egui_github_link_file!());
|
||||
}
|
||||
|
||||
fn paint(&mut self, painter: &Painter) {
|
||||
struct Hand {
|
||||
length: f32,
|
||||
angle: f32,
|
||||
vec: Vec2,
|
||||
}
|
||||
|
||||
fn paint(&mut self, painter: &Painter) {
|
||||
struct Hand {
|
||||
length: f32,
|
||||
angle: f32,
|
||||
vec: Vec2,
|
||||
impl Hand {
|
||||
fn from_length_angle(length: f32, angle: f32) -> Self {
|
||||
Self {
|
||||
length,
|
||||
angle,
|
||||
vec: length * Vec2::angled(angle),
|
||||
}
|
||||
|
||||
impl Hand {
|
||||
fn from_length_angle(length: f32, angle: f32) -> Self {
|
||||
Self {
|
||||
length,
|
||||
angle,
|
||||
vec: length * Vec2::angled(angle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let angle_from_period =
|
||||
|period| TAU * (self.time.rem_euclid(period) / period) as f32 - TAU / 4.0;
|
||||
|
||||
let hands = [
|
||||
// Second hand:
|
||||
Hand::from_length_angle(self.length_factor, angle_from_period(60.0)),
|
||||
// Minute hand:
|
||||
Hand::from_length_angle(self.length_factor, angle_from_period(60.0 * 60.0)),
|
||||
// Hour hand:
|
||||
Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)),
|
||||
];
|
||||
|
||||
let mut shapes: Vec<Shape> = Vec::new();
|
||||
|
||||
let rect = painter.clip_rect();
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom),
|
||||
rect,
|
||||
);
|
||||
|
||||
let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| {
|
||||
let line = [to_screen * points[0], to_screen * points[1]];
|
||||
|
||||
// culling
|
||||
if rect.intersects(Rect::from_two_pos(line[0], line[1])) {
|
||||
shapes.push(Shape::line_segment(line, (width, color)));
|
||||
}
|
||||
};
|
||||
|
||||
let hand_rotations = [
|
||||
hands[0].angle - hands[2].angle + TAU / 2.0,
|
||||
hands[1].angle - hands[2].angle + TAU / 2.0,
|
||||
];
|
||||
|
||||
let hand_rotors = [
|
||||
hands[0].length * emath::Rot2::from_angle(hand_rotations[0]),
|
||||
hands[1].length * emath::Rot2::from_angle(hand_rotations[1]),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Node {
|
||||
pos: Pos2,
|
||||
dir: Vec2,
|
||||
}
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let mut width = self.start_line_width;
|
||||
|
||||
for (i, hand) in hands.iter().enumerate() {
|
||||
let center = pos2(0.0, 0.0);
|
||||
let end = center + hand.vec;
|
||||
paint_line([center, end], Color32::from_additive_luminance(255), width);
|
||||
if i < 2 {
|
||||
nodes.push(Node {
|
||||
pos: end,
|
||||
dir: hand.vec,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut luminance = 0.7; // Start dimmer than main hands
|
||||
|
||||
let mut new_nodes = Vec::new();
|
||||
for _ in 0..self.depth {
|
||||
new_nodes.clear();
|
||||
new_nodes.reserve(nodes.len() * 2);
|
||||
|
||||
luminance *= self.luminance_factor;
|
||||
width *= self.width_factor;
|
||||
|
||||
let luminance_u8 = (255.0 * luminance).round() as u8;
|
||||
if luminance_u8 == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
for &rotor in &hand_rotors {
|
||||
for a in &nodes {
|
||||
let new_dir = rotor * a.dir;
|
||||
let b = Node {
|
||||
pos: a.pos + new_dir,
|
||||
dir: new_dir,
|
||||
};
|
||||
paint_line(
|
||||
[a.pos, b.pos],
|
||||
Color32::from_additive_luminance(luminance_u8),
|
||||
width,
|
||||
);
|
||||
new_nodes.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
std::mem::swap(&mut nodes, &mut new_nodes);
|
||||
}
|
||||
self.line_count = shapes.len();
|
||||
painter.extend(shapes);
|
||||
}
|
||||
}
|
||||
|
||||
let angle_from_period =
|
||||
|period| TAU * (self.time.rem_euclid(period) / period) as f32 - TAU / 4.0;
|
||||
|
||||
let hands = [
|
||||
// Second hand:
|
||||
Hand::from_length_angle(self.length_factor, angle_from_period(60.0)),
|
||||
// Minute hand:
|
||||
Hand::from_length_angle(self.length_factor, angle_from_period(60.0 * 60.0)),
|
||||
// Hour hand:
|
||||
Hand::from_length_angle(0.5, angle_from_period(12.0 * 60.0 * 60.0)),
|
||||
];
|
||||
|
||||
let mut shapes: Vec<Shape> = Vec::new();
|
||||
|
||||
let rect = painter.clip_rect();
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_center_size(Pos2::ZERO, rect.square_proportions() / self.zoom),
|
||||
rect,
|
||||
);
|
||||
|
||||
let mut paint_line = |points: [Pos2; 2], color: Color32, width: f32| {
|
||||
let line = [to_screen * points[0], to_screen * points[1]];
|
||||
|
||||
// culling
|
||||
if rect.intersects(Rect::from_two_pos(line[0], line[1])) {
|
||||
shapes.push(Shape::line_segment(line, (width, color)));
|
||||
}
|
||||
};
|
||||
|
||||
let hand_rotations = [
|
||||
hands[0].angle - hands[2].angle + TAU / 2.0,
|
||||
hands[1].angle - hands[2].angle + TAU / 2.0,
|
||||
];
|
||||
|
||||
let hand_rotors = [
|
||||
hands[0].length * emath::Rot2::from_angle(hand_rotations[0]),
|
||||
hands[1].length * emath::Rot2::from_angle(hand_rotations[1]),
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Node {
|
||||
pos: Pos2,
|
||||
dir: Vec2,
|
||||
}
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let mut width = self.start_line_width;
|
||||
|
||||
for (i, hand) in hands.iter().enumerate() {
|
||||
let center = pos2(0.0, 0.0);
|
||||
let end = center + hand.vec;
|
||||
paint_line([center, end], Color32::from_additive_luminance(255), width);
|
||||
if i < 2 {
|
||||
nodes.push(Node {
|
||||
pos: end,
|
||||
dir: hand.vec,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut luminance = 0.7; // Start dimmer than main hands
|
||||
|
||||
let mut new_nodes = Vec::new();
|
||||
for _ in 0..self.depth {
|
||||
new_nodes.clear();
|
||||
new_nodes.reserve(nodes.len() * 2);
|
||||
|
||||
luminance *= self.luminance_factor;
|
||||
width *= self.width_factor;
|
||||
|
||||
let luminance_u8 = (255.0 * luminance).round() as u8;
|
||||
if luminance_u8 == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
for &rotor in &hand_rotors {
|
||||
for a in &nodes {
|
||||
let new_dir = rotor * a.dir;
|
||||
let b = Node {
|
||||
pos: a.pos + new_dir,
|
||||
dir: new_dir,
|
||||
};
|
||||
paint_line(
|
||||
[a.pos, b.pos],
|
||||
Color32::from_additive_luminance(luminance_u8),
|
||||
width,
|
||||
);
|
||||
new_nodes.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
std::mem::swap(&mut nodes, &mut new_nodes);
|
||||
}
|
||||
self.line_count = shapes.len();
|
||||
painter.extend(shapes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,216 +2,216 @@ use egui_extras::RetainedImage;
|
||||
use poll_promise::Promise;
|
||||
|
||||
struct Resource {
|
||||
/// HTTP response
|
||||
response: ehttp::Response,
|
||||
/// HTTP response
|
||||
response: ehttp::Response,
|
||||
|
||||
text: Option<String>,
|
||||
text: Option<String>,
|
||||
|
||||
/// If set, the response was an image.
|
||||
image: Option<RetainedImage>,
|
||||
/// If set, the response was an image.
|
||||
image: Option<RetainedImage>,
|
||||
|
||||
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
||||
colored_text: Option<ColoredText>,
|
||||
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
|
||||
colored_text: Option<ColoredText>,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
let image = if content_type.starts_with("image/") {
|
||||
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
|
||||
let content_type = response.content_type().unwrap_or_default();
|
||||
let image = if content_type.starts_with("image/") {
|
||||
RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let text = response.text();
|
||||
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||
let text = text.map(|text| text.to_owned());
|
||||
let text = response.text();
|
||||
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
|
||||
let text = text.map(|text| text.to_owned());
|
||||
|
||||
Self {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
colored_text,
|
||||
}
|
||||
Self {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
colored_text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct HttpApp {
|
||||
url: String,
|
||||
url: String,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
promise: Option<Promise<ehttp::Result<Resource>>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
promise: Option<Promise<ehttp::Result<Resource>>>,
|
||||
}
|
||||
|
||||
impl Default for HttpApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
|
||||
promise: Default::default(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(),
|
||||
promise: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for HttpApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(egui_demo_lib::egui_github_link_file!())
|
||||
})
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::bottom("http_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(egui_demo_lib::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("HTTP requests made using ");
|
||||
ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp");
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
if trigger_fetch {
|
||||
let ctx = ctx.clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(&self.url);
|
||||
ehttp::fetch(request, move |response| {
|
||||
ctx.request_repaint(); // wake up UI thread
|
||||
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
||||
sender.send(resource);
|
||||
});
|
||||
self.promise = Some(promise);
|
||||
}
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let trigger_fetch = ui_url(ui, frame, &mut self.url);
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("HTTP requests made using ");
|
||||
ui.hyperlink_to("ehttp", "https://www.github.com/emilk/ehttp");
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
if trigger_fetch {
|
||||
let ctx = ctx.clone();
|
||||
let (sender, promise) = Promise::new();
|
||||
let request = ehttp::Request::get(&self.url);
|
||||
ehttp::fetch(request, move |response| {
|
||||
ctx.request_repaint(); // wake up UI thread
|
||||
let resource = response.map(|response| Resource::from_response(&ctx, response));
|
||||
sender.send(resource);
|
||||
});
|
||||
self.promise = Some(promise);
|
||||
if let Some(promise) = &self.promise {
|
||||
if let Some(result) = promise.ready() {
|
||||
match result {
|
||||
Ok(resource) => {
|
||||
ui_resource(ui, resource);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if let Some(promise) = &self.promise {
|
||||
if let Some(result) = promise.ready() {
|
||||
match result {
|
||||
Ok(resource) => {
|
||||
ui_resource(ui, resource);
|
||||
}
|
||||
Err(error) => {
|
||||
// This should only happen if the fetch API isn't available or something similar.
|
||||
ui.colored_label(
|
||||
ui.visuals().error_fg_color,
|
||||
if error.is_empty() { "Error" } else { error },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.spinner();
|
||||
}
|
||||
Err(error) => {
|
||||
// This should only happen if the fetch API isn't available or something similar.
|
||||
ui.colored_label(
|
||||
ui.visuals().error_fg_color,
|
||||
if error.is_empty() { "Error" } else { error },
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.spinner();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_url(ui: &mut egui::Ui, frame: &mut eframe::Frame, url: &mut String) -> bool {
|
||||
let mut trigger_fetch = false;
|
||||
let mut trigger_fetch = false;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("URL:");
|
||||
trigger_fetch |= ui
|
||||
.add(egui::TextEdit::singleline(url).desired_width(f32::INFINITY))
|
||||
.lost_focus();
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("URL:");
|
||||
trigger_fetch |= ui
|
||||
.add(egui::TextEdit::singleline(url).desired_width(f32::INFINITY))
|
||||
.lost_focus();
|
||||
});
|
||||
|
||||
if frame.is_web() {
|
||||
ui.label("HINT: paste the url of this page into the field above!");
|
||||
if frame.is_web() {
|
||||
ui.label("HINT: paste the url of this page into the field above!");
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Source code for this example").clicked() {
|
||||
*url = format!(
|
||||
"https://raw.githubusercontent.com/emilk/egui/master/{}",
|
||||
file!()
|
||||
);
|
||||
trigger_fetch = true;
|
||||
}
|
||||
if ui.button("Random image").clicked() {
|
||||
let seed = ui.input().time;
|
||||
let side = 640;
|
||||
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
|
||||
trigger_fetch = true;
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Source code for this example").clicked() {
|
||||
*url = format!(
|
||||
"https://raw.githubusercontent.com/emilk/egui/master/{}",
|
||||
file!()
|
||||
);
|
||||
trigger_fetch = true;
|
||||
}
|
||||
if ui.button("Random image").clicked() {
|
||||
let seed = ui.input().time;
|
||||
let side = 640;
|
||||
*url = format!("https://picsum.photos/seed/{}/{}", seed, side);
|
||||
trigger_fetch = true;
|
||||
}
|
||||
});
|
||||
|
||||
trigger_fetch
|
||||
trigger_fetch
|
||||
}
|
||||
|
||||
fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||
let Resource {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
colored_text,
|
||||
} = resource;
|
||||
let Resource {
|
||||
response,
|
||||
text,
|
||||
image,
|
||||
colored_text,
|
||||
} = resource;
|
||||
|
||||
ui.monospace(format!("url: {}", response.url));
|
||||
ui.monospace(format!(
|
||||
"status: {} ({})",
|
||||
response.status, response.status_text
|
||||
));
|
||||
ui.monospace(format!(
|
||||
"content-type: {}",
|
||||
response.content_type().unwrap_or_default()
|
||||
));
|
||||
ui.monospace(format!(
|
||||
"size: {:.1} kB",
|
||||
response.bytes.len() as f32 / 1000.0
|
||||
));
|
||||
ui.monospace(format!("url: {}", response.url));
|
||||
ui.monospace(format!(
|
||||
"status: {} ({})",
|
||||
response.status, response.status_text
|
||||
));
|
||||
ui.monospace(format!(
|
||||
"content-type: {}",
|
||||
response.content_type().unwrap_or_default()
|
||||
));
|
||||
ui.monospace(format!(
|
||||
"size: {:.1} kB",
|
||||
response.bytes.len() as f32 / 1000.0
|
||||
));
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
egui::CollapsingHeader::new("Response headers")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
egui::CollapsingHeader::new("Response headers")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
egui::Grid::new("response_headers")
|
||||
.spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))
|
||||
.show(ui, |ui| {
|
||||
for header in &response.headers {
|
||||
ui.label(header.0);
|
||||
ui.label(header.1);
|
||||
ui.end_row();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if let Some(text) = &text {
|
||||
let tooltip = "Click to copy the response body";
|
||||
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
||||
ui.output().copied_text = text.clone();
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
if let Some(image) = image {
|
||||
let mut size = image.size_vec2();
|
||||
size *= (ui.available_width() / size.x).min(1.0);
|
||||
image.show_size(ui, size);
|
||||
} else if let Some(colored_text) = colored_text {
|
||||
colored_text.ui(ui);
|
||||
} else if let Some(text) = &text {
|
||||
selectable_text(ui, text);
|
||||
} else {
|
||||
ui.monospace("[binary]");
|
||||
}
|
||||
egui::Grid::new("response_headers")
|
||||
.spacing(egui::vec2(ui.spacing().item_spacing.x * 2.0, 0.0))
|
||||
.show(ui, |ui| {
|
||||
for header in &response.headers {
|
||||
ui.label(header.0);
|
||||
ui.label(header.1);
|
||||
ui.end_row();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
if let Some(text) = &text {
|
||||
let tooltip = "Click to copy the response body";
|
||||
if ui.button("📋").on_hover_text(tooltip).clicked() {
|
||||
ui.output().copied_text = text.clone();
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
|
||||
if let Some(image) = image {
|
||||
let mut size = image.size_vec2();
|
||||
size *= (ui.available_width() / size.x).min(1.0);
|
||||
image.show_size(ui, size);
|
||||
} else if let Some(colored_text) = colored_text {
|
||||
colored_text.ui(ui);
|
||||
} else if let Some(text) = &text {
|
||||
selectable_text(ui, text);
|
||||
} else {
|
||||
ui.monospace("[binary]");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut text)
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Monospace),
|
||||
);
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut text)
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Monospace),
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -219,48 +219,48 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
fn syntax_highlighting(
|
||||
ctx: &egui::Context,
|
||||
response: &ehttp::Response,
|
||||
text: &str,
|
||||
ctx: &egui::Context,
|
||||
response: &ehttp::Response,
|
||||
text: &str,
|
||||
) -> Option<ColoredText> {
|
||||
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
||||
let extension = extension_and_rest.get(0)?;
|
||||
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
||||
Some(ColoredText(crate::syntax_highlighting::highlight(
|
||||
ctx, &theme, text, extension,
|
||||
)))
|
||||
let extension_and_rest: Vec<&str> = response.url.rsplitn(2, '.').collect();
|
||||
let extension = extension_and_rest.get(0)?;
|
||||
let theme = crate::syntax_highlighting::CodeTheme::from_style(&ctx.style());
|
||||
Some(ColoredText(crate::syntax_highlighting::highlight(
|
||||
ctx, &theme, text, extension,
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
fn syntax_highlighting(_ctx: &egui::Context, _: &ehttp::Response, _: &str) -> Option<ColoredText> {
|
||||
None
|
||||
None
|
||||
}
|
||||
|
||||
struct ColoredText(egui::text::LayoutJob);
|
||||
|
||||
impl ColoredText {
|
||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||
if true {
|
||||
// Selectable text:
|
||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
||||
let mut layout_job = self.0.clone();
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||
if true {
|
||||
// Selectable text:
|
||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
||||
let mut layout_job = self.0.clone();
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
let mut text = self.0.text.as_str();
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut text)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
} else {
|
||||
let mut job = self.0.clone();
|
||||
job.wrap.max_width = ui.available_width();
|
||||
let galley = ui.fonts().layout_job(job);
|
||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||
}
|
||||
let mut text = self.0.text.as_str();
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut text)
|
||||
.font(egui::TextStyle::Monospace)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
} else {
|
||||
let mut job = self.0.clone();
|
||||
job.wrap.max_width = ui.available_width();
|
||||
let galley = ui.fonts().layout_job(job);
|
||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
||||
painter.add(egui::Shape::galley(response.rect.min, galley));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,32 +3,32 @@ use egui::Widget;
|
||||
/// How often we repaint the demo app by default
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum RunMode {
|
||||
/// This is the default for the demo.
|
||||
///
|
||||
/// If this is selected, egui is only updated if are input events
|
||||
/// (like mouse movements) or there are some animations in the GUI.
|
||||
///
|
||||
/// Reactive mode saves CPU.
|
||||
///
|
||||
/// The downside is that the UI can become out-of-date if something it is supposed to monitor changes.
|
||||
/// For instance, a GUI for a thermostat need to repaint each time the temperature changes.
|
||||
/// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each
|
||||
/// time such an event happens. You can also chose to call `request_repaint()` once every second
|
||||
/// or after every single frame - this is called [`Continuous`](RunMode::Continuous) mode,
|
||||
/// and for games and interactive tools that need repainting every frame anyway, this should be the default.
|
||||
Reactive,
|
||||
/// This is the default for the demo.
|
||||
///
|
||||
/// If this is selected, egui is only updated if are input events
|
||||
/// (like mouse movements) or there are some animations in the GUI.
|
||||
///
|
||||
/// Reactive mode saves CPU.
|
||||
///
|
||||
/// The downside is that the UI can become out-of-date if something it is supposed to monitor changes.
|
||||
/// For instance, a GUI for a thermostat need to repaint each time the temperature changes.
|
||||
/// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each
|
||||
/// time such an event happens. You can also chose to call `request_repaint()` once every second
|
||||
/// or after every single frame - this is called [`Continuous`](RunMode::Continuous) mode,
|
||||
/// and for games and interactive tools that need repainting every frame anyway, this should be the default.
|
||||
Reactive,
|
||||
|
||||
/// This will call `egui::Context::request_repaint()` at the end of each frame
|
||||
/// to request the backend to repaint as soon as possible.
|
||||
///
|
||||
/// On most platforms this will mean that egui will run at the display refresh rate of e.g. 60 Hz.
|
||||
///
|
||||
/// For this demo it is not any reason to do so except to
|
||||
/// demonstrate how quickly egui runs.
|
||||
///
|
||||
/// For games or other interactive apps, this is probably what you want to do.
|
||||
/// It will guarantee that egui is always up-to-date.
|
||||
Continuous,
|
||||
/// This will call `egui::Context::request_repaint()` at the end of each frame
|
||||
/// to request the backend to repaint as soon as possible.
|
||||
///
|
||||
/// On most platforms this will mean that egui will run at the display refresh rate of e.g. 60 Hz.
|
||||
///
|
||||
/// For this demo it is not any reason to do so except to
|
||||
/// demonstrate how quickly egui runs.
|
||||
///
|
||||
/// For games or other interactive apps, this is probably what you want to do.
|
||||
/// It will guarantee that egui is always up-to-date.
|
||||
Continuous,
|
||||
}
|
||||
|
||||
/// Default for demo is Reactive since
|
||||
@@ -36,9 +36,9 @@ enum RunMode {
|
||||
/// 2) There are no external events that could invalidate the UI
|
||||
/// so there are no events to miss.
|
||||
impl Default for RunMode {
|
||||
fn default() -> Self {
|
||||
RunMode::Reactive
|
||||
}
|
||||
fn default() -> Self {
|
||||
RunMode::Reactive
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -46,340 +46,345 @@ impl Default for RunMode {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct BackendPanel {
|
||||
pub open: bool,
|
||||
pub open: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
// go back to [`RunMode::Reactive`] mode each time we start
|
||||
run_mode: RunMode,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
// go back to [`RunMode::Reactive`] mode each time we start
|
||||
run_mode: RunMode,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
repaint_after_seconds: f32,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
repaint_after_seconds: f32,
|
||||
|
||||
/// current slider value for current gui scale
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pixels_per_point: Option<f32>,
|
||||
/// current slider value for current gui scale
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
pixels_per_point: Option<f32>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
frame_history: crate::frame_history::FrameHistory,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
frame_history: crate::frame_history::FrameHistory,
|
||||
|
||||
egui_windows: EguiWindows,
|
||||
egui_windows: EguiWindows,
|
||||
}
|
||||
|
||||
impl Default for BackendPanel {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
run_mode: Default::default(),
|
||||
repaint_after_seconds: 1.0,
|
||||
pixels_per_point: None,
|
||||
frame_history: Default::default(),
|
||||
egui_windows: Default::default(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
run_mode: Default::default(),
|
||||
repaint_after_seconds: 1.0,
|
||||
pixels_per_point: None,
|
||||
frame_history: Default::default(),
|
||||
egui_windows: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendPanel {
|
||||
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self.frame_history
|
||||
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
|
||||
pub fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
self
|
||||
.frame_history
|
||||
.on_new_frame(ctx.input().time, frame.info().cpu_usage);
|
||||
|
||||
match self.run_mode {
|
||||
RunMode::Continuous => {
|
||||
// Tell the backend to repaint as soon as possible
|
||||
ctx.request_repaint();
|
||||
}
|
||||
RunMode::Reactive => {
|
||||
// let the computer rest for a bit
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f32(
|
||||
self.repaint_after_seconds,
|
||||
));
|
||||
}
|
||||
}
|
||||
match self.run_mode {
|
||||
RunMode::Continuous => {
|
||||
// Tell the backend to repaint as soon as possible
|
||||
ctx.request_repaint();
|
||||
}
|
||||
RunMode::Reactive => {
|
||||
// let the computer rest for a bit
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f32(
|
||||
self.repaint_after_seconds,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_of_frame(&mut self, ctx: &egui::Context) {
|
||||
self.egui_windows.windows(ctx);
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::trace!(ui);
|
||||
|
||||
self.integration_ui(ui, frame);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.run_mode_ui(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.frame_history.ui(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("egui windows:");
|
||||
self.egui_windows.checkboxes(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
{
|
||||
let mut debug_on_hover = ui.ctx().debug_on_hover();
|
||||
ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover")
|
||||
.on_hover_text("Show structure of the ui when you hover with the mouse");
|
||||
ui.ctx().set_debug_on_hover(debug_on_hover);
|
||||
}
|
||||
|
||||
pub fn end_of_frame(&mut self, ctx: &egui::Context) {
|
||||
self.egui_windows.windows(ctx);
|
||||
ui.separator();
|
||||
|
||||
{
|
||||
let mut screen_reader = ui.ctx().options().screen_reader;
|
||||
ui.checkbox(&mut screen_reader, "🔈 Screen reader")
|
||||
.on_hover_text(
|
||||
"Experimental feature: checking this will turn on the screen reader on supported platforms",
|
||||
);
|
||||
ui.ctx().options().screen_reader = screen_reader;
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::trace!(ui);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ui.separator();
|
||||
if ui.button("Quit").clicked() {
|
||||
frame.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.integration_ui(ui, frame);
|
||||
fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("egui running inside ");
|
||||
ui.hyperlink_to(
|
||||
"eframe",
|
||||
"https://github.com/emilk/egui/tree/master/crates/eframe",
|
||||
);
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
ui.collapsing("Web info (location)", |ui| {
|
||||
ui.monospace(format!("{:#?}", frame.info().web_info.location));
|
||||
});
|
||||
|
||||
self.run_mode_ui(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.frame_history.ui(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("egui windows:");
|
||||
self.egui_windows.checkboxes(ui);
|
||||
|
||||
ui.separator();
|
||||
// For instance: `eframe` web sets `pixels_per_point` every frame to force
|
||||
// egui to use the same scale as the web zoom factor.
|
||||
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();
|
||||
if !integration_controls_pixels_per_point {
|
||||
if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, &frame.info()) {
|
||||
ui.ctx().set_pixels_per_point(new_pixels_per_point);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
{
|
||||
let mut debug_on_hover = ui.ctx().debug_on_hover();
|
||||
ui.checkbox(&mut debug_on_hover, "🐛 Debug on hover")
|
||||
.on_hover_text("Show structure of the ui when you hover with the mouse");
|
||||
ui.ctx().set_debug_on_hover(debug_on_hover);
|
||||
let mut fullscreen = frame.info().window_info.fullscreen;
|
||||
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
|
||||
.on_hover_text("Fullscreen the window");
|
||||
frame.set_fullscreen(fullscreen);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
if ui
|
||||
.button("📱 Phone Size")
|
||||
.on_hover_text("Resize the window to be small like a phone.")
|
||||
.clicked()
|
||||
{
|
||||
let mut screen_reader = ui.ctx().options().screen_reader;
|
||||
ui.checkbox(&mut screen_reader, "🔈 Screen reader").on_hover_text("Experimental feature: checking this will turn on the screen reader on supported platforms");
|
||||
ui.ctx().options().screen_reader = screen_reader;
|
||||
// frame.set_window_size(egui::vec2(375.0, 812.0)); // iPhone 12 mini
|
||||
frame.set_window_size(egui::vec2(375.0, 667.0)); // iPhone SE 2nd gen
|
||||
frame.set_fullscreen(false);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if !frame.info().window_info.fullscreen
|
||||
&& ui
|
||||
.button("Drag me to drag window")
|
||||
.is_pointer_button_down_on()
|
||||
{
|
||||
frame.drag_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels_per_point_ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
info: &eframe::IntegrationInfo,
|
||||
) -> Option<f32> {
|
||||
let pixels_per_point = self.pixels_per_point.get_or_insert_with(|| {
|
||||
info
|
||||
.native_pixels_per_point
|
||||
.unwrap_or_else(|| ui.ctx().pixels_per_point())
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().slider_width = 90.0;
|
||||
ui.add(
|
||||
egui::Slider::new(pixels_per_point, 0.5..=5.0)
|
||||
.logarithmic(true)
|
||||
.clamp_to_range(true)
|
||||
.text("Scale"),
|
||||
)
|
||||
.on_hover_text("Physical pixels per point.");
|
||||
if let Some(native_pixels_per_point) = info.native_pixels_per_point {
|
||||
let enabled = *pixels_per_point != native_pixels_per_point;
|
||||
if ui
|
||||
.add_enabled(enabled, egui::Button::new("Reset"))
|
||||
.on_hover_text(format!(
|
||||
"Reset scale to native value ({:.1})",
|
||||
native_pixels_per_point
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
ui.separator();
|
||||
if ui.button("Quit").clicked() {
|
||||
frame.close();
|
||||
}
|
||||
*pixels_per_point = native_pixels_per_point;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We wait until mouse release to activate:
|
||||
if ui.ctx().is_using_pointer() {
|
||||
None
|
||||
} else {
|
||||
Some(*pixels_per_point)
|
||||
}
|
||||
}
|
||||
|
||||
fn integration_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("egui running inside ");
|
||||
ui.hyperlink_to(
|
||||
"eframe",
|
||||
"https://github.com/emilk/egui/tree/master/crates/eframe",
|
||||
);
|
||||
ui.label(".");
|
||||
});
|
||||
fn run_mode_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
let run_mode = &mut self.run_mode;
|
||||
ui.label("Mode:");
|
||||
ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
|
||||
.on_hover_text("Repaint when there are animations or input (e.g. mouse movement)");
|
||||
ui.radio_value(run_mode, RunMode::Continuous, "Continuous")
|
||||
.on_hover_text("Repaint everything each frame");
|
||||
});
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
ui.collapsing("Web info (location)", |ui| {
|
||||
ui.monospace(format!("{:#?}", frame.info().web_info.location));
|
||||
});
|
||||
if self.run_mode == RunMode::Continuous {
|
||||
ui.label(format!(
|
||||
"Repainting the UI each frame. FPS: {:.1}",
|
||||
self.frame_history.fps()
|
||||
));
|
||||
} else {
|
||||
ui.label("Only running UI code when there are animations or input.");
|
||||
|
||||
// For instance: `eframe` web sets `pixels_per_point` every frame to force
|
||||
// egui to use the same scale as the web zoom factor.
|
||||
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();
|
||||
if !integration_controls_pixels_per_point {
|
||||
if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, &frame.info()) {
|
||||
ui.ctx().set_pixels_per_point(new_pixels_per_point);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
{
|
||||
let mut fullscreen = frame.info().window_info.fullscreen;
|
||||
ui.checkbox(&mut fullscreen, "🗖 Fullscreen")
|
||||
.on_hover_text("Fullscreen the window");
|
||||
frame.set_fullscreen(fullscreen);
|
||||
}
|
||||
|
||||
if ui
|
||||
.button("📱 Phone Size")
|
||||
.on_hover_text("Resize the window to be small like a phone.")
|
||||
.clicked()
|
||||
{
|
||||
// frame.set_window_size(egui::vec2(375.0, 812.0)); // iPhone 12 mini
|
||||
frame.set_window_size(egui::vec2(375.0, 667.0)); // iPhone SE 2nd gen
|
||||
frame.set_fullscreen(false);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
if !frame.info().window_info.fullscreen
|
||||
&& ui
|
||||
.button("Drag me to drag window")
|
||||
.is_pointer_button_down_on()
|
||||
{
|
||||
frame.drag_window();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels_per_point_ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
info: &eframe::IntegrationInfo,
|
||||
) -> Option<f32> {
|
||||
let pixels_per_point = self.pixels_per_point.get_or_insert_with(|| {
|
||||
info.native_pixels_per_point
|
||||
.unwrap_or_else(|| ui.ctx().pixels_per_point())
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().slider_width = 90.0;
|
||||
ui.add(
|
||||
egui::Slider::new(pixels_per_point, 0.5..=5.0)
|
||||
.logarithmic(true)
|
||||
.clamp_to_range(true)
|
||||
.text("Scale"),
|
||||
)
|
||||
.on_hover_text("Physical pixels per point.");
|
||||
if let Some(native_pixels_per_point) = info.native_pixels_per_point {
|
||||
let enabled = *pixels_per_point != native_pixels_per_point;
|
||||
if ui
|
||||
.add_enabled(enabled, egui::Button::new("Reset"))
|
||||
.on_hover_text(format!(
|
||||
"Reset scale to native value ({:.1})",
|
||||
native_pixels_per_point
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
*pixels_per_point = native_pixels_per_point;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We wait until mouse release to activate:
|
||||
if ui.ctx().is_using_pointer() {
|
||||
None
|
||||
} else {
|
||||
Some(*pixels_per_point)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_mode_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
let run_mode = &mut self.run_mode;
|
||||
ui.label("Mode:");
|
||||
ui.radio_value(run_mode, RunMode::Reactive, "Reactive")
|
||||
.on_hover_text("Repaint when there are animations or input (e.g. mouse movement)");
|
||||
ui.radio_value(run_mode, RunMode::Continuous, "Continuous")
|
||||
.on_hover_text("Repaint everything each frame");
|
||||
});
|
||||
|
||||
if self.run_mode == RunMode::Continuous {
|
||||
ui.label(format!(
|
||||
"Repainting the UI each frame. FPS: {:.1}",
|
||||
self.frame_history.fps()
|
||||
));
|
||||
} else {
|
||||
ui.label("Only running UI code when there are animations or input.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("(but at least every ");
|
||||
egui::DragValue::new(&mut self.repaint_after_seconds)
|
||||
.clamp_range(0.1..=10.0)
|
||||
.speed(0.1)
|
||||
.suffix(" s")
|
||||
.ui(ui)
|
||||
.on_hover_text("Repaint this often, even if there is no input.");
|
||||
ui.label(")");
|
||||
});
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("(but at least every ");
|
||||
egui::DragValue::new(&mut self.repaint_after_seconds)
|
||||
.clamp_range(0.1..=10.0)
|
||||
.speed(0.1)
|
||||
.suffix(" s")
|
||||
.ui(ui)
|
||||
.on_hover_text("Repaint this often, even if there is no input.");
|
||||
ui.label(")");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct EguiWindows {
|
||||
// egui stuff:
|
||||
settings: bool,
|
||||
inspection: bool,
|
||||
memory: bool,
|
||||
output_events: bool,
|
||||
// egui stuff:
|
||||
settings: bool,
|
||||
inspection: bool,
|
||||
memory: bool,
|
||||
output_events: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
output_event_history: std::collections::VecDeque<egui::output::OutputEvent>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
output_event_history: std::collections::VecDeque<egui::output::OutputEvent>,
|
||||
}
|
||||
|
||||
impl Default for EguiWindows {
|
||||
fn default() -> Self {
|
||||
EguiWindows::none()
|
||||
}
|
||||
fn default() -> Self {
|
||||
EguiWindows::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl EguiWindows {
|
||||
fn none() -> Self {
|
||||
Self {
|
||||
settings: false,
|
||||
inspection: false,
|
||||
memory: false,
|
||||
output_events: false,
|
||||
output_event_history: Default::default(),
|
||||
}
|
||||
fn none() -> Self {
|
||||
Self {
|
||||
settings: false,
|
||||
inspection: false,
|
||||
memory: false,
|
||||
output_events: false,
|
||||
output_event_history: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn checkboxes(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
settings,
|
||||
inspection,
|
||||
memory,
|
||||
output_events,
|
||||
output_event_history: _,
|
||||
} = self;
|
||||
|
||||
ui.checkbox(settings, "🔧 Settings");
|
||||
ui.checkbox(inspection, "🔍 Inspection");
|
||||
ui.checkbox(memory, "📝 Memory");
|
||||
ui.checkbox(output_events, "📤 Output Events");
|
||||
}
|
||||
|
||||
fn windows(&mut self, ctx: &egui::Context) {
|
||||
let Self {
|
||||
settings,
|
||||
inspection,
|
||||
memory,
|
||||
output_events,
|
||||
output_event_history,
|
||||
} = self;
|
||||
|
||||
for event in &ctx.output().events {
|
||||
output_event_history.push_back(event.clone());
|
||||
}
|
||||
while output_event_history.len() > 1000 {
|
||||
output_event_history.pop_front();
|
||||
}
|
||||
|
||||
fn checkboxes(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
settings,
|
||||
inspection,
|
||||
memory,
|
||||
output_events,
|
||||
output_event_history: _,
|
||||
} = self;
|
||||
egui::Window::new("🔧 Settings")
|
||||
.open(settings)
|
||||
.vscroll(true)
|
||||
.show(ctx, |ui| {
|
||||
ctx.settings_ui(ui);
|
||||
});
|
||||
|
||||
ui.checkbox(settings, "🔧 Settings");
|
||||
ui.checkbox(inspection, "🔍 Inspection");
|
||||
ui.checkbox(memory, "📝 Memory");
|
||||
ui.checkbox(output_events, "📤 Output Events");
|
||||
}
|
||||
egui::Window::new("🔍 Inspection")
|
||||
.open(inspection)
|
||||
.vscroll(true)
|
||||
.show(ctx, |ui| {
|
||||
ctx.inspection_ui(ui);
|
||||
});
|
||||
|
||||
fn windows(&mut self, ctx: &egui::Context) {
|
||||
let Self {
|
||||
settings,
|
||||
inspection,
|
||||
memory,
|
||||
output_events,
|
||||
output_event_history,
|
||||
} = self;
|
||||
egui::Window::new("📝 Memory")
|
||||
.open(memory)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ctx.memory_ui(ui);
|
||||
});
|
||||
|
||||
for event in &ctx.output().events {
|
||||
output_event_history.push_back(event.clone());
|
||||
}
|
||||
while output_event_history.len() > 1000 {
|
||||
output_event_history.pop_front();
|
||||
}
|
||||
|
||||
egui::Window::new("🔧 Settings")
|
||||
.open(settings)
|
||||
.vscroll(true)
|
||||
.show(ctx, |ui| {
|
||||
ctx.settings_ui(ui);
|
||||
});
|
||||
|
||||
egui::Window::new("🔍 Inspection")
|
||||
.open(inspection)
|
||||
.vscroll(true)
|
||||
.show(ctx, |ui| {
|
||||
ctx.inspection_ui(ui);
|
||||
});
|
||||
|
||||
egui::Window::new("📝 Memory")
|
||||
.open(memory)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ctx.memory_ui(ui);
|
||||
});
|
||||
|
||||
egui::Window::new("📤 Output Events")
|
||||
.open(output_events)
|
||||
.resizable(true)
|
||||
.default_width(520.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
"Recent output events from egui. \
|
||||
egui::Window::new("📤 Output Events")
|
||||
.open(output_events)
|
||||
.resizable(true)
|
||||
.default_width(520.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
"Recent output events from egui. \
|
||||
These are emitted when you interact with widgets, or move focus between them with TAB. \
|
||||
They can be hooked up to a screen reader on supported platforms.",
|
||||
);
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical()
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
for event in output_event_history {
|
||||
ui.label(format!("{:?}", event));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
egui::ScrollArea::vertical()
|
||||
.stick_to_bottom(true)
|
||||
.show(ui, |ui| {
|
||||
for event in output_event_history {
|
||||
ui.label(format!("{:?}", event));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
use egui::util::History;
|
||||
|
||||
pub struct FrameHistory {
|
||||
frame_times: History<f32>,
|
||||
frame_times: History<f32>,
|
||||
}
|
||||
|
||||
impl Default for FrameHistory {
|
||||
fn default() -> Self {
|
||||
let max_age: f32 = 1.0;
|
||||
let max_len = (max_age * 300.0).round() as usize;
|
||||
Self {
|
||||
frame_times: History::new(0..max_len, max_age),
|
||||
}
|
||||
fn default() -> Self {
|
||||
let max_age: f32 = 1.0;
|
||||
let max_len = (max_age * 300.0).round() as usize;
|
||||
Self {
|
||||
frame_times: History::new(0..max_len, max_age),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameHistory {
|
||||
// Called first
|
||||
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
|
||||
let previous_frame_time = previous_frame_time.unwrap_or_default();
|
||||
if let Some(latest) = self.frame_times.latest_mut() {
|
||||
*latest = previous_frame_time; // rewrite history now that we know
|
||||
}
|
||||
self.frame_times.add(now, previous_frame_time); // projected
|
||||
// Called first
|
||||
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
|
||||
let previous_frame_time = previous_frame_time.unwrap_or_default();
|
||||
if let Some(latest) = self.frame_times.latest_mut() {
|
||||
*latest = previous_frame_time; // rewrite history now that we know
|
||||
}
|
||||
self.frame_times.add(now, previous_frame_time); // projected
|
||||
}
|
||||
|
||||
pub fn mean_frame_time(&self) -> f32 {
|
||||
self.frame_times.average().unwrap_or_default()
|
||||
}
|
||||
pub fn mean_frame_time(&self) -> f32 {
|
||||
self.frame_times.average().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn fps(&self) -> f32 {
|
||||
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
|
||||
}
|
||||
pub fn fps(&self) -> f32 {
|
||||
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(format!(
|
||||
"Total frames painted: {}",
|
||||
self.frame_times.total_count()
|
||||
))
|
||||
.on_hover_text("Includes this frame.");
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(format!(
|
||||
"Total frames painted: {}",
|
||||
self.frame_times.total_count()
|
||||
))
|
||||
.on_hover_text("Includes this frame.");
|
||||
|
||||
ui.label(format!(
|
||||
"Mean CPU usage: {:.2} ms / frame",
|
||||
1e3 * self.mean_frame_time()
|
||||
))
|
||||
.on_hover_text(
|
||||
"Includes egui layout and tessellation time.\n\
|
||||
ui.label(format!(
|
||||
"Mean CPU usage: {:.2} ms / frame",
|
||||
1e3 * self.mean_frame_time()
|
||||
))
|
||||
.on_hover_text(
|
||||
"Includes egui layout and tessellation time.\n\
|
||||
Does not include GPU usage, nor overhead for sending data to GPU.",
|
||||
);
|
||||
egui::warn_if_debug_build(ui);
|
||||
);
|
||||
egui::warn_if_debug_build(ui);
|
||||
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
egui::CollapsingHeader::new("📊 CPU usage history")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.graph(ui);
|
||||
});
|
||||
}
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
egui::CollapsingHeader::new("📊 CPU usage history")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
self.graph(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::*;
|
||||
|
||||
ui.label("egui CPU usage history");
|
||||
|
||||
let history = &self.frame_times;
|
||||
|
||||
// TODO(emilk): we should not use `slider_width` as default graph width.
|
||||
let height = ui.spacing().slider_width;
|
||||
let size = vec2(ui.available_size_before_wrap().x, height);
|
||||
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
|
||||
let style = ui.style().noninteractive();
|
||||
|
||||
let graph_top_cpu_usage = 0.010;
|
||||
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
|
||||
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
|
||||
|
||||
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
|
||||
shapes.push(Shape::Rect(epaint::RectShape {
|
||||
rect,
|
||||
rounding: style.rounding,
|
||||
fill: ui.visuals().extreme_bg_color,
|
||||
stroke: ui.style().noninteractive().bg_stroke,
|
||||
}));
|
||||
|
||||
let rect = rect.shrink(4.0);
|
||||
let color = ui.visuals().text_color();
|
||||
let line_stroke = Stroke::new(1.0, color);
|
||||
|
||||
if let Some(pointer_pos) = response.hover_pos() {
|
||||
let y = pointer_pos.y;
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(rect.left(), y), pos2(rect.right(), y)],
|
||||
line_stroke,
|
||||
));
|
||||
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
|
||||
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
||||
shapes.push(Shape::text(
|
||||
&*ui.fonts(),
|
||||
pos2(rect.left(), y),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Monospace.resolve(ui.style()),
|
||||
color,
|
||||
));
|
||||
}
|
||||
|
||||
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::*;
|
||||
let circle_color = color;
|
||||
let radius = 2.0;
|
||||
let right_side_time = ui.input().time; // Time at right side of screen
|
||||
|
||||
ui.label("egui CPU usage history");
|
||||
for (time, cpu_usage) in history.iter() {
|
||||
let age = (right_side_time - time) as f32;
|
||||
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
|
||||
|
||||
let history = &self.frame_times;
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(pos.x, rect.bottom()), pos],
|
||||
line_stroke,
|
||||
));
|
||||
|
||||
// TODO(emilk): we should not use `slider_width` as default graph width.
|
||||
let height = ui.spacing().slider_width;
|
||||
let size = vec2(ui.available_size_before_wrap().x, height);
|
||||
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
|
||||
let style = ui.style().noninteractive();
|
||||
|
||||
let graph_top_cpu_usage = 0.010;
|
||||
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
|
||||
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
|
||||
|
||||
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
|
||||
shapes.push(Shape::Rect(epaint::RectShape {
|
||||
rect,
|
||||
rounding: style.rounding,
|
||||
fill: ui.visuals().extreme_bg_color,
|
||||
stroke: ui.style().noninteractive().bg_stroke,
|
||||
}));
|
||||
|
||||
let rect = rect.shrink(4.0);
|
||||
let color = ui.visuals().text_color();
|
||||
let line_stroke = Stroke::new(1.0, color);
|
||||
|
||||
if let Some(pointer_pos) = response.hover_pos() {
|
||||
let y = pointer_pos.y;
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(rect.left(), y), pos2(rect.right(), y)],
|
||||
line_stroke,
|
||||
));
|
||||
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
|
||||
let text = format!("{:.1} ms", 1e3 * cpu_usage);
|
||||
shapes.push(Shape::text(
|
||||
&*ui.fonts(),
|
||||
pos2(rect.left(), y),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Monospace.resolve(ui.style()),
|
||||
color,
|
||||
));
|
||||
}
|
||||
|
||||
let circle_color = color;
|
||||
let radius = 2.0;
|
||||
let right_side_time = ui.input().time; // Time at right side of screen
|
||||
|
||||
for (time, cpu_usage) in history.iter() {
|
||||
let age = (right_side_time - time) as f32;
|
||||
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
|
||||
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(pos.x, rect.bottom()), pos],
|
||||
line_stroke,
|
||||
));
|
||||
|
||||
if cpu_usage < graph_top_cpu_usage {
|
||||
shapes.push(Shape::circle_filled(pos, radius, circle_color));
|
||||
}
|
||||
}
|
||||
|
||||
ui.painter().extend(shapes);
|
||||
|
||||
response
|
||||
if cpu_usage < graph_top_cpu_usage {
|
||||
shapes.push(Shape::circle_filled(pos, radius, circle_color));
|
||||
}
|
||||
}
|
||||
|
||||
ui.painter().extend(shapes);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ pub use wrap_app::WrapApp;
|
||||
|
||||
/// Time of day as seconds since midnight. Used for clock in demo app.
|
||||
pub(crate) fn seconds_since_midnight() -> f64 {
|
||||
use chrono::Timelike;
|
||||
let time = chrono::Local::now().time();
|
||||
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
|
||||
use chrono::Timelike;
|
||||
let time = chrono::Local::now().time();
|
||||
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -25,48 +25,48 @@ use eframe::wasm_bindgen::{self, prelude::*};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub struct WebHandle {
|
||||
handle: AppRunnerRef,
|
||||
handle: AppRunnerRef,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
impl WebHandle {
|
||||
#[wasm_bindgen]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let mut app = self.handle.lock();
|
||||
let res = app.destroy();
|
||||
#[wasm_bindgen]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn stop_web(&self) -> Result<(), wasm_bindgen::JsValue> {
|
||||
let mut app = self.handle.lock();
|
||||
let res = app.destroy();
|
||||
|
||||
// let numw = Arc::weak_count(&app);
|
||||
// let nums = Arc::strong_count(&app);
|
||||
// tracing::debug!("runner ref {:?}, {:?}", numw, nums);
|
||||
// let numw = Arc::weak_count(&app);
|
||||
// let nums = Arc::strong_count(&app);
|
||||
// tracing::debug!("runner ref {:?}, {:?}", numw, nums);
|
||||
|
||||
res
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub fn init_wasm_hooks() {
|
||||
// Make sure panics are logged using `console.error`.
|
||||
console_error_panic_hook::set_once();
|
||||
// Make sure panics are logged using `console.error`.
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Redirect tracing to console.log and friends:
|
||||
tracing_wasm::set_as_global_default();
|
||||
// Redirect tracing to console.log and friends:
|
||||
tracing_wasm::set_as_global_default();
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
||||
let web_options = eframe::WebOptions::default();
|
||||
let handle = eframe::start_web(
|
||||
canvas_id,
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
||||
)
|
||||
.map(|handle| WebHandle { handle });
|
||||
let web_options = eframe::WebOptions::default();
|
||||
let handle = eframe::start_web(
|
||||
canvas_id,
|
||||
web_options,
|
||||
Box::new(|cc| Box::new(WrapApp::new(cc))),
|
||||
)
|
||||
.map(|handle| WebHandle { handle });
|
||||
|
||||
handle
|
||||
handle
|
||||
}
|
||||
|
||||
/// This is the entry-point for all the web-assembly.
|
||||
@@ -76,6 +76,6 @@ pub fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValu
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen]
|
||||
pub fn start(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
|
||||
init_wasm_hooks();
|
||||
start_separate(canvas_id)
|
||||
init_wasm_hooks();
|
||||
start_separate(canvas_id)
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
|
||||
// When compiling natively:
|
||||
fn main() {
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
drag_and_drop_support: true,
|
||||
let options = eframe::NativeOptions {
|
||||
drag_and_drop_support: true,
|
||||
|
||||
initial_window_size: Some([1280.0, 1024.0].into()),
|
||||
initial_window_size: Some([1280.0, 1024.0].into()),
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
renderer: eframe::Renderer::Wgpu,
|
||||
#[cfg(feature = "wgpu")]
|
||||
renderer: eframe::Renderer::Wgpu,
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"egui demo app",
|
||||
options,
|
||||
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
|
||||
);
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"egui demo app",
|
||||
options,
|
||||
Box::new(|cc| Box::new(egui_demo_app::WrapApp::new(cc))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ use eframe::glow;
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
struct EasyMarkApp {
|
||||
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
|
||||
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
|
||||
}
|
||||
|
||||
impl eframe::App for EasyMarkApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.editor.panels(ctx);
|
||||
}
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.editor.panels(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -20,13 +20,13 @@ impl eframe::App for EasyMarkApp {
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DemoApp {
|
||||
demo_windows: egui_demo_lib::DemoWindows,
|
||||
demo_windows: egui_demo_lib::DemoWindows,
|
||||
}
|
||||
|
||||
impl eframe::App for DemoApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.demo_windows.ui(ctx);
|
||||
}
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
self.demo_windows.ui(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -34,18 +34,19 @@ impl eframe::App for DemoApp {
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct FractalClockApp {
|
||||
fractal_clock: crate::apps::FractalClock,
|
||||
fractal_clock: crate::apps::FractalClock,
|
||||
}
|
||||
|
||||
impl eframe::App for FractalClockApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
||||
.show(ctx, |ui| {
|
||||
self.fractal_clock
|
||||
.ui(ui, Some(crate::seconds_since_midnight()));
|
||||
});
|
||||
}
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::dark_canvas(&ctx.style()))
|
||||
.show(ctx, |ui| {
|
||||
self
|
||||
.fractal_clock
|
||||
.ui(ui, Some(crate::seconds_since_midnight()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -53,12 +54,12 @@ impl eframe::App for FractalClockApp {
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ColorTestApp {
|
||||
color_test: egui_demo_lib::ColorTest,
|
||||
color_test: egui_demo_lib::ColorTest,
|
||||
}
|
||||
|
||||
impl eframe::App for ColorTestApp {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
if frame.is_web() {
|
||||
ui.label(
|
||||
"NOTE: Some old browsers stuck on WebGL1 without sRGB support will not pass the color test.",
|
||||
@@ -69,7 +70,7 @@ impl eframe::App for ColorTestApp {
|
||||
self.color_test.ui(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -79,307 +80,304 @@ impl eframe::App for ColorTestApp {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct State {
|
||||
demo: DemoApp,
|
||||
easy_mark_editor: EasyMarkApp,
|
||||
#[cfg(feature = "http")]
|
||||
http: crate::apps::HttpApp,
|
||||
clock: FractalClockApp,
|
||||
color_test: ColorTestApp,
|
||||
demo: DemoApp,
|
||||
easy_mark_editor: EasyMarkApp,
|
||||
#[cfg(feature = "http")]
|
||||
http: crate::apps::HttpApp,
|
||||
clock: FractalClockApp,
|
||||
color_test: ColorTestApp,
|
||||
|
||||
selected_anchor: String,
|
||||
backend_panel: super::backend_panel::BackendPanel,
|
||||
selected_anchor: String,
|
||||
backend_panel: super::backend_panel::BackendPanel,
|
||||
}
|
||||
|
||||
unsafe impl Send for WrapApp {}
|
||||
|
||||
/// Wraps many demo/test apps into one.
|
||||
pub struct WrapApp {
|
||||
state: State,
|
||||
state: State,
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
custom3d: crate::apps::Custom3d,
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
custom3d: crate::apps::Custom3d,
|
||||
|
||||
dropped_files: Vec<egui::DroppedFile>,
|
||||
dropped_files: Vec<egui::DroppedFile>,
|
||||
}
|
||||
|
||||
impl WrapApp {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
#[allow(unused_mut)]
|
||||
let mut slf = Self {
|
||||
state: State::default(),
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
#[allow(unused_mut)]
|
||||
let mut slf = Self {
|
||||
state: State::default(),
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
custom3d: crate::apps::Custom3d::new(_cc),
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
custom3d: crate::apps::Custom3d::new(_cc),
|
||||
|
||||
dropped_files: Default::default(),
|
||||
};
|
||||
dropped_files: Default::default(),
|
||||
};
|
||||
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = _cc.storage {
|
||||
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
|
||||
slf.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
slf
|
||||
#[cfg(feature = "persistence")]
|
||||
if let Some(storage) = _cc.storage {
|
||||
if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) {
|
||||
slf.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn eframe::App)> {
|
||||
let mut vec = vec![
|
||||
(
|
||||
"✨ Demos",
|
||||
"demo",
|
||||
&mut self.state.demo as &mut dyn eframe::App,
|
||||
),
|
||||
(
|
||||
"🖹 EasyMark editor",
|
||||
"easymark",
|
||||
&mut self.state.easy_mark_editor as &mut dyn eframe::App,
|
||||
),
|
||||
#[cfg(feature = "http")]
|
||||
(
|
||||
"⬇ HTTP",
|
||||
"http",
|
||||
&mut self.state.http as &mut dyn eframe::App,
|
||||
),
|
||||
(
|
||||
"🕑 Fractal Clock",
|
||||
"clock",
|
||||
&mut self.state.clock as &mut dyn eframe::App,
|
||||
),
|
||||
];
|
||||
slf
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
vec.push((
|
||||
"🔺 3D painting",
|
||||
"custom3d",
|
||||
&mut self.custom3d as &mut dyn eframe::App,
|
||||
));
|
||||
fn apps_iter_mut(&mut self) -> impl Iterator<Item = (&str, &str, &mut dyn eframe::App)> {
|
||||
let mut vec = vec![
|
||||
(
|
||||
"✨ Demos",
|
||||
"demo",
|
||||
&mut self.state.demo as &mut dyn eframe::App,
|
||||
),
|
||||
(
|
||||
"🖹 EasyMark editor",
|
||||
"easymark",
|
||||
&mut self.state.easy_mark_editor as &mut dyn eframe::App,
|
||||
),
|
||||
#[cfg(feature = "http")]
|
||||
(
|
||||
"⬇ HTTP",
|
||||
"http",
|
||||
&mut self.state.http as &mut dyn eframe::App,
|
||||
),
|
||||
(
|
||||
"🕑 Fractal Clock",
|
||||
"clock",
|
||||
&mut self.state.clock as &mut dyn eframe::App,
|
||||
),
|
||||
];
|
||||
|
||||
vec.push((
|
||||
"🎨 Color test",
|
||||
"colors",
|
||||
&mut self.state.color_test as &mut dyn eframe::App,
|
||||
));
|
||||
#[cfg(any(feature = "glow", feature = "wgpu"))]
|
||||
vec.push((
|
||||
"🔺 3D painting",
|
||||
"custom3d",
|
||||
&mut self.custom3d as &mut dyn eframe::App,
|
||||
));
|
||||
|
||||
vec.into_iter()
|
||||
}
|
||||
vec.push((
|
||||
"🎨 Color test",
|
||||
"colors",
|
||||
&mut self.state.color_test as &mut dyn eframe::App,
|
||||
));
|
||||
|
||||
vec.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for WrapApp {
|
||||
#[cfg(feature = "persistence")]
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, eframe::APP_KEY, &self.state);
|
||||
#[cfg(feature = "persistence")]
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, eframe::APP_KEY, &self.state);
|
||||
}
|
||||
|
||||
fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
|
||||
visuals.window_fill().into()
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') {
|
||||
self.state.selected_anchor = anchor.to_owned();
|
||||
}
|
||||
|
||||
fn clear_color(&self, visuals: &egui::Visuals) -> egui::Rgba {
|
||||
visuals.window_fill().into()
|
||||
if self.state.selected_anchor.is_empty() {
|
||||
let selected_anchor = self.apps_iter_mut().next().unwrap().0.to_owned();
|
||||
self.state.selected_anchor = selected_anchor;
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') {
|
||||
self.state.selected_anchor = anchor.to_owned();
|
||||
}
|
||||
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.visuals_mut().button_frame = false;
|
||||
self.bar_contents(ui, frame);
|
||||
});
|
||||
});
|
||||
|
||||
if self.state.selected_anchor.is_empty() {
|
||||
let selected_anchor = self.apps_iter_mut().next().unwrap().0.to_owned();
|
||||
self.state.selected_anchor = selected_anchor;
|
||||
}
|
||||
self.state.backend_panel.update(ctx, frame);
|
||||
|
||||
egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.visuals_mut().button_frame = false;
|
||||
self.bar_contents(ui, frame);
|
||||
});
|
||||
if !is_mobile(ctx) && (self.state.backend_panel.open || ctx.memory().everything_is_visible()) {
|
||||
egui::SidePanel::left("backend_panel")
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("💻 Backend");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
self.backend_panel_contents(ui, frame);
|
||||
});
|
||||
|
||||
self.state.backend_panel.update(ctx, frame);
|
||||
|
||||
if !is_mobile(ctx)
|
||||
&& (self.state.backend_panel.open || ctx.memory().everything_is_visible())
|
||||
{
|
||||
egui::SidePanel::left("backend_panel")
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("💻 Backend");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
self.backend_panel_contents(ui, frame);
|
||||
});
|
||||
}
|
||||
|
||||
self.show_selected_app(ctx, frame);
|
||||
|
||||
self.state.backend_panel.end_of_frame(ctx);
|
||||
|
||||
self.ui_file_drag_and_drop(ctx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
fn on_exit(&mut self, gl: Option<&glow::Context>) {
|
||||
self.custom3d.on_exit(gl);
|
||||
}
|
||||
self.show_selected_app(ctx, frame);
|
||||
|
||||
self.state.backend_panel.end_of_frame(ctx);
|
||||
|
||||
self.ui_file_drag_and_drop(ctx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "glow")]
|
||||
fn on_exit(&mut self, gl: Option<&glow::Context>) {
|
||||
self.custom3d.on_exit(gl);
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapApp {
|
||||
fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
self.state.backend_panel.ui(ui, frame);
|
||||
fn backend_panel_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
self.state.backend_panel.ui(ui, frame);
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.button("Reset egui")
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.button("Reset egui")
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
if ui.button("Reset everything").clicked() {
|
||||
self.state = Default::default();
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
if ui.button("Reset everything").clicked() {
|
||||
self.state = Default::default();
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn show_selected_app(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let mut found_anchor = false;
|
||||
let selected_anchor = self.state.selected_anchor.clone();
|
||||
for (_name, anchor, app) in self.apps_iter_mut() {
|
||||
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
|
||||
app.update(ctx, frame);
|
||||
found_anchor = true;
|
||||
}
|
||||
}
|
||||
if !found_anchor {
|
||||
self.state.selected_anchor = "demo".into();
|
||||
}
|
||||
}
|
||||
|
||||
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::widgets::global_dark_light_mode_switch(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
if is_mobile(ui.ctx()) {
|
||||
ui.menu_button("💻 Backend", |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.backend_panel_contents(ui, frame);
|
||||
});
|
||||
} else {
|
||||
ui.toggle_value(&mut self.state.backend_panel.open, "💻 Backend");
|
||||
}
|
||||
|
||||
fn show_selected_app(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let mut found_anchor = false;
|
||||
let selected_anchor = self.state.selected_anchor.clone();
|
||||
for (_name, anchor, app) in self.apps_iter_mut() {
|
||||
if anchor == selected_anchor || ctx.memory().everything_is_visible() {
|
||||
app.update(ctx, frame);
|
||||
found_anchor = true;
|
||||
}
|
||||
}
|
||||
if !found_anchor {
|
||||
self.state.selected_anchor = "demo".into();
|
||||
ui.separator();
|
||||
|
||||
let mut selected_anchor = self.state.selected_anchor.clone();
|
||||
for (name, anchor, _app) in self.apps_iter_mut() {
|
||||
if ui
|
||||
.selectable_label(selected_anchor == anchor, name)
|
||||
.clicked()
|
||||
{
|
||||
selected_anchor = anchor.to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url(format!("#{}", anchor));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state.selected_anchor = selected_anchor;
|
||||
|
||||
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
egui::widgets::global_dark_light_mode_switch(ui);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if false {
|
||||
// TODO(emilk): fix the overlap on small screens
|
||||
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
||||
self.state.selected_anchor = "clock".to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url("#clock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
egui::warn_if_debug_build(ui);
|
||||
});
|
||||
}
|
||||
|
||||
if is_mobile(ui.ctx()) {
|
||||
ui.menu_button("💻 Backend", |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.backend_panel_contents(ui, frame);
|
||||
});
|
||||
fn ui_file_drag_and_drop(&mut self, ctx: &egui::Context) {
|
||||
use egui::*;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
// Preview hovering files:
|
||||
if !ctx.input().raw.hovered_files.is_empty() {
|
||||
let mut text = "Dropping files:\n".to_owned();
|
||||
for file in &ctx.input().raw.hovered_files {
|
||||
if let Some(path) = &file.path {
|
||||
write!(text, "\n{}", path.display()).ok();
|
||||
} else if !file.mime.is_empty() {
|
||||
write!(text, "\n{}", file.mime).ok();
|
||||
} else {
|
||||
ui.toggle_value(&mut self.state.backend_panel.open, "💻 Backend");
|
||||
text += "\n???";
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
let painter = ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
|
||||
|
||||
let mut selected_anchor = self.state.selected_anchor.clone();
|
||||
for (name, anchor, _app) in self.apps_iter_mut() {
|
||||
if ui
|
||||
.selectable_label(selected_anchor == anchor, name)
|
||||
.clicked()
|
||||
{
|
||||
selected_anchor = anchor.to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url(format!("#{}", anchor));
|
||||
}
|
||||
let screen_rect = ctx.input().screen_rect();
|
||||
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
|
||||
painter.text(
|
||||
screen_rect.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
text,
|
||||
TextStyle::Heading.resolve(&ctx.style()),
|
||||
Color32::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect dropped files:
|
||||
if !ctx.input().raw.dropped_files.is_empty() {
|
||||
self.dropped_files = ctx.input().raw.dropped_files.clone();
|
||||
}
|
||||
|
||||
// Show dropped files (if any):
|
||||
if !self.dropped_files.is_empty() {
|
||||
let mut open = true;
|
||||
egui::Window::new("Dropped files")
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
for file in &self.dropped_files {
|
||||
let mut info = if let Some(path) = &file.path {
|
||||
path.display().to_string()
|
||||
} else if !file.name.is_empty() {
|
||||
file.name.clone()
|
||||
} else {
|
||||
"???".to_owned()
|
||||
};
|
||||
if let Some(bytes) = &file.bytes {
|
||||
write!(info, " ({} bytes)", bytes.len()).ok();
|
||||
}
|
||||
}
|
||||
self.state.selected_anchor = selected_anchor;
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if false {
|
||||
// TODO(emilk): fix the overlap on small screens
|
||||
if clock_button(ui, crate::seconds_since_midnight()).clicked() {
|
||||
self.state.selected_anchor = "clock".to_owned();
|
||||
if frame.is_web() {
|
||||
ui.output().open_url("#clock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
egui::warn_if_debug_build(ui);
|
||||
ui.label(info);
|
||||
}
|
||||
});
|
||||
if !open {
|
||||
self.dropped_files.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_file_drag_and_drop(&mut self, ctx: &egui::Context) {
|
||||
use egui::*;
|
||||
use std::fmt::Write as _;
|
||||
|
||||
// Preview hovering files:
|
||||
if !ctx.input().raw.hovered_files.is_empty() {
|
||||
let mut text = "Dropping files:\n".to_owned();
|
||||
for file in &ctx.input().raw.hovered_files {
|
||||
if let Some(path) = &file.path {
|
||||
write!(text, "\n{}", path.display()).ok();
|
||||
} else if !file.mime.is_empty() {
|
||||
write!(text, "\n{}", file.mime).ok();
|
||||
} else {
|
||||
text += "\n???";
|
||||
}
|
||||
}
|
||||
|
||||
let painter =
|
||||
ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
|
||||
|
||||
let screen_rect = ctx.input().screen_rect();
|
||||
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
|
||||
painter.text(
|
||||
screen_rect.center(),
|
||||
Align2::CENTER_CENTER,
|
||||
text,
|
||||
TextStyle::Heading.resolve(&ctx.style()),
|
||||
Color32::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
// Collect dropped files:
|
||||
if !ctx.input().raw.dropped_files.is_empty() {
|
||||
self.dropped_files = ctx.input().raw.dropped_files.clone();
|
||||
}
|
||||
|
||||
// Show dropped files (if any):
|
||||
if !self.dropped_files.is_empty() {
|
||||
let mut open = true;
|
||||
egui::Window::new("Dropped files")
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
for file in &self.dropped_files {
|
||||
let mut info = if let Some(path) = &file.path {
|
||||
path.display().to_string()
|
||||
} else if !file.name.is_empty() {
|
||||
file.name.clone()
|
||||
} else {
|
||||
"???".to_owned()
|
||||
};
|
||||
if let Some(bytes) = &file.bytes {
|
||||
write!(info, " ({} bytes)", bytes.len()).ok();
|
||||
}
|
||||
ui.label(info);
|
||||
}
|
||||
});
|
||||
if !open {
|
||||
self.dropped_files.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Response {
|
||||
let time = seconds_since_midnight;
|
||||
let time = format!(
|
||||
"{:02}:{:02}:{:02}.{:02}",
|
||||
(time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(time % (60.0 * 60.0) / 60.0).floor(),
|
||||
(time % 60.0).floor(),
|
||||
(time % 1.0 * 100.0).floor()
|
||||
);
|
||||
let time = seconds_since_midnight;
|
||||
let time = format!(
|
||||
"{:02}:{:02}:{:02}.{:02}",
|
||||
(time % (24.0 * 60.0 * 60.0) / 3600.0).floor(),
|
||||
(time % (60.0 * 60.0) / 60.0).floor(),
|
||||
(time % 60.0).floor(),
|
||||
(time % 1.0 * 100.0).floor()
|
||||
);
|
||||
|
||||
ui.button(egui::RichText::new(time).monospace())
|
||||
ui.button(egui::RichText::new(time).monospace())
|
||||
}
|
||||
|
||||
@@ -4,142 +4,138 @@ use egui::epaint::TextShape;
|
||||
use egui_demo_lib::LOREM_IPSUM_LONG;
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
use egui::RawInput;
|
||||
use egui::RawInput;
|
||||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
|
||||
// The most end-to-end benchmark.
|
||||
c.bench_function("demo_with_tessellate__realistic", |b| {
|
||||
b.iter(|| {
|
||||
let full_output = ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
ctx.tessellate(full_output.shapes)
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("demo_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
})
|
||||
});
|
||||
});
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
|
||||
// The most end-to-end benchmark.
|
||||
c.bench_function("demo_with_tessellate__realistic", |b| {
|
||||
b.iter(|| {
|
||||
let full_output = ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
c.bench_function("demo_only_tessellate", |b| {
|
||||
b.iter(|| ctx.tessellate(full_output.shapes.clone()));
|
||||
});
|
||||
}
|
||||
ctx.tessellate(full_output.shapes)
|
||||
});
|
||||
});
|
||||
|
||||
if false {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.memory().set_everything_is_visible(true); // give us everything
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
c.bench_function("demo_full_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
c.bench_function("demo_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let full_output = ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
c.bench_function("demo_only_tessellate", |b| {
|
||||
b.iter(|| ctx.tessellate(full_output.shapes.clone()));
|
||||
});
|
||||
}
|
||||
|
||||
if false {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.memory().set_everything_is_visible(true); // give us everything
|
||||
let mut demo_windows = egui_demo_lib::DemoWindows::default();
|
||||
c.bench_function("demo_full_no_tessellate", |b| {
|
||||
b.iter(|| {
|
||||
ctx.run(RawInput::default(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let _ = ctx.run(RawInput::default(), |ctx| {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
c.bench_function("label &str", |b| {
|
||||
b.iter(|| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog");
|
||||
});
|
||||
});
|
||||
c.bench_function("label format!", |b| {
|
||||
b.iter(|| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog".to_owned());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
ctx.begin_frame(RawInput::default());
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
c.bench_function("Painter::rect", |b| {
|
||||
let painter = ui.painter();
|
||||
let rect = ui.max_rect();
|
||||
b.iter(|| {
|
||||
painter.rect(rect, 2.0, egui::Color32::RED, (1.0, egui::Color32::WHITE));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Don't call `end_frame` to not have to drain the huge paint list
|
||||
}
|
||||
|
||||
{
|
||||
let pixels_per_point = 1.0;
|
||||
let max_texture_side = 8 * 1024;
|
||||
let wrap_width = 512.0;
|
||||
let font_id = egui::FontId::default();
|
||||
let color = egui::Color32::WHITE;
|
||||
let fonts = egui::epaint::text::Fonts::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
egui::FontDefinitions::default(),
|
||||
);
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
let _ = ctx.run(RawInput::default(), |ctx| {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
c.bench_function("label &str", |b| {
|
||||
b.iter(|| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog");
|
||||
});
|
||||
});
|
||||
c.bench_function("label format!", |b| {
|
||||
b.iter(|| {
|
||||
ui.label("the quick brown fox jumps over the lazy dog".to_owned());
|
||||
});
|
||||
});
|
||||
});
|
||||
let mut locked_fonts = fonts.lock();
|
||||
c.bench_function("text_layout_uncached", |b| {
|
||||
b.iter(|| {
|
||||
use egui::epaint::text::{layout, LayoutJob};
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id.clone(),
|
||||
color,
|
||||
wrap_width,
|
||||
);
|
||||
layout(&mut locked_fonts.fonts, job.into())
|
||||
});
|
||||
});
|
||||
}
|
||||
c.bench_function("text_layout_cached", |b| {
|
||||
b.iter(|| {
|
||||
fonts.layout(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id.clone(),
|
||||
color,
|
||||
wrap_width,
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
let ctx = egui::Context::default();
|
||||
ctx.begin_frame(RawInput::default());
|
||||
|
||||
egui::CentralPanel::default().show(&ctx, |ui| {
|
||||
c.bench_function("Painter::rect", |b| {
|
||||
let painter = ui.painter();
|
||||
let rect = ui.max_rect();
|
||||
b.iter(|| {
|
||||
painter.rect(rect, 2.0, egui::Color32::RED, (1.0, egui::Color32::WHITE));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Don't call `end_frame` to not have to drain the huge paint list
|
||||
}
|
||||
|
||||
{
|
||||
let pixels_per_point = 1.0;
|
||||
let max_texture_side = 8 * 1024;
|
||||
let wrap_width = 512.0;
|
||||
let font_id = egui::FontId::default();
|
||||
let color = egui::Color32::WHITE;
|
||||
let fonts = egui::epaint::text::Fonts::new(
|
||||
pixels_per_point,
|
||||
max_texture_side,
|
||||
egui::FontDefinitions::default(),
|
||||
);
|
||||
{
|
||||
let mut locked_fonts = fonts.lock();
|
||||
c.bench_function("text_layout_uncached", |b| {
|
||||
b.iter(|| {
|
||||
use egui::epaint::text::{layout, LayoutJob};
|
||||
|
||||
let job = LayoutJob::simple(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id.clone(),
|
||||
color,
|
||||
wrap_width,
|
||||
);
|
||||
layout(&mut locked_fonts.fonts, job.into())
|
||||
});
|
||||
});
|
||||
}
|
||||
c.bench_function("text_layout_cached", |b| {
|
||||
b.iter(|| {
|
||||
fonts.layout(
|
||||
LOREM_IPSUM_LONG.to_owned(),
|
||||
font_id.clone(),
|
||||
color,
|
||||
wrap_width,
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
|
||||
let font_image_size = fonts.font_image_size();
|
||||
let prepared_discs = fonts.texture_atlas().lock().prepared_discs();
|
||||
let mut tessellator = egui::epaint::Tessellator::new(
|
||||
1.0,
|
||||
Default::default(),
|
||||
font_image_size,
|
||||
prepared_discs,
|
||||
);
|
||||
let mut mesh = egui::epaint::Mesh::default();
|
||||
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
|
||||
c.bench_function("tessellate_text", |b| {
|
||||
b.iter(|| {
|
||||
tessellator.tessellate_text(&text_shape, &mut mesh);
|
||||
mesh.clear();
|
||||
});
|
||||
});
|
||||
}
|
||||
let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
|
||||
let font_image_size = fonts.font_image_size();
|
||||
let prepared_discs = fonts.texture_atlas().lock().prepared_discs();
|
||||
let mut tessellator =
|
||||
egui::epaint::Tessellator::new(1.0, Default::default(), font_image_size, prepared_discs);
|
||||
let mut mesh = egui::epaint::Mesh::default();
|
||||
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
|
||||
c.bench_function("tessellate_text", |b| {
|
||||
b.iter(|| {
|
||||
tessellator.tessellate_text(&text_shape, &mut mesh);
|
||||
mesh.clear();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
|
||||
@@ -12,463 +12,463 @@ const WHITE: Color32 = Color32::WHITE;
|
||||
/// A test for sanity-checking and diagnosing egui rendering backends.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ColorTest {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
tex_mngr: TextureManager,
|
||||
vertex_gradients: bool,
|
||||
texture_gradients: bool,
|
||||
srgb: bool,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
tex_mngr: TextureManager,
|
||||
vertex_gradients: bool,
|
||||
texture_gradients: bool,
|
||||
srgb: bool,
|
||||
}
|
||||
|
||||
impl Default for ColorTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tex_mngr: Default::default(),
|
||||
vertex_gradients: true,
|
||||
texture_gradients: true,
|
||||
srgb: false,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tex_mngr: Default::default(),
|
||||
vertex_gradients: true,
|
||||
texture_gradients: true,
|
||||
srgb: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorTest {
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.set_max_width(680.0);
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.set_max_width(680.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
ui.label("This is made to test that the egui painter backend is set up correctly, so that all colors are interpolated and blended in linear space with premultiplied alpha.");
|
||||
ui.label("If everything is set up correctly, all groups of gradients will look uniform");
|
||||
ui.label("This is made to test that the egui painter backend is set up correctly, so that all colors are interpolated and blended in linear space with premultiplied alpha.");
|
||||
ui.label("If everything is set up correctly, all groups of gradients will look uniform");
|
||||
|
||||
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
|
||||
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
|
||||
ui.checkbox(&mut self.srgb, "Show naive sRGBA horror");
|
||||
ui.checkbox(&mut self.vertex_gradients, "Vertex gradients");
|
||||
ui.checkbox(&mut self.texture_gradients, "Texture gradients");
|
||||
ui.checkbox(&mut self.srgb, "Show naive sRGBA horror");
|
||||
|
||||
ui.heading("sRGB color test");
|
||||
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
|
||||
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
|
||||
self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
|
||||
});
|
||||
ui.heading("sRGB color test");
|
||||
ui.label("Use a color picker to ensure this color is (255, 165, 0) / #ffa500");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
|
||||
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
|
||||
self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
ui.label("Test that vertex color times texture color is done in linear space:");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
ui.label("Test that vertex color times texture color is done in linear space:");
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
|
||||
let tex_color = Rgba::from_rgb(1.0, 0.25, 0.25);
|
||||
let vertex_color = Rgba::from_rgb(0.5, 0.75, 0.75);
|
||||
let tex_color = Rgba::from_rgb(1.0, 0.25, 0.25);
|
||||
let vertex_color = Rgba::from_rgb(0.5, 0.75, 0.75);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
ui.label("texture");
|
||||
show_color(ui, tex_color, color_size);
|
||||
ui.label(" * ");
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
{
|
||||
let g = Gradient::one_color(Color32::from(tex_color * vertex_color));
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
ui.label("texture");
|
||||
show_color(ui, tex_color, color_size);
|
||||
ui.label(" * ");
|
||||
show_color(ui, vertex_color, color_size);
|
||||
ui.label(" vertex color =");
|
||||
});
|
||||
{
|
||||
let g = Gradient::one_color(Color32::from(tex_color * vertex_color));
|
||||
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
|
||||
self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let g = Gradient::one_color(Color32::from(tex_color));
|
||||
let tex = self.tex_mngr.get(ui.ctx(), &g);
|
||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.label("GPU result");
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let g = Gradient::one_color(Color32::from(tex_color));
|
||||
let tex = self.tex_mngr.get(ui.ctx(), &g);
|
||||
let texel_offset = 0.5 / (g.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
|
||||
.on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
|
||||
ui.label("GPU result");
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
// TODO(emilk): test color multiplication (image tint),
|
||||
// to make sure vertex and texture color multiplication is done in linear space.
|
||||
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN));
|
||||
if self.srgb {
|
||||
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
|
||||
}
|
||||
self.show_gradients(ui, WHITE, (RED, GREEN));
|
||||
if self.srgb {
|
||||
ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, RED, (TRANSPARENT, GREEN));
|
||||
self.show_gradients(ui, RED, (TRANSPARENT, GREEN));
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN));
|
||||
if self.srgb {
|
||||
ui.label(
|
||||
self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN));
|
||||
if self.srgb {
|
||||
ui.label(
|
||||
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
self.show_gradients(ui, BLACK, (BLACK, WHITE));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, BLACK, (BLACK, WHITE));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, WHITE, (BLACK, TRANSPARENT));
|
||||
ui.separator();
|
||||
self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE));
|
||||
ui.separator();
|
||||
|
||||
ui.label("Additive blending: add more and more blue to the red background:");
|
||||
self.show_gradients(
|
||||
ui,
|
||||
RED,
|
||||
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
|
||||
ui.label("Additive blending: add more and more blue to the red background:");
|
||||
self.show_gradients(
|
||||
ui,
|
||||
RED,
|
||||
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
pixel_test(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
blending_and_feathering_test(ui);
|
||||
}
|
||||
|
||||
fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) {
|
||||
let is_opaque = left.is_opaque() && right.is_opaque();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
if !is_opaque {
|
||||
ui.label("Background:");
|
||||
show_color(ui, bg_fill, color_size);
|
||||
}
|
||||
ui.label("gradient");
|
||||
show_color(ui, left, color_size);
|
||||
ui.label("-");
|
||||
show_color(ui, right, color_size);
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
if is_opaque {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
|
||||
} else {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - vertices",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
let g = Gradient::texture_gradient(left, right);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh of width 2 (test vertex decode and interpolation)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
|
||||
pixel_test(ui);
|
||||
if self.srgb {
|
||||
let g = Gradient::ground_truth_bad_srgba_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh with naive sRGBA interpolation (WRONG)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
blending_and_feathering_test(ui);
|
||||
fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.texture_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
|
||||
fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) {
|
||||
let is_opaque = left.is_opaque() && right.is_opaque();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let color_size = ui.spacing().interact_size;
|
||||
if !is_opaque {
|
||||
ui.label("Background:");
|
||||
show_color(ui, bg_fill, color_size);
|
||||
}
|
||||
ui.label("gradient");
|
||||
show_color(ui, left, color_size);
|
||||
ui.label("-");
|
||||
show_color(ui, right, color_size);
|
||||
});
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
|
||||
if is_opaque {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
|
||||
} else {
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - vertices",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(
|
||||
ui,
|
||||
"Ground Truth (CPU gradient, CPU blending) - texture",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
let g = Gradient::ground_truth_linear_gradient(left, right);
|
||||
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
|
||||
self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
|
||||
}
|
||||
|
||||
let g = Gradient::texture_gradient(left, right);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh of width 2 (test vertex decode and interpolation)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
|
||||
|
||||
if self.srgb {
|
||||
let g =
|
||||
Gradient::ground_truth_bad_srgba_gradient(left, right).with_bg_fill(bg_fill);
|
||||
self.vertex_gradient(
|
||||
ui,
|
||||
"Triangle mesh with naive sRGBA interpolation (WRONG)",
|
||||
bg_fill,
|
||||
&g,
|
||||
);
|
||||
self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.texture_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let tex = self.tex_mngr.get(ui.ctx(), gradient);
|
||||
let texel_offset = 0.5 / (gradient.0.len() as f32);
|
||||
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
|
||||
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
|
||||
.on_hover_text(format!(
|
||||
"A texture that is {} texels wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
|
||||
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.vertex_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!(
|
||||
"A triangle mesh that is {} vertices wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
|
||||
if !self.vertex_gradients {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
vertex_gradient(ui, bg_fill, gradient).on_hover_text(format!(
|
||||
"A triangle mesh that is {} vertices wide",
|
||||
gradient.0.len()
|
||||
));
|
||||
ui.label(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response {
|
||||
use egui::epaint::*;
|
||||
let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover());
|
||||
if bg_fill != Default::default() {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, bg_fill);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
use egui::epaint::*;
|
||||
let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover());
|
||||
if bg_fill != Default::default() {
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, bg_fill);
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
let mut mesh = Mesh::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
let x = lerp(rect.x_range(), t);
|
||||
mesh.colored_vertex(pos2(x, rect.top()), color);
|
||||
mesh.colored_vertex(pos2(x, rect.bottom()), color);
|
||||
if i < n - 1 {
|
||||
let i = i as u32;
|
||||
mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
|
||||
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
|
||||
}
|
||||
}
|
||||
{
|
||||
let n = gradient.0.len();
|
||||
assert!(n >= 2);
|
||||
let mut mesh = Mesh::default();
|
||||
for (i, &color) in gradient.0.iter().enumerate() {
|
||||
let t = i as f32 / (n as f32 - 1.0);
|
||||
let x = lerp(rect.x_range(), t);
|
||||
mesh.colored_vertex(pos2(x, rect.top()), color);
|
||||
mesh.colored_vertex(pos2(x, rect.bottom()), color);
|
||||
if i < n - 1 {
|
||||
let i = i as u32;
|
||||
mesh.add_triangle(2 * i, 2 * i + 1, 2 * i + 2);
|
||||
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
|
||||
}
|
||||
}
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
response
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
struct Gradient(pub Vec<Color32>);
|
||||
|
||||
impl Gradient {
|
||||
pub fn one_color(srgba: Color32) -> Self {
|
||||
Self(vec![srgba, srgba])
|
||||
}
|
||||
pub fn one_color(srgba: Color32) -> Self {
|
||||
Self(vec![srgba, srgba])
|
||||
}
|
||||
|
||||
pub fn texture_gradient(left: Color32, right: Color32) -> Self {
|
||||
Self(vec![left, right])
|
||||
}
|
||||
pub fn texture_gradient(left: Color32, right: Color32) -> Self {
|
||||
Self(vec![left, right])
|
||||
}
|
||||
|
||||
pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self {
|
||||
let left = Rgba::from(left);
|
||||
let right = Rgba::from(right);
|
||||
pub fn ground_truth_linear_gradient(left: Color32, right: Color32) -> Self {
|
||||
let left = Rgba::from(left);
|
||||
let right = Rgba::from(right);
|
||||
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Color32::from(lerp(left..=right, t))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Color32::from(lerp(left..=right, t))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// This is how a bad person blends `sRGBA`
|
||||
pub fn ground_truth_bad_srgba_gradient(left: Color32, right: Color32) -> Self {
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Color32::from_rgba_premultiplied(
|
||||
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, // Don't ever do this please!
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
/// This is how a bad person blends `sRGBA`
|
||||
pub fn ground_truth_bad_srgba_gradient(left: Color32, right: Color32) -> Self {
|
||||
let n = 255;
|
||||
Self(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f32 / n as f32;
|
||||
Color32::from_rgba_premultiplied(
|
||||
lerp((left[0] as f32)..=(right[0] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[1] as f32)..=(right[1] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[2] as f32)..=(right[2] as f32), t).round() as u8, // Don't ever do this please!
|
||||
lerp((left[3] as f32)..=(right[3] as f32), t).round() as u8, // Don't ever do this please!
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
|
||||
pub fn with_bg_fill(self, bg: Color32) -> Self {
|
||||
let bg = Rgba::from(bg);
|
||||
Self(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|fg| {
|
||||
let fg = Rgba::from(fg);
|
||||
Color32::from(bg * (1.0 - fg.a()) + fg)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
/// Do premultiplied alpha-aware blending of the gradient on top of the fill color
|
||||
pub fn with_bg_fill(self, bg: Color32) -> Self {
|
||||
let bg = Rgba::from(bg);
|
||||
Self(
|
||||
self
|
||||
.0
|
||||
.into_iter()
|
||||
.map(|fg| {
|
||||
let fg = Rgba::from(fg);
|
||||
Color32::from(bg * (1.0 - fg.a()) + fg)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_pixel_row(&self) -> Vec<Color32> {
|
||||
self.0.clone()
|
||||
}
|
||||
pub fn to_pixel_row(&self) -> Vec<Color32> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TextureManager(HashMap<Gradient, TextureHandle>);
|
||||
|
||||
impl TextureManager {
|
||||
fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
|
||||
self.0.entry(gradient.clone()).or_insert_with(|| {
|
||||
let pixels = gradient.to_pixel_row();
|
||||
let width = pixels.len();
|
||||
let height = 1;
|
||||
ctx.load_texture(
|
||||
"color_test_gradient",
|
||||
epaint::ColorImage {
|
||||
size: [width, height],
|
||||
pixels,
|
||||
},
|
||||
TextureFilter::Linear,
|
||||
)
|
||||
})
|
||||
}
|
||||
fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
|
||||
self.0.entry(gradient.clone()).or_insert_with(|| {
|
||||
let pixels = gradient.to_pixel_row();
|
||||
let width = pixels.len();
|
||||
let height = 1;
|
||||
ctx.load_texture(
|
||||
"color_test_gradient",
|
||||
epaint::ColorImage {
|
||||
size: [width, height],
|
||||
pixels,
|
||||
},
|
||||
TextureFilter::Linear,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_test(ui: &mut Ui) {
|
||||
ui.label("Each subsequent square should be one physical pixel larger than the previous. They should be exactly one physical pixel apart. They should be perfectly aligned to the pixel grid.");
|
||||
ui.label("Each subsequent square should be one physical pixel larger than the previous. They should be exactly one physical pixel apart. They should be perfectly aligned to the pixel grid.");
|
||||
|
||||
let color = if ui.style().visuals.dark_mode {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::BLACK
|
||||
};
|
||||
let color = if ui.style().visuals.dark_mode {
|
||||
egui::Color32::WHITE
|
||||
} else {
|
||||
egui::Color32::BLACK
|
||||
};
|
||||
|
||||
let pixels_per_point = ui.ctx().pixels_per_point();
|
||||
let num_squares: u32 = 8;
|
||||
let size_pixels = vec2(
|
||||
((num_squares + 1) * (num_squares + 2) / 2) as f32,
|
||||
num_squares as f32,
|
||||
let pixels_per_point = ui.ctx().pixels_per_point();
|
||||
let num_squares: u32 = 8;
|
||||
let size_pixels = vec2(
|
||||
((num_squares + 1) * (num_squares + 2) / 2) as f32,
|
||||
num_squares as f32,
|
||||
);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
let mut cursor_pixel = Pos2::new(
|
||||
response.rect.min.x * pixels_per_point,
|
||||
response.rect.min.y * pixels_per_point,
|
||||
)
|
||||
.ceil();
|
||||
for size in 1..=num_squares {
|
||||
let rect_points = Rect::from_min_size(
|
||||
Pos2::new(
|
||||
cursor_pixel.x / pixels_per_point,
|
||||
cursor_pixel.y / pixels_per_point,
|
||||
),
|
||||
Vec2::splat(size as f32) / pixels_per_point,
|
||||
);
|
||||
let size_points = size_pixels / pixels_per_point + Vec2::splat(2.0);
|
||||
let (response, painter) = ui.allocate_painter(size_points, Sense::hover());
|
||||
|
||||
let mut cursor_pixel = Pos2::new(
|
||||
response.rect.min.x * pixels_per_point,
|
||||
response.rect.min.y * pixels_per_point,
|
||||
)
|
||||
.ceil();
|
||||
for size in 1..=num_squares {
|
||||
let rect_points = Rect::from_min_size(
|
||||
Pos2::new(
|
||||
cursor_pixel.x / pixels_per_point,
|
||||
cursor_pixel.y / pixels_per_point,
|
||||
),
|
||||
Vec2::splat(size as f32) / pixels_per_point,
|
||||
);
|
||||
painter.rect_filled(rect_points, 0.0, color);
|
||||
cursor_pixel.x += (1 + size) as f32;
|
||||
}
|
||||
painter.rect_filled(rect_points, 0.0, color);
|
||||
cursor_pixel.x += (1 + size) as f32;
|
||||
}
|
||||
}
|
||||
|
||||
fn blending_and_feathering_test(ui: &mut Ui) {
|
||||
ui.label("Some fine lines for testing anti-aliasing and blending:");
|
||||
ui.label("Some fine lines for testing anti-aliasing and blending:");
|
||||
|
||||
let size = vec2(512.0, 512.0);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
let size = vec2(512.0, 512.0);
|
||||
let (response, painter) = ui.allocate_painter(size, Sense::hover());
|
||||
let rect = response.rect;
|
||||
|
||||
let mut top_half = rect;
|
||||
top_half.set_bottom(top_half.center().y);
|
||||
painter.rect_filled(top_half, 0.0, Color32::BLACK);
|
||||
paint_fine_lines_and_text(&painter, top_half, Color32::WHITE);
|
||||
let mut top_half = rect;
|
||||
top_half.set_bottom(top_half.center().y);
|
||||
painter.rect_filled(top_half, 0.0, Color32::BLACK);
|
||||
paint_fine_lines_and_text(&painter, top_half, Color32::WHITE);
|
||||
|
||||
let mut bottom_half = rect;
|
||||
bottom_half.set_top(bottom_half.center().y);
|
||||
painter.rect_filled(bottom_half, 0.0, Color32::WHITE);
|
||||
paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK);
|
||||
let mut bottom_half = rect;
|
||||
bottom_half.set_top(bottom_half.center().y);
|
||||
painter.rect_filled(bottom_half, 0.0, Color32::WHITE);
|
||||
paint_fine_lines_and_text(&painter, bottom_half, Color32::BLACK);
|
||||
}
|
||||
|
||||
fn paint_fine_lines_and_text(painter: &egui::Painter, mut rect: Rect, color: Color32) {
|
||||
{
|
||||
let mut x = 0.0;
|
||||
for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] {
|
||||
painter.text(
|
||||
rect.center_top() + vec2(0.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% white", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::WHITE.linear_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(80.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% gray", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::GRAY.linear_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(160.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% black", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::BLACK.linear_multiply(opacity),
|
||||
);
|
||||
x += 20.0;
|
||||
}
|
||||
{
|
||||
let mut x = 0.0;
|
||||
for opacity in [1.00, 0.50, 0.25, 0.10, 0.05, 0.02, 0.01, 0.00] {
|
||||
painter.text(
|
||||
rect.center_top() + vec2(0.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% white", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::WHITE.linear_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(80.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% gray", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::GRAY.linear_multiply(opacity),
|
||||
);
|
||||
painter.text(
|
||||
rect.center_top() + vec2(160.0, x),
|
||||
Align2::LEFT_TOP,
|
||||
format!("{:.0}% black", 100.0 * opacity),
|
||||
FontId::proportional(16.0),
|
||||
Color32::BLACK.linear_multiply(opacity),
|
||||
);
|
||||
x += 20.0;
|
||||
}
|
||||
}
|
||||
|
||||
rect.max.x = rect.center().x;
|
||||
rect.max.x = rect.center().x;
|
||||
|
||||
rect = rect.shrink(12.0);
|
||||
for width in [0.5, 1.0, 2.0] {
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::CENTER_CENTER,
|
||||
width.to_string(),
|
||||
FontId::monospace(14.0),
|
||||
color,
|
||||
);
|
||||
|
||||
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(
|
||||
[
|
||||
rect.left_top() + vec2(16.0, 0.0),
|
||||
rect.right_top(),
|
||||
rect.right_center(),
|
||||
rect.right_bottom(),
|
||||
],
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
Stroke::new(width, color),
|
||||
));
|
||||
|
||||
rect.min.y += 32.0;
|
||||
rect.max.x -= 32.0;
|
||||
}
|
||||
|
||||
rect.min.y += 16.0;
|
||||
rect = rect.shrink(12.0);
|
||||
for width in [0.5, 1.0, 2.0] {
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::LEFT_CENTER,
|
||||
"transparent --> opaque",
|
||||
FontId::monospace(11.0),
|
||||
color,
|
||||
rect.left_top(),
|
||||
Align2::CENTER_CENTER,
|
||||
width.to_string(),
|
||||
FontId::monospace(14.0),
|
||||
color,
|
||||
);
|
||||
rect.min.y += 12.0;
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.right_bottom(), color);
|
||||
mesh.colored_vertex(rect.right_top(), color);
|
||||
mesh.add_triangle(0, 1, 2);
|
||||
mesh.add_triangle(1, 2, 3);
|
||||
painter.add(mesh);
|
||||
|
||||
painter.add(egui::epaint::CubicBezierShape::from_points_stroke(
|
||||
[
|
||||
rect.left_top() + vec2(16.0, 0.0),
|
||||
rect.right_top(),
|
||||
rect.right_center(),
|
||||
rect.right_bottom(),
|
||||
],
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
Stroke::new(width, color),
|
||||
));
|
||||
|
||||
rect.min.y += 32.0;
|
||||
rect.max.x -= 32.0;
|
||||
}
|
||||
|
||||
rect.min.y += 16.0;
|
||||
painter.text(
|
||||
rect.left_top(),
|
||||
Align2::LEFT_CENTER,
|
||||
"transparent --> opaque",
|
||||
FontId::monospace(11.0),
|
||||
color,
|
||||
);
|
||||
rect.min.y += 12.0;
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.colored_vertex(rect.left_bottom(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.left_top(), Color32::TRANSPARENT);
|
||||
mesh.colored_vertex(rect.right_bottom(), color);
|
||||
mesh.colored_vertex(rect.right_top(), color);
|
||||
mesh.add_triangle(0, 1, 2);
|
||||
mesh.add_triangle(1, 2, 3);
|
||||
painter.add(mesh);
|
||||
}
|
||||
|
||||
@@ -4,27 +4,27 @@
|
||||
pub struct About {}
|
||||
|
||||
impl super::Demo for About {
|
||||
fn name(&self) -> &'static str {
|
||||
"About egui"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"About egui"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.default_width(320.0)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.default_width(320.0)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for About {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS};
|
||||
|
||||
ui.heading("egui");
|
||||
ui.label(format!(
|
||||
ui.heading("egui");
|
||||
ui.label(format!(
|
||||
"egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \
|
||||
On the web it is compiled to WebAssembly and rendered with WebGL.{}",
|
||||
OS_APPLE, OS_LINUX, OS_WINDOWS,
|
||||
@@ -32,63 +32,63 @@ impl super::View for About {
|
||||
" Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust."
|
||||
} else {""}
|
||||
));
|
||||
ui.label("egui is designed to be easy to use, portable, and fast.");
|
||||
ui.label("egui is designed to be easy to use, portable, and fast.");
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Immediate mode");
|
||||
about_immediate_mode(ui);
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Immediate mode");
|
||||
about_immediate_mode(ui);
|
||||
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Links");
|
||||
links(ui);
|
||||
}
|
||||
ui.add_space(12.0); // ui.separator();
|
||||
ui.heading("Links");
|
||||
links(ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn about_immediate_mode(ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text.
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a ");
|
||||
let _ = ui.small_button("button");
|
||||
ui.label(" in egui:");
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
code_view_ui(
|
||||
ui,
|
||||
r#"
|
||||
ui.add_space(8.0);
|
||||
code_view_ui(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Save").clicked() {
|
||||
my_state.save();
|
||||
}"#
|
||||
.trim_start_matches('\n'),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
.trim_start_matches('\n'),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.label("Note how there are no callbacks or messages, and no button state to store.");
|
||||
ui.label("Note how there are no callbacks or messages, and no button state to store.");
|
||||
|
||||
ui.label("Immediate mode has its roots in gaming, where everything on the screen is painted at the display refresh rate, i.e. at 60+ frames per second. \
|
||||
ui.label("Immediate mode has its roots in gaming, where everything on the screen is painted at the display refresh rate, i.e. at 60+ frames per second. \
|
||||
In immediate mode GUIs, the entire interface is layed out and painted at the same high rate. \
|
||||
This makes immediate mode GUIs especially well suited for highly interactive applications.");
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("More about immediate mode ");
|
||||
ui.hyperlink_to("here", "https://github.com/emilk/egui#why-immediate-mode");
|
||||
ui.label(".");
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("More about immediate mode ");
|
||||
ui.hyperlink_to("here", "https://github.com/emilk/egui#why-immediate-mode");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
fn links(ui: &mut egui::Ui) {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to("egui documentation", "https://docs.rs/egui/");
|
||||
}
|
||||
|
||||
@@ -3,95 +3,95 @@
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct CodeEditor {
|
||||
language: String,
|
||||
code: String,
|
||||
language: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
impl Default for CodeEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "rs".into(),
|
||||
code: "// A very simple example\n\
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "rs".into(),
|
||||
code: "// A very simple example\n\
|
||||
fn main() {\n\
|
||||
\tprintln!(\"Hello world!\");\n\
|
||||
}\n\
|
||||
"
|
||||
.into(),
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeEditor {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Editor"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Editor"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_height(500.0)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_height(500.0)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeEditor {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { language, code } = self;
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { language, code } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_height(0.0);
|
||||
ui.label("An example of syntax highlighting in a TextEdit.");
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_height(0.0);
|
||||
ui.label("An example of syntax highlighting in a TextEdit.");
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
if cfg!(feature = "syntect") {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Language:");
|
||||
ui.text_edit_singleline(language);
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Syntax highlighting powered by ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Compile the demo with the ");
|
||||
ui.code("syntax_highlighting");
|
||||
ui.label(" feature to enable more accurate syntax highlighting using ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
ui.group(|ui| {
|
||||
theme.ui(ui);
|
||||
theme.clone().store_in_memory(ui.ctx());
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layout_job =
|
||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(10)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
});
|
||||
if cfg!(feature = "syntect") {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Language:");
|
||||
ui.text_edit_singleline(language);
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Syntax highlighting powered by ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
} else {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Compile the demo with the ");
|
||||
ui.code("syntax_highlighting");
|
||||
ui.label(" feature to enable more accurate syntax highlighting using ");
|
||||
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
|
||||
ui.label(".");
|
||||
});
|
||||
}
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
ui.group(|ui| {
|
||||
theme.ui(ui);
|
||||
theme.clone().store_in_memory(ui.ctx());
|
||||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layout_job =
|
||||
crate::syntax_highlighting::highlight(ui.ctx(), &theme, string, language);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(10)
|
||||
.lock_focus(true)
|
||||
.desired_width(f32::INFINITY)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
#[derive(Debug)]
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
|
||||
impl Default for CodeExample {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Arthur".to_owned(),
|
||||
age: 42,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Arthur".to_owned(),
|
||||
age: 42,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeExample {
|
||||
fn samples_in_grid(&mut self, ui: &mut egui::Ui) {
|
||||
show_code(ui, r#"ui.heading("Code samples");"#);
|
||||
ui.heading("Code samples");
|
||||
ui.end_row();
|
||||
fn samples_in_grid(&mut self, ui: &mut egui::Ui) {
|
||||
show_code(ui, r#"ui.heading("Code samples");"#);
|
||||
ui.heading("Code samples");
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});"#,
|
||||
);
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
ui.end_row();
|
||||
);
|
||||
// Putting things on the same line using ui.horizontal:
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Your name: ");
|
||||
ui.text_edit_singleline(&mut self.name);
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"egui::Slider::new(&mut self.age, 0..=120).text("age")"#,
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
||||
ui.end_row();
|
||||
show_code(
|
||||
ui,
|
||||
r#"egui::Slider::new(&mut self.age, 0..=120).text("age")"#,
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
show_code(
|
||||
ui,
|
||||
r#"
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}"#,
|
||||
);
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"ui.label(format!("Hello '{}', age {}", self.name, self.age));"#,
|
||||
);
|
||||
ui.label(format!("Hello '{}', age {}", self.name, self.age));
|
||||
ui.end_row();
|
||||
);
|
||||
if ui.button("Click each year").clicked() {
|
||||
self.age += 1;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
show_code(
|
||||
ui,
|
||||
r#"ui.label(format!("Hello '{}', age {}", self.name, self.age));"#,
|
||||
);
|
||||
ui.label(format!("Hello '{}', age {}", self.name, self.age));
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for CodeExample {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Example"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🖮 Code Example"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size([800.0, 400.0])
|
||||
.vscroll(false)
|
||||
.hscroll(true)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size([800.0, 400.0])
|
||||
.vscroll(false)
|
||||
.hscroll(true)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
use crate::syntax_highlighting::code_view_ui;
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
code_view_ui(
|
||||
ui,
|
||||
r"
|
||||
code_view_ui(
|
||||
ui,
|
||||
r"
|
||||
pub struct CodeExample {
|
||||
name: String,
|
||||
age: u32,
|
||||
@@ -98,64 +98,64 @@ pub struct CodeExample {
|
||||
impl CodeExample {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
"
|
||||
.trim(),
|
||||
);
|
||||
.trim(),
|
||||
);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
|
||||
let item_spacing = ui.spacing_mut().item_spacing;
|
||||
ui.add_space(indentation - item_spacing.x);
|
||||
ui.horizontal(|ui| {
|
||||
let font_id = egui::TextStyle::Monospace.resolve(ui.style());
|
||||
let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' ');
|
||||
let item_spacing = ui.spacing_mut().item_spacing;
|
||||
ui.add_space(indentation - item_spacing.x);
|
||||
|
||||
egui::Grid::new("code_samples")
|
||||
.striped(true)
|
||||
.num_columns(2)
|
||||
.min_col_width(16.0)
|
||||
.spacing([16.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
self.samples_in_grid(ui);
|
||||
});
|
||||
egui::Grid::new("code_samples")
|
||||
.striped(true)
|
||||
.num_columns(2)
|
||||
.min_col_width(16.0)
|
||||
.spacing([16.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
self.samples_in_grid(ui);
|
||||
});
|
||||
});
|
||||
|
||||
code_view_ui(ui, " }\n}");
|
||||
code_view_ui(ui, " }\n}");
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
code_view_ui(ui, &format!("{:#?}", self));
|
||||
code_view_ui(ui, &format!("{:#?}", self));
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
theme.ui(ui);
|
||||
theme.store_in_memory(ui.ctx());
|
||||
});
|
||||
}
|
||||
let mut theme = crate::syntax_highlighting::CodeTheme::from_memory(ui.ctx());
|
||||
ui.collapsing("Theme", |ui| {
|
||||
theme.ui(ui);
|
||||
theme.store_in_memory(ui.ctx());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn show_code(ui: &mut egui::Ui, code: &str) {
|
||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
||||
crate::syntax_highlighting::code_view_ui(ui, &code);
|
||||
let code = remove_leading_indentation(code.trim_start_matches('\n'));
|
||||
crate::syntax_highlighting::code_view_ui(ui, &code);
|
||||
}
|
||||
|
||||
fn remove_leading_indentation(code: &str) -> String {
|
||||
fn is_indent(c: &u8) -> bool {
|
||||
matches!(*c, b' ' | b'\t')
|
||||
}
|
||||
fn is_indent(c: &u8) -> bool {
|
||||
matches!(*c, b' ' | b'\t')
|
||||
}
|
||||
|
||||
let first_line_indent = code.bytes().take_while(is_indent).count();
|
||||
let first_line_indent = code.bytes().take_while(is_indent).count();
|
||||
|
||||
let mut out = String::new();
|
||||
let mut out = String::new();
|
||||
|
||||
let mut code = code;
|
||||
while !code.is_empty() {
|
||||
let indent = code.bytes().take_while(is_indent).count();
|
||||
let start = first_line_indent.min(indent);
|
||||
let end = code
|
||||
.find('\n')
|
||||
.map_or_else(|| code.len(), |endline| endline + 1);
|
||||
out += &code[start..end];
|
||||
code = &code[end..];
|
||||
}
|
||||
out
|
||||
let mut code = code;
|
||||
while !code.is_empty() {
|
||||
let indent = code.bytes().take_while(is_indent).count();
|
||||
let start = first_line_indent.min(indent);
|
||||
let end = code
|
||||
.find('\n')
|
||||
.map_or_else(|| code.len(), |endline| endline + 1);
|
||||
out += &code[start..end];
|
||||
code = &code[end..];
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -1,182 +1,182 @@
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Plot {
|
||||
Sin,
|
||||
Bell,
|
||||
Sigmoid,
|
||||
Sin,
|
||||
Bell,
|
||||
Sigmoid,
|
||||
}
|
||||
|
||||
fn gaussian(x: f64) -> f64 {
|
||||
let var: f64 = 2.0;
|
||||
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
|
||||
let var: f64 = 2.0;
|
||||
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
|
||||
}
|
||||
|
||||
fn sigmoid(x: f64) -> f64 {
|
||||
-1.0 + 2.0 / (1.0 + f64::exp(-x))
|
||||
-1.0 + 2.0 / (1.0 + f64::exp(-x))
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {
|
||||
plot: Plot,
|
||||
show_axes: [bool; 2],
|
||||
allow_drag: bool,
|
||||
allow_zoom: bool,
|
||||
allow_scroll: bool,
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
width: f32,
|
||||
height: f32,
|
||||
plot: Plot,
|
||||
show_axes: [bool; 2],
|
||||
allow_drag: bool,
|
||||
allow_zoom: bool,
|
||||
allow_scroll: bool,
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Default for ContextMenus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plot: Plot::Sin,
|
||||
show_axes: [true, true],
|
||||
allow_drag: true,
|
||||
allow_zoom: true,
|
||||
allow_scroll: true,
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plot: Plot::Sin,
|
||||
show_axes: [true, true],
|
||||
allow_drag: true,
|
||||
allow_zoom: true,
|
||||
allow_scroll: true,
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Context Menus"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View;
|
||||
egui::Window::new(self.name())
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
});
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.horizontal(|ui| {
|
||||
self.example_plot(ui).context_menu(|ui| {
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
|
||||
.clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
egui::Grid::new("button_grid").show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
ui.checkbox(&mut self.show_axes[1], "y-Axis");
|
||||
ui.end_row();
|
||||
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|
||||
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|
||||
|| ui.checkbox(&mut self.allow_scroll, "Scroll").changed()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.horizontal(|ui| {
|
||||
self.example_plot(ui).context_menu(|ui| {
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
|
||||
.clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
egui::Grid::new("button_grid").show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
ui.checkbox(&mut self.show_axes[1], "y-Axis");
|
||||
ui.end_row();
|
||||
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|
||||
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|
||||
|| ui.checkbox(&mut self.allow_scroll, "Scroll").changed()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line = Line::new(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
match self.plot {
|
||||
Plot::Sin => [x, x.sin()],
|
||||
Plot::Bell => [x, 10.0 * gaussian(x)],
|
||||
Plot::Sigmoid => [x, sigmoid(x)],
|
||||
}
|
||||
})
|
||||
.collect::<PlotPoints>(),
|
||||
);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.show_axes(self.show_axes)
|
||||
.allow_drag(self.allow_drag)
|
||||
.allow_zoom(self.allow_zoom)
|
||||
.allow_scroll(self.allow_scroll)
|
||||
.center_x_axis(self.center_x_axis)
|
||||
.center_x_axis(self.center_y_axis)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line = Line::new(
|
||||
(0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
match self.plot {
|
||||
Plot::Sin => [x, x.sin()],
|
||||
Plot::Bell => [x, 10.0 * gaussian(x)],
|
||||
Plot::Sigmoid => [x, sigmoid(x)],
|
||||
}
|
||||
})
|
||||
.collect::<PlotPoints>(),
|
||||
);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.show_axes(self.show_axes)
|
||||
.allow_drag(self.allow_drag)
|
||||
.allow_zoom(self.allow_zoom)
|
||||
.allow_scroll(self.allow_scroll)
|
||||
.center_x_axis(self.center_x_axis)
|
||||
.center_x_axis(self.center_y_axis)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item");
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,62 +6,62 @@ use egui::{containers::*, *};
|
||||
pub struct DancingStrings {}
|
||||
|
||||
impl super::Demo for DancingStrings {
|
||||
fn name(&self) -> &'static str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"♫ Dancing Strings"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 256.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DancingStrings {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::from_additive_luminance(196)
|
||||
} else {
|
||||
Color32::from_black_alpha(240)
|
||||
};
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::from_additive_luminance(196)
|
||||
} else {
|
||||
Color32::from_black_alpha(240)
|
||||
};
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
ui.ctx().request_repaint();
|
||||
let time = ui.input().time;
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
ui.ctx().request_repaint();
|
||||
let time = ui.input().time;
|
||||
|
||||
let desired_size = ui.available_width() * vec2(1.0, 0.35);
|
||||
let (_id, rect) = ui.allocate_space(desired_size);
|
||||
let desired_size = ui.available_width() * vec2(1.0, 0.35);
|
||||
let (_id, rect) = ui.allocate_space(desired_size);
|
||||
|
||||
let to_screen =
|
||||
emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect);
|
||||
let to_screen =
|
||||
emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect);
|
||||
|
||||
let mut shapes = vec![];
|
||||
let mut shapes = vec![];
|
||||
|
||||
for &mode in &[2, 3, 5] {
|
||||
let mode = mode as f64;
|
||||
let n = 120;
|
||||
let speed = 1.5;
|
||||
for &mode in &[2, 3, 5] {
|
||||
let mode = mode as f64;
|
||||
let n = 120;
|
||||
let speed = 1.5;
|
||||
|
||||
let points: Vec<Pos2> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (n as f64);
|
||||
let amp = (time * speed * mode).sin() / mode;
|
||||
let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin();
|
||||
to_screen * pos2(t as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
let points: Vec<Pos2> = (0..=n)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (n as f64);
|
||||
let amp = (time * speed * mode).sin() / mode;
|
||||
let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin();
|
||||
to_screen * pos2(t as f32, y as f32)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let thickness = 10.0 / mode as f32;
|
||||
shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color)));
|
||||
}
|
||||
let thickness = 10.0 / mode as f32;
|
||||
shapes.push(epaint::Shape::line(points, Stroke::new(thickness, color)));
|
||||
}
|
||||
|
||||
ui.painter().extend(shapes);
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
ui.painter().extend(shapes);
|
||||
});
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,68 +11,68 @@ use crate::is_mobile;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct Demos {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
|
||||
open: BTreeSet<String>,
|
||||
open: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Demos {
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::paint_bezier::PaintBezier::default()),
|
||||
Box::new(super::code_editor::CodeEditor::default()),
|
||||
Box::new(super::code_example::CodeExample::default()),
|
||||
Box::new(super::context_menu::ContextMenus::default()),
|
||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||
Box::new(super::font_book::FontBook::default()),
|
||||
Box::new(super::MiscDemoWindow::default()),
|
||||
Box::new(super::multi_touch::MultiTouch::default()),
|
||||
Box::new(super::painting::Painting::default()),
|
||||
Box::new(super::plot_demo::PlotDemo::default()),
|
||||
Box::new(super::scrolling::Scrolling::default()),
|
||||
Box::new(super::sliders::Sliders::default()),
|
||||
Box::new(super::strip_demo::StripDemo::default()),
|
||||
Box::new(super::table_demo::TableDemo::default()),
|
||||
Box::new(super::text_edit::TextEdit::default()),
|
||||
Box::new(super::widget_gallery::WidgetGallery::default()),
|
||||
Box::new(super::window_options::WindowOptions::default()),
|
||||
Box::new(super::tests::WindowResizeTest::default()),
|
||||
Box::new(super::window_with_panels::WindowWithPanels::default()),
|
||||
])
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::paint_bezier::PaintBezier::default()),
|
||||
Box::new(super::code_editor::CodeEditor::default()),
|
||||
Box::new(super::code_example::CodeExample::default()),
|
||||
Box::new(super::context_menu::ContextMenus::default()),
|
||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||
Box::new(super::font_book::FontBook::default()),
|
||||
Box::new(super::MiscDemoWindow::default()),
|
||||
Box::new(super::multi_touch::MultiTouch::default()),
|
||||
Box::new(super::painting::Painting::default()),
|
||||
Box::new(super::plot_demo::PlotDemo::default()),
|
||||
Box::new(super::scrolling::Scrolling::default()),
|
||||
Box::new(super::sliders::Sliders::default()),
|
||||
Box::new(super::strip_demo::StripDemo::default()),
|
||||
Box::new(super::table_demo::TableDemo::default()),
|
||||
Box::new(super::text_edit::TextEdit::default()),
|
||||
Box::new(super::widget_gallery::WidgetGallery::default()),
|
||||
Box::new(super::window_options::WindowOptions::default()),
|
||||
Box::new(super::tests::WindowResizeTest::default()),
|
||||
Box::new(super::window_with_panels::WindowWithPanels::default()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Demos {
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
Self { demos, open }
|
||||
}
|
||||
Self { demos, open }
|
||||
}
|
||||
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -80,66 +80,66 @@ impl Demos {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
struct Tests {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
demos: Vec<Box<dyn Demo>>,
|
||||
|
||||
open: BTreeSet<String>,
|
||||
open: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl Default for Tests {
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::tests::CursorTest::default()),
|
||||
Box::new(super::tests::IdTest::default()),
|
||||
Box::new(super::tests::InputTest::default()),
|
||||
Box::new(super::layout_test::LayoutTest::default()),
|
||||
Box::new(super::tests::ManualLayoutTest::default()),
|
||||
Box::new(super::tests::TableTest::default()),
|
||||
])
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::from_demos(vec![
|
||||
Box::new(super::tests::CursorTest::default()),
|
||||
Box::new(super::tests::IdTest::default()),
|
||||
Box::new(super::tests::InputTest::default()),
|
||||
Box::new(super::layout_test::LayoutTest::default()),
|
||||
Box::new(super::tests::ManualLayoutTest::default()),
|
||||
Box::new(super::tests::TableTest::default()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Tests {
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
pub fn from_demos(demos: Vec<Box<dyn Demo>>) -> Self {
|
||||
let mut open = BTreeSet::new();
|
||||
open.insert(
|
||||
super::widget_gallery::WidgetGallery::default()
|
||||
.name()
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
Self { demos, open }
|
||||
}
|
||||
Self { demos, open }
|
||||
}
|
||||
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
pub fn checkboxes(&mut self, ui: &mut Ui) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
ui.toggle_value(&mut is_open, demo.name());
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
pub fn windows(&mut self, ctx: &Context) {
|
||||
let Self { demos, open } = self;
|
||||
for demo in demos {
|
||||
let mut is_open = open.contains(demo.name());
|
||||
demo.show(ctx, &mut is_open);
|
||||
set_open(open, demo.name(), is_open);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn set_open(open: &mut BTreeSet<String>, key: &'static str, is_open: bool) {
|
||||
if is_open {
|
||||
if !open.contains(key) {
|
||||
open.insert(key.to_owned());
|
||||
}
|
||||
} else {
|
||||
open.remove(key);
|
||||
if is_open {
|
||||
if !open.contains(key) {
|
||||
open.insert(key.to_owned());
|
||||
}
|
||||
} else {
|
||||
open.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -148,171 +148,171 @@ fn set_open(open: &mut BTreeSet<String>, key: &'static str, is_open: bool) {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct DemoWindows {
|
||||
about_is_open: bool,
|
||||
about: About,
|
||||
demos: Demos,
|
||||
tests: Tests,
|
||||
about_is_open: bool,
|
||||
about: About,
|
||||
demos: Demos,
|
||||
tests: Tests,
|
||||
}
|
||||
|
||||
impl Default for DemoWindows {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
about_is_open: true,
|
||||
about: Default::default(),
|
||||
demos: Default::default(),
|
||||
tests: Default::default(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
about_is_open: true,
|
||||
about: Default::default(),
|
||||
demos: Default::default(),
|
||||
tests: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DemoWindows {
|
||||
/// Show the app ui (menu bar and windows).
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if is_mobile(ctx) {
|
||||
self.mobile_ui(ctx);
|
||||
} else {
|
||||
self.desktop_ui(ctx);
|
||||
/// Show the app ui (menu bar and windows).
|
||||
pub fn ui(&mut self, ctx: &Context) {
|
||||
if is_mobile(ctx) {
|
||||
self.mobile_ui(ctx);
|
||||
} else {
|
||||
self.desktop_ui(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_ui(&mut self, ctx: &Context) {
|
||||
if self.about_is_open {
|
||||
let screen_size = ctx.input().screen_rect.size();
|
||||
let default_width = (screen_size.x - 20.0).min(400.0);
|
||||
|
||||
let mut close = false;
|
||||
egui::Window::new(self.about.name())
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.default_width(default_width)
|
||||
.default_height(ctx.available_rect().height() - 46.0)
|
||||
.vscroll(true)
|
||||
.open(&mut self.about_is_open)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
self.about.ui(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
if ui
|
||||
.button(egui::RichText::new("Continue to the demo!").size(24.0))
|
||||
.clicked()
|
||||
{
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
self.about_is_open &= !close;
|
||||
} else {
|
||||
self.mobile_top_bar(ctx);
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 20.0;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(TWITTER).size(font_size),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_width(145.0)
|
||||
.show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✒ egui demos");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
|
||||
/// Show the open windows.
|
||||
fn show_windows(&mut self, ctx: &Context) {
|
||||
self.about.show(ctx, &mut self.about_is_open);
|
||||
self.demos.windows(ctx);
|
||||
self.tests.windows(ctx);
|
||||
}
|
||||
|
||||
fn demo_list_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||
ui.toggle_value(&mut self.about_is_open, self.about.name());
|
||||
|
||||
ui.separator();
|
||||
self.demos.checkboxes(ui);
|
||||
ui.separator();
|
||||
self.tests.checkboxes(ui);
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_ui(&mut self, ctx: &Context) {
|
||||
if self.about_is_open {
|
||||
let screen_size = ctx.input().screen_rect.size();
|
||||
let default_width = (screen_size.x - 20.0).min(400.0);
|
||||
|
||||
let mut close = false;
|
||||
egui::Window::new(self.about.name())
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.default_width(default_width)
|
||||
.default_height(ctx.available_rect().height() - 46.0)
|
||||
.vscroll(true)
|
||||
.open(&mut self.about_is_open)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
self.about.ui(ui);
|
||||
ui.add_space(12.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
if ui
|
||||
.button(egui::RichText::new("Continue to the demo!").size(24.0))
|
||||
.clicked()
|
||||
{
|
||||
close = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
self.about_is_open &= !close;
|
||||
} else {
|
||||
self.mobile_top_bar(ctx);
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn mobile_top_bar(&mut self, ctx: &Context) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
let font_size = 20.0;
|
||||
|
||||
ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| {
|
||||
ui.set_style(ui.ctx().style()); // ignore the "menu" style set by `menu_button`.
|
||||
self.demo_list_ui(ui);
|
||||
if ui.ui_contains_pointer() && ui.input().pointer.any_click() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(TWITTER).size(font_size),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
egui::RichText::new(GITHUB).size(font_size),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn desktop_ui(&mut self, ctx: &Context) {
|
||||
egui::SidePanel::right("egui_demo_panel")
|
||||
.resizable(false)
|
||||
.default_width(145.0)
|
||||
.show(ctx, |ui| {
|
||||
egui::trace!(ui);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("✒ egui demos");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
use egui::special_emojis::{GITHUB, TWITTER};
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.hyperlink_to(
|
||||
format!("{} @ernerfeldt", TWITTER),
|
||||
"https://twitter.com/ernerfeldt",
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
self.demo_list_ui(ui);
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
file_menu_button(ui);
|
||||
});
|
||||
});
|
||||
|
||||
self.show_windows(ctx);
|
||||
}
|
||||
|
||||
/// Show the open windows.
|
||||
fn show_windows(&mut self, ctx: &Context) {
|
||||
self.about.show(ctx, &mut self.about_is_open);
|
||||
self.demos.windows(ctx);
|
||||
self.tests.windows(ctx);
|
||||
}
|
||||
|
||||
fn demo_list_ui(&mut self, ui: &mut egui::Ui) {
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||
ui.toggle_value(&mut self.about_is_open, self.about.name());
|
||||
|
||||
ui.separator();
|
||||
self.demos.checkboxes(ui);
|
||||
ui.separator();
|
||||
self.tests.checkboxes(ui);
|
||||
ui.separator();
|
||||
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
fn file_menu_button(ui: &mut Ui) {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui
|
||||
.button("Reset egui memory")
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui
|
||||
.button("Reset egui memory")
|
||||
.on_hover_text("Forget scroll, positions, sizes etc")
|
||||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,178 +1,178 @@
|
||||
use egui::*;
|
||||
|
||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||
let is_being_dragged = ui.memory().is_being_dragged(id);
|
||||
let is_being_dragged = ui.memory().is_being_dragged(id);
|
||||
|
||||
if !is_being_dragged {
|
||||
let response = ui.scope(body).response;
|
||||
if !is_being_dragged {
|
||||
let response = ui.scope(body).response;
|
||||
|
||||
// Check for drags:
|
||||
let response = ui.interact(response.rect, id, Sense::drag());
|
||||
if response.hovered() {
|
||||
ui.output().cursor_icon = CursorIcon::Grab;
|
||||
}
|
||||
} else {
|
||||
ui.output().cursor_icon = CursorIcon::Grabbing;
|
||||
|
||||
// Paint the body to a new layer:
|
||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||
let response = ui.with_layer_id(layer_id, body).response;
|
||||
|
||||
// Now we move the visuals of the body to where the mouse is.
|
||||
// Normally you need to decide a location for a widget first,
|
||||
// because otherwise that widget cannot interact with the mouse.
|
||||
// However, a dragged component cannot be interacted with anyway
|
||||
// (anything with `Order::Tooltip` always gets an empty [`Response`])
|
||||
// So this is fine!
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
}
|
||||
// Check for drags:
|
||||
let response = ui.interact(response.rect, id, Sense::drag());
|
||||
if response.hovered() {
|
||||
ui.output().cursor_icon = CursorIcon::Grab;
|
||||
}
|
||||
} else {
|
||||
ui.output().cursor_icon = CursorIcon::Grabbing;
|
||||
|
||||
// Paint the body to a new layer:
|
||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||
let response = ui.with_layer_id(layer_id, body).response;
|
||||
|
||||
// Now we move the visuals of the body to where the mouse is.
|
||||
// Normally you need to decide a location for a widget first,
|
||||
// because otherwise that widget cannot interact with the mouse.
|
||||
// However, a dragged component cannot be interacted with anyway
|
||||
// (anything with `Order::Tooltip` always gets an empty [`Response`])
|
||||
// So this is fine!
|
||||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_target<R>(
|
||||
ui: &mut Ui,
|
||||
can_accept_what_is_being_dragged: bool,
|
||||
body: impl FnOnce(&mut Ui) -> R,
|
||||
ui: &mut Ui,
|
||||
can_accept_what_is_being_dragged: bool,
|
||||
body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
|
||||
let margin = Vec2::splat(4.0);
|
||||
let margin = Vec2::splat(4.0);
|
||||
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
let inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||
let ret = body(&mut content_ui);
|
||||
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin);
|
||||
let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover());
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
let inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||
let ret = body(&mut content_ui);
|
||||
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin);
|
||||
let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover());
|
||||
|
||||
let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
ui.visuals().widgets.active
|
||||
} else {
|
||||
ui.visuals().widgets.inactive
|
||||
};
|
||||
let style = if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
ui.visuals().widgets.active
|
||||
} else {
|
||||
ui.visuals().widgets.inactive
|
||||
};
|
||||
|
||||
let mut fill = style.bg_fill;
|
||||
let mut stroke = style.bg_stroke;
|
||||
if is_being_dragged && !can_accept_what_is_being_dragged {
|
||||
// gray out:
|
||||
fill = color::tint_color_towards(fill, ui.visuals().window_fill());
|
||||
stroke.color = color::tint_color_towards(stroke.color, ui.visuals().window_fill());
|
||||
}
|
||||
let mut fill = style.bg_fill;
|
||||
let mut stroke = style.bg_stroke;
|
||||
if is_being_dragged && !can_accept_what_is_being_dragged {
|
||||
// gray out:
|
||||
fill = color::tint_color_towards(fill, ui.visuals().window_fill());
|
||||
stroke.color = color::tint_color_towards(stroke.color, ui.visuals().window_fill());
|
||||
}
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
epaint::RectShape {
|
||||
rounding: style.rounding,
|
||||
fill,
|
||||
stroke,
|
||||
rect,
|
||||
},
|
||||
);
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
epaint::RectShape {
|
||||
rounding: style.rounding,
|
||||
fill,
|
||||
stroke,
|
||||
rect,
|
||||
},
|
||||
);
|
||||
|
||||
InnerResponse::new(ret, response)
|
||||
InnerResponse::new(ret, response)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DragAndDropDemo {
|
||||
/// columns with items
|
||||
columns: Vec<Vec<String>>,
|
||||
/// columns with items
|
||||
columns: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for DragAndDropDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: vec![
|
||||
vec!["Item A", "Item B", "Item C"],
|
||||
vec!["Item D", "Item E"],
|
||||
vec!["Item F", "Item G", "Item H"],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(ToString::to_string).collect())
|
||||
.collect(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: vec![
|
||||
vec!["Item A", "Item B", "Item C"],
|
||||
vec!["Item D", "Item E"],
|
||||
vec!["Item F", "Item G", "Item H"],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(ToString::to_string).collect())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for DragAndDropDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"✋ Drag and Drop"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"✋ Drag and Drop"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(256.0, 256.0))
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(256.0, 256.0))
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for DragAndDropDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This is a proof-of-concept of drag-and-drop in egui.");
|
||||
ui.label("Drag items between columns.");
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This is a proof-of-concept of drag-and-drop in egui.");
|
||||
ui.label("Drag items between columns.");
|
||||
|
||||
let id_source = "my_drag_and_drop_demo";
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.clone().into_iter().enumerate() {
|
||||
let ui = &mut uis[col_idx];
|
||||
let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯
|
||||
let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| {
|
||||
ui.set_min_size(vec2(64.0, 100.0));
|
||||
for (row_idx, item) in column.iter().enumerate() {
|
||||
let item_id = Id::new(id_source).with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
let response = ui.add(Label::new(item).sense(Sense::click()));
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
self.columns[col_idx].remove(row_idx);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ui.memory().is_being_dragged(item_id) {
|
||||
source_col_row = Some((col_idx, row_idx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.response;
|
||||
|
||||
let response = response.context_menu(|ui| {
|
||||
if ui.button("New Item").clicked() {
|
||||
self.columns[col_idx].push("New Item".to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
drop_col = Some(col_idx);
|
||||
let id_source = "my_drag_and_drop_demo";
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.clone().into_iter().enumerate() {
|
||||
let ui = &mut uis[col_idx];
|
||||
let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯
|
||||
let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| {
|
||||
ui.set_min_size(vec2(64.0, 100.0));
|
||||
for (row_idx, item) in column.iter().enumerate() {
|
||||
let item_id = Id::new(id_source).with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
let response = ui.add(Label::new(item).sense(Sense::click()));
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
self.columns[col_idx].remove(row_idx);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ui.memory().is_being_dragged(item_id) {
|
||||
source_col_row = Some((col_idx, row_idx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.response;
|
||||
|
||||
let response = response.context_menu(|ui| {
|
||||
if ui.button("New Item").clicked() {
|
||||
self.columns[col_idx].push("New Item".to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((source_col, source_row)) = source_col_row {
|
||||
if let Some(drop_col) = drop_col {
|
||||
if ui.input().pointer.any_released() {
|
||||
// do the drop:
|
||||
let item = self.columns[source_col].remove(source_row);
|
||||
self.columns[drop_col].push(item);
|
||||
}
|
||||
}
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
drop_col = Some(col_idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
if let Some((source_col, source_row)) = source_col_row {
|
||||
if let Some(drop_col) = drop_col {
|
||||
if ui.input().pointer.any_released() {
|
||||
// do the drop:
|
||||
let item = self.columns[source_col].remove(source_row);
|
||||
self.columns[drop_col].push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,261 +1,259 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct FontBook {
|
||||
filter: String,
|
||||
font_id: egui::FontId,
|
||||
named_chars: BTreeMap<egui::FontFamily, BTreeMap<char, String>>,
|
||||
filter: String,
|
||||
font_id: egui::FontId,
|
||||
named_chars: BTreeMap<egui::FontFamily, BTreeMap<char, String>>,
|
||||
}
|
||||
|
||||
impl Default for FontBook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: Default::default(),
|
||||
font_id: egui::FontId::proportional(20.0),
|
||||
named_chars: Default::default(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filter: Default::default(),
|
||||
font_id: egui::FontId::proportional(20.0),
|
||||
named_chars: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for FontBook {
|
||||
fn name(&self) -> &'static str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🔤 Font Book"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for FontBook {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
ui.label(format!(
|
||||
"The selected font supports {} characters.",
|
||||
self.named_chars
|
||||
.get(&self.font_id.family)
|
||||
.map(|map| map.len())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
ui.label(format!(
|
||||
"The selected font supports {} characters.",
|
||||
self
|
||||
.named_chars
|
||||
.get(&self.font_id.family)
|
||||
.map(|map| map.len())
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("You can add more characters by installing additional fonts with ");
|
||||
ui.add(egui::Hyperlink::from_label_and_url(
|
||||
egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
|
||||
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
|
||||
));
|
||||
ui.label(".");
|
||||
});
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("You can add more characters by installing additional fonts with ");
|
||||
ui.add(egui::Hyperlink::from_label_and_url(
|
||||
egui::RichText::new("Context::set_fonts").text_style(egui::TextStyle::Monospace),
|
||||
"https://docs.rs/egui/latest/egui/struct.Context.html#method.set_fonts",
|
||||
));
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
egui::introspection::font_id_ui(ui, &mut self.font_id);
|
||||
egui::introspection::font_id_ui(ui, &mut self.font_id);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.filter).desired_width(120.0));
|
||||
self.filter = self.filter.to_lowercase();
|
||||
if ui.button("x").clicked() {
|
||||
self.filter.clear();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Filter:");
|
||||
ui.add(egui::TextEdit::singleline(&mut self.filter).desired_width(120.0));
|
||||
self.filter = self.filter.to_lowercase();
|
||||
if ui.button("x").clicked() {
|
||||
self.filter.clear();
|
||||
}
|
||||
});
|
||||
|
||||
let filter = &self.filter;
|
||||
let named_chars = self
|
||||
.named_chars
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0);
|
||||
|
||||
for (&chr, name) in named_chars {
|
||||
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
|
||||
let button =
|
||||
egui::Button::new(egui::RichText::new(chr.to_string()).font(self.font_id.clone()))
|
||||
.frame(false);
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
ui.label(egui::RichText::new(chr.to_string()).font(self.font_id.clone()));
|
||||
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
|
||||
};
|
||||
|
||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
||||
ui.output().copied_text = chr.to_string();
|
||||
}
|
||||
});
|
||||
|
||||
let filter = &self.filter;
|
||||
let named_chars = self
|
||||
.named_chars
|
||||
.entry(self.font_id.family.clone())
|
||||
.or_insert_with(|| available_characters(ui, self.font_id.family.clone()));
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::splat(2.0);
|
||||
|
||||
for (&chr, name) in named_chars {
|
||||
if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() {
|
||||
let button = egui::Button::new(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
)
|
||||
.frame(false);
|
||||
|
||||
let tooltip_ui = |ui: &mut egui::Ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(chr.to_string()).font(self.font_id.clone()),
|
||||
);
|
||||
ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32));
|
||||
};
|
||||
|
||||
if ui.add(button).on_hover_ui(tooltip_ui).clicked() {
|
||||
ui.output().copied_text = chr.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap<char, String> {
|
||||
ui.fonts()
|
||||
.lock()
|
||||
.fonts
|
||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||
.characters()
|
||||
.iter()
|
||||
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||
.map(|&chr| (chr, char_name(chr)))
|
||||
.collect()
|
||||
ui.fonts()
|
||||
.lock()
|
||||
.fonts
|
||||
.font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters
|
||||
.characters()
|
||||
.iter()
|
||||
.filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control())
|
||||
.map(|&chr| (chr, char_name(chr)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn char_name(chr: char) -> String {
|
||||
special_char_name(chr)
|
||||
.map(|s| s.to_owned())
|
||||
.or_else(|| unicode_names2::name(chr).map(|name| name.to_string().to_lowercase()))
|
||||
.unwrap_or_else(|| "unknown".to_owned())
|
||||
special_char_name(chr)
|
||||
.map(|s| s.to_owned())
|
||||
.or_else(|| unicode_names2::name(chr).map(|name| name.to_string().to_lowercase()))
|
||||
.unwrap_or_else(|| "unknown".to_owned())
|
||||
}
|
||||
|
||||
fn special_char_name(chr: char) -> Option<&'static str> {
|
||||
#[allow(clippy::match_same_arms)] // many "flag"
|
||||
match chr {
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Private use area extensions:
|
||||
'\u{FE4E5}' => Some("flag japan"),
|
||||
'\u{FE4E6}' => Some("flag usa"),
|
||||
'\u{FE4E7}' => Some("flag"),
|
||||
'\u{FE4E8}' => Some("flag"),
|
||||
'\u{FE4E9}' => Some("flag"),
|
||||
'\u{FE4EA}' => Some("flag great britain"),
|
||||
'\u{FE4EB}' => Some("flag"),
|
||||
'\u{FE4EC}' => Some("flag"),
|
||||
'\u{FE4ED}' => Some("flag"),
|
||||
'\u{FE4EE}' => Some("flag south korea"),
|
||||
'\u{FE82C}' => Some("number sign in square"),
|
||||
'\u{FE82E}' => Some("digit one in square"),
|
||||
'\u{FE82F}' => Some("digit two in square"),
|
||||
'\u{FE830}' => Some("digit three in square"),
|
||||
'\u{FE831}' => Some("digit four in square"),
|
||||
'\u{FE832}' => Some("digit five in square"),
|
||||
'\u{FE833}' => Some("digit six in square"),
|
||||
'\u{FE834}' => Some("digit seven in square"),
|
||||
'\u{FE835}' => Some("digit eight in square"),
|
||||
'\u{FE836}' => Some("digit nine in square"),
|
||||
'\u{FE837}' => Some("digit zero in square"),
|
||||
#[allow(clippy::match_same_arms)] // many "flag"
|
||||
match chr {
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Private use area extensions:
|
||||
'\u{FE4E5}' => Some("flag japan"),
|
||||
'\u{FE4E6}' => Some("flag usa"),
|
||||
'\u{FE4E7}' => Some("flag"),
|
||||
'\u{FE4E8}' => Some("flag"),
|
||||
'\u{FE4E9}' => Some("flag"),
|
||||
'\u{FE4EA}' => Some("flag great britain"),
|
||||
'\u{FE4EB}' => Some("flag"),
|
||||
'\u{FE4EC}' => Some("flag"),
|
||||
'\u{FE4ED}' => Some("flag"),
|
||||
'\u{FE4EE}' => Some("flag south korea"),
|
||||
'\u{FE82C}' => Some("number sign in square"),
|
||||
'\u{FE82E}' => Some("digit one in square"),
|
||||
'\u{FE82F}' => Some("digit two in square"),
|
||||
'\u{FE830}' => Some("digit three in square"),
|
||||
'\u{FE831}' => Some("digit four in square"),
|
||||
'\u{FE832}' => Some("digit five in square"),
|
||||
'\u{FE833}' => Some("digit six in square"),
|
||||
'\u{FE834}' => Some("digit seven in square"),
|
||||
'\u{FE835}' => Some("digit eight in square"),
|
||||
'\u{FE836}' => Some("digit nine in square"),
|
||||
'\u{FE837}' => Some("digit zero in square"),
|
||||
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Web services / operating systems / browsers
|
||||
'\u{E600}' => Some("web-dribbble"),
|
||||
'\u{E601}' => Some("web-stackoverflow"),
|
||||
'\u{E602}' => Some("web-vimeo"),
|
||||
'\u{E603}' => Some("web-twitter"),
|
||||
'\u{E604}' => Some("web-facebook"),
|
||||
'\u{E605}' => Some("web-googleplus"),
|
||||
'\u{E606}' => Some("web-pinterest"),
|
||||
'\u{E607}' => Some("web-tumblr"),
|
||||
'\u{E608}' => Some("web-linkedin"),
|
||||
'\u{E60A}' => Some("web-stumbleupon"),
|
||||
'\u{E60B}' => Some("web-lastfm"),
|
||||
'\u{E60C}' => Some("web-rdio"),
|
||||
'\u{E60D}' => Some("web-spotify"),
|
||||
'\u{E60E}' => Some("web-qq"),
|
||||
'\u{E60F}' => Some("web-instagram"),
|
||||
'\u{E610}' => Some("web-dropbox"),
|
||||
'\u{E611}' => Some("web-evernote"),
|
||||
'\u{E612}' => Some("web-flattr"),
|
||||
'\u{E613}' => Some("web-skype"),
|
||||
'\u{E614}' => Some("web-renren"),
|
||||
'\u{E615}' => Some("web-sina-weibo"),
|
||||
'\u{E616}' => Some("web-paypal"),
|
||||
'\u{E617}' => Some("web-picasa"),
|
||||
'\u{E618}' => Some("os-android"),
|
||||
'\u{E619}' => Some("web-mixi"),
|
||||
'\u{E61A}' => Some("web-behance"),
|
||||
'\u{E61B}' => Some("web-circles"),
|
||||
'\u{E61C}' => Some("web-vk"),
|
||||
'\u{E61D}' => Some("web-smashing"),
|
||||
'\u{E61E}' => Some("web-forrst"),
|
||||
'\u{E61F}' => Some("os-windows"),
|
||||
'\u{E620}' => Some("web-flickr"),
|
||||
'\u{E621}' => Some("web-picassa"),
|
||||
'\u{E622}' => Some("web-deviantart"),
|
||||
'\u{E623}' => Some("web-steam"),
|
||||
'\u{E624}' => Some("web-github"),
|
||||
'\u{E625}' => Some("web-git"),
|
||||
'\u{E626}' => Some("web-blogger"),
|
||||
'\u{E627}' => Some("web-soundcloud"),
|
||||
'\u{E628}' => Some("web-reddit"),
|
||||
'\u{E629}' => Some("web-delicious"),
|
||||
'\u{E62A}' => Some("browser-chrome"),
|
||||
'\u{E62B}' => Some("browser-firefox"),
|
||||
'\u{E62C}' => Some("browser-ie"),
|
||||
'\u{E62D}' => Some("browser-opera"),
|
||||
'\u{E62E}' => Some("browser-safari"),
|
||||
'\u{E62F}' => Some("web-google-drive"),
|
||||
'\u{E630}' => Some("web-wordpress"),
|
||||
'\u{E631}' => Some("web-joomla"),
|
||||
'\u{E632}' => Some("lastfm"),
|
||||
'\u{E633}' => Some("web-foursquare"),
|
||||
'\u{E634}' => Some("web-yelp"),
|
||||
'\u{E635}' => Some("web-drupal"),
|
||||
'\u{E636}' => Some("youtube"),
|
||||
'\u{F189}' => Some("vk"),
|
||||
'\u{F1A6}' => Some("digg"),
|
||||
'\u{F1CA}' => Some("web-vine"),
|
||||
'\u{F8FF}' => Some("os-apple"),
|
||||
// Special private-use-area extensions found in `emoji-icon-font.ttf`:
|
||||
// Web services / operating systems / browsers
|
||||
'\u{E600}' => Some("web-dribbble"),
|
||||
'\u{E601}' => Some("web-stackoverflow"),
|
||||
'\u{E602}' => Some("web-vimeo"),
|
||||
'\u{E603}' => Some("web-twitter"),
|
||||
'\u{E604}' => Some("web-facebook"),
|
||||
'\u{E605}' => Some("web-googleplus"),
|
||||
'\u{E606}' => Some("web-pinterest"),
|
||||
'\u{E607}' => Some("web-tumblr"),
|
||||
'\u{E608}' => Some("web-linkedin"),
|
||||
'\u{E60A}' => Some("web-stumbleupon"),
|
||||
'\u{E60B}' => Some("web-lastfm"),
|
||||
'\u{E60C}' => Some("web-rdio"),
|
||||
'\u{E60D}' => Some("web-spotify"),
|
||||
'\u{E60E}' => Some("web-qq"),
|
||||
'\u{E60F}' => Some("web-instagram"),
|
||||
'\u{E610}' => Some("web-dropbox"),
|
||||
'\u{E611}' => Some("web-evernote"),
|
||||
'\u{E612}' => Some("web-flattr"),
|
||||
'\u{E613}' => Some("web-skype"),
|
||||
'\u{E614}' => Some("web-renren"),
|
||||
'\u{E615}' => Some("web-sina-weibo"),
|
||||
'\u{E616}' => Some("web-paypal"),
|
||||
'\u{E617}' => Some("web-picasa"),
|
||||
'\u{E618}' => Some("os-android"),
|
||||
'\u{E619}' => Some("web-mixi"),
|
||||
'\u{E61A}' => Some("web-behance"),
|
||||
'\u{E61B}' => Some("web-circles"),
|
||||
'\u{E61C}' => Some("web-vk"),
|
||||
'\u{E61D}' => Some("web-smashing"),
|
||||
'\u{E61E}' => Some("web-forrst"),
|
||||
'\u{E61F}' => Some("os-windows"),
|
||||
'\u{E620}' => Some("web-flickr"),
|
||||
'\u{E621}' => Some("web-picassa"),
|
||||
'\u{E622}' => Some("web-deviantart"),
|
||||
'\u{E623}' => Some("web-steam"),
|
||||
'\u{E624}' => Some("web-github"),
|
||||
'\u{E625}' => Some("web-git"),
|
||||
'\u{E626}' => Some("web-blogger"),
|
||||
'\u{E627}' => Some("web-soundcloud"),
|
||||
'\u{E628}' => Some("web-reddit"),
|
||||
'\u{E629}' => Some("web-delicious"),
|
||||
'\u{E62A}' => Some("browser-chrome"),
|
||||
'\u{E62B}' => Some("browser-firefox"),
|
||||
'\u{E62C}' => Some("browser-ie"),
|
||||
'\u{E62D}' => Some("browser-opera"),
|
||||
'\u{E62E}' => Some("browser-safari"),
|
||||
'\u{E62F}' => Some("web-google-drive"),
|
||||
'\u{E630}' => Some("web-wordpress"),
|
||||
'\u{E631}' => Some("web-joomla"),
|
||||
'\u{E632}' => Some("lastfm"),
|
||||
'\u{E633}' => Some("web-foursquare"),
|
||||
'\u{E634}' => Some("web-yelp"),
|
||||
'\u{E635}' => Some("web-drupal"),
|
||||
'\u{E636}' => Some("youtube"),
|
||||
'\u{F189}' => Some("vk"),
|
||||
'\u{F1A6}' => Some("digg"),
|
||||
'\u{F1CA}' => Some("web-vine"),
|
||||
'\u{F8FF}' => Some("os-apple"),
|
||||
|
||||
// Special private-use-area extensions found in `Ubuntu-Light.ttf`
|
||||
'\u{F000}' => Some("uniF000"),
|
||||
'\u{F001}' => Some("fi"),
|
||||
'\u{F002}' => Some("fl"),
|
||||
'\u{F506}' => Some("one seventh"),
|
||||
'\u{F507}' => Some("two sevenths"),
|
||||
'\u{F508}' => Some("three sevenths"),
|
||||
'\u{F509}' => Some("four sevenths"),
|
||||
'\u{F50A}' => Some("five sevenths"),
|
||||
'\u{F50B}' => Some("six sevenths"),
|
||||
'\u{F50C}' => Some("one ninth"),
|
||||
'\u{F50D}' => Some("two ninths"),
|
||||
'\u{F50E}' => Some("four ninths"),
|
||||
'\u{F50F}' => Some("five ninths"),
|
||||
'\u{F510}' => Some("seven ninths"),
|
||||
'\u{F511}' => Some("eight ninths"),
|
||||
'\u{F800}' => Some("zero.alt"),
|
||||
'\u{F801}' => Some("one.alt"),
|
||||
'\u{F802}' => Some("two.alt"),
|
||||
'\u{F803}' => Some("three.alt"),
|
||||
'\u{F804}' => Some("four.alt"),
|
||||
'\u{F805}' => Some("five.alt"),
|
||||
'\u{F806}' => Some("six.alt"),
|
||||
'\u{F807}' => Some("seven.alt"),
|
||||
'\u{F808}' => Some("eight.alt"),
|
||||
'\u{F809}' => Some("nine.alt"),
|
||||
'\u{F80A}' => Some("zero.sups"),
|
||||
'\u{F80B}' => Some("one.sups"),
|
||||
'\u{F80C}' => Some("two.sups"),
|
||||
'\u{F80D}' => Some("three.sups"),
|
||||
'\u{F80E}' => Some("four.sups"),
|
||||
'\u{F80F}' => Some("five.sups"),
|
||||
'\u{F810}' => Some("six.sups"),
|
||||
'\u{F811}' => Some("seven.sups"),
|
||||
'\u{F812}' => Some("eight.sups"),
|
||||
'\u{F813}' => Some("nine.sups"),
|
||||
'\u{F814}' => Some("zero.sinf"),
|
||||
'\u{F815}' => Some("one.sinf"),
|
||||
'\u{F816}' => Some("two.sinf"),
|
||||
'\u{F817}' => Some("three.sinf"),
|
||||
'\u{F818}' => Some("four.sinf"),
|
||||
'\u{F819}' => Some("five.sinf"),
|
||||
'\u{F81A}' => Some("six.sinf"),
|
||||
'\u{F81B}' => Some("seven.sinf"),
|
||||
'\u{F81C}' => Some("eight.sinf"),
|
||||
'\u{F81D}' => Some("nine.sinf"),
|
||||
// Special private-use-area extensions found in `Ubuntu-Light.ttf`
|
||||
'\u{F000}' => Some("uniF000"),
|
||||
'\u{F001}' => Some("fi"),
|
||||
'\u{F002}' => Some("fl"),
|
||||
'\u{F506}' => Some("one seventh"),
|
||||
'\u{F507}' => Some("two sevenths"),
|
||||
'\u{F508}' => Some("three sevenths"),
|
||||
'\u{F509}' => Some("four sevenths"),
|
||||
'\u{F50A}' => Some("five sevenths"),
|
||||
'\u{F50B}' => Some("six sevenths"),
|
||||
'\u{F50C}' => Some("one ninth"),
|
||||
'\u{F50D}' => Some("two ninths"),
|
||||
'\u{F50E}' => Some("four ninths"),
|
||||
'\u{F50F}' => Some("five ninths"),
|
||||
'\u{F510}' => Some("seven ninths"),
|
||||
'\u{F511}' => Some("eight ninths"),
|
||||
'\u{F800}' => Some("zero.alt"),
|
||||
'\u{F801}' => Some("one.alt"),
|
||||
'\u{F802}' => Some("two.alt"),
|
||||
'\u{F803}' => Some("three.alt"),
|
||||
'\u{F804}' => Some("four.alt"),
|
||||
'\u{F805}' => Some("five.alt"),
|
||||
'\u{F806}' => Some("six.alt"),
|
||||
'\u{F807}' => Some("seven.alt"),
|
||||
'\u{F808}' => Some("eight.alt"),
|
||||
'\u{F809}' => Some("nine.alt"),
|
||||
'\u{F80A}' => Some("zero.sups"),
|
||||
'\u{F80B}' => Some("one.sups"),
|
||||
'\u{F80C}' => Some("two.sups"),
|
||||
'\u{F80D}' => Some("three.sups"),
|
||||
'\u{F80E}' => Some("four.sups"),
|
||||
'\u{F80F}' => Some("five.sups"),
|
||||
'\u{F810}' => Some("six.sups"),
|
||||
'\u{F811}' => Some("seven.sups"),
|
||||
'\u{F812}' => Some("eight.sups"),
|
||||
'\u{F813}' => Some("nine.sups"),
|
||||
'\u{F814}' => Some("zero.sinf"),
|
||||
'\u{F815}' => Some("one.sinf"),
|
||||
'\u{F816}' => Some("two.sinf"),
|
||||
'\u{F817}' => Some("three.sinf"),
|
||||
'\u{F818}' => Some("four.sinf"),
|
||||
'\u{F819}' => Some("five.sinf"),
|
||||
'\u{F81A}' => Some("six.sinf"),
|
||||
'\u{F81B}' => Some("seven.sinf"),
|
||||
'\u{F81C}' => Some("eight.sinf"),
|
||||
'\u{F81D}' => Some("nine.sinf"),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,178 +3,176 @@ use egui::*;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutTest {
|
||||
// Identical to contents of `egui::Layout`
|
||||
layout: LayoutSettings,
|
||||
// Identical to contents of `egui::Layout`
|
||||
layout: LayoutSettings,
|
||||
|
||||
// Extra for testing wrapping:
|
||||
wrap_column_width: f32,
|
||||
wrap_row_height: f32,
|
||||
// Extra for testing wrapping:
|
||||
wrap_column_width: f32,
|
||||
wrap_row_height: f32,
|
||||
}
|
||||
|
||||
impl Default for LayoutTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: LayoutSettings::top_down(),
|
||||
wrap_column_width: 150.0,
|
||||
wrap_row_height: 20.0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
layout: LayoutSettings::top_down(),
|
||||
wrap_column_width: 150.0,
|
||||
wrap_row_height: 20.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct LayoutSettings {
|
||||
// Similar to the contents of `egui::Layout`
|
||||
main_dir: Direction,
|
||||
main_wrap: bool,
|
||||
cross_align: Align,
|
||||
cross_justify: bool,
|
||||
// Similar to the contents of `egui::Layout`
|
||||
main_dir: Direction,
|
||||
main_wrap: bool,
|
||||
cross_align: Align,
|
||||
cross_justify: bool,
|
||||
}
|
||||
|
||||
impl Default for LayoutSettings {
|
||||
fn default() -> Self {
|
||||
Self::top_down()
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::top_down()
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutSettings {
|
||||
fn top_down() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Min,
|
||||
cross_justify: false,
|
||||
}
|
||||
fn top_down() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Min,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn top_down_justified_centered() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: true,
|
||||
}
|
||||
fn top_down_justified_centered() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::TopDown,
|
||||
main_wrap: false,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_wrapped() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
main_wrap: true,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: false,
|
||||
}
|
||||
fn horizontal_wrapped() -> Self {
|
||||
Self {
|
||||
main_dir: Direction::LeftToRight,
|
||||
main_wrap: true,
|
||||
cross_align: Align::Center,
|
||||
cross_justify: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
||||
.with_main_wrap(self.main_wrap)
|
||||
.with_cross_justify(self.cross_justify)
|
||||
}
|
||||
fn layout(&self) -> Layout {
|
||||
Layout::from_main_dir_and_cross_align(self.main_dir, self.cross_align)
|
||||
.with_main_wrap(self.main_wrap)
|
||||
.with_cross_justify(self.cross_justify)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for LayoutTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Layout Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Layout Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for LayoutTest {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Tests and demonstrates the limits of the egui layouts");
|
||||
self.content_ui(ui);
|
||||
Resize::default()
|
||||
.default_size([150.0, 200.0])
|
||||
.show(ui, |ui| {
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.allocate_ui(
|
||||
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui(
|
||||
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.with_layout(self.layout.layout(), demo_ui);
|
||||
}
|
||||
});
|
||||
ui.label("Resize to see effect");
|
||||
}
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Tests and demonstrates the limits of the egui layouts");
|
||||
self.content_ui(ui);
|
||||
Resize::default()
|
||||
.default_size([150.0, 200.0])
|
||||
.show(ui, |ui| {
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.allocate_ui(
|
||||
vec2(ui.available_size_before_wrap().x, self.wrap_row_height),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
} else {
|
||||
ui.allocate_ui(
|
||||
vec2(self.wrap_column_width, ui.available_size_before_wrap().y),
|
||||
|ui| ui.with_layout(self.layout.layout(), demo_ui),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ui.with_layout(self.layout.layout(), demo_ui);
|
||||
}
|
||||
});
|
||||
ui.label("Resize to see effect");
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutTest {
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.layout, LayoutSettings::top_down(), "Top-down");
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::top_down_justified_centered(),
|
||||
"Top-down, centered and justified",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::horizontal_wrapped(),
|
||||
"Horizontal wrapped",
|
||||
);
|
||||
});
|
||||
pub fn content_ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.layout, LayoutSettings::top_down(), "Top-down");
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::top_down_justified_centered(),
|
||||
"Top-down, centered and justified",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.layout,
|
||||
LayoutSettings::horizontal_wrapped(),
|
||||
"Horizontal wrapped",
|
||||
);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Main Direction:");
|
||||
for &dir in &[
|
||||
Direction::LeftToRight,
|
||||
Direction::RightToLeft,
|
||||
Direction::TopDown,
|
||||
Direction::BottomUp,
|
||||
] {
|
||||
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Main Direction:");
|
||||
for &dir in &[
|
||||
Direction::LeftToRight,
|
||||
Direction::RightToLeft,
|
||||
Direction::TopDown,
|
||||
Direction::BottomUp,
|
||||
] {
|
||||
ui.radio_value(&mut self.layout.main_dir, dir, format!("{:?}", dir));
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.layout.main_wrap, "Main wrap")
|
||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.layout.main_wrap, "Main wrap")
|
||||
.on_hover_text("Wrap when next widget doesn't fit the current row/column");
|
||||
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(&mut self.wrap_column_width, 0.0..=200.0).text("Column width"),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
if self.layout.main_wrap {
|
||||
if self.layout.main_dir.is_horizontal() {
|
||||
ui.add(Slider::new(&mut self.wrap_row_height, 0.0..=200.0).text("Row height"));
|
||||
} else {
|
||||
ui.add(Slider::new(&mut self.wrap_column_width, 0.0..=200.0).text("Column width"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Cross Align:");
|
||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Cross Align:");
|
||||
for &align in &[Align::Min, Align::Center, Align::Max] {
|
||||
ui.radio_value(&mut self.layout.cross_align, align, format!("{:?}", align));
|
||||
}
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.layout.cross_justify, "Cross Justified")
|
||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||
}
|
||||
ui.checkbox(&mut self.layout.cross_justify, "Cross Justified")
|
||||
.on_hover_text("Try to fill full width/height (e.g. buttons)");
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_ui(ui: &mut Ui) {
|
||||
ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true));
|
||||
let mut dummy = false;
|
||||
ui.checkbox(&mut dummy, "checkbox");
|
||||
ui.radio_value(&mut dummy, false, "radio");
|
||||
let _ = ui.button("button");
|
||||
ui.add(egui::Label::new("Wrapping text followed by example widgets:").wrap(true));
|
||||
let mut dummy = false;
|
||||
ui.checkbox(&mut dummy, "checkbox");
|
||||
ui.radio_value(&mut dummy, false, "radio");
|
||||
let _ = ui.button("button");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,22 +31,22 @@ pub mod window_options;
|
||||
pub mod window_with_panels;
|
||||
|
||||
pub use {
|
||||
about::About, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
|
||||
widget_gallery::WidgetGallery,
|
||||
about::About, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
|
||||
widget_gallery::WidgetGallery,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Something to view in the demo windows
|
||||
pub trait View {
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
fn ui(&mut self, ui: &mut egui::Ui);
|
||||
}
|
||||
|
||||
/// Something to view
|
||||
pub trait Demo {
|
||||
/// `&'static` so we can also use it as a key to store open/close state.
|
||||
fn name(&self) -> &'static str;
|
||||
/// `&'static` so we can also use it as a key to store open/close state.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool);
|
||||
/// Show windows, etc
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool);
|
||||
}
|
||||
|
||||
@@ -1,145 +1,143 @@
|
||||
use egui::{
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
emath::{RectTransform, Rot2},
|
||||
vec2, Color32, Frame, Pos2, Rect, Sense, Stroke, Vec2,
|
||||
};
|
||||
|
||||
pub struct MultiTouch {
|
||||
rotation: f32,
|
||||
translation: Vec2,
|
||||
zoom: f32,
|
||||
last_touch_time: f64,
|
||||
rotation: f32,
|
||||
translation: Vec2,
|
||||
zoom: f32,
|
||||
last_touch_time: f64,
|
||||
}
|
||||
|
||||
impl Default for MultiTouch {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rotation: 0.,
|
||||
translation: Vec2::ZERO,
|
||||
zoom: 1.,
|
||||
last_touch_time: 0.0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rotation: 0.,
|
||||
translation: Vec2::ZERO,
|
||||
zoom: 1.,
|
||||
last_touch_time: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for MultiTouch {
|
||||
fn name(&self) -> &'static str {
|
||||
"👌 Multi Touch"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"👌 Multi Touch"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.resizable(true)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for MultiTouch {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.strong(
|
||||
"This demo only works on devices with multitouch support (e.g. mobiles and tablets).",
|
||||
);
|
||||
ui.separator();
|
||||
ui.label("Try touch gestures Pinch/Stretch, Rotation, and Pressure with 2+ fingers.");
|
||||
|
||||
let num_touches = ui.input().multi_touch().map_or(0, |mt| mt.num_touches);
|
||||
ui.label(format!("Current touches: {}", num_touches));
|
||||
let num_touches = ui.input().multi_touch().map_or(0, |mt| mt.num_touches);
|
||||
ui.label(format!("Current touches: {}", num_touches));
|
||||
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
let color = if ui.visuals().dark_mode {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
// Note that we use `Sense::drag()` although we do not use any pointer events. With
|
||||
// the current implementation, the fact that a touch event of two or more fingers is
|
||||
// recognized, does not mean that the pointer events are suppressed, which are always
|
||||
// generated for the first finger. Therefore, if we do not explicitly consume pointer
|
||||
// events, the window will move around, not only when dragged with a single finger, but
|
||||
// also when a two-finger touch is active. I guess this problem can only be cleanly
|
||||
// solved when the synthetic pointer events are created by egui, and not by the
|
||||
// backend.
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
// Note that we use `Sense::drag()` although we do not use any pointer events. With
|
||||
// the current implementation, the fact that a touch event of two or more fingers is
|
||||
// recognized, does not mean that the pointer events are suppressed, which are always
|
||||
// generated for the first finger. Therefore, if we do not explicitly consume pointer
|
||||
// events, the window will move around, not only when dragged with a single finger, but
|
||||
// also when a two-finger touch is active. I guess this problem can only be cleanly
|
||||
// solved when the synthetic pointer events are created by egui, and not by the
|
||||
// backend.
|
||||
|
||||
// set up the drawing canvas with normalized coordinates:
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
// set up the drawing canvas with normalized coordinates:
|
||||
let (response, painter) = ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
|
||||
// normalize painter coordinates to ±1 units in each direction with [0,0] in the center:
|
||||
let painter_proportions = response.rect.square_proportions();
|
||||
let to_screen = RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions),
|
||||
response.rect,
|
||||
);
|
||||
// normalize painter coordinates to ±1 units in each direction with [0,0] in the center:
|
||||
let painter_proportions = response.rect.square_proportions();
|
||||
let to_screen = RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO - painter_proportions, 2. * painter_proportions),
|
||||
response.rect,
|
||||
);
|
||||
|
||||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
// check for touch input (or the lack thereof) and update zoom and scale factors, plus
|
||||
// color and width:
|
||||
let mut stroke_width = 1.;
|
||||
if let Some(multi_touch) = ui.ctx().multi_touch() {
|
||||
// This adjusts the current zoom factor and rotation angle according to the dynamic
|
||||
// change (for the current frame) of the touch gesture:
|
||||
self.zoom *= multi_touch.zoom_delta;
|
||||
self.rotation += multi_touch.rotation_delta;
|
||||
// the translation we get from `multi_touch` needs to be scaled down to the
|
||||
// normalized coordinates we use as the basis for painting:
|
||||
self.translation += to_screen.inverse().scale() * multi_touch.translation_delta;
|
||||
// touch pressure will make the arrow thicker (not all touch devices support this):
|
||||
stroke_width += 10. * multi_touch.force;
|
||||
|
||||
self.last_touch_time = ui.input().time;
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation);
|
||||
let arrow_start_offset = self.translation + zoom_and_rotate * vec2(-0.5, 0.5);
|
||||
self.last_touch_time = ui.input().time;
|
||||
} else {
|
||||
self.slowly_reset(ui);
|
||||
}
|
||||
let zoom_and_rotate = self.zoom * Rot2::from_angle(self.rotation);
|
||||
let arrow_start_offset = self.translation + zoom_and_rotate * vec2(-0.5, 0.5);
|
||||
|
||||
// Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), but
|
||||
// scaled, rotated, and translated according to the current touch gesture:
|
||||
let arrow_start = Pos2::ZERO + arrow_start_offset;
|
||||
let arrow_direction = zoom_and_rotate * vec2(1., -1.);
|
||||
painter.arrow(
|
||||
to_screen * arrow_start,
|
||||
to_screen.scale() * arrow_direction,
|
||||
Stroke::new(stroke_width, color),
|
||||
);
|
||||
});
|
||||
}
|
||||
// Paints an arrow pointing from bottom-left (-0.5, 0.5) to top-right (0.5, -0.5), but
|
||||
// scaled, rotated, and translated according to the current touch gesture:
|
||||
let arrow_start = Pos2::ZERO + arrow_start_offset;
|
||||
let arrow_direction = zoom_and_rotate * vec2(1., -1.);
|
||||
painter.arrow(
|
||||
to_screen * arrow_start,
|
||||
to_screen.scale() * arrow_direction,
|
||||
Stroke::new(stroke_width, color),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiTouch {
|
||||
fn slowly_reset(&mut self, ui: &egui::Ui) {
|
||||
// This has nothing to do with the touch gesture. It just smoothly brings the
|
||||
// painted arrow back into its original position, for a nice visual effect:
|
||||
fn slowly_reset(&mut self, ui: &egui::Ui) {
|
||||
// This has nothing to do with the touch gesture. It just smoothly brings the
|
||||
// painted arrow back into its original position, for a nice visual effect:
|
||||
|
||||
let time_since_last_touch = (ui.input().time - self.last_touch_time) as f32;
|
||||
let time_since_last_touch = (ui.input().time - self.last_touch_time) as f32;
|
||||
|
||||
let delay = 0.5;
|
||||
if time_since_last_touch < delay {
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
// seconds after which half the amount of zoom/rotation will be reverted:
|
||||
let half_life =
|
||||
egui::remap_clamp(time_since_last_touch, delay..=1.0, 1.0..=0.0).powf(4.0);
|
||||
let delay = 0.5;
|
||||
if time_since_last_touch < delay {
|
||||
ui.ctx().request_repaint();
|
||||
} else {
|
||||
// seconds after which half the amount of zoom/rotation will be reverted:
|
||||
let half_life = egui::remap_clamp(time_since_last_touch, delay..=1.0, 1.0..=0.0).powf(4.0);
|
||||
|
||||
if half_life <= 1e-3 {
|
||||
self.zoom = 1.0;
|
||||
self.rotation = 0.0;
|
||||
self.translation = Vec2::ZERO;
|
||||
} else {
|
||||
let dt = ui.input().unstable_dt;
|
||||
let half_life_factor = (-(2_f32.ln()) / half_life * dt).exp();
|
||||
self.zoom = 1. + ((self.zoom - 1.) * half_life_factor);
|
||||
self.rotation *= half_life_factor;
|
||||
self.translation *= half_life_factor;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
if half_life <= 1e-3 {
|
||||
self.zoom = 1.0;
|
||||
self.rotation = 0.0;
|
||||
self.translation = Vec2::ZERO;
|
||||
} else {
|
||||
let dt = ui.input().unstable_dt;
|
||||
let half_life_factor = (-(2_f32.ln()) / half_life * dt).exp();
|
||||
self.zoom = 1. + ((self.zoom - 1.) * half_life_factor);
|
||||
self.rotation *= half_life_factor;
|
||||
self.translation *= half_life_factor;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,169 +4,167 @@ use egui::*;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct PaintBezier {
|
||||
/// Bézier curve degree, it can be 3, 4.
|
||||
degree: usize,
|
||||
/// Bézier curve degree, it can be 3, 4.
|
||||
degree: usize,
|
||||
|
||||
/// The control points. The [`Self::degree`] first of them are used.
|
||||
control_points: [Pos2; 4],
|
||||
/// The control points. The [`Self::degree`] first of them are used.
|
||||
control_points: [Pos2; 4],
|
||||
|
||||
/// Stroke for Bézier curve.
|
||||
stroke: Stroke,
|
||||
/// Stroke for Bézier curve.
|
||||
stroke: Stroke,
|
||||
|
||||
/// Fill for Bézier curve.
|
||||
fill: Color32,
|
||||
/// Fill for Bézier curve.
|
||||
fill: Color32,
|
||||
|
||||
/// Stroke for auxiliary lines.
|
||||
aux_stroke: Stroke,
|
||||
/// Stroke for auxiliary lines.
|
||||
aux_stroke: Stroke,
|
||||
|
||||
bounding_box_stroke: Stroke,
|
||||
bounding_box_stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Default for PaintBezier {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
degree: 4,
|
||||
control_points: [
|
||||
pos2(50.0, 50.0),
|
||||
pos2(60.0, 250.0),
|
||||
pos2(200.0, 200.0),
|
||||
pos2(250.0, 50.0),
|
||||
],
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25),
|
||||
aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
|
||||
bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
degree: 4,
|
||||
control_points: [
|
||||
pos2(50.0, 50.0),
|
||||
pos2(60.0, 250.0),
|
||||
pos2(200.0, 200.0),
|
||||
pos2(250.0, 50.0),
|
||||
],
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
fill: Color32::from_rgb(50, 100, 150).linear_multiply(0.25),
|
||||
aux_stroke: Stroke::new(1.0, Color32::RED.linear_multiply(0.25)),
|
||||
bounding_box_stroke: Stroke::new(0.0, Color32::LIGHT_GREEN.linear_multiply(0.25)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintBezier {
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) {
|
||||
ui.collapsing("Colors", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Fill color:");
|
||||
ui.color_edit_button_srgba(&mut self.fill);
|
||||
});
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
|
||||
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
|
||||
egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
|
||||
});
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) {
|
||||
ui.collapsing("Colors", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Fill color:");
|
||||
ui.color_edit_button_srgba(&mut self.fill);
|
||||
});
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Curve Stroke");
|
||||
egui::stroke_ui(ui, &mut self.aux_stroke, "Auxiliary Stroke");
|
||||
egui::stroke_ui(ui, &mut self.bounding_box_stroke, "Bounding Box Stroke");
|
||||
});
|
||||
|
||||
ui.collapsing("Global tessellation options", |ui| {
|
||||
let mut tessellation_options = *(ui.ctx().tessellation_options());
|
||||
let tessellation_options = &mut tessellation_options;
|
||||
tessellation_options.ui(ui);
|
||||
let mut new_tessellation_options = ui.ctx().tessellation_options();
|
||||
*new_tessellation_options = *tessellation_options;
|
||||
});
|
||||
ui.collapsing("Global tessellation options", |ui| {
|
||||
let mut tessellation_options = *(ui.ctx().tessellation_options());
|
||||
let tessellation_options = &mut tessellation_options;
|
||||
tessellation_options.ui(ui);
|
||||
let mut new_tessellation_options = ui.ctx().tessellation_options();
|
||||
*new_tessellation_options = *tessellation_options;
|
||||
});
|
||||
|
||||
ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");
|
||||
ui.radio_value(&mut self.degree, 4, "Cubic Bézier");
|
||||
ui.label("Move the points by dragging them.");
|
||||
ui.small("Only convex curves can be accurately filled.");
|
||||
ui.radio_value(&mut self.degree, 3, "Quadratic Bézier");
|
||||
ui.radio_value(&mut self.degree, 4, "Cubic Bézier");
|
||||
ui.label("Move the points by dragging them.");
|
||||
ui.small("Only convex curves can be accurately filled.");
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover());
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.size()),
|
||||
response.rect,
|
||||
);
|
||||
|
||||
let control_point_radius = 8.0;
|
||||
|
||||
let mut control_point_shapes = vec![];
|
||||
|
||||
for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) {
|
||||
let size = Vec2::splat(2.0 * control_point_radius);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let point_rect = Rect::from_center_size(point_in_screen, size);
|
||||
let point_id = response.id.with(i);
|
||||
let point_response = ui.interact(point_rect, point_id, Sense::drag());
|
||||
|
||||
*point += point_response.drag_delta();
|
||||
*point = to_screen.from().clamp(*point);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let stroke = ui.style().interact(&point_response).fg_stroke;
|
||||
|
||||
control_point_shapes.push(Shape::circle_stroke(
|
||||
point_in_screen,
|
||||
control_point_radius,
|
||||
stroke,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (response, painter) =
|
||||
ui.allocate_painter(Vec2::new(ui.available_width(), 300.0), Sense::hover());
|
||||
let points_in_screen: Vec<Pos2> = self
|
||||
.control_points
|
||||
.iter()
|
||||
.take(self.degree)
|
||||
.map(|p| to_screen * *p)
|
||||
.collect();
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.size()),
|
||||
response.rect,
|
||||
);
|
||||
match self.degree {
|
||||
3 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape = QuadraticBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
4 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape = CubicBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
let control_point_radius = 8.0;
|
||||
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
|
||||
painter.extend(control_point_shapes);
|
||||
|
||||
let mut control_point_shapes = vec![];
|
||||
|
||||
for (i, point) in self.control_points.iter_mut().enumerate().take(self.degree) {
|
||||
let size = Vec2::splat(2.0 * control_point_radius);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let point_rect = Rect::from_center_size(point_in_screen, size);
|
||||
let point_id = response.id.with(i);
|
||||
let point_response = ui.interact(point_rect, point_id, Sense::drag());
|
||||
|
||||
*point += point_response.drag_delta();
|
||||
*point = to_screen.from().clamp(*point);
|
||||
|
||||
let point_in_screen = to_screen.transform_pos(*point);
|
||||
let stroke = ui.style().interact(&point_response).fg_stroke;
|
||||
|
||||
control_point_shapes.push(Shape::circle_stroke(
|
||||
point_in_screen,
|
||||
control_point_radius,
|
||||
stroke,
|
||||
));
|
||||
}
|
||||
|
||||
let points_in_screen: Vec<Pos2> = self
|
||||
.control_points
|
||||
.iter()
|
||||
.take(self.degree)
|
||||
.map(|p| to_screen * *p)
|
||||
.collect();
|
||||
|
||||
match self.degree {
|
||||
3 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape =
|
||||
QuadraticBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
4 => {
|
||||
let points = points_in_screen.clone().try_into().unwrap();
|
||||
let shape =
|
||||
CubicBezierShape::from_points_stroke(points, true, self.fill, self.stroke);
|
||||
painter.add(epaint::RectShape::stroke(
|
||||
shape.visual_bounding_rect(),
|
||||
0.0,
|
||||
self.bounding_box_stroke,
|
||||
));
|
||||
painter.add(shape);
|
||||
}
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
|
||||
painter.add(PathShape::line(points_in_screen, self.aux_stroke));
|
||||
painter.extend(control_point_shapes);
|
||||
|
||||
response
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for PaintBezier {
|
||||
fn name(&self) -> &'static str {
|
||||
") Bézier Curve"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
") Bézier Curve"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.default_size([300.0, 350.0])
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(false)
|
||||
.default_size([300.0, 350.0])
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for PaintBezier {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,96 +3,96 @@ use egui::*;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Painting {
|
||||
/// in 0-1 normalized coordinates
|
||||
lines: Vec<Vec<Pos2>>,
|
||||
stroke: Stroke,
|
||||
/// in 0-1 normalized coordinates
|
||||
lines: Vec<Vec<Pos2>>,
|
||||
stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Default for Painting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lines: Default::default(),
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lines: Default::default(),
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(25, 200, 100)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Painting {
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.horizontal(|ui| {
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Stroke");
|
||||
ui.separator();
|
||||
if ui.button("Clear Painting").clicked() {
|
||||
self.lines.clear();
|
||||
}
|
||||
})
|
||||
.response
|
||||
pub fn ui_control(&mut self, ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.horizontal(|ui| {
|
||||
egui::stroke_ui(ui, &mut self.stroke, "Stroke");
|
||||
ui.separator();
|
||||
if ui.button("Clear Painting").clicked() {
|
||||
self.lines.clear();
|
||||
}
|
||||
})
|
||||
.response
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (mut response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
||||
response.rect,
|
||||
);
|
||||
let from_screen = to_screen.inverse();
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
pub fn ui_content(&mut self, ui: &mut Ui) -> egui::Response {
|
||||
let (mut response, painter) =
|
||||
ui.allocate_painter(ui.available_size_before_wrap(), Sense::drag());
|
||||
let current_line = self.lines.last_mut().unwrap();
|
||||
|
||||
let to_screen = emath::RectTransform::from_to(
|
||||
Rect::from_min_size(Pos2::ZERO, response.rect.square_proportions()),
|
||||
response.rect,
|
||||
);
|
||||
let from_screen = to_screen.inverse();
|
||||
|
||||
if self.lines.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
}
|
||||
|
||||
let current_line = self.lines.last_mut().unwrap();
|
||||
|
||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
let canvas_pos = from_screen * pointer_pos;
|
||||
if current_line.last() != Some(&canvas_pos) {
|
||||
current_line.push(canvas_pos);
|
||||
response.mark_changed();
|
||||
}
|
||||
} else if !current_line.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
let mut shapes = vec![];
|
||||
for line in &self.lines {
|
||||
if line.len() >= 2 {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| to_screen * *p).collect();
|
||||
shapes.push(egui::Shape::line(points, self.stroke));
|
||||
}
|
||||
}
|
||||
painter.extend(shapes);
|
||||
|
||||
response
|
||||
if let Some(pointer_pos) = response.interact_pointer_pos() {
|
||||
let canvas_pos = from_screen * pointer_pos;
|
||||
if current_line.last() != Some(&canvas_pos) {
|
||||
current_line.push(canvas_pos);
|
||||
response.mark_changed();
|
||||
}
|
||||
} else if !current_line.is_empty() {
|
||||
self.lines.push(vec![]);
|
||||
response.mark_changed();
|
||||
}
|
||||
|
||||
let mut shapes = vec![];
|
||||
for line in &self.lines {
|
||||
if line.len() >= 2 {
|
||||
let points: Vec<Pos2> = line.iter().map(|p| to_screen * *p).collect();
|
||||
shapes.push(egui::Shape::line(points, self.stroke));
|
||||
}
|
||||
}
|
||||
painter.extend(shapes);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Painting {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🖊 Painting"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
Window::new(self.name())
|
||||
.open(open)
|
||||
.default_size(vec2(512.0, 512.0))
|
||||
.vscroll(false)
|
||||
.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Painting {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
ui.label("Paint with your mouse/touch!");
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
self.ui_control(ui);
|
||||
ui.label("Paint with your mouse/touch!");
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.ui_content(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,44 +10,44 @@
|
||||
/// ```
|
||||
#[allow(clippy::ptr_arg)] // false positive
|
||||
pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
// It must implement at least `Clone` and be `'static`.
|
||||
// If you use the `persistence` feature, it also must implement `serde::{Deserialize, Serialize}`.
|
||||
// This widget has its own state — show or hide password characters (`show_plaintext`).
|
||||
// In this case we use a simple `bool`, but you can also declare your own type.
|
||||
// It must implement at least `Clone` and be `'static`.
|
||||
// If you use the `persistence` feature, it also must implement `serde::{Deserialize, Serialize}`.
|
||||
|
||||
// Generate an id for the state
|
||||
let state_id = ui.id().with("show_plaintext");
|
||||
// Generate an id for the state
|
||||
let state_id = ui.id().with("show_plaintext");
|
||||
|
||||
// Get state for this widget.
|
||||
// You should get state by value, not by reference to avoid borrowing of [`Memory`].
|
||||
let mut show_plaintext = ui.data().get_temp::<bool>(state_id).unwrap_or(false);
|
||||
// Get state for this widget.
|
||||
// You should get state by value, not by reference to avoid borrowing of [`Memory`].
|
||||
let mut show_plaintext = ui.data().get_temp::<bool>(state_id).unwrap_or(false);
|
||||
|
||||
// Process ui, change a local copy of the state
|
||||
// We want TextEdit to fill entire space, and have button after that, so in that case we can
|
||||
// change direction to right_to_left.
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.on_hover_text("Show/hide password");
|
||||
// Process ui, change a local copy of the state
|
||||
// We want TextEdit to fill entire space, and have button after that, so in that case we can
|
||||
// change direction to right_to_left.
|
||||
let result = ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
// Toggle the `show_plaintext` bool with a button:
|
||||
let response = ui
|
||||
.add(egui::SelectableLabel::new(show_plaintext, "👁"))
|
||||
.on_hover_text("Show/hide password");
|
||||
|
||||
if response.clicked() {
|
||||
show_plaintext = !show_plaintext;
|
||||
}
|
||||
if response.clicked() {
|
||||
show_plaintext = !show_plaintext;
|
||||
}
|
||||
|
||||
// Show the password field:
|
||||
ui.add_sized(
|
||||
ui.available_size(),
|
||||
egui::TextEdit::singleline(password).password(!show_plaintext),
|
||||
);
|
||||
});
|
||||
// Show the password field:
|
||||
ui.add_sized(
|
||||
ui.available_size(),
|
||||
egui::TextEdit::singleline(password).password(!show_plaintext),
|
||||
);
|
||||
});
|
||||
|
||||
// Store the (possibly changed) state:
|
||||
ui.data().insert_temp(state_id, show_plaintext);
|
||||
// Store the (possibly changed) state:
|
||||
ui.data().insert_temp(state_id, show_plaintext);
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, …) and maybe show a tooltip:
|
||||
result.response
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, …) and maybe show a tooltip:
|
||||
result.response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(…)`
|
||||
@@ -58,9 +58,9 @@ pub fn password_ui(ui: &mut egui::Ui, password: &mut String) -> egui::Response {
|
||||
/// ui.add(password(&mut my_password));
|
||||
/// ```
|
||||
pub fn password(password: &mut String) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| password_ui(ui, password)
|
||||
move |ui: &mut egui::Ui| password_ui(ui, password)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,141 +3,139 @@ use egui::{color::*, *};
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum ScrollDemo {
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
LargeCanvas,
|
||||
StickToEnd,
|
||||
ScrollTo,
|
||||
ManyLines,
|
||||
LargeCanvas,
|
||||
StickToEnd,
|
||||
}
|
||||
|
||||
impl Default for ScrollDemo {
|
||||
fn default() -> Self {
|
||||
Self::ScrollTo
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::ScrollTo
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
pub struct Scrolling {
|
||||
demo: ScrollDemo,
|
||||
scroll_to: ScrollTo,
|
||||
scroll_stick_to: ScrollStickTo,
|
||||
demo: ScrollDemo,
|
||||
scroll_to: ScrollTo,
|
||||
scroll_stick_to: ScrollStickTo,
|
||||
}
|
||||
|
||||
impl super::Demo for Scrolling {
|
||||
fn name(&self) -> &'static str {
|
||||
"↕ Scrolling"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"↕ Scrolling"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Scrolling {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to");
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::ManyLines,
|
||||
"Scroll a lot of lines",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::LargeCanvas,
|
||||
"Scroll a large canvas",
|
||||
);
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::StickToEnd, "Stick to end");
|
||||
});
|
||||
ui.separator();
|
||||
match self.demo {
|
||||
ScrollDemo::ScrollTo => {
|
||||
self.scroll_to.ui(ui);
|
||||
}
|
||||
ScrollDemo::ManyLines => {
|
||||
huge_content_lines(ui);
|
||||
}
|
||||
ScrollDemo::LargeCanvas => {
|
||||
huge_content_painter(ui);
|
||||
}
|
||||
ScrollDemo::StickToEnd => {
|
||||
self.scroll_stick_to.ui(ui);
|
||||
}
|
||||
}
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::ScrollTo, "Scroll to");
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::ManyLines,
|
||||
"Scroll a lot of lines",
|
||||
);
|
||||
ui.selectable_value(
|
||||
&mut self.demo,
|
||||
ScrollDemo::LargeCanvas,
|
||||
"Scroll a large canvas",
|
||||
);
|
||||
ui.selectable_value(&mut self.demo, ScrollDemo::StickToEnd, "Stick to end");
|
||||
});
|
||||
ui.separator();
|
||||
match self.demo {
|
||||
ScrollDemo::ScrollTo => {
|
||||
self.scroll_to.ui(ui);
|
||||
}
|
||||
ScrollDemo::ManyLines => {
|
||||
huge_content_lines(ui);
|
||||
}
|
||||
ScrollDemo::LargeCanvas => {
|
||||
huge_content_painter(ui);
|
||||
}
|
||||
ScrollDemo::StickToEnd => {
|
||||
self.scroll_stick_to.ui(ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn huge_content_lines(ui: &mut egui::Ui) {
|
||||
ui.label(
|
||||
"A lot of rows, but only the visible ones are layed out, so performance is still good:",
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.label("A lot of rows, but only the visible ones are layed out, so performance is still good:");
|
||||
ui.add_space(4.0);
|
||||
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
let num_rows = 10_000;
|
||||
ScrollArea::vertical().auto_shrink([false; 2]).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
num_rows,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}/{}", row + 1, num_rows);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
let num_rows = 10_000;
|
||||
ScrollArea::vertical().auto_shrink([false; 2]).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
num_rows,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}/{}", row + 1, num_rows);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
// This is similar to the other demo, but is fully manual, for when you want to do custom painting.
|
||||
ui.label("A lot of rows, but only the visible ones are painted, so performance is still good:");
|
||||
ui.add_space(4.0);
|
||||
// This is similar to the other demo, but is fully manual, for when you want to do custom painting.
|
||||
ui.label("A lot of rows, but only the visible ones are painted, so performance is still good:");
|
||||
ui.add_space(4.0);
|
||||
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id) + ui.spacing().item_spacing.y;
|
||||
let num_rows = 10_000;
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id) + ui.spacing().item_spacing.y;
|
||||
let num_rows = 10_000;
|
||||
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
ui.set_height(row_height * num_rows as f32);
|
||||
ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show_viewport(ui, |ui, viewport| {
|
||||
ui.set_height(row_height * num_rows as f32);
|
||||
|
||||
let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize;
|
||||
let last_item = (viewport.max.y / row_height).ceil() as usize + 1;
|
||||
let last_item = last_item.at_most(num_rows);
|
||||
let first_item = (viewport.min.y / row_height).floor().at_least(0.0) as usize;
|
||||
let last_item = (viewport.max.y / row_height).ceil() as usize + 1;
|
||||
let last_item = last_item.at_most(num_rows);
|
||||
|
||||
let mut used_rect = Rect::NOTHING;
|
||||
let mut used_rect = Rect::NOTHING;
|
||||
|
||||
for i in first_item..last_item {
|
||||
let indentation = (i % 100) as f32;
|
||||
let x = ui.min_rect().left() + indentation;
|
||||
let y = ui.min_rect().top() + i as f32 * row_height;
|
||||
let text = format!(
|
||||
"This is row {}/{}, indented by {} pixels",
|
||||
i + 1,
|
||||
num_rows,
|
||||
indentation
|
||||
);
|
||||
let text_rect = ui.painter().text(
|
||||
pos2(x, y),
|
||||
Align2::LEFT_TOP,
|
||||
text,
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
used_rect = used_rect.union(text_rect);
|
||||
}
|
||||
for i in first_item..last_item {
|
||||
let indentation = (i % 100) as f32;
|
||||
let x = ui.min_rect().left() + indentation;
|
||||
let y = ui.min_rect().top() + i as f32 * row_height;
|
||||
let text = format!(
|
||||
"This is row {}/{}, indented by {} pixels",
|
||||
i + 1,
|
||||
num_rows,
|
||||
indentation
|
||||
);
|
||||
let text_rect = ui.painter().text(
|
||||
pos2(x, y),
|
||||
Align2::LEFT_TOP,
|
||||
text,
|
||||
font_id.clone(),
|
||||
ui.visuals().text_color(),
|
||||
);
|
||||
used_rect = used_rect.union(text_rect);
|
||||
}
|
||||
|
||||
ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible!
|
||||
});
|
||||
ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible!
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -146,144 +144,143 @@ fn huge_content_painter(ui: &mut egui::Ui) {
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(PartialEq)]
|
||||
struct ScrollTo {
|
||||
track_item: usize,
|
||||
tack_item_align: Option<Align>,
|
||||
offset: f32,
|
||||
track_item: usize,
|
||||
tack_item_align: Option<Align>,
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
impl Default for ScrollTo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
track_item: 25,
|
||||
tack_item_align: Some(Align::Center),
|
||||
offset: 0.0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
track_item: 25,
|
||||
tack_item_align: Some(Align::Center),
|
||||
offset: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ScrollTo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This shows how you can scroll to a specific item or pixel offset");
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This shows how you can scroll to a specific item or pixel offset");
|
||||
|
||||
let mut track_item = false;
|
||||
let mut go_to_scroll_offset = false;
|
||||
let mut scroll_top = false;
|
||||
let mut scroll_bottom = false;
|
||||
let mut track_item = false;
|
||||
let mut go_to_scroll_offset = false;
|
||||
let mut scroll_top = false;
|
||||
let mut scroll_bottom = false;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific item index:");
|
||||
track_item |= ui
|
||||
.add(Slider::new(&mut self.track_item, 1..=50).text("Track Item"))
|
||||
.dragged();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific item index:");
|
||||
track_item |= ui
|
||||
.add(Slider::new(&mut self.track_item, 1..=50).text("Track Item"))
|
||||
.dragged();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Item align:");
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Min), "Top")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Center), "Center")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Max), "Bottom")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, None, "None (Bring into view)")
|
||||
.clicked();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific offset:");
|
||||
go_to_scroll_offset |= ui
|
||||
.add(DragValue::new(&mut self.offset).speed(1.0).suffix("px"))
|
||||
.dragged();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
scroll_top |= ui.button("Scroll to top").clicked();
|
||||
scroll_bottom |= ui.button("Scroll to bottom").clicked();
|
||||
});
|
||||
|
||||
let mut scroll_area = ScrollArea::vertical()
|
||||
.max_height(200.0)
|
||||
.auto_shrink([false; 2]);
|
||||
if go_to_scroll_offset {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(self.offset);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
let (current_scroll, max_scroll) = scroll_area
|
||||
.show(ui, |ui| {
|
||||
if scroll_top {
|
||||
ui.scroll_to_cursor(Some(Align::TOP));
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
for item in 1..=50 {
|
||||
if track_item && item == self.track_item {
|
||||
let response = ui.colored_label(Color32::YELLOW, format!("This is item {}", item));
|
||||
response.scroll_to_me(self.tack_item_align);
|
||||
} else {
|
||||
ui.label(format!("This is item {}", item));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Item align:");
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Min), "Top")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Center), "Center")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, Some(Align::Max), "Bottom")
|
||||
.clicked();
|
||||
track_item |= ui
|
||||
.radio_value(&mut self.tack_item_align, None, "None (Bring into view)")
|
||||
.clicked();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Scroll to a specific offset:");
|
||||
go_to_scroll_offset |= ui
|
||||
.add(DragValue::new(&mut self.offset).speed(1.0).suffix("px"))
|
||||
.dragged();
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
scroll_top |= ui.button("Scroll to top").clicked();
|
||||
scroll_bottom |= ui.button("Scroll to bottom").clicked();
|
||||
});
|
||||
|
||||
let mut scroll_area = ScrollArea::vertical()
|
||||
.max_height(200.0)
|
||||
.auto_shrink([false; 2]);
|
||||
if go_to_scroll_offset {
|
||||
scroll_area = scroll_area.vertical_scroll_offset(self.offset);
|
||||
if scroll_bottom {
|
||||
ui.scroll_to_cursor(Some(Align::BOTTOM));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
let (current_scroll, max_scroll) = scroll_area
|
||||
.show(ui, |ui| {
|
||||
if scroll_top {
|
||||
ui.scroll_to_cursor(Some(Align::TOP));
|
||||
}
|
||||
ui.vertical(|ui| {
|
||||
for item in 1..=50 {
|
||||
if track_item && item == self.track_item {
|
||||
let response =
|
||||
ui.colored_label(Color32::YELLOW, format!("This is item {}", item));
|
||||
response.scroll_to_me(self.tack_item_align);
|
||||
} else {
|
||||
ui.label(format!("This is item {}", item));
|
||||
}
|
||||
}
|
||||
});
|
||||
let margin = ui.visuals().clip_rect_margin;
|
||||
|
||||
if scroll_bottom {
|
||||
ui.scroll_to_cursor(Some(Align::BOTTOM));
|
||||
}
|
||||
let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
|
||||
let max_scroll = ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
|
||||
(current_scroll, max_scroll)
|
||||
})
|
||||
.inner;
|
||||
ui.separator();
|
||||
|
||||
let margin = ui.visuals().clip_rect_margin;
|
||||
ui.label(format!(
|
||||
"Scroll offset: {:.0}/{:.0} px",
|
||||
current_scroll, max_scroll
|
||||
));
|
||||
|
||||
let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
|
||||
let max_scroll = ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
|
||||
(current_scroll, max_scroll)
|
||||
})
|
||||
.inner;
|
||||
ui.separator();
|
||||
|
||||
ui.label(format!(
|
||||
"Scroll offset: {:.0}/{:.0} px",
|
||||
current_scroll, max_scroll
|
||||
));
|
||||
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Default, PartialEq)]
|
||||
struct ScrollStickTo {
|
||||
n_items: usize,
|
||||
n_items: usize,
|
||||
}
|
||||
|
||||
impl super::View for ScrollStickTo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Rows enter from the bottom, we want the scroll handle to start and stay at bottom unless moved");
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("Rows enter from the bottom, we want the scroll handle to start and stay at bottom unless moved");
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.add_space(4.0);
|
||||
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
self.n_items,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}", row + 1);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
let text_style = TextStyle::Body;
|
||||
let row_height = ui.text_style_height(&text_style);
|
||||
ScrollArea::vertical().stick_to_bottom(true).show_rows(
|
||||
ui,
|
||||
row_height,
|
||||
self.n_items,
|
||||
|ui, row_range| {
|
||||
for row in row_range {
|
||||
let text = format!("This is row {}", row + 1);
|
||||
ui.label(text);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
self.n_items += 1;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
self.n_items += 1;
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,177 +6,177 @@ use std::f64::INFINITY;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct Sliders {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub logarithmic: bool,
|
||||
pub clamp_to_range: bool,
|
||||
pub smart_aim: bool,
|
||||
pub step: f64,
|
||||
pub use_steps: bool,
|
||||
pub integer: bool,
|
||||
pub vertical: bool,
|
||||
pub value: f64,
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub logarithmic: bool,
|
||||
pub clamp_to_range: bool,
|
||||
pub smart_aim: bool,
|
||||
pub step: f64,
|
||||
pub use_steps: bool,
|
||||
pub integer: bool,
|
||||
pub vertical: bool,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
impl Default for Sliders {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min: 0.0,
|
||||
max: 10000.0,
|
||||
logarithmic: true,
|
||||
clamp_to_range: false,
|
||||
smart_aim: true,
|
||||
step: 10.0,
|
||||
use_steps: false,
|
||||
integer: false,
|
||||
vertical: false,
|
||||
value: 10.0,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min: 0.0,
|
||||
max: 10000.0,
|
||||
logarithmic: true,
|
||||
clamp_to_range: false,
|
||||
smart_aim: true,
|
||||
step: 10.0,
|
||||
use_steps: false,
|
||||
integer: false,
|
||||
vertical: false,
|
||||
value: 10.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for Sliders {
|
||||
fn name(&self) -> &'static str {
|
||||
"⬌ Sliders"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"⬌ Sliders"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for Sliders {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
min,
|
||||
max,
|
||||
logarithmic,
|
||||
clamp_to_range,
|
||||
smart_aim,
|
||||
step,
|
||||
use_steps,
|
||||
integer,
|
||||
vertical,
|
||||
value,
|
||||
} = self;
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
let Self {
|
||||
min,
|
||||
max,
|
||||
logarithmic,
|
||||
clamp_to_range,
|
||||
smart_aim,
|
||||
step,
|
||||
use_steps,
|
||||
integer,
|
||||
vertical,
|
||||
value,
|
||||
} = self;
|
||||
|
||||
ui.label("You can click a slider value to edit it with the keyboard.");
|
||||
ui.label("You can click a slider value to edit it with the keyboard.");
|
||||
|
||||
let (type_min, type_max) = if *integer {
|
||||
((i32::MIN as f64), (i32::MAX as f64))
|
||||
} else if *logarithmic {
|
||||
(-INFINITY, INFINITY)
|
||||
} else {
|
||||
(-1e5, 1e5) // linear sliders make little sense with huge numbers
|
||||
};
|
||||
let (type_min, type_max) = if *integer {
|
||||
((i32::MIN as f64), (i32::MAX as f64))
|
||||
} else if *logarithmic {
|
||||
(-INFINITY, INFINITY)
|
||||
} else {
|
||||
(-1e5, 1e5) // linear sliders make little sense with huge numbers
|
||||
};
|
||||
|
||||
*min = min.clamp(type_min, type_max);
|
||||
*max = max.clamp(type_min, type_max);
|
||||
*min = min.clamp(type_min, type_max);
|
||||
*max = max.clamp(type_min, type_max);
|
||||
|
||||
let orientation = if *vertical {
|
||||
SliderOrientation::Vertical
|
||||
} else {
|
||||
SliderOrientation::Horizontal
|
||||
};
|
||||
let orientation = if *vertical {
|
||||
SliderOrientation::Vertical
|
||||
} else {
|
||||
SliderOrientation::Horizontal
|
||||
};
|
||||
|
||||
let istep = if *use_steps { *step } else { 0.0 };
|
||||
if *integer {
|
||||
let mut value_i32 = *value as i32;
|
||||
ui.add(
|
||||
Slider::new(&mut value_i32, (*min as i32)..=(*max as i32))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("i32 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
*value = value_i32 as f64;
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(value, (*min)..=(*max))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("f64 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
let istep = if *use_steps { *step } else { 0.0 };
|
||||
if *integer {
|
||||
let mut value_i32 = *value as i32;
|
||||
ui.add(
|
||||
Slider::new(&mut value_i32, (*min as i32)..=(*max as i32))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("i32 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
*value = value_i32 as f64;
|
||||
} else {
|
||||
ui.add(
|
||||
Slider::new(value, (*min)..=(*max))
|
||||
.logarithmic(*logarithmic)
|
||||
.clamp_to_range(*clamp_to_range)
|
||||
.smart_aim(*smart_aim)
|
||||
.orientation(orientation)
|
||||
.text("f64 demo slider")
|
||||
.step_by(istep),
|
||||
);
|
||||
|
||||
ui.label(
|
||||
"Sliders will intelligently pick how many decimals to show. \
|
||||
ui.label(
|
||||
"Sliders will intelligently pick how many decimals to show. \
|
||||
You can always see the full precision value by hovering the value.",
|
||||
);
|
||||
);
|
||||
|
||||
if ui.button("Assign PI").clicked() {
|
||||
self.value = std::f64::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Slider range:");
|
||||
ui.add(
|
||||
Slider::new(min, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("left"),
|
||||
);
|
||||
ui.add(
|
||||
Slider::new(max, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("right"),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.checkbox(use_steps, "Use steps");
|
||||
ui.label("When enabled, the minimal value change would be restricted to a given step.");
|
||||
if *use_steps {
|
||||
ui.add(egui::DragValue::new(step).speed(1.0));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider type:");
|
||||
ui.radio_value(integer, true, "i32");
|
||||
ui.radio_value(integer, false, "f64");
|
||||
})
|
||||
.response
|
||||
.on_hover_text("All numeric types (f32, usize, …) are supported.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider orientation:");
|
||||
ui.radio_value(vertical, false, "Horizontal");
|
||||
ui.radio_value(vertical, true, "Vertical");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(logarithmic, "Logarithmic");
|
||||
ui.label("Logarithmic sliders are great for when you want to span a huge range, i.e. from zero to a million.");
|
||||
ui.label("Logarithmic sliders can include infinity and zero.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(clamp_to_range, "Clamp to range");
|
||||
ui.label("If true, the slider will clamp incoming and outgoing values to the given range.");
|
||||
ui.label("If false, the slider can shows values outside its range, and you can manually enter values outside the range.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(smart_aim, "Smart Aim");
|
||||
ui.label("Smart Aim will guide you towards round values when you drag the slider so you you are more likely to hit 250 than 247.23");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
if ui.button("Assign PI").clicked() {
|
||||
self.value = std::f64::consts::PI;
|
||||
}
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("Slider range:");
|
||||
ui.add(
|
||||
Slider::new(min, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("left"),
|
||||
);
|
||||
ui.add(
|
||||
Slider::new(max, type_min..=type_max)
|
||||
.logarithmic(true)
|
||||
.smart_aim(*smart_aim)
|
||||
.text("right"),
|
||||
);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.checkbox(use_steps, "Use steps");
|
||||
ui.label("When enabled, the minimal value change would be restricted to a given step.");
|
||||
if *use_steps {
|
||||
ui.add(egui::DragValue::new(step).speed(1.0));
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider type:");
|
||||
ui.radio_value(integer, true, "i32");
|
||||
ui.radio_value(integer, false, "f64");
|
||||
})
|
||||
.response
|
||||
.on_hover_text("All numeric types (f32, usize, …) are supported.");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Slider orientation:");
|
||||
ui.radio_value(vertical, false, "Horizontal");
|
||||
ui.radio_value(vertical, true, "Vertical");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(logarithmic, "Logarithmic");
|
||||
ui.label("Logarithmic sliders are great for when you want to span a huge range, i.e. from zero to a million.");
|
||||
ui.label("Logarithmic sliders can include infinity and zero.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(clamp_to_range, "Clamp to range");
|
||||
ui.label("If true, the slider will clamp incoming and outgoing values to the given range.");
|
||||
ui.label("If false, the slider can shows values outside its range, and you can manually enter values outside the range.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.checkbox(smart_aim, "Smart Aim");
|
||||
ui.label("Smart Aim will guide you towards round values when you drag the slider so you you are more likely to hit 250 than 247.23");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,113 +7,113 @@ use egui_extras::{Size, StripBuilder};
|
||||
pub struct StripDemo {}
|
||||
|
||||
impl super::Demo for StripDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"▣ Strip Demo"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"▣ Strip Demo"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for StripDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let dark_mode = ui.visuals().dark_mode;
|
||||
let faded_color = ui.visuals().window_fill();
|
||||
let faded_color = |color: Color32| -> Color32 {
|
||||
use egui::Rgba;
|
||||
let t = if dark_mode { 0.95 } else { 0.8 };
|
||||
egui::lerp(Rgba::from(color)..=Rgba::from(faded_color), t).into()
|
||||
};
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let dark_mode = ui.visuals().dark_mode;
|
||||
let faded_color = ui.visuals().window_fill();
|
||||
let faded_color = |color: Color32| -> Color32 {
|
||||
use egui::Rgba;
|
||||
let t = if dark_mode { 0.95 } else { 0.8 };
|
||||
egui::lerp(Rgba::from(color)..=Rgba::from(faded_color), t).into()
|
||||
};
|
||||
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(50.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::relative(0.5).at_least(60.0))
|
||||
.size(Size::exact(10.0))
|
||||
.vertical(|mut strip| {
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(50.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::relative(0.5).at_least(60.0))
|
||||
.size(Size::exact(10.0))
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::BLUE),
|
||||
);
|
||||
ui.label("width: 100%\nheight: 50px");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::RED),
|
||||
);
|
||||
ui.label("width: 50%\nheight: remaining");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::YELLOW),
|
||||
);
|
||||
ui.label("width: 50%\nheight: 1/3 of the red region");
|
||||
});
|
||||
strip.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(120.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(70.0))
|
||||
.horizontal(|mut strip| {
|
||||
strip.empty();
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(60.0))
|
||||
.size(Size::remainder())
|
||||
.vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::BLUE),
|
||||
);
|
||||
ui.label("width: 100%\nheight: 50px");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::RED),
|
||||
);
|
||||
ui.label("width: 50%\nheight: remaining");
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder.sizes(Size::remainder(), 3).vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::YELLOW),
|
||||
);
|
||||
ui.label("width: 50%\nheight: 1/3 of the red region");
|
||||
});
|
||||
strip.empty();
|
||||
});
|
||||
});
|
||||
faded_color(Color32::GOLD),
|
||||
);
|
||||
ui.label("width: 120px\nheight: 60px");
|
||||
});
|
||||
});
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(120.0))
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(70.0))
|
||||
.horizontal(|mut strip| {
|
||||
strip.empty();
|
||||
strip.strip(|builder| {
|
||||
builder
|
||||
.size(Size::remainder())
|
||||
.size(Size::exact(60.0))
|
||||
.size(Size::remainder())
|
||||
.vertical(|mut strip| {
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GOLD),
|
||||
);
|
||||
ui.label("width: 120px\nheight: 60px");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GREEN),
|
||||
);
|
||||
ui.label("width: 70px\n\nheight: 50%, but at least 60px.");
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
strip.empty();
|
||||
strip.cell(|ui| {
|
||||
ui.painter().rect_filled(
|
||||
ui.available_rect_before_wrap(),
|
||||
0.0,
|
||||
faded_color(Color32::GREEN),
|
||||
);
|
||||
ui.label("width: 70px\n\nheight: 50%, but at least 60px.");
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +1,197 @@
|
||||
#[derive(PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum DemoType {
|
||||
Manual,
|
||||
ManyHomogenous,
|
||||
ManyHeterogenous,
|
||||
Manual,
|
||||
ManyHomogenous,
|
||||
ManyHeterogenous,
|
||||
}
|
||||
|
||||
/// Shows off a table with dynamic layout
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct TableDemo {
|
||||
demo: DemoType,
|
||||
resizable: bool,
|
||||
num_rows: usize,
|
||||
demo: DemoType,
|
||||
resizable: bool,
|
||||
num_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for TableDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
resizable: true,
|
||||
num_rows: 10_000,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
demo: DemoType::Manual,
|
||||
resizable: true,
|
||||
num_rows: 10_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TableDemo {
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Table Demo"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"☰ Table Demo"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(400.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TableDemo {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
|
||||
ui.label("Table type:");
|
||||
ui.radio_value(&mut self.demo, DemoType::Manual, "Few, manual rows");
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHomogenous,
|
||||
"Thousands of rows of same height",
|
||||
);
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHeterogenous,
|
||||
"Thousands of rows of differing heights",
|
||||
);
|
||||
ui.label("Table type:");
|
||||
ui.radio_value(&mut self.demo, DemoType::Manual, "Few, manual rows");
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHomogenous,
|
||||
"Thousands of rows of same height",
|
||||
);
|
||||
ui.radio_value(
|
||||
&mut self.demo,
|
||||
DemoType::ManyHeterogenous,
|
||||
"Thousands of rows of differing heights",
|
||||
);
|
||||
|
||||
if self.demo != DemoType::Manual {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.num_rows, 0..=100_000)
|
||||
.logarithmic(true)
|
||||
.text("Num rows"),
|
||||
);
|
||||
}
|
||||
if self.demo != DemoType::Manual {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.num_rows, 0..=100_000)
|
||||
.logarithmic(true)
|
||||
.text("Num rows"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Leave room for the source code link after the table demo:
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder()) // for the table
|
||||
.size(Size::exact(10.0)) // for the source code link
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
self.table_ui(ui);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// Leave room for the source code link after the table demo:
|
||||
use egui_extras::{Size, StripBuilder};
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::remainder()) // for the table
|
||||
.size(Size::exact(10.0)) // for the source code link
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| {
|
||||
self.table_ui(ui);
|
||||
});
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
strip.cell(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TableDemo {
|
||||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui_extras::{Size, TableBuilder};
|
||||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui_extras::{Size, TableBuilder};
|
||||
|
||||
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||
|
||||
TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::remainder().at_least(60.0))
|
||||
.resizable(self.resizable)
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.heading("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Clock");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Content");
|
||||
});
|
||||
})
|
||||
.body(|mut body| match self.demo {
|
||||
DemoType::Manual => {
|
||||
for row_index in 0..20 {
|
||||
let is_thick = thick_row(row_index);
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if is_thick {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
DemoType::ManyHomogenous => {
|
||||
body.rows(text_height, self.num_rows, |row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.add(
|
||||
egui::Label::new("Thousands of rows of even height").wrap(false),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
DemoType::ManyHeterogenous => {
|
||||
fn row_thickness(row_index: usize) -> f32 {
|
||||
if thick_row(row_index) {
|
||||
30.0
|
||||
} else {
|
||||
18.0
|
||||
}
|
||||
}
|
||||
body.heterogeneous_rows(
|
||||
(0..self.num_rows).into_iter().map(row_thickness),
|
||||
|row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
TableBuilder::new(ui)
|
||||
.striped(true)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::initial(60.0).at_least(40.0))
|
||||
.column(Size::remainder().at_least(60.0))
|
||||
.resizable(self.resizable)
|
||||
.header(20.0, |mut header| {
|
||||
header.col(|ui| {
|
||||
ui.heading("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Clock");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.heading("Content");
|
||||
});
|
||||
})
|
||||
.body(|mut body| match self.demo {
|
||||
DemoType::Manual => {
|
||||
for row_index in 0..20 {
|
||||
let is_thick = thick_row(row_index);
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if is_thick {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
DemoType::ManyHomogenous => {
|
||||
body.rows(text_height, self.num_rows, |row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.add(egui::Label::new("Thousands of rows of even height").wrap(false));
|
||||
});
|
||||
});
|
||||
}
|
||||
DemoType::ManyHeterogenous => {
|
||||
fn row_thickness(row_index: usize) -> f32 {
|
||||
if thick_row(row_index) {
|
||||
30.0
|
||||
} else {
|
||||
18.0
|
||||
}
|
||||
}
|
||||
body.heterogeneous_rows(
|
||||
(0..self.num_rows).into_iter().map(row_thickness),
|
||||
|row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label(clock_emoji(row_index));
|
||||
});
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_emoji(row_index: usize) -> String {
|
||||
char::from_u32(0x1f550 + row_index as u32 % 24)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
char::from_u32(0x1f550 + row_index as u32 % 24)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn thick_row(row_index: usize) -> bool {
|
||||
row_index % 6 == 0
|
||||
row_index % 6 == 0
|
||||
}
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
pub struct CursorTest {}
|
||||
|
||||
impl super::Demo for CursorTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Cursor Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Cursor Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for CursorTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.heading("Hover to switch cursor icon:");
|
||||
for &cursor_icon in &egui::CursorIcon::ALL {
|
||||
let _ = ui
|
||||
.button(format!("{:?}", cursor_icon))
|
||||
.on_hover_cursor(cursor_icon);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
ui.heading("Hover to switch cursor icon:");
|
||||
for &cursor_icon in &egui::CursorIcon::ALL {
|
||||
let _ = ui
|
||||
.button(format!("{:?}", cursor_icon))
|
||||
.on_hover_cursor(cursor_icon);
|
||||
}
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -34,37 +34,37 @@ impl super::View for CursorTest {
|
||||
pub struct IdTest {}
|
||||
|
||||
impl super::Demo for IdTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"ID Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"ID Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for IdTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Name collision example");
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.heading("Name collision example");
|
||||
|
||||
ui.label("\
|
||||
ui.label("\
|
||||
Widgets that store state require unique and persisting identifiers so we can track their state between frames.\n\
|
||||
For instance, collapsable headers needs to store whether or not they are open. \
|
||||
Their Id:s are derived from their names. \
|
||||
If you fail to give them unique names then clicking one will open both. \
|
||||
To help you debug this, an error message is printed on screen:");
|
||||
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of first foldable ui");
|
||||
});
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of second foldable ui");
|
||||
});
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of first foldable ui");
|
||||
});
|
||||
ui.collapsing("Collapsing header", |ui| {
|
||||
ui.label("Contents of second foldable ui");
|
||||
});
|
||||
|
||||
ui.label("\
|
||||
ui.label("\
|
||||
Any widget that can be interacted with also need a unique Id. \
|
||||
For most widgets the Id is generated by a running counter. \
|
||||
As long as elements are not added or removed, the Id stays the same. \
|
||||
@@ -72,233 +72,228 @@ impl super::View for IdTest {
|
||||
the number of widgets previously in the same window is most likely not changing \
|
||||
(and if it is, the window will have a new layout, and the slider will endup somewhere else, and so aborthing the interaction probably makes sense).");
|
||||
|
||||
ui.label("So these buttons have automatic Id:s, and therefore there is no name clash:");
|
||||
let _ = ui.button("Button");
|
||||
let _ = ui.button("Button");
|
||||
ui.label("So these buttons have automatic Id:s, and therefore there is no name clash:");
|
||||
let _ = ui.button("Button");
|
||||
let _ = ui.button("Button");
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum WidgetType {
|
||||
Label,
|
||||
Button,
|
||||
TextEdit,
|
||||
Label,
|
||||
Button,
|
||||
TextEdit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ManualLayoutTest {
|
||||
widget_offset: egui::Vec2,
|
||||
widget_size: egui::Vec2,
|
||||
widget_type: WidgetType,
|
||||
text_edit_contents: String,
|
||||
widget_offset: egui::Vec2,
|
||||
widget_size: egui::Vec2,
|
||||
widget_type: WidgetType,
|
||||
text_edit_contents: String,
|
||||
}
|
||||
|
||||
impl Default for ManualLayoutTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_offset: egui::Vec2::splat(150.0),
|
||||
widget_size: egui::vec2(200.0, 100.0),
|
||||
widget_type: WidgetType::Button,
|
||||
text_edit_contents: crate::LOREM_IPSUM.to_owned(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
widget_offset: egui::Vec2::splat(150.0),
|
||||
widget_size: egui::vec2(200.0, 100.0),
|
||||
widget_type: WidgetType::Button,
|
||||
text_edit_contents: crate::LOREM_IPSUM.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for ManualLayoutTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Manual Layout Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Manual Layout Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.resizable(false)
|
||||
.open(open)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ManualLayoutTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::reset_button(ui, self);
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::reset_button(ui, self);
|
||||
|
||||
let Self {
|
||||
widget_offset,
|
||||
widget_size,
|
||||
widget_type,
|
||||
text_edit_contents,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Test widget:");
|
||||
ui.radio_value(widget_type, WidgetType::Button, "Button");
|
||||
ui.radio_value(widget_type, WidgetType::Label, "Label");
|
||||
ui.radio_value(widget_type, WidgetType::TextEdit, "TextEdit");
|
||||
});
|
||||
egui::Grid::new("pos_size").show(ui, |ui| {
|
||||
ui.label("Widget position:");
|
||||
ui.add(egui::Slider::new(&mut widget_offset.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_offset.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
let Self {
|
||||
widget_offset,
|
||||
widget_size,
|
||||
widget_type,
|
||||
text_edit_contents,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Test widget:");
|
||||
ui.radio_value(widget_type, WidgetType::Button, "Button");
|
||||
ui.radio_value(widget_type, WidgetType::Label, "Label");
|
||||
ui.radio_value(widget_type, WidgetType::TextEdit, "TextEdit");
|
||||
});
|
||||
egui::Grid::new("pos_size").show(ui, |ui| {
|
||||
ui.label("Widget position:");
|
||||
ui.add(egui::Slider::new(&mut widget_offset.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_offset.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Widget size:");
|
||||
ui.add(egui::Slider::new(&mut widget_size.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_size.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label("Widget size:");
|
||||
ui.add(egui::Slider::new(&mut widget_size.x, 0.0..=400.0));
|
||||
ui.add(egui::Slider::new(&mut widget_size.y, 0.0..=400.0));
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
let widget_rect =
|
||||
egui::Rect::from_min_size(ui.min_rect().min + *widget_offset, *widget_size);
|
||||
let widget_rect = egui::Rect::from_min_size(ui.min_rect().min + *widget_offset, *widget_size);
|
||||
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
|
||||
// Showing how to place a widget anywhere in the [`Ui`]:
|
||||
match *widget_type {
|
||||
WidgetType::Button => {
|
||||
ui.put(widget_rect, egui::Button::new("Example button"));
|
||||
}
|
||||
WidgetType::Label => {
|
||||
ui.put(widget_rect, egui::Label::new("Example label"));
|
||||
}
|
||||
WidgetType::TextEdit => {
|
||||
ui.put(widget_rect, egui::TextEdit::multiline(text_edit_contents));
|
||||
}
|
||||
}
|
||||
// Showing how to place a widget anywhere in the [`Ui`]:
|
||||
match *widget_type {
|
||||
WidgetType::Button => {
|
||||
ui.put(widget_rect, egui::Button::new("Example button"));
|
||||
}
|
||||
WidgetType::Label => {
|
||||
ui.put(widget_rect, egui::Label::new("Example label"));
|
||||
}
|
||||
WidgetType::TextEdit => {
|
||||
ui.put(widget_rect, egui::TextEdit::multiline(text_edit_contents));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct TableTest {
|
||||
num_cols: usize,
|
||||
num_rows: usize,
|
||||
min_col_width: f32,
|
||||
max_col_width: f32,
|
||||
text_length: usize,
|
||||
num_cols: usize,
|
||||
num_rows: usize,
|
||||
min_col_width: f32,
|
||||
max_col_width: f32,
|
||||
text_length: usize,
|
||||
}
|
||||
|
||||
impl Default for TableTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_cols: 4,
|
||||
num_rows: 4,
|
||||
min_col_width: 10.0,
|
||||
max_col_width: 200.0,
|
||||
text_length: 10,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_cols: 4,
|
||||
num_rows: 4,
|
||||
min_col_width: 10.0,
|
||||
max_col_width: 200.0,
|
||||
text_length: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TableTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Table Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Table Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name()).open(open).show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TableTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"),
|
||||
);
|
||||
ui.add(
|
||||
egui::Slider::new(&mut self.max_col_width, 0.0..=400.0).text("Maximum column width"),
|
||||
);
|
||||
ui.add(egui::Slider::new(&mut self.num_cols, 0..=5).text("Columns"));
|
||||
ui.add(egui::Slider::new(&mut self.num_rows, 0..=20).text("Rows"));
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add(egui::Slider::new(&mut self.min_col_width, 0.0..=400.0).text("Minimum column width"));
|
||||
ui.add(egui::Slider::new(&mut self.max_col_width, 0.0..=400.0).text("Maximum column width"));
|
||||
ui.add(egui::Slider::new(&mut self.num_cols, 0..=5).text("Columns"));
|
||||
ui.add(egui::Slider::new(&mut self.num_rows, 0..=20).text("Rows"));
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
let words = [
|
||||
"random", "words", "in", "a", "random", "order", "that", "just", "keeps", "going",
|
||||
"with", "some", "more",
|
||||
];
|
||||
let words = [
|
||||
"random", "words", "in", "a", "random", "order", "that", "just", "keeps", "going", "with",
|
||||
"some", "more",
|
||||
];
|
||||
|
||||
egui::Grid::new("my_grid")
|
||||
.striped(true)
|
||||
.min_col_width(self.min_col_width)
|
||||
.max_col_width(self.max_col_width)
|
||||
.show(ui, |ui| {
|
||||
for row in 0..self.num_rows {
|
||||
for col in 0..self.num_cols {
|
||||
if col == 0 {
|
||||
ui.label(format!("row {}", row));
|
||||
} else {
|
||||
let word_idx = row * 3 + col * 5;
|
||||
let word_count = (row * 5 + col * 75) % 13;
|
||||
let mut string = String::new();
|
||||
for word in words.iter().cycle().skip(word_idx).take(word_count) {
|
||||
string += word;
|
||||
string += " ";
|
||||
}
|
||||
ui.label(string);
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
egui::Grid::new("my_grid")
|
||||
.striped(true)
|
||||
.min_col_width(self.min_col_width)
|
||||
.max_col_width(self.max_col_width)
|
||||
.show(ui, |ui| {
|
||||
for row in 0..self.num_rows {
|
||||
for col in 0..self.num_cols {
|
||||
if col == 0 {
|
||||
ui.label(format!("row {}", row));
|
||||
} else {
|
||||
let word_idx = row * 3 + col * 5;
|
||||
let word_count = (row * 5 + col * 75) % 13;
|
||||
let mut string = String::new();
|
||||
for word in words.iter().cycle().skip(word_idx).take(word_count) {
|
||||
string += word;
|
||||
string += " ";
|
||||
}
|
||||
ui.label(string);
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.add(egui::Slider::new(&mut self.text_length, 1..=40).text("Text length"));
|
||||
egui::Grid::new("parent grid").striped(true).show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Vertical nest1");
|
||||
ui.label("Vertical nest2");
|
||||
});
|
||||
ui.label("First row, second column");
|
||||
ui.end_row();
|
||||
ui.separator();
|
||||
ui.add(egui::Slider::new(&mut self.text_length, 1..=40).text("Text length"));
|
||||
egui::Grid::new("parent grid").striped(true).show(ui, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Vertical nest1");
|
||||
ui.label("Vertical nest2");
|
||||
});
|
||||
ui.label("First row, second column");
|
||||
ui.end_row();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Horizontal nest1");
|
||||
ui.label("Horizontal nest2");
|
||||
});
|
||||
ui.label("Second row, second column");
|
||||
ui.end_row();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Horizontal nest1");
|
||||
ui.label("Horizontal nest2");
|
||||
});
|
||||
ui.label("Second row, second column");
|
||||
ui.end_row();
|
||||
|
||||
ui.scope(|ui| {
|
||||
ui.label("Scope nest 1");
|
||||
ui.label("Scope nest 2");
|
||||
});
|
||||
ui.label("Third row, second column");
|
||||
ui.end_row();
|
||||
ui.scope(|ui| {
|
||||
ui.label("Scope nest 1");
|
||||
ui.label("Scope nest 2");
|
||||
});
|
||||
ui.label("Third row, second column");
|
||||
ui.end_row();
|
||||
|
||||
egui::Grid::new("nested grid").show(ui, |ui| {
|
||||
ui.label("Grid nest11");
|
||||
ui.label("Grid nest12");
|
||||
ui.end_row();
|
||||
ui.label("Grid nest21");
|
||||
ui.label("Grid nest22");
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label("Fourth row, second column");
|
||||
ui.end_row();
|
||||
egui::Grid::new("nested grid").show(ui, |ui| {
|
||||
ui.label("Grid nest11");
|
||||
ui.label("Grid nest12");
|
||||
ui.end_row();
|
||||
ui.label("Grid nest21");
|
||||
ui.label("Grid nest22");
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label("Fourth row, second column");
|
||||
ui.end_row();
|
||||
|
||||
let mut dyn_text = String::from("O");
|
||||
dyn_text.extend(std::iter::repeat('h').take(self.text_length));
|
||||
ui.label(dyn_text);
|
||||
ui.label("Fifth row, second column");
|
||||
ui.end_row();
|
||||
});
|
||||
let mut dyn_text = String::from("O");
|
||||
dyn_text.extend(std::iter::repeat('h').take(self.text_length));
|
||||
ui.label(dyn_text);
|
||||
ui.label("Fifth row, second column");
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
ui.vertical_centered(|ui| {
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -306,138 +301,136 @@ impl super::View for TableTest {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Default)]
|
||||
pub struct InputTest {
|
||||
info: String,
|
||||
info: String,
|
||||
}
|
||||
|
||||
impl super::Demo for InputTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"Input Test"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Input Test"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for InputTest {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
|
||||
let response = ui.add(
|
||||
egui::Button::new("Click, double-click, triple-click or drag me with any mouse button")
|
||||
.sense(egui::Sense::click_and_drag()),
|
||||
);
|
||||
let response = ui.add(
|
||||
egui::Button::new("Click, double-click, triple-click or drag me with any mouse button")
|
||||
.sense(egui::Sense::click_and_drag()),
|
||||
);
|
||||
|
||||
let mut new_info = String::new();
|
||||
for &button in &[
|
||||
egui::PointerButton::Primary,
|
||||
egui::PointerButton::Secondary,
|
||||
egui::PointerButton::Middle,
|
||||
egui::PointerButton::Extra1,
|
||||
egui::PointerButton::Extra2,
|
||||
] {
|
||||
use std::fmt::Write as _;
|
||||
let mut new_info = String::new();
|
||||
for &button in &[
|
||||
egui::PointerButton::Primary,
|
||||
egui::PointerButton::Secondary,
|
||||
egui::PointerButton::Middle,
|
||||
egui::PointerButton::Extra1,
|
||||
egui::PointerButton::Extra2,
|
||||
] {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
if response.clicked_by(button) {
|
||||
writeln!(new_info, "Clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.double_clicked_by(button) {
|
||||
writeln!(new_info, "Double-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.triple_clicked_by(button) {
|
||||
writeln!(new_info, "Triple-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.dragged_by(button) {
|
||||
writeln!(
|
||||
new_info,
|
||||
"Dragged by {:?} button, delta: {:?}",
|
||||
button,
|
||||
response.drag_delta()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if !new_info.is_empty() {
|
||||
self.info = new_info;
|
||||
}
|
||||
|
||||
ui.label(&self.info);
|
||||
if response.clicked_by(button) {
|
||||
writeln!(new_info, "Clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.double_clicked_by(button) {
|
||||
writeln!(new_info, "Double-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.triple_clicked_by(button) {
|
||||
writeln!(new_info, "Triple-clicked by {:?} button", button).ok();
|
||||
}
|
||||
if response.dragged_by(button) {
|
||||
writeln!(
|
||||
new_info,
|
||||
"Dragged by {:?} button, delta: {:?}",
|
||||
button,
|
||||
response.drag_delta()
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
if !new_info.is_empty() {
|
||||
self.info = new_info;
|
||||
}
|
||||
|
||||
ui.label(&self.info);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub struct WindowResizeTest {
|
||||
text: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl Default for WindowResizeTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: crate::LOREM_IPSUM_LONG.to_owned(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: crate::LOREM_IPSUM_LONG.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WindowResizeTest {
|
||||
fn name(&self) -> &'static str {
|
||||
"↔ Window Resize"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"↔ Window Resize"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use egui::*;
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use egui::*;
|
||||
|
||||
Window::new("↔ auto-sized")
|
||||
.open(open)
|
||||
.auto_sized()
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window will auto-size based on its contents.");
|
||||
ui.heading("Resize this area:");
|
||||
Resize::default().show(ui, |ui| {
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||
});
|
||||
ui.heading("Resize the above area!");
|
||||
});
|
||||
Window::new("↔ auto-sized")
|
||||
.open(open)
|
||||
.auto_sized()
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window will auto-size based on its contents.");
|
||||
ui.heading("Resize this area:");
|
||||
Resize::default().show(ui, |ui| {
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||
});
|
||||
ui.heading("Resize the above area!");
|
||||
});
|
||||
|
||||
Window::new("↔ resizable + scroll")
|
||||
.open(open)
|
||||
.vscroll(true)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
"This window is resizable and has a scroll area. You can shrink it to any size.",
|
||||
);
|
||||
ui.separator();
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
Window::new("↔ resizable + scroll")
|
||||
.open(open)
|
||||
.vscroll(true)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window is resizable and has a scroll area. You can shrink it to any size.");
|
||||
ui.separator();
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM_LONG);
|
||||
});
|
||||
|
||||
Window::new("↔ resizable + embedded scroll")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window is resizable but has no built-in scroll area.");
|
||||
ui.label("However, we have a sub-region with a scroll bar:");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
let lorem_ipsum_extra_long =
|
||||
format!("{}\n\n{}", crate::LOREM_IPSUM_LONG, crate::LOREM_IPSUM_LONG);
|
||||
lorem_ipsum(ui, &lorem_ipsum_extra_long);
|
||||
});
|
||||
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
||||
});
|
||||
Window::new("↔ resizable + embedded scroll")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window is resizable but has no built-in scroll area.");
|
||||
ui.label("However, we have a sub-region with a scroll bar:");
|
||||
ui.separator();
|
||||
ScrollArea::vertical().show(ui, |ui| {
|
||||
let lorem_ipsum_extra_long =
|
||||
format!("{}\n\n{}", crate::LOREM_IPSUM_LONG, crate::LOREM_IPSUM_LONG);
|
||||
lorem_ipsum(ui, &lorem_ipsum_extra_long);
|
||||
});
|
||||
// ui.heading("Some additional text here, that should also be visible"); // this works, but messes with the resizing a bit
|
||||
});
|
||||
|
||||
Window::new("↔ resizable without scroll")
|
||||
Window::new("↔ resizable without scroll")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
@@ -448,33 +441,35 @@ impl super::Demo for WindowResizeTest {
|
||||
lorem_ipsum(ui, crate::LOREM_IPSUM);
|
||||
});
|
||||
|
||||
Window::new("↔ resizable with TextEdit")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Shows how you can fill an area with a widget.");
|
||||
ui.add_sized(ui.available_size(), TextEdit::multiline(&mut self.text));
|
||||
});
|
||||
Window::new("↔ resizable with TextEdit")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_height(300.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Shows how you can fill an area with a widget.");
|
||||
ui.add_sized(ui.available_size(), TextEdit::multiline(&mut self.text));
|
||||
});
|
||||
|
||||
Window::new("↔ freely resized")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_size([250.0, 150.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label("This window has empty space that fills up the available space, preventing auto-shrink.");
|
||||
ui.allocate_space(ui.available_size());
|
||||
});
|
||||
}
|
||||
Window::new("↔ freely resized")
|
||||
.open(open)
|
||||
.vscroll(false)
|
||||
.resizable(true)
|
||||
.default_size([250.0, 150.0])
|
||||
.show(ctx, |ui| {
|
||||
ui.label(
|
||||
"This window has empty space that fills up the available space, preventing auto-shrink.",
|
||||
);
|
||||
ui.allocate_space(ui.available_size());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lorem_ipsum(ui: &mut egui::Ui, text: &str) {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(text).weak());
|
||||
},
|
||||
);
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(text).weak());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
/// Showcase [`TextEdit`].
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct TextEdit {
|
||||
pub text: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl Default for TextEdit {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: "Edit this text".to_owned(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: "Edit this text".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for TextEdit {
|
||||
fn name(&self) -> &'static str {
|
||||
"🖹 TextEdit"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🖹 TextEdit"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(false)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for TextEdit {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { text } = self;
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self { text } = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Advanced usage of ");
|
||||
ui.code("TextEdit");
|
||||
ui.label(".");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Advanced usage of ");
|
||||
ui.code("TextEdit");
|
||||
ui.label(".");
|
||||
});
|
||||
|
||||
let output = egui::TextEdit::multiline(text)
|
||||
.hint_text("Type something!")
|
||||
.show(ui);
|
||||
let output = egui::TextEdit::multiline(text)
|
||||
.hint_text("Type something!")
|
||||
.show(ui);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Selected text: ");
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars);
|
||||
ui.code(selected_text);
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("Selected text: ");
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars);
|
||||
ui.code(selected_text);
|
||||
}
|
||||
});
|
||||
|
||||
let anything_selected = output
|
||||
.cursor_range
|
||||
.map_or(false, |cursor| !cursor.is_empty());
|
||||
let anything_selected = output
|
||||
.cursor_range
|
||||
.map_or(false, |cursor| !cursor.is_empty());
|
||||
|
||||
ui.add_enabled(
|
||||
anything_selected,
|
||||
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
|
||||
);
|
||||
ui.add_enabled(
|
||||
anything_selected,
|
||||
egui::Label::new("Press ctrl+Y to toggle the case of selected text (cmd+Y on Mac)"),
|
||||
);
|
||||
|
||||
if ui
|
||||
.input_mut()
|
||||
.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)
|
||||
{
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Move cursor to the:");
|
||||
|
||||
if ui.button("start").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(0);
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("end").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(text.chars().count());
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
});
|
||||
if ui
|
||||
.input_mut()
|
||||
.consume_key(egui::Modifiers::COMMAND, egui::Key::Y)
|
||||
{
|
||||
if let Some(text_cursor_range) = output.cursor_range {
|
||||
use egui::TextBuffer as _;
|
||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
||||
let selected_text = text.char_range(selected_chars.clone());
|
||||
let upper_case = selected_text.to_uppercase();
|
||||
let new_text = if selected_text == upper_case {
|
||||
selected_text.to_lowercase()
|
||||
} else {
|
||||
upper_case
|
||||
};
|
||||
text.delete_char_range(selected_chars.clone());
|
||||
text.insert_text(&new_text, selected_chars.start);
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Move cursor to the:");
|
||||
|
||||
if ui.button("start").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(0);
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
|
||||
if ui.button("end").clicked() {
|
||||
let text_edit_id = output.response.id;
|
||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||
let ccursor = egui::text::CCursor::new(text.chars().count());
|
||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||
state.store(ui.ctx(), text_edit_id);
|
||||
ui.ctx().memory().request_focus(text_edit_id); // give focus back to the [`TextEdit`].
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,84 +15,84 @@
|
||||
/// toggle_ui(ui, &mut my_bool);
|
||||
/// ```
|
||||
pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
// 4. Paint!
|
||||
// Make sure we need to paint:
|
||||
if ui.is_rect_visible(rect) {
|
||||
// Let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
// 4. Paint!
|
||||
// Make sure we need to paint:
|
||||
if ui.is_rect_visible(rect) {
|
||||
// Let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
}
|
||||
|
||||
/// Here is the same code again, but a bit more compact:
|
||||
#[allow(dead_code)]
|
||||
fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed();
|
||||
}
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
if ui.is_rect_visible(rect) {
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
}
|
||||
|
||||
response
|
||||
response
|
||||
}
|
||||
|
||||
// A wrapper that allows the more idiomatic usage pattern: `ui.add(toggle(&mut my_bool))`
|
||||
@@ -103,9 +103,9 @@ fn toggle_ui_compact(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
/// ui.add(toggle(&mut my_bool));
|
||||
/// ```
|
||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
|
||||
@@ -1,291 +1,291 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Enum {
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
First,
|
||||
Second,
|
||||
Third,
|
||||
}
|
||||
|
||||
/// Shows off one example of each major type of widget.
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WidgetGallery {
|
||||
enabled: bool,
|
||||
visible: bool,
|
||||
boolean: bool,
|
||||
radio: Enum,
|
||||
scalar: f32,
|
||||
string: String,
|
||||
color: egui::Color32,
|
||||
animate_progress_bar: bool,
|
||||
enabled: bool,
|
||||
visible: bool,
|
||||
boolean: bool,
|
||||
radio: Enum,
|
||||
scalar: f32,
|
||||
string: String,
|
||||
color: egui::Color32,
|
||||
animate_progress_bar: bool,
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
date: Option<chrono::Date<chrono::Utc>>,
|
||||
#[cfg(feature = "chrono")]
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
date: Option<chrono::Date<chrono::Utc>>,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
texture: Option<egui::TextureHandle>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
texture: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl Default for WidgetGallery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
boolean: false,
|
||||
radio: Enum::First,
|
||||
scalar: 42.0,
|
||||
string: Default::default(),
|
||||
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
||||
animate_progress_bar: false,
|
||||
#[cfg(feature = "chrono")]
|
||||
date: None,
|
||||
texture: None,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
visible: true,
|
||||
boolean: false,
|
||||
radio: Enum::First,
|
||||
scalar: 42.0,
|
||||
string: Default::default(),
|
||||
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
|
||||
animate_progress_bar: false,
|
||||
#[cfg(feature = "chrono")]
|
||||
date: None,
|
||||
texture: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WidgetGallery {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗄 Widget Gallery"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🗄 Widget Gallery"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
egui::Window::new(self.name())
|
||||
.open(open)
|
||||
.resizable(true)
|
||||
.default_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
use super::View as _;
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WidgetGallery {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_enabled_ui(self.enabled, |ui| {
|
||||
ui.set_visible(self.visible);
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.add_enabled_ui(self.enabled, |ui| {
|
||||
ui.set_visible(self.visible);
|
||||
|
||||
egui::Grid::new("my_grid")
|
||||
.num_columns(2)
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
self.gallery_grid_contents(ui);
|
||||
});
|
||||
egui::Grid::new("my_grid")
|
||||
.num_columns(2)
|
||||
.spacing([40.0, 4.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
self.gallery_grid_contents(ui);
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.visible, "Visible")
|
||||
.on_hover_text("Uncheck to hide all the widgets.");
|
||||
if self.visible {
|
||||
ui.checkbox(&mut self.enabled, "Interactive")
|
||||
.on_hover_text("Uncheck to inspect how the widgets look when disabled.");
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.visible, "Visible")
|
||||
.on_hover_text("Uncheck to hide all the widgets.");
|
||||
if self.visible {
|
||||
ui.checkbox(&mut self.enabled, "Interactive")
|
||||
.on_hover_text("Uncheck to inspect how the widgets look when disabled.");
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.separator();
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
let tooltip_text = "The full egui documentation.\nYou can also click the different widgets names in the left column.";
|
||||
ui.hyperlink("https://docs.rs/egui/").on_hover_text(tooltip_text);
|
||||
ui.add(crate::egui_github_link_file!(
|
||||
"Source code of the widget gallery"
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetGallery {
|
||||
fn gallery_grid_contents(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
enabled: _,
|
||||
visible: _,
|
||||
boolean,
|
||||
radio,
|
||||
scalar,
|
||||
string,
|
||||
color,
|
||||
animate_progress_bar,
|
||||
#[cfg(feature = "chrono")]
|
||||
date,
|
||||
texture,
|
||||
} = self;
|
||||
fn gallery_grid_contents(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
enabled: _,
|
||||
visible: _,
|
||||
boolean,
|
||||
radio,
|
||||
scalar,
|
||||
string,
|
||||
color,
|
||||
animate_progress_bar,
|
||||
#[cfg(feature = "chrono")]
|
||||
date,
|
||||
texture,
|
||||
} = self;
|
||||
|
||||
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
|
||||
ui.ctx().load_texture(
|
||||
"example",
|
||||
egui::ColorImage::example(),
|
||||
egui::TextureFilter::Linear,
|
||||
)
|
||||
});
|
||||
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
|
||||
ui.ctx().load_texture(
|
||||
"example",
|
||||
egui::ColorImage::example(),
|
||||
egui::TextureFilter::Linear,
|
||||
)
|
||||
});
|
||||
|
||||
ui.add(doc_link_label("Label", "label,heading"));
|
||||
ui.label("Welcome to the widget gallery!");
|
||||
ui.end_row();
|
||||
ui.add(doc_link_label("Label", "label,heading"));
|
||||
ui.label("Welcome to the widget gallery!");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Hyperlink", "Hyperlink"));
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.end_row();
|
||||
ui.add(doc_link_label("Hyperlink", "Hyperlink"));
|
||||
use egui::special_emojis::GITHUB;
|
||||
ui.hyperlink_to(
|
||||
format!("{} egui on GitHub", GITHUB),
|
||||
"https://github.com/emilk/egui",
|
||||
);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("TextEdit", "TextEdit,text_edit"));
|
||||
ui.add(egui::TextEdit::singleline(string).hint_text("Write something here"));
|
||||
ui.end_row();
|
||||
ui.add(doc_link_label("TextEdit", "TextEdit,text_edit"));
|
||||
ui.add(egui::TextEdit::singleline(string).hint_text("Write something here"));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Button", "button"));
|
||||
if ui.button("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Link", "link"));
|
||||
if ui.link("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Checkbox", "checkbox"));
|
||||
ui.checkbox(boolean, "Checkbox");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("RadioButton", "radio"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(radio, Enum::First, "First");
|
||||
ui.radio_value(radio, Enum::Second, "Second");
|
||||
ui.radio_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label(
|
||||
"SelectableLabel",
|
||||
"selectable_value,SelectableLabel",
|
||||
));
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ComboBox", "ComboBox"));
|
||||
|
||||
egui::ComboBox::from_label("Take your pick")
|
||||
.selected_text(format!("{:?}", radio))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Slider", "Slider"));
|
||||
ui.add(egui::Slider::new(scalar, 0.0..=360.0).suffix("°"));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("DragValue", "DragValue"));
|
||||
ui.add(egui::DragValue::new(scalar).speed(1.0));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ProgressBar", "ProgressBar"));
|
||||
let progress = *scalar / 360.0;
|
||||
let progress_bar = egui::ProgressBar::new(progress)
|
||||
.show_percentage()
|
||||
.animate(*animate_progress_bar);
|
||||
*animate_progress_bar = ui
|
||||
.add(progress_bar)
|
||||
.on_hover_text("The progress bar can be animated!")
|
||||
.hovered();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Color picker", "color_edit"));
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.end_row();
|
||||
|
||||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
||||
|
||||
ui.add(doc_link_label("Image", "Image"));
|
||||
ui.image(texture, img_size);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
||||
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date());
|
||||
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));
|
||||
ui.add(egui_extras::DatePickerButton::new(date));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
ui.add(doc_link_label("Separator", "separator"));
|
||||
ui.separator();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("CollapsingHeader", "collapsing"));
|
||||
ui.collapsing("Click to see what is hidden!", |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("It's a ");
|
||||
ui.add(doc_link_label("Spinner", "spinner"));
|
||||
ui.add_space(4.0);
|
||||
ui.add(egui::Spinner::new());
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Plot", "plot"));
|
||||
example_plot(ui);
|
||||
ui.end_row();
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Custom widget:",
|
||||
super::toggle_switch::url_to_file_source_code(),
|
||||
);
|
||||
ui.add(super::toggle_switch::toggle(boolean)).on_hover_text(
|
||||
"It's easy to create your own widgets!\n\
|
||||
This toggle switch is just 15 lines of code.",
|
||||
);
|
||||
ui.end_row();
|
||||
ui.add(doc_link_label("Button", "button"));
|
||||
if ui.button("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Link", "link"));
|
||||
if ui.link("Click me!").clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Checkbox", "checkbox"));
|
||||
ui.checkbox(boolean, "Checkbox");
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("RadioButton", "radio"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.radio_value(radio, Enum::First, "First");
|
||||
ui.radio_value(radio, Enum::Second, "Second");
|
||||
ui.radio_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label(
|
||||
"SelectableLabel",
|
||||
"selectable_value,SelectableLabel",
|
||||
));
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ComboBox", "ComboBox"));
|
||||
|
||||
egui::ComboBox::from_label("Take your pick")
|
||||
.selected_text(format!("{:?}", radio))
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(radio, Enum::First, "First");
|
||||
ui.selectable_value(radio, Enum::Second, "Second");
|
||||
ui.selectable_value(radio, Enum::Third, "Third");
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Slider", "Slider"));
|
||||
ui.add(egui::Slider::new(scalar, 0.0..=360.0).suffix("°"));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("DragValue", "DragValue"));
|
||||
ui.add(egui::DragValue::new(scalar).speed(1.0));
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ProgressBar", "ProgressBar"));
|
||||
let progress = *scalar / 360.0;
|
||||
let progress_bar = egui::ProgressBar::new(progress)
|
||||
.show_percentage()
|
||||
.animate(*animate_progress_bar);
|
||||
*animate_progress_bar = ui
|
||||
.add(progress_bar)
|
||||
.on_hover_text("The progress bar can be animated!")
|
||||
.hovered();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Color picker", "color_edit"));
|
||||
ui.color_edit_button_srgba(color);
|
||||
ui.end_row();
|
||||
|
||||
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
|
||||
|
||||
ui.add(doc_link_label("Image", "Image"));
|
||||
ui.image(texture, img_size);
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("ImageButton", "ImageButton"));
|
||||
if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
|
||||
*boolean = !*boolean;
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
{
|
||||
let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date());
|
||||
ui.add(doc_link_label("DatePickerButton", "DatePickerButton"));
|
||||
ui.add(egui_extras::DatePickerButton::new(date));
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
ui.add(doc_link_label("Separator", "separator"));
|
||||
ui.separator();
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("CollapsingHeader", "collapsing"));
|
||||
ui.collapsing("Click to see what is hidden!", |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.label("It's a ");
|
||||
ui.add(doc_link_label("Spinner", "spinner"));
|
||||
ui.add_space(4.0);
|
||||
ui.add(egui::Spinner::new());
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
ui.add(doc_link_label("Plot", "plot"));
|
||||
example_plot(ui);
|
||||
ui.end_row();
|
||||
|
||||
ui.hyperlink_to(
|
||||
"Custom widget:",
|
||||
super::toggle_switch::url_to_file_source_code(),
|
||||
);
|
||||
ui.add(super::toggle_switch::toggle(boolean)).on_hover_text(
|
||||
"It's easy to create your own widgets!\n\
|
||||
This toggle switch is just 15 lines of code.",
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
|
||||
fn example_plot(ui: &mut egui::Ui) -> egui::Response {
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line_points: PlotPoints = (0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
[x, x.sin()]
|
||||
})
|
||||
.collect();
|
||||
let line = Line::new(line_points);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
use egui::plot::{Line, PlotPoints};
|
||||
let n = 128;
|
||||
let line_points: PlotPoints = (0..=n)
|
||||
.map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
[x, x.sin()]
|
||||
})
|
||||
.collect();
|
||||
let line = Line::new(line_points);
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
.show(ui, |plot_ui| plot_ui.line(line))
|
||||
.response
|
||||
}
|
||||
|
||||
fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a {
|
||||
let label = format!("{}:", title);
|
||||
let url = format!("https://docs.rs/egui?search={}", search_term);
|
||||
move |ui: &mut egui::Ui| {
|
||||
ui.hyperlink_to(label, url).on_hover_ui(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Search egui docs for");
|
||||
ui.code(search_term);
|
||||
});
|
||||
})
|
||||
}
|
||||
let label = format!("{}:", title);
|
||||
let url = format!("https://docs.rs/egui?search={}", search_term);
|
||||
move |ui: &mut egui::Ui| {
|
||||
ui.hyperlink_to(label, url).on_hover_ui(|ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label("Search egui docs for");
|
||||
ui.code(search_term);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowOptions {
|
||||
title: String,
|
||||
title_bar: bool,
|
||||
closable: bool,
|
||||
collapsible: bool,
|
||||
resizable: bool,
|
||||
scroll2: [bool; 2],
|
||||
disabled_time: f64,
|
||||
title: String,
|
||||
title_bar: bool,
|
||||
closable: bool,
|
||||
collapsible: bool,
|
||||
resizable: bool,
|
||||
scroll2: [bool; 2],
|
||||
disabled_time: f64,
|
||||
|
||||
anchored: bool,
|
||||
anchor: egui::Align2,
|
||||
anchor_offset: egui::Vec2,
|
||||
anchored: bool,
|
||||
anchor: egui::Align2,
|
||||
anchor_offset: egui::Vec2,
|
||||
}
|
||||
|
||||
impl Default for WindowOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "🗖 Window Options".to_owned(),
|
||||
title_bar: true,
|
||||
closable: true,
|
||||
collapsible: true,
|
||||
resizable: true,
|
||||
scroll2: [true; 2],
|
||||
disabled_time: f64::NEG_INFINITY,
|
||||
anchored: false,
|
||||
anchor: egui::Align2::RIGHT_TOP,
|
||||
anchor_offset: egui::Vec2::ZERO,
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "🗖 Window Options".to_owned(),
|
||||
title_bar: true,
|
||||
closable: true,
|
||||
collapsible: true,
|
||||
resizable: true,
|
||||
scroll2: [true; 2],
|
||||
disabled_time: f64::NEG_INFINITY,
|
||||
anchored: false,
|
||||
anchor: egui::Align2::RIGHT_TOP,
|
||||
anchor_offset: egui::Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Demo for WindowOptions {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window Options"
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window Options"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self.clone();
|
||||
|
||||
let enabled = ctx.input().time - disabled_time > 2.0;
|
||||
if !enabled {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self.clone();
|
||||
|
||||
let enabled = ctx.input().time - disabled_time > 2.0;
|
||||
if !enabled {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
use super::View as _;
|
||||
let mut window = egui::Window::new(title)
|
||||
.id(egui::Id::new("demo_window_options")) // required since we change the title
|
||||
.resizable(resizable)
|
||||
.collapsible(collapsible)
|
||||
.title_bar(title_bar)
|
||||
.scroll2(scroll2)
|
||||
.enabled(enabled);
|
||||
if closable {
|
||||
window = window.open(open);
|
||||
}
|
||||
if anchored {
|
||||
window = window.anchor(anchor, anchor_offset);
|
||||
}
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
use super::View as _;
|
||||
let mut window = egui::Window::new(title)
|
||||
.id(egui::Id::new("demo_window_options")) // required since we change the title
|
||||
.resizable(resizable)
|
||||
.collapsible(collapsible)
|
||||
.title_bar(title_bar)
|
||||
.scroll2(scroll2)
|
||||
.enabled(enabled);
|
||||
if closable {
|
||||
window = window.open(open);
|
||||
}
|
||||
if anchored {
|
||||
window = window.anchor(anchor, anchor_offset);
|
||||
}
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WindowOptions {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time: _,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("title:");
|
||||
ui.text_edit_singleline(title);
|
||||
});
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
title,
|
||||
title_bar,
|
||||
closable,
|
||||
collapsible,
|
||||
resizable,
|
||||
scroll2,
|
||||
disabled_time: _,
|
||||
anchored,
|
||||
anchor,
|
||||
anchor_offset,
|
||||
} = self;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("title:");
|
||||
ui.text_edit_singleline(title);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(title_bar, "title_bar");
|
||||
ui.checkbox(closable, "closable");
|
||||
ui.checkbox(collapsible, "collapsible");
|
||||
ui.checkbox(resizable, "resizable");
|
||||
ui.checkbox(&mut scroll2[0], "hscroll");
|
||||
ui.checkbox(&mut scroll2[1], "vscroll");
|
||||
});
|
||||
});
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(anchored, "anchored");
|
||||
ui.set_enabled(*anchored);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("x:");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::LEFT, "Left");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::RIGHT, "Right");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("y:");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::TOP, "Top");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::BOTTOM, "Bottom");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Offset:");
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.x));
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.y));
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(title_bar, "title_bar");
|
||||
ui.checkbox(closable, "closable");
|
||||
ui.checkbox(collapsible, "collapsible");
|
||||
ui.checkbox(resizable, "resizable");
|
||||
ui.checkbox(&mut scroll2[0], "hscroll");
|
||||
ui.checkbox(&mut scroll2[1], "vscroll");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Disable for 2 seconds").clicked() {
|
||||
self.disabled_time = ui.input().time;
|
||||
}
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.group(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.checkbox(anchored, "anchored");
|
||||
ui.set_enabled(*anchored);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("x:");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::LEFT, "Left");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[0], egui::Align::RIGHT, "Right");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("y:");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::TOP, "Top");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::Center, "Center");
|
||||
ui.selectable_value(&mut anchor[1], egui::Align::BOTTOM, "Bottom");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Offset:");
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.x));
|
||||
ui.add(egui::DragValue::new(&mut anchor_offset.y));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Disable for 2 seconds").clicked() {
|
||||
self.disabled_time = ui.input().time;
|
||||
}
|
||||
egui::reset_button(ui, self);
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
#[derive(Clone, PartialEq, Default)]
|
||||
#[derive(Clone, PartialEq, Eq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowWithPanels {}
|
||||
|
||||
impl super::Demo for WindowWithPanels {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window With Panels"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window With Panels"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
let window = egui::Window::new("Window with Panels")
|
||||
.default_width(600.0)
|
||||
.default_height(400.0)
|
||||
.vscroll(false)
|
||||
.open(open);
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
let window = egui::Window::new("Window with Panels")
|
||||
.default_width(600.0)
|
||||
.default_height(400.0)
|
||||
.vscroll(false)
|
||||
.open(open);
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for WindowWithPanels {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Note that the order we add the panels is very important!
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Note that the order we add the panels is very important!
|
||||
|
||||
egui::TopBottomPanel::top("top_panel")
|
||||
.resizable(true)
|
||||
.min_height(32.0)
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Expandable Upper Panel");
|
||||
});
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::left("left_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Left Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::right("right_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Right Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
.resizable(false)
|
||||
.min_height(0.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Bottom Panel");
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Central Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
egui::TopBottomPanel::top("top_panel")
|
||||
.resizable(true)
|
||||
.min_height(32.0)
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Expandable Upper Panel");
|
||||
});
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
egui::SidePanel::left("left_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Left Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::SidePanel::right("right_panel")
|
||||
.resizable(true)
|
||||
.default_width(150.0)
|
||||
.width_range(80.0..=200.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Right Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel")
|
||||
.resizable(false)
|
||||
.min_height(0.0)
|
||||
.show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Bottom Panel");
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.heading("Central Panel");
|
||||
});
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
lorem_ipsum(ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lorem_ipsum(ui: &mut egui::Ui) {
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(crate::LOREM_IPSUM_LONG).small().weak());
|
||||
},
|
||||
);
|
||||
ui.with_layout(
|
||||
egui::Layout::top_down(egui::Align::LEFT).with_cross_justify(true),
|
||||
|ui| {
|
||||
ui.label(egui::RichText::new(crate::LOREM_IPSUM_LONG).small().weak());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,178 +3,178 @@ use egui::{text_edit::CCursorRange, *};
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct EasyMarkEditor {
|
||||
code: String,
|
||||
highlight_editor: bool,
|
||||
show_rendered: bool,
|
||||
code: String,
|
||||
highlight_editor: bool,
|
||||
show_rendered: bool,
|
||||
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
highlighter: crate::easy_mark::MemoizedEasymarkHighlighter,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
highlighter: crate::easy_mark::MemoizedEasymarkHighlighter,
|
||||
}
|
||||
|
||||
impl PartialEq for EasyMarkEditor {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(&self.code, self.highlight_editor, self.show_rendered)
|
||||
== (&other.code, other.highlight_editor, other.show_rendered)
|
||||
}
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
(&self.code, self.highlight_editor, self.show_rendered)
|
||||
== (&other.code, other.highlight_editor, other.show_rendered)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EasyMarkEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code: DEFAULT_CODE.trim().to_owned(),
|
||||
highlight_editor: true,
|
||||
show_rendered: true,
|
||||
highlighter: Default::default(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
code: DEFAULT_CODE.trim().to_owned(),
|
||||
highlight_editor: true,
|
||||
show_rendered: true,
|
||||
highlighter: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EasyMarkEditor {
|
||||
pub fn panels(&mut self, ctx: &egui::Context) {
|
||||
egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(crate::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
pub fn panels(&mut self, ctx: &egui::Context) {
|
||||
egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| {
|
||||
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
|
||||
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
|
||||
ui.add(crate::egui_github_link_file!())
|
||||
})
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.ui(ui);
|
||||
});
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.ui(ui);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Grid::new("controls").show(ui, |ui| {
|
||||
let _ = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui);
|
||||
ui.checkbox(&mut self.show_rendered, "Show rendered");
|
||||
ui.checkbox(&mut self.highlight_editor, "Highlight editor");
|
||||
egui::reset_button(ui, self);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
if self.show_rendered {
|
||||
ui.columns(2, |columns| {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(&mut columns[0], |ui| self.editor_ui(ui));
|
||||
ScrollArea::vertical()
|
||||
.id_source("rendered")
|
||||
.show(&mut columns[1], |ui| {
|
||||
// TODO(emilk): we can save some more CPU by caching the rendered output.
|
||||
crate::easy_mark::easy_mark(ui, &self.code);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(ui, |ui| self.editor_ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::Grid::new("controls").show(ui, |ui| {
|
||||
let _ = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui);
|
||||
ui.checkbox(&mut self.show_rendered, "Show rendered");
|
||||
ui.checkbox(&mut self.highlight_editor, "Highlight editor");
|
||||
egui::reset_button(ui, self);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.separator();
|
||||
fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
code, highlighter, ..
|
||||
} = self;
|
||||
|
||||
if self.show_rendered {
|
||||
ui.columns(2, |columns| {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(&mut columns[0], |ui| self.editor_ui(ui));
|
||||
ScrollArea::vertical()
|
||||
.id_source("rendered")
|
||||
.show(&mut columns[1], |ui| {
|
||||
// TODO(emilk): we can save some more CPU by caching the rendered output.
|
||||
crate::easy_mark::easy_mark(ui, &self.code);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
ScrollArea::vertical()
|
||||
.id_source("source")
|
||||
.show(ui, |ui| self.editor_ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
||||
let Self {
|
||||
code, highlighter, ..
|
||||
} = self;
|
||||
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.layouter(&mut layouter),
|
||||
)
|
||||
} else {
|
||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||
};
|
||||
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||
if let Some(mut ccursor_range) = state.ccursor_range() {
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.set_ccursor_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
}
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(code)
|
||||
.desired_width(f32::INFINITY)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.layouter(&mut layouter),
|
||||
)
|
||||
} else {
|
||||
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
|
||||
};
|
||||
|
||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||
if let Some(mut ccursor_range) = state.ccursor_range() {
|
||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||
if any_change {
|
||||
state.set_ccursor_range(Some(ccursor_range));
|
||||
state.store(ui.ctx(), response.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nested_hotkeys_ui(ui: &mut egui::Ui) {
|
||||
let _ = ui.label("CTRL+B *bold*");
|
||||
let _ = ui.label("CTRL+N `code`");
|
||||
let _ = ui.label("CTRL+I /italics/");
|
||||
let _ = ui.label("CTRL+L $subscript$");
|
||||
let _ = ui.label("CTRL+Y ^superscript^");
|
||||
let _ = ui.label("ALT+SHIFT+Q ~strikethrough~");
|
||||
let _ = ui.label("ALT+SHIFT+W _underline_");
|
||||
let _ = ui.label("ALT+SHIFT+E two spaces"); // Placeholder for tab indent
|
||||
let _ = ui.label("CTRL+B *bold*");
|
||||
let _ = ui.label("CTRL+N `code`");
|
||||
let _ = ui.label("CTRL+I /italics/");
|
||||
let _ = ui.label("CTRL+L $subscript$");
|
||||
let _ = ui.label("CTRL+Y ^superscript^");
|
||||
let _ = ui.label("ALT+SHIFT+Q ~strikethrough~");
|
||||
let _ = ui.label("ALT+SHIFT+W _underline_");
|
||||
let _ = ui.label("ALT+SHIFT+E two spaces"); // Placeholder for tab indent
|
||||
}
|
||||
|
||||
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
|
||||
let mut any_change = false;
|
||||
if ui
|
||||
.input_mut()
|
||||
.consume_key(egui::Modifiers::ALT_SHIFT, Key::E)
|
||||
{
|
||||
// This is a placeholder till we can indent the active line
|
||||
any_change = true;
|
||||
let [primary, _secondary] = ccursor_range.sorted();
|
||||
let mut any_change = false;
|
||||
if ui
|
||||
.input_mut()
|
||||
.consume_key(egui::Modifiers::ALT_SHIFT, Key::E)
|
||||
{
|
||||
// This is a placeholder till we can indent the active line
|
||||
any_change = true;
|
||||
let [primary, _secondary] = ccursor_range.sorted();
|
||||
|
||||
let advance = code.insert_text(" ", primary.index);
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
for (modifier, key, surrounding) in [
|
||||
(egui::Modifiers::COMMAND, Key::B, "*"), // *bold*
|
||||
(egui::Modifiers::COMMAND, Key::N, "`"), // `code`
|
||||
(egui::Modifiers::COMMAND, Key::I, "/"), // /italics/
|
||||
(egui::Modifiers::COMMAND, Key::L, "$"), // $subscript$
|
||||
(egui::Modifiers::COMMAND, Key::Y, "^"), // ^superscript^
|
||||
(egui::Modifiers::ALT_SHIFT, Key::Q, "~"), // ~strikethrough~
|
||||
(egui::Modifiers::ALT_SHIFT, Key::W, "_"), // _underline_
|
||||
] {
|
||||
if ui.input_mut().consume_key(modifier, key) {
|
||||
any_change = true;
|
||||
toggle_surrounding(code, ccursor_range, surrounding);
|
||||
};
|
||||
}
|
||||
any_change
|
||||
let advance = code.insert_text(" ", primary.index);
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
for (modifier, key, surrounding) in [
|
||||
(egui::Modifiers::COMMAND, Key::B, "*"), // *bold*
|
||||
(egui::Modifiers::COMMAND, Key::N, "`"), // `code`
|
||||
(egui::Modifiers::COMMAND, Key::I, "/"), // /italics/
|
||||
(egui::Modifiers::COMMAND, Key::L, "$"), // $subscript$
|
||||
(egui::Modifiers::COMMAND, Key::Y, "^"), // ^superscript^
|
||||
(egui::Modifiers::ALT_SHIFT, Key::Q, "~"), // ~strikethrough~
|
||||
(egui::Modifiers::ALT_SHIFT, Key::W, "_"), // _underline_
|
||||
] {
|
||||
if ui.input_mut().consume_key(modifier, key) {
|
||||
any_change = true;
|
||||
toggle_surrounding(code, ccursor_range, surrounding);
|
||||
};
|
||||
}
|
||||
any_change
|
||||
}
|
||||
|
||||
/// E.g. toggle *strong* with `toggle_surrounding(&mut text, &mut cursor, "*")`
|
||||
fn toggle_surrounding(
|
||||
code: &mut dyn TextBuffer,
|
||||
ccursor_range: &mut CCursorRange,
|
||||
surrounding: &str,
|
||||
code: &mut dyn TextBuffer,
|
||||
ccursor_range: &mut CCursorRange,
|
||||
surrounding: &str,
|
||||
) {
|
||||
let [primary, secondary] = ccursor_range.sorted();
|
||||
let [primary, secondary] = ccursor_range.sorted();
|
||||
|
||||
let surrounding_ccount = surrounding.chars().count();
|
||||
let surrounding_ccount = surrounding.chars().count();
|
||||
|
||||
let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index;
|
||||
let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount);
|
||||
let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding
|
||||
&& code.char_range(suffix_crange.clone()) == surrounding;
|
||||
let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index;
|
||||
let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount);
|
||||
let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding
|
||||
&& code.char_range(suffix_crange.clone()) == surrounding;
|
||||
|
||||
if already_surrounded {
|
||||
code.delete_char_range(suffix_crange);
|
||||
code.delete_char_range(prefix_crange);
|
||||
ccursor_range.primary.index -= surrounding_ccount;
|
||||
ccursor_range.secondary.index -= surrounding_ccount;
|
||||
} else {
|
||||
code.insert_text(surrounding, secondary.index);
|
||||
let advance = code.insert_text(surrounding, primary.index);
|
||||
if already_surrounded {
|
||||
code.delete_char_range(suffix_crange);
|
||||
code.delete_char_range(prefix_crange);
|
||||
ccursor_range.primary.index -= surrounding_ccount;
|
||||
ccursor_range.secondary.index -= surrounding_ccount;
|
||||
} else {
|
||||
code.insert_text(surrounding, secondary.index);
|
||||
let advance = code.insert_text(surrounding, primary.index);
|
||||
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
ccursor_range.primary.index += advance;
|
||||
ccursor_range.secondary.index += advance;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -5,188 +5,188 @@ use crate::easy_mark::easy_mark_parser;
|
||||
/// In practice, the highlighter is fast enough not to need any caching.
|
||||
#[derive(Default)]
|
||||
pub struct MemoizedEasymarkHighlighter {
|
||||
style: egui::Style,
|
||||
code: String,
|
||||
output: egui::text::LayoutJob,
|
||||
style: egui::Style,
|
||||
code: String,
|
||||
output: egui::text::LayoutJob,
|
||||
}
|
||||
|
||||
impl MemoizedEasymarkHighlighter {
|
||||
pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob {
|
||||
if (&self.style, self.code.as_str()) != (egui_style, code) {
|
||||
self.style = egui_style.clone();
|
||||
self.code = code.to_owned();
|
||||
self.output = highlight_easymark(egui_style, code);
|
||||
}
|
||||
self.output.clone()
|
||||
pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob {
|
||||
if (&self.style, self.code.as_str()) != (egui_style, code) {
|
||||
self.style = egui_style.clone();
|
||||
self.code = code.to_owned();
|
||||
self.output = highlight_easymark(egui_style, code);
|
||||
}
|
||||
self.output.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::text::LayoutJob {
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
let mut style = easy_mark_parser::Style::default();
|
||||
let mut start_of_line = true;
|
||||
let mut job = egui::text::LayoutJob::default();
|
||||
let mut style = easy_mark_parser::Style::default();
|
||||
let mut start_of_line = true;
|
||||
|
||||
while !text.is_empty() {
|
||||
if start_of_line && text.starts_with("```") {
|
||||
let end = text.find("\n```").map_or_else(|| text.len(), |i| i + 4);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
format_from_style(
|
||||
egui_style,
|
||||
&easy_mark_parser::Style {
|
||||
code: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
text = &text[end..];
|
||||
style = Default::default();
|
||||
continue;
|
||||
}
|
||||
|
||||
if text.starts_with('`') {
|
||||
style.code = true;
|
||||
let end = text[1..]
|
||||
.find(&['`', '\n'][..])
|
||||
.map_or_else(|| text.len(), |i| i + 2);
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
style.code = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut skip;
|
||||
|
||||
if text.starts_with('\\') && text.len() >= 2 {
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with(' ') {
|
||||
// we don't preview indentation, because it is confusing
|
||||
skip = 1;
|
||||
} else if start_of_line && text.starts_with("# ") {
|
||||
style.heading = true;
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with("> ") {
|
||||
style.quoted = true;
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if start_of_line && text.starts_with("- ") {
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if text.starts_with('*') {
|
||||
skip = 1;
|
||||
if style.strong {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.strong ^= true;
|
||||
} else if text.starts_with('$') {
|
||||
skip = 1;
|
||||
if style.small {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.small ^= true;
|
||||
} else if text.starts_with('^') {
|
||||
skip = 1;
|
||||
if style.raised {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.raised ^= true;
|
||||
} else {
|
||||
skip = 0;
|
||||
}
|
||||
// Note: we don't preview underline, strikethrough and italics because it confuses things.
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let line_end = text[skip..]
|
||||
.find('\n')
|
||||
.map_or_else(|| text.len(), |i| (skip + i + 1));
|
||||
let end = text[skip..]
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
|
||||
.map_or_else(|| text.len(), |i| (skip + i).max(1));
|
||||
|
||||
if line_end <= end {
|
||||
job.append(
|
||||
&text[..line_end],
|
||||
0.0,
|
||||
format_from_style(egui_style, &style),
|
||||
);
|
||||
text = &text[line_end..];
|
||||
start_of_line = true;
|
||||
style = Default::default();
|
||||
} else {
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
start_of_line = false;
|
||||
}
|
||||
while !text.is_empty() {
|
||||
if start_of_line && text.starts_with("```") {
|
||||
let end = text.find("\n```").map_or_else(|| text.len(), |i| i + 4);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
format_from_style(
|
||||
egui_style,
|
||||
&easy_mark_parser::Style {
|
||||
code: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
);
|
||||
text = &text[end..];
|
||||
style = Default::default();
|
||||
continue;
|
||||
}
|
||||
|
||||
job
|
||||
if text.starts_with('`') {
|
||||
style.code = true;
|
||||
let end = text[1..]
|
||||
.find(&['`', '\n'][..])
|
||||
.map_or_else(|| text.len(), |i| i + 2);
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
style.code = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut skip;
|
||||
|
||||
if text.starts_with('\\') && text.len() >= 2 {
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with(' ') {
|
||||
// we don't preview indentation, because it is confusing
|
||||
skip = 1;
|
||||
} else if start_of_line && text.starts_with("# ") {
|
||||
style.heading = true;
|
||||
skip = 2;
|
||||
} else if start_of_line && text.starts_with("> ") {
|
||||
style.quoted = true;
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if start_of_line && text.starts_with("- ") {
|
||||
skip = 2;
|
||||
// we don't preview indentation, because it is confusing
|
||||
} else if text.starts_with('*') {
|
||||
skip = 1;
|
||||
if style.strong {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.strong ^= true;
|
||||
} else if text.starts_with('$') {
|
||||
skip = 1;
|
||||
if style.small {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.small ^= true;
|
||||
} else if text.starts_with('^') {
|
||||
skip = 1;
|
||||
if style.raised {
|
||||
// Include the character that is ending this style:
|
||||
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[skip..];
|
||||
skip = 0;
|
||||
}
|
||||
style.raised ^= true;
|
||||
} else {
|
||||
skip = 0;
|
||||
}
|
||||
// Note: we don't preview underline, strikethrough and italics because it confuses things.
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let line_end = text[skip..]
|
||||
.find('\n')
|
||||
.map_or_else(|| text.len(), |i| (skip + i + 1));
|
||||
let end = text[skip..]
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
|
||||
.map_or_else(|| text.len(), |i| (skip + i).max(1));
|
||||
|
||||
if line_end <= end {
|
||||
job.append(
|
||||
&text[..line_end],
|
||||
0.0,
|
||||
format_from_style(egui_style, &style),
|
||||
);
|
||||
text = &text[line_end..];
|
||||
start_of_line = true;
|
||||
style = Default::default();
|
||||
} else {
|
||||
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
|
||||
text = &text[end..];
|
||||
start_of_line = false;
|
||||
}
|
||||
}
|
||||
|
||||
job
|
||||
}
|
||||
|
||||
fn format_from_style(
|
||||
egui_style: &egui::Style,
|
||||
emark_style: &easy_mark_parser::Style,
|
||||
egui_style: &egui::Style,
|
||||
emark_style: &easy_mark_parser::Style,
|
||||
) -> egui::text::TextFormat {
|
||||
use egui::{Align, Color32, Stroke, TextStyle};
|
||||
use egui::{Align, Color32, Stroke, TextStyle};
|
||||
|
||||
let color = if emark_style.strong || emark_style.heading {
|
||||
egui_style.visuals.strong_text_color()
|
||||
} else if emark_style.quoted {
|
||||
egui_style.visuals.weak_text_color()
|
||||
} else {
|
||||
egui_style.visuals.text_color()
|
||||
};
|
||||
let color = if emark_style.strong || emark_style.heading {
|
||||
egui_style.visuals.strong_text_color()
|
||||
} else if emark_style.quoted {
|
||||
egui_style.visuals.weak_text_color()
|
||||
} else {
|
||||
egui_style.visuals.text_color()
|
||||
};
|
||||
|
||||
let text_style = if emark_style.heading {
|
||||
TextStyle::Heading
|
||||
} else if emark_style.code {
|
||||
TextStyle::Monospace
|
||||
} else if emark_style.small | emark_style.raised {
|
||||
TextStyle::Small
|
||||
} else {
|
||||
TextStyle::Body
|
||||
};
|
||||
let text_style = if emark_style.heading {
|
||||
TextStyle::Heading
|
||||
} else if emark_style.code {
|
||||
TextStyle::Monospace
|
||||
} else if emark_style.small | emark_style.raised {
|
||||
TextStyle::Small
|
||||
} else {
|
||||
TextStyle::Body
|
||||
};
|
||||
|
||||
let background = if emark_style.code {
|
||||
egui_style.visuals.code_bg_color
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
let background = if emark_style.code {
|
||||
egui_style.visuals.code_bg_color
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
|
||||
let underline = if emark_style.underline {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
};
|
||||
let underline = if emark_style.underline {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
};
|
||||
|
||||
let strikethrough = if emark_style.strikethrough {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
};
|
||||
let strikethrough = if emark_style.strikethrough {
|
||||
Stroke::new(1.0, color)
|
||||
} else {
|
||||
Stroke::none()
|
||||
};
|
||||
|
||||
let valign = if emark_style.raised {
|
||||
Align::TOP
|
||||
} else {
|
||||
Align::BOTTOM
|
||||
};
|
||||
let valign = if emark_style.raised {
|
||||
Align::TOP
|
||||
} else {
|
||||
Align::BOTTOM
|
||||
};
|
||||
|
||||
egui::text::TextFormat {
|
||||
font_id: text_style.resolve(egui_style),
|
||||
color,
|
||||
background,
|
||||
italics: emark_style.italics,
|
||||
underline,
|
||||
strikethrough,
|
||||
valign,
|
||||
}
|
||||
egui::text::TextFormat {
|
||||
font_id: text_style.resolve(egui_style),
|
||||
color,
|
||||
background,
|
||||
italics: emark_style.italics,
|
||||
underline,
|
||||
strikethrough,
|
||||
valign,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,63 +9,63 @@
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Item<'a> {
|
||||
/// `\n`
|
||||
// TODO(emilk): add Style here so empty heading still uses up the right amount of space.
|
||||
Newline,
|
||||
/// `\n`
|
||||
// TODO(emilk): add Style here so empty heading still uses up the right amount of space.
|
||||
Newline,
|
||||
|
||||
///
|
||||
Text(Style, &'a str),
|
||||
///
|
||||
Text(Style, &'a str),
|
||||
|
||||
/// title, url
|
||||
Hyperlink(Style, &'a str, &'a str),
|
||||
/// title, url
|
||||
Hyperlink(Style, &'a str, &'a str),
|
||||
|
||||
/// leading space before e.g. a [`Self::BulletPoint`].
|
||||
Indentation(usize),
|
||||
/// leading space before e.g. a [`Self::BulletPoint`].
|
||||
Indentation(usize),
|
||||
|
||||
/// >
|
||||
QuoteIndent,
|
||||
/// >
|
||||
QuoteIndent,
|
||||
|
||||
/// - a point well made.
|
||||
BulletPoint,
|
||||
/// - a point well made.
|
||||
BulletPoint,
|
||||
|
||||
/// 1. numbered list. The string is the number(s).
|
||||
NumberedPoint(&'a str),
|
||||
/// 1. numbered list. The string is the number(s).
|
||||
NumberedPoint(&'a str),
|
||||
|
||||
/// ---
|
||||
Separator,
|
||||
/// ---
|
||||
Separator,
|
||||
|
||||
/// language, code
|
||||
CodeBlock(&'a str, &'a str),
|
||||
/// language, code
|
||||
CodeBlock(&'a str, &'a str),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Style {
|
||||
/// # heading (large text)
|
||||
pub heading: bool,
|
||||
/// # heading (large text)
|
||||
pub heading: bool,
|
||||
|
||||
/// > quoted (slightly dimmer color or other font style)
|
||||
pub quoted: bool,
|
||||
/// > quoted (slightly dimmer color or other font style)
|
||||
pub quoted: bool,
|
||||
|
||||
/// `code` (monospace, some other color)
|
||||
pub code: bool,
|
||||
/// `code` (monospace, some other color)
|
||||
pub code: bool,
|
||||
|
||||
/// self.strong* (emphasized, e.g. bold)
|
||||
pub strong: bool,
|
||||
/// self.strong* (emphasized, e.g. bold)
|
||||
pub strong: bool,
|
||||
|
||||
/// _underline_
|
||||
pub underline: bool,
|
||||
/// _underline_
|
||||
pub underline: bool,
|
||||
|
||||
/// ~strikethrough~
|
||||
pub strikethrough: bool,
|
||||
/// ~strikethrough~
|
||||
pub strikethrough: bool,
|
||||
|
||||
/// /italics/
|
||||
pub italics: bool,
|
||||
/// /italics/
|
||||
pub italics: bool,
|
||||
|
||||
/// $small$
|
||||
pub small: bool,
|
||||
/// $small$
|
||||
pub small: bool,
|
||||
|
||||
/// ^raised^
|
||||
pub raised: bool,
|
||||
/// ^raised^
|
||||
pub raised: bool,
|
||||
}
|
||||
|
||||
/// Parser for the `EasyMark` markup language.
|
||||
@@ -80,277 +80,277 @@ pub struct Style {
|
||||
///
|
||||
/// ```
|
||||
pub struct Parser<'a> {
|
||||
/// The remainder of the input text
|
||||
s: &'a str,
|
||||
/// The remainder of the input text
|
||||
s: &'a str,
|
||||
|
||||
/// Are we at the start of a line?
|
||||
start_of_line: bool,
|
||||
/// Are we at the start of a line?
|
||||
start_of_line: bool,
|
||||
|
||||
/// Current self.style. Reset after a newline.
|
||||
style: Style,
|
||||
/// Current self.style. Reset after a newline.
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
pub fn new(s: &'a str) -> Self {
|
||||
Self {
|
||||
s,
|
||||
start_of_line: true,
|
||||
style: Style::default(),
|
||||
pub fn new(s: &'a str) -> Self {
|
||||
Self {
|
||||
s,
|
||||
start_of_line: true,
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `1. `, `42. ` etc.
|
||||
fn numbered_list(&mut self) -> Option<Item<'a>> {
|
||||
let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) {
|
||||
let number = &self.s[..n_digits];
|
||||
self.s = &self.s[(n_digits + 2)..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::NumberedPoint(number));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||
if let Some(newline) = language_start.find('\n') {
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// `code`
|
||||
fn inline_code(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(rest) = self.s.strip_prefix('`') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.code = true;
|
||||
let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(end) = rest_of_line.find('`') {
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end + 1..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
} else {
|
||||
let end = rest_of_line.len();
|
||||
let item = Item::Text(self.style, rest_of_line);
|
||||
self.s = &self.s[end..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `<url>` or `[link](url)`
|
||||
fn url(&mut self) -> Option<Item<'a>> {
|
||||
if self.s.starts_with('<') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(url_end) = this_line.find('>') {
|
||||
let url = &self.s[1..url_end];
|
||||
self.s = &self.s[url_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, url, url));
|
||||
}
|
||||
}
|
||||
|
||||
/// `1. `, `42. ` etc.
|
||||
fn numbered_list(&mut self) -> Option<Item<'a>> {
|
||||
let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count();
|
||||
if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) {
|
||||
let number = &self.s[..n_digits];
|
||||
self.s = &self.s[(n_digits + 2)..];
|
||||
// [text](url)
|
||||
if self.s.starts_with('[') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(bracket_end) = this_line.find(']') {
|
||||
let text = &this_line[1..bracket_end];
|
||||
if this_line[bracket_end + 1..].starts_with('(') {
|
||||
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::NumberedPoint(number));
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
fn code_block(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(language_start) = self.s.strip_prefix("```") {
|
||||
if let Some(newline) = language_start.find('\n') {
|
||||
let language = &language_start[..newline];
|
||||
let code_start = &language_start[newline + 1..];
|
||||
if let Some(end) = code_start.find("\n```") {
|
||||
let code = &code_start[..end].trim();
|
||||
self.s = &code_start[end + 4..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::CodeBlock(language, code));
|
||||
} else {
|
||||
self.s = "";
|
||||
return Some(Item::CodeBlock(language, code_start));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// `code`
|
||||
fn inline_code(&mut self) -> Option<Item<'a>> {
|
||||
if let Some(rest) = self.s.strip_prefix('`') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.code = true;
|
||||
let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(end) = rest_of_line.find('`') {
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end + 1..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
} else {
|
||||
let end = rest_of_line.len();
|
||||
let item = Item::Text(self.style, rest_of_line);
|
||||
self.s = &self.s[end..];
|
||||
self.style.code = false;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `<url>` or `[link](url)`
|
||||
fn url(&mut self) -> Option<Item<'a>> {
|
||||
if self.s.starts_with('<') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(url_end) = this_line.find('>') {
|
||||
let url = &self.s[1..url_end];
|
||||
self.s = &self.s[url_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, url, url));
|
||||
}
|
||||
}
|
||||
|
||||
// [text](url)
|
||||
if self.s.starts_with('[') {
|
||||
let this_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())];
|
||||
if let Some(bracket_end) = this_line.find(']') {
|
||||
let text = &this_line[1..bracket_end];
|
||||
if this_line[bracket_end + 1..].starts_with('(') {
|
||||
if let Some(parens_end) = this_line[bracket_end + 2..].find(')') {
|
||||
let parens_end = bracket_end + 2 + parens_end;
|
||||
let url = &self.s[bracket_end + 2..parens_end];
|
||||
self.s = &self.s[parens_end + 1..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Hyperlink(self.style, text, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Parser<'a> {
|
||||
type Item = Item<'a>;
|
||||
type Item = Item<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// \n
|
||||
if self.s.starts_with('\n') {
|
||||
self.s = &self.s[1..];
|
||||
self.start_of_line = true;
|
||||
self.style = Style::default();
|
||||
return Some(Item::Newline);
|
||||
}
|
||||
// \n
|
||||
if self.s.starts_with('\n') {
|
||||
self.s = &self.s[1..];
|
||||
self.start_of_line = true;
|
||||
self.style = Style::default();
|
||||
return Some(Item::Newline);
|
||||
}
|
||||
|
||||
// Ignore line break (continue on the same line)
|
||||
if self.s.starts_with("\\\n") && self.s.len() >= 2 {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
continue;
|
||||
}
|
||||
// Ignore line break (continue on the same line)
|
||||
if self.s.starts_with("\\\n") && self.s.len() >= 2 {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// \ escape (to show e.g. a backtick)
|
||||
if self.s.starts_with('\\') && self.s.len() >= 2 {
|
||||
let text = &self.s[1..2];
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Text(self.style, text));
|
||||
}
|
||||
// \ escape (to show e.g. a backtick)
|
||||
if self.s.starts_with('\\') && self.s.len() >= 2 {
|
||||
let text = &self.s[1..2];
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Text(self.style, text));
|
||||
}
|
||||
|
||||
if self.start_of_line {
|
||||
// leading space (indentation)
|
||||
if self.s.starts_with(' ') {
|
||||
let length = self.s.find(|c| c != ' ').unwrap_or(self.s.len());
|
||||
self.s = &self.s[length..];
|
||||
self.start_of_line = true; // indentation doesn't count
|
||||
return Some(Item::Indentation(length));
|
||||
}
|
||||
|
||||
// # Heading
|
||||
if let Some(after) = self.s.strip_prefix("# ") {
|
||||
self.s = after;
|
||||
self.start_of_line = false;
|
||||
self.style.heading = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// > quote
|
||||
if let Some(after) = self.s.strip_prefix("> ") {
|
||||
self.s = after;
|
||||
self.start_of_line = true; // quote indentation doesn't count
|
||||
self.style.quoted = true;
|
||||
return Some(Item::QuoteIndent);
|
||||
}
|
||||
|
||||
// - bullet point
|
||||
if self.s.starts_with("- ") {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::BulletPoint);
|
||||
}
|
||||
|
||||
// `1. `, `42. ` etc.
|
||||
if let Some(item) = self.numbered_list() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// --- separator
|
||||
if let Some(after) = self.s.strip_prefix("---") {
|
||||
self.s = after.trim_start_matches('-'); // remove extra dashes
|
||||
self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Separator);
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
if let Some(item) = self.code_block() {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
// `code`
|
||||
if let Some(item) = self.inline_code() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
if let Some(rest) = self.s.strip_prefix('*') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strong = !self.style.strong;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('_') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.underline = !self.style.underline;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('~') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strikethrough = !self.style.strikethrough;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('/') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.italics = !self.style.italics;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('$') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.small = !self.style.small;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('^') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.raised = !self.style.raised;
|
||||
continue;
|
||||
}
|
||||
|
||||
// `<url>` or `[link](url)`
|
||||
if let Some(item) = self.url() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let end = self
|
||||
.s
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..])
|
||||
.map_or_else(|| self.s.len(), |special| special.max(1));
|
||||
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end..];
|
||||
self.start_of_line = false;
|
||||
return Some(item);
|
||||
if self.start_of_line {
|
||||
// leading space (indentation)
|
||||
if self.s.starts_with(' ') {
|
||||
let length = self.s.find(|c| c != ' ').unwrap_or(self.s.len());
|
||||
self.s = &self.s[length..];
|
||||
self.start_of_line = true; // indentation doesn't count
|
||||
return Some(Item::Indentation(length));
|
||||
}
|
||||
|
||||
// # Heading
|
||||
if let Some(after) = self.s.strip_prefix("# ") {
|
||||
self.s = after;
|
||||
self.start_of_line = false;
|
||||
self.style.heading = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// > quote
|
||||
if let Some(after) = self.s.strip_prefix("> ") {
|
||||
self.s = after;
|
||||
self.start_of_line = true; // quote indentation doesn't count
|
||||
self.style.quoted = true;
|
||||
return Some(Item::QuoteIndent);
|
||||
}
|
||||
|
||||
// - bullet point
|
||||
if self.s.starts_with("- ") {
|
||||
self.s = &self.s[2..];
|
||||
self.start_of_line = false;
|
||||
return Some(Item::BulletPoint);
|
||||
}
|
||||
|
||||
// `1. `, `42. ` etc.
|
||||
if let Some(item) = self.numbered_list() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// --- separator
|
||||
if let Some(after) = self.s.strip_prefix("---") {
|
||||
self.s = after.trim_start_matches('-'); // remove extra dashes
|
||||
self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline
|
||||
self.start_of_line = false;
|
||||
return Some(Item::Separator);
|
||||
}
|
||||
|
||||
// ```{language}\n{code}```
|
||||
if let Some(item) = self.code_block() {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
// `code`
|
||||
if let Some(item) = self.inline_code() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
if let Some(rest) = self.s.strip_prefix('*') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strong = !self.style.strong;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('_') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.underline = !self.style.underline;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('~') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.strikethrough = !self.style.strikethrough;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('/') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.italics = !self.style.italics;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('$') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.small = !self.style.small;
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = self.s.strip_prefix('^') {
|
||||
self.s = rest;
|
||||
self.start_of_line = false;
|
||||
self.style.raised = !self.style.raised;
|
||||
continue;
|
||||
}
|
||||
|
||||
// `<url>` or `[link](url)`
|
||||
if let Some(item) = self.url() {
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
// Swallow everything up to the next special character:
|
||||
let end = self
|
||||
.s
|
||||
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..])
|
||||
.map_or_else(|| self.s.len(), |special| special.max(1));
|
||||
|
||||
let item = Item::Text(self.style, &self.s[..end]);
|
||||
self.s = &self.s[end..];
|
||||
self.start_of_line = false;
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_easy_mark_parser() {
|
||||
let items: Vec<_> = Parser::new("~strikethrough `code`~").collect();
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
Item::Text(
|
||||
Style {
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"strikethrough "
|
||||
),
|
||||
Item::Text(
|
||||
Style {
|
||||
code: true,
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"code"
|
||||
),
|
||||
]
|
||||
);
|
||||
let items: Vec<_> = Parser::new("~strikethrough `code`~").collect();
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![
|
||||
Item::Text(
|
||||
Style {
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"strikethrough "
|
||||
),
|
||||
Item::Text(
|
||||
Style {
|
||||
code: true,
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
},
|
||||
"code"
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,157 +3,157 @@ use egui::*;
|
||||
|
||||
/// Parse and display a VERY simple and small subset of Markdown.
|
||||
pub fn easy_mark(ui: &mut Ui, easy_mark: &str) {
|
||||
easy_mark_it(ui, easy_mark::Parser::new(easy_mark));
|
||||
easy_mark_it(ui, easy_mark::Parser::new(easy_mark));
|
||||
}
|
||||
|
||||
pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator<Item = easy_mark::Item<'em>>) {
|
||||
let initial_size = vec2(
|
||||
ui.available_width(),
|
||||
ui.spacing().interact_size.y, // Assume there will be
|
||||
);
|
||||
let initial_size = vec2(
|
||||
ui.available_width(),
|
||||
ui.spacing().interact_size.y, // Assume there will be
|
||||
);
|
||||
|
||||
let layout = Layout::left_to_right(Align::BOTTOM).with_main_wrap(true);
|
||||
let layout = Layout::left_to_right(Align::BOTTOM).with_main_wrap(true);
|
||||
|
||||
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
ui.set_row_height(row_height);
|
||||
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
ui.set_row_height(row_height);
|
||||
|
||||
for item in items {
|
||||
item_ui(ui, item);
|
||||
}
|
||||
});
|
||||
for item in items {
|
||||
item_ui(ui, item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let one_indent = row_height / 2.0;
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let one_indent = row_height / 2.0;
|
||||
|
||||
match item {
|
||||
easy_mark::Item::Newline => {
|
||||
// ui.label("\n"); // too much spacing (paragraph spacing)
|
||||
ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height
|
||||
ui.end_row();
|
||||
ui.set_row_height(row_height);
|
||||
}
|
||||
match item {
|
||||
easy_mark::Item::Newline => {
|
||||
// ui.label("\n"); // too much spacing (paragraph spacing)
|
||||
ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height
|
||||
ui.end_row();
|
||||
ui.set_row_height(row_height);
|
||||
}
|
||||
|
||||
easy_mark::Item::Text(style, text) => {
|
||||
ui.label(rich_text_from_style(text, &style));
|
||||
}
|
||||
easy_mark::Item::Hyperlink(style, text, url) => {
|
||||
let label = rich_text_from_style(text, &style);
|
||||
ui.add(Hyperlink::from_label_and_url(label, url));
|
||||
}
|
||||
easy_mark::Item::Text(style, text) => {
|
||||
ui.label(rich_text_from_style(text, &style));
|
||||
}
|
||||
easy_mark::Item::Hyperlink(style, text, url) => {
|
||||
let label = rich_text_from_style(text, &style);
|
||||
ui.add(Hyperlink::from_label_and_url(label, url));
|
||||
}
|
||||
|
||||
easy_mark::Item::Separator => {
|
||||
ui.add(Separator::default().horizontal());
|
||||
}
|
||||
easy_mark::Item::Indentation(indent) => {
|
||||
let indent = indent as f32 * one_indent;
|
||||
ui.allocate_exact_size(vec2(indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::QuoteIndent => {
|
||||
let rect = ui
|
||||
.allocate_exact_size(vec2(2.0 * one_indent, row_height), Sense::hover())
|
||||
.0;
|
||||
let rect = rect.expand2(ui.style().spacing.item_spacing * 0.5);
|
||||
ui.painter().line_segment(
|
||||
[rect.center_top(), rect.center_bottom()],
|
||||
(1.0, ui.visuals().weak_text_color()),
|
||||
);
|
||||
}
|
||||
easy_mark::Item::BulletPoint => {
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
bullet_point(ui, one_indent);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::NumberedPoint(number) => {
|
||||
let width = 3.0 * one_indent;
|
||||
numbered_point(ui, width, number);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::CodeBlock(_language, code) => {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut rect = ui.monospace(code).rect;
|
||||
rect = rect.expand(1.0); // looks better
|
||||
rect.max.x = ui.max_rect().max.x;
|
||||
let code_bg_color = ui.visuals().code_bg_color;
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||
);
|
||||
}
|
||||
};
|
||||
easy_mark::Item::Separator => {
|
||||
ui.add(Separator::default().horizontal());
|
||||
}
|
||||
easy_mark::Item::Indentation(indent) => {
|
||||
let indent = indent as f32 * one_indent;
|
||||
ui.allocate_exact_size(vec2(indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::QuoteIndent => {
|
||||
let rect = ui
|
||||
.allocate_exact_size(vec2(2.0 * one_indent, row_height), Sense::hover())
|
||||
.0;
|
||||
let rect = rect.expand2(ui.style().spacing.item_spacing * 0.5);
|
||||
ui.painter().line_segment(
|
||||
[rect.center_top(), rect.center_bottom()],
|
||||
(1.0, ui.visuals().weak_text_color()),
|
||||
);
|
||||
}
|
||||
easy_mark::Item::BulletPoint => {
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
bullet_point(ui, one_indent);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::NumberedPoint(number) => {
|
||||
let width = 3.0 * one_indent;
|
||||
numbered_point(ui, width, number);
|
||||
ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover());
|
||||
}
|
||||
easy_mark::Item::CodeBlock(_language, code) => {
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut rect = ui.monospace(code).rect;
|
||||
rect = rect.expand(1.0); // looks better
|
||||
rect.max.x = ui.max_rect().max.x;
|
||||
let code_bg_color = ui.visuals().code_bg_color;
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
Shape::rect_filled(rect, 1.0, code_bg_color),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
|
||||
let easy_mark::Style {
|
||||
heading,
|
||||
quoted,
|
||||
code,
|
||||
strong,
|
||||
underline,
|
||||
strikethrough,
|
||||
italics,
|
||||
small,
|
||||
raised,
|
||||
} = *style;
|
||||
let easy_mark::Style {
|
||||
heading,
|
||||
quoted,
|
||||
code,
|
||||
strong,
|
||||
underline,
|
||||
strikethrough,
|
||||
italics,
|
||||
small,
|
||||
raised,
|
||||
} = *style;
|
||||
|
||||
let small = small || raised; // Raised text is also smaller
|
||||
let small = small || raised; // Raised text is also smaller
|
||||
|
||||
let mut rich_text = RichText::new(text);
|
||||
if heading && !small {
|
||||
rich_text = rich_text.heading().strong();
|
||||
}
|
||||
if small && !heading {
|
||||
rich_text = rich_text.small();
|
||||
}
|
||||
if code {
|
||||
rich_text = rich_text.code();
|
||||
}
|
||||
if strong {
|
||||
rich_text = rich_text.strong();
|
||||
} else if quoted {
|
||||
rich_text = rich_text.weak();
|
||||
}
|
||||
if underline {
|
||||
rich_text = rich_text.underline();
|
||||
}
|
||||
if strikethrough {
|
||||
rich_text = rich_text.strikethrough();
|
||||
}
|
||||
if italics {
|
||||
rich_text = rich_text.italics();
|
||||
}
|
||||
if raised {
|
||||
rich_text = rich_text.raised();
|
||||
}
|
||||
rich_text
|
||||
let mut rich_text = RichText::new(text);
|
||||
if heading && !small {
|
||||
rich_text = rich_text.heading().strong();
|
||||
}
|
||||
if small && !heading {
|
||||
rich_text = rich_text.small();
|
||||
}
|
||||
if code {
|
||||
rich_text = rich_text.code();
|
||||
}
|
||||
if strong {
|
||||
rich_text = rich_text.strong();
|
||||
} else if quoted {
|
||||
rich_text = rich_text.weak();
|
||||
}
|
||||
if underline {
|
||||
rich_text = rich_text.underline();
|
||||
}
|
||||
if strikethrough {
|
||||
rich_text = rich_text.strikethrough();
|
||||
}
|
||||
if italics {
|
||||
rich_text = rich_text.italics();
|
||||
}
|
||||
if raised {
|
||||
rich_text = rich_text.raised();
|
||||
}
|
||||
rich_text
|
||||
}
|
||||
|
||||
fn bullet_point(ui: &mut Ui, width: f32) -> Response {
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
ui.painter().circle_filled(
|
||||
rect.center(),
|
||||
rect.height() / 8.0,
|
||||
ui.visuals().strong_text_color(),
|
||||
);
|
||||
response
|
||||
let row_height = ui.text_style_height(&TextStyle::Body);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
ui.painter().circle_filled(
|
||||
rect.center(),
|
||||
rect.height() / 8.0,
|
||||
ui.visuals().strong_text_color(),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
let text = format!("{}.", number);
|
||||
let text_color = ui.visuals().strong_text_color();
|
||||
ui.painter().text(
|
||||
rect.right_center(),
|
||||
Align2::RIGHT_CENTER,
|
||||
text,
|
||||
font_id,
|
||||
text_color,
|
||||
);
|
||||
response
|
||||
let font_id = TextStyle::Body.resolve(ui.style());
|
||||
let row_height = ui.fonts().row_height(&font_id);
|
||||
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
|
||||
let text = format!("{}.", number);
|
||||
let text_color = ui.visuals().strong_text_color();
|
||||
ui.painter().text(
|
||||
rect.right_center(),
|
||||
Align2::RIGHT_CENTER,
|
||||
text,
|
||||
font_id,
|
||||
text_color,
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -24,29 +24,29 @@ pub use demo::DemoWindows;
|
||||
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file on github.
|
||||
#[macro_export]
|
||||
macro_rules! egui_github_link_file {
|
||||
() => {
|
||||
$crate::egui_github_link_file!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
() => {
|
||||
$crate::egui_github_link_file!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a [`Hyperlink`](egui::Hyperlink) to this egui source code file and line on github.
|
||||
#[macro_export]
|
||||
macro_rules! egui_github_link_file_line {
|
||||
() => {
|
||||
$crate::egui_github_link_file_line!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file_line!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
() => {
|
||||
$crate::egui_github_link_file_line!("(source code)")
|
||||
};
|
||||
($label: expr) => {
|
||||
egui::github_link_file_line!(
|
||||
"https://github.com/emilk/egui/blob/master/",
|
||||
egui::RichText::new($label).small()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -61,40 +61,40 @@ Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, tur
|
||||
|
||||
#[test]
|
||||
fn test_egui_e2e() {
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput::default();
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput::default();
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(!clipped_primitives.is_empty());
|
||||
}
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(!clipped_primitives.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_egui_zero_window_size() {
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_max(egui::Pos2::ZERO, egui::Pos2::ZERO)),
|
||||
..Default::default()
|
||||
};
|
||||
let mut demo_windows = crate::DemoWindows::default();
|
||||
let ctx = egui::Context::default();
|
||||
let raw_input = egui::RawInput {
|
||||
screen_rect: Some(egui::Rect::from_min_max(egui::Pos2::ZERO, egui::Pos2::ZERO)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(
|
||||
clipped_primitives.is_empty(),
|
||||
"There should be nothing to show"
|
||||
);
|
||||
}
|
||||
const NUM_FRAMES: usize = 5;
|
||||
for _ in 0..NUM_FRAMES {
|
||||
let full_output = ctx.run(raw_input.clone(), |ctx| {
|
||||
demo_windows.ui(ctx);
|
||||
});
|
||||
let clipped_primitives = ctx.tessellate(full_output.shapes);
|
||||
assert!(
|
||||
clipped_primitives.is_empty(),
|
||||
"There should be nothing to show"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -102,6 +102,6 @@ fn test_egui_zero_window_size() {
|
||||
/// Detect narrow screens. This is used to show a simpler UI on mobile devices,
|
||||
/// especially for the web demo at <https://egui.rs>.
|
||||
pub fn is_mobile(ctx: &egui::Context) -> bool {
|
||||
let screen_size = ctx.input().screen_rect().size();
|
||||
screen_size.x < 550.0
|
||||
let screen_size = ctx.input().screen_rect().size();
|
||||
screen_size.x < 550.0
|
||||
}
|
||||
|
||||
@@ -2,38 +2,38 @@ use egui::text::LayoutJob;
|
||||
|
||||
/// View some code with syntax highlighting and selection.
|
||||
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||
let language = "rs";
|
||||
let theme = CodeTheme::from_memory(ui.ctx());
|
||||
let language = "rs";
|
||||
let theme = CodeTheme::from_memory(ui.ctx());
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
||||
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
|
||||
let layout_job = highlight(ui.ctx(), &theme, string, language);
|
||||
// layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||
ui.fonts().layout_job(layout_job)
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(1)
|
||||
.lock_focus(true)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
ui.add(
|
||||
egui::TextEdit::multiline(&mut code)
|
||||
.font(egui::TextStyle::Monospace) // for cursor height
|
||||
.code_editor()
|
||||
.desired_rows(1)
|
||||
.lock_focus(true)
|
||||
.layouter(&mut layouter),
|
||||
);
|
||||
}
|
||||
|
||||
/// Memoized Code highlighting
|
||||
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
||||
self.highlight(theme, code, lang)
|
||||
}
|
||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
||||
self.highlight(theme, code, lang)
|
||||
}
|
||||
}
|
||||
|
||||
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
||||
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
||||
|
||||
let mut memory = ctx.memory();
|
||||
let highlight_cache = memory.caches.cache::<HighlightCache>();
|
||||
highlight_cache.get((theme, code, language))
|
||||
let mut memory = ctx.memory();
|
||||
let highlight_cache = memory.caches.cache::<HighlightCache>();
|
||||
highlight_cache.get((theme, code, language))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -43,349 +43,351 @@ pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(enum_map::Enum)]
|
||||
enum TokenType {
|
||||
Comment,
|
||||
Keyword,
|
||||
Literal,
|
||||
StringLiteral,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
Comment,
|
||||
Keyword,
|
||||
Literal,
|
||||
StringLiteral,
|
||||
Punctuation,
|
||||
Whitespace,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
#[derive(Clone, Copy, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum SyntectTheme {
|
||||
Base16EightiesDark,
|
||||
Base16MochaDark,
|
||||
Base16OceanDark,
|
||||
Base16OceanLight,
|
||||
InspiredGitHub,
|
||||
SolarizedDark,
|
||||
SolarizedLight,
|
||||
Base16EightiesDark,
|
||||
Base16MochaDark,
|
||||
Base16OceanDark,
|
||||
Base16OceanLight,
|
||||
InspiredGitHub,
|
||||
SolarizedDark,
|
||||
SolarizedLight,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl SyntectTheme {
|
||||
fn all() -> impl ExactSizeIterator<Item = Self> {
|
||||
[
|
||||
Self::Base16EightiesDark,
|
||||
Self::Base16MochaDark,
|
||||
Self::Base16OceanDark,
|
||||
Self::Base16OceanLight,
|
||||
Self::InspiredGitHub,
|
||||
Self::SolarizedDark,
|
||||
Self::SolarizedLight,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
fn all() -> impl ExactSizeIterator<Item = Self> {
|
||||
[
|
||||
Self::Base16EightiesDark,
|
||||
Self::Base16MochaDark,
|
||||
Self::Base16OceanDark,
|
||||
Self::Base16OceanLight,
|
||||
Self::InspiredGitHub,
|
||||
Self::SolarizedDark,
|
||||
Self::SolarizedLight,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
||||
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
||||
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
||||
Self::Base16OceanLight => "Base16 Ocean (light)",
|
||||
Self::InspiredGitHub => "InspiredGitHub (light)",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
||||
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
||||
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
||||
Self::Base16OceanLight => "Base16 Ocean (light)",
|
||||
Self::InspiredGitHub => "InspiredGitHub (light)",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
}
|
||||
|
||||
fn syntect_key_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "base16-eighties.dark",
|
||||
Self::Base16MochaDark => "base16-mocha.dark",
|
||||
Self::Base16OceanDark => "base16-ocean.dark",
|
||||
Self::Base16OceanLight => "base16-ocean.light",
|
||||
Self::InspiredGitHub => "InspiredGitHub",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
fn syntect_key_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Base16EightiesDark => "base16-eighties.dark",
|
||||
Self::Base16MochaDark => "base16-mocha.dark",
|
||||
Self::Base16OceanDark => "base16-ocean.dark",
|
||||
Self::Base16OceanLight => "base16-ocean.light",
|
||||
Self::InspiredGitHub => "InspiredGitHub",
|
||||
Self::SolarizedDark => "Solarized (dark)",
|
||||
Self::SolarizedLight => "Solarized (light)",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dark(&self) -> bool {
|
||||
match self {
|
||||
Self::Base16EightiesDark
|
||||
| Self::Base16MochaDark
|
||||
| Self::Base16OceanDark
|
||||
| Self::SolarizedDark => true,
|
||||
pub fn is_dark(&self) -> bool {
|
||||
match self {
|
||||
Self::Base16EightiesDark
|
||||
| Self::Base16MochaDark
|
||||
| Self::Base16OceanDark
|
||||
| Self::SolarizedDark => true,
|
||||
|
||||
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
||||
}
|
||||
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub struct CodeTheme {
|
||||
dark_mode: bool,
|
||||
dark_mode: bool,
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
syntect_theme: SyntectTheme,
|
||||
#[cfg(feature = "syntect")]
|
||||
syntect_theme: SyntectTheme,
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
||||
}
|
||||
|
||||
impl Default for CodeTheme {
|
||||
fn default() -> Self {
|
||||
Self::dark()
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self::dark()
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeTheme {
|
||||
pub fn from_style(style: &egui::Style) -> Self {
|
||||
if style.visuals.dark_mode {
|
||||
Self::dark()
|
||||
} else {
|
||||
Self::light()
|
||||
}
|
||||
pub fn from_style(style: &egui::Style) -> Self {
|
||||
if style.visuals.dark_mode {
|
||||
Self::dark()
|
||||
} else {
|
||||
Self::light()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||
if ctx.style().visuals.dark_mode {
|
||||
ctx.data()
|
||||
.get_persisted(egui::Id::new("dark"))
|
||||
.unwrap_or_else(CodeTheme::dark)
|
||||
} else {
|
||||
ctx.data()
|
||||
.get_persisted(egui::Id::new("light"))
|
||||
.unwrap_or_else(CodeTheme::light)
|
||||
}
|
||||
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||
if ctx.style().visuals.dark_mode {
|
||||
ctx
|
||||
.data()
|
||||
.get_persisted(egui::Id::new("dark"))
|
||||
.unwrap_or_else(CodeTheme::dark)
|
||||
} else {
|
||||
ctx
|
||||
.data()
|
||||
.get_persisted(egui::Id::new("light"))
|
||||
.unwrap_or_else(CodeTheme::light)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||
if self.dark_mode {
|
||||
ctx.data().insert_persisted(egui::Id::new("dark"), self);
|
||||
} else {
|
||||
ctx.data().insert_persisted(egui::Id::new("light"), self);
|
||||
}
|
||||
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||
if self.dark_mode {
|
||||
ctx.data().insert_persisted(egui::Id::new("dark"), self);
|
||||
} else {
|
||||
ctx.data().insert_persisted(egui::Id::new("light"), self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl CodeTheme {
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
dark_mode: true,
|
||||
syntect_theme: SyntectTheme::Base16MochaDark,
|
||||
}
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
dark_mode: true,
|
||||
syntect_theme: SyntectTheme::Base16MochaDark,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
dark_mode: false,
|
||||
syntect_theme: SyntectTheme::SolarizedLight,
|
||||
}
|
||||
pub fn light() -> Self {
|
||||
Self {
|
||||
dark_mode: false,
|
||||
syntect_theme: SyntectTheme::SolarizedLight,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
|
||||
for theme in SyntectTheme::all() {
|
||||
if theme.is_dark() == self.dark_mode {
|
||||
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
||||
}
|
||||
}
|
||||
for theme in SyntectTheme::all() {
|
||||
if theme.is_dark() == self.dark_mode {
|
||||
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl CodeTheme {
|
||||
pub fn dark() -> Self {
|
||||
let font_id = egui::FontId::monospace(12.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: true,
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
pub fn dark() -> Self {
|
||||
let font_id = egui::FontId::monospace(12.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: true,
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn light() -> Self {
|
||||
let font_id = egui::FontId::monospace(12.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: false,
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
pub fn light() -> Self {
|
||||
let font_id = egui::FontId::monospace(12.0);
|
||||
use egui::{Color32, TextFormat};
|
||||
Self {
|
||||
dark_mode: false,
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
formats: enum_map::enum_map![
|
||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY),
|
||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)),
|
||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)),
|
||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)),
|
||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY),
|
||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal_top(|ui| {
|
||||
let selected_id = egui::Id::null();
|
||||
let mut selected_tt: TokenType = *ui
|
||||
.data()
|
||||
.get_persisted_mut_or(selected_id, TokenType::Comment);
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal_top(|ui| {
|
||||
let selected_id = egui::Id::null();
|
||||
let mut selected_tt: TokenType = *ui
|
||||
.data()
|
||||
.get_persisted_mut_or(selected_id, TokenType::Comment);
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(150.0);
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(150.0);
|
||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.scope(|ui| {
|
||||
for (tt, tt_name) in [
|
||||
(TokenType::Comment, "// comment"),
|
||||
(TokenType::Keyword, "keyword"),
|
||||
(TokenType::Literal, "literal"),
|
||||
(TokenType::StringLiteral, "\"string literal\""),
|
||||
(TokenType::Punctuation, "punctuation ;"),
|
||||
// (TokenType::Whitespace, "whitespace"),
|
||||
] {
|
||||
let format = &mut self.formats[tt];
|
||||
ui.style_mut().override_font_id = Some(format.font_id.clone());
|
||||
ui.visuals_mut().override_text_color = Some(format.color);
|
||||
ui.radio_value(&mut selected_tt, tt, tt_name);
|
||||
}
|
||||
});
|
||||
|
||||
let reset_value = if self.dark_mode {
|
||||
CodeTheme::dark()
|
||||
} else {
|
||||
CodeTheme::light()
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(*self != reset_value, egui::Button::new("Reset theme"))
|
||||
.clicked()
|
||||
{
|
||||
*self = reset_value;
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.data().insert_persisted(selected_id, selected_tt);
|
||||
|
||||
egui::Frame::group(ui.style())
|
||||
.inner_margin(egui::Vec2::splat(2.0))
|
||||
.show(ui, |ui| {
|
||||
// ui.group(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
||||
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
||||
egui::widgets::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.formats[selected_tt].color,
|
||||
egui::color_picker::Alpha::Opaque,
|
||||
);
|
||||
});
|
||||
ui.scope(|ui| {
|
||||
for (tt, tt_name) in [
|
||||
(TokenType::Comment, "// comment"),
|
||||
(TokenType::Keyword, "keyword"),
|
||||
(TokenType::Literal, "literal"),
|
||||
(TokenType::StringLiteral, "\"string literal\""),
|
||||
(TokenType::Punctuation, "punctuation ;"),
|
||||
// (TokenType::Whitespace, "whitespace"),
|
||||
] {
|
||||
let format = &mut self.formats[tt];
|
||||
ui.style_mut().override_font_id = Some(format.font_id.clone());
|
||||
ui.visuals_mut().override_text_color = Some(format.color);
|
||||
ui.radio_value(&mut selected_tt, tt, tt_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let reset_value = if self.dark_mode {
|
||||
CodeTheme::dark()
|
||||
} else {
|
||||
CodeTheme::light()
|
||||
};
|
||||
|
||||
if ui
|
||||
.add_enabled(*self != reset_value, egui::Button::new("Reset theme"))
|
||||
.clicked()
|
||||
{
|
||||
*self = reset_value;
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.data().insert_persisted(selected_id, selected_tt);
|
||||
|
||||
egui::Frame::group(ui.style())
|
||||
.inner_margin(egui::Vec2::splat(2.0))
|
||||
.show(ui, |ui| {
|
||||
// ui.group(|ui| {
|
||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
||||
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
||||
egui::widgets::color_picker::color_picker_color32(
|
||||
ui,
|
||||
&mut self.formats[selected_tt].color,
|
||||
egui::color_picker::Alpha::Opaque,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
struct Highlighter {
|
||||
ps: syntect::parsing::SyntaxSet,
|
||||
ts: syntect::highlighting::ThemeSet,
|
||||
ps: syntect::parsing::SyntaxSet,
|
||||
ts: syntect::highlighting::ThemeSet,
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl Default for Highlighter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||
}
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
||||
// Fallback:
|
||||
LayoutJob::simple(
|
||||
code.into(),
|
||||
egui::FontId::monospace(14.0),
|
||||
if theme.dark_mode {
|
||||
egui::Color32::LIGHT_GRAY
|
||||
} else {
|
||||
egui::Color32::DARK_GRAY
|
||||
},
|
||||
f32::INFINITY,
|
||||
)
|
||||
})
|
||||
}
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
||||
// Fallback:
|
||||
LayoutJob::simple(
|
||||
code.into(),
|
||||
egui::FontId::monospace(14.0),
|
||||
if theme.dark_mode {
|
||||
egui::Color32::LIGHT_GRAY
|
||||
} else {
|
||||
egui::Color32::DARK_GRAY
|
||||
},
|
||||
f32::INFINITY,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::FontStyle;
|
||||
use syntect::util::LinesWithEndings;
|
||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::FontStyle;
|
||||
use syntect::util::LinesWithEndings;
|
||||
|
||||
let syntax = self
|
||||
.ps
|
||||
.find_syntax_by_name(language)
|
||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||
let syntax = self
|
||||
.ps
|
||||
.find_syntax_by_name(language)
|
||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||
|
||||
let theme = theme.syntect_theme.syntect_key_name();
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
||||
let theme = theme.syntect_theme.syntect_key_name();
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
||||
|
||||
use egui::text::{LayoutSection, TextFormat};
|
||||
use egui::text::{LayoutSection, TextFormat};
|
||||
|
||||
let mut job = LayoutJob {
|
||||
text: text.into(),
|
||||
..Default::default()
|
||||
let mut job = LayoutJob {
|
||||
text: text.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for line in LinesWithEndings::from(text) {
|
||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
||||
let fg = style.foreground;
|
||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = if underline {
|
||||
egui::Stroke::new(1.0, text_color)
|
||||
} else {
|
||||
egui::Stroke::none()
|
||||
};
|
||||
|
||||
for line in LinesWithEndings::from(text) {
|
||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
||||
let fg = style.foreground;
|
||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||
let underline = if underline {
|
||||
egui::Stroke::new(1.0, text_color)
|
||||
} else {
|
||||
egui::Stroke::none()
|
||||
};
|
||||
job.sections.push(LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: as_byte_range(text, range),
|
||||
format: TextFormat {
|
||||
font_id: egui::FontId::monospace(14.0),
|
||||
color: text_color,
|
||||
italics,
|
||||
underline,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(job)
|
||||
job.sections.push(LayoutSection {
|
||||
leading_space: 0.0,
|
||||
byte_range: as_byte_range(text, range),
|
||||
format: TextFormat {
|
||||
font_id: egui::FontId::monospace(14.0),
|
||||
color: text_color,
|
||||
italics,
|
||||
underline,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(job)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||
let whole_start = whole.as_ptr() as usize;
|
||||
let range_start = range.as_ptr() as usize;
|
||||
assert!(whole_start <= range_start);
|
||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||
let offset = range_start - whole_start;
|
||||
offset..(offset + range.len())
|
||||
let whole_start = whole.as_ptr() as usize;
|
||||
let range_start = range.as_ptr() as usize;
|
||||
assert!(whole_start <= range_start);
|
||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||
let offset = range_start - whole_start;
|
||||
offset..(offset + range.len())
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -396,108 +398,109 @@ struct Highlighter {}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
impl Highlighter {
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
||||
// Extremely simple syntax highlighter for when we compile without syntect
|
||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
||||
// Extremely simple syntax highlighter for when we compile without syntect
|
||||
|
||||
let mut job = LayoutJob::default();
|
||||
let mut job = LayoutJob::default();
|
||||
|
||||
while !text.is_empty() {
|
||||
if text.starts_with("//") {
|
||||
let end = text.find('\n').unwrap_or(text.len());
|
||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with('"') {
|
||||
let end = text[1..]
|
||||
.find('"')
|
||||
.map(|i| i + 2)
|
||||
.or_else(|| text.find('\n'))
|
||||
.unwrap_or(text.len());
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::StringLiteral].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
let word = &text[..end];
|
||||
let tt = if is_keyword(word) {
|
||||
TokenType::Keyword
|
||||
} else {
|
||||
TokenType::Literal
|
||||
};
|
||||
job.append(word, 0.0, theme.formats[tt].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_whitespace())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Whitespace].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else {
|
||||
let mut it = text.char_indices();
|
||||
it.next();
|
||||
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Punctuation].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
}
|
||||
}
|
||||
|
||||
job
|
||||
while !text.is_empty() {
|
||||
if text.starts_with("//") {
|
||||
let end = text.find('\n').unwrap_or(text.len());
|
||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with('"') {
|
||||
let end = text[1..]
|
||||
.find('"')
|
||||
.map(|i| i + 2)
|
||||
.or_else(|| text.find('\n'))
|
||||
.unwrap_or(text.len());
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::StringLiteral].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
let word = &text[..end];
|
||||
let tt = if is_keyword(word) {
|
||||
TokenType::Keyword
|
||||
} else {
|
||||
TokenType::Literal
|
||||
};
|
||||
job.append(word, 0.0, theme.formats[tt].clone());
|
||||
text = &text[end..];
|
||||
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
||||
let end = text[1..]
|
||||
.find(|c: char| !c.is_ascii_whitespace())
|
||||
.map_or_else(|| text.len(), |i| i + 1);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Whitespace].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
} else {
|
||||
let mut it = text.char_indices();
|
||||
it.next();
|
||||
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
||||
job.append(
|
||||
&text[..end],
|
||||
0.0,
|
||||
theme.formats[TokenType::Punctuation].clone(),
|
||||
);
|
||||
text = &text[end..];
|
||||
}
|
||||
}
|
||||
|
||||
job
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "syntect"))]
|
||||
fn is_keyword(word: &str) -> bool {
|
||||
matches!(
|
||||
word,
|
||||
"as" | "async"
|
||||
| "await"
|
||||
| "break"
|
||||
| "const"
|
||||
| "continue"
|
||||
| "crate"
|
||||
| "dyn"
|
||||
| "else"
|
||||
| "enum"
|
||||
| "extern"
|
||||
| "false"
|
||||
| "fn"
|
||||
| "for"
|
||||
| "if"
|
||||
| "impl"
|
||||
| "in"
|
||||
| "let"
|
||||
| "loop"
|
||||
| "match"
|
||||
| "mod"
|
||||
| "move"
|
||||
| "mut"
|
||||
| "pub"
|
||||
| "ref"
|
||||
| "return"
|
||||
| "self"
|
||||
| "Self"
|
||||
| "static"
|
||||
| "struct"
|
||||
| "super"
|
||||
| "trait"
|
||||
| "true"
|
||||
| "type"
|
||||
| "unsafe"
|
||||
| "use"
|
||||
| "where"
|
||||
| "while"
|
||||
)
|
||||
matches!(
|
||||
word,
|
||||
"as"
|
||||
| "async"
|
||||
| "await"
|
||||
| "break"
|
||||
| "const"
|
||||
| "continue"
|
||||
| "crate"
|
||||
| "dyn"
|
||||
| "else"
|
||||
| "enum"
|
||||
| "extern"
|
||||
| "false"
|
||||
| "fn"
|
||||
| "for"
|
||||
| "if"
|
||||
| "impl"
|
||||
| "in"
|
||||
| "let"
|
||||
| "loop"
|
||||
| "match"
|
||||
| "mod"
|
||||
| "move"
|
||||
| "mut"
|
||||
| "pub"
|
||||
| "ref"
|
||||
| "return"
|
||||
| "self"
|
||||
| "Self"
|
||||
| "static"
|
||||
| "struct"
|
||||
| "super"
|
||||
| "trait"
|
||||
| "true"
|
||||
| "type"
|
||||
| "unsafe"
|
||||
| "use"
|
||||
| "where"
|
||||
| "while"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ impl<T: UserEvent, R: Runtime> PluginBuilder<T> for EguiPluginBuilder<R> {
|
||||
main_thread: plugin::MainThreadContext {
|
||||
windows: Default::default(),
|
||||
},
|
||||
webview_id_map: Default::default(),
|
||||
},
|
||||
create_window_channel: sync_channel(1),
|
||||
is_focused: false,
|
||||
|
||||
141
src/plugin.rs
141
src/plugin.rs
@@ -4,10 +4,7 @@
|
||||
|
||||
use eframe::CreationContext;
|
||||
use raw_window_handle::HasRawWindowHandle;
|
||||
use tauri::utils::Theme;
|
||||
use tauri_runtime::{window::WindowEvent, RunEvent, UserEvent};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_runtime_wry::wry::application::platform::macos::WindowExtMacOS;
|
||||
#[cfg(target_os = "linux")]
|
||||
use tauri_runtime_wry::wry::application::platform::unix::WindowExtUnix;
|
||||
#[cfg(windows)]
|
||||
@@ -22,8 +19,8 @@ use tauri_runtime_wry::{
|
||||
},
|
||||
Context as WryContext, CursorIconWrapper, EventLoopIterationContext, Message,
|
||||
PhysicalPositionWrapper, PhysicalSizeWrapper, Plugin, PositionWrapper, RawWindowHandle,
|
||||
SizeWrapper, WebContextStore, WebviewId, WebviewIdStore, WindowEventListeners,
|
||||
WindowEventWrapper, WindowMenuEventListeners, WindowMessage,
|
||||
SizeWrapper, WebContextStore, WebviewId, WindowEventListeners, WindowEventWrapper, WindowId,
|
||||
WindowMenuEventListeners, WindowMessage,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -44,6 +41,19 @@ use window::Window;
|
||||
|
||||
pub type AppCreator = Box<dyn FnOnce(&CreationContext<'_>) -> Box<dyn eframe::App + Send> + Send>;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WebviewIdStore(Arc<Mutex<HashMap<WindowId, WebviewId>>>);
|
||||
|
||||
impl WebviewIdStore {
|
||||
pub fn insert(&self, w: WindowId, id: WebviewId) {
|
||||
self.0.lock().unwrap().insert(w, id);
|
||||
}
|
||||
|
||||
fn get(&self, w: &WindowId) -> Option<WebviewId> {
|
||||
self.0.lock().unwrap().get(w).copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateWindowPayload {
|
||||
window_id: WebviewId,
|
||||
label: String,
|
||||
@@ -70,6 +80,7 @@ unsafe impl Sync for MainThreadContext {}
|
||||
pub struct Context<T: UserEvent> {
|
||||
pub(crate) inner: WryContext<T>,
|
||||
pub(crate) main_thread: MainThreadContext,
|
||||
pub(crate) webview_id_map: WebviewIdStore,
|
||||
}
|
||||
|
||||
pub struct EguiPlugin<T: UserEvent> {
|
||||
@@ -139,7 +150,7 @@ impl<T: UserEvent> EguiPluginHandle<T> {
|
||||
if let Some(main_thread) = main_thread {
|
||||
create_gl_window(
|
||||
&main_thread.window_target,
|
||||
&self.context.inner.webview_id_map,
|
||||
&self.context.webview_id_map,
|
||||
&self.context.main_thread.windows,
|
||||
label,
|
||||
app_creator,
|
||||
@@ -189,7 +200,7 @@ impl<T: UserEvent> Plugin<T> for EguiPlugin<T> {
|
||||
if let Ok(payload) = self.create_window_channel.1.try_recv() {
|
||||
let res = create_gl_window(
|
||||
event_loop,
|
||||
&context.webview_id_map,
|
||||
&self.context.webview_id_map,
|
||||
&self.context.main_thread.windows,
|
||||
payload.label,
|
||||
payload.app_creator,
|
||||
@@ -201,7 +212,7 @@ impl<T: UserEvent> Plugin<T> for EguiPlugin<T> {
|
||||
payload.tx.send(res).unwrap();
|
||||
}
|
||||
handle_gl_loop(
|
||||
&self.context.main_thread.windows,
|
||||
&self.context,
|
||||
event,
|
||||
event_loop,
|
||||
control_flow,
|
||||
@@ -359,7 +370,7 @@ pub fn create_gl_window<T: UserEvent>(
|
||||
|
||||
let system_theme = native_options.system_theme();
|
||||
let mut integration = epi_integration::EpiIntegration::new(
|
||||
&event_loop,
|
||||
event_loop,
|
||||
painter.max_texture_side(),
|
||||
gl_window.window(),
|
||||
system_theme,
|
||||
@@ -429,7 +440,7 @@ fn win_mac_gl_loop<T: UserEvent>(
|
||||
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
|
||||
|
||||
egui_glow::painter::clear(
|
||||
&gl,
|
||||
gl,
|
||||
screen_size_in_pixels,
|
||||
app.clear_color(&integration.egui_ctx.style().visuals),
|
||||
);
|
||||
@@ -491,16 +502,14 @@ fn win_mac_gl_loop<T: UserEvent>(
|
||||
};
|
||||
|
||||
match event {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
Event::RedrawEventsCleared => paint(),
|
||||
#[cfg(target_os = "macos")]
|
||||
Event::RedrawRequested(_) => paint(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_gl_loop<T: UserEvent>(
|
||||
windows: &Arc<Mutex<HashMap<WebviewId, WindowWrapper>>>,
|
||||
egui_context: &Context<T>,
|
||||
event: &Event<'_, Message<T>>,
|
||||
_event_loop: &EventLoopWindowTarget<Message<T>>,
|
||||
control_flow: &mut ControlFlow,
|
||||
@@ -509,11 +518,12 @@ pub fn handle_gl_loop<T: UserEvent>(
|
||||
is_focused: &mut bool,
|
||||
) -> bool {
|
||||
let mut prevent_default = false;
|
||||
let EventLoopIterationContext {
|
||||
callback,
|
||||
let Context {
|
||||
main_thread: MainThreadContext { windows, .. },
|
||||
webview_id_map,
|
||||
..
|
||||
} = context;
|
||||
} = egui_context;
|
||||
let EventLoopIterationContext { callback, .. } = context;
|
||||
let has_egui_window = !windows.lock().unwrap().is_empty();
|
||||
if has_egui_window {
|
||||
let mut windows_lock = windows.lock().unwrap();
|
||||
@@ -525,7 +535,7 @@ pub fn handle_gl_loop<T: UserEvent>(
|
||||
for win in iter {
|
||||
let mut should_close = false;
|
||||
if let Some(glutin_window_context) = &mut win.inner {
|
||||
should_close = win_mac_gl_loop(control_flow, glutin_window_context, &event, *is_focused);
|
||||
should_close = win_mac_gl_loop(control_flow, glutin_window_context, event, *is_focused);
|
||||
}
|
||||
|
||||
if should_close {
|
||||
@@ -537,57 +547,55 @@ pub fn handle_gl_loop<T: UserEvent>(
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => {
|
||||
let window_id = webview_id_map.get(window_id);
|
||||
if let Some(window_id) = webview_id_map.get(window_id) {
|
||||
if let TaoWindowEvent::Destroyed = event {
|
||||
windows_lock.remove(&window_id);
|
||||
}
|
||||
|
||||
if let TaoWindowEvent::Destroyed = event {
|
||||
windows_lock.remove(&window_id);
|
||||
}
|
||||
if let Some(window) = windows_lock.get_mut(&window_id) {
|
||||
let label = &window.label;
|
||||
let glutin_window_context = &mut window.inner;
|
||||
let window_event_listeners = &window.window_event_listeners;
|
||||
let handled = match event {
|
||||
TaoWindowEvent::Focused(new_focused) => {
|
||||
*is_focused = *new_focused;
|
||||
false
|
||||
}
|
||||
TaoWindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if physical_size.width > 0 && physical_size.height > 0 {
|
||||
if let Some(glutin_window_context) = glutin_window_context.as_ref() {
|
||||
glutin_window_context.context.resize(*physical_size);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
TaoWindowEvent::CloseRequested => on_close_requested(
|
||||
callback,
|
||||
(label, glutin_window_context),
|
||||
window_event_listeners,
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if let Some(window) = windows_lock.get_mut(&window_id) {
|
||||
let label = &window.label;
|
||||
let glutin_window_context = &mut window.inner;
|
||||
let window_event_listeners = &window.window_event_listeners;
|
||||
let handled = match event {
|
||||
TaoWindowEvent::Focused(new_focused) => {
|
||||
*is_focused = *new_focused;
|
||||
false
|
||||
}
|
||||
TaoWindowEvent::Resized(physical_size) => {
|
||||
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
|
||||
// See: https://github.com/rust-windowing/winit/issues/208
|
||||
// This solves an issue where the app would panic when minimizing on Windows.
|
||||
if physical_size.width > 0 && physical_size.height > 0 {
|
||||
if let Some(glutin_window_context) = glutin_window_context.as_ref() {
|
||||
glutin_window_context.context.resize(*physical_size);
|
||||
if let Some(glutin_window_context) = glutin_window_context.as_mut() {
|
||||
let gl_window = &glutin_window_context.context;
|
||||
let app = &mut glutin_window_context.app;
|
||||
if !handled {
|
||||
let mut integration = glutin_window_context.integration.borrow_mut();
|
||||
integration.on_event(app.as_mut(), event);
|
||||
if integration.should_close() {
|
||||
should_close = true;
|
||||
*control_flow = ControlFlow::Wait;
|
||||
}
|
||||
}
|
||||
false
|
||||
gl_window.window().request_redraw();
|
||||
}
|
||||
TaoWindowEvent::CloseRequested => on_close_requested(
|
||||
callback,
|
||||
(label, glutin_window_context),
|
||||
window_event_listeners,
|
||||
),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if let Some(glutin_window_context) = glutin_window_context.as_mut() {
|
||||
let gl_window = &glutin_window_context.context;
|
||||
let app = &mut glutin_window_context.app;
|
||||
if !handled {
|
||||
let mut integration = glutin_window_context.integration.borrow_mut();
|
||||
integration.on_event(app.as_mut(), event);
|
||||
if integration.should_close() {
|
||||
should_close = true;
|
||||
*control_flow = ControlFlow::Wait;
|
||||
}
|
||||
}
|
||||
gl_window.window().request_redraw();
|
||||
}
|
||||
if should_close {
|
||||
on_window_close(glutin_window_context);
|
||||
} else {
|
||||
if let Some(window) = windows_lock.get(&window_id) {
|
||||
if should_close {
|
||||
on_window_close(glutin_window_context);
|
||||
} else if let Some(window) = windows_lock.get(&window_id) {
|
||||
if let Some(event) = WindowEventWrapper::from(event).0 {
|
||||
let label = window.label.clone();
|
||||
let window_event_listeners = window.window_event_listeners.clone();
|
||||
@@ -603,9 +611,9 @@ pub fn handle_gl_loop<T: UserEvent>(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevent_default = true;
|
||||
prevent_default = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,11 +681,8 @@ pub(crate) fn handle_user_message<T: UserEvent>(
|
||||
.send(RawWindowHandle(window.raw_window_handle()))
|
||||
.unwrap(),
|
||||
WindowMessage::Theme(tx) => {
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
tx.send(tauri_runtime_wry::map_theme(&window.theme()))
|
||||
.unwrap();
|
||||
#[cfg(not(windows))]
|
||||
tx.send(Theme::Light).unwrap();
|
||||
}
|
||||
// Setters
|
||||
WindowMessage::Center => {
|
||||
|
||||
Reference in New Issue
Block a user