use std::collections::HashMap;
use std::path::Path;
use wgpu::util::DeviceExt;
const MIN_FRAME_DELAY_SECS: f32 = 1.0 / 60.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextureId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
pub enum GifMode {
Loop,
Once,
OnceHide,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UvRect {
pub u_min: f32,
pub v_min: f32,
pub u_max: f32,
pub v_max: f32,
}
impl UvRect {
pub const FULL: Self = Self {
u_min: 0.0,
v_min: 0.0,
u_max: 1.0,
v_max: 1.0,
};
}
struct StoredTexture {
_texture: wgpu::Texture,
_view: wgpu::TextureView,
bind_group: wgpu::BindGroup,
width: u32,
height: u32,
}
enum TextureKind {
Static(StoredTexture),
Gif {
frames: Vec<StoredTexture>,
delays: Vec<f32>,
current: usize,
timer: f32,
mode: GifMode,
done: bool,
},
}
pub struct TextureRegistry {
entries: Vec<TextureKind>,
path_cache: HashMap<String, TextureId>,
sampler: wgpu::Sampler,
dummy_bind_group: wgpu::BindGroup,
pub(crate) bind_group_layout: wgpu::BindGroupLayout,
}
impl TextureRegistry {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("texture_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("texture_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let white_texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some("dummy_white"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
&[255u8, 255, 255, 255],
);
let white_view = white_texture.create_view(&wgpu::TextureViewDescriptor::default());
let dummy_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("dummy_bind_group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&white_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
],
});
Self {
entries: Vec::new(),
path_cache: HashMap::new(),
sampler,
dummy_bind_group,
bind_group_layout,
}
}
pub const fn dummy(&self) -> &wgpu::BindGroup {
&self.dummy_bind_group
}
pub fn load(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
path: &str,
) -> Result<TextureId, String> {
if let Some(&id) = self.path_cache.get(path) {
return Ok(id);
}
let img = image::open(Path::new(path))
.map_err(|e| format!("Failed to load image '{path}': {e}"))?
.into_rgba8();
let (width, height) = img.dimensions();
let stored = self.upload(device, queue, path, width, height, &img.into_raw());
let id = TextureId(self.entries.len());
self.entries.push(TextureKind::Static(stored));
self.path_cache.insert(path.to_string(), id);
Ok(id)
}
pub fn load_gif(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
path: &str,
mode: GifMode,
) -> Result<TextureId, String> {
use image::AnimationDecoder;
if let Some(&id) = self.path_cache.get(path) {
return Ok(id);
}
let file =
std::fs::File::open(path).map_err(|e| format!("Failed to open gif '{path}': {e}"))?;
let decoder = image::codecs::gif::GifDecoder::new(std::io::BufReader::new(file))
.map_err(|e| format!("Failed to decode gif '{path}': {e}"))?;
let frames: Vec<image::Frame> = decoder
.into_frames()
.collect_frames()
.map_err(|e| format!("Failed to collect gif frames '{path}': {e}"))?;
if frames.is_empty() {
return Err(format!("Gif '{path}' has no frames"));
}
let (w0, h0) = frames[0].buffer().dimensions();
for (i, frame) in frames.iter().enumerate().skip(1) {
let (w, h) = frame.buffer().dimensions();
assert!(
w == w0 && h == h0,
"[pane_ui] GIF '{path}' frame {i} is {w}×{h}, expected {w0}×{h0}"
);
}
let mut stored_frames = Vec::with_capacity(frames.len());
let mut delays = Vec::with_capacity(frames.len());
for (i, frame) in frames.iter().enumerate() {
let img = frame.buffer();
let (width, height) = img.dimensions();
let stored = self.upload(
device,
queue,
&format!("{path}_frame{i}"),
width,
height,
img.as_raw(),
);
stored_frames.push(stored);
let (num, den) = frame.delay().numer_denom_ms();
delays.push((num as f32 / den as f32 / 1000.0).max(MIN_FRAME_DELAY_SECS));
}
let id = TextureId(self.entries.len());
self.entries.push(TextureKind::Gif {
frames: stored_frames,
delays,
current: 0,
timer: 0.0,
mode,
done: false,
});
self.path_cache.insert(path.to_string(), id);
Ok(id)
}
pub fn update(&mut self, dt: f32) {
for entry in &mut self.entries {
let TextureKind::Gif {
frames,
delays,
current,
timer,
mode,
done,
} = entry
else {
continue;
};
if *done {
continue;
}
*timer += dt;
loop {
let delay = delays[*current];
if *timer < delay {
break;
}
*timer -= delay;
let next = *current + 1;
if next >= frames.len() {
match mode {
GifMode::Loop => {
*current = 0;
}
GifMode::Once | GifMode::OnceHide => {
*current = frames.len() - 1;
*done = true;
break;
}
}
} else {
*current = next;
}
}
}
}
pub fn reset_gif(&mut self, id: TextureId) {
if let TextureKind::Gif {
current,
timer,
done,
..
} = &mut self.entries[id.0]
{
*current = 0;
*timer = 0.0;
*done = false;
}
}
pub fn current_bind_group(&self, id: TextureId) -> &wgpu::BindGroup {
match &self.entries[id.0] {
TextureKind::Static(t) => &t.bind_group,
TextureKind::Gif {
frames, current, ..
} => &frames[*current].bind_group,
}
}
pub const fn current_uv_rect(_id: TextureId) -> UvRect {
UvRect::FULL
}
pub fn is_hidden(&self, id: TextureId) -> bool {
match &self.entries[id.0] {
TextureKind::Gif { mode, done, .. } => *done && *mode == GifMode::OnceHide,
TextureKind::Static(_) => false,
}
}
pub fn dimensions(&self, id: TextureId) -> (u32, u32) {
match &self.entries[id.0] {
TextureKind::Static(t) => (t.width, t.height),
TextureKind::Gif { frames, .. } => (frames[0].width, frames[0].height),
}
}
fn upload(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
label: &str,
width: u32,
height: u32,
rgba: &[u8],
) -> StoredTexture {
let texture = device.create_texture_with_data(
queue,
&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
rgba,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("texture_bind_group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
StoredTexture {
_texture: texture,
_view: view,
bind_group,
width,
height,
}
}
}
pub trait TextureInfo {
fn is_hidden(&self, id: TextureId) -> bool;
fn current_uv_rect(&self, id: TextureId) -> UvRect;
}
impl TextureInfo for TextureRegistry {
fn is_hidden(&self, id: TextureId) -> bool {
self.is_hidden(id)
}
fn current_uv_rect(&self, id: TextureId) -> UvRect {
Self::current_uv_rect(id)
}
}
#[derive(Default)]
pub struct DummyTextureRegistry;
impl DummyTextureRegistry {
pub const fn new() -> Self {
Self
}
}
pub static DUMMY_TEX: DummyTextureRegistry = DummyTextureRegistry::new();
impl TextureInfo for DummyTextureRegistry {
fn is_hidden(&self, _id: TextureId) -> bool {
false
}
fn current_uv_rect(&self, _id: TextureId) -> UvRect {
UvRect::FULL
}
}