Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-ui/src/main.rs')
| -rw-r--r-- | helix-ui/src/main.rs | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/helix-ui/src/main.rs b/helix-ui/src/main.rs new file mode 100644 index 00000000..47c0c7ad --- /dev/null +++ b/helix-ui/src/main.rs @@ -0,0 +1,503 @@ +use parley::{ + layout::Alignment, + style::{FontFamily, FontStack, StyleProperty}, + FontContext, LayoutContext, +}; +use std::borrow::Cow; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; + +use wgpu::util::DeviceExt; + +// new femto-like framework: +// wgpu renderer +// kurbo, (alternative is euclid + lyon) +// vector math: glam? and drop euclid (glam is faster https://docs.rs/glam/latest/glam/) +// swash + parley for text + +// imgref, bitflags +// fnv, rgb + +// resource, image +// usvg for svg + +use swash::{ + scale::ScaleContext, + shape::ShapeContext, + text::Script, + zeno::{Vector, Verb}, + Attributes, CacheKey, Charmap, FontRef, +}; + +use lyon::{ + math::{point, Transform}, + path::{builder::*, Path}, + tessellation::{BuffersBuilder, FillOptions, FillTessellator, FillVertex, VertexBuffers}, +}; + +use bytemuck::{Pod, Zeroable}; + +// Vertex for lines drawn by lyon +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +struct Vertex { + position: [f32; 2], + // color: [f32; 4], // Use this when I want more colors +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +struct View { + size: [f32; 2], +} + +fn font() -> VertexBuffers<Vertex, u16> { + let data = std::fs::read("assets/fonts/ttf/FiraCode-Regular.ttf").unwrap(); + + // -- Shaping + // let mut context = ShapeContext::new(); + // let mut shaper = context + // .builder(font) + // .script(Script::Latin) + // .size(12.) + // .variations(&[("wght", 400.0)]) + // .build(); + + // shaper.add_str("a quick brown fox?"); + + // add_str with boundary analysis + // use swash::text::{analyze, Script}; + // use swash::text::cluster::{CharInfo, Parser, Token}; + // let text = "a quick brown fox?"; + // let mut parser = Parser::new( + // Script::Latin, + // text.char_indices() + // // Call analyze passing the same text and zip + // // the results + // .zip(analyze(text.chars())) + // // Analyze yields the tuple (Properties, Boundary) + // .map(|((i, ch), (props, boundary))| Token { + // ch, + // offset: i as u32, + // len: ch.len_utf8() as u8, + // // Create character information from properties and boundary + // info: CharInfo::new(props, boundary), + // data: 0, + // }), + // ); + + // shaper.shape_with(|c| { + // // use the glyph cluster + // // c.glyphs + // }); + + // -- Scaling + + // let mut scaler = scale_ctx + // .builder(font) + // .hint(true) + // .size(12.) + // .variations(&[("wght", 400.0)]) + // .build(); + // let glyph_id = font.charmap().map('H'); + // let outline = scaler.scale_outline(glyph_id).unwrap(); + + // -- Layout + + let mut font_ctx = FontContext::new(); + let font_family = font_ctx.register_fonts(data).unwrap(); + let mut layout_ctx: LayoutContext<[u8; 4]> = LayoutContext::new(); + + // Encode glyphs into lyon paths + let mut encoder = Path::builder(); + let mut encoder = encoder.transformed(Transform::default()); + + let mut builder = layout_ctx.ranged_builder(&mut font_ctx, "fn draw_edit_box_base<T: Renderer>(canvas: &mut Canvas<T>, x: f32, y: f32, w: f32, h: f32) { ", 1.); + builder.push_default(&StyleProperty::FontStack(FontStack::Single( + FontFamily::Named(&font_family), + ))); + builder.push_default(&StyleProperty::FontSize(12.)); + builder.push_default(&StyleProperty::Brush([255, 255, 255, 255])); + // builder.push() with range to set styling + let mut layout = builder.build(); + let max_width = None; + layout.break_all_lines(max_width, Alignment::Start); + + let mut scale_ctx = ScaleContext::new(); + + for line in layout.lines() { + let mut last_x = 0.0; + let mut last_y = 0.0; + + for glyph_run in line.glyph_runs() { + let run = glyph_run.run(); + let color = &glyph_run.style().brush; + let font = run.font(); + let font = font.as_ref(); + + let mut first = true; + + // TODO: handle .variations(&[("wght", 400.0)]) + let mut scaler = scale_ctx.builder(font).size(run.font_size()).build(); + + for glyph in glyph_run.positioned_glyphs() { + let delta_x = glyph.x - last_x; + let delta_y = glyph.y - last_y; + + last_x = glyph.x; + last_y = glyph.y; + + if first { + // TODO: handle underline + // TODO: handle strikethrough + } + first = false; + + encoder.set_transform(Transform::new( + 1.0, 0.0, // + 0.0, -1.0, // invert y axis + glyph.x, glyph.y, + )); + + if let Some(outline) = scaler.scale_outline(glyph.id) { + append_outline(&mut encoder, outline.verbs(), outline.points()); + }; + } + } + } + + // -- Tesselation + let path = encoder.build(); + + let mut geometry: VertexBuffers<Vertex, u16> = VertexBuffers::new(); + + let mut tessellator = FillTessellator::new(); + { + // Compute the tessellation. + tessellator + .tessellate_path( + &path, + &FillOptions::non_zero().with_tolerance(0.01), // defaults to 0.1, compare further + &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| Vertex { + position: vertex.position().to_array(), + }), + ) + .unwrap(); + } + + geometry +} + +fn append_outline<T: lyon::path::builder::PathBuilder>( + encoder: &mut T, + verbs: &[Verb], + points: &[Vector], +) { + let mut i = 0; + for verb in verbs { + match verb { + Verb::MoveTo => { + let p = points[i]; + // TODO: can MoveTo appear halfway through? + encoder.begin(point(p.x, p.y)); + i += 1; + } + Verb::LineTo => { + let p = points[i]; + encoder.line_to(point(p.x, p.y)); + i += 1; + } + Verb::QuadTo => { + let p1 = points[i]; + let p2 = points[i + 1]; + encoder.quadratic_bezier_to(point(p1.x, p1.y), point(p2.x, p2.y)); + i += 2; + } + Verb::CurveTo => { + let p1 = points[i]; + let p2 = points[i + 1]; + let p3 = points[i + 2]; + encoder.cubic_bezier_to(point(p1.x, p1.y), point(p2.x, p2.y), point(p3.x, p3.y)); + i += 3; + } + Verb::Close => { + encoder.close(); + } + } + } +} + +fn create_multisampled_framebuffer( + device: &wgpu::Device, + config: &wgpu::SurfaceConfiguration, + sample_count: u32, +) -> wgpu::TextureView { + let multisampled_texture_extent = wgpu::Extent3d { + width: config.width, + height: config.height, + depth_or_array_layers: 1, + }; + let multisampled_frame_descriptor = &wgpu::TextureDescriptor { + size: multisampled_texture_extent, + mip_level_count: 1, + sample_count, + dimension: wgpu::TextureDimension::D2, + format: config.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + label: None, + }; + + device + .create_texture(multisampled_frame_descriptor) + .create_view(&wgpu::TextureViewDescriptor::default()) +} + +async fn run(event_loop: EventLoop<()>, window: Window) { + let sample_count = 4; + + let size = window.inner_size(); + let instance = wgpu::Instance::new(wgpu::Backends::all()); + let surface = unsafe { instance.create_surface(&window) }; + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), // TODO: select based on backend + force_fallback_adapter: false, + // Request an adapter which can render to our surface + compatible_surface: Some(&surface), + }) + .await + .expect("Failed to find an appropriate adapter"); + + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + // Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain. + limits: wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()), + }, + None, + ) + .await + .expect("Failed to create device"); + + // Load the shaders from disk + let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))), + }); + + // --- + + let geometry = font(); + + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(&geometry.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(&geometry.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + // + + // TODO: use size fetched before + let data = View { size: [0.0, 0.0] }; + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Uniform Buffer"), + contents: bytemuck::cast_slice(&[data]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let uniform_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("uniform_bind_group_layout"), + }); + + let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &uniform_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + label: Some("uniform_bind_group"), + }); + + // + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&uniform_bind_group_layout], // &texture_bind_group_layout + push_constant_ranges: &[], // TODO: could use push constants for uniforms but that's not available on web + }); + + let swapchain_format = surface.get_preferred_format(&adapter).unwrap(); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &wgpu::vertex_attr_array![0 => Float32x2], + }], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[swapchain_format.into()], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: sample_count, + ..Default::default() + }, + multiview: None, + }); + + let mut config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }; + + let mut multisampled_framebuffer = + create_multisampled_framebuffer(&device, &config, sample_count); + + surface.configure(&device, &config); + + // + + event_loop.run(move |event, _, control_flow| { + // Have the closure take ownership of the resources. + // `event_loop.run` never returns, therefore we must do this to ensure + // the resources are properly cleaned up. + let _ = (&instance, &adapter, &shader, &pipeline_layout); + + *control_flow = ControlFlow::Wait; + match event { + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + // Reconfigure the surface with the new size + config.width = size.width; + config.height = size.height; + + multisampled_framebuffer = + create_multisampled_framebuffer(&device, &config, sample_count); + + surface.configure(&device, &config); + // On macos the window needs to be redrawn manually after resizing + window.request_redraw(); + } + Event::RedrawRequested(_) => { + let frame = surface + .get_current_texture() + .expect("Failed to acquire next swap chain texture"); + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + // TODO: need to use queue.write_buffer or staging_belt to write to it + + // Pass the current window size in + let dpi_factor = window.scale_factor(); + let size = window.inner_size(); + let winit::dpi::LogicalSize { width, height } = size.to_logical::<f32>(dpi_factor); + + let data = View { + size: [width, height], + }; + + queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[data])); + + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[wgpu::RenderPassColorAttachment { + view: &multisampled_framebuffer, + resolve_target: Some(&view), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + + // rpass.set_viewport(); + + rpass.set_pipeline(&render_pipeline); + rpass.set_bind_group(0, &uniform_bind_group, &[]); + rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16); + rpass.set_vertex_buffer(0, vertex_buffer.slice(..)); + rpass.draw_indexed(0..(geometry.indices.len() as u32), 0, 0..1); + } + + queue.submit(Some(encoder.finish())); + frame.present(); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => {} + } + }); +} + +fn main() { + let event_loop = EventLoop::new(); + let window = winit::window::Window::new(&event_loop).unwrap(); + #[cfg(not(target_arch = "wasm32"))] + { + env_logger::init(); + // Temporarily avoid srgb formats for the swapchain on the web + pollster::block_on(run(event_loop, window)); + } + #[cfg(target_arch = "wasm32")] + { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init().expect("could not initialize logger"); + use winit::platform::web::WindowExtWebSys; + // On wasm, append the canvas to the document body + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| { + body.append_child(&web_sys::Element::from(window.canvas())) + .ok() + }) + .expect("couldn't append canvas to document body"); + wasm_bindgen_futures::spawn_local(run(event_loop, window)); + } +} |