1
0
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:
Lucas Meurer
2026-08-31 15:01:57 +02:00
128 changed files with 1800 additions and 699 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383
size 334759
oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58
size 334814

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e
size 92771
oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265
size 92929

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb
size 168430
oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d
size 168735

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c
size 98780
oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9
size 101394

View File

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

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

View File

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

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad
size 141933

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599
size 32422
oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4
size 32538

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795
size 26911
oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9
size 27023

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc
size 27253
oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a
size 27426

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11
size 99950
oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd
size 100303

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818
size 25116
oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7
size 26327

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17
size 99316
oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788
size 100543

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7
size 50512
oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833
size 50940

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076
size 22994
oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e
size 24039

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6
size 35378
oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784
size 35474

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0
size 17686
oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd
size 17899

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935
size 57262
oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c
size 58044

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf
size 30450
oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d
size 30877

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1
size 23807
oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa
size 24045

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f
size 151376
oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd
size 151843

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227
size 66050
oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b
size 67107

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94
size 60701
oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63
size 62104

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067
size 41516
oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1
size 42790

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516
size 446483
oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba
size 446518

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8
size 48277
oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1
size 48620

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251
size 44357
oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7
size 44600

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2
size 44347
oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170
size 44488

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40
size 353

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c
size 590679
oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b
size 590735

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d
size 745771
oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c
size 745855

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d
size 973781
oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a
size 973927

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827
size 1081864
oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639
size 1081792

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f
size 1131030
oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b
size 1131176

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b
size 1363589
oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325
size 1363775

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23
size 45044
oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b
size 46677

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8
size 86830
oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056
size 88246

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6
size 118889
oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154
size 120579

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a
size 51376
oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b
size 52790

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128
size 54582
oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb
size 55982

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8
size 55053
oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1
size 56466

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c
size 35943
oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9
size 37595

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24
size 35925
oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4
size 37340

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed
size 64973
oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4
size 66043

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54
size 151090
oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6
size 153589

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4
size 59950
oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965
size 60813

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9
size 145940
oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455
size 148235

View File

@@ -6,7 +6,7 @@
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
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`:

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d
size 8028
oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98
size 8160

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937
size 2205
oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1
size 2246

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
name = "popups"
edition.workspace = true
license.workspace = true
publish = false
rust-version.workspace = true
version.workspace = true

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

View 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
```
<!-- ![](screenshot.png) -->

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e43b410ab0a06b40da5ad9c7fd105d11201d44a3afd63fd213429cc39013c0d4
size 6964
oid sha256:da19989c02e0dc6c558575c7ab0f5e51b2747b2d65d69f6883636d039ed54d2a
size 7335

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1bafb447c177f2bd94980c1d146a2826ecf62070fb6ee18e63e6a925e2741210
size 314478
oid sha256:c9df24e02feeac296cf03f42b7a5343f943ee1dd00508c5b6da2563928ec228b
size 324343

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eadf74cc7f7e10be804df0e7df65c232ecaed7d77c2c89197548ab7206706843
size 351669
oid sha256:106d9e06bbcae5db4e0a5813df48899f16c3ac131c9a44e45b45253bede781d1
size 361653

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:60739a18972a5e866610ae2c8f5af719a1bbaeffcdd8c6319e783c37e28d3107
size 435108
oid sha256:83f6000e0ff5705df6012036da6337d589baeb6652d1b554213ab5948c505040
size 445361

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2fb6db99b4dfa1b0c6f97799ffd72c264c02eefd803eee5737075231760c80bd
size 417925
oid sha256:4bcbf331a0d18b1233689132d149e4766e8db2827bafce50696d73195d83fc02
size 426870

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f54f444c25fbd1def8d0f11d03e1fd8bde6745a2593d0029e346cbaafa584ed3
size 264007
oid sha256:4372664983d9ae7e9889610349f86f025e0c9fccccaaa20dd4735663baede051
size 274014

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97c2f56dad8d6aa45385901f9b23a13c49c21fc5aa811ab50dcf7c8451b177dd
size 401596
oid sha256:45f1b8f5b31f925b7ceb0552c67bcf2df6e43dfba1980381d3ae677a4b713b5c
size 412451

View File

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