Add examples/breakout

This commit is contained in:
Richard Feldman 2022-04-04 10:02:13 -04:00
parent 97fff2e84d
commit a111f510a4
No known key found for this signature in database
GPG key ID: 7E4127D1E4241798
27 changed files with 5036 additions and 0 deletions

1
examples/breakout/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
hello-gui

View file

@ -0,0 +1,30 @@
app "hello-gui"
packages { pf: "platform" }
imports []# [ pf.Action.{ Action }, pf.Elem.{ button, text, row, col } ]
provides [ program ] to pf
program = { render }
render = \state ->
div0 = \numerator, denominator -> (numerator / denominator) |> Result.withDefault 0
rgba = \r, g, b, a -> { r: div0 r 255, g: div0 g 255, b: div0 b 255, a }
styles = { bgColor: rgba 100 50 50 1, borderColor: rgba 10 20 30 1, borderWidth: 10, textColor: rgba 220 220 250 1 }
height = if state.height == 1000 then "correct!" else if state.height == 0 then "zero" else "incorrect"
width = if state.width == 1900 then "Correct!" else if state.width == 0 then "zero" else "Incorrect"
Col
[
Row
[
Button (Text "Corner ") styles,
Button (Text "Top Mid ") { styles & bgColor: rgba 100 100 50 1 },
Button (Text "Top Right ") { styles & bgColor: rgba 50 50 150 1 },
],
Button (Text "Mid Left ") { styles & bgColor: rgba 150 100 100 1 },
Button (Text "Bottom Left") { styles & bgColor: rgba 150 50 50 1 },
Button (Text "height: \(height)") { styles & bgColor: rgba 50 150 50 1 },
Button (Text "width: \(width)") { styles & bgColor: rgba 50 100 50 1 },
]

1
examples/breakout/platform/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target

View file

@ -0,0 +1,20 @@
interface Action
exposes [ Action, none, update, map ]
imports []
Action state : [ None, Update state ]
none : Action *
none = None
update : state -> Action state
update = Update
map : Action a, (a -> b) -> Action b
map = \action, transform ->
when action is
None ->
None
Update state ->
Update (transform state)

2835
examples/breakout/platform/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
[package]
name = "host"
version = "0.1.0"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2018"
# Needed to be able to run on non-Windows systems for some reason. Without this, cargo panics with:
#
# error: DX12 API enabled on non-Windows OS. If your project is not using resolver="2" in Cargo.toml, it should.
resolver = "2"
[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "rlib"]
[[bin]]
name = "host"
path = "src/main.rs"
[dependencies]
roc_std = { path = "../../../roc_std" }
libc = "0.2"
arrayvec = "0.7.2"
page_size = "0.4.2"
# when changing winit version, check if copypasta can be updated simultaneously so they use the same versions for their dependencies. This will save build time.
winit = "0.26.1"
wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" }
wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" }
glyph_brush = "0.7.2"
log = "0.4.14"
env_logger = "0.9.0"
futures = "0.3.17"
cgmath = "0.18.0"
snafu = { version = "0.6.10", features = ["backtraces"] }
colored = "2.0.0"
pest = "2.1.3"
pest_derive = "2.1.0"
copypasta = "0.7.1"
palette = "0.6.0"
confy = { git = 'https://github.com/rust-cli/confy', features = [
"yaml_conf"
], default-features = false }
serde = { version = "1.0.130", features = ["derive"] }
nonempty = "0.7.0"
fs_extra = "1.2.0"
rodio = { version = "0.14.0", optional = true } # to play sounds
threadpool = "1.8.1"
[package.metadata.cargo-udeps.ignore]
# confy is currently unused but should not be removed
normal = ["confy"]
#development = []
#build = []
[features]
default = []
with_sound = ["rodio"]
[dependencies.bytemuck]
version = "1.7.2"
features = ["derive"]
[workspace]
# Optimizations based on https://deterministic.space/high-performance-rust.html
[profile.release]
lto = "thin"
codegen-units = 1
# debug = true # enable when profiling
[profile.bench]
lto = "thin"
codegen-units = 1

View file

@ -0,0 +1,193 @@
interface Elem
exposes [ Elem, PressEvent, row, col, text, button, none, translate, list ]
imports [ Action.{ Action } ]
Elem state :
# PERFORMANCE NOTE:
# If there are 8 or fewer tags here, then on a 64-bit system, the tag can be stored
# in the pointer - for massive memory savings. Try extremely hard to always limit the number
# of tags in this union to 8 or fewer!
[
Button (ButtonConfig state) (Elem state),
Text Str,
Col (List (Elem state)),
Row (List (Elem state)),
Lazy (Result { state, elem : Elem state } [ NotCached ] -> { state, elem : Elem state }),
# TODO FIXME: using this definition of Lazy causes a stack overflow in the compiler!
# Lazy (Result (Cached state) [ NotCached ] -> Cached state),
None,
]
## Used internally in the type definition of Lazy
Cached state : { state, elem : Elem state }
ButtonConfig state : { onPress : state, PressEvent -> Action state }
PressEvent : { button : [ Touch, Mouse [ Left, Right, Middle ] ] }
text : Str -> Elem *
text = \str ->
Text str
button : { onPress : state, PressEvent -> Action state }, Elem state -> Elem state
button = \config, label ->
Button config label
row : List (Elem state) -> Elem state
row = \children ->
Row children
col : List (Elem state) -> Elem state
col = \children ->
Col children
lazy : state, (state -> Elem state) -> Elem state
lazy = \state, render ->
# This function gets called by the host during rendering. It will
# receive the cached state and element (wrapped in Ok) if we've
# ever rendered this before, and Err otherwise.
Lazy
\result ->
when result is
Ok cached if cached.state == state ->
# If we have a cached value, and the new state is the
# same as the cached one, then we can return exactly
# what we had cached.
cached
_ ->
# Either the state changed or else we didn't have a
# cached value to use. Either way, we need to render
# with the new state and store that for future use.
{ state, elem: render state }
none : Elem *
none = None# I've often wanted this in elm/html. Usually end up resorting to (Html.text "") - this seems nicer.
## Change an element's state type.
##
## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed.
## State : { photo : Photo }
##
## render : State -> Elem State
## render = \state ->
## child : Elem State
## child =
## Photo.render state.photo
## |> Elem.translate .photo &photo
##
## col {} [ child, otherElems ]
##
translate = \child, toChild, toParent ->
when child is
Text str ->
Text str
Col elems ->
Col (List.map elems \elem -> translate elem toChild toParent)
Row elems ->
Row (List.map elems \elem -> translate elem toChild toParent)
Button config label ->
onPress = \parentState, event ->
toChild parentState
|> config.onPress event
|> Action.map \c -> toParent parentState c
Button { onPress } (translate label toChild toParent)
Lazy renderChild ->
Lazy
\parentState ->
{ elem, state } = renderChild (toChild parentState)
{
elem: translate toChild toParent newChild,
state: toParent parentState state,
}
None ->
None
## Render a list of elements, using [Elem.translate] on each of them.
##
## Convenient when you have a [List] in your state and want to make
## a [List] of child elements out of it.
##
## TODO: indent the following once https://github.com/rtfeldman/roc/issues/2585 is fixed.
## State : { photos : List Photo }
##
## render : State -> Elem State
## render = \state ->
## children : List (Elem State)
## children =
## Elem.list Photo.render state .photos &photos
##
## col {} children
## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed
list : (child -> Elem child), parent, (parent -> List child), (parent, List child -> parent) -> List (Elem parent)
list = \renderChild, parent, toChildren, toParent ->
List.mapWithIndex
(toChildren parent)
\index, child ->
toChild = \par -> List.get (toChildren par) index
newChild = translateOrDrop
child
toChild
\par, ch ->
toChildren par
|> List.set ch index
|> toParent
renderChild newChild
## Internal helper function for Elem.list
##
## Tries to translate a child to a parent, but
## if the child has been removed from the parent,
## drops it.
##
## TODO: format as multiline type annotation once https://github.com/rtfeldman/roc/issues/2586 is fixed
translateOrDrop : Elem child, (parent -> Result child *), (parent, child -> parent) -> Elem parent
translateOrDrop = \child, toChild, toParent ->
when child is
Text str ->
Text str
Col elems ->
Col (List.map elems \elem -> translateOrDrop elem toChild toParent)
Row elems ->
Row (List.map elems \elem -> translateOrDrop elem toChild toParent)
Button config label ->
onPress = \parentState, event ->
when toChild parentState is
Ok newChild ->
newChild
|> config.onPress event
|> Action.map \c -> toParent parentState c
Err _ ->
# The child was removed from the list before this onPress handler resolved.
# (For example, by a previous event handler that fired simultaneously.)
Action.none
Button { onPress } (translateOrDrop label toChild toParent)
Lazy childState renderChild ->
Lazy
(toParent childState)
\parentState ->
when toChild parentState is
Ok newChild ->
renderChild newChild
|> translateOrDrop toChild toParent
Err _ ->
None
# I don't think this should ever happen in practice.
None ->
None

View file

@ -0,0 +1,20 @@
platform "gui"
requires {} { program : Program State }
exposes []
packages {}
imports []
provides [ programForHost ]
Rgba : { r : F32, g : F32, b : F32, a : F32 }
ButtonStyles : { bgColor : Rgba, borderColor : Rgba, borderWidth : F32, textColor : Rgba }
Elem : [ Button Elem ButtonStyles, Col (List Elem), Row (List Elem), Text Str ]
State : { width : F32, height : F32 }
Program state : { render : state -> Elem }
# TODO allow changing the title - maybe via Action.setTitle
programForHost : { render : (State -> Elem) as Render }
programForHost = program

View file

@ -0,0 +1,3 @@
extern int rust_main();
int main() { return rust_main(); }

View file

@ -0,0 +1,172 @@
use crate::roc::{ElemId, RocElem, RocElemTag};
#[derive(Debug, PartialEq, Eq)]
pub struct Focus {
focused: Option<ElemId>,
focused_ancestors: Vec<(ElemId, usize)>,
}
impl Default for Focus {
fn default() -> Self {
Self {
focused: None,
focused_ancestors: Vec::new(),
}
}
}
impl Focus {
pub fn focused_elem(&self) -> Option<ElemId> {
self.focused
}
/// e.g. the user pressed Tab.
///
/// This is in contrast to next_local, which advances within a button group.
/// For example, if I have three radio buttons in a group, pressing the
/// arrow keys will cycle through them over and over without exiting the group -
/// whereas pressing Tab will cycle through them once and then exit the group.
pub fn next_global(&mut self, root: &RocElem) {
match self.focused {
Some(focused) => {
// while let Some((ancestor_id, index)) = self.focused_ancestors.pop() {
// let ancestor = ancestor_id.elem();
// // TODO FIXME - right now this will re-traverse a lot of ground! To prevent this,
// // we should remember past indices searched, and tell the ancestors "hey stop searching when"
// // you reach these indices, because they were already covered previously.
// // One potentially easy way to do this: pass a min_index and max_index, and only look between those!
// //
// // Related idea: instead of doing .pop() here, iterate normally so we can `break;` after storing
// // `new_ancestors = Some(next_ancestors);` - this way, we still have access to the full ancestry, and
// // can maybe even pass it in to make it clear what work has already been done!
// if let Some((new_id, new_ancestors)) =
// Self::next_focusable_sibling(focused, Some(ancestor), Some(index))
// {
// // We found the next element to focus, so record that.
// self.focused = Some(new_id);
// // We got a path to the new focusable's ancestor(s), so add them to the path.
// // (This may restore some of the ancestors we've been .pop()-ing as we iterated.)
// self.focused_ancestors.extend(new_ancestors);
// return;
// }
// // Need to write a bunch of tests for this, especially tests of focus wrapping around - e.g.
// // what happens if it wraps around to a sibling? What happens if it wraps around to something
// // higher up the tree? Lower down the tree? What if nothing is focusable?
// // A separate question: what if we should have a separate text-to-speech concept separate from focus?
// }
}
None => {
// Nothing was focused in the first place, so try to focus the root.
if root.is_focusable() {
self.focused = Some(root.id());
self.focused_ancestors = Vec::new();
} else if let Some((new_id, new_ancestors)) =
Self::next_focusable_sibling(root, None, None)
{
// If the root itself is not focusable, use its next focusable sibling.
self.focused = Some(new_id);
self.focused_ancestors = new_ancestors;
}
// Regardless of whether we found a focusable Elem, we're done.
return;
}
}
}
/// Return the next focusable sibling element after this one.
/// If this element has no siblings, or no *next* sibling after the given index
/// (e.g. the given index refers to the last element in a Row element), return None.
fn next_focusable_sibling(
elem: &RocElem,
ancestor: Option<&RocElem>,
opt_index: Option<usize>,
) -> Option<(ElemId, Vec<(ElemId, usize)>)> {
use RocElemTag::*;
match elem.tag() {
Button | Text => None,
Row | Col => {
let children = unsafe { &elem.entry().row_or_col.children.as_slice() };
let iter = match opt_index {
Some(focus_index) => children[0..focus_index].iter(),
None => children.iter(),
};
for child in iter {
if let Some(focused) = Self::next_focusable_sibling(child, ancestor, None) {
return Some(focused);
}
}
None
}
}
}
}
#[test]
fn next_global_button_root() {
use crate::roc::{ButtonStyles, RocElem};
let child = RocElem::text("");
let root = RocElem::button(ButtonStyles::default(), child);
let mut focus = Focus::default();
// At first, nothing should be focused.
assert_eq!(focus.focused_elem(), None);
focus.next_global(&root);
// Buttons should be focusable, so advancing focus should give the button focus.
assert_eq!(focus.focused_elem(), Some(root.id()));
// Since the button is at the root, advancing again should maintain focus on it.
focus.next_global(&root);
assert_eq!(focus.focused_elem(), Some(root.id()));
}
#[test]
fn next_global_text_root() {
let root = RocElem::text("");
let mut focus = Focus::default();
// At first, nothing should be focused.
assert_eq!(focus.focused_elem(), None);
focus.next_global(&root);
// Text should not be focusable, so advancing focus should have no effect here.
assert_eq!(focus.focused_elem(), None);
// Just to double-check, advancing a second time should not change this.
focus.next_global(&root);
assert_eq!(focus.focused_elem(), None);
}
#[test]
fn next_global_row() {
use crate::roc::{ButtonStyles, RocElem};
let child = RocElem::text("");
let button = RocElem::button(ButtonStyles::default(), child);
let button_id = button.id();
let root = RocElem::row(&[button] as &[_]);
let mut focus = Focus::default();
// At first, nothing should be focused.
assert_eq!(focus.focused_elem(), None);
focus.next_global(&root);
// Buttons should be focusable, so advancing focus should give the first button in the row focus.
assert_eq!(focus.focused_elem(), Some(button_id));
// Since the button is the only element in the row, advancing again should maintain focus on it.
focus.next_global(&root);
assert_eq!(focus.focused_elem(), Some(button_id));
}

View file

@ -0,0 +1,50 @@
use cgmath::Vector4;
use palette::{FromColor, Hsv, Srgb};
/// This order is optimized for what Roc will send
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub struct Rgba {
a: f32,
b: f32,
g: f32,
r: f32,
}
impl Rgba {
pub const WHITE: Self = Self::new(1.0, 1.0, 1.0, 1.0);
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const fn to_array(self) -> [f32; 4] {
[self.r, self.b, self.g, self.a]
}
pub fn from_hsb(hue: usize, saturation: usize, brightness: usize) -> Self {
Self::from_hsba(hue, saturation, brightness, 1.0)
}
pub fn from_hsba(hue: usize, saturation: usize, brightness: usize, alpha: f32) -> Self {
let rgb = Srgb::from_color(Hsv::new(
hue as f32,
(saturation as f32) / 100.0,
(brightness as f32) / 100.0,
));
Self::new(rgb.red, rgb.green, rgb.blue, alpha)
}
}
impl From<Rgba> for [f32; 4] {
fn from(rgba: Rgba) -> Self {
rgba.to_array()
}
}
impl From<Rgba> for Vector4<f32> {
fn from(rgba: Rgba) -> Self {
Vector4::new(rgba.r, rgba.b, rgba.g, rgba.a)
}
}

View file

@ -0,0 +1,96 @@
// Contains parts of https://github.com/sotrh/learn-wgpu
// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS
// file in the root directory of this distribution.
//
// Thank you, Benjamin!
// Contains parts of https://github.com/iced-rs/iced/blob/adce9e04213803bd775538efddf6e7908d1c605e/wgpu/src/shader/quad.wgsl
// By Héctor Ramón, Iced contributors Licensed under the MIT license.
// The license is included in the LEGAL_DETAILS file in the root directory of this distribution.
// Thank you Héctor Ramón and Iced contributors!
use std::mem;
use super::{quad::Quad, vertex::Vertex};
use crate::graphics::primitives::rect::RectElt;
use wgpu::util::DeviceExt;
pub struct RectBuffers {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub quad_buffer: wgpu::Buffer,
}
pub const QUAD_INDICES: [u16; 6] = [0, 1, 2, 0, 2, 3];
const QUAD_VERTS: [Vertex; 4] = [
Vertex {
_position: [0.0, 0.0],
},
Vertex {
_position: [1.0, 0.0],
},
Vertex {
_position: [1.0, 1.0],
},
Vertex {
_position: [0.0, 1.0],
},
];
pub const MAX_QUADS: usize = 1_000;
pub fn create_rect_buffers(
gpu_device: &wgpu::Device,
cmd_encoder: &mut wgpu::CommandEncoder,
rects: &[RectElt],
) -> RectBuffers {
let vertex_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&QUAD_VERTS),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&QUAD_INDICES),
usage: wgpu::BufferUsages::INDEX,
});
let quad_buffer = gpu_device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: mem::size_of::<Quad>() as u64 * MAX_QUADS as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let quads: Vec<Quad> = rects.iter().map(|rect| to_quad(rect)).collect();
let buffer_size = (quads.len() as u64) * Quad::SIZE;
let staging_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: None,
contents: bytemuck::cast_slice(&quads),
usage: wgpu::BufferUsages::COPY_SRC,
});
cmd_encoder.copy_buffer_to_buffer(&staging_buffer, 0, &quad_buffer, 0, buffer_size);
RectBuffers {
vertex_buffer,
index_buffer,
quad_buffer,
}
}
pub fn to_quad(rect_elt: &RectElt) -> Quad {
Quad {
pos: rect_elt.rect.pos.into(),
width: rect_elt.rect.width,
height: rect_elt.rect.height,
color: (rect_elt.color.to_array()),
border_color: rect_elt.border_color.into(),
border_width: rect_elt.border_width,
}
}

View file

@ -0,0 +1,5 @@
pub mod buffer;
pub mod ortho;
pub mod pipelines;
pub mod vertex;
pub mod quad;

View file

@ -0,0 +1,118 @@
use cgmath::{Matrix4, Ortho};
use wgpu::util::DeviceExt;
use wgpu::{
BindGroup, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, Buffer,
ShaderStages,
};
// orthographic projection is used to transform pixel coords to the coordinate system used by wgpu
#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
// We can't use cgmath with bytemuck directly so we'll have
// to convert the Matrix4 into a 4x4 f32 array
ortho: [[f32; 4]; 4],
}
impl Uniforms {
fn new(w: u32, h: u32) -> Self {
let ortho: Matrix4<f32> = Ortho::<f32> {
left: 0.0,
right: w as f32,
bottom: h as f32,
top: 0.0,
near: -1.0,
far: 1.0,
}
.into();
Self {
ortho: ortho.into(),
}
}
}
// update orthographic buffer according to new window size
pub fn update_ortho_buffer(
inner_width: u32,
inner_height: u32,
gpu_device: &wgpu::Device,
ortho_buffer: &Buffer,
cmd_queue: &wgpu::Queue,
) {
let new_uniforms = Uniforms::new(inner_width, inner_height);
let new_ortho_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Ortho uniform buffer"),
contents: bytemuck::cast_slice(&[new_uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_SRC,
});
// get a command encoder for the current frame
let mut encoder = gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Resize"),
});
// overwrite the new buffer over the old one
encoder.copy_buffer_to_buffer(
&new_ortho_buffer,
0,
ortho_buffer,
0,
(std::mem::size_of::<Uniforms>() * vec![new_uniforms].as_slice().len())
as wgpu::BufferAddress,
);
cmd_queue.submit(Some(encoder.finish()));
}
#[derive(Debug)]
pub struct OrthoResources {
pub buffer: Buffer,
pub bind_group_layout: BindGroupLayout,
pub bind_group: BindGroup,
}
pub fn init_ortho(
inner_width: u32,
inner_height: u32,
gpu_device: &wgpu::Device,
) -> OrthoResources {
let uniforms = Uniforms::new(inner_width, inner_height);
let ortho_buffer = gpu_device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Ortho uniform buffer"),
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
// bind groups consist of extra resources that are provided to the shaders
let ortho_bind_group_layout = gpu_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("Ortho bind group layout"),
});
let ortho_bind_group = gpu_device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &ortho_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: ortho_buffer.as_entire_binding(),
}],
label: Some("Ortho bind group"),
});
OrthoResources {
buffer: ortho_buffer,
bind_group_layout: ortho_bind_group_layout,
bind_group: ortho_bind_group,
}
}

