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.rs503
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));
+ }
+}