Skip to content

Instantly share code, notes, and snippets.

@MarinPostma
Created September 15, 2025 15:58
Show Gist options
  • Select an option

  • Save MarinPostma/8b1411569603a60c472334294490b420 to your computer and use it in GitHub Desktop.

Select an option

Save MarinPostma/8b1411569603a60c472334294490b420 to your computer and use it in GitHub Desktop.
use std::borrow::Cow;
use std::sync::Arc;
use anyhow::Result;
use iced::Font;
use iced::Pixels;
use iced::Size;
use iced::Theme;
use iced::widget;
use iced_core::text::Renderer as _;
use iced_wgpu::core::renderer;
use iced_wgpu::graphics::Viewport;
use iced_wgpu::graphics::text::font_system;
use iced_wgpu::{Engine, Renderer, wgpu};
use iced_winit::Clipboard;
use iced_winit::conversion;
use iced_winit::core::{Event, mouse};
use iced_winit::runtime::user_interface::{self, UserInterface};
use iced_winit::winit::event_loop::ControlFlow;
use iced_winit::winit::keyboard::ModifiersState;
use iced_winit::winit::{
self, application::ApplicationHandler, event::WindowEvent, event_loop::EventLoop,
window::Window,
};
use wasm_bindgen::UnwrapThrowExt;
use wgpu::TextureFormat;
struct Counter {
count: i32,
}
const FONT: &[u8] = include_bytes!("../FiraSans-Regular.ttf");
const FONT2: &[u8] = include_bytes!("../roboto.ttf");
impl Counter {
fn new() -> Self {
Self { count: 0 }
}
fn update(&mut self, message: Message) -> iced::Task<Message> {
match message {
Message::IncrementCount => self.count += 1,
Message::DecrementCount => self.count -= 1,
}
iced::Task::none()
}
fn view(&self) -> iced::Element<'_, Message, iced::Theme, iced_wgpu::Renderer> {
let row = widget::row![
widget::button("-").on_press(Message::DecrementCount),
widget::text!("Count: {}", self.count),
]
.spacing(10);
widget::container(row)
.center_x(iced::Length::Fill)
.center_y(iced::Length::Fill)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.into()
}
}
macro_rules! dbg2 {
// NOTE: We cannot use `concat!` to make a static string as a format argument
// of `eprintln!` because `file!` could contain a `{` or
// `$val` expression could be a block (`{ .. }`), in which case the `eprintln!`
// will be malformed.
() => {
log::info!("[{}:{}:{}]", file!(), line!(), column!())
};
($val:expr $(,)?) => {
// Use of `match` here is intentional because it affects the lifetimes
// of temporaries - https://stackoverflow.com/a/48732525/1063961
match $val {
tmp => {
log::info!("[{}:{}:{}] {} = {:#?}",
file!(), line!(), column!(), stringify!($val), &tmp);
tmp
}
}
};
($($val:expr),+ $(,)?) => {
($($crate::dbg2!($val)),+,)
};
}
#[derive(Copy, Clone, Debug)]
enum Message {
IncrementCount,
DecrementCount,
}
pub struct State {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
is_surface_configured: bool,
renderer: Renderer,
resized: bool,
viewport: Viewport,
counter: Counter,
format: TextureFormat,
cache: user_interface::Cache,
cursor: mouse::Cursor,
clipboard: iced_winit::Clipboard,
events: Vec<Event>,
modifiers: ModifiersState,
}
impl State {
pub async fn new(window: Arc<Window>) -> Result<Self> {
let size = window.inner_size();
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
})
.await?;
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
desired_maximum_frame_latency: 2,
alpha_mode: surface_caps.alpha_modes[0],
view_formats: Vec::new(),
};
let engine = Engine::new(
&adapter,
device.clone(),
queue.clone(),
surface_format,
None,
);
register_font_manually(FONT2);
let renderer = Renderer::new(engine, Font::with_name("Roboto-Regular"), Pixels::from(16));
dbg2!(renderer.default_font());
let counter = Counter::new();
let size = window.inner_size();
let phy_size = iced::Size::new(size.width.max(1), size.height.max(1));
let viewport = Viewport::with_physical_size(
iced::Size::new(phy_size.width, phy_size.height),
window.scale_factor() as f32,
);
surface.configure(&device, &config);
let clipboard = Clipboard::connect(window.clone());
Ok(Self {
window,
format: surface_format,
surface,
device,
queue,
config,
is_surface_configured: true,
counter,
renderer,
resized: false,
viewport,
cache: user_interface::Cache::new(),
cursor: mouse::Cursor::Unavailable,
clipboard,
events: Vec::new(),
modifiers: ModifiersState::default(),
})
}
}
pub struct App {
proxy: Option<winit::event_loop::EventLoopProxy<State>>,
state: Option<State>,
}
impl App {
pub fn new(event_loop: &EventLoop<State>) -> Self {
Self {
state: None,
proxy: Some(event_loop.create_proxy()),
}
}
}
impl ApplicationHandler<State> for App {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let mut window_attributes = Window::default_attributes();
use wasm_bindgen::JsCast;
use winit::platform::web::WindowAttributesExtWebSys;
const CANVAS_ID: &str = "canvas";
let window = wgpu::web_sys::window().unwrap_throw();
let document = window.document().unwrap_throw();
let canvas = document.get_element_by_id(CANVAS_ID).unwrap_throw();
let html_canvas_element = canvas.unchecked_into();
window_attributes = window_attributes.with_canvas(Some(html_canvas_element));
let window = event_loop.create_window(window_attributes).unwrap().into();
if let Some(proxy) = self.proxy.take() {
wasm_bindgen_futures::spawn_local(async move {
assert!(
proxy
.send_event(State::new(window).await.expect("couldn't create canvas"))
.is_ok()
)
});
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
mut event: WindowEvent,
) {
let mut app_state = match &mut self.state {
Some(canvas) => canvas,
None => return,
};
if !app_state.is_surface_configured {
return;
}
event_loop.set_control_flow(ControlFlow::Wait);
match event {
WindowEvent::RedrawRequested => {
if app_state.resized {
let size = app_state.window.inner_size();
app_state.viewport = Viewport::with_physical_size(
Size::new(size.width, size.height),
app_state.window.scale_factor() as f32,
);
app_state.surface.configure(
&app_state.device,
&wgpu::SurfaceConfiguration {
format: app_state.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: 2,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
},
);
app_state.resized = false;
}
match app_state.surface.get_current_texture() {
Ok(frame) => {
let theme = Theme::Dracula;
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut interface = UserInterface::build(
app_state.counter.view(),
app_state.viewport.logical_size(),
std::mem::take(&mut app_state.cache),
&mut app_state.renderer,
);
let (state, _) = interface.update(
&[Event::Window(iced::window::Event::RedrawRequested(
iced::time::Instant::now(),
))],
app_state.cursor,
&mut app_state.renderer,
&mut app_state.clipboard,
&mut Vec::new(),
);
let mut encoder = app_state.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor { label: None },
);
// draw background
let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: theme.palette().background.r as f64,
g: theme.palette().background.g as f64,
b: theme.palette().background.b as f64,
a: theme.palette().background.a as f64,
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
app_state.queue.submit(std::iter::once(encoder.finish()));
if let user_interface::State::Updated {
mouse_interaction, ..
} = state
{
app_state
.window
.set_cursor(conversion::mouse_interaction(mouse_interaction));
}
interface.draw(
&mut app_state.renderer,
&theme,
&renderer::Style::default(),
app_state.cursor,
);
app_state.cache = interface.into_cache();
app_state.renderer.present(
None,
frame.texture.format(),
&view,
&app_state.viewport,
);
frame.present();
}
Err(e) => {
println!("error: {e}");
}
}
}
WindowEvent::CursorMoved { position, .. } => {
app_state.cursor = mouse::Cursor::Available(conversion::cursor_position(
position,
app_state.viewport.scale_factor(),
))
}
WindowEvent::ModifiersChanged(new_modifiers) => {
app_state.modifiers = new_modifiers.state();
}
WindowEvent::Resized(_) => {
app_state.resized = true;
}
WindowEvent::CloseRequested => {
event_loop.exit();
}
_ => (),
}
if let Some(event) = iced_winit::conversion::window_event(
event,
app_state.window.scale_factor() as _,
app_state.modifiers,
) {
app_state.events.push(event)
}
if !app_state.events.is_empty() {
let mut interface = UserInterface::build(
app_state.counter.view(),
app_state.viewport.logical_size(),
std::mem::take(&mut app_state.cache),
&mut app_state.renderer,
);
let mut messages = Vec::new();
let _ = interface.update(
&app_state.events,
app_state.cursor,
&mut app_state.renderer,
&mut app_state.clipboard,
&mut messages,
);
app_state.events.clear();
app_state.cache = interface.into_cache();
for message in messages {
app_state.counter.update(message);
}
app_state.window.request_redraw();
}
}
fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, mut event: State) {
event.window.request_redraw();
self.state = Some(event)
}
}
// https://ggando.com/til/wgpu-font/
fn register_font_manually(font_data: &'static [u8]) {
use std::sync::RwLockWriteGuard;
// Get a mutable reference to the font system
let font_system = font_system();
let mut font_system_guard: RwLockWriteGuard<_> = font_system
.write()
.expect("Failed to acquire font system lock");
// Load the font into the global font system
font_system_guard.load_font(Cow::Borrowed(font_data));
for f in font_system_guard.raw().db().faces() {
dbg2!(&f.post_script_name);
}
}
fn main() -> Result<()> {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Info).unwrap_throw();
log::info!("hello");
let event_loop = EventLoop::with_user_event().build()?;
let mut app = App::new(&event_loop);
event_loop.run_app(&mut app)?;
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment