mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 13:50:04 -04:00
Merge branch 'main' into lucas/text-edit-min-size
# Conflicts: # tests/egui_tests/tests/snapshots/visuals/drag_value.png
This commit is contained in:
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@@ -3,11 +3,12 @@ Please read the "Making a PR" section of [`CONTRIBUTING.md`](https://github.com/
|
||||
|
||||
* Keep your PR:s small and focused.
|
||||
* The PR title is what ends up in the changelog, so make it descriptive!
|
||||
* Keep PR description short and to the point! No long LLM slop.
|
||||
* If applicable, add a screenshot or gif.
|
||||
* If it is a non-trivial addition, consider adding a demo for it to `egui_demo_lib`, or a new example.
|
||||
* Do NOT open PR:s from your `master` branch, as that makes it hard for maintainers to test and add commits to your PR.
|
||||
* Remember to run `cargo fmt` and `cargo clippy`.
|
||||
* Open the PR as a draft until you have self-reviewed it and run `./scripts/check.sh`.
|
||||
* Open the PR as a draft until a human has reviewed it
|
||||
* When you have addressed a PR comment, mark it as resolved.
|
||||
|
||||
Please be patient! I will review your PR, but my time is limited!
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.DS_Store
|
||||
# `target_ra` and `target_wasm` are legacy locations; they now live inside `target/`.
|
||||
# Keep them on their own lines: a trailing `#` is not a comment in .gitignore.
|
||||
**/target
|
||||
**/target_ra
|
||||
**/target_wasm
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -8,7 +8,7 @@
|
||||
}
|
||||
},
|
||||
"files.exclude": {
|
||||
"target_ra/**": true,
|
||||
"target_ra/**": true, // legacy Rust Analyzer target dir
|
||||
"target_wasm/**": true,
|
||||
"target/**": true,
|
||||
},
|
||||
@@ -18,7 +18,7 @@
|
||||
"rust-analyzer.cargo.allTargets": true,
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
// Use a separate target directory for Rust Analyzer so it doesn't prevent cargo/clippy from doing things.
|
||||
"rust-analyzer.cargo.targetDir": "target_ra",
|
||||
"rust-analyzer.cargo.targetDir": true,
|
||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
||||
// Uncomment the following options and restart rust-analyzer to get it to check code behind `cfg(target_arch=wasm32)`.
|
||||
// Don't forget to put it in a comment again before committing.
|
||||
|
||||
@@ -48,6 +48,18 @@ Don't worry about having many small commits in the PR - they will be squashed to
|
||||
|
||||
Please keep pull requests small and focused. The smaller it is, the more likely it is to get merged.
|
||||
|
||||
### Agents & LLM policy
|
||||
We care about maintaining high code quality, no matter who or what wrote the code.
|
||||
|
||||
You (human!) are always responsible for the quality of code, issues, and PR descriptions.
|
||||
|
||||
A human should always review the code before undrafting the PR.
|
||||
|
||||
Make sure the PR description is short and to the point.
|
||||
|
||||
#### Undue burden
|
||||
We reserve the right to close any PRs that cause an undue burden on the maintainership. This could include, but is not limited to, massive PRs, LLM slop, or contributions not in good faith.
|
||||
|
||||
## Working with git lfs
|
||||
|
||||
We use [git-lfs](https://git-lfs.com/) to store big files in the repository.
|
||||
|
||||
@@ -4479,6 +4479,14 @@ dependencies = [
|
||||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "styling_engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
||||
@@ -179,7 +179,11 @@ all = "warn"
|
||||
|
||||
# See also clippy.toml
|
||||
[workspace.lints.clippy]
|
||||
# `all` = `correctness` + `suspicious` + `style` + `complexity` + `perf`.
|
||||
# The remaining groups are `nursery` and `restriction`,
|
||||
# which are not meant to be enabled wholesale - we cherry-pick from them below.
|
||||
all = { level = "warn", priority = -1 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
allow_attributes = "warn"
|
||||
@@ -216,7 +220,6 @@ missing_assert_message = "warn"
|
||||
mutex_integer = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_type_cast = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
or_fun_call = "warn"
|
||||
@@ -258,7 +261,6 @@ unwrap_used = "warn"
|
||||
use_self = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
|
||||
|
||||
# Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`:
|
||||
@@ -286,10 +288,11 @@ trivially_copy_pass_by_ref = "allow" # 119 hits
|
||||
unreadable_literal = "allow" # 513 hits
|
||||
used_underscore_binding = "allow" # 25 hits
|
||||
|
||||
# These are meh:
|
||||
# Other:
|
||||
assigning_clones = "allow" # No please
|
||||
manual_range_contains = "allow" # this one is just worse imho
|
||||
map_unwrap_or = "allow" # so is this one
|
||||
multiple_crate_versions = "allow" # we handle this with `cargo deny`
|
||||
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
||||
|
||||
# NOTE: these are in `restriction`/`nursery`, so the `allow` is a no-op today.
|
||||
|
||||
@@ -66,7 +66,7 @@ ui.image(egui::include_image!("ferris.png"));
|
||||
|
||||
## Quick start
|
||||
|
||||
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
||||
There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to <https://github.com/emilk/eframe_template/> and follow the instructions. The official docs are at <https://docs.rs/egui>. For inspiration and more examples, check out [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code.
|
||||
|
||||
If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
||||
use crate::{Rgba, fast_round, mul_frac_round};
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
@@ -131,35 +131,10 @@ impl Color32 {
|
||||
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
||||
#[inline]
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
use std::sync::OnceLock;
|
||||
match a {
|
||||
// common-case optimization:
|
||||
0 => Self::TRANSPARENT,
|
||||
|
||||
// common-case optimization:
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
|
||||
let lut = LOOKUP_TABLE.get_or_init(|| {
|
||||
(0..=u16::MAX)
|
||||
.map(|i| {
|
||||
let [value, alpha] = i.to_ne_bytes();
|
||||
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
let [r, g, b] =
|
||||
[r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
Self::from_rgba_unmultiplied_const(r, g, b, a)
|
||||
}
|
||||
|
||||
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
|
||||
///
|
||||
/// It is slightly slower when operating on non-const data.
|
||||
/// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
|
||||
#[inline]
|
||||
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
match a {
|
||||
@@ -170,9 +145,9 @@ impl Color32 {
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
|
||||
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
|
||||
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
|
||||
let r = mul_frac_round(r, a);
|
||||
let g = mul_frac_round(g, a);
|
||||
let b = mul_frac_round(b, a);
|
||||
Self::from_rgba_premultiplied(r, g, b, a)
|
||||
}
|
||||
}
|
||||
@@ -535,4 +510,14 @@ mod test {
|
||||
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_frac_round_vs_old() {
|
||||
for x in (0..=255u8).step_by(4) {
|
||||
for a in (1..=255u8).step_by(4) {
|
||||
let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
|
||||
assert_eq!(old, mul_frac_round(x, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
/// Compute val * (frac/255) with no floating point or divisions.
|
||||
#[inline]
|
||||
const fn mul_frac_round(val: u8, frac: u8) -> u8 {
|
||||
// Treat this as a simple fixed point calculation
|
||||
let p = (val as u16) * (frac as u16) + 128;
|
||||
((p + (p >> 8)) >> 8) as u8
|
||||
// Logic split out a bit more.
|
||||
//let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
|
||||
//return ((p + 1 + (p >> 8)) >> 8) as u8;
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_srgba_conversion() {
|
||||
for b in 0..=255 {
|
||||
|
||||
@@ -53,6 +53,12 @@ android-native-activity = ["egui-winit/android-native-activity"]
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
|
||||
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
|
||||
## Enabling this won't break semver, it's just future compatibility risk.
|
||||
##
|
||||
## Currently, this enables the theme plugin.
|
||||
experimental = ["egui/experimental"]
|
||||
|
||||
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
|
||||
##
|
||||
## There is generally no need to enable both the `wgpu` and `glow` features,
|
||||
|
||||
@@ -1094,9 +1094,10 @@ impl GlutinWindowContext {
|
||||
//
|
||||
// The justification for FallbackEgl over PreferEgl is at https://github.com/emilk/egui/pull/2526#issuecomment-1400229576 .
|
||||
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
|
||||
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
|
||||
egui_ctx,
|
||||
viewport_builder.clone(),
|
||||
.with_window_attributes(Some(egui_winit::apply_monitor_to_window_attributes(
|
||||
egui_winit::create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
|
||||
&viewport_builder,
|
||||
event_loop,
|
||||
)));
|
||||
|
||||
let (window, gl_config) = {
|
||||
@@ -1262,17 +1263,40 @@ impl GlutinWindowContext {
|
||||
window
|
||||
} else {
|
||||
log::debug!("Creating a window for viewport {viewport_id:?}");
|
||||
let window_attributes = egui_winit::create_winit_window_attributes(
|
||||
&self.egui_ctx,
|
||||
viewport.builder.clone(),
|
||||
let window_attributes = egui_winit::apply_monitor_to_window_attributes(
|
||||
egui_winit::create_winit_window_attributes(
|
||||
&self.egui_ctx,
|
||||
viewport.builder.clone(),
|
||||
),
|
||||
&viewport.builder,
|
||||
event_loop,
|
||||
);
|
||||
if window_attributes.transparent()
|
||||
&& self.gl_config.supports_transparency() == Some(false)
|
||||
&& !cfg!(target_os = "windows")
|
||||
{
|
||||
log::error!("Cannot create transparent window: the GL config does not support it");
|
||||
}
|
||||
let window =
|
||||
glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?;
|
||||
|
||||
let window = cfg_select! {
|
||||
target_os = "windows" => {
|
||||
if viewport_id != ViewportId::ROOT && window_attributes.transparent() {
|
||||
// Preserve explicitly requested transparent child viewports on Windows.
|
||||
// Some GL paths report no transparency support although composition works.
|
||||
event_loop.create_window(window_attributes)?
|
||||
} else {
|
||||
glutin_winit::finalize_window(
|
||||
event_loop,
|
||||
window_attributes,
|
||||
&self.gl_config,
|
||||
)?
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Keep the normal platform-specific finalization path elsewhere.
|
||||
glutin_winit::finalize_window(event_loop, window_attributes, &self.gl_config)?
|
||||
}
|
||||
};
|
||||
egui_winit::apply_viewport_builder_to_window(
|
||||
&self.egui_ctx,
|
||||
&window,
|
||||
@@ -1497,9 +1521,17 @@ fn initialize_or_update_viewport(
|
||||
.and_then(|vp| vp.builder.icon.clone());
|
||||
}
|
||||
|
||||
let root_transparent = viewports
|
||||
.get(&ViewportId::ROOT)
|
||||
.and_then(|viewport| viewport.builder.transparent);
|
||||
|
||||
match viewports.entry(ids.this) {
|
||||
Entry::Vacant(entry) => {
|
||||
// New viewport:
|
||||
if ids.this != ViewportId::ROOT && builder.transparent.is_none() {
|
||||
// Child viewports inherit the root setting unless they explicitly override it.
|
||||
builder.transparent = root_transparent;
|
||||
}
|
||||
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
|
||||
entry.insert(Viewport {
|
||||
ids,
|
||||
|
||||
@@ -98,24 +98,64 @@ fn vs_main(
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||
|
||||
|
||||
/// Set in bit 0 of `r_tex_flags` if the sampler uses nearest filtering.
|
||||
///
|
||||
/// Must match `TEX_FLAG_NEAREST` in `renderer.rs`.
|
||||
const TEX_FLAG_NEAREST: u32 = 1u;
|
||||
|
||||
/// Wrap modes, stored in bits 1+ of `r_tex_flags`.
|
||||
///
|
||||
/// Must match the `WRAP_MODE_*` constants in `renderer.rs`.
|
||||
const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0u;
|
||||
const WRAP_MODE_REPEAT: u32 = 1u;
|
||||
const WRAP_MODE_MIRRORED_REPEAT: u32 = 2u;
|
||||
|
||||
/// Texture flags, only read when `predictable_texture_filtering` is on.
|
||||
///
|
||||
/// Bit 0: `TEX_FLAG_NEAREST`.
|
||||
/// Bits 1+: one of the `WRAP_MODE_*` constants.
|
||||
@group(1) @binding(2) var<uniform> r_tex_flags: vec4<u32>;
|
||||
|
||||
/// Map a texel coordinate to a valid texel according to the texture's wrap mode.
|
||||
fn wrap_texel_coord(coord: vec2<i32>, texture_size: vec2<i32>) -> vec2<i32> {
|
||||
let wrap_mode = r_tex_flags[0] >> 1u;
|
||||
if wrap_mode == WRAP_MODE_REPEAT {
|
||||
return ((coord % texture_size) + texture_size) % texture_size;
|
||||
} else if wrap_mode == WRAP_MODE_MIRRORED_REPEAT {
|
||||
let period = 2 * texture_size;
|
||||
let phase = ((coord % period) + period) % period;
|
||||
return min(phase, period - vec2<i32>(1, 1) - phase);
|
||||
} else {
|
||||
// WRAP_MODE_CLAMP_TO_EDGE
|
||||
return clamp(coord, vec2<i32>(0, 0), texture_size - vec2<i32>(1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
||||
if r_locals.predictable_texture_filtering == 0 {
|
||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
} else {
|
||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
||||
let texture_size_f = vec2<f32>(texture_size);
|
||||
|
||||
if (r_tex_flags[0] & TEX_FLAG_NEAREST) != 0u {
|
||||
// Nearest filtering: load the texel under the sample position.
|
||||
let texel = wrap_texel_coord(vec2<i32>(floor(in.tex_coord * texture_size_f)), texture_size);
|
||||
return textureLoad(r_tex_color, texel, 0);
|
||||
}
|
||||
|
||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
||||
let pixel_fract = fract(pixel_coord);
|
||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||
|
||||
// Manual texture clamping
|
||||
let max_coord = texture_size - vec2<i32>(1, 1);
|
||||
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
||||
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
|
||||
let p00 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 0), texture_size);
|
||||
let p10 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 0), texture_size);
|
||||
let p01 = wrap_texel_coord(pixel_floor + vec2<i32>(0, 1), texture_size);
|
||||
let p11 = wrap_texel_coord(pixel_floor + vec2<i32>(1, 1), texture_size);
|
||||
|
||||
// Load at pixel centers
|
||||
let tl = textureLoad(r_tex_color, p00, 0);
|
||||
|
||||
@@ -245,6 +245,11 @@ pub struct Renderer {
|
||||
uniform_bind_group: wgpu::BindGroup,
|
||||
texture_bind_group_layout: wgpu::BindGroupLayout,
|
||||
|
||||
/// Uniform buffers each holding a single `u32` of texture flags
|
||||
/// (see [`texture_flags`]), indexed by that flag value.
|
||||
/// Read by the shader when `predictable_texture_filtering` is on.
|
||||
texture_flag_buffers: [wgpu::Buffer; NUM_TEXTURE_FLAGS],
|
||||
|
||||
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
|
||||
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
|
||||
/// sampler.
|
||||
@@ -347,10 +352,31 @@ impl Renderer {
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: NonZeroU64::new(
|
||||
(core::mem::size_of::<u32>() * 4) as _,
|
||||
),
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
};
|
||||
|
||||
let texture_flag_buffers = core::array::from_fn::<_, NUM_TEXTURE_FLAGS, _>(|flag| {
|
||||
let flag = flag as u32;
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some(&format!("egui_texture_flags_{flag}")),
|
||||
contents: bytemuck::bytes_of(&[flag, 0, 0, 0]),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
})
|
||||
});
|
||||
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("egui_pipeline_layout"),
|
||||
bind_group_layouts: &[
|
||||
@@ -458,6 +484,7 @@ impl Renderer {
|
||||
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
||||
uniform_bind_group,
|
||||
texture_bind_group_layout,
|
||||
texture_flag_buffers,
|
||||
textures: HashMap::default(),
|
||||
next_user_texture_id: 0,
|
||||
samplers: HashMap::default(),
|
||||
@@ -709,6 +736,9 @@ impl Renderer {
|
||||
};
|
||||
|
||||
let bind_group = bind_group.unwrap_or_else(|| {
|
||||
let nearest =
|
||||
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
|
||||
let wrap_mode = wrap_mode_flag(image_delta.options.wrap_mode);
|
||||
let sampler = self
|
||||
.samplers
|
||||
.entry(image_delta.options)
|
||||
@@ -727,6 +757,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
|
||||
.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
@@ -829,6 +864,8 @@ impl Renderer {
|
||||
) -> epaint::TextureId {
|
||||
profiling::function_scope!();
|
||||
|
||||
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
|
||||
let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
@@ -846,6 +883,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
|
||||
.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -885,6 +927,8 @@ impl Renderer {
|
||||
.get_mut(&id)
|
||||
.expect("Tried to update a texture that has not been allocated yet.");
|
||||
|
||||
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
|
||||
let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
@@ -902,6 +946,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
|
||||
.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1080,6 +1129,49 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set in bit 0 of the texture flags if the sampler uses nearest filtering.
|
||||
///
|
||||
/// Must match `TEX_FLAG_NEAREST` in `egui.wgsl`.
|
||||
const TEX_FLAG_NEAREST: u32 = 1;
|
||||
|
||||
/// Wrap modes, stored in bits 1+ of the texture flags.
|
||||
///
|
||||
/// Must match the `WRAP_MODE_*` constants in `egui.wgsl`.
|
||||
const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0;
|
||||
const WRAP_MODE_REPEAT: u32 = 1;
|
||||
const WRAP_MODE_MIRRORED_REPEAT: u32 = 2;
|
||||
|
||||
/// Number of distinct values [`texture_flags`] can return.
|
||||
const NUM_TEXTURE_FLAGS: usize = 6;
|
||||
|
||||
fn wrap_mode_flag(wrap_mode: epaint::textures::TextureWrapMode) -> u32 {
|
||||
match wrap_mode {
|
||||
epaint::textures::TextureWrapMode::ClampToEdge => WRAP_MODE_CLAMP_TO_EDGE,
|
||||
epaint::textures::TextureWrapMode::Repeat => WRAP_MODE_REPEAT,
|
||||
epaint::textures::TextureWrapMode::MirroredRepeat => WRAP_MODE_MIRRORED_REPEAT,
|
||||
}
|
||||
}
|
||||
|
||||
fn address_mode_wrap_flag(address_mode: wgpu::AddressMode) -> u32 {
|
||||
match address_mode {
|
||||
wgpu::AddressMode::Repeat => WRAP_MODE_REPEAT,
|
||||
wgpu::AddressMode::MirrorRepeat => WRAP_MODE_MIRRORED_REPEAT,
|
||||
wgpu::AddressMode::ClampToEdge | wgpu::AddressMode::ClampToBorder => {
|
||||
WRAP_MODE_CLAMP_TO_EDGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Index into [`Renderer::texture_flag_buffers`]: the texture flags
|
||||
/// read by the shader when `predictable_texture_filtering` is on.
|
||||
///
|
||||
/// Bit 0: [`TEX_FLAG_NEAREST`].
|
||||
/// Bits 1+: one of the `WRAP_MODE_*` constants.
|
||||
fn texture_flags(nearest: bool, wrap_mode: u32) -> usize {
|
||||
let nearest = if nearest { TEX_FLAG_NEAREST } else { 0 };
|
||||
(nearest | (wrap_mode << 1)) as usize
|
||||
}
|
||||
|
||||
fn create_sampler(
|
||||
options: epaint::textures::TextureOptions,
|
||||
device: &wgpu::Device,
|
||||
|
||||
@@ -101,7 +101,10 @@ impl WgpuSetup {
|
||||
let mut backends = create_new.instance_descriptor.backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
// Emscripten is excluded: wgpu gates both its `webgpu` backend and
|
||||
// its `web_sys` re-export on `not(Emscripten)` (see the cfg aliases
|
||||
// in `wgpu/build.rs`).
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
|
||||
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
|
||||
let is_secure_context =
|
||||
wgpu::web_sys::window().is_some_and(|w| w.is_secure_context());
|
||||
|
||||
@@ -1867,7 +1867,9 @@ fn process_viewport_command(
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use winit::platform::windows::WindowExtWindows as _;
|
||||
window.set_undecorated_shadow(!v);
|
||||
|
||||
// don't request the undecorated-window drop shadow in fullscreen (#8399)
|
||||
window.set_undecorated_shadow(!v && window.fullscreen().is_none());
|
||||
}
|
||||
}
|
||||
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
|
||||
@@ -1971,12 +1973,33 @@ pub fn create_window(
|
||||
) -> Result<Window, winit::error::OsError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
|
||||
let window_attributes = apply_monitor_to_window_attributes(
|
||||
create_winit_window_attributes(egui_ctx, viewport_builder.clone()),
|
||||
viewport_builder,
|
||||
event_loop,
|
||||
);
|
||||
|
||||
// Resolve target monitor index → MonitorHandle, so the window is created
|
||||
// directly in borderless fullscreen on the requested output. This is the
|
||||
// only reliable way to target a specific monitor under Wayland, and also
|
||||
// avoids the Mutter race where OuterPosition is ignored pre-mapping.
|
||||
let window = event_loop.create_window(window_attributes)?;
|
||||
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
/// Apply [`ViewportBuilder::with_monitor`] to freshly-built [`winit::window::WindowAttributes`].
|
||||
///
|
||||
/// Resolve the target monitor index → `MonitorHandle` and request borderless
|
||||
/// fullscreen on that output, so the window is created directly on the right
|
||||
/// monitor. This is the only reliable way to target a specific monitor under
|
||||
/// Wayland, and also avoids the Mutter race where `OuterPosition` is ignored
|
||||
/// pre-mapping.
|
||||
///
|
||||
/// Must be called by every backend that builds its own window from
|
||||
/// [`create_winit_window_attributes`] (the glow backend and per-viewport window
|
||||
/// creation do this) — otherwise `with_monitor` silently does nothing there.
|
||||
pub fn apply_monitor_to_window_attributes(
|
||||
mut window_attributes: winit::window::WindowAttributes,
|
||||
viewport_builder: &ViewportBuilder,
|
||||
event_loop: &ActiveEventLoop,
|
||||
) -> winit::window::WindowAttributes {
|
||||
if let Some(idx) = viewport_builder.monitor {
|
||||
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
|
||||
window_attributes = window_attributes
|
||||
@@ -1988,10 +2011,7 @@ pub fn create_window(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let window = event_loop.create_window(window_attributes)?;
|
||||
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
|
||||
Ok(window)
|
||||
window_attributes
|
||||
}
|
||||
|
||||
pub fn create_winit_window_attributes(
|
||||
@@ -2171,7 +2191,10 @@ pub fn create_winit_window_attributes(
|
||||
if let Some(show) = _taskbar {
|
||||
window_attributes = window_attributes.with_skip_taskbar(!show);
|
||||
}
|
||||
window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
|
||||
|
||||
// don't request the undecorated-window drop shadow in fullscreen (#8399)
|
||||
let want_undecorated_shadow = !decorations.unwrap_or(true) && !fullscreen.unwrap_or(false);
|
||||
window_attributes = window_attributes.with_undecorated_shadow(want_undecorated_shadow);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -45,6 +45,12 @@ color-hex = ["epaint/color-hex"]
|
||||
## If you plan on specifying your own fonts you may disable this feature.
|
||||
default_fonts = ["epaint/default_fonts"]
|
||||
|
||||
## Enable experimental egui features that might have massive breaking changes or be removed entirely in future updates.
|
||||
## Enabling this won't break semver, it's just future compatibility risk.
|
||||
##
|
||||
## Currently, this enables the theme plugin.
|
||||
experimental = []
|
||||
|
||||
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
|
||||
mint = ["epaint/mint"]
|
||||
|
||||
|
||||
@@ -36,9 +36,10 @@ use crate::{
|
||||
output::{FullOutput, LogicOutput},
|
||||
pass_state::PassState,
|
||||
plugin::{self, TypedPluginHandle},
|
||||
resize, response, scroll_area,
|
||||
resize, response, scroll_area, theme,
|
||||
util::IdTypeMap,
|
||||
viewport::ViewportClass,
|
||||
widget_style::{StyleArgs, WidgetStyle},
|
||||
};
|
||||
|
||||
use crate::IdMap;
|
||||
@@ -412,6 +413,8 @@ struct ContextImpl {
|
||||
is_accesskit_enabled: bool,
|
||||
|
||||
loaders: Arc<Loaders>,
|
||||
|
||||
themes: theme::Themes,
|
||||
}
|
||||
|
||||
impl ContextImpl {
|
||||
@@ -2093,6 +2096,48 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Experimental theming, gated behind the `experimental_theme` feature.
|
||||
impl Context {
|
||||
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget type.
|
||||
///
|
||||
/// A theme can only be added once for a specified widget.
|
||||
/// If a theme is already registered for this widget, this is a no-op (useful for `eframe::run_simple_native`).
|
||||
///
|
||||
/// If you want to add the theme anyway, use [`Self::replace_widget_theme`] instead.
|
||||
#[cfg(feature = "experimental")]
|
||||
pub fn add_widget_theme<S: WidgetStyle + 'static>(
|
||||
&self,
|
||||
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
|
||||
) {
|
||||
self.write(|ctx| ctx.themes.register::<S>(theme, false));
|
||||
}
|
||||
|
||||
/// Register a [`StyleProvider`](crate::theme::StyleProvider) for the specified widget.
|
||||
///
|
||||
/// Overwrite any theme already registered for the specified widget [`WidgetStyle`].
|
||||
/// This allow to live edit a theme.
|
||||
#[cfg(feature = "experimental")]
|
||||
pub fn replace_widget_theme<S: WidgetStyle + 'static>(
|
||||
&self,
|
||||
theme: impl theme::StyleProvider<S> + Send + Sync + 'static,
|
||||
) {
|
||||
self.write(|ctx| ctx.themes.register::<S>(theme, true));
|
||||
}
|
||||
|
||||
/// Compute the `WidgetStyle` using the registered theme.
|
||||
///
|
||||
/// The types you need to call this (e.g. `StyleArgs`) are only public
|
||||
/// with the `experimental_theme` feature.
|
||||
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
|
||||
pub fn get_widget_style<S: WidgetStyle + Clone + 'static>(
|
||||
&self,
|
||||
modifiers: &StyleArgs<'_>,
|
||||
) -> S {
|
||||
let theme = self.read(move |ctx| ctx.themes.get::<S>());
|
||||
theme.lock().style(modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Tell `egui` which fonts to use.
|
||||
///
|
||||
@@ -2407,6 +2452,65 @@ impl Context {
|
||||
TextureHandle::new(tex_mngr, tex_id)
|
||||
}
|
||||
|
||||
/// Load a texture, or update a previously cached one if the image changed.
|
||||
///
|
||||
/// This is like [`Self::load_texture`], but caches the texture by `id`,
|
||||
/// and only re-uploads the image when it changes.
|
||||
/// This makes it safe to call every frame,
|
||||
/// which is convenient for small, procedurally generated images.
|
||||
///
|
||||
/// The `id` must be globally unique for each cached image
|
||||
/// (e.g. derived from a widget [`Id`]),
|
||||
/// or the callers will fight over the same texture, re-uploading it every frame.
|
||||
///
|
||||
/// If this is not called for a full frame, the cache entry is evicted,
|
||||
/// dropping both the cached [`ImageData`] (the CPU-side pixels)
|
||||
/// and the cached [`TextureHandle`].
|
||||
/// Dropping the handle also frees the texture itself,
|
||||
/// unless you keep a clone of the handle alive.
|
||||
pub fn load_texture_cached(
|
||||
&self,
|
||||
name: impl Into<String>,
|
||||
id: Id,
|
||||
image: impl Into<ImageData>,
|
||||
options: TextureOptions,
|
||||
) -> TextureHandle {
|
||||
profiling::function_scope!();
|
||||
|
||||
use crate::cache::FramePublisher;
|
||||
|
||||
type TextureCache = FramePublisher<Id, (ImageData, TextureOptions, TextureHandle)>;
|
||||
|
||||
let image = image.into();
|
||||
let cached: Option<(ImageData, TextureOptions, TextureHandle)> =
|
||||
self.memory_mut(|mem| mem.caches.cache::<TextureCache>().get(&id).cloned());
|
||||
|
||||
let (image, handle) = match cached {
|
||||
Some((cached_image, cached_options, handle))
|
||||
if cached_image == image && cached_options == options =>
|
||||
{
|
||||
(cached_image, handle)
|
||||
}
|
||||
Some((_, _, mut handle)) => {
|
||||
handle.set(image.clone(), options);
|
||||
(image, handle)
|
||||
}
|
||||
None => {
|
||||
let handle = self.load_texture(name, image.clone(), options);
|
||||
(image, handle)
|
||||
}
|
||||
};
|
||||
|
||||
// (Re-)publish to keep the entry from being evicted:
|
||||
self.memory_mut(|mem| {
|
||||
mem.caches
|
||||
.cache::<TextureCache>()
|
||||
.set(id, (image, options, handle.clone()));
|
||||
});
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
/// Low-level texture manager.
|
||||
///
|
||||
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].
|
||||
|
||||
@@ -417,13 +417,20 @@ pub mod response;
|
||||
mod sense;
|
||||
pub mod style;
|
||||
pub mod text_selection;
|
||||
#[cfg(feature = "experimental")]
|
||||
pub mod theme;
|
||||
#[cfg(not(feature = "experimental"))]
|
||||
mod theme;
|
||||
mod ui;
|
||||
mod ui_builder;
|
||||
mod ui_stack;
|
||||
pub mod util;
|
||||
pub mod viewport;
|
||||
mod widget_rect;
|
||||
#[cfg(feature = "experimental")]
|
||||
pub mod widget_style;
|
||||
#[cfg(not(feature = "experimental"))]
|
||||
mod widget_style;
|
||||
pub mod widget_text;
|
||||
pub mod widgets;
|
||||
|
||||
|
||||
@@ -1685,7 +1685,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(140)), // normal text color
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
@@ -1693,7 +1693,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(60), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(180)), // button text
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
@@ -1701,7 +1701,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
corner_radius: CornerRadius::same(3),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
@@ -1709,7 +1709,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
@@ -1717,7 +1717,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
}
|
||||
@@ -1730,7 +1730,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(248),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // separators, indentation lines
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
@@ -1738,7 +1738,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(230), // checkbox background
|
||||
bg_stroke: Default::default(),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
@@ -1746,7 +1746,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
corner_radius: CornerRadius::same(3),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
@@ -1754,7 +1754,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
@@ -1762,7 +1762,7 @@ impl Widgets {
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
corner_radius: CornerRadius::same(2),
|
||||
corner_radius: CornerRadius::same(4),
|
||||
expansion: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
156
crates/egui/src/theme/default_style.rs
Normal file
156
crates/egui/src/theme/default_style.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use emath::Vec2;
|
||||
use epaint::{Shadow, Stroke, text::TextWrapMode};
|
||||
|
||||
use crate::{
|
||||
Frame, TextStyle,
|
||||
theme::StyleProvider,
|
||||
widget_style::{
|
||||
BaseStyle, ButtonStyle, CheckboxStyle, HasClasses as _, LabelStyle, SELECTED_CLASS,
|
||||
SeparatorStyle, StyleArgs, TextVisuals, WidgetState,
|
||||
},
|
||||
};
|
||||
|
||||
/// The default [`StyleProvider`], implementing the default egui look based on
|
||||
/// [`crate::style::WidgetVisuals`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DefaultStyle;
|
||||
|
||||
impl StyleProvider<BaseStyle> for DefaultStyle {
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> BaseStyle {
|
||||
let StyleArgs { style, state, .. } = modifiers;
|
||||
let spacing = &style.spacing;
|
||||
let widget_visuals = match state {
|
||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||
WidgetState::Active => style.visuals.widgets.active,
|
||||
};
|
||||
|
||||
BaseStyle {
|
||||
frame: Frame {
|
||||
fill: widget_visuals.bg_fill,
|
||||
stroke: widget_visuals.bg_stroke,
|
||||
corner_radius: widget_visuals.corner_radius,
|
||||
inner_margin: spacing.button_padding.into(),
|
||||
..Default::default()
|
||||
},
|
||||
stroke: widget_visuals.fg_stroke,
|
||||
text: TextVisuals {
|
||||
color: widget_visuals.text_color(),
|
||||
font_id: modifiers
|
||||
.style
|
||||
.override_font_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| TextStyle::Body.resolve(style)),
|
||||
strikethrough: Stroke::NONE,
|
||||
underline: Stroke::NONE,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleProvider<ButtonStyle> for DefaultStyle {
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> ButtonStyle {
|
||||
let StyleArgs {
|
||||
ctx,
|
||||
classes,
|
||||
style,
|
||||
state,
|
||||
..
|
||||
} = modifiers;
|
||||
let spacing = &style.spacing;
|
||||
let mut widget_visuals = match state {
|
||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||
WidgetState::Active => style.visuals.widgets.active,
|
||||
};
|
||||
|
||||
let mut ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||
|
||||
if classes.has(SELECTED_CLASS) {
|
||||
let visuals = &style.visuals;
|
||||
widget_visuals.weak_bg_fill = visuals.selection.bg_fill;
|
||||
widget_visuals.bg_fill = visuals.selection.bg_fill;
|
||||
widget_visuals.fg_stroke = visuals.selection.stroke;
|
||||
ws.text.color = visuals.selection.stroke.color;
|
||||
}
|
||||
|
||||
ButtonStyle {
|
||||
frame: Frame {
|
||||
fill: widget_visuals.weak_bg_fill,
|
||||
stroke: widget_visuals.bg_stroke,
|
||||
corner_radius: widget_visuals.corner_radius,
|
||||
outer_margin: (-Vec2::splat(widget_visuals.expansion)).into(),
|
||||
inner_margin: (spacing.button_padding + Vec2::splat(widget_visuals.expansion)
|
||||
- Vec2::splat(widget_visuals.bg_stroke.width))
|
||||
.into(),
|
||||
..Default::default()
|
||||
},
|
||||
text_style: ws.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleProvider<CheckboxStyle> for DefaultStyle {
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> CheckboxStyle {
|
||||
let StyleArgs {
|
||||
ctx, style, state, ..
|
||||
} = modifiers;
|
||||
let spacing = &style.spacing;
|
||||
let widget_visuals = match state {
|
||||
WidgetState::Noninteractive => style.visuals.widgets.noninteractive,
|
||||
WidgetState::Inactive => style.visuals.widgets.inactive,
|
||||
WidgetState::Hovered => style.visuals.widgets.hovered,
|
||||
WidgetState::Active => style.visuals.widgets.active,
|
||||
};
|
||||
|
||||
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||
|
||||
CheckboxStyle {
|
||||
frame: Frame::new(),
|
||||
checkbox_size: spacing.icon_width,
|
||||
check_size: spacing.icon_width_inner,
|
||||
checkbox_frame: Frame {
|
||||
fill: widget_visuals.bg_fill,
|
||||
corner_radius: widget_visuals.corner_radius,
|
||||
stroke: widget_visuals.bg_stroke,
|
||||
..Default::default()
|
||||
},
|
||||
text_style: ws.text,
|
||||
check_stroke: ws.stroke,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleProvider<LabelStyle> for DefaultStyle {
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> LabelStyle {
|
||||
let StyleArgs { ctx, .. } = modifiers;
|
||||
let ws: BaseStyle = ctx.get_widget_style(modifiers);
|
||||
|
||||
LabelStyle {
|
||||
frame: Frame {
|
||||
fill: ws.frame.fill,
|
||||
inner_margin: 0.0.into(),
|
||||
outer_margin: 0.0.into(),
|
||||
stroke: Stroke::NONE,
|
||||
shadow: Shadow::NONE,
|
||||
corner_radius: 0.into(),
|
||||
},
|
||||
text: ws.text,
|
||||
wrap_mode: TextWrapMode::Wrap,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleProvider<SeparatorStyle> for DefaultStyle {
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> SeparatorStyle {
|
||||
let StyleArgs { style, .. } = modifiers;
|
||||
|
||||
SeparatorStyle {
|
||||
spacing: 6.0,
|
||||
// A separator is never interactive, so its stroke doesn't depend on the widget state:
|
||||
stroke: style.visuals.widgets.noninteractive.bg_stroke,
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/egui/src/theme/mod.rs
Normal file
48
crates/egui/src/theme/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Theming: pluggable [`StyleProvider`]s that compute the style of each widget.
|
||||
|
||||
// This module is only public with the `experimental_theme` feature,
|
||||
// so without it a lot of it looks unused:
|
||||
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
|
||||
|
||||
mod default_style;
|
||||
mod style_provider;
|
||||
mod themes;
|
||||
|
||||
pub use self::{default_style::DefaultStyle, style_provider::StyleProvider, themes::Themes};
|
||||
|
||||
use crate::{
|
||||
Ui,
|
||||
widget_style::{Classes, StyleArgs, WidgetState, WidgetStyle},
|
||||
};
|
||||
|
||||
impl Ui {
|
||||
/// The style of the widget with the given [`crate::Id`] and `Classes`,
|
||||
/// as computed by the registered theme.
|
||||
///
|
||||
/// The types you need to call this are only public with the
|
||||
/// `experimental_theme` feature.
|
||||
#[cfg_attr(not(feature = "experimental"), doc(hidden))]
|
||||
pub fn widget_style<S: WidgetStyle + Clone + 'static>(
|
||||
&self,
|
||||
id: crate::Id,
|
||||
classes: &Classes,
|
||||
) -> S {
|
||||
// Fetch the state of the widget, as it was in the previous pass
|
||||
let state = if let Some(response) = self.read_response(id) {
|
||||
response.widget_state()
|
||||
} else {
|
||||
// We don't know the state of the widget yet, so we would style it wrong.
|
||||
// It will be styled correctly on next frame.
|
||||
self.ctx().request_repaint();
|
||||
WidgetState::default()
|
||||
};
|
||||
|
||||
self.get_widget_style::<S>(&StyleArgs {
|
||||
classes,
|
||||
state,
|
||||
style: self.style(),
|
||||
stack: self.stack(),
|
||||
ctx: self,
|
||||
})
|
||||
}
|
||||
}
|
||||
17
crates/egui/src/theme/style_provider.rs
Normal file
17
crates/egui/src/theme/style_provider.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use core::any::TypeId;
|
||||
|
||||
use crate::widget_style::StyleArgs;
|
||||
|
||||
/// A Theme plugin that implement a style computation for a defined `WidgetStyle`
|
||||
pub trait StyleProvider<S> {
|
||||
/// The style according to the classes and state of the widget
|
||||
fn style(&mut self, modifiers: &StyleArgs<'_>) -> S;
|
||||
|
||||
/// Used to tell different themes apart
|
||||
fn type_id(&self) -> TypeId
|
||||
where
|
||||
Self: 'static,
|
||||
{
|
||||
TypeId::of::<Self>()
|
||||
}
|
||||
}
|
||||
96
crates/egui/src/theme/themes.rs
Normal file
96
crates/egui/src/theme/themes.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::Mutex;
|
||||
|
||||
use crate::{
|
||||
Id,
|
||||
theme::{StyleProvider, default_style::DefaultStyle},
|
||||
util::IdTypeMap,
|
||||
widget_style::{
|
||||
BaseStyle, ButtonStyle, CheckboxStyle, LabelStyle, SeparatorStyle, WidgetStyle,
|
||||
},
|
||||
};
|
||||
|
||||
/// The registry of [`StyleProvider`]s, one per [`WidgetStyle`] type.
|
||||
///
|
||||
/// Each widget asks this registry for the provider of its style type
|
||||
/// (e.g. [`ButtonStyle`]), and that provider computes the final style from the
|
||||
/// widget's classes and state.
|
||||
///
|
||||
/// A default provider is registered for every built-in style; register your
|
||||
/// own with [`Context::add_widget_theme`](crate::Context::add_widget_theme) or
|
||||
/// [`Context::replace_widget_theme`](crate::Context::replace_widget_theme).
|
||||
pub struct Themes {
|
||||
themes: IdTypeMap,
|
||||
}
|
||||
|
||||
type ThemeWrap<S> = Arc<Mutex<Box<dyn StyleProvider<S> + Send + Sync>>>;
|
||||
|
||||
impl Default for Themes {
|
||||
/// Register the default egui theme
|
||||
fn default() -> Self {
|
||||
let mut themes = IdTypeMap::default();
|
||||
|
||||
themes.insert_temp::<ThemeWrap<BaseStyle>>(
|
||||
Id::NULL,
|
||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||
);
|
||||
|
||||
themes.insert_temp::<ThemeWrap<ButtonStyle>>(
|
||||
Id::NULL,
|
||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||
);
|
||||
|
||||
themes.insert_temp::<ThemeWrap<SeparatorStyle>>(
|
||||
Id::NULL,
|
||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||
);
|
||||
|
||||
themes.insert_temp::<ThemeWrap<CheckboxStyle>>(
|
||||
Id::NULL,
|
||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||
);
|
||||
|
||||
themes.insert_temp::<ThemeWrap<LabelStyle>>(
|
||||
Id::NULL,
|
||||
Arc::new(Mutex::new(Box::new(DefaultStyle))),
|
||||
);
|
||||
|
||||
Self { themes }
|
||||
}
|
||||
}
|
||||
|
||||
impl Themes {
|
||||
/// Register a [`StyleProvider`] for the specified widget [`WidgetStyle`] `S`
|
||||
///
|
||||
/// Existing themes are overwritten if `force` is `true` or the new theme differs.
|
||||
pub(crate) fn register<S: WidgetStyle + 'static>(
|
||||
&mut self,
|
||||
theme: impl StyleProvider<S> + Send + Sync + 'static,
|
||||
force: bool,
|
||||
) {
|
||||
if !force
|
||||
&& self
|
||||
.themes
|
||||
.get_temp::<ThemeWrap<S>>(Id::NULL)
|
||||
.is_some_and(|t| t.lock().type_id() == theme.type_id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.themes
|
||||
.insert_temp::<ThemeWrap<S>>(Id::NULL, Arc::new(Mutex::new(Box::new(theme))));
|
||||
}
|
||||
|
||||
/// Fetch the style of the current theme
|
||||
pub fn get<S: WidgetStyle + 'static>(&self) -> ThemeWrap<S> {
|
||||
let v = self.themes.get_temp::<ThemeWrap<S>>(Id::NULL);
|
||||
|
||||
v.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"A style should be set for {:?}",
|
||||
core::any::type_name::<S>()
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use emath::Vec2;
|
||||
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
Frame, Response, Style, TextBuffer as _, TextStyle,
|
||||
style::{WidgetVisuals, Widgets},
|
||||
};
|
||||
|
||||
/// General text style
|
||||
pub struct TextVisuals {
|
||||
/// Font used
|
||||
pub font_id: FontId,
|
||||
|
||||
/// Font color
|
||||
pub color: Color32,
|
||||
|
||||
/// Text decoration
|
||||
pub underline: Stroke,
|
||||
pub strikethrough: Stroke,
|
||||
}
|
||||
|
||||
/// General widget style
|
||||
pub struct WidgetStyle {
|
||||
pub frame: Frame,
|
||||
|
||||
pub text: TextVisuals,
|
||||
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
/// Dedicated button style
|
||||
pub struct ButtonStyle {
|
||||
pub frame: Frame,
|
||||
pub text_style: TextVisuals,
|
||||
}
|
||||
|
||||
/// Dedicated checkbox style
|
||||
pub struct CheckboxStyle {
|
||||
/// Frame around
|
||||
pub frame: Frame,
|
||||
|
||||
/// Text next to it
|
||||
pub text_style: TextVisuals,
|
||||
|
||||
/// Checkbox size
|
||||
pub checkbox_size: f32,
|
||||
|
||||
/// Checkmark size
|
||||
pub check_size: f32,
|
||||
|
||||
/// Frame of the checkbox itself
|
||||
pub checkbox_frame: Frame,
|
||||
|
||||
/// Checkmark stroke
|
||||
pub check_stroke: Stroke,
|
||||
}
|
||||
|
||||
/// Dedicated label style
|
||||
pub struct LabelStyle {
|
||||
/// Frame around
|
||||
pub frame: Frame,
|
||||
|
||||
/// Text style
|
||||
pub text: TextVisuals,
|
||||
|
||||
/// Wrap mode used
|
||||
pub wrap_mode: TextWrapMode,
|
||||
}
|
||||
|
||||
/// Dedicated separator style
|
||||
pub struct SeparatorStyle {
|
||||
/// How much space is allocated in the layout direction
|
||||
pub spacing: f32,
|
||||
|
||||
/// How to paint it
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
/// The different state of a widget can be
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum WidgetState {
|
||||
Noninteractive,
|
||||
#[default]
|
||||
Inactive,
|
||||
Hovered,
|
||||
Active,
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
/// The widget visuals according to the state
|
||||
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
|
||||
match state {
|
||||
WidgetState::Noninteractive => &self.noninteractive,
|
||||
WidgetState::Inactive => &self.inactive,
|
||||
WidgetState::Hovered => &self.hovered,
|
||||
WidgetState::Active => &self.active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn widget_state(&self) -> WidgetState {
|
||||
if !self.sense.interactive() {
|
||||
WidgetState::Noninteractive
|
||||
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
|
||||
WidgetState::Active
|
||||
} else if self.hovered() || self.highlighted() {
|
||||
WidgetState::Hovered
|
||||
} else {
|
||||
WidgetState::Inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// The general widget style. The style is computed according to the classes and state of the widget.
|
||||
pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
|
||||
let visuals = self.visuals.widgets.state(state);
|
||||
let font_id = self.override_font_id.clone();
|
||||
WidgetStyle {
|
||||
frame: Frame {
|
||||
fill: visuals.bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
corner_radius: visuals.corner_radius,
|
||||
inner_margin: self.spacing.button_padding.into(),
|
||||
..Default::default()
|
||||
},
|
||||
stroke: visuals.fg_stroke,
|
||||
text: TextVisuals {
|
||||
color: self
|
||||
.visuals
|
||||
.override_text_color
|
||||
.unwrap_or_else(|| visuals.text_color()),
|
||||
font_id: font_id.unwrap_or_else(|| TextStyle::Body.resolve(self)),
|
||||
strikethrough: Stroke::NONE,
|
||||
underline: Stroke::NONE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The dedicated button style. The style is computed according to the classes and state of the widget.
|
||||
/// It depend on the general widget style.
|
||||
pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
|
||||
let mut visuals = *self.visuals.widgets.state(state);
|
||||
let mut ws = self.widget_style(classes, state);
|
||||
|
||||
if classes.has(SELECTED_CLASS) {
|
||||
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
|
||||
visuals.bg_fill = self.visuals.selection.bg_fill;
|
||||
visuals.fg_stroke = self.visuals.selection.stroke;
|
||||
ws.text.color = self.visuals.selection.stroke.color;
|
||||
}
|
||||
|
||||
ButtonStyle {
|
||||
frame: Frame {
|
||||
fill: visuals.weak_bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
corner_radius: visuals.corner_radius,
|
||||
outer_margin: (-Vec2::splat(visuals.expansion)).into(),
|
||||
inner_margin: (self.spacing.button_padding + Vec2::splat(visuals.expansion)
|
||||
- Vec2::splat(visuals.bg_stroke.width))
|
||||
.into(),
|
||||
..Default::default()
|
||||
},
|
||||
text_style: ws.text,
|
||||
}
|
||||
}
|
||||
|
||||
/// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
|
||||
/// It depend on the general widget style.
|
||||
pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
|
||||
let visuals = self.visuals.widgets.state(state);
|
||||
let ws = self.widget_style(classes, state);
|
||||
CheckboxStyle {
|
||||
frame: Frame::new(),
|
||||
checkbox_size: self.spacing.icon_width,
|
||||
check_size: self.spacing.icon_width_inner,
|
||||
checkbox_frame: Frame {
|
||||
fill: visuals.bg_fill,
|
||||
corner_radius: visuals.corner_radius,
|
||||
stroke: visuals.bg_stroke,
|
||||
..Default::default()
|
||||
},
|
||||
text_style: ws.text,
|
||||
check_stroke: ws.stroke,
|
||||
}
|
||||
}
|
||||
|
||||
/// The dedicated label style. The style is computed according to the classes and state of the widget.
|
||||
/// It depend on the general widget style.
|
||||
pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
|
||||
let ws = self.widget_style(classes, state);
|
||||
LabelStyle {
|
||||
frame: Frame {
|
||||
fill: ws.frame.fill,
|
||||
inner_margin: 0.0.into(),
|
||||
outer_margin: 0.0.into(),
|
||||
stroke: Stroke::NONE,
|
||||
shadow: Shadow::NONE,
|
||||
corner_radius: 0.into(),
|
||||
},
|
||||
text: ws.text,
|
||||
wrap_mode: TextWrapMode::Wrap,
|
||||
}
|
||||
}
|
||||
|
||||
/// The dedicated separator style. The style is computed according to the classes and state of the widget.
|
||||
/// It depend on the general widget style.
|
||||
pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
|
||||
let visuals = self.visuals.noninteractive();
|
||||
SeparatorStyle {
|
||||
spacing: 6.0,
|
||||
stroke: visuals.bg_stroke,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The root class is a special class present on every top-level [`crate::Ui`].
|
||||
pub const ROOT_CLASS: &str = "root";
|
||||
|
||||
/// The selected class is a special class present on selected [`crate::Button`].
|
||||
pub const SELECTED_CLASS: &str = "selected";
|
||||
|
||||
/// A class is a static string identifier.
|
||||
pub type ClassName = Cow<'static, str>;
|
||||
|
||||
/// Classes are string identifier that can be set on widget/Ui.
|
||||
///
|
||||
/// This can be used by styling engine to compute a different style
|
||||
/// based on the set of classes present on the widget/Ui.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Classes {
|
||||
classes: SmallVec<[ClassName; 5]>,
|
||||
}
|
||||
|
||||
impl Classes {
|
||||
/// Add a class to the list if the condition is true
|
||||
#[inline]
|
||||
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
|
||||
if condition {
|
||||
self.classes.push(class.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClasses for Classes {
|
||||
fn classes(&self) -> &Classes {
|
||||
self
|
||||
}
|
||||
|
||||
fn classes_mut(&mut self) -> &mut Classes {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Classes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.classes.iter().for_each(|class| {
|
||||
let _ = f.write_str(class.as_str());
|
||||
});
|
||||
f.write_str("")
|
||||
}
|
||||
}
|
||||
|
||||
/// Any widgets supporting [`Classes`] must implement this trait
|
||||
pub trait HasClasses {
|
||||
fn classes(&self) -> &Classes;
|
||||
|
||||
fn classes_mut(&mut self) -> &mut Classes;
|
||||
|
||||
/// Add the given class by consuming [`self`]
|
||||
#[inline]
|
||||
fn with_class(mut self, class: impl Into<ClassName>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class by consuming [`self`] if the condition is true
|
||||
#[inline]
|
||||
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), condition);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class in-place
|
||||
#[inline]
|
||||
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class in-place if the condition is true
|
||||
#[inline]
|
||||
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), condition);
|
||||
self
|
||||
}
|
||||
|
||||
/// True if the class is present
|
||||
fn has(&self, class: impl Into<ClassName>) -> bool {
|
||||
self.classes().classes.contains(&class.into())
|
||||
}
|
||||
}
|
||||
109
crates/egui/src/widget_style/classes.rs
Normal file
109
crates/egui/src/widget_style/classes.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::TextBuffer as _;
|
||||
|
||||
/// The root class is a special class present on every top-level [`crate::Ui`].
|
||||
pub const ROOT_CLASS: &str = "root";
|
||||
|
||||
/// The selected class is a special class present on selected [`crate::Button`].
|
||||
pub const SELECTED_CLASS: &str = "selected";
|
||||
|
||||
/// A class is a static string identifier.
|
||||
pub type ClassName = Cow<'static, str>;
|
||||
|
||||
/// Classes are string identifier that can be set on widget/Ui.
|
||||
///
|
||||
/// This can be used by styling engine to compute a different style
|
||||
/// based on the set of classes present on the widget/Ui.
|
||||
#[derive(Debug, Default, Clone, Hash)]
|
||||
pub struct Classes {
|
||||
classes: SmallVec<[ClassName; 5]>,
|
||||
}
|
||||
|
||||
impl Classes {
|
||||
/// Add a class to the list if the condition is true
|
||||
#[inline]
|
||||
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
|
||||
if condition {
|
||||
self.classes.push(class.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClasses for Classes {
|
||||
fn classes(&self) -> &Classes {
|
||||
self
|
||||
}
|
||||
|
||||
fn classes_mut(&mut self) -> &mut Classes {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Classes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.classes.iter().for_each(|class| {
|
||||
let _ = f.write_str(class.as_str());
|
||||
});
|
||||
f.write_str("")
|
||||
}
|
||||
}
|
||||
|
||||
/// Any widgets supporting [`Classes`] must implement this trait
|
||||
pub trait HasClasses {
|
||||
fn classes(&self) -> &Classes;
|
||||
|
||||
fn classes_mut(&mut self) -> &mut Classes;
|
||||
|
||||
/// Add the given class by consuming `self`
|
||||
#[inline]
|
||||
fn with_class(mut self, class: impl Into<ClassName>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class by consuming `self` if the condition is true
|
||||
#[inline]
|
||||
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), condition);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class in-place
|
||||
#[inline]
|
||||
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), true);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add the given class in-place if the condition is true
|
||||
#[inline]
|
||||
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.classes_mut().add_if(class.into(), condition);
|
||||
self
|
||||
}
|
||||
|
||||
/// True if the class is present
|
||||
fn has(&self, class: impl Into<ClassName>) -> bool {
|
||||
self.classes().classes.contains(&class.into())
|
||||
}
|
||||
|
||||
/// The list of class
|
||||
fn as_slice(&self) -> &[ClassName] {
|
||||
&self.classes().classes
|
||||
}
|
||||
}
|
||||
149
crates/egui/src/widget_style/mod.rs
Normal file
149
crates/egui/src/widget_style/mod.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// This module is only public with the `experimental_theme` feature,
|
||||
// so without it a lot of it looks unused:
|
||||
#![cfg_attr(not(feature = "experimental"), allow(dead_code, unused_imports))]
|
||||
|
||||
mod classes;
|
||||
|
||||
pub use self::classes::{ClassName, Classes, HasClasses, ROOT_CLASS, SELECTED_CLASS};
|
||||
|
||||
use core::fmt::Debug;
|
||||
|
||||
use epaint::{Color32, FontId, Stroke, text::TextWrapMode};
|
||||
|
||||
use crate::{
|
||||
Context, Frame, Response, Style, UiStack,
|
||||
style::{WidgetVisuals, Widgets},
|
||||
};
|
||||
|
||||
/// Each dedicated style must implement this trait to be used in the theme plugin system
|
||||
pub trait WidgetStyle: Debug + Clone + Send + Sync + core::any::Any + 'static {}
|
||||
|
||||
/// General text style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TextVisuals {
|
||||
/// Font used
|
||||
pub font_id: FontId,
|
||||
|
||||
/// Font color
|
||||
pub color: Color32,
|
||||
|
||||
/// Text decoration
|
||||
pub underline: Stroke,
|
||||
pub strikethrough: Stroke,
|
||||
}
|
||||
|
||||
/// General widget style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BaseStyle {
|
||||
pub frame: Frame,
|
||||
|
||||
pub text: TextVisuals,
|
||||
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl WidgetStyle for BaseStyle {}
|
||||
|
||||
/// Dedicated button style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ButtonStyle {
|
||||
pub frame: Frame,
|
||||
pub text_style: TextVisuals,
|
||||
}
|
||||
|
||||
impl WidgetStyle for ButtonStyle {}
|
||||
|
||||
/// Dedicated checkbox style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckboxStyle {
|
||||
/// Frame around
|
||||
pub frame: Frame,
|
||||
|
||||
/// Text next to it
|
||||
pub text_style: TextVisuals,
|
||||
|
||||
/// Checkbox size
|
||||
pub checkbox_size: f32,
|
||||
|
||||
/// Checkmark size
|
||||
pub check_size: f32,
|
||||
|
||||
/// Frame of the checkbox itself
|
||||
pub checkbox_frame: Frame,
|
||||
|
||||
/// Checkmark stroke
|
||||
pub check_stroke: Stroke,
|
||||
}
|
||||
|
||||
impl WidgetStyle for CheckboxStyle {}
|
||||
|
||||
/// Dedicated label style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LabelStyle {
|
||||
/// Frame around
|
||||
pub frame: Frame,
|
||||
|
||||
/// Text style
|
||||
pub text: TextVisuals,
|
||||
|
||||
/// Wrap mode used
|
||||
pub wrap_mode: TextWrapMode,
|
||||
}
|
||||
|
||||
impl WidgetStyle for LabelStyle {}
|
||||
|
||||
/// Dedicated separator style
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SeparatorStyle {
|
||||
/// How much space is allocated in the layout direction
|
||||
pub spacing: f32,
|
||||
|
||||
/// How to paint it
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl WidgetStyle for SeparatorStyle {}
|
||||
|
||||
/// The different state of a widget can be
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum WidgetState {
|
||||
Noninteractive,
|
||||
#[default]
|
||||
Inactive,
|
||||
Hovered,
|
||||
Active,
|
||||
}
|
||||
|
||||
impl Widgets {
|
||||
/// The widget visuals according to the state
|
||||
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
|
||||
match state {
|
||||
WidgetState::Noninteractive => &self.noninteractive,
|
||||
WidgetState::Inactive => &self.inactive,
|
||||
WidgetState::Hovered => &self.hovered,
|
||||
WidgetState::Active => &self.active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn widget_state(&self) -> WidgetState {
|
||||
if !self.sense.interactive() {
|
||||
WidgetState::Noninteractive
|
||||
} else if self.is_pointer_button_down_on() || self.has_focus() || self.clicked() {
|
||||
WidgetState::Active
|
||||
} else if self.hovered() || self.highlighted() {
|
||||
WidgetState::Hovered
|
||||
} else {
|
||||
WidgetState::Inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyleArgs<'a> {
|
||||
pub classes: &'a Classes,
|
||||
pub state: WidgetState,
|
||||
pub stack: &'a UiStack,
|
||||
pub style: &'a Style,
|
||||
pub ctx: &'a Context,
|
||||
}
|
||||
@@ -328,7 +328,7 @@ impl<'a> Button<'a> {
|
||||
|
||||
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
|
||||
|
||||
let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
|
||||
let ButtonStyle { frame, text_style } = ui.widget_style(id, &classes);
|
||||
|
||||
let mut button_padding = if has_frame_margin {
|
||||
frame.inner_margin
|
||||
|
||||
@@ -70,9 +70,6 @@ impl Widget for Checkbox<'_> {
|
||||
|
||||
// Get the widget style by reading the response from the previous pass
|
||||
let id = ui.next_auto_id();
|
||||
let response: Option<Response> = ui.ctx().read_response(id);
|
||||
let state = response.map(|r| r.widget_state()).unwrap_or_default();
|
||||
|
||||
let CheckboxStyle {
|
||||
check_size,
|
||||
checkbox_frame,
|
||||
@@ -80,7 +77,7 @@ impl Widget for Checkbox<'_> {
|
||||
frame,
|
||||
check_stroke,
|
||||
text_style,
|
||||
} = ui.style().checkbox_style(&classes, state);
|
||||
} = ui.widget_style(id, &classes);
|
||||
|
||||
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
|
||||
min_size.y = min_size.y.at_least(checkbox_size);
|
||||
|
||||
@@ -6,9 +6,11 @@ use crate::{
|
||||
WidgetInfo, WidgetType, epaint, lerp, remap_clamp,
|
||||
};
|
||||
use epaint::{
|
||||
Mesh, Rect, Shape, Stroke, StrokeKind, Vec2,
|
||||
ColorImage, Rect, RectShape, RoundedRect, Shape, Stroke, StrokeKind, Vec2,
|
||||
ecolor::{Color32, Hsva, HsvaGamma, Rgba},
|
||||
pos2, vec2,
|
||||
pos2,
|
||||
textures::TextureOptions,
|
||||
vec2,
|
||||
};
|
||||
|
||||
fn contrast_color(color: impl Into<Rgba>) -> Color32 {
|
||||
@@ -19,38 +21,63 @@ fn contrast_color(color: impl Into<Rgba>) -> Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of vertices per dimension in the color sliders.
|
||||
/// Resolution of the color slider gradients: the textures have `N + 1` texels per dimension.
|
||||
/// We need at least 6 for hues, and more for smooth 2D areas.
|
||||
/// Should always be a multiple of 6 to hit the peak hues in HSV/HSL (every 60°).
|
||||
const N: u32 = 6 * 6;
|
||||
|
||||
fn background_checkers(painter: &Painter, rect: Rect) {
|
||||
let rect = rect.shrink(0.5); // Small hack to avoid the checkers from peeking through the sides
|
||||
if !rect.is_positive() {
|
||||
fn background_checkers(painter: &Painter, bounds: RoundedRect) {
|
||||
// Shrink slightly, so the dark checkers don't peek through
|
||||
// the antialiased edge of the color painted on top:
|
||||
let pixel_width = 1.0 / painter.ctx().pixels_per_point();
|
||||
let (rect, corner_radius) = bounds.shrink(pixel_width).into_parts();
|
||||
|
||||
if !rect.is_positive() || !rect.is_finite() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dark_color = Color32::from_gray(32);
|
||||
let bright_color = Color32::from_gray(128);
|
||||
|
||||
let checker_size = Vec2::splat(rect.height() / 2.0);
|
||||
let n = (rect.width() / checker_size.x).round() as u32;
|
||||
// A single repeating 2x2 checker tile, stretched so that
|
||||
// each checker is half the rect height, like before:
|
||||
let image = ColorImage::new(
|
||||
[2, 2],
|
||||
vec![bright_color, dark_color, dark_color, bright_color],
|
||||
);
|
||||
let texture = painter.ctx().load_texture_cached(
|
||||
"color_picker_checkers",
|
||||
Id::new("color_picker_checkers"),
|
||||
image,
|
||||
TextureOptions::NEAREST_REPEAT,
|
||||
);
|
||||
|
||||
let mut mesh = Mesh::default();
|
||||
mesh.add_colored_rect(rect, dark_color);
|
||||
// One tile (one uv unit) covers 2x2 checkers, i.e. the full rect height:
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(rect.width() / rect.height(), 1.0));
|
||||
painter
|
||||
.add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
|
||||
}
|
||||
|
||||
let mut top = true;
|
||||
for i in 0..n {
|
||||
let x = lerp(rect.left()..=rect.right(), i as f32 / (n as f32));
|
||||
let small_rect = if top {
|
||||
Rect::from_min_size(pos2(x, rect.top()), checker_size)
|
||||
} else {
|
||||
Rect::from_min_size(pos2(x, rect.center().y), checker_size)
|
||||
};
|
||||
mesh.add_colored_rect(small_rect, bright_color);
|
||||
top = !top;
|
||||
}
|
||||
painter.add(Shape::mesh(mesh));
|
||||
/// Paint a gradient image over the given rounded rect, with an outline.
|
||||
fn paint_gradient(ui: &Ui, id: Id, bounds: RoundedRect, outline: Stroke, image: ColorImage) {
|
||||
let (rect, corner_radius) = bounds.into_parts();
|
||||
let [width, height] = image.size;
|
||||
let texture = ui
|
||||
.ctx()
|
||||
.load_texture_cached("color_gradient", id, image, TextureOptions::LINEAR);
|
||||
|
||||
// Inset the uv by half a texel, so the edges sample the exact edge colors:
|
||||
let inset = vec2(0.5 / width as f32, 0.5 / height as f32);
|
||||
let uv = Rect::from_min_max(inset.to_pos2(), pos2(1.0 - inset.x, 1.0 - inset.y));
|
||||
|
||||
ui.painter()
|
||||
.add(RectShape::filled(rect, corner_radius, Color32::WHITE).with_texture(texture.id(), uv));
|
||||
|
||||
// The outline must be a separate shape:
|
||||
// a textured `RectShape` cannot have a stroke,
|
||||
// since the stroke vertices would sample the same texture.
|
||||
ui.painter()
|
||||
.rect_stroke(rect, corner_radius, outline, StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Show a color with background checkers to demonstrate transparency (if any).
|
||||
@@ -72,7 +99,7 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
} else {
|
||||
// Transparent: how both the transparent and opaque versions of the color
|
||||
background_checkers(painter, rect);
|
||||
background_checkers(painter, rect.into());
|
||||
|
||||
if color == Color32::TRANSPARENT {
|
||||
// There is no opaque version, so just show the background checkers
|
||||
@@ -89,21 +116,35 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) {
|
||||
fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response {
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover());
|
||||
if ui.is_rect_visible(rect) {
|
||||
show_srgba_unmultiplied_at(ui.painter(), srgba, rect);
|
||||
let corner_radius = ui.visuals().widgets.noninteractive.corner_radius;
|
||||
show_srgba_unmultiplied_at(ui.painter(), srgba, RoundedRect::new(rect, corner_radius));
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// Show a color with background checkers to demonstrate transparency (if any).
|
||||
fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) {
|
||||
fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], bounds: RoundedRect) {
|
||||
let (rect, corner_radius) = bounds.into_parts();
|
||||
if a == 255 {
|
||||
painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
|
||||
painter.rect_filled(rect, corner_radius, Color32::from_rgb(r, g, b));
|
||||
} else {
|
||||
background_checkers(painter, rect);
|
||||
let left = Rect::from_min_max(rect.left_top(), rect.center_bottom());
|
||||
let right = Rect::from_min_max(rect.center_top(), rect.right_bottom());
|
||||
painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a));
|
||||
painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b));
|
||||
|
||||
// Clamp to what the half-width rects can express,
|
||||
// so the checkers and both halves all round their corners the same:
|
||||
let corner_radius = corner_radius.at_most(0.5 * left.size().min_elem());
|
||||
|
||||
background_checkers(painter, RoundedRect::new(rect, corner_radius));
|
||||
|
||||
let left_corner_radius = corner_radius.with_east(0.0);
|
||||
let right_corner_radius = corner_radius.with_west(0.0);
|
||||
painter.rect_filled(
|
||||
left,
|
||||
left_corner_radius,
|
||||
Color32::from_rgba_unmultiplied(r, g, b, a),
|
||||
);
|
||||
painter.rect_filled(right, right_corner_radius, Color32::from_rgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +162,15 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
|
||||
let stroke_width = 1.0;
|
||||
show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width));
|
||||
let corner_radius = visuals.corner_radius;
|
||||
show_srgba_unmultiplied_at(
|
||||
ui.painter(),
|
||||
srgba,
|
||||
// Shrink both the rect and the corner radius,
|
||||
// so the fill arcs stay concentric with the inside stroke:
|
||||
RoundedRect::new(rect, corner_radius).shrink(stroke_width),
|
||||
);
|
||||
|
||||
let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding
|
||||
ui.painter().rect_stroke(
|
||||
rect,
|
||||
corner_radius,
|
||||
@@ -136,8 +183,6 @@ fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response {
|
||||
}
|
||||
|
||||
fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color32) -> Response {
|
||||
#![expect(clippy::identity_op)]
|
||||
|
||||
let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y);
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag());
|
||||
|
||||
@@ -147,29 +192,25 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let visuals = ui.style().interact(&response);
|
||||
let corner_radius = visuals.corner_radius;
|
||||
let bounds = RoundedRect::new(rect, corner_radius);
|
||||
|
||||
background_checkers(ui.painter(), rect); // for alpha:
|
||||
background_checkers(ui.painter(), bounds); // for alpha:
|
||||
|
||||
{
|
||||
// fill color:
|
||||
let mut mesh = Mesh::default();
|
||||
for i in 0..=N {
|
||||
let t = i as f32 / (N as f32);
|
||||
let color = color_at(t);
|
||||
let x = lerp(rect.left()..=rect.right(), t);
|
||||
mesh.colored_vertex(pos2(x, rect.top()), color);
|
||||
mesh.colored_vertex(pos2(x, rect.bottom()), color);
|
||||
if i < N {
|
||||
mesh.add_triangle(2 * i + 0, 2 * i + 1, 2 * i + 2);
|
||||
mesh.add_triangle(2 * i + 1, 2 * i + 2, 2 * i + 3);
|
||||
}
|
||||
}
|
||||
ui.painter().add(Shape::mesh(mesh));
|
||||
// fill color gradient:
|
||||
let width = N as usize + 1;
|
||||
let pixels = (0..width).map(|i| color_at(i as f32 / N as f32)).collect();
|
||||
let image = ColorImage::new([width, 1], pixels);
|
||||
paint_gradient(
|
||||
ui,
|
||||
response.id.with("gradient"),
|
||||
bounds,
|
||||
visuals.bg_stroke,
|
||||
image,
|
||||
);
|
||||
}
|
||||
|
||||
ui.painter()
|
||||
.rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
|
||||
|
||||
{
|
||||
// Show where the slider is at:
|
||||
let x = lerp(rect.left()..=rect.right(), *value);
|
||||
@@ -215,30 +256,28 @@ fn color_slider_2d(
|
||||
|
||||
if ui.is_rect_visible(rect) {
|
||||
let visuals = ui.style().interact(&response);
|
||||
let mut mesh = Mesh::default();
|
||||
let corner_radius = visuals.corner_radius;
|
||||
|
||||
for xi in 0..=N {
|
||||
{
|
||||
// fill color gradient:
|
||||
let width = N as usize + 1;
|
||||
let mut pixels = Vec::with_capacity(width * width);
|
||||
for yi in 0..=N {
|
||||
let xt = xi as f32 / (N as f32);
|
||||
let yt = yi as f32 / (N as f32);
|
||||
let color = color_at(xt, yt);
|
||||
let x = lerp(rect.left()..=rect.right(), xt);
|
||||
let y = lerp(rect.bottom()..=rect.top(), yt);
|
||||
mesh.colored_vertex(pos2(x, y), color);
|
||||
|
||||
if xi < N && yi < N {
|
||||
let x_offset = 1;
|
||||
let y_offset = N + 1;
|
||||
let tl = yi * y_offset + xi;
|
||||
mesh.add_triangle(tl, tl + x_offset, tl + y_offset);
|
||||
mesh.add_triangle(tl + x_offset, tl + y_offset, tl + y_offset + x_offset);
|
||||
let yt = 1.0 - yi as f32 / (N as f32); // texel rows go from top to bottom
|
||||
for xi in 0..=N {
|
||||
let xt = xi as f32 / (N as f32);
|
||||
pixels.push(color_at(xt, yt));
|
||||
}
|
||||
}
|
||||
let image = ColorImage::new([width, width], pixels);
|
||||
paint_gradient(
|
||||
ui,
|
||||
response.id.with("gradient"),
|
||||
RoundedRect::new(rect, corner_radius),
|
||||
visuals.bg_stroke,
|
||||
image,
|
||||
);
|
||||
}
|
||||
ui.painter().add(Shape::mesh(mesh)); // fill
|
||||
|
||||
ui.painter()
|
||||
.rect_stroke(rect, 0.0, visuals.bg_stroke, StrokeKind::Inside); // outline
|
||||
|
||||
// Show where the slider is at:
|
||||
let x = lerp(rect.left()..=rect.right(), *x_value);
|
||||
|
||||
@@ -101,12 +101,10 @@ impl Widget for Separator {
|
||||
|
||||
// Get the widget style by reading the response from the previous pass
|
||||
let id = ui.next_auto_id();
|
||||
let response: Option<Response> = ui.ctx().read_response(id);
|
||||
let state = response.map(|r| r.widget_state()).unwrap_or_default();
|
||||
let SeparatorStyle {
|
||||
spacing: spacing_style,
|
||||
stroke,
|
||||
} = ui.style().separator_style(&classes, state);
|
||||
} = ui.widget_style(id, &classes);
|
||||
|
||||
// override the spacing if not set
|
||||
let spacing = spacing.unwrap_or(spacing_style);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383
|
||||
size 334759
|
||||
oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58
|
||||
size 334814
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e
|
||||
size 92771
|
||||
oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265
|
||||
size 92929
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb
|
||||
size 168430
|
||||
oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d
|
||||
size 168735
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c
|
||||
size 98780
|
||||
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
|
||||
size 101394
|
||||
|
||||
@@ -71,13 +71,13 @@ impl crate::Demo for WindowOptions {
|
||||
let mut window = egui::Window::new(title)
|
||||
.id(egui::Id::new("demo_window_options")) // required since we change the title
|
||||
.resizable(resizable)
|
||||
.constrain_to(ui.available_rect_before_wrap())
|
||||
.constrain(constrain)
|
||||
.collapsible(collapsible)
|
||||
.movable(movable)
|
||||
.title_bar(title_bar)
|
||||
.drag_area(drag_area)
|
||||
.scroll(scroll2)
|
||||
.constrain_to(ui.available_rect_before_wrap())
|
||||
.enabled(enabled);
|
||||
if closable {
|
||||
window = window.open(open);
|
||||
|
||||
17
crates/egui_demo_lib/tests/color_picker.rs
Normal file
17
crates/egui_demo_lib/tests/color_picker.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use egui::Color32;
|
||||
use egui::color_picker::{Alpha, color_picker_color32};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
#[test]
|
||||
fn color_picker() {
|
||||
let mut color = Color32::from_rgba_unmultiplied(130, 130, 130, 45);
|
||||
let mut harness = Harness::builder()
|
||||
.with_pixels_per_point(2.0)
|
||||
.build_ui(move |ui| {
|
||||
ui.spacing_mut().slider_width = 275.0;
|
||||
color_picker_color32(ui, &mut color, Alpha::OnlyBlend);
|
||||
});
|
||||
harness.run();
|
||||
harness.fit_contents();
|
||||
harness.snapshot("color_picker");
|
||||
}
|
||||
@@ -1,6 +1,41 @@
|
||||
use egui::{Color32, accesskit::Role};
|
||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||
|
||||
/// Textures with [`egui::TextureOptions::NEAREST`] should render crisp,
|
||||
/// also with kittest's predictable texture filtering.
|
||||
#[test]
|
||||
fn test_nearest_texture_filtering() {
|
||||
let mut texture: Option<egui::TextureHandle> = None;
|
||||
let mut harness = Harness::builder()
|
||||
.with_size(egui::Vec2::new(80.0, 48.0))
|
||||
.build_ui(move |ui| {
|
||||
let texture = texture.get_or_insert_with(|| {
|
||||
let pixels = [
|
||||
Color32::BLACK,
|
||||
Color32::WHITE,
|
||||
Color32::BLACK,
|
||||
Color32::WHITE,
|
||||
Color32::WHITE,
|
||||
Color32::BLACK,
|
||||
Color32::WHITE,
|
||||
Color32::BLACK,
|
||||
];
|
||||
let image = egui::ColorImage::new([4, 2], pixels.to_vec());
|
||||
ui.ctx()
|
||||
.load_texture("checkerboard", image, egui::TextureOptions::NEAREST)
|
||||
});
|
||||
|
||||
let rect = egui::Rect::from_min_size(egui::pos2(8.0, 8.0), egui::vec2(64.0, 32.0));
|
||||
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
|
||||
ui.painter().add(
|
||||
egui::epaint::RectShape::filled(rect, 0, Color32::WHITE)
|
||||
.with_texture(texture.id(), uv),
|
||||
);
|
||||
});
|
||||
harness.run();
|
||||
harness.snapshot("nearest_texture_filtering");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kerning() {
|
||||
let mut results = egui_kittest::SnapshotResults::new();
|
||||
|
||||
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
3
crates/egui_demo_lib/tests/snapshots/color_picker.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
|
||||
size 141933
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
|
||||
size 32422
|
||||
oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4
|
||||
size 32538
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
|
||||
size 26911
|
||||
oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9
|
||||
size 27023
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
|
||||
size 27253
|
||||
oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a
|
||||
size 27426
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11
|
||||
size 99950
|
||||
oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd
|
||||
size 100303
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
|
||||
size 25116
|
||||
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
|
||||
size 26327
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
|
||||
size 99316
|
||||
oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788
|
||||
size 100543
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
|
||||
size 50512
|
||||
oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833
|
||||
size 50940
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
|
||||
size 22994
|
||||
oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e
|
||||
size 24039
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
|
||||
size 35378
|
||||
oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784
|
||||
size 35474
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
|
||||
size 17686
|
||||
oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd
|
||||
size 17899
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
|
||||
size 57262
|
||||
oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c
|
||||
size 58044
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
|
||||
size 30450
|
||||
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
|
||||
size 30877
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
|
||||
size 23807
|
||||
oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa
|
||||
size 24045
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f
|
||||
size 151376
|
||||
oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd
|
||||
size 151843
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227
|
||||
size 66050
|
||||
oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b
|
||||
size 67107
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
|
||||
size 60701
|
||||
oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63
|
||||
size 62104
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
|
||||
size 41516
|
||||
oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1
|
||||
size 42790
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516
|
||||
size 446483
|
||||
oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba
|
||||
size 446518
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
|
||||
size 48277
|
||||
oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1
|
||||
size 48620
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
|
||||
size 44357
|
||||
oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7
|
||||
size 44600
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
|
||||
size 44347
|
||||
oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170
|
||||
size 44488
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40
|
||||
size 353
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
|
||||
size 590679
|
||||
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
|
||||
size 590735
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
|
||||
size 745771
|
||||
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
|
||||
size 745855
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
|
||||
size 973781
|
||||
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
|
||||
size 973927
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
|
||||
size 1081864
|
||||
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
|
||||
size 1081792
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
|
||||
size 1131030
|
||||
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
|
||||
size 1131176
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
|
||||
size 1363589
|
||||
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
|
||||
size 1363775
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
|
||||
size 45044
|
||||
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
|
||||
size 46677
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
|
||||
size 86830
|
||||
oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056
|
||||
size 88246
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
|
||||
size 118889
|
||||
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
|
||||
size 120579
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
|
||||
size 51376
|
||||
oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b
|
||||
size 52790
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
|
||||
size 54582
|
||||
oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb
|
||||
size 55982
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
|
||||
size 55053
|
||||
oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1
|
||||
size 56466
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
|
||||
size 35943
|
||||
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
|
||||
size 37595
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
|
||||
size 35925
|
||||
oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4
|
||||
size 37340
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
|
||||
size 64973
|
||||
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
|
||||
size 66043
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
|
||||
size 151090
|
||||
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
|
||||
size 153589
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
|
||||
size 59950
|
||||
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
|
||||
size 60813
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
|
||||
size 145940
|
||||
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
|
||||
size 148235
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||

|
||||

|
||||
|
||||
This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
|
||||
This is a crate that adds some features on top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`.
|
||||
|
||||
## Images
|
||||
One thing `egui_extras` is commonly used for is to install image loaders for `egui`:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
|
||||
size 8028
|
||||
oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98
|
||||
size 8160
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
|
||||
size 2205
|
||||
oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1
|
||||
size 2246
|
||||
|
||||
@@ -73,6 +73,7 @@ impl CornerRadiusF32 {
|
||||
|
||||
/// Same rounding on all four corners.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn same(radius: f32) -> Self {
|
||||
Self {
|
||||
nw: radius,
|
||||
@@ -90,6 +91,7 @@ impl CornerRadiusF32 {
|
||||
|
||||
/// Make sure each corner has a rounding of at least this.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn at_least(&self, min: f32) -> Self {
|
||||
Self {
|
||||
nw: self.nw.max(min),
|
||||
@@ -101,6 +103,7 @@ impl CornerRadiusF32 {
|
||||
|
||||
/// Make sure each corner has a rounding of at most this.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn at_most(&self, max: f32) -> Self {
|
||||
Self {
|
||||
nw: self.nw.min(max),
|
||||
@@ -109,79 +112,111 @@ impl CornerRadiusF32 {
|
||||
se: self.se.min(max),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rounding of the two east (right) corners.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn with_east(self, radius: f32) -> Self {
|
||||
Self {
|
||||
ne: radius,
|
||||
se: radius,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rounding of the two north (top) corners.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn with_north(self, radius: f32) -> Self {
|
||||
Self {
|
||||
nw: radius,
|
||||
ne: radius,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rounding of the two south (bottom) corners.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn with_south(self, radius: f32) -> Self {
|
||||
Self {
|
||||
sw: radius,
|
||||
se: radius,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the rounding of the two west (left) corners.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn with_west(self, radius: f32) -> Self {
|
||||
Self {
|
||||
nw: radius,
|
||||
sw: radius,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for CornerRadiusF32 {
|
||||
type Output = Self;
|
||||
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Self {
|
||||
nw: self.nw + rhs.nw,
|
||||
ne: self.ne + rhs.ne,
|
||||
sw: self.sw + rhs.sw,
|
||||
se: self.se + rhs.se,
|
||||
nw: (self.nw + rhs.nw).max(0.0),
|
||||
ne: (self.ne + rhs.ne).max(0.0),
|
||||
sw: (self.sw + rhs.sw).max(0.0),
|
||||
se: (self.se + rhs.se).max(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::AddAssign for CornerRadiusF32 {
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
*self = Self {
|
||||
nw: self.nw + rhs.nw,
|
||||
ne: self.ne + rhs.ne,
|
||||
sw: self.sw + rhs.sw,
|
||||
se: self.se + rhs.se,
|
||||
};
|
||||
*self = *self + rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::AddAssign<f32> for CornerRadiusF32 {
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn add_assign(&mut self, rhs: f32) {
|
||||
*self = Self {
|
||||
nw: self.nw + rhs,
|
||||
ne: self.ne + rhs,
|
||||
sw: self.sw + rhs,
|
||||
se: self.se + rhs,
|
||||
};
|
||||
*self = *self + Self::same(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Sub for CornerRadiusF32 {
|
||||
type Output = Self;
|
||||
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn sub(self, rhs: Self) -> Self {
|
||||
Self {
|
||||
nw: self.nw - rhs.nw,
|
||||
ne: self.ne - rhs.ne,
|
||||
sw: self.sw - rhs.sw,
|
||||
se: self.se - rhs.se,
|
||||
nw: (self.nw - rhs.nw).max(0.0),
|
||||
ne: (self.ne - rhs.ne).max(0.0),
|
||||
sw: (self.sw - rhs.sw).max(0.0),
|
||||
se: (self.se - rhs.se).max(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::SubAssign for CornerRadiusF32 {
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
*self = Self {
|
||||
nw: self.nw - rhs.nw,
|
||||
ne: self.ne - rhs.ne,
|
||||
sw: self.sw - rhs.sw,
|
||||
se: self.se - rhs.se,
|
||||
};
|
||||
*self = *self - rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::SubAssign<f32> for CornerRadiusF32 {
|
||||
/// Saturates at zero: no corner radius will go negative.
|
||||
#[inline]
|
||||
fn sub_assign(&mut self, rhs: f32) {
|
||||
*self = Self {
|
||||
nw: self.nw - rhs,
|
||||
ne: self.ne - rhs,
|
||||
sw: self.sw - rhs,
|
||||
se: self.se - rhs,
|
||||
};
|
||||
*self = *self - Self::same(rhs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,3 +269,26 @@ impl core::ops::MulAssign<f32> for CornerRadiusF32 {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CornerRadiusF32;
|
||||
|
||||
#[test]
|
||||
fn add_and_sub_saturate_at_zero() {
|
||||
assert_eq!(
|
||||
CornerRadiusF32::same(2.0) + CornerRadiusF32::same(-5.0),
|
||||
CornerRadiusF32::ZERO
|
||||
);
|
||||
assert_eq!(
|
||||
CornerRadiusF32::same(2.0) - CornerRadiusF32::same(5.0),
|
||||
CornerRadiusF32::ZERO
|
||||
);
|
||||
|
||||
let mut cr = CornerRadiusF32::same(1.0);
|
||||
cr += -3.0;
|
||||
assert_eq!(cr, CornerRadiusF32::ZERO);
|
||||
cr -= -2.0;
|
||||
assert_eq!(cr, CornerRadiusF32::same(2.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ mod margin;
|
||||
mod margin_f32;
|
||||
mod mesh;
|
||||
pub mod mutex;
|
||||
mod rounded_rect;
|
||||
mod shadow;
|
||||
pub mod shape_transform;
|
||||
mod shapes;
|
||||
@@ -56,6 +57,7 @@ pub use self::{
|
||||
margin::Margin,
|
||||
margin_f32::*,
|
||||
mesh::{Mesh, Mesh16, Vertex},
|
||||
rounded_rect::RoundedRect,
|
||||
shadow::Shadow,
|
||||
shapes::{
|
||||
CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape,
|
||||
|
||||
159
crates/epaint/src/rounded_rect.rs
Normal file
159
crates/epaint/src/rounded_rect.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use emath::{Pos2, Rect, Vec2, vec2};
|
||||
|
||||
use crate::CornerRadiusF32;
|
||||
|
||||
/// A rectangle geometry with rounded corners.
|
||||
///
|
||||
/// Not a painting primitive. For that, see [`crate::RectShape`].
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct RoundedRect {
|
||||
rect: Rect,
|
||||
corner_radius: CornerRadiusF32,
|
||||
}
|
||||
|
||||
impl RoundedRect {
|
||||
/// The corner radius is clamped to half the size of the rectangle.
|
||||
#[inline]
|
||||
pub fn new(rect: Rect, corner_radius: impl Into<CornerRadiusF32>) -> Self {
|
||||
let max_radius = 0.5 * rect.size().min_elem();
|
||||
Self {
|
||||
rect,
|
||||
corner_radius: corner_radius.into().at_most(max_radius).at_least(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rect(&self) -> Rect {
|
||||
self.rect
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn corner_radius(&self) -> CornerRadiusF32 {
|
||||
self.corner_radius
|
||||
}
|
||||
|
||||
/// Split into the rectangle and the corner radius.
|
||||
#[inline]
|
||||
pub fn into_parts(self) -> (Rect, CornerRadiusF32) {
|
||||
let Self {
|
||||
rect,
|
||||
corner_radius,
|
||||
} = self;
|
||||
(rect, corner_radius)
|
||||
}
|
||||
|
||||
/// Expand the rectangle and the corner radii by the given amount.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn expand(self, amount: f32) -> Self {
|
||||
Self::new(
|
||||
self.rect.expand(amount),
|
||||
self.corner_radius + CornerRadiusF32::same(amount),
|
||||
)
|
||||
}
|
||||
|
||||
/// Shrink the rectangle and the corner radii by the given amount.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn shrink(self, amount: f32) -> Self {
|
||||
self.expand(-amount)
|
||||
}
|
||||
|
||||
/// Clamp the given position to lie within this rounded rectangle.
|
||||
///
|
||||
/// Positions in the corner regions are projected onto the corner arcs.
|
||||
pub fn clamp_pos(&self, pos: Pos2) -> Pos2 {
|
||||
let Self {
|
||||
rect,
|
||||
corner_radius,
|
||||
} = *self;
|
||||
let pos = rect.clamp(pos);
|
||||
let corners = [
|
||||
(corner_radius.nw, vec2(-1.0, -1.0)),
|
||||
(corner_radius.ne, vec2(1.0, -1.0)),
|
||||
(corner_radius.sw, vec2(-1.0, 1.0)),
|
||||
(corner_radius.se, vec2(1.0, 1.0)),
|
||||
];
|
||||
for (radius, dir) in corners {
|
||||
let arc_center = rect.center() + dir * (rect.size() / 2.0 - Vec2::splat(radius));
|
||||
let offset = pos - arc_center;
|
||||
if 0.0 < offset.x * dir.x && 0.0 < offset.y * dir.y && radius < offset.length() {
|
||||
return arc_center + (radius / offset.length()) * offset;
|
||||
}
|
||||
}
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for RoundedRect {
|
||||
#[inline]
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
corner_radius: CornerRadiusF32::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use emath::pos2;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn clamp_pos() {
|
||||
let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
|
||||
let rounded = RoundedRect::new(
|
||||
rect,
|
||||
CornerRadiusF32 {
|
||||
nw: 10.0,
|
||||
ne: 0.0,
|
||||
sw: 0.0,
|
||||
se: 20.0,
|
||||
},
|
||||
);
|
||||
|
||||
// Interior point is untouched:
|
||||
assert_eq!(rounded.clamp_pos(pos2(50.0, 50.0)), pos2(50.0, 50.0));
|
||||
|
||||
// Sharp corner is untouched:
|
||||
assert_eq!(rounded.clamp_pos(pos2(100.0, 0.0)), pos2(100.0, 0.0));
|
||||
|
||||
// Outside the rect is clamped to the edge:
|
||||
assert_eq!(rounded.clamp_pos(pos2(-10.0, 50.0)), pos2(0.0, 50.0));
|
||||
|
||||
// Rounded corner is projected onto the arc:
|
||||
let clamped = rounded.clamp_pos(pos2(0.0, 0.0));
|
||||
let arc_center = pos2(10.0, 10.0);
|
||||
assert!((clamped - arc_center).length() - 10.0 < 0.001);
|
||||
let expected = 10.0 - 10.0 / core::f32::consts::SQRT_2;
|
||||
assert!((clamped - pos2(expected, expected)).length() < 0.001);
|
||||
|
||||
// Point on the arc stays put:
|
||||
assert_eq!(rounded.clamp_pos(pos2(10.0, 0.0)), pos2(10.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand() {
|
||||
let rect = Rect::from_min_max(pos2(10.0, 10.0), pos2(90.0, 90.0));
|
||||
let expanded = RoundedRect::new(rect, 20.0).expand(10.0);
|
||||
assert_eq!(
|
||||
expanded.rect(),
|
||||
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0))
|
||||
);
|
||||
assert_eq!(expanded.corner_radius(), CornerRadiusF32::same(30.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_radius_is_clamped() {
|
||||
// A radius larger than half the rect is clamped, like in the tessellator:
|
||||
let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0));
|
||||
assert_eq!(RoundedRect::new(rect, 200.0), RoundedRect::new(rect, 50.0));
|
||||
assert_eq!(
|
||||
RoundedRect::new(rect, 200.0).corner_radius(),
|
||||
CornerRadiusF32::same(50.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2};
|
||||
use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, RoundedRect, Vec2};
|
||||
|
||||
/// The color and fuzziness of a fuzzy shape.
|
||||
///
|
||||
@@ -56,10 +56,12 @@ impl Shadow {
|
||||
} = *self;
|
||||
let [offset_x, offset_y] = offset;
|
||||
|
||||
let rect = rect
|
||||
.translate(Vec2::new(offset_x as _, offset_y as _))
|
||||
.expand(spread as _);
|
||||
let corner_radius = corner_radius.into() + CornerRadius::from(spread);
|
||||
let (rect, corner_radius) = RoundedRect::new(
|
||||
rect.translate(Vec2::new(offset_x as _, offset_y as _)),
|
||||
corner_radius.into(),
|
||||
)
|
||||
.expand(f32::from(spread))
|
||||
.into_parts();
|
||||
|
||||
RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use emath::{
|
||||
|
||||
use crate::{
|
||||
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
|
||||
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
|
||||
StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
|
||||
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape,
|
||||
Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
|
||||
texture_atlas::PreparedDisc,
|
||||
};
|
||||
|
||||
@@ -536,18 +536,19 @@ impl Path {
|
||||
|
||||
pub mod path {
|
||||
//! Helpers for constructing paths
|
||||
use crate::CornerRadiusF32;
|
||||
use emath::{Pos2, Rect, pos2};
|
||||
use crate::{CornerRadiusF32, RoundedRect};
|
||||
use emath::{Pos2, pos2};
|
||||
|
||||
/// overwrites existing points
|
||||
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, cr: CornerRadiusF32) {
|
||||
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rounded_rect: RoundedRect) {
|
||||
path.clear();
|
||||
|
||||
// The corner radius is already clamped to half the rect size by `RoundedRect`:
|
||||
let (rect, cr) = rounded_rect.into_parts();
|
||||
|
||||
let min = rect.min;
|
||||
let max = rect.max;
|
||||
|
||||
let cr = clamp_corner_radius(cr, rect);
|
||||
|
||||
if cr == CornerRadiusF32::ZERO {
|
||||
path.reserve(4);
|
||||
path.push(pos2(min.x, min.y)); // left top
|
||||
@@ -633,14 +634,6 @@ pub mod path {
|
||||
path.extend(quadrant_vertices.iter().map(|&n| center + radius * n));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures the radius of each corner is within a valid range
|
||||
fn clamp_corner_radius(cr: CornerRadiusF32, rect: Rect) -> CornerRadiusF32 {
|
||||
let half_width = rect.width() * 0.5;
|
||||
let half_height = rect.height() * 0.5;
|
||||
let max_cr = half_width.min(half_height);
|
||||
cr.at_most(max_cr).at_least(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -1938,7 +1931,10 @@ impl Tessellator {
|
||||
|
||||
let path = &mut self.scratchpad_path;
|
||||
path.clear();
|
||||
path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius);
|
||||
path::rounded_rectangle(
|
||||
&mut self.scratchpad_points,
|
||||
RoundedRect::new(rect, corner_radius),
|
||||
);
|
||||
|
||||
// Apply rotation if angle is non-zero
|
||||
if angle != 0.0 {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "popups"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
|
||||
20
examples/styling_engine/Cargo.toml
Normal file
20
examples/styling_engine/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "styling_engine"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.92"
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
[dependencies]
|
||||
eframe = { workspace = true, features = [
|
||||
"default",
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
"experimental", # this example is all about the experimental theming API
|
||||
] }
|
||||
env_logger = { workspace = true, features = ["auto-color", "humantime"] }
|
||||
10
examples/styling_engine/README.md
Normal file
10
examples/styling_engine/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Example showing how the style engine work.
|
||||
|
||||
A custom `StyleProvider<ButtonStyle>` styles every button, using _classes_ to tell them apart.
|
||||
The theme can be edited live in the left panel.
|
||||
|
||||
```sh
|
||||
cargo run -p styling_engine
|
||||
```
|
||||
|
||||
<!--  -->
|
||||
162
examples/styling_engine/src/main.rs
Normal file
162
examples/styling_engine/src/main.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
//! A small styling engine: a custom [`StyleProvider`] that styles every [`egui::Button`]
|
||||
//! based on the _classes_ set on it, and that can be edited live.
|
||||
|
||||
use eframe::egui::{
|
||||
self, CentralPanel, Color32, Frame, Panel,
|
||||
theme::StyleProvider,
|
||||
widget_style::{BaseStyle, ButtonStyle, HasClasses as _, StyleArgs, WidgetState},
|
||||
};
|
||||
|
||||
/// Buttons with this class are styled as a destructive action.
|
||||
const DANGER: &str = "danger";
|
||||
|
||||
/// Our styling engine: it decides what every button looks like.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct MyTheme {
|
||||
normal: Color32,
|
||||
danger: Color32,
|
||||
corner_radius: u8,
|
||||
}
|
||||
|
||||
impl MyTheme {
|
||||
fn preset(preset: Preset) -> Self {
|
||||
match preset {
|
||||
Preset::Ocean => Self {
|
||||
normal: Color32::from_rgb(0x1E, 0x5A, 0x8A),
|
||||
danger: Color32::from_rgb(0x9B, 0x2C, 0x2C),
|
||||
corner_radius: 4,
|
||||
},
|
||||
Preset::Candy => Self {
|
||||
normal: Color32::from_rgb(0xB8, 0x3B, 0x9E),
|
||||
danger: Color32::from_rgb(0xD9, 0x6A, 0x1F),
|
||||
corner_radius: 16,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyleProvider<ButtonStyle> for MyTheme {
|
||||
fn style(&mut self, args: &StyleArgs<'_>) -> ButtonStyle {
|
||||
let StyleArgs {
|
||||
classes,
|
||||
state,
|
||||
ctx,
|
||||
..
|
||||
} = args;
|
||||
|
||||
// Start from the style egui computed for a generic widget, so we inherit e.g. the font:
|
||||
let base: BaseStyle = ctx.get_widget_style(args);
|
||||
|
||||
let fill = if classes.has(DANGER) {
|
||||
self.danger
|
||||
} else {
|
||||
self.normal
|
||||
};
|
||||
|
||||
// React to what the user is doing with the button:
|
||||
let fill = match state {
|
||||
WidgetState::Hovered => fill.gamma_multiply(1.4),
|
||||
WidgetState::Active => fill.gamma_multiply(0.7),
|
||||
_ => fill,
|
||||
};
|
||||
|
||||
ButtonStyle {
|
||||
frame: Frame::new()
|
||||
.fill(fill)
|
||||
.corner_radius(self.corner_radius)
|
||||
.inner_margin(8),
|
||||
text_style: base.text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Preset {
|
||||
Ocean,
|
||||
Candy,
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size([600.0, 340.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut preset = Preset::Ocean;
|
||||
let mut theme = MyTheme::preset(preset);
|
||||
let mut last_click = "nothing";
|
||||
|
||||
eframe::run_ui_native("Styling engine", options, move |ui, _frame| {
|
||||
// Register our theme for all buttons. This is a no-op after the first frame.
|
||||
ui.add_widget_theme::<ButtonStyle>(theme);
|
||||
|
||||
Panel::left("controls").default_size(260.0).show(ui, |ui| {
|
||||
// The custom theme inherits the text color from egui's light/dark theme:
|
||||
egui::global_theme_preference_buttons(ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.heading("Button theme");
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
egui::ComboBox::from_label("Preset")
|
||||
.selected_text(match preset {
|
||||
Preset::Ocean => "Ocean",
|
||||
Preset::Candy => "Candy",
|
||||
})
|
||||
.show_ui(ui, |ui| {
|
||||
changed |= ui
|
||||
.selectable_value(&mut preset, Preset::Ocean, "Ocean")
|
||||
.changed();
|
||||
changed |= ui
|
||||
.selectable_value(&mut preset, Preset::Candy, "Candy")
|
||||
.changed();
|
||||
});
|
||||
if changed {
|
||||
theme = MyTheme::preset(preset);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
changed |= ui.color_edit_button_srgba(&mut theme.normal).changed();
|
||||
ui.label("Normal");
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
changed |= ui.color_edit_button_srgba(&mut theme.danger).changed();
|
||||
ui.label("Danger");
|
||||
});
|
||||
changed |= ui
|
||||
.add(egui::Slider::new(&mut theme.corner_radius, 0..=24).text("Corner radius"))
|
||||
.changed();
|
||||
|
||||
if changed {
|
||||
// Overwrite the registered theme with the edited one:
|
||||
ui.replace_widget_theme::<ButtonStyle>(theme);
|
||||
}
|
||||
});
|
||||
|
||||
CentralPanel::default().show(ui, |ui| {
|
||||
ui.heading("Buttons");
|
||||
ui.label("All buttons below are styled by the custom theme.");
|
||||
ui.add_space(8.0);
|
||||
|
||||
if ui.button("Save").clicked() {
|
||||
last_click = "Save";
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
if ui
|
||||
.add(egui::Button::new("Delete everything").with_class(DANGER))
|
||||
.clicked()
|
||||
{
|
||||
last_click = "Delete everything";
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("Last clicked: {last_click}"));
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -66,11 +66,8 @@ def lint_lines(filepath, lines_in):
|
||||
)
|
||||
lines_out.append("#[inline]")
|
||||
|
||||
|
||||
if re.search(r"TODO[^(]", line):
|
||||
errors.append(
|
||||
f"{filepath}:{line_nr}: write 'TODO(username):' instead"
|
||||
)
|
||||
errors.append(f"{filepath}:{line_nr}: write 'TODO(username):' instead")
|
||||
|
||||
if re.search(r"\.zip\(", line):
|
||||
errors.append(
|
||||
@@ -177,7 +174,11 @@ def main():
|
||||
root_dirpath = os.path.abspath(f"{script_dirpath}/..")
|
||||
os.chdir(root_dirpath)
|
||||
|
||||
exclude = set(["target", "target_ra", "target_wasm"])
|
||||
exclude = {
|
||||
"target",
|
||||
"target_ra", # legacy Rust Analyzer target dir
|
||||
"target_wasm",
|
||||
}
|
||||
for root, dirs, files in os.walk(".", topdown=True):
|
||||
dirs[:] = [d for d in dirs if d not in exclude]
|
||||
for filename in files:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "egui_tests"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
@@ -9,7 +10,7 @@ version.workspace = true
|
||||
ignored = ["image"] # We need the png feature
|
||||
|
||||
[dev-dependencies]
|
||||
egui = { workspace = true, default-features = true }
|
||||
egui = { workspace = true, default-features = true, features = ["experimental"] }
|
||||
egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] }
|
||||
egui_extras = { workspace = true, features = ["image"] }
|
||||
image = { workspace = true, features = ["png"] }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e43b410ab0a06b40da5ad9c7fd105d11201d44a3afd63fd213429cc39013c0d4
|
||||
size 6964
|
||||
oid sha256:da19989c02e0dc6c558575c7ab0f5e51b2747b2d65d69f6883636d039ed54d2a
|
||||
size 7335
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bafb447c177f2bd94980c1d146a2826ecf62070fb6ee18e63e6a925e2741210
|
||||
size 314478
|
||||
oid sha256:c9df24e02feeac296cf03f42b7a5343f943ee1dd00508c5b6da2563928ec228b
|
||||
size 324343
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:eadf74cc7f7e10be804df0e7df65c232ecaed7d77c2c89197548ab7206706843
|
||||
size 351669
|
||||
oid sha256:106d9e06bbcae5db4e0a5813df48899f16c3ac131c9a44e45b45253bede781d1
|
||||
size 361653
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:60739a18972a5e866610ae2c8f5af719a1bbaeffcdd8c6319e783c37e28d3107
|
||||
size 435108
|
||||
oid sha256:83f6000e0ff5705df6012036da6337d589baeb6652d1b554213ab5948c505040
|
||||
size 445361
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2fb6db99b4dfa1b0c6f97799ffd72c264c02eefd803eee5737075231760c80bd
|
||||
size 417925
|
||||
oid sha256:4bcbf331a0d18b1233689132d149e4766e8db2827bafce50696d73195d83fc02
|
||||
size 426870
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f54f444c25fbd1def8d0f11d03e1fd8bde6745a2593d0029e346cbaafa584ed3
|
||||
size 264007
|
||||
oid sha256:4372664983d9ae7e9889610349f86f025e0c9fccccaaa20dd4735663baede051
|
||||
size 274014
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:97c2f56dad8d6aa45385901f9b23a13c49c21fc5aa811ab50dcf7c8451b177dd
|
||||
size 401596
|
||||
oid sha256:45f1b8f5b31f925b7ceb0552c67bcf2df6e43dfba1980381d3ae677a4b713b5c
|
||||
size 412451
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:619139eb166d383cc9b35653c8c4a97c55cd21b171a24e253d923558cc0d35e1
|
||||
size 275512
|
||||
oid sha256:d9395e6534bd5d1ddcdd2be8c446bf85b7c3b65dc7a0df7c018db68d2a563a6f
|
||||
size 312206
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user