Created
September 15, 2025 15:58
-
-
Save MarinPostma/8b1411569603a60c472334294490b420 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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