Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: chrome rect pipeline, PaintPrim lowering

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 7, 2026, 7:17 PM +0300) commit 5f462098 parent 2af84552 change-id yzswrnzp
+474 -120
+4 -1
crates/bone-render/src/lib.rs
··· 11 11 pub use diff::{PixelDiff, PixelDiffError, PixelDiffReport, PixelDiffThreshold, PixelMismatch}; 12 12 pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext}; 13 13 pub use pick::{EntityKindTag, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker}; 14 - pub use pipelines::{ArcPipeline, GlyphPipeline, GridPipeline, LinesPipeline, TextPipeline}; 14 + pub use pipelines::{ 15 + ArcPipeline, ChromeInstance, ChromePipeline, GlyphPipeline, GridPipeline, LinesPipeline, 16 + TextPipeline, 17 + }; 15 18 pub use scene::{ 16 19 RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, ScenePoint, 17 20 SceneRelationGlyph, SketchScene,
+214
crates/bone-render/src/pipelines/chrome.rs
··· 1 + use crate::gpu::Gpu; 2 + 3 + #[repr(C, align(16))] 4 + #[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] 5 + pub struct ChromeInstance { 6 + pub rect_xywh_px: [f32; 4], 7 + pub fill_premul_rgba: [f32; 4], 8 + pub border_premul_rgba: [f32; 4], 9 + pub thickness_radius_px: [f32; 2], 10 + pub(crate) pad: [f32; 2], 11 + } 12 + 13 + impl ChromeInstance { 14 + #[must_use] 15 + pub const fn new( 16 + rect_xywh_px: [f32; 4], 17 + fill_premul_rgba: [f32; 4], 18 + border_premul_rgba: [f32; 4], 19 + thickness_radius_px: [f32; 2], 20 + ) -> Self { 21 + Self { 22 + rect_xywh_px, 23 + fill_premul_rgba, 24 + border_premul_rgba, 25 + thickness_radius_px, 26 + pad: [0.0, 0.0], 27 + } 28 + } 29 + } 30 + 31 + const INSTANCE_STRIDE: u64 = core::mem::size_of::<ChromeInstance>() as u64; 32 + const INITIAL_INSTANCE_CAP: u64 = 256; 33 + 34 + #[repr(C, align(16))] 35 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 36 + struct ChromeFrame { 37 + viewport_px: [f32; 2], 38 + pad: [f32; 2], 39 + } 40 + 41 + const FRAME_SIZE: u64 = core::mem::size_of::<ChromeFrame>() as u64; 42 + 43 + pub struct ChromePipeline { 44 + device: wgpu::Device, 45 + queue: wgpu::Queue, 46 + pipeline: wgpu::RenderPipeline, 47 + uniform_buffer: wgpu::Buffer, 48 + bind_group: wgpu::BindGroup, 49 + instance_buffer: wgpu::Buffer, 50 + instance_capacity: u64, 51 + } 52 + 53 + impl ChromePipeline { 54 + #[must_use] 55 + pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self { 56 + let device = gpu.device().clone(); 57 + let queue = gpu.queue().clone(); 58 + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 59 + label: Some("bone-render:chrome-shader"), 60 + source: wgpu::ShaderSource::Wgsl(include_str!("chrome.wgsl").into()), 61 + }); 62 + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 63 + label: Some("bone-render:chrome-bgl"), 64 + entries: &[wgpu::BindGroupLayoutEntry { 65 + binding: 0, 66 + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 67 + ty: wgpu::BindingType::Buffer { 68 + ty: wgpu::BufferBindingType::Uniform, 69 + has_dynamic_offset: false, 70 + min_binding_size: wgpu::BufferSize::new(FRAME_SIZE), 71 + }, 72 + count: None, 73 + }], 74 + }); 75 + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 76 + label: Some("bone-render:chrome-layout"), 77 + bind_group_layouts: &[Some(&bind_group_layout)], 78 + immediate_size: 0, 79 + }); 80 + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 81 + label: Some("bone-render:chrome-pipeline"), 82 + layout: Some(&pipeline_layout), 83 + vertex: wgpu::VertexState { 84 + module: &shader, 85 + entry_point: Some("vs"), 86 + compilation_options: wgpu::PipelineCompilationOptions::default(), 87 + buffers: &[wgpu::VertexBufferLayout { 88 + array_stride: INSTANCE_STRIDE, 89 + step_mode: wgpu::VertexStepMode::Instance, 90 + attributes: &INSTANCE_ATTRS, 91 + }], 92 + }, 93 + fragment: Some(wgpu::FragmentState { 94 + module: &shader, 95 + entry_point: Some("fs"), 96 + compilation_options: wgpu::PipelineCompilationOptions::default(), 97 + targets: &[Some(wgpu::ColorTargetState { 98 + format: color_format, 99 + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 100 + write_mask: wgpu::ColorWrites::ALL, 101 + })], 102 + }), 103 + primitive: wgpu::PrimitiveState { 104 + topology: wgpu::PrimitiveTopology::TriangleList, 105 + strip_index_format: None, 106 + front_face: wgpu::FrontFace::Ccw, 107 + cull_mode: None, 108 + polygon_mode: wgpu::PolygonMode::Fill, 109 + conservative: false, 110 + unclipped_depth: false, 111 + }, 112 + depth_stencil: None, 113 + multisample: wgpu::MultisampleState::default(), 114 + multiview_mask: None, 115 + cache: None, 116 + }); 117 + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 118 + label: Some("bone-render:chrome-uniform"), 119 + size: FRAME_SIZE, 120 + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 121 + mapped_at_creation: false, 122 + }); 123 + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 124 + label: Some("bone-render:chrome-bg"), 125 + layout: &bind_group_layout, 126 + entries: &[wgpu::BindGroupEntry { 127 + binding: 0, 128 + resource: uniform_buffer.as_entire_binding(), 129 + }], 130 + }); 131 + let instance_buffer = create_instance_buffer(&device, INITIAL_INSTANCE_CAP); 132 + Self { 133 + device, 134 + queue, 135 + pipeline, 136 + uniform_buffer, 137 + bind_group, 138 + instance_buffer, 139 + instance_capacity: INITIAL_INSTANCE_CAP, 140 + } 141 + } 142 + 143 + pub fn draw( 144 + &mut self, 145 + encoder: &mut wgpu::CommandEncoder, 146 + color_view: &wgpu::TextureView, 147 + viewport_px: [f32; 2], 148 + instances: &[ChromeInstance], 149 + ) { 150 + if instances.is_empty() { 151 + return; 152 + } 153 + let frame = ChromeFrame { 154 + viewport_px, 155 + pad: [0.0, 0.0], 156 + }; 157 + self.queue 158 + .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&frame)); 159 + let needed = instances.len() as u64; 160 + if needed > self.instance_capacity { 161 + let new_cap = needed.next_power_of_two().max(self.instance_capacity * 2); 162 + self.instance_buffer = create_instance_buffer(&self.device, new_cap); 163 + self.instance_capacity = new_cap; 164 + } 165 + self.queue 166 + .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances)); 167 + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 168 + label: Some("bone-render:chrome-pass"), 169 + color_attachments: &[Some(wgpu::RenderPassColorAttachment { 170 + view: color_view, 171 + resolve_target: None, 172 + depth_slice: None, 173 + ops: wgpu::Operations { 174 + load: wgpu::LoadOp::Load, 175 + store: wgpu::StoreOp::Store, 176 + }, 177 + })], 178 + depth_stencil_attachment: None, 179 + timestamp_writes: None, 180 + occlusion_query_set: None, 181 + multiview_mask: None, 182 + }); 183 + pass.set_pipeline(&self.pipeline); 184 + pass.set_bind_group(0, &self.bind_group, &[]); 185 + let used_bytes = needed * INSTANCE_STRIDE; 186 + pass.set_vertex_buffer(0, self.instance_buffer.slice(0..used_bytes)); 187 + let Ok(count) = u32::try_from(needed) else { 188 + panic!("chrome instance count {needed} exceeds u32::MAX"); 189 + }; 190 + pass.draw(0..6, 0..count); 191 + } 192 + } 193 + 194 + fn create_instance_buffer(device: &wgpu::Device, capacity: u64) -> wgpu::Buffer { 195 + device.create_buffer(&wgpu::BufferDescriptor { 196 + label: Some("bone-render:chrome-instances"), 197 + size: capacity * INSTANCE_STRIDE, 198 + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 199 + mapped_at_creation: false, 200 + }) 201 + } 202 + 203 + impl core::fmt::Debug for ChromePipeline { 204 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 205 + f.debug_struct("ChromePipeline").finish_non_exhaustive() 206 + } 207 + } 208 + 209 + const INSTANCE_ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ 210 + 0 => Float32x4, 211 + 1 => Float32x4, 212 + 2 => Float32x4, 213 + 3 => Float32x2, 214 + ];
+80
crates/bone-render/src/pipelines/chrome.wgsl
··· 1 + struct Frame { 2 + viewport_px: vec2<f32>, 3 + _pad: vec2<f32>, 4 + }; 5 + 6 + @group(0) @binding(0) var<uniform> u: Frame; 7 + 8 + struct VsOut { 9 + @builtin(position) clip: vec4<f32>, 10 + @location(0) local_px: vec2<f32>, 11 + @location(1) half_px: vec2<f32>, 12 + @location(2) fill: vec4<f32>, 13 + @location(3) border: vec4<f32>, 14 + @location(4) border_thickness_px: f32, 15 + @location(5) corner_radius_px: f32, 16 + }; 17 + 18 + @vertex 19 + fn vs( 20 + @builtin(vertex_index) vid: u32, 21 + @location(0) rect_xywh_px: vec4<f32>, 22 + @location(1) fill: vec4<f32>, 23 + @location(2) border: vec4<f32>, 24 + @location(3) thickness_radius: vec2<f32>, 25 + ) -> VsOut { 26 + var corners = array<vec2<f32>, 6>( 27 + vec2<f32>(0.0, 0.0), 28 + vec2<f32>(1.0, 0.0), 29 + vec2<f32>(0.0, 1.0), 30 + vec2<f32>(1.0, 0.0), 31 + vec2<f32>(1.0, 1.0), 32 + vec2<f32>(0.0, 1.0), 33 + ); 34 + let c = corners[vid]; 35 + let pos_px = rect_xywh_px.xy + c * rect_xywh_px.zw; 36 + let half_px = rect_xywh_px.zw * 0.5; 37 + let center_px = rect_xywh_px.xy + half_px; 38 + let local_px = pos_px - center_px; 39 + let ndc = vec2<f32>( 40 + (pos_px.x / u.viewport_px.x) * 2.0 - 1.0, 41 + 1.0 - (pos_px.y / u.viewport_px.y) * 2.0, 42 + ); 43 + var out: VsOut; 44 + out.clip = vec4<f32>(ndc, 0.0, 1.0); 45 + out.local_px = local_px; 46 + out.half_px = half_px; 47 + out.fill = fill; 48 + out.border = border; 49 + out.border_thickness_px = thickness_radius.x; 50 + out.corner_radius_px = thickness_radius.y; 51 + return out; 52 + } 53 + 54 + fn sd_rounded_rect(p: vec2<f32>, half: vec2<f32>, r: f32) -> f32 { 55 + let smaller_half = min(half.x, half.y); 56 + let r_clamped = clamp(r, 0.0, smaller_half); 57 + let q = abs(p) - half + vec2<f32>(r_clamped, r_clamped); 58 + let outside = length(max(q, vec2<f32>(0.0, 0.0))); 59 + let inside = min(max(q.x, q.y), 0.0); 60 + return outside + inside - r_clamped; 61 + } 62 + 63 + @fragment 64 + fn fs(in: VsOut) -> @location(0) vec4<f32> { 65 + let aa = 0.5; 66 + let sd_outer = sd_rounded_rect(in.local_px, in.half_px, in.corner_radius_px); 67 + let cov_outer = clamp(0.5 - sd_outer / aa, 0.0, 1.0); 68 + if in.border_thickness_px <= 0.0 { 69 + return in.fill * cov_outer; 70 + } 71 + let inner_half = max(in.half_px - vec2<f32>(in.border_thickness_px, in.border_thickness_px), vec2<f32>(0.0, 0.0)); 72 + let inner_radius = max(in.corner_radius_px - in.border_thickness_px, 0.0); 73 + let sd_inner = sd_rounded_rect(in.local_px, inner_half, inner_radius); 74 + let cov_inner = clamp(0.5 - sd_inner / aa, 0.0, 1.0); 75 + let cov_border = max(cov_outer - cov_inner, 0.0); 76 + let fill_premul = in.fill * cov_inner; 77 + let border_premul = in.border * cov_border; 78 + let inv_border_alpha = 1.0 - border_premul.a; 79 + return border_premul + fill_premul * inv_border_alpha; 80 + }
+2
crates/bone-render/src/pipelines/mod.rs
··· 1 1 pub mod arc; 2 + pub mod chrome; 2 3 pub mod glyph; 3 4 pub mod grid; 4 5 pub mod lines; 5 6 pub mod text; 6 7 7 8 pub use arc::ArcPipeline; 9 + pub use chrome::{ChromeInstance, ChromePipeline}; 8 10 pub use glyph::GlyphPipeline; 9 11 pub use grid::GridPipeline; 10 12 pub use lines::LinesPipeline;
+19 -13
crates/bone-ui/src/a11y.rs
··· 7 7 use crate::layout::LayoutRect; 8 8 use crate::strings::{StringKey, StringTable}; 9 9 use crate::widget_id::WidgetId; 10 + use crate::widgets::LabelText; 10 11 11 12 #[derive(Copy, Clone, Debug, Default, PartialEq)] 12 13 pub struct AccessState { ··· 24 25 pub step: f64, 25 26 } 26 27 27 - #[derive(Copy, Clone, Debug, PartialEq)] 28 + #[derive(Clone, Debug, PartialEq)] 28 29 pub struct AccessNode { 29 30 pub role: Role, 30 - pub label: Option<StringKey>, 31 + pub label: Option<LabelText>, 31 32 pub description: Option<StringKey>, 32 33 pub state: AccessState, 33 34 pub range: Option<AccessRange>, ··· 51 52 } 52 53 53 54 #[must_use] 54 - pub const fn with_label(self, key: StringKey) -> Self { 55 + pub fn with_label(self, key: StringKey) -> Self { 56 + self.with_label_text(LabelText::Key(key)) 57 + } 58 + 59 + #[must_use] 60 + pub fn with_label_text(self, label: LabelText) -> Self { 55 61 Self { 56 - label: Some(key), 62 + label: Some(label), 57 63 ..self 58 64 } 59 65 } 60 66 61 67 #[must_use] 62 - pub const fn with_description(self, key: StringKey) -> Self { 68 + pub fn with_description(self, key: StringKey) -> Self { 63 69 Self { 64 70 description: Some(key), 65 71 ..self ··· 67 73 } 68 74 69 75 #[must_use] 70 - pub const fn with_disabled(self, disabled: bool) -> Self { 76 + pub fn with_disabled(self, disabled: bool) -> Self { 71 77 Self { 72 78 state: AccessState { 73 79 disabled, ··· 78 84 } 79 85 80 86 #[must_use] 81 - pub const fn with_selected(self, selected: bool) -> Self { 87 + pub fn with_selected(self, selected: bool) -> Self { 82 88 Self { 83 89 state: AccessState { 84 90 selected: Some(selected), ··· 89 95 } 90 96 91 97 #[must_use] 92 - pub const fn with_expanded(self, expanded: bool) -> Self { 98 + pub fn with_expanded(self, expanded: bool) -> Self { 93 99 Self { 94 100 state: AccessState { 95 101 expanded: Some(expanded), ··· 100 106 } 101 107 102 108 #[must_use] 103 - pub const fn with_toggled(self, toggled: Toggled) -> Self { 109 + pub fn with_toggled(self, toggled: Toggled) -> Self { 104 110 Self { 105 111 state: AccessState { 106 112 toggled: Some(toggled), ··· 111 117 } 112 118 113 119 #[must_use] 114 - pub const fn with_range(self, range: AccessRange) -> Self { 120 + pub fn with_range(self, range: AccessRange) -> Self { 115 121 Self { 116 122 range: Some(range), 117 123 ..self ··· 201 207 fn build_node(strings: &StringTable, entry: &AccessEntry) -> Node { 202 208 let mut node = Node::new(entry.node.role); 203 209 node.set_bounds(rect_to_accesskit(entry.rect)); 204 - if let Some(key) = entry.node.label { 205 - node.set_label(strings.resolve(key)); 210 + if let Some(label) = &entry.node.label { 211 + node.set_label(label.resolve(strings)); 206 212 } 207 213 if let Some(key) = entry.node.description { 208 214 node.set_description(strings.resolve(key)); ··· 275 281 fn push_panics_on_duplicate_id_in_debug() { 276 282 let mut builder = AccessTreeBuilder::new(); 277 283 let node = AccessNode::new(Role::Button).with_label(LABEL); 278 - builder.push(id("a"), rect(), node); 284 + builder.push(id("a"), rect(), node.clone()); 279 285 builder.push(id("a"), rect(), node); 280 286 } 281 287
+6 -6
crates/bone-ui/src/frame.rs
··· 10 10 use crate::theme::Theme; 11 11 use crate::widget_id::WidgetId; 12 12 13 - #[derive(Copy, Clone, Debug, PartialEq)] 13 + #[derive(Clone, Debug, PartialEq)] 14 14 pub struct InteractDeclaration { 15 15 pub id: WidgetId, 16 16 pub rect: LayoutRect, ··· 38 38 } 39 39 40 40 #[must_use] 41 - pub const fn at_z(self, z: ZLayer) -> Self { 41 + pub fn at_z(self, z: ZLayer) -> Self { 42 42 Self { z, ..self } 43 43 } 44 44 45 45 #[must_use] 46 - pub const fn disabled(self, disabled: bool) -> Self { 46 + pub fn disabled(self, disabled: bool) -> Self { 47 47 Self { disabled, ..self } 48 48 } 49 49 50 50 #[must_use] 51 - pub const fn focusable(self, focusable: bool) -> Self { 51 + pub fn focusable(self, focusable: bool) -> Self { 52 52 Self { focusable, ..self } 53 53 } 54 54 55 55 #[must_use] 56 - pub const fn active(self, active: bool) -> Self { 56 + pub fn active(self, active: bool) -> Self { 57 57 Self { active, ..self } 58 58 } 59 59 60 60 #[must_use] 61 - pub const fn a11y(self, node: AccessNode) -> Self { 61 + pub fn a11y(self, node: AccessNode) -> Self { 62 62 Self { 63 63 a11y: Some(node), 64 64 ..self
+10 -96
crates/bone-ui/src/raster.rs
··· 1 1 use std::io::Cursor; 2 2 3 3 use crate::layout::{LayoutPx, LayoutRect}; 4 - use crate::theme::{Color, ElevationLevel, SurfaceLevel, Theme}; 5 - use crate::widgets::WidgetPaint; 4 + use crate::theme::{Color, SurfaceLevel, Theme}; 5 + use crate::widgets::{PaintPrim, WidgetPaint, lower_paint}; 6 6 7 7 #[derive(Debug, thiserror::Error)] 8 8 pub enum PngError { ··· 247 247 } 248 248 249 249 fn draw(canvas: &mut Canvas, paint: &WidgetPaint, theme: &Theme) { 250 - match paint { 251 - WidgetPaint::Surface { 252 - rect, 253 - fill, 254 - border, 255 - .. 256 - } => { 257 - canvas.fill_rect(*rect, *fill); 258 - if let Some(b) = border { 259 - canvas.outline_rect(*rect, b.color, b.width.value_px()); 260 - } 261 - } 262 - WidgetPaint::Label { rect, color, .. } => { 263 - canvas.fill_rect(label_bar(*rect), color.with_alpha(0.55 * color.alpha())); 264 - } 265 - WidgetPaint::Mark { rect, color, .. } => { 266 - canvas.fill_rect(centered_square(*rect, 0.55), *color); 267 - } 268 - WidgetPaint::FocusRing { 269 - rect, 270 - color, 271 - thickness, 272 - .. 273 - } => { 274 - canvas.outline_rect(*rect, *color, thickness.value_px()); 275 - } 276 - WidgetPaint::SelectionHighlight { rect, color, .. } => { 277 - canvas.fill_rect(*rect, color.with_alpha(0.35 * color.alpha())); 278 - } 279 - WidgetPaint::Caret { rect, color, .. } => { 280 - canvas.fill_rect(caret_band(*rect), *color); 281 - } 282 - WidgetPaint::Tooltip { 283 - rect, elevation, .. 284 - } => { 285 - draw_elevation(canvas, theme, *rect, *elevation); 286 - } 287 - } 250 + let prim = lower_paint(theme, paint); 251 + draw_prim(canvas, &prim); 288 252 } 289 253 290 - fn draw_elevation(canvas: &mut Canvas, theme: &Theme, rect: LayoutRect, elevation: ElevationLevel) { 291 - canvas.fill_rect(rect, theme.colors.surface(elevation.surface)); 292 - if let Some(b) = elevation.border { 293 - canvas.outline_rect(rect, b.color, b.width.value_px()); 254 + fn draw_prim(canvas: &mut Canvas, prim: &PaintPrim) { 255 + if prim.fill.alpha() > 0.0 { 256 + canvas.fill_rect(prim.rect, prim.fill); 294 257 } 295 - } 296 - 297 - fn label_bar(rect: LayoutRect) -> LayoutRect { 298 - use crate::layout::{LayoutPos, LayoutSize}; 299 - let height = (rect.size.height.value() * 0.4).max(2.0); 300 - let pad = (rect.size.height.value() - height) * 0.5; 301 - let inset_x = (rect.size.width.value() * 0.1).min(8.0); 302 - LayoutRect::new( 303 - LayoutPos::new( 304 - LayoutPx::saturating(rect.origin.x.value() + inset_x), 305 - LayoutPx::saturating(rect.origin.y.value() + pad), 306 - ), 307 - LayoutSize::new( 308 - LayoutPx::saturating_nonneg(rect.size.width.value() - 2.0 * inset_x), 309 - LayoutPx::saturating_nonneg(height), 310 - ), 311 - ) 312 - } 313 - 314 - fn centered_square(rect: LayoutRect, factor: f32) -> LayoutRect { 315 - use crate::layout::{LayoutPos, LayoutSize}; 316 - let side = rect 317 - .size 318 - .width 319 - .value() 320 - .min(rect.size.height.value()) 321 - .max(0.0) 322 - * factor; 323 - let cx = rect.origin.x.value() + 0.5 * rect.size.width.value(); 324 - let cy = rect.origin.y.value() + 0.5 * rect.size.height.value(); 325 - LayoutRect::new( 326 - LayoutPos::new( 327 - LayoutPx::saturating(cx - 0.5 * side), 328 - LayoutPx::saturating(cy - 0.5 * side), 329 - ), 330 - LayoutSize::new( 331 - LayoutPx::saturating_nonneg(side), 332 - LayoutPx::saturating_nonneg(side), 333 - ), 334 - ) 335 - } 336 - 337 - fn caret_band(rect: LayoutRect) -> LayoutRect { 338 - use crate::layout::{LayoutPos, LayoutSize}; 339 - let width = rect.size.width.value().max(1.0); 340 - LayoutRect::new( 341 - LayoutPos::new(rect.origin.x, rect.origin.y), 342 - LayoutSize::new( 343 - LayoutPx::saturating_nonneg(width), 344 - rect.size.height, 345 - ), 346 - ) 258 + if let Some(b) = prim.border { 259 + canvas.outline_rect(prim.rect, b.color, b.width.value_px()); 260 + } 347 261 } 348 262 349 263 pub fn encode_png(rgba: &[u8], size: CanvasSize) -> Result<Vec<u8>, PngError> {
+3 -1
crates/bone-ui/src/widgets/mod.rs
··· 49 49 MenuResponse, MenuState, show_context_menu, show_menu, show_menu_bar, 50 50 }; 51 51 pub use numeric_input::{NumericFloatParseError, NumericInput, NumericInputResponse}; 52 - pub use paint::{ButtonPaintKind, GlyphMark, LabelText, SelectionByteRange, WidgetPaint}; 52 + pub use paint::{ 53 + ButtonPaintKind, GlyphMark, LabelText, PaintPrim, SelectionByteRange, WidgetPaint, lower_paint, 54 + }; 53 55 pub use panel::{Panel, PanelResponse, PanelState, PanelTitlebar, PanelVariant, show_panel}; 54 56 pub use parsed_input::{ParsedInput, ParsedInputResponse, ParsedValue, show_parsed_input}; 55 57 pub use property_grid::{
+136 -3
crates/bone-ui/src/widgets/paint.rs
··· 1 1 use serde::Serialize; 2 2 3 - use crate::layout::LayoutRect; 4 - use crate::strings::StringKey; 3 + use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 4 + use crate::strings::{StringKey, StringTable}; 5 5 use crate::text::SourceByteIndex; 6 - use crate::theme::{Border, Color, ElevationLevel, Radius, Spacing, TypographyRole}; 6 + use crate::theme::{ 7 + Border, Color, ElevationLevel, Radius, Spacing, StrokeWidth, Theme, TypographyRole, 8 + }; 7 9 use crate::widget_id::WidgetId; 8 10 9 11 #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] ··· 37 39 pub enum LabelText { 38 40 Key(StringKey), 39 41 Owned(String), 42 + } 43 + 44 + impl LabelText { 45 + #[must_use] 46 + pub fn resolve<'a>(&'a self, strings: &'a StringTable) -> &'a str { 47 + match self { 48 + Self::Key(key) => strings.resolve(*key), 49 + Self::Owned(s) => s.as_str(), 50 + } 51 + } 40 52 } 41 53 42 54 #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] ··· 95 107 elevation: ElevationLevel, 96 108 }, 97 109 } 110 + 111 + #[derive(Copy, Clone, Debug, PartialEq)] 112 + pub struct PaintPrim { 113 + pub rect: LayoutRect, 114 + pub fill: Color, 115 + pub border: Option<Border>, 116 + pub radius: Radius, 117 + } 118 + 119 + impl PaintPrim { 120 + const fn solid(rect: LayoutRect, fill: Color) -> Self { 121 + Self { 122 + rect, 123 + fill, 124 + border: None, 125 + radius: Radius::px(0.0), 126 + } 127 + } 128 + } 129 + 130 + const LABEL_PLACEHOLDER_ALPHA: f32 = 0.55; 131 + const SELECTION_PLACEHOLDER_ALPHA: f32 = 0.35; 132 + const MARK_PLACEHOLDER_FACTOR: f32 = 0.55; 133 + 134 + #[must_use] 135 + pub fn lower_paint(theme: &Theme, paint: &WidgetPaint) -> PaintPrim { 136 + match paint { 137 + WidgetPaint::Surface { 138 + rect, 139 + fill, 140 + border, 141 + radius, 142 + .. 143 + } => PaintPrim { 144 + rect: *rect, 145 + fill: *fill, 146 + border: *border, 147 + radius: *radius, 148 + }, 149 + WidgetPaint::Label { rect, color, .. } => PaintPrim::solid( 150 + label_placeholder_bar(*rect), 151 + color.with_alpha(LABEL_PLACEHOLDER_ALPHA * color.alpha()), 152 + ), 153 + WidgetPaint::Mark { rect, color, .. } => { 154 + PaintPrim::solid(centered_square(*rect, MARK_PLACEHOLDER_FACTOR), *color) 155 + } 156 + WidgetPaint::FocusRing { 157 + rect, 158 + color, 159 + radius, 160 + thickness, 161 + } => PaintPrim { 162 + rect: *rect, 163 + fill: Color::TRANSPARENT, 164 + border: Some(Border { 165 + color: *color, 166 + width: StrokeWidth::px(thickness.value_px()), 167 + }), 168 + radius: *radius, 169 + }, 170 + WidgetPaint::SelectionHighlight { rect, color, .. } => PaintPrim::solid( 171 + *rect, 172 + color.with_alpha(SELECTION_PLACEHOLDER_ALPHA * color.alpha()), 173 + ), 174 + WidgetPaint::Caret { rect, color, .. } => PaintPrim::solid(caret_band(*rect), *color), 175 + WidgetPaint::Tooltip { 176 + rect, elevation, .. 177 + } => PaintPrim { 178 + rect: *rect, 179 + fill: theme.colors.surface(elevation.surface), 180 + border: elevation.border, 181 + radius: Radius::px(0.0), 182 + }, 183 + } 184 + } 185 + 186 + fn label_placeholder_bar(rect: LayoutRect) -> LayoutRect { 187 + let height = (rect.size.height.value() * 0.4).max(2.0); 188 + let pad = (rect.size.height.value() - height) * 0.5; 189 + let inset_x = (rect.size.width.value() * 0.1).min(8.0); 190 + LayoutRect::new( 191 + LayoutPos::new( 192 + LayoutPx::saturating(rect.origin.x.value() + inset_x), 193 + LayoutPx::saturating(rect.origin.y.value() + pad), 194 + ), 195 + LayoutSize::new( 196 + LayoutPx::saturating_nonneg(rect.size.width.value() - 2.0 * inset_x), 197 + LayoutPx::saturating_nonneg(height), 198 + ), 199 + ) 200 + } 201 + 202 + fn centered_square(rect: LayoutRect, factor: f32) -> LayoutRect { 203 + let side = rect 204 + .size 205 + .width 206 + .value() 207 + .min(rect.size.height.value()) 208 + .max(0.0) 209 + * factor; 210 + let cx = rect.origin.x.value() + 0.5 * rect.size.width.value(); 211 + let cy = rect.origin.y.value() + 0.5 * rect.size.height.value(); 212 + LayoutRect::new( 213 + LayoutPos::new( 214 + LayoutPx::saturating(cx - 0.5 * side), 215 + LayoutPx::saturating(cy - 0.5 * side), 216 + ), 217 + LayoutSize::new( 218 + LayoutPx::saturating_nonneg(side), 219 + LayoutPx::saturating_nonneg(side), 220 + ), 221 + ) 222 + } 223 + 224 + fn caret_band(rect: LayoutRect) -> LayoutRect { 225 + let width = rect.size.width.value().max(1.0); 226 + LayoutRect::new( 227 + rect.origin, 228 + LayoutSize::new(LayoutPx::saturating_nonneg(width), rect.size.height), 229 + ) 230 + }