use crate::input::Input;
use crate::textures::{TextureId, TextureRegistry, UvRect};
use glyphon::{
Attrs, Buffer, Cache, Color as GlyphColor, Family, FontSystem, Metrics, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
};
use std::collections::HashMap;
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
#[must_use]
pub fn to_ndc(gx: f32, gy: f32, pw: f32, ph: f32) -> [f32; 2] {
let gw = grid_width(pw, ph);
let unit = ph / 1080.0;
let ox = gw.mul_add(-unit, pw) * 0.5;
let px = gw.mul_add(0.5, gx).mul_add(unit, ox);
let py = (gy + 540.0) * unit;
[(px / pw).mul_add(2.0, -1.0), (py / ph).mul_add(-2.0, 1.0)]
}
#[must_use]
pub fn grid_width(pw: f32, ph: f32) -> f32 {
(pw / ph) * 1080.0
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
#[must_use]
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const WHITE: Self = Self::rgba(1.0, 1.0, 1.0, 1.0);
fn to_glyph(self) -> GlyphColor {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "value is clamped to 0.0..=255.0 and rounded before cast"
)]
fn channel(v: f32) -> u8 {
(v * 255.0).clamp(0.0, 255.0).round() as u8
}
GlyphColor::rgba(
channel(self.r),
channel(self.g),
channel(self.b),
channel(self.a),
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Rect {
#[must_use]
pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Self { x, y, w, h }
}
#[must_use]
pub fn contains(&self, px: f32, py: f32) -> bool {
px >= self.x && px <= self.x + self.w && py >= self.y && py <= self.y + self.h
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShaderId(usize);
impl ShaderId {
pub(crate) const fn new(index: usize) -> Self {
Self(index)
}
pub(crate) const fn index(self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ClipRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
fn clip_to_pixels(c: ClipRect, pw: f32, ph: f32) -> (f32, f32, f32, f32) {
let unit = ph / 1080.0;
let gw = grid_width(pw, ph);
let ox = gw.mul_add(-unit, pw) * 0.5;
let left = gw.mul_add(0.5, c.x).mul_add(unit, ox);
let top = (c.y + 540.0) * unit;
let right = gw.mul_add(0.5, c.x + c.w).mul_add(unit, ox);
let bottom = (c.y + c.h + 540.0) * unit;
(left, top, right, bottom)
}
struct Batch {
shader: ShaderId,
verts: Vec<[f32; 2]>,
color: Color,
z: f32,
clip: Option<ClipRect>,
texture: Option<TextureId>,
uv_rect: Option<UvRect>,
corner_radius: f32,
border_width: f32,
state: [f32; 4],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, Default)]
pub enum TextAlign {
Left,
#[default]
Center,
Right,
}
type BatchRange = (
ShaderId,
std::ops::Range<u32>,
Option<ClipRect>,
Option<TextureId>,
f32,
);
pub(crate) struct TextDraw<'a> {
pub text: &'a str,
pub x: f32,
pub y: f32,
pub w: f32,
pub size: f32,
pub color: Color,
pub align: TextAlign,
pub font: Option<&'a str>,
pub bold: bool,
pub italic: bool,
pub clip: Option<ClipRect>,
pub z: f32,
}
pub(crate) struct GpuFrame<'a> {
pub encoder: &'a mut wgpu::CommandEncoder,
pub view: &'a wgpu::TextureView,
pub device: &'a wgpu::Device,
pub queue: &'a wgpu::Queue,
}
struct TextEntry {
text: String,
x: f32,
y: f32,
w: f32,
size: f32,
color: Color,
align: TextAlign,
font: Option<String>,
bold: bool,
italic: bool,
clip: Option<ClipRect>,
z: f32,
}
pub struct Scene {
batches: Vec<Batch>,
texts: Vec<TextEntry>,
}
impl Scene {
#[must_use]
pub const fn new() -> Self {
Self {
batches: Vec::new(),
texts: Vec::new(),
}
}
pub fn push_full(
&mut self,
verts: Vec<[f32; 2]>,
shader: ShaderId,
color: Color,
z: f32,
clip: Option<ClipRect>,
corner_radius: f32,
) {
self.push_widget(verts, shader, color, z, clip, corner_radius, 0.0, [0.0; 4]);
}
pub fn push_widget(
&mut self,
verts: Vec<[f32; 2]>,
shader: ShaderId,
color: Color,
z: f32,
clip: Option<ClipRect>,
corner_radius: f32,
border_width: f32,
state: [f32; 4],
) {
self.batches.push(Batch {
shader,
verts,
color,
z,
clip,
texture: None,
uv_rect: None,
corner_radius,
border_width,
state,
});
}
pub fn push_image(
&mut self,
rect: Rect,
shader: ShaderId,
texture: TextureId,
z: f32,
clip: Option<ClipRect>,
) {
self.push_image_uv(rect, shader, texture, z, clip, None);
}
pub fn push_image_uv(
&mut self,
rect: Rect,
shader: ShaderId,
texture: TextureId,
depth: f32,
clip: Option<ClipRect>,
uv_rect: Option<UvRect>,
) {
let (rx, ry, rw, rh) = (rect.x, rect.y, rect.w, rect.h);
let verts = vec![
[rx, ry],
[rx + rw, ry],
[rx, ry + rh],
[rx + rw, ry],
[rx + rw, ry + rh],
[rx, ry + rh],
];
self.batches.push(Batch {
shader,
verts,
color: Color::WHITE,
z: depth,
clip,
texture: Some(texture),
uv_rect,
corner_radius: 0.0,
border_width: 0.0,
state: [0.0; 4],
});
}
pub fn text(&mut self, text: &str, x: f32, y: f32, w: f32, size: f32, color: Color) {
self.push_text(&TextDraw {
text,
x,
y,
w,
size,
color,
align: TextAlign::Center,
font: None,
bold: false,
italic: false,
clip: None,
z: 0.5,
});
}
pub fn text_left(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
self.push_text(&TextDraw {
text,
x,
y,
w: 0.0,
size,
color,
align: TextAlign::Left,
font: None,
bold: false,
italic: false,
clip: None,
z: 0.5,
});
}
pub(crate) fn push_text(&mut self, p: &TextDraw<'_>) {
self.texts.push(TextEntry {
text: p.text.to_string(),
x: p.x,
y: p.y,
w: p.w,
size: p.size,
color: p.color,
align: p.align,
font: p.font.map(std::string::ToString::to_string),
bold: p.bold,
italic: p.italic,
clip: p.clip,
z: p.z,
});
}
pub fn clear(&mut self) {
self.batches.clear();
self.texts.clear();
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.batches.is_empty() && self.texts.is_empty()
}
}
impl Default for Scene {
fn default() -> Self {
Self::new()
}
}
const VERT_SIZE: u64 = 64;
const MAX_VERTS: u64 = 2_000_000;
const GLOBALS_SIZE: u64 = (std::mem::size_of::<f32>() * 8) as u64;
struct TextSystem {
font_system: FontSystem,
swash_cache: SwashCache,
_cache: Cache,
atlas: TextAtlas,
renderer: TextRenderer,
viewport: Viewport,
}
impl TextSystem {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
let font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(device);
let viewport = Viewport::new(device, &cache);
let mut atlas = TextAtlas::new(device, queue, &cache, format);
let renderer =
TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
Self {
font_system,
swash_cache,
_cache: cache,
atlas,
renderer,
viewport,
}
}
fn build_buffer(&mut self, t: &TextEntry, unit: f32, pw: f32, ph: f32) -> Buffer {
let font_size = t.size * unit;
let use_left = matches!(t.align, TextAlign::Left) || t.w <= 0.0;
let buf_w = if use_left { pw } else { t.w * unit };
let family = t.font.as_deref().map_or(Family::SansSerif, Family::Name);
let weight = if t.bold {
glyphon::Weight::BOLD
} else {
glyphon::Weight::NORMAL
};
let style = if t.italic {
glyphon::Style::Italic
} else {
glyphon::Style::Normal
};
let mut buf = Buffer::new(
&mut self.font_system,
Metrics::new(font_size, font_size * 1.2),
);
buf.set_size(&mut self.font_system, Some(buf_w), Some(ph));
buf.set_text(
&mut self.font_system,
&t.text,
&Attrs::new().family(family).weight(weight).style(style),
Shaping::Advanced,
None,
);
let align = match t.align {
TextAlign::Left => glyphon::cosmic_text::Align::Left,
TextAlign::Center => glyphon::cosmic_text::Align::Center,
TextAlign::Right => glyphon::cosmic_text::Align::Right,
};
for line in &mut buf.lines {
line.set_align(Some(align));
}
buf.shape_until_scroll(&mut self.font_system, false);
buf
}
fn build_text_area<'a>(
t: &TextEntry,
buf: &'a Buffer,
unit: f32,
pw: f32,
ph: f32,
gw: f32,
ox: f32,
) -> TextArea<'a> {
let bounds = t.clip.map_or_else(
|| TextBounds {
left: 0,
top: 0,
right: pw.max(0.0).trunc() as i32,
bottom: ph.max(0.0).trunc() as i32,
},
|c| {
let (l, t, r, b) = clip_to_pixels(c, pw, ph);
TextBounds {
left: l.trunc() as i32,
top: t.trunc() as i32,
right: r.trunc() as i32,
bottom: b.trunc() as i32,
}
},
);
TextArea {
buffer: buf,
left: gw.mul_add(0.5, t.x).mul_add(unit, ox),
top: (t.y + 540.0) * unit,
scale: 1.0,
bounds,
default_color: t.color.to_glyph(),
custom_glyphs: &[],
}
}
pub(crate) fn measure_cursor_px(
&mut self,
text: &str,
font_size_px: f32,
buf_w_px: f32,
_ph: f32,
align: TextAlign,
cursor_byte: usize,
bold: bool,
italic: bool,
font: Option<&str>,
) -> f32 {
let family = font.map_or(Family::SansSerif, Family::Name);
let weight = if bold {
glyphon::Weight::BOLD
} else {
glyphon::Weight::NORMAL
};
let style = if italic {
glyphon::Style::Italic
} else {
glyphon::Style::Normal
};
let mut buf = Buffer::new(
&mut self.font_system,
Metrics::new(font_size_px, font_size_px * 1.2),
);
buf.set_size(&mut self.font_system, Some(buf_w_px), None);
buf.set_text(
&mut self.font_system,
text,
&Attrs::new().family(family).weight(weight).style(style),
Shaping::Advanced,
None,
);
let cosmic_align = match align {
TextAlign::Left => glyphon::cosmic_text::Align::Left,
TextAlign::Center => glyphon::cosmic_text::Align::Center,
TextAlign::Right => glyphon::cosmic_text::Align::Right,
};
for line in &mut buf.lines {
line.set_align(Some(cosmic_align));
}
buf.shape_until_scroll(&mut self.font_system, false);
for run in buf.layout_runs() {
let glyphs = run.glyphs;
for (i, glyph) in glyphs.iter().enumerate() {
if cursor_byte >= glyph.start && cursor_byte < glyph.end {
return glyph.x;
}
if cursor_byte == glyph.end {
return glyphs.get(i + 1).map_or(glyph.x + glyph.w, |next| next.x);
}
}
}
buf.layout_runs()
.last()
.and_then(|r| r.glyphs.last())
.map_or_else(
|| match align {
TextAlign::Center => buf_w_px * 0.5,
TextAlign::Right => buf_w_px,
TextAlign::Left => 0.0,
},
|g| g.x + g.w,
)
}
fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
pass: &mut wgpu::RenderPass,
texts: &[TextEntry],
pw: f32,
ph: f32,
) {
if texts.is_empty() {
return;
}
let unit = ph / 1080.0;
let ox = grid_width(pw, ph).mul_add(-unit, pw) * 0.5; #[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "viewport dimensions are clamped to >=0 before cast"
)]
self.viewport.update(
queue,
glyphon::Resolution {
width: pw.max(0.0) as u32,
height: ph.max(0.0) as u32,
},
);
let buffers: Vec<Buffer> = texts
.iter()
.map(|t| self.build_buffer(t, unit, pw, ph))
.collect();
let gw = grid_width(pw, ph);
let areas: Vec<TextArea> = texts
.iter()
.zip(buffers.iter())
.map(|(t, buf)| Self::build_text_area(t, buf, unit, pw, ph, gw, ox))
.collect();
self.atlas.trim();
if let Err(e) = self.renderer.prepare(
device,
queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
areas,
&mut self.swash_cache,
) {
eprintln!("[pane] text prepare error: {e}");
return;
}
if let Err(e) = self.renderer.render(&self.atlas, &self.viewport, pass) {
eprintln!("[pane] text render error: {e}");
}
}
}
struct StandaloneReady {
surface: wgpu::Surface<'static>,
config: wgpu::SurfaceConfiguration,
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
}
struct GpuResources {
vertex_buf: wgpu::Buffer,
text: TextSystem,
text_overlay: TextSystem,
globals_bgl: wgpu::BindGroupLayout,
globals_buf: wgpu::Buffer,
globals_bg: wgpu::BindGroup,
}
pub struct Pane {
pipelines: Vec<wgpu::RenderPipeline>,
shader_cache: HashMap<String, ShaderId>,
gpu: Option<GpuResources>, format: wgpu::TextureFormat,
standalone: Option<StandaloneReady>, total_time: f32,
frame_count: u32,
}
impl Pane {
const fn gpu(&self) -> &GpuResources {
self.gpu.as_ref().unwrap()
}
const fn gpu_mut(&mut self) -> &mut GpuResources {
self.gpu.as_mut().unwrap()
}
fn make_gpu(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> GpuResources {
let vertex_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: MAX_VERTS * VERT_SIZE,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let globals_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let globals_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: GLOBALS_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let globals_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &globals_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: globals_buf.as_entire_binding(),
}],
});
GpuResources {
vertex_buf,
text: TextSystem::new(device, queue, format),
text_overlay: TextSystem::new(device, queue, format),
globals_bgl,
globals_buf,
globals_bg,
}
}
#[must_use]
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
Self {
pipelines: Vec::new(),
shader_cache: HashMap::new(),
gpu: Some(Self::make_gpu(device, queue, format)),
format,
standalone: None,
total_time: 0.0,
frame_count: 0,
}
}
fn new_standalone() -> Self {
Self {
pipelines: Vec::new(),
shader_cache: HashMap::new(),
gpu: None,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
standalone: None,
total_time: 0.0,
frame_count: 0,
}
}
async fn init(&mut self, window: Arc<Window>) -> Result<(), String> {
let size = window.inner_size();
let instance = wgpu::Instance::default();
let surface = instance
.create_surface(window)
.map_err(|e| format!("[pane] failed to create surface: {e}"))?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface: Some(&surface),
..Default::default()
})
.await
.map_err(|e| format!("[pane] no compatible GPU adapter found: {e}"))?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await
.map_err(|e| format!("[pane] failed to acquire GPU device: {e}"))?;
let cap = surface.get_capabilities(&adapter);
let format = cap
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(cap.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: cap.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
let device = Arc::new(device);
let queue = Arc::new(queue);
self.gpu = Some(Self::make_gpu(&device, &queue, format));
self.format = format;
self.standalone = Some(StandaloneReady {
surface,
config,
device,
queue,
});
Ok(())
}
fn advance_time(
&mut self,
dt: f32,
pw: f32,
ph: f32,
cursor: [f32; 3],
queue: &wgpu::Queue,
tex_reg: &mut TextureRegistry,
) {
self.total_time += dt;
self.frame_count = self.frame_count.wrapping_add(1);
let [cx, cy] = to_ndc(cursor[0], cursor[1], pw, ph);
let globals_data: [f32; 8] = [
self.total_time,
dt,
self.frame_count as f32,
cx,
cy,
cursor[2],
0.0,
0.0,
];
queue.write_buffer(
&self.gpu().globals_buf,
0,
bytemuck::cast_slice(&globals_data),
);
tex_reg.update(dt);
}
pub(crate) fn device(&self) -> Option<&Arc<wgpu::Device>> {
self.standalone.as_ref().map(|s| &s.device)
}
pub(crate) fn queue(&self) -> Option<&Arc<wgpu::Queue>> {
self.standalone.as_ref().map(|s| &s.queue)
}
pub fn register_shader(
&mut self,
device: &wgpu::Device,
wgsl: &str,
tex_reg: &TextureRegistry,
) -> ShaderId {
if let Some(&id) = self.shader_cache.get(wgsl) {
return id;
}
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[
Some(&tex_reg.bind_group_layout),
Some(&self.gpu().globals_bgl),
],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: VERT_SIZE,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 8,
shader_location: 1,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 16,
shader_location: 2,
format: wgpu::VertexFormat::Float32x4,
},
wgpu::VertexAttribute {
offset: 32,
shader_location: 3,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 40,
shader_location: 4,
format: wgpu::VertexFormat::Float32x2,
},
wgpu::VertexAttribute {
offset: 48,
shader_location: 5,
format: wgpu::VertexFormat::Float32x4,
},
],
}],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
targets: &[Some(wgpu::ColorTargetState {
format: self.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let id = ShaderId::new(self.pipelines.len());
self.pipelines.push(pipeline);
self.shader_cache.insert(wgsl.to_string(), id);
id
}
pub(crate) fn measure_cursor(
&mut self,
text: &str,
font_size_grid: f32,
rect_w_grid: f32,
align: TextAlign,
pw: f32,
ph: f32,
cursor_byte: usize,
bold: bool,
italic: bool,
font: Option<&str>,
) -> f32 {
let unit = ph / 1080.0;
let font_size_px = font_size_grid * unit;
let buf_w_px = match align {
TextAlign::Left => pw,
_ => (rect_w_grid * unit).max(1.0),
};
self.gpu_mut().text.measure_cursor_px(
text,
font_size_px,
buf_w_px,
ph,
align,
cursor_byte,
bold,
italic,
font,
) / unit
}
pub(crate) fn render(
&mut self,
frame: GpuFrame<'_>,
scene: &mut Scene,
pw: f32,
ph: f32,
dt: f32,
cursor: [f32; 3],
clear_color: Option<crate::draw::Color>,
tex_reg: &mut TextureRegistry,
) {
let GpuFrame {
encoder,
view,
device,
queue,
} = frame;
self.advance_time(dt, pw, ph, cursor, queue, tex_reg);
if scene.is_empty() {
return;
}
let load_op = clear_color.map_or(wgpu::LoadOp::Load, |c| {
wgpu::LoadOp::Clear(wgpu::Color {
r: f64::from(c.r),
g: f64::from(c.g),
b: f64::from(c.b),
a: f64::from(c.a),
})
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
multiview_mask: None,
});
let ranges = self.build_batches(scene, pw, ph, queue);
scene.texts.sort_by(|a, b| a.z.total_cmp(&b.z));
let split = scene.texts.partition_point(|t| t.z < 1.0);
self.issue_draw_calls(&mut pass, pw, ph, &ranges, 0.0, 1.0, tex_reg);
self.gpu_mut()
.text
.render(device, queue, &mut pass, &scene.texts[..split], pw, ph);
self.issue_draw_calls(&mut pass, pw, ph, &ranges, 1.0, f32::MAX, tex_reg);
self.gpu_mut()
.text_overlay
.render(device, queue, &mut pass, &scene.texts[split..], pw, ph);
}
pub(crate) fn present_standalone(
&mut self,
scene: &mut Scene,
pw: f32,
ph: f32,
dt: f32,
cursor: [f32; 3],
clear_color: Option<crate::draw::Color>,
tex_reg: &mut TextureRegistry,
) {
let Some(sa) = &self.standalone else { return };
let queue = sa.queue.clone();
self.advance_time(dt, pw, ph, cursor, &queue, tex_reg);
if scene.is_empty() {
return;
}
let sa = self.standalone.as_ref().unwrap();
let output = match sa.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t)
| wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
sa.surface.configure(&sa.device, &sa.config);
return;
}
other => {
eprintln!("[pane_ui] Surface error: {other:?}");
return;
}
};
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let device = sa.device.clone();
let queue = sa.queue.clone();
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
self.render(
GpuFrame {
encoder: &mut enc,
view: &view,
device: &device,
queue: &queue,
},
scene,
pw,
ph,
dt,
cursor,
clear_color,
tex_reg,
);
queue.submit(std::iter::once(enc.finish()));
output.present();
}
fn resize(&mut self, w: u32, h: u32) {
if let Some(sa) = &mut self.standalone {
sa.config.width = w;
sa.config.height = h;
sa.surface.configure(&sa.device, &sa.config);
}
}
fn build_batches(
&self,
scene: &mut Scene,
pw: f32,
ph: f32,
queue: &wgpu::Queue,
) -> Vec<BatchRange> {
if scene.batches.is_empty() {
return Vec::new();
}
scene.batches.sort_by(|a, b| a.z.total_cmp(&b.z));
let mut all_verts: Vec<f32> = Vec::new();
let mut ranges: Vec<BatchRange> = Vec::new();
for batch in &scene.batches {
let start = u32::try_from(all_verts.len() / 16).unwrap_or(0);
let min_x = batch.verts.iter().map(|v| v[0]).fold(f32::MAX, f32::min);
let max_x = batch.verts.iter().map(|v| v[0]).fold(f32::MIN, f32::max);
let min_y = batch.verts.iter().map(|v| v[1]).fold(f32::MAX, f32::min);
let max_y = batch.verts.iter().map(|v| v[1]).fold(f32::MIN, f32::max);
let rw = (max_x - min_x).max(0.0001);
let rh = (max_y - min_y).max(0.0001);
let uv = batch.uv_rect.unwrap_or(UvRect::FULL);
let unit = ph / 1080.0;
let cr_px = (batch.corner_radius * unit).min(rw.min(rh) * unit * 0.5);
let bw_px = batch.border_width * unit;
let size_w = rw * unit;
let size_h = rh * unit;
let s = batch.state;
for v in &batch.verts {
let [x, y] = to_ndc(v[0], v[1], pw, ph);
let t_u = (v[0] - min_x) / rw;
let t_v = (v[1] - min_y) / rh;
let u = uv.u_min + t_u * (uv.u_max - uv.u_min);
let vv = uv.v_min + t_v * (uv.v_max - uv.v_min);
all_verts.extend_from_slice(&[
x,
y,
u,
vv,
batch.color.r,
batch.color.g,
batch.color.b,
batch.color.a,
cr_px,
bw_px,
size_w,
size_h,
s[0],
s[1],
s[2],
s[3],
]);
}
ranges.push((
batch.shader,
start..u32::try_from(all_verts.len() / 16).unwrap_or(0),
batch.clip,
batch.texture,
batch.z,
));
}
if all_verts.len() > (MAX_VERTS * 16) as usize {
eprintln!(
"[pane] vertex buffer overflow: {} verts (max {}); frame skipped",
all_verts.len() / 16,
MAX_VERTS
);
return Vec::new();
}
queue.write_buffer(&self.gpu().vertex_buf, 0, bytemuck::cast_slice(&all_verts));
ranges
}
fn issue_draw_calls(
&self,
pass: &mut wgpu::RenderPass,
pw: f32,
ph: f32,
ranges: &[BatchRange],
z_min: f32,
z_max: f32,
tex_reg: &TextureRegistry,
) {
if ranges
.iter()
.all(|(_, _, _, _, z)| *z < z_min || *z >= z_max)
{
return;
}
pass.set_vertex_buffer(0, self.gpu().vertex_buf.slice(..));
pass.set_bind_group(1, &self.gpu().globals_bg, &[]);
let mut current_clip: Option<ClipRect> = None;
for (shader_id, range, clip, texture, z) in ranges {
if *z < z_min || *z >= z_max {
continue;
}
if *clip != current_clip {
match clip {
Some(c) => {
let (l, t, r, b) = clip_to_pixels(*c, pw, ph);
let max_w = pw.max(0.0) as u32;
let max_h = ph.max(0.0) as u32;
let sx = (l.max(0.0) as u32).min(max_w);
let sy = (t.max(0.0) as u32).min(max_h);
let sw = ((r - l).max(0.0) as u32).min(max_w - sx);
let sh = ((b - t).max(0.0) as u32).min(max_h - sy);
if sw > 0 && sh > 0 {
pass.set_scissor_rect(sx, sy, sw, sh);
}
}
None => pass.set_scissor_rect(0, 0, pw.max(0.0) as u32, ph.max(0.0) as u32),
}
current_clip = *clip;
}
pass.set_pipeline(&self.pipelines[shader_id.index()]);
if let Some(id) = texture
&& tex_reg.is_hidden(*id)
{
continue;
}
let bg = texture.map_or_else(|| tex_reg.dummy(), |id| tex_reg.current_bind_group(id));
pass.set_bind_group(0, bg, &[]);
pass.draw(range.clone(), 0..1);
}
pass.set_scissor_rect(0, 0, pw.max(0.0) as u32, ph.max(0.0) as u32);
}
}
struct App<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32)> {
window: Option<Arc<Window>>,
pane: Pane,
scene: Scene,
input: Input,
draw_fn: F,
last_frame: std::time::Instant,
}
impl<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32) + 'static> ApplicationHandler
for App<F>
{
fn resumed(&mut self, el: &ActiveEventLoop) {
let win = Arc::new(
el.create_window(Window::default_attributes().with_title("Pane"))
.unwrap(),
);
if let Err(e) = pollster::block_on(self.pane.init(win.clone())) {
eprintln!("{e}");
el.exit();
return;
}
self.window = Some(win);
}
fn window_event(&mut self, el: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
if let Some(win) = &self.window {
let s = win.inner_size();
self.input
.handle_event(&event, s.width as f32, s.height as f32);
}
match event {
WindowEvent::CloseRequested => el.exit(),
WindowEvent::Resized(s) => self.pane.resize(s.width, s.height),
WindowEvent::RedrawRequested => {
if let Some(win) = &self.window {
let s = win.inner_size();
let pw = s.width as f32;
let ph = s.height as f32;
let now = std::time::Instant::now();
let dt = now.duration_since(self.last_frame).as_secs_f32().min(0.1);
self.last_frame = now;
self.scene.clear();
(self.draw_fn)(&mut self.pane, &mut self.scene, &mut self.input, pw, ph, dt);
}
}
_ => {}
}
}
fn about_to_wait(&mut self, _: &ActiveEventLoop) {
self.input.poll_gamepad();
if let Some(win) = &self.window {
win.request_redraw();
}
}
}
pub fn run<F: FnMut(&mut Pane, &mut Scene, &mut Input, f32, f32, f32) + 'static>(draw_fn: F) {
let el = EventLoop::new().unwrap();
let mut app = App {
window: None,
pane: Pane::new_standalone(),
scene: Scene::new(),
input: Input::new_with_gilrs(),
draw_fn,
last_frame: std::time::Instant::now(),
};
el.run_app(&mut app).unwrap();
}