Merge pull request #1 from tauri-apps/feat/egui-tao-0.20

This commit is contained in:
Lucas Fernandes Nogueira
2022-09-16 19:50:17 -03:00
committed by GitHub
57 changed files with 7782 additions and 7474 deletions

75
.changes/config.json Executable file
View 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"
}
}
}

View File

@@ -0,0 +1,5 @@
---
"tauri-egui": minor
---
Initial release.

16
.changes/readme.md Executable file
View 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
View 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
View 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'

View 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
View 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
View 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
View 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

View File

@@ -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",
]
]

View File

@@ -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,
)

View File

@@ -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"] }

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
});
});
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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))),
);
}

View File

@@ -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())
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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/");
}

View File

@@ -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),
);
});
}
}

View File

@@ -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
}

View File

@@ -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");
}
}

View File

@@ -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!());
});
}
}

View 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();
}
});
}

View File

@@ -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!());
});
}
}

View 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("").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("").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,
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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();
}
}
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
});
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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!());
});
}
}

View 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!());
});
});
});
}
}

View 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
}

View File

@@ -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());
},
);
}

View File

@@ -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`].
}
}
});
}
}

View File

@@ -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!())
}

View 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);
});
})
}
}

View File

@@ -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!());
});
}
}

View 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());
},
);
}

View File

@@ -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;
}
}
// ----------------------------------------------------------------------------

View File

@@ -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,
}
}

View File

@@ -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"
),
]
);
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
)
}

View File

@@ -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,

View File

@@ -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 => {