mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-02 16:21:11 +00:00
Add examples/breakout
This commit is contained in:
parent
97fff2e84d
commit
a111f510a4
27 changed files with 5036 additions and 0 deletions
1
examples/breakout/.gitignore
vendored
Normal file
1
examples/breakout/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello-gui
|
30
examples/breakout/breakout.roc
Normal file
30
examples/breakout/breakout.roc
Normal 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
1
examples/breakout/platform/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
target
|
20
examples/breakout/platform/Action.roc
Normal file
20
examples/breakout/platform/Action.roc
Normal 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
2835
examples/breakout/platform/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
75
examples/breakout/platform/Cargo.toml
Normal file
75
examples/breakout/platform/Cargo.toml
Normal 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
|
193
examples/breakout/platform/Elem.roc
Normal file
193
examples/breakout/platform/Elem.roc
Normal 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
|
20
examples/breakout/platform/Package-Config.roc
Normal file
20
examples/breakout/platform/Package-Config.roc
Normal 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
|
3
examples/breakout/platform/host.c
Normal file
3
examples/breakout/platform/host.c
Normal file
|
@ -0,0 +1,3 @@
|
|||
extern int rust_main();
|
||||
|
||||
int main() { return rust_main(); }
|
172
examples/breakout/platform/src/focus.rs
Normal file
172
examples/breakout/platform/src/focus.rs
Normal 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));
|
||||
}
|
50
examples/breakout/platform/src/graphics/colors.rs
Normal file
50
examples/breakout/platform/src/graphics/colors.rs
Normal 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)
|
||||
}
|
||||
}
|
96
examples/breakout/platform/src/graphics/lowlevel/buffer.rs
Normal file
96
examples/breakout/platform/src/graphics/lowlevel/buffer.rs
Normal 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,
|
||||
}
|
||||
}
|
5
examples/breakout/platform/src/graphics/lowlevel/mod.rs
Normal file
5
examples/breakout/platform/src/graphics/lowlevel/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod buffer;
|
||||
pub mod ortho;
|
||||
pub mod pipelines;
|
||||
pub mod vertex;
|
||||
pub mod quad;
|
118
examples/breakout/platform/src/graphics/lowlevel/ortho.rs
Normal file
118
examples/breakout/platform/src/graphics/lowlevel/ortho.rs
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
31
examples/breakout/platform/src/graphics/lowlevel/quad.rs
Normal file
31
examples/breakout/platform/src/graphics/lowlevel/quad.rs
Normal 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,
|
||||
),
|
||||
};
|
||||
}
|
35
examples/breakout/platform/src/graphics/lowlevel/vertex.rs
Normal file
35
examples/breakout/platform/src/graphics/lowlevel/vertex.rs
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
4
examples/breakout/platform/src/graphics/mod.rs
Normal file
4
examples/breakout/platform/src/graphics/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod colors;
|
||||
pub mod lowlevel;
|
||||
pub mod primitives;
|
||||
pub mod style;
|
|
@ -0,0 +1,2 @@
|
|||
pub mod rect;
|
||||
pub mod text;
|
27
examples/breakout/platform/src/graphics/primitives/rect.rs
Normal file
27
examples/breakout/platform/src/graphics/primitives/rect.rs
Normal 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,
|
||||
}
|
134
examples/breakout/platform/src/graphics/primitives/text.rs
Normal file
134
examples/breakout/platform/src/graphics/primitives/text.rs
Normal 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))
|
||||
}
|
60
examples/breakout/platform/src/graphics/shaders/quad.wgsl
Normal file
60
examples/breakout/platform/src/graphics/shaders/quad.wgsl
Normal 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;
|
||||
}
|
1
examples/breakout/platform/src/graphics/style.rs
Normal file
1
examples/breakout/platform/src/graphics/style.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub const DEFAULT_FONT_SIZE: f32 = 30.0;
|
631
examples/breakout/platform/src/gui.rs
Normal file
631
examples/breakout/platform/src/gui.rs
Normal 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),
|
||||
)
|
||||
}
|
17
examples/breakout/platform/src/lib.rs
Normal file
17
examples/breakout/platform/src/lib.rs
Normal 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
|
||||
}
|
3
examples/breakout/platform/src/main.rs
Normal file
3
examples/breakout/platform/src/main.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
std::process::exit(host::rust_main());
|
||||
}
|
400
examples/breakout/platform/src/roc.rs
Normal file
400
examples/breakout/platform/src/roc.rs
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue