diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fb9926e3a..559430c7f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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! diff --git a/.gitignore b/.gitignore index 588826e7d..86507503b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 21d4bb4b0..113ad5092 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3140d942..a2ce09dcf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/Cargo.lock b/Cargo.lock index 71ed40f62..88207d5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index b6534d2d1..3aae7fa74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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. diff --git a/README.md b/README.md index 45bd23f6d..4f105a0ef 100644 --- a/README.md +++ b/README.md @@ -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 and follow the instructions. The official docs are at . 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 and follow the instructions. The official docs are at . 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. diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 68a8fc3d6..7a9cf72b7 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -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> = 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)); + } + } + } } diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index ea7cff6f7..02ea96c51 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -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 { diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 4b30ad18d..a9475365e 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -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, diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index cca1d22c6..82a150aad 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -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, diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl index 39210841b..9567b0d3e 100644 --- a/crates/egui-wgpu/src/egui.wgsl +++ b/crates/egui-wgpu/src/egui.wgsl @@ -98,24 +98,64 @@ fn vs_main( @group(1) @binding(0) var r_tex_color: texture_2d; @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 r_tex_flags: vec4; + +/// Map a texel coordinate to a valid texel according to the texture's wrap mode. +fn wrap_texel_coord(coord: vec2, texture_size: vec2) -> vec2 { + 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(1, 1) - phase); + } else { + // WRAP_MODE_CLAMP_TO_EDGE + return clamp(coord, vec2(0, 0), texture_size - vec2(1, 1)); + } +} + + fn sample_texture(in: VertexOutput) -> vec4 { 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(textureDimensions(r_tex_color, 0)); let texture_size_f = vec2(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(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(floor(pixel_coord)); - // Manual texture clamping - let max_coord = texture_size - vec2(1, 1); - let p00 = clamp(pixel_floor + vec2(0, 0), vec2(0, 0), max_coord); - let p10 = clamp(pixel_floor + vec2(1, 0), vec2(0, 0), max_coord); - let p01 = clamp(pixel_floor + vec2(0, 1), vec2(0, 0), max_coord); - let p11 = clamp(pixel_floor + vec2(1, 1), vec2(0, 0), max_coord); + let p00 = wrap_texel_coord(pixel_floor + vec2(0, 0), texture_size); + let p10 = wrap_texel_coord(pixel_floor + vec2(1, 0), texture_size); + let p01 = wrap_texel_coord(pixel_floor + vec2(0, 1), texture_size); + let p11 = wrap_texel_coord(pixel_floor + vec2(1, 1), texture_size); // Load at pixel centers let tl = textureLoad(r_tex_color, p00, 0); diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index de8808de3..d5130d5c0 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -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::() * 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, diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 3909a9b53..3c9ceae5d 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -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()); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 0029a47b7..5ca800108 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -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 { 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")] diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 4aee66384..0bbd8494e 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -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"] diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 25b384c95..a48b10570 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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, + + 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( + &self, + theme: impl theme::StyleProvider + Send + Sync + 'static, + ) { + self.write(|ctx| ctx.themes.register::(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( + &self, + theme: impl theme::StyleProvider + Send + Sync + 'static, + ) { + self.write(|ctx| ctx.themes.register::(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( + &self, + modifiers: &StyleArgs<'_>, + ) -> S { + let theme = self.read(move |ctx| ctx.themes.get::()); + 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, + id: Id, + image: impl Into, + options: TextureOptions, + ) -> TextureHandle { + profiling::function_scope!(); + + use crate::cache::FramePublisher; + + type TextureCache = FramePublisher; + + let image = image.into(); + let cached: Option<(ImageData, TextureOptions, TextureHandle)> = + self.memory_mut(|mem| mem.caches.cache::().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::() + .set(id, (image, options, handle.clone())); + }); + + handle + } + /// Low-level texture manager. /// /// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`]. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 27ab7035c..2d4099d92 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -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; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index f6df21f09..ae467656c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -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, }, } diff --git a/crates/egui/src/theme/default_style.rs b/crates/egui/src/theme/default_style.rs new file mode 100644 index 000000000..a23ef8fcd --- /dev/null +++ b/crates/egui/src/theme/default_style.rs @@ -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 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 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 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 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 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, + } + } +} diff --git a/crates/egui/src/theme/mod.rs b/crates/egui/src/theme/mod.rs new file mode 100644 index 000000000..088d7f778 --- /dev/null +++ b/crates/egui/src/theme/mod.rs @@ -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( + &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::(&StyleArgs { + classes, + state, + style: self.style(), + stack: self.stack(), + ctx: self, + }) + } +} diff --git a/crates/egui/src/theme/style_provider.rs b/crates/egui/src/theme/style_provider.rs new file mode 100644 index 000000000..5f9205a85 --- /dev/null +++ b/crates/egui/src/theme/style_provider.rs @@ -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 { + /// 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::() + } +} diff --git a/crates/egui/src/theme/themes.rs b/crates/egui/src/theme/themes.rs new file mode 100644 index 000000000..4294f9460 --- /dev/null +++ b/crates/egui/src/theme/themes.rs @@ -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 = Arc + Send + Sync>>>; + +impl Default for Themes { + /// Register the default egui theme + fn default() -> Self { + let mut themes = IdTypeMap::default(); + + themes.insert_temp::>( + Id::NULL, + Arc::new(Mutex::new(Box::new(DefaultStyle))), + ); + + themes.insert_temp::>( + Id::NULL, + Arc::new(Mutex::new(Box::new(DefaultStyle))), + ); + + themes.insert_temp::>( + Id::NULL, + Arc::new(Mutex::new(Box::new(DefaultStyle))), + ); + + themes.insert_temp::>( + Id::NULL, + Arc::new(Mutex::new(Box::new(DefaultStyle))), + ); + + themes.insert_temp::>( + 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( + &mut self, + theme: impl StyleProvider + Send + Sync + 'static, + force: bool, + ) { + if !force + && self + .themes + .get_temp::>(Id::NULL) + .is_some_and(|t| t.lock().type_id() == theme.type_id()) + { + return; + } + + self.themes + .insert_temp::>(Id::NULL, Arc::new(Mutex::new(Box::new(theme)))); + } + + /// Fetch the style of the current theme + pub fn get(&self) -> ThemeWrap { + let v = self.themes.get_temp::>(Id::NULL); + + v.unwrap_or_else(|| { + panic!( + "A style should be set for {:?}", + core::any::type_name::() + ) + }) + } +} diff --git a/crates/egui/src/widget_style.rs b/crates/egui/src/widget_style.rs deleted file mode 100644 index f6239cae9..000000000 --- a/crates/egui/src/widget_style.rs +++ /dev/null @@ -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, 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) -> 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, 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) -> &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, 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) -> bool { - self.classes().classes.contains(&class.into()) - } -} diff --git a/crates/egui/src/widget_style/classes.rs b/crates/egui/src/widget_style/classes.rs new file mode 100644 index 000000000..95ef43aae --- /dev/null +++ b/crates/egui/src/widget_style/classes.rs @@ -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, 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) -> 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, 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) -> &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, 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) -> bool { + self.classes().classes.contains(&class.into()) + } + + /// The list of class + fn as_slice(&self) -> &[ClassName] { + &self.classes().classes + } +} diff --git a/crates/egui/src/widget_style/mod.rs b/crates/egui/src/widget_style/mod.rs new file mode 100644 index 000000000..1a2c19959 --- /dev/null +++ b/crates/egui/src/widget_style/mod.rs @@ -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, +} diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index a1d2f84ed..d7f6c8a0c 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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 diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 0aaedec76..9310c64d5 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -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 = 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); diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a85c2a7b4..90a548660 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -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) -> Color32 { @@ -19,38 +21,63 @@ fn contrast_color(color: impl Into) -> 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); diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index 48d02fe8e..e188d4127 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -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 = 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); diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 87c4b5374..812163491 100644 --- a/crates/egui_demo_app/tests/snapshots/clock.png +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383 -size 334759 +oid sha256:f5290f645746e693fa7c9c7cb2985b0ec4a2522d52629e2cf583e683a3d67b58 +size 334814 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index fc393aa9e..3e8e97e79 100644 --- a/crates/egui_demo_app/tests/snapshots/custom3d.png +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e -size 92771 +oid sha256:5ac1089b1502496408e932025892e9f587ae62861e2d3cf2620bf5c5a09aa265 +size 92929 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 7f908cbea..afcc360bc 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb -size 168430 +oid sha256:dde8e504560d658fbb3eaee0dc12bbbd5c930841bc5637bc60274773d8a50e2d +size 168735 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 7381f2737..c08ab1aac 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c -size 98780 +oid sha256:24fc466f6470761a5064a590929fa3a0d95741ed691fc257415eee349bc528e9 +size 101394 diff --git a/crates/egui_demo_lib/src/demo/window_options.rs b/crates/egui_demo_lib/src/demo/window_options.rs index d95faa546..6a45778e9 100644 --- a/crates/egui_demo_lib/src/demo/window_options.rs +++ b/crates/egui_demo_lib/src/demo/window_options.rs @@ -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); diff --git a/crates/egui_demo_lib/tests/color_picker.rs b/crates/egui_demo_lib/tests/color_picker.rs new file mode 100644 index 000000000..e151e22e8 --- /dev/null +++ b/crates/egui_demo_lib/tests/color_picker.rs @@ -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"); +} diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 427710629..07b61215a 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -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 = 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(); diff --git a/crates/egui_demo_lib/tests/snapshots/color_picker.png b/crates/egui_demo_lib/tests/snapshots/color_picker.png new file mode 100644 index 000000000..f39bcb23a --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/color_picker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed0f9f3ec94ac3d75266343d11cae5842d72fbea3c78f7995b1b0ad5aa3cdcad +size 141933 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png index aa52b8bda..92726ea05 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Bézier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c777cd6b36219b92c88ebf5987f9239a5750d25be4a87a58841bcf6db259599 -size 32422 +oid sha256:1bb3195aa5eb1fa7da6f6bb52eec745704e10eb82efb68d5f328f9bfa09fc2d4 +size 32538 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index 12b0c24d3..30c75f122 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d941c979def6f30fd227c144d20c7f138f9c06c3338d563ac075c5e17e9b795 -size 26911 +oid sha256:b6351ddb148941f73a182e676a3be4ac5d7165c618324459d0061dbf93f213d9 +size 27023 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index ec34487f1..18c633650 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05527c073b1ee2f6a15052a9097d1ad515331ff32d572cf510086f9ebb7a7bbc -size 27253 +oid sha256:3a4ac42a9b5e44074caa2ec450bd54d834c14182ad8929a5f0567b53bbe5359a +size 27426 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index bd3cd328c..09067c974 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73429ec71d1ae05518352f1a99304f55647dac362092c9cd5618f709b9ba0d11 -size 99950 +oid sha256:611b18575d2768d40b89f28427bd987d90393e886da2a2c4e09ea6503f85acdd +size 100303 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index bacae8846..3464b0467 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79ee28a9c0d8ef80d53560584312a33569f668e0a49f6ad5d277ffef507f2818 -size 25116 +oid sha256:68351659df756ce331a4eeb147be8d1b4fed39dbbb54a0d6f685997b54dc07a7 +size 26327 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index d7a4c8e1e..c1ccf772a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff02fa99b92059d7b4f8286b8a2fd2e27a7f8f005328da97f0b9786aac9bbb17 -size 99316 +oid sha256:f7d190cbc76e61133a6a1d1c800c24fa5de7dba09e3ba89dff9dc5b71ab8e788 +size 100543 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png index c1627ebba..524e55d17 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e2f29678c41e45bedc916dbe845f05c343bfad5528a4143c32cac6c3dee41e7 -size 50512 +oid sha256:a2b67e1e43d55c26ec2dfcf420ef7c5f009e3e5c501780ac59b3f1848728b833 +size 50940 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png index 89e4a647b..6955d9f92 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:345ab9ac3586c3cb9d3b02d46c590a81c7bc31778a3b631874520e57b6694076 -size 22994 +oid sha256:7eacac965912ded40f68e8b3c6d91218e6688c12b383e64fabf0d92ea9c73d9e +size 24039 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index b3c10c6a2..2172cd528 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:353d92d99e9c2350267a43bff7bb9100d3109c82f8d54f9f7a9e3a312ea0c4e6 -size 35378 +oid sha256:eba72ac8696d81552f5106b02437f0b524eb77e61ff1d8014cebce6483eaa784 +size 35474 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index 9a5859bf5..a4b4ccdf6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:218cce13bd96c97aca6a529ba49baaeda5a126cf657e6af1788646cb845725d0 -size 17686 +oid sha256:e9d4349ba73e1d1191191ebd1918ed80e373036e13dee837400194f53b0b9abd +size 17899 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png index c452f78da..4b1488773 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3edbe6debf700364949ef481ba1e8b2319db624ddc316570b6180636dcd88935 -size 57262 +oid sha256:294a43fb1f3117450cde67cb38c4684b90a7b0f69d9c6e7002c0f6d06c70d02c +size 58044 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 16b02f86d..0fe8d6285 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:141271330d4cef517c4ec2c4c2c41306f8b5de386738381a0e4446fa89df7cdf -size 30450 +oid sha256:65d1d15d78c871eb87224c44d2fe889cc0d7a33eb15aeb4852c10bdb02aa730d +size 30877 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 603404a9b..1bf13363f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be289c58296a7656c8550d25ff92b8cd0b03c5c9c527b9ca4a38acda8bbe3ac1 -size 23807 +oid sha256:80091cab95735142ca35100cc74f784aa92812eeaf2e6ef991389de3651b4cfa +size 24045 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 004257863..e55a44cff 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc28830c402d3aa861a220cfc600c3ce2d7b8ca77307691b3f844a34f27afd1f -size 151376 +oid sha256:e7777f85030bad9c28237cdcb77d0835eafd3a1c69603545860cd980c1a2cbbd +size 151843 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 1316fe84e..acdce7292 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca08bb7cd0be0b5a9d10833e5c9b65e889f78a5a8056df69d6b0006c3a228227 -size 66050 +oid sha256:17c16934b22f0439d4a13a78d91a85aec7ff740aeff9f8feb9dda3b059c4d83b +size 67107 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index ad87167a3..e97fe67a2 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97452108d7809775756f1a229644076f5b4c75a6a6d8b8edc082c2448ae35e94 -size 60701 +oid sha256:ccac475bb38e76dc044d34f0a48e73b61e9d4734d02105c5635e02bcdaa19c63 +size 62104 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 83750aba9..c5c4ead81 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d27392f625445e82285c8eaeb532e11dcb02030f24c7a769356cb4c23d7c2067 -size 41516 +oid sha256:7bbfacbbc740729ce8729f4507c6cc155a0dcda51dd4442d3db332cf2f6fd9f1 +size 42790 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index 3a448e1dd..25227f44a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90c4ea93f234aded298ddeeb98e2d6a0566f07efcdaf5c990e8a47a6533db516 -size 446483 +oid sha256:9ac3830efd9c5eb051ed6bf22bd90dd556883b29505096c3901f28f8673b6eba +size 446518 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 0995b1814..70d6b8139 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b069ad508fd3a518df3a39e1740d1786c4af4c3c4a479db04c00f701980efa8 -size 48277 +oid sha256:2718c333123388c8751f461382867b0640ce7a02c220db311c06ff634e6178c1 +size 48620 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 182b7336d..cd460f8a4 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e92e72c651e0ad2c0fc38008d24d9ddf28a70e3b35b26898af78e74028e2c251 -size 44357 +oid sha256:a41a61cce2cda5fb9372324f915ffe2b47f68ead5e26b471ea29dfb5208695a7 +size 44600 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index 7d1519d01..44c5f4d1e 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a5f7bc46c7baf62b559458b3596e386d66bf2281b7c35e5fdb6c5c4a64569f2 -size 44347 +oid sha256:bff8f0531d5cf92e0afe857a04705599d30e77d526091b03296e381b45f02170 +size 44488 diff --git a/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png new file mode 100644 index 000000000..fedbc8f13 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40 +size 353 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index ea32cd32f..be9c23ad9 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c -size 590679 +oid sha256:10d58fa20fe3cc06a98dc79adf6237652b87871d2dc394476ea524ac62f2750b +size 590735 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index 1bff62c8d..41e2ecba3 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d -size 745771 +oid sha256:1de8583bef3fb9e3530b4a09f498360d37d21c099dd44cd45ee99ccc332d7c1c +size 745855 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index 39c2a6b36..c17b4e694 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d -size 973781 +oid sha256:43c1f297b0d3e62483b05efe3eb7089a61944b2e348bc791f0503ccbf6c5ab4a +size 973927 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index fb0da4a80..061cf2451 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827 -size 1081864 +oid sha256:17790adf9f5ba91aa00a22c44303422ed02e71a1ab7b5c1484e13e2a132bd639 +size 1081792 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index 736c3dd67..4b5cbb4d4 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f -size 1131030 +oid sha256:b434961f2ac616e2563aa2ff9e706158190dfcc2713de6fd7740290e603c8a1b +size 1131176 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index b4bf73cb7..06fd5cd91 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b -size 1363589 +oid sha256:de79f0a3dacdf5387faaeeb64f8939ff5912e362d8d649d6e25fd3dbda940325 +size 1363775 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index f2494709e..a3bfe564d 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23 -size 45044 +oid sha256:a62ed81b1693cd7eea2099c0840d82318d27e382f88ca1dd15ba9e2a59e3cf4b +size 46677 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 7254be665..b442e6d3b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8 -size 86830 +oid sha256:d9139bb87beac6bdb91f0e48f5cabba61ca7ba5344a8fcefd094d2efb98e7056 +size 88246 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index e88d67b6b..33acf005a 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6 -size 118889 +oid sha256:5ad40e8b41882e3456569c8cb2e831d33fafa910989674e05b20568798528154 +size 120579 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index a3c23b831..c81ef8fa0 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a -size 51376 +oid sha256:a596437f8274a87525ce107f94d6c8394166c3aa26d06737f1abdc2ae894f77b +size 52790 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index ec1182d15..9b305c524 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128 -size 54582 +oid sha256:001c89ba338b51d48f1e471e17306e84e74b0f952a856b65532ffeacbf52f2cb +size 55982 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index fa5ea442d..48d0dcd74 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8 -size 55053 +oid sha256:bf299c3c52250b1a17411e3e1aef57a4293787c9a9777da6fda01bb6a0e387c1 +size 56466 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 44c159f4f..c04b50283 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c -size 35943 +oid sha256:ea03f6b0e0071910cf2b4279549fb0347cd6a9f8d0334830ce4f34f5824a1ef9 +size 37595 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index b725ec08f..ecb524b23 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24 -size 35925 +oid sha256:db4795c68fffb20f74d46664be9b7c4bfcaf941a8b3e27a7315f9525dd90d5c4 +size 37340 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index 5ea006f1f..6ccb7780e 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed -size 64973 +oid sha256:952df0869c400d8b1f5dc0a33cb7a8be1fa30d91929e57b7c33af0cfbc9cead4 +size 66043 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index c37f49163..025a284fd 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54 -size 151090 +oid sha256:066067e34d47aa8d2631e64b2ea37d94bae77bdfb1af9337f34f724e1ff14cb6 +size 153589 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index 54f2d37fd..d5aa7b0d0 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4 -size 59950 +oid sha256:0d79861c18cbaf2f2ab738d0a5633f354c11a52b8a8e87c623e624ecdfcc6965 +size 60813 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 318fb89e4..005503f34 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9 -size 145940 +oid sha256:cde27ba5614f4bd5553d609b23e6dd78aea113c88443de42fa47aa85929c9455 +size 148235 diff --git a/crates/egui_extras/README.md b/crates/egui_extras/README.md index 4e5e96dce..41c9ece8a 100644 --- a/crates/egui_extras/README.md +++ b/crates/egui_extras/README.md @@ -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`: diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 20dd3e232..1269fb4f6 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d -size 8028 +oid sha256:c32712afee568ce9dca8eeb6bd1dc0f072f9e9b9189ba0bbcd32f11317d73b98 +size 8160 diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 55a13d3e4..89a3733b4 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937 -size 2205 +oid sha256:1933058e8eb0568b6a42be5bcb4aae28a478daa9b62ccb0e72d33623f9161cb1 +size 2246 diff --git a/crates/epaint/src/corner_radius_f32.rs b/crates/epaint/src/corner_radius_f32.rs index ef6e597f5..75210b744 100644 --- a/crates/epaint/src/corner_radius_f32.rs +++ b/crates/epaint/src/corner_radius_f32.rs @@ -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 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 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 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)); + } +} diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index bff5c79a3..c3e7a5b74 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -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, diff --git a/crates/epaint/src/rounded_rect.rs b/crates/epaint/src/rounded_rect.rs new file mode 100644 index 000000000..07de5f1cb --- /dev/null +++ b/crates/epaint/src/rounded_rect.rs @@ -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) -> 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 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) + ); + } +} diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index 251b57b7a..11dd3f1d1 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -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 _) } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a9784f48b..6f3a3698b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -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, rect: Rect, cr: CornerRadiusF32) { + pub fn rounded_rectangle(path: &mut Vec, 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 { diff --git a/examples/popups/Cargo.toml b/examples/popups/Cargo.toml index d1f199918..c0d4384c7 100644 --- a/examples/popups/Cargo.toml +++ b/examples/popups/Cargo.toml @@ -2,6 +2,7 @@ name = "popups" edition.workspace = true license.workspace = true +publish = false rust-version.workspace = true version.workspace = true diff --git a/examples/styling_engine/Cargo.toml b/examples/styling_engine/Cargo.toml new file mode 100644 index 000000000..038b59fd6 --- /dev/null +++ b/examples/styling_engine/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "styling_engine" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +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"] } diff --git a/examples/styling_engine/README.md b/examples/styling_engine/README.md new file mode 100644 index 000000000..43f54f5bb --- /dev/null +++ b/examples/styling_engine/README.md @@ -0,0 +1,10 @@ +Example showing how the style engine work. + +A custom `StyleProvider` 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 +``` + + diff --git a/examples/styling_engine/src/main.rs b/examples/styling_engine/src/main.rs new file mode 100644 index 000000000..54763696a --- /dev/null +++ b/examples/styling_engine/src/main.rs @@ -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 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::(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::(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}")); + }); + }) +} diff --git a/scripts/lint.py b/scripts/lint.py index 0c1319e8a..1d26e5ca1 100755 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -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: diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml index 44a7b9c8f..7e894a2c0 100644 --- a/tests/egui_tests/Cargo.toml +++ b/tests/egui_tests/Cargo.toml @@ -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"] } diff --git a/tests/egui_tests/tests/snapshots/atom_layout_nesting.png b/tests/egui_tests/tests/snapshots/atom_layout_nesting.png index bbff7f706..8042de636 100644 --- a/tests/egui_tests/tests/snapshots/atom_layout_nesting.png +++ b/tests/egui_tests/tests/snapshots/atom_layout_nesting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e43b410ab0a06b40da5ad9c7fd105d11201d44a3afd63fd213429cc39013c0d4 -size 6964 +oid sha256:da19989c02e0dc6c558575c7ab0f5e51b2747b2d65d69f6883636d039ed54d2a +size 7335 diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png index 464daa5de..de7da072b 100644 --- a/tests/egui_tests/tests/snapshots/layout/button.png +++ b/tests/egui_tests/tests/snapshots/layout/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bafb447c177f2bd94980c1d146a2826ecf62070fb6ee18e63e6a925e2741210 -size 314478 +oid sha256:c9df24e02feeac296cf03f42b7a5343f943ee1dd00508c5b6da2563928ec228b +size 324343 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index ce2844f8f..1a4d273b0 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eadf74cc7f7e10be804df0e7df65c232ecaed7d77c2c89197548ab7206706843 -size 351669 +oid sha256:106d9e06bbcae5db4e0a5813df48899f16c3ac131c9a44e45b45253bede781d1 +size 361653 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index 3a8d09f95..807420053 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60739a18972a5e866610ae2c8f5af719a1bbaeffcdd8c6319e783c37e28d3107 -size 435108 +oid sha256:83f6000e0ff5705df6012036da6337d589baeb6652d1b554213ab5948c505040 +size 445361 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png index 3a2676aba..175e97d95 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fb6db99b4dfa1b0c6f97799ffd72c264c02eefd803eee5737075231760c80bd -size 417925 +oid sha256:4bcbf331a0d18b1233689132d149e4766e8db2827bafce50696d73195d83fc02 +size 426870 diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png index 105cd3453..9044959b1 100644 --- a/tests/egui_tests/tests/snapshots/layout/drag_value.png +++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f54f444c25fbd1def8d0f11d03e1fd8bde6745a2593d0029e346cbaafa584ed3 -size 264007 +oid sha256:4372664983d9ae7e9889610349f86f025e0c9fccccaaa20dd4735663baede051 +size 274014 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png index a52c4de9a..96369bc4c 100644 --- a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97c2f56dad8d6aa45385901f9b23a13c49c21fc5aa811ab50dcf7c8451b177dd -size 401596 +oid sha256:45f1b8f5b31f925b7ceb0552c67bcf2df6e43dfba1980381d3ae677a4b713b5c +size 412451 diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png index 306d2a97f..bee3b9c16 100644 --- a/tests/egui_tests/tests/snapshots/layout/slider.png +++ b/tests/egui_tests/tests/snapshots/layout/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:619139eb166d383cc9b35653c8c4a97c55cd21b171a24e253d923558cc0d35e1 -size 275512 +oid sha256:d9395e6534bd5d1ddcdd2be8c446bf85b7c3b65dc7a0df7c018db68d2a563a6f +size 312206 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png index 8adc7676b..9a0048ea3 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01795b279efc5c680d77817f7d0e6e9f18dc3969ec964d8d3419d0175b0f4d87 -size 512262 +oid sha256:82a7349d6e7ac45656b38c5e96a49f7d1c84b14ea14b1f548420040d618c1c3e +size 515130 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png index bb3745c59..5957cf916 100644 --- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbe8eda2e6162220ff8baa6e84156ea18f0bb15ffc46ebf9d60b6d3eeb8861d4 -size 9016 +oid sha256:38d8a73f05854a07e233e0b5b95baf591e82d7941133443cbba14fd6e78aeb8b +size 9170 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png index 8a690c1a5..322b14bf2 100644 --- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:547316973b563bd92841f428838848323d656688cd03e8babcbc24626a389419 -size 7195 +oid sha256:776b759db9ca940c3a9c716c35e64a8e6d12ad8bdd2eadaf8527346cd2edc354 +size 7349 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png index 43e51aa02..2b014293a 100644 --- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b18774a40c1c1065645be7b59c90e65bb6fb40d4931ab37ccf40126e2767b709 -size 9019 +oid sha256:cb1950b37981750c1a391d6faa54954e6255608ec9ec30db6c76ec9561a20614 +size 9173 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png index 3c92145ab..2f88ac5b2 100644 --- a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e9ffcb9f14f86155a4a8279d694ae3e43c6bb8df59fa681673199c01ff68f96 -size 7206 +oid sha256:119252d84d2315e069a11a7ce40b4c699f86f44fdeec5e5fab2c4e8bfe1d8b20 +size 7359 diff --git a/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png index 9f489aa81..e8d9d654b 100644 --- a/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png +++ b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59bef0e3593896985988c03171c710d8ed31ed8bb89c7a3f63c060c573e4fb74 -size 6510 +oid sha256:ad01abdc39b9432aea6f46fca7517e78ad45d95165a2cfb4159a198e3de40667 +size 6566 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png index 66ad3f4a4..89de59f1d 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e74e32fb12decf36c8685274226c29c4e748b2bf276bf6b47ba19ac9a3def66a -size 2290 +oid sha256:806f3f69228e62d9d0838fb27a02f46cae29efffee54d8182ecbfd2c12f405ba +size 2413 diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png index 2f81509e6..31c19aede 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_halign.png +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48114ad6d116fb9288ce9fe3b173017bda317f69753e7ac03a090b8d02d6cb4d -size 13258 +oid sha256:b66afca19d0a0a6fac4737816443fbd481635897807689dbd75321360ffa079d +size 13803 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png index 796a1e3b5..b4338fa85 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa -size 2296 +oid sha256:20afbde3e1edc7a599936f83188fb1a2c35d2d1344ce7ad8e115b937266481fc +size 2547 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png index cd6d5a621..d270bf1ca 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c -size 2982 +oid sha256:60f3988b6b0758fa7c2e97ab2bba282b45eee7b6092e4dafc68f692865aeafa7 +size 3211 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png index cd6d5a621..d270bf1ca 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c -size 2982 +oid sha256:60f3988b6b0758fa7c2e97ab2bba282b45eee7b6092e4dafc68f692865aeafa7 +size 3211 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png index b9e0dea1a..a2a4b086b 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7db4a0f33da131ca69771fef974689680c802187cdd8e835131d70457aca775c -size 1639 +oid sha256:b464510b3423f92637c9ce502187c55017288ed7a6265e2a577e20cbd40ee448 +size 1703 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png index 4978b5a3b..095667ea3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button.png +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2eb21d7b99171d249127377698225f0b2e103733864f6809785dfba169e133bf -size 11213 +oid sha256:f8ed8c3114cc34e8415ed5ebed12d54d8790a0b1dfd38cbe8d8d52cef9cf817a +size 11582 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index 9d3669597..4bfa690b0 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3322b616a292f685f6051d2bf9be0df28ca7472e8fc3c982cd094ca44dc47ff8 -size 12117 +oid sha256:e918acf970083110d9bf2e5fe5fa8570c1d17861117375b3c78ff32988c4e5df +size 12600 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 41d88992d..b4b1a8586 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2021446525a7b1090aaa6b3f12fdd9295c67cde00de61c7f0d36838b6dfc2593 -size 15148 +oid sha256:dac827ce7a3c829acb789d6b87006ec58929ce72efbf552b595266b220ca5960 +size 15524 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 3bb097aeb..f3ad0c0d1 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e341f6d3fb3d9d09bd573584f68e4ada4e9af0dcb27cf1bc164797f62865c398 -size 14734 +oid sha256:9b7961331cf4842ea955e1e62b3ab74de0e589441d0830c45d0285080ea67d03 +size 15169 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox.png b/tests/egui_tests/tests/snapshots/visuals/checkbox.png index e45ea9fb3..b26d2e611 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fd43ec4633a59798d2077d9fd3ad8292d5da66678a2d75d33c957a062d7848a -size 13218 +oid sha256:0885a39324b015b92761d09fc1a6694d11d4b1fa9dfb23e05273479ed82a7b09 +size 13624 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png index 31f84dbeb..c5fe38c6c 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e2252dbe6b26cc6b484d09c57a47f4ecd0d75df88f2279a7dd95ca43b597e9d -size 14282 +oid sha256:6572c4c44e12f49152cc2dfcae39140ffe69a0a1172d6aca540f10d5ba9b609e +size 14633 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 88fafe442..90be73b8d 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e19af6fe25539c5624a27e20fe78f8d9076bb0c11decf916ecc6434e321b87e -size 9054 +oid sha256:76be7b08da637ecbf898efd8322615d5e2f1cb1757665e5486db86ccb7fb3438 +size 9462 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png index bb2b2a32b..90c0e8010 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b42c8d774d1ecb50f5bde1aa358a37cbb669b59d934786fb1cc4c6f0c3400701 -size 13711 +oid sha256:a05d5b941f2681f3466f0caa19dd6a71099fc897ba5eb7ac760e6ed6ba92d4be +size 13971 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png index 87163dd91..cc73e5f92 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7037f694fef1211f2ea9d8e7e60836d8eb97c9242c7b396efeb8390eaf0852f1 -size 13730 +oid sha256:568e1c2c0d28bcf5507d97562bedf12d32e99f78f75b3ac70f937115ea97a3ed +size 14141 diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png index 8b758e108..2cf499527 100644 --- a/tests/egui_tests/tests/snapshots/visuals/slider.png +++ b/tests/egui_tests/tests/snapshots/visuals/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d09706e0dbbbf6d07e39470285dc8bcd1b3c6d09bedd91cd20d86c96dba1131 -size 9911 +oid sha256:3e4b3d8ad1d40ca0d4c8f6a547cc470239b65ffd7de59fdb413fcf39934d9bd9 +size 11078 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 2cd2c737a..c099f7ee3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf723b58802e9cf5aefb0a06689ed2bfaa45afff9f0fc48d37e742244da097e6 -size 8149 +oid sha256:01afe50f7bc1177c7beaff73d10b63fbf6ff73ad621e76d9da05ecc3f5c362c7 +size 8614 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png index 666364d68..be087fdb5 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e900a4004a1fc808bf74bef7b1ece7c98e16582c40155e222c04f759d229fc70 -size 10125 +oid sha256:5b4e19e088cacfc0524aad0e4cba32f701e57f3d9fd27bfc2b285949315c07a9 +size 10577 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png index fff21ac13..b06736cc1 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2119fd3aa9aefe8198e7b79968f7fafc4aa93294ba4c2577dc55929302067996 -size 21827 +oid sha256:5be608caad5e27606dada8f99a2e95bced62b865d8274463b76224299c8abc7e +size 22257 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png index 0fb7a4708..b3cd53d18 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02a333af5e58c53a89d436c4540579d3ea0c1e99c1cad4c019db1da34ff9de10 -size 8840 +oid sha256:0e6eb347e0622502e2fab2e06aee376d1b41338d504c136b9dfeae6de0465ddf +size 9234 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png index e00868b1f..f67caa61b 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a37ed30425967301ffa7dda3fdc8f316dfd7f4665c731b17778e3e5942783e81 -size 10534 +oid sha256:51949a5a32c81c03dcb93d7ea02780abce5acde7a627681562470ee4f3f932fb +size 10993 diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index 6368de3a8..b6cd6cda8 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -1,5 +1,7 @@ use egui::{ - Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui, Vec2, + Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui, + Vec2, + widget_style::{ButtonStyle, StyleArgs}, }; use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults}; @@ -129,11 +131,14 @@ fn test_atom_layout_nesting_and_direction() { let style = ui.style(); let canvas_frame = Frame::canvas(style); - let button_frame = style - .button_style( - &egui::widget_style::Classes::default(), - egui::widget_style::WidgetState::Inactive, - ) + let button_frame = ui + .get_widget_style::(&StyleArgs { + classes: &egui::widget_style::Classes::default(), + state: egui::widget_style::WidgetState::Inactive, + ctx: ui, + stack: ui.stack(), + style, + }) .frame; let row = |direction: Direction| {