View file

@ -0,0 +1,72 @@
use super::ortho::{init_ortho, OrthoResources};
use super::quad::Quad;
use super::vertex::Vertex;
use std::borrow::Cow;
pub struct RectResources {
pub pipeline: wgpu::RenderPipeline,
pub ortho: OrthoResources,
}
pub fn make_rect_pipeline(
gpu_device: &wgpu::Device,
surface_config: &wgpu::SurfaceConfiguration,
) -> RectResources {
let ortho = init_ortho(surface_config.width, surface_config.height, gpu_device);
let pipeline_layout = gpu_device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&ortho.bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = create_render_pipeline(
gpu_device,
&pipeline_layout,
surface_config.format,
&wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../shaders/quad.wgsl"))),
},
);
RectResources { pipeline, ortho }
}
pub fn create_render_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
color_format: wgpu::TextureFormat,
shader_module_desc: &wgpu::ShaderModuleDescriptor,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(shader_module_desc);
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Render pipeline"),
layout: Some(layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[Vertex::DESC, Quad::DESC],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
operation: wgpu::BlendOperation::Add,
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
},
alpha: wgpu::BlendComponent::REPLACE,
}),
write_mask: wgpu::ColorWrites::ALL,
}],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
})
}

View file

@ -0,0 +1,31 @@
/// A polygon with 4 corners
#[derive(Copy, Clone)]
pub struct Quad {
pub pos: [f32; 2],
pub width: f32,
pub height: f32,
pub color: [f32; 4],
pub border_color: [f32; 4],
pub border_width: f32,
}
unsafe impl bytemuck::Pod for Quad {}
unsafe impl bytemuck::Zeroable for Quad {}
impl Quad {
pub const SIZE: wgpu::BufferAddress = std::mem::size_of::<Self>() as wgpu::BufferAddress;
pub const DESC: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: Self::SIZE,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array!(
1 => Float32x2,
2 => Float32,
3 => Float32,
4 => Float32x4,
5 => Float32x4,
6 => Float32,
),
};
}

View file

@ -0,0 +1,35 @@
// Inspired by https://github.com/sotrh/learn-wgpu
// by Benjamin Hansen - license information can be found in the LEGAL_DETAILS
// file in the root directory of this distribution.
//
// Thank you, Benjamin!
// Inspired by https://github.com/iced-rs/iced/blob/adce9e04213803bd775538efddf6e7908d1c605e/wgpu/src/shader/quad.wgsl
// By Héctor Ramón, Iced contributors Licensed under the MIT license.
// The license is included in the LEGAL_DETAILS file in the root directory of this distribution.
// Thank you Héctor Ramón and Iced contributors!
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Clone, Zeroable, Pod)]
pub struct Vertex {
pub _position: [f32; 2],
}
impl Vertex {
pub const SIZE: wgpu::BufferAddress = std::mem::size_of::<Self>() as wgpu::BufferAddress;
pub const DESC: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: Self::SIZE,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
// position
wgpu::VertexAttribute {
offset: 0,
shader_location: 0,
format: wgpu::VertexFormat::Float32x2,
},
],
};
}

View file

@ -0,0 +1,4 @@
pub mod colors;
pub mod lowlevel;
pub mod primitives;
pub mod style;

View file

@ -0,0 +1,2 @@
pub mod rect;
pub mod text;

View file

@ -0,0 +1,27 @@
use crate::graphics::colors::Rgba;
use cgmath::Vector2;
#[derive(Debug, Copy, Clone)]
pub struct RectElt {
pub rect: Rect,
pub color: Rgba,
pub border_width: f32,
pub border_color: Rgba,
}
/// These fields are ordered this way because in Roc, the corresponding stuct is:
///
/// { top : F32, left : F32, width : F32, height : F32 }
///
/// alphabetically, that's { height, left, top, width } - which works out to the same as:
///
/// struct Rect { height: f32, pos: Vector2<f32>, width: f32 }
///
/// ...because Vector2<f32> is a repr(C) struct of { x: f32, y: f32 }
#[derive(Debug, Copy, Clone)]
#[repr(C)]
pub struct Rect {
pub height: f32,
pub pos: Vector2<f32>,
pub width: f32,
}

View file

@ -0,0 +1,134 @@
// Adapted from https://github.com/sotrh/learn-wgpu
// by Benjamin Hansen - license information can be found in the COPYRIGHT
// file in the root directory of this distribution.
//
// Thank you, Benjamin!
use crate::graphics::colors::Rgba;
use crate::graphics::style::DEFAULT_FONT_SIZE;
use ab_glyph::{FontArc, InvalidFont};
use cgmath::Vector2;
use wgpu_glyph::{ab_glyph, GlyphBrush, GlyphBrushBuilder};
#[derive(Debug)]
pub struct Text<'a> {
pub position: Vector2<f32>,
pub area_bounds: Vector2<f32>,
pub color: Rgba,
pub text: &'a str,
pub size: f32,
pub visible: bool,
pub centered: bool,
}
impl<'a> Default for Text<'a> {
fn default() -> Self {
Self {
position: (0.0, 0.0).into(),
area_bounds: (std::f32::INFINITY, std::f32::INFINITY).into(),
color: Rgba::WHITE,
text: "",
size: DEFAULT_FONT_SIZE,
visible: true,
centered: false,
}
}
}
// pub fn layout_from_text(text: &Text) -> wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker> {
// wgpu_glyph::Layout::default().h_align(if text.centered {
// wgpu_glyph::HorizontalAlign::Center
// } else {
// wgpu_glyph::HorizontalAlign::Left
// })
// }
// fn section_from_text<'a>(
// text: &'a Text,
// layout: wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker>,
// ) -> wgpu_glyph::Section<'a> {
// Section {
// screen_position: text.position.into(),
// bounds: text.area_bounds.into(),
// layout,
// ..Section::default()
// }
// .add_text(
// wgpu_glyph::Text::new(text.text)
// .with_color(text.color)
// .with_scale(text.size),
// )
// }
// pub fn owned_section_from_text(text: &Text) -> OwnedSection {
// let layout = layout_from_text(text);
// OwnedSection {
// screen_position: text.position.into(),
// bounds: text.area_bounds.into(),
// layout,
// ..OwnedSection::default()
// }
// .add_text(
// glyph_brush::OwnedText::new(text.text)
// .with_color(Vector4::from(text.color))
// .with_scale(text.size),
// )
// }
// pub fn owned_section_from_glyph_texts(
// text: Vec<glyph_brush::OwnedText>,
// screen_position: (f32, f32),
// area_bounds: (f32, f32),
// layout: wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker>,
// ) -> glyph_brush::OwnedSection {
// glyph_brush::OwnedSection {
// screen_position,
// bounds: area_bounds,
// layout,
// text,
// }
// }
// pub fn queue_text_draw(text: &Text, glyph_brush: &mut GlyphBrush<()>) {
// let layout = layout_from_text(text);
// let section = section_from_text(text, layout);
// glyph_brush.queue(section.clone());
// }
// fn glyph_to_rect(glyph: &wgpu_glyph::SectionGlyph) -> Rect {
// let position = glyph.glyph.position;
// let px_scale = glyph.glyph.scale;
// let width = glyph_width(&glyph.glyph);
// let height = px_scale.y;
// let top_y = glyph_top_y(&glyph.glyph);
// Rect {
// pos: [position.x, top_y].into(),
// width,
// height,
// }
// }
// pub fn glyph_top_y(glyph: &Glyph) -> f32 {
// let height = glyph.scale.y;
// glyph.position.y - height * 0.75
// }
// pub fn glyph_width(glyph: &Glyph) -> f32 {
// glyph.scale.x * 0.4765
// }
pub fn build_glyph_brush(
gpu_device: &wgpu::Device,
render_format: wgpu::TextureFormat,
) -> Result<GlyphBrush<()>, InvalidFont> {
let inconsolata = FontArc::try_from_slice(include_bytes!(
"../../../../../../editor/Inconsolata-Regular.ttf"
))?;
Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format))
}

View file

@ -0,0 +1,60 @@
struct Globals {
ortho: mat4x4<f32>;
};
@group(0)
@binding(0)
var<uniform> globals: Globals;
struct VertexInput {
@location(0) position: vec2<f32>;
};
struct Quad {
@location(1) pos: vec2<f32>; // can't use the name "position" twice for compatibility with metal on MacOS
@location(2) width: f32;
@location(3) height: f32;
@location(4) color: vec4<f32>;
@location(5) border_color: vec4<f32>;
@location(6) border_width: f32;
};
struct VertexOutput {
@builtin(position) position: vec4<f32>;
@location(0) color: vec4<f32>;
@location(1) border_color: vec4<f32>;
@location(2) border_width: f32;
};
@stage(vertex)
fn vs_main(
input: VertexInput,
quad: Quad
) -> VertexOutput {
var transform: mat4x4<f32> = mat4x4<f32>(
vec4<f32>(quad.width, 0.0, 0.0, 0.0),
vec4<f32>(0.0, quad.height, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(quad.pos, 0.0, 1.0)
);
var out: VertexOutput;
out.position = globals.ortho * transform * vec4<f32>(input.position, 0.0, 1.0);;
out.color = quad.color;
out.border_color = quad.border_color;
out.border_width = quad.border_width;
return out;
}
@stage(fragment)
fn fs_main(
input: VertexOutput
) -> @location(0) vec4<f32> {
return input.color;
}

View file

@ -0,0 +1 @@
pub const DEFAULT_FONT_SIZE: f32 = 30.0;

View file

@ -0,0 +1,631 @@
use crate::{
graphics::{
colors::Rgba,
lowlevel::buffer::create_rect_buffers,
lowlevel::{buffer::MAX_QUADS, ortho::update_ortho_buffer},
lowlevel::{buffer::QUAD_INDICES, pipelines},
primitives::{
rect::{Rect, RectElt},
text::build_glyph_brush,
},
},
roc::{self, RocElem, RocElemTag},
};
use cgmath::{Vector2, Vector4};
use glyph_brush::OwnedSection;
use pipelines::RectResources;
use std::error::Error;
use wgpu::{CommandEncoder, LoadOp, RenderPass, TextureView};
use wgpu_glyph::{GlyphBrush, GlyphCruncher};
use winit::{
dpi::PhysicalSize,
event,
event::{Event, ModifiersState},
event_loop::ControlFlow,
platform::run_return::EventLoopExtRunReturn,
};
// Inspired by:
// https://github.com/sotrh/learn-wgpu by Benjamin Hansen, which is licensed under the MIT license
// https://github.com/cloudhead/rgx by Alexis Sellier, which is licensed under the MIT license
//
// See this link to learn wgpu: https://sotrh.github.io/learn-wgpu/
pub fn run_event_loop(title: &str, state: roc::State) -> Result<(), Box<dyn Error>> {
// Open window and create a surface
let mut event_loop = winit::event_loop::EventLoop::new();
let window = winit::window::WindowBuilder::new()
.with_inner_size(PhysicalSize::new(state.width, state.height))
.with_title(title)
.build(&event_loop)
.unwrap();
let mut root = roc::app_render(state);
let instance = wgpu::Instance::new(wgpu::Backends::all());
let surface = unsafe { instance.create_surface(&window) };
// Initialize GPU
let (gpu_device, cmd_queue) = futures::executor::block_on(async {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.expect(r#"Request adapter
If you're running this from inside nix, follow the instructions here to resolve this: https://github.com/rtfeldman/roc/blob/trunk/BUILDING_FROM_SOURCE.md#editor
"#);
adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
limits: wgpu::Limits::default(),
},
None,
)
.await
.expect("Request device")
});
// Create staging belt and a local pool
let mut staging_belt = wgpu::util::StagingBelt::new(1024);
let mut local_pool = futures::executor::LocalPool::new();
let local_spawner = local_pool.spawner();
// Prepare swap chain
let render_format = wgpu::TextureFormat::Bgra8Unorm;
let mut size = window.inner_size();
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Mailbox,
};
surface.configure(&gpu_device, &surface_config);
let rect_resources = pipelines::make_rect_pipeline(&gpu_device, &surface_config);
let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
let is_animating = true;
let mut keyboard_modifiers = ModifiersState::empty();
// Render loop
window.request_redraw();
event_loop.run_return(|event, _, control_flow| {
// TODO dynamically switch this on/off depending on whether any
// animations are running. Should conserve CPU usage and battery life!
if is_animating {
*control_flow = ControlFlow::Poll;
} else {
*control_flow = ControlFlow::Wait;
}
match event {
//Close
Event::WindowEvent {
event: event::WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
//Resize
Event::WindowEvent {
event: event::WindowEvent::Resized(new_size),
..
} => {
size = new_size;
surface.configure(
&gpu_device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Mailbox,
},
);
update_ortho_buffer(
size.width,
size.height,
&gpu_device,
&rect_resources.ortho.buffer,
&cmd_queue,
);
}
// Keyboard input
Event::WindowEvent {
event:
event::WindowEvent::KeyboardInput {
input:
event::KeyboardInput {
virtual_keycode: Some(keycode),
state: input_state,
..
},
..
},
..
} => {
use event::ElementState::*;
use event::VirtualKeyCode::*;
match keycode {
Left => match input_state {
Pressed => println!("Left pressed!"),
Released => println!("Left released!"),
},
Right => match input_state {
Pressed => println!("Right pressed!"),
Released => println!("Right released!"),
},
_ => {
println!("Other!");
}
};
root = roc::app_render(roc::State {
height: 0.0,
width: 0.0,
});
}
//Modifiers Changed
Event::WindowEvent {
event: event::WindowEvent::ModifiersChanged(modifiers),
..
} => {
keyboard_modifiers = modifiers;
}
Event::MainEventsCleared => window.request_redraw(),
Event::RedrawRequested { .. } => {
// Get a command cmd_encoder for the current frame
let mut cmd_encoder =
gpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Redraw"),
});
let surface_texture = surface
.get_current_texture()
.expect("Failed to acquire next SwapChainTexture");
let view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let focus_ancestry: Vec<(*const RocElem, usize)> = Vec::new(); // TODO test that root node can get focus!
let focused_elem: *const RocElem = match focus_ancestry.first() {
Some((ptr_ref, _)) => *ptr_ref,
None => std::ptr::null(),
};
let (_bounds, drawable) = to_drawable(
&root,
focused_elem,
Bounds {
width: size.width as f32,
height: size.height as f32,
},
&mut glyph_brush,
);
process_drawable(
drawable,
&mut staging_belt,
&mut glyph_brush,
&mut cmd_encoder,
&view,
&gpu_device,
&rect_resources,
wgpu::LoadOp::Load,
Bounds {
width: size.width as f32,
height: size.height as f32,
},
);
// for text_section in &rects_and_texts.text_sections_front {
// let borrowed_text = text_section.to_borrowed();
// glyph_brush.queue(borrowed_text);
// }
// draw text
// glyph_brush
// .draw_queued(
// &gpu_device,
// &mut staging_belt,
// &mut cmd_encoder,
// &view,
// size.width,
// size.height,
// )
// .expect("Failed to draw queued text.");
staging_belt.finish();
cmd_queue.submit(Some(cmd_encoder.finish()));
surface_texture.present();
// Recall unused staging buffers
use futures::task::SpawnExt;
local_spawner
.spawn(staging_belt.recall())
.expect("Recall staging belt");
local_pool.run_until_stalled();
}
_ => {
*control_flow = winit::event_loop::ControlFlow::Wait;
}
}
});
Ok(())
}
fn draw_rects(
all_rects: &[RectElt],
cmd_encoder: &mut CommandEncoder,
texture_view: &TextureView,
gpu_device: &wgpu::Device,
rect_resources: &RectResources,
load_op: LoadOp<wgpu::Color>,
) {
let rect_buffers = create_rect_buffers(gpu_device, cmd_encoder, all_rects);
let mut render_pass = begin_render_pass(cmd_encoder, texture_view, load_op);
render_pass.set_pipeline(&rect_resources.pipeline);
render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]);
render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..));
render_pass.set_vertex_buffer(1, rect_buffers.quad_buffer.slice(..));
render_pass.set_index_buffer(
rect_buffers.index_buffer.slice(..),
wgpu::IndexFormat::Uint16,
);
render_pass.draw_indexed(0..QUAD_INDICES.len() as u32, 0, 0..MAX_QUADS as u32);
}
fn begin_render_pass<'a>(
cmd_encoder: &'a mut CommandEncoder,
texture_view: &'a TextureView,
load_op: LoadOp<wgpu::Color>,
) -> RenderPass<'a> {
cmd_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[wgpu::RenderPassColorAttachment {
view: texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: load_op,
store: true,
},
}],
depth_stencil_attachment: None,
label: None,
})
}
#[derive(Copy, Clone, Debug, Default)]
struct Bounds {
width: f32,
height: f32,
}
#[derive(Clone, Debug)]
struct Drawable {
bounds: Bounds,
content: DrawableContent,
}
#[derive(Clone, Debug)]
enum DrawableContent {
/// This stores an actual Section because an earlier step needs to know the bounds of
/// the text, and making a Section is a convenient way to compute those bounds.
Text(OwnedSection, Vector2<f32>),
FillRect {
color: Rgba,
border_width: f32,
border_color: Rgba,
},
Multi(Vec<Drawable>),
Offset(Vec<(Vector2<f32>, Drawable)>),
}
fn process_drawable(
drawable: Drawable,
staging_belt: &mut wgpu::util::StagingBelt,
glyph_brush: &mut GlyphBrush<()>,
cmd_encoder: &mut CommandEncoder,
texture_view: &TextureView,
gpu_device: &wgpu::Device,
rect_resources: &RectResources,
load_op: LoadOp<wgpu::Color>,
texture_size: Bounds,
) {
// TODO iterate through drawables,
// calculating a pos using offset,
// calling draw and updating bounding boxes
let pos: Vector2<f32> = (0.0, 0.0).into();
draw(
drawable.bounds,
drawable.content,
pos,
staging_belt,
glyph_brush,
cmd_encoder,
texture_view,
gpu_device,
rect_resources,
load_op,
texture_size,
);
}
fn draw(
bounds: Bounds,
content: DrawableContent,
pos: Vector2<f32>,
staging_belt: &mut wgpu::util::StagingBelt,
glyph_brush: &mut GlyphBrush<()>,
cmd_encoder: &mut CommandEncoder,
texture_view: &TextureView,
gpu_device: &wgpu::Device,
rect_resources: &RectResources,
load_op: LoadOp<wgpu::Color>,
texture_size: Bounds,
) {
use DrawableContent::*;
match content {
Text(section, offset) => {
glyph_brush.queue(section.with_screen_position(pos + offset).to_borrowed());
glyph_brush
.draw_queued(
gpu_device,
staging_belt,
cmd_encoder,
texture_view,
texture_size.width as u32, // TODO why do we make these be u32 and then cast to f32 in orthorgraphic_projection?
texture_size.height as u32,
)
.expect("Failed to draw text element");
}
FillRect {
color,
border_width,
border_color,
} => {
// TODO store all these colors and things in FillRect
let rect_elt = RectElt {
rect: Rect {
pos,
width: bounds.width,
height: bounds.height,
},
color,
border_width,
border_color,
};
// TODO inline draw_rects into here!
draw_rects(
&[rect_elt],
cmd_encoder,
texture_view,
gpu_device,
rect_resources,
load_op,
);
}
Offset(children) => {
for (offset, child) in children.into_iter() {
draw(
child.bounds,
child.content,
pos + offset,
staging_belt,
glyph_brush,
cmd_encoder,
texture_view,
gpu_device,
rect_resources,
load_op,
texture_size,
);
}
}
Multi(children) => {
for child in children.into_iter() {
draw(
child.bounds,
child.content,
pos,
staging_belt,
glyph_brush,
cmd_encoder,
texture_view,
gpu_device,
rect_resources,
load_op,
texture_size,
);
}
}
}
}
/// focused_elem is the currently-focused element (or NULL if nothing has the focus)
fn to_drawable(
elem: &RocElem,
focused_elem: *const RocElem,
bounds: Bounds,
glyph_brush: &mut GlyphBrush<()>,
) -> (Bounds, Drawable) {
use RocElemTag::*;
let is_focused = focused_elem == elem as *const RocElem;
match elem.tag() {
Button => {
let button = unsafe { &elem.entry().button };
let styles = button.styles;
let (child_bounds, child_drawable) =
to_drawable(&*button.child, focused_elem, bounds, glyph_brush);
let button_drawable = Drawable {
bounds: child_bounds,
content: DrawableContent::FillRect {
color: styles.bg_color,
border_width: styles.border_width,
border_color: styles.border_color,
},
};
let drawable = Drawable {
bounds: child_bounds,
content: DrawableContent::Multi(vec![button_drawable, child_drawable]),
};
(child_bounds, drawable)
}
Text => {
// TODO let text color and font settings inherit from parent
let text = unsafe { &elem.entry().text };
let is_centered = true; // TODO don't hardcode this
let layout = wgpu_glyph::Layout::default().h_align(if is_centered {
wgpu_glyph::HorizontalAlign::Center
} else {
wgpu_glyph::HorizontalAlign::Left
});
let section = owned_section_from_str(text.as_str(), bounds, layout);
// Calculate the bounds and offset by measuring glyphs
let text_bounds;
let offset;
match glyph_brush.glyph_bounds(section.to_borrowed()) {
Some(glyph_bounds) => {
text_bounds = Bounds {
width: glyph_bounds.max.x - glyph_bounds.min.x,
height: glyph_bounds.max.y - glyph_bounds.min.y,
};
offset = (-glyph_bounds.min.x, -glyph_bounds.min.y).into();
}
None => {
text_bounds = Bounds {
width: 0.0,
height: 0.0,
};
offset = (0.0, 0.0).into();
}
}
let drawable = Drawable {
bounds: text_bounds,
content: DrawableContent::Text(section, offset),
};
(text_bounds, drawable)
}
Row => {
let row = unsafe { &elem.entry().row_or_col };
let mut final_bounds = Bounds::default();
let mut offset: Vector2<f32> = (0.0, 0.0).into();
let mut offset_entries = Vec::with_capacity(row.children.len());
for child in row.children.as_slice().iter() {
let (child_bounds, child_drawable) =
to_drawable(&child, focused_elem, bounds, glyph_brush);
offset_entries.push((offset, child_drawable));
// Make sure the final height is enough to fit this child
final_bounds.height = final_bounds.height.max(child_bounds.height);
// Add the child's width to the final width
final_bounds.width = final_bounds.width + child_bounds.width;
// Offset the next child to make sure it appears after this one.
offset.x += child_bounds.width;
}
(
final_bounds,
Drawable {
bounds: final_bounds,
content: DrawableContent::Offset(offset_entries),
},
)
}
Col => {
let col = unsafe { &elem.entry().row_or_col };
let mut final_bounds = Bounds::default();
let mut offset: Vector2<f32> = (0.0, 0.0).into();
let mut offset_entries = Vec::with_capacity(col.children.len());
for child in col.children.as_slice().iter() {
let (child_bounds, child_drawable) =
to_drawable(&child, focused_elem, bounds, glyph_brush);
offset_entries.push((offset, child_drawable));
// Make sure the final width is enough to fit this child
final_bounds.width = final_bounds.width.max(child_bounds.width);
// Add the child's height to the final height
final_bounds.height = final_bounds.height + child_bounds.height;
// Offset the next child to make sure it appears after this one.
offset.y += child_bounds.height;
}
(
final_bounds,
Drawable {
bounds: final_bounds,
content: DrawableContent::Offset(offset_entries),
},
)
}
}
}
fn owned_section_from_str(
string: &str,
bounds: Bounds,
layout: wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker>,
) -> OwnedSection {
// TODO don't hardcode any of this!
let color = Rgba::WHITE;
let size: f32 = 40.0;
OwnedSection {
bounds: (bounds.width, bounds.height),
layout,
..OwnedSection::default()
}
.add_text(
glyph_brush::OwnedText::new(string)
.with_color(Vector4::from(color))
.with_scale(size),
)
}

View file

@ -0,0 +1,17 @@
mod focus;
mod graphics;
mod gui;
mod roc;
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
let state = roc::State {
width: 1900.0,
height: 1000.0,
};
gui::run_event_loop("test title", state).expect("Error running event loop");
// Exit code
0
}

View file

@ -0,0 +1,3 @@
fn main() {
std::process::exit(host::rust_main());
}

View file

@ -0,0 +1,400 @@
use crate::graphics::colors::Rgba;
use core::alloc::Layout;
use core::ffi::c_void;
use core::mem::{self, ManuallyDrop};
use roc_std::{ReferenceCount, RocList, RocStr};
use std::ffi::CStr;
use std::fmt::Debug;
use std::os::raw::c_char;
extern "C" {
#[link_name = "roc__programForHost_1_exposed_generic"]
fn roc_program() -> ();
#[link_name = "roc__programForHost_1_Render_caller"]
fn call_Render(state: *const State, closure_data: *const u8, output: *mut u8) -> RocElem;
#[link_name = "roc__programForHost_size"]
fn roc_program_size() -> i64;
#[allow(dead_code)]
#[link_name = "roc__programForHost_1_Render_size"]
fn size_Render() -> i64;
#[link_name = "roc__programForHost_1_Render_result_size"]
fn size_Render_result() -> i64;
}
#[derive(Debug)]
#[repr(C)]
pub struct State {
pub height: f32,
pub width: f32,
}
#[no_mangle]
pub unsafe extern "C" fn roc_alloc(size: usize, _alignment: u32) -> *mut c_void {
return libc::malloc(size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_realloc(
c_ptr: *mut c_void,
new_size: usize,
_old_size: usize,
_alignment: u32,
) -> *mut c_void {
return libc::realloc(c_ptr, new_size);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dealloc(c_ptr: *mut c_void, _alignment: u32) {
return libc::free(c_ptr);
}
#[no_mangle]
pub unsafe extern "C" fn roc_panic(c_ptr: *mut c_void, tag_id: u32) {
match tag_id {
0 => {
let slice = CStr::from_ptr(c_ptr as *const c_char);
let string = slice.to_str().unwrap();
eprintln!("Roc hit a panic: {}", string);
std::process::exit(1);
}
_ => todo!(),
}
}
#[no_mangle]
pub unsafe extern "C" fn roc_memcpy(dst: *mut c_void, src: *mut c_void, n: usize) -> *mut c_void {
libc::memcpy(dst, src, n)
}
#[no_mangle]
pub unsafe extern "C" fn roc_memset(dst: *mut c_void, c: i32, n: usize) -> *mut c_void {
libc::memset(dst, c, n)
}
#[repr(transparent)]
#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ElemId(*const RocElemEntry);
#[repr(transparent)]
#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits
pub struct RocElem {
entry: *const RocElemEntry,
}
impl Debug for RocElem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use RocElemTag::*;
match self.tag() {
Button => unsafe { &*self.entry().button }.fmt(f),
Text => unsafe { &*self.entry().text }.fmt(f),
Row => {
let row_or_col = unsafe { &*self.entry().row_or_col };
f.debug_struct("RocRow")
.field("children", &row_or_col.children)
.finish()
}
Col => {
let row_or_col = unsafe { &*self.entry().row_or_col };
f.debug_struct("RocCol")
.field("children", &row_or_col.children)
.finish()
}
}
}
}
impl RocElem {
#[allow(unused)]
pub fn id(&self) -> ElemId {
ElemId(self.entry)
}
#[cfg(target_pointer_width = "64")]
pub fn tag(&self) -> RocElemTag {
// On a 64-bit system, the last 3 bits of the pointer store the tag
unsafe { mem::transmute::<u8, RocElemTag>((self.entry as u8) & 0b0000_0111) }
}
#[allow(unused)]
pub fn entry(&self) -> &RocElemEntry {
unsafe { &*self.entry_ptr() }
}
pub fn entry_ptr(&self) -> *const RocElemEntry {
// On a 64-bit system, the last 3 bits of the pointer store the tag
let cleared = self.entry as usize & !0b111;
cleared as *const RocElemEntry
}
// fn diff(self, other: RocElem, patches: &mut Vec<(usize, Patch)>, index: usize) {
// use RocElemTag::*;
// let tag = self.tag();
// if tag != other.tag() {
// // They were totally different elem types!
// // TODO should we handle Row -> Col or Col -> Row differently?
// // Elm doesn't: https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.js#L714
// return;
// }
// match tag {
// Button => unsafe {
// let button_self = &*self.entry().button;
// let button_other = &*other.entry().button;
// // TODO compute a diff and patch for the button
// },
// Text => unsafe {
// let str_self = &*self.entry().text;
// let str_other = &*other.entry().text;
// if str_self != str_other {
// todo!("fix this");
// // let roc_str = other.entry().text;
// // let patch = Patch::Text(ManuallyDrop::into_inner(roc_str));
// // patches.push((index, patch));
// }
// },
// Row => unsafe {
// let children_self = &self.entry().row_or_col.children;
// let children_other = &other.entry().row_or_col.children;
// // TODO diff children
// },
// Col => unsafe {
// let children_self = &self.entry().row_or_col.children;
// let children_other = &other.entry().row_or_col.children;
// // TODO diff children
// },
// }
// }
#[allow(unused)]
pub fn is_focusable(&self) -> bool {
use RocElemTag::*;
match self.tag() {
Button => true,
Text | Row | Col => false,
}
}
#[allow(unused)]
pub fn row<T: Into<RocList<RocElem>>>(children: T) -> RocElem {
Self::elem_from_tag(Self::row_or_col(children), RocElemTag::Row)
}
#[allow(unused)]
pub fn col<T: Into<RocList<RocElem>>>(children: T) -> RocElem {
Self::elem_from_tag(Self::row_or_col(children), RocElemTag::Col)
}
fn row_or_col<T: Into<RocList<RocElem>>>(children: T) -> RocElemEntry {
let row_or_col = RocRowOrCol {
children: children.into(),
};
RocElemEntry {
row_or_col: ManuallyDrop::new(row_or_col),
}
}
#[allow(unused)]
pub fn button(styles: ButtonStyles, child: RocElem) -> RocElem {
let button = RocButton {
child: ManuallyDrop::new(child),
styles,
};
let entry = RocElemEntry {
button: ManuallyDrop::new(button),
};
Self::elem_from_tag(entry, RocElemTag::Button)
}
#[allow(unused)]
pub fn text<T: Into<RocStr>>(into_roc_str: T) -> RocElem {
let entry = RocElemEntry {
text: ManuallyDrop::new(into_roc_str.into()),
};
Self::elem_from_tag(entry, RocElemTag::Text)
}
fn elem_from_tag(entry: RocElemEntry, tag: RocElemTag) -> Self {
let tagged_ptr = unsafe {
let entry_ptr = roc_alloc(
core::mem::size_of_val(&entry),
core::mem::align_of_val(&entry) as u32,
) as *mut RocElemEntry;
*entry_ptr = entry;
entry_ptr as usize | tag as usize
};
Self {
entry: tagged_ptr as *const RocElemEntry,
}
}
}
#[repr(u8)]
#[allow(unused)] // This is actually used, just via a mem::transmute from u8
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RocElemTag {
Button = 0,
Col,
Row,
Text,
}
#[repr(C)]
#[derive(Debug)]
pub struct RocButton {
pub child: ManuallyDrop<RocElem>,
pub styles: ButtonStyles,
}
#[repr(C)]
pub struct RocRowOrCol {
pub children: RocList<RocElem>,
}
unsafe impl ReferenceCount for RocElem {
/// Increment the reference count.
fn increment(&self) {
use RocElemTag::*;
match self.tag() {
Button => unsafe { &*self.entry().button.child }.increment(),
Text => unsafe { &*self.entry().text }.increment(),
Row | Col => {
let children = unsafe { &self.entry().row_or_col.children };
for child in children.as_slice().iter() {
child.increment();
}
}
}
}
/// Decrement the reference count.
///
/// # Safety
///
/// The caller must ensure that `ptr` points to a value with a non-zero
/// reference count.
unsafe fn decrement(ptr: *const Self) {
use RocElemTag::*;
let elem = &*ptr;
match elem.tag() {
Button => ReferenceCount::decrement(&*elem.entry().button.child),
Text => ReferenceCount::decrement(&*elem.entry().text),
Row | Col => {
let children = &elem.entry().row_or_col.children;
for child in children.as_slice().iter() {
ReferenceCount::decrement(child);
}
}
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default)]
pub struct ButtonStyles {
pub bg_color: Rgba,
pub border_color: Rgba,
pub border_width: f32,
pub text_color: Rgba,
}
#[repr(C)]
pub union RocElemEntry {
pub button: ManuallyDrop<RocButton>,
pub text: ManuallyDrop<RocStr>,
pub row_or_col: ManuallyDrop<RocRowOrCol>,
}
// enum Patch {
// Text(RocStr),
// }
#[test]
fn make_text() {
let text = RocElem::text("blah");
assert_eq!(text.tag(), RocElemTag::Text);
}
#[test]
fn make_button() {
let text = RocElem::text("blah");
let button = RocElem::button(ButtonStyles::default(), text);
assert_eq!(button.tag(), RocElemTag::Button);
}
#[test]
fn make_row_with_text() {
let text = RocElem::text("");
let row = RocElem::row(&[text] as &[_]);
assert_eq!(row.tag(), RocElemTag::Row);
}
#[test]
fn make_row_with_button() {
let text = RocElem::text("");
let button = RocElem::button(ButtonStyles::default(), text);
let row = RocElem::row(&[button] as &[_]);
assert_eq!(row.tag(), RocElemTag::Row);
}
pub fn app_render(state: State) -> RocElem {
let size = unsafe { roc_program_size() } as usize;
let layout = Layout::array::<u8>(size).unwrap();
unsafe {
roc_program();
// TODO allocate on the stack if it's under a certain size
let buffer = std::alloc::alloc(layout);
// Call the program's render function
let result = call_the_closure(state, buffer);
std::alloc::dealloc(buffer, layout);
result
}
}
unsafe fn call_the_closure(state: State, closure_data_ptr: *const u8) -> RocElem {
let size = size_Render_result() as usize;
let layout = Layout::array::<u8>(size).unwrap();
let buffer = std::alloc::alloc(layout) as *mut u8;
let answer = call_Render(&state, closure_data_ptr as *const u8, buffer as *mut u8);
std::alloc::dealloc(buffer, layout);
answer
}