Merge pull request #6770 from roc-lang/move-basic-cli-tests

Move basic cli tests from examples
This commit is contained in:
Anton-4 2024-05-22 16:13:40 +02:00 committed by GitHub
commit f10f18980d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 20 additions and 2254 deletions

View file

@ -1,7 +1,7 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" }
import pf.Stdout import pf.Stdout
import "../../LICENSE" as license import "ingested-file.roc" as license
main = main =
license license

View file

@ -1,7 +1,7 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" }
import pf.Stdout import pf.Stdout
import "../../LICENSE" as license : _ # A type hole can also be used here. import "ingested-file.roc" as license : _ # A type hole can also be used here.
main = main =
# Due to how license is used, it will be a List U8. # Due to how license is used, it will be a List U8.

View file

@ -478,7 +478,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn hello_world() { fn hello_world() {
test_roc_app_slim( test_roc_app_slim(
"examples", "crates/cli/tests/cli",
"helloWorld.roc", "helloWorld.roc",
"Hello, World!\n", "Hello, World!\n",
UseValgrind::Yes, UseValgrind::Yes,
@ -716,29 +716,6 @@ mod cli_run {
test_roc_app_slim("examples/gui", "hello-guiBROKEN.roc", "", UseValgrind::No) test_roc_app_slim("examples/gui", "hello-guiBROKEN.roc", "", UseValgrind::No)
} }
#[cfg_attr(windows, ignore)] // flaky error; issue #5024
#[serial(breakout)]
#[test]
fn breakout() {
test_roc_app_slim(
"examples/gui/breakout",
"breakoutBROKEN.roc",
"",
UseValgrind::No,
)
}
#[test]
#[serial(breakout)]
fn breakout_hello_gui() {
test_roc_app_slim(
"examples/gui/breakout",
"hello-guiBROKEN.roc",
"",
UseValgrind::No,
)
}
#[test] #[test]
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn quicksort() { fn quicksort() {
@ -778,7 +755,7 @@ mod cli_run {
#[cfg_attr(windows, ignore = "missing __udivdi3 and some other symbols")] #[cfg_attr(windows, ignore = "missing __udivdi3 and some other symbols")]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_args_check() { fn cli_args_check() {
let path = file_path_from_root("examples/cli", "argsBROKEN.roc"); let path = file_path_from_root("crates/cli/tests/cli", "argsBROKEN.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -805,7 +782,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_countdown_check() { fn cli_countdown_check() {
let path = file_path_from_root("examples/cli", "countdown.roc"); let path = file_path_from_root("crates/cli/tests/cli", "countdown.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -814,7 +791,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_echo_check() { fn cli_echo_check() {
let path = file_path_from_root("examples/cli", "echo.roc"); let path = file_path_from_root("crates/cli/tests/cli", "echo.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -823,7 +800,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_file_check() { fn cli_file_check() {
let path = file_path_from_root("examples/cli", "fileBROKEN.roc"); let path = file_path_from_root("crates/cli/tests/cli", "fileBROKEN.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -832,7 +809,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_form_check() { fn cli_form_check() {
let path = file_path_from_root("examples/cli", "form.roc"); let path = file_path_from_root("crates/cli/tests/cli", "form.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -841,7 +818,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
#[serial(cli_platform)] #[serial(cli_platform)]
fn cli_http_get_check() { fn cli_http_get_check() {
let path = file_path_from_root("examples/cli", "http-get.roc"); let path = file_path_from_root("crates/cli/tests/cli", "http-get.roc");
let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]); let out = run_roc([CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success()); assert!(out.status.success());
} }
@ -917,7 +894,7 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn with_env_vars() { fn with_env_vars() {
test_roc_app( test_roc_app(
"examples/cli", "crates/cli/tests/cli",
"env.roc", "env.roc",
&[], &[],
&[], &[],
@ -939,14 +916,14 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn ingested_file() { fn ingested_file() {
test_roc_app( test_roc_app(
"examples/cli", "crates/cli/tests/cli",
"ingested-file.roc", "ingested-file.roc",
&[], &[],
&[], &[],
&[], &[],
format!( format!(
"\nThis roc file can print its own source code. The source is:\n\n{}\n", "\nThis roc file can print its own source code. The source is:\n\n{}\n",
include_str!("../../../examples/cli/ingested-file.roc") include_str!("cli/ingested-file.roc")
) )
.as_str(), .as_str(),
UseValgrind::No, UseValgrind::No,
@ -959,12 +936,12 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn ingested_file_bytes() { fn ingested_file_bytes() {
test_roc_app( test_roc_app(
"examples/cli", "crates/cli/tests/cli",
"ingested-file-bytes.roc", "ingested-file-bytes.roc",
&[], &[],
&[], &[],
&[], &[],
"162088\n", "27101\n",
UseValgrind::No, UseValgrind::No,
TestCliCommands::Run, TestCliCommands::Run,
) )
@ -974,12 +951,12 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn ingested_file_bytes_no_ann() { fn ingested_file_bytes_no_ann() {
test_roc_app( test_roc_app(
"examples/cli", "crates/cli/tests/cli",
"ingested-file-bytes-no-ann.roc", "ingested-file-bytes-no-ann.roc",
&[], &[],
&[], &[],
&[], &[],
"162088\n", "27101\n",
UseValgrind::No, UseValgrind::No,
TestCliCommands::Run, TestCliCommands::Run,
) )
@ -990,8 +967,8 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn parse_movies_csv() { fn parse_movies_csv() {
test_roc_app_slim( test_roc_app_slim(
"examples/parser", "crates/cli/tests/cli",
"parse-movies-csv.roc", "parser-movies-csv.roc",
"2 movies were found:\n\nThe movie 'Airplane!' was released in 1980 and stars Robert Hays and Julie Hagerty\nThe movie 'Caddyshack' was released in 1980 and stars Chevy Chase, Rodney Dangerfield, Ted Knight, Michael O'Keefe and Bill Murray\n\nParse success!\n\n", "2 movies were found:\n\nThe movie 'Airplane!' was released in 1980 and stars Robert Hays and Julie Hagerty\nThe movie 'Caddyshack' was released in 1980 and stars Chevy Chase, Rodney Dangerfield, Ted Knight, Michael O'Keefe and Bill Murray\n\nParse success!\n\n",
UseValgrind::No, UseValgrind::No,
) )
@ -1002,8 +979,8 @@ mod cli_run {
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn parse_letter_counts() { fn parse_letter_counts() {
test_roc_app_slim( test_roc_app_slim(
"examples/parser", "crates/cli/tests/cli",
"letter-counts.roc", "parser-letter-counts.roc",
"I counted 7 letter A's!\n", "I counted 7 letter A's!\n",
UseValgrind::No, UseValgrind::No,
) )

View file

@ -1,164 +0,0 @@
app [program, Model] { pf: platform "platform/main.roc" }
import pf.Game exposing [Bounds, Elem, Event]
paddleWidth = 0.2 # width of the paddle, as a % of screen width
paddleHeight = 50 # height of the paddle, in pixels
paddleSpeed = 65 # how many pixels the paddle moves per keypress
blockHeight = 80 # height of a block, in pixels
blockBorder = 0.025 # border of a block, as a % of its width
ballSize = 55
numRows = 4
numCols = 8
numBlocks = numRows * numCols
Model : {
# Screen height and width
height : F32,
width : F32,
# Paddle X-coordinate
paddleX : F32,
# Ball coordinates
ballX : F32,
ballY : F32,
dBallX : F32,
# delta x - how much it moves per tick
dBallY : F32,
# delta y - how much it moves per tick
}
init : Bounds -> Model
init = \{ width, height } -> {
# Screen height and width
width,
height,
# Paddle X-coordinate
paddleX: (width * 0.5) - (paddleWidth * width * 0.5),
# Ball coordinates
ballX: width * 0.5,
ballY: height * 0.4,
# Delta - how much ball moves in each tick
dBallX: 4,
dBallY: 4,
}
update : Model, Event -> Model
update = \model, event ->
when event is
Resize size ->
{ model & width: size.width, height: size.height }
KeyDown Left ->
{ model & paddleX: model.paddleX - paddleSpeed }
KeyDown Right ->
{ model & paddleX: model.paddleX + paddleSpeed }
Tick _ ->
tick model
_ ->
model
tick : Model -> Model
tick = \model ->
model
|> moveBall
moveBall : Model -> Model
moveBall = \model ->
ballX = model.ballX + model.dBallX
ballY = model.ballY + model.dBallY
paddleTop = model.height - blockHeight - (paddleHeight * 2)
paddleLeft = model.paddleX
paddleRight = paddleLeft + (model.width * paddleWidth)
# If its y used to be less than the paddle, and now it's greater than or equal,
# then this is the frame where the ball collided with it.
crossingPaddle = model.ballY < paddleTop && ballY >= paddleTop
# If it collided with the paddle, bounce off.
directionChange =
if crossingPaddle && (ballX >= paddleLeft && ballX <= paddleRight) then
-1f32
else
1f32
dBallX = model.dBallX * directionChange
dBallY = model.dBallY * directionChange
{ model & ballX, ballY, dBallX, dBallY }
render : Model -> List Elem
render = \model ->
blocks = List.map
(List.range { start: At 0, end: Length numBlocks })
\index ->
col =
Num.rem index numCols
|> Num.toF32
row =
index
// numCols
|> Num.toF32
red = col / Num.toF32 numCols
green = row / Num.toF32 numRows
blue = Num.toF32 index / Num.toF32 numBlocks
color = { r: red * 0.8, g: 0.2 + green * 0.6, b: 0.2 + blue * 0.8, a: 1 }
{ row, col, color }
blockWidth = model.width / numCols
rects =
List.joinMap
blocks
\{ row, col, color } ->
left = Num.toF32 col * blockWidth
top = Num.toF32 (row * blockHeight)
border = blockBorder * blockWidth
outer = Rect {
left,
top,
width: blockWidth,
height: blockHeight,
color: { r: color.r * 0.8, g: color.g * 0.8, b: color.b * 0.8, a: 1 },
}
inner = Rect {
left: left + border,
top: top + border,
width: blockWidth - (border * 2),
height: blockHeight - (border * 2),
color,
}
[outer, inner]
ball =
color = { r: 0.7, g: 0.3, b: 0.9, a: 1.0 }
width = ballSize
height = ballSize
left = model.ballX
top = model.ballY
Rect { left, top, width, height, color }
paddle =
color = { r: 0.8, g: 0.8, b: 0.8, a: 1.0 }
width = model.width * paddleWidth
height = paddleHeight
left = model.paddleX
top = model.height - blockHeight - height
Rect { left, top, width, height, color }
List.concat rects [paddle, ball]
program = { init, update, render }

View file

@ -1,16 +0,0 @@
app [program, Model] { pf: platform "platform/main.roc" }
import pf.Game exposing [Bounds, Elem, Event]
Model : { text : Str }
init : Bounds -> Model
init = \_ -> { text: "Hello, World!" }
update : Model, Event -> Model
update = \model, _ -> model
render : Model -> List Elem
render = \model -> [Text { text: model.text, top: 0, left: 0, size: 40, color: { r: 1, g: 1, b: 1, a: 1 } }]
program = { init, update, render }

View file

@ -1,15 +0,0 @@
module [Action, none, update, map]
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)

View file

@ -1,57 +0,0 @@
[package]
name = "host"
authors = ["The Roc Contributors"]
edition = "2021"
license = "UPL-1.0"
version = "0.0.1"
[lib]
name = "host"
path = "src/lib.rs"
crate-type = ["staticlib", "lib"]
[[bin]]
name = "host"
path = "src/main.rs"
[dependencies]
arrayvec = "0.7.2"
libc = "0.2"
page_size = "0.4.2"
roc_std = { path = "../../../../crates/roc_std" }
cgmath = "0.18.0"
colored = "2.0.0"
copypasta = "0.7.1"
fs_extra = "1.2.0"
futures = "0.3.17"
glyph_brush = "0.7.2"
log = "0.4.14"
nonempty = "0.7.0"
palette = "0.6.0"
pest = "2.1.3"
pest_derive = "2.1.0"
serde = { version = "1.0.130", features = ["derive"] }
snafu = { version = "0.6.10", features = ["backtraces"] }
threadpool = "1.8.1"
wgpu = { git = "https://github.com/gfx-rs/wgpu", rev = "0545e36" }
wgpu_glyph = { git = "https://github.com/Anton-4/wgpu_glyph", rev = "257d109" }
winit = "0.26.1"
[features]
default = []
[dependencies.bytemuck]
version = "1.7.2"
features = ["derive"]
[workspace]
# Optimizations based on https://deterministic.space/high-performance-rust.html
[profile.release]
lto = "fat"
codegen-units = 1
# debug = true # enable when profiling
[profile.bench]
lto = "thin"
codegen-units = 1

View file

@ -1,192 +0,0 @@
module [Elem, PressEvent, row, col, text, button, none, translate, list]
import Action exposing [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/roc-lang/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/roc-lang/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/roc-lang/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/roc-lang/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

@ -1,11 +0,0 @@
module [Bounds, Elem, Event]
Rgba : { r : F32, g : F32, b : F32, a : F32 }
Bounds : { height : F32, width : F32 }
Elem : [Rect { color : Rgba, left : F32, top : F32, width : F32, height : F32 }, Text { text : Str, color : Rgba, left : F32, top : F32, size : F32 }]
KeyCode : [Left, Right, Other, Up, Down]
Event : [Resize { width : F32, height : F32 }, KeyDown KeyCode, KeyUp KeyCode, Tick U128]

View file

@ -1,9 +0,0 @@
fn main() {
#[cfg(not(windows))]
println!("cargo:rustc-link-lib=dylib=app");
#[cfg(windows)]
println!("cargo:rustc-link-lib=dylib=libapp");
println!("cargo:rustc-link-search=.");
}

View file

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

View file

@ -1,14 +0,0 @@
platform "gui"
requires { Model } { program : _ }
exposes [Game]
packages {}
imports [Game.{ Bounds, Elem, Event }]
provides [programForHost]
# TODO allow changing the window title - maybe via a Task, since that shouldn't happen all the time
programForHost : {
init : (Bounds -> Model) as Init,
update : (Model, Event -> Model) as Update,
render : (Model -> List Elem) as Render,
}
programForHost = program

View file

@ -1,50 +0,0 @@
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.g, self.b, 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

@ -1,96 +0,0 @@
// 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

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

View file

@ -1,118 +0,0 @@
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

@ -1,72 +0,0 @@
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

@ -1,36 +0,0 @@
/// A polygon with 4 corners
#[derive(Copy, Clone)]
#[repr(C)]
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,
}
// Safety: Pod's contract says the type must
// not have any padding, and must be repr(C).
// As currrently defined, Quad does not have
// any padding.
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

@ -1,35 +0,0 @@
// 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

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

View file

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

View file

@ -1,27 +0,0 @@
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

@ -1,134 +0,0 @@
// 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!(
"../../../../../Inconsolata-Regular.ttf"
))?;
Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format))
}

View file

@ -1,60 +0,0 @@
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

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

View file

@ -1,513 +0,0 @@
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, Bounds, RocElem, RocElemTag, RocEvent},
};
use cgmath::{Vector2, Vector4};
use glyph_brush::{GlyphCruncher, OwnedSection};
use pipelines::RectResources;
use std::{
error::Error,
time::{Duration, Instant},
};
use wgpu::{CommandEncoder, LoadOp, RenderPass, TextureView};
use wgpu_glyph::GlyphBrush;
use winit::{
dpi::PhysicalSize,
event,
event::{ElementState, Event, ModifiersState, StartCause},
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/
const TIME_BETWEEN_TICKS: Duration = Duration::new(0, 1000 / 60);
pub fn run_event_loop(title: &str, window_bounds: Bounds) -> Result<(), Box<dyn Error>> {
let (mut model, mut elems) = roc::init_and_render(window_bounds);
// 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(window_bounds.width, window_bounds.height))
.with_title(title)
.build(&event_loop)
.unwrap();
macro_rules! update_and_rerender {
($event:expr) => {
// TODO use (model, elems) = ... once we've upgraded rust versions
let pair = roc::update_and_render(model, $event);
model = pair.0;
elems = pair.1;
window.request_redraw();
};
}
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, run with:
`nixVulkanIntel <your previous command that generated this error>`.
See extra docs here: github.com/guibou/nixGL
"#);
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 mut keyboard_modifiers = ModifiersState::empty();
// Render loop
let app_start_time = Instant::now();
let mut next_tick = app_start_time + TIME_BETWEEN_TICKS;
window.request_redraw();
event_loop.run_return(|event, _, control_flow| {
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,
);
update_and_rerender!(RocEvent::Resize(Bounds {
height: size.height as f32,
width: size.width as f32,
}));
}
// Keyboard input
Event::WindowEvent {
event:
event::WindowEvent::KeyboardInput {
input:
event::KeyboardInput {
virtual_keycode: Some(keycode),
state: input_state,
..
},
..
},
..
} => {
let roc_event = match input_state {
ElementState::Pressed => RocEvent::KeyDown(keycode.into()),
ElementState::Released => RocEvent::KeyUp(keycode.into()),
};
model = roc::update(model, roc_event);
}
// Modifiers Changed
Event::WindowEvent {
event: event::WindowEvent::ModifiersChanged(modifiers),
..
} => {
keyboard_modifiers = modifiers;
}
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());
for elem in elems.iter() {
let (_bounds, drawable) = to_drawable(
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,
},
);
}
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();
}
Event::NewEvents(StartCause::ResumeTimeReached {
requested_resume, ..
}) => {
// Only run this logic if this is the tick we originally requested.
if requested_resume == next_tick {
let now = Instant::now();
// Set a new next_tick *before* running update and rerender,
// so their runtime isn't factored into when we want to render next.
next_tick = now + TIME_BETWEEN_TICKS;
let tick = now.saturating_duration_since(app_start_time);
update_and_rerender!(RocEvent::Tick(tick));
*control_flow = winit::event_loop::ControlFlow::WaitUntil(next_tick);
}
}
_ => {
// Keep waiting until the next tick.
*control_flow = winit::event_loop::ControlFlow::WaitUntil(next_tick);
}
}
});
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(Clone, Debug)]
struct Drawable {
pos: Vector2<f32>,
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,
},
}
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,
) {
draw(
drawable.bounds,
drawable.content,
drawable.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,
);
}
}
}
/// focused_elem is the currently-focused element (or NULL if nothing has the focus)
fn to_drawable(
elem: &RocElem,
bounds: Bounds,
glyph_brush: &mut GlyphBrush<()>,
) -> (Bounds, Drawable) {
use RocElemTag::*;
match elem.tag() {
Rect => {
let rect = unsafe { &elem.entry().rect };
let bounds = Bounds {
width: rect.width,
height: rect.height,
};
let drawable = Drawable {
pos: (rect.left, rect.top).into(),
bounds,
content: DrawableContent::FillRect {
color: rect.color,
border_width: 1.0,
border_color: rect.color,
},
};
(bounds, drawable)
}
Text => {
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.text.as_str(),text.color, text.size, 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 {
pos: (text.left, text.top).into(),
bounds: text_bounds,
content: DrawableContent::Text(section, offset),
};
(text_bounds, drawable)
}
}
}
fn owned_section_from_str(
string: &str,
color: Rgba,
size: f32,
bounds: Bounds,
layout: wgpu_glyph::Layout<wgpu_glyph::BuiltInLineBreaker>,
) -> OwnedSection {
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

@ -1,18 +0,0 @@
#![allow(unused)]
mod graphics;
mod gui;
mod roc;
#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
let bounds = roc::Bounds {
width: 1900.0,
height: 1000.0,
};
gui::run_event_loop("RocOut!", bounds).expect("Error running event loop");
// Exit code
0
}

View file

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

View file

@ -1,453 +0,0 @@
use crate::graphics::colors::Rgba;
use core::alloc::Layout;
use core::ffi::c_void;
use core::mem::{self, ManuallyDrop};
use roc_std::{RocList, RocStr};
use std::ffi::CStr;
use std::fmt::Debug;
use std::mem::MaybeUninit;
use std::os::raw::c_char;
use std::time::Duration;
use winit::event::VirtualKeyCode;
extern "C" {
// program
// #[link_name = "roc__programForHost_1_exposed_generic"]
// fn roc_program();
// #[link_name = "roc__programForHost_1_exposed_size"]
// fn roc_program_size() -> i64;
// init
#[link_name = "roc__programForHost_0_caller"]
fn call_init(size: *const Bounds, closure_data: *const u8, output: *mut Model);
#[link_name = "roc__programForHost_0_size"]
fn init_size() -> i64;
#[link_name = "roc__programForHost_0_result_size"]
fn init_result_size() -> i64;
// update
#[link_name = "roc__programForHost_1_caller"]
fn call_update(
model: *const Model,
event: *const RocEvent,
closure_data: *const u8,
output: *mut Model,
);
#[link_name = "roc__programForHost_1_size"]
fn update_size() -> i64;
#[link_name = "roc__programForHost_1_result_size"]
fn update_result_size() -> i64;
// render
#[link_name = "roc__programForHost_2_caller"]
fn call_render(model: *const Model, closure_data: *const u8, output: *mut RocList<RocElem>);
#[link_name = "roc__programForHost_2_size"]
fn roc_render_size() -> i64;
}
#[repr(C)]
pub union RocEventEntry {
pub key_down: RocKeyCode,
pub key_up: RocKeyCode,
pub resize: Bounds,
pub tick: [u8; 16], // u128 is unsupported in repr(C)
}
#[repr(u8)]
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RocEventTag {
KeyDown = 0,
KeyUp,
Resize,
Tick,
}
#[repr(C)]
#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits
pub struct RocEvent {
entry: RocEventEntry,
tag: RocEventTag,
}
impl Debug for RocEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use RocEventTag::*;
match self.tag() {
KeyDown => unsafe { self.entry().key_down }.fmt(f),
KeyUp => unsafe { self.entry().key_up }.fmt(f),
Resize => unsafe { self.entry().resize }.fmt(f),
Tick => unsafe { self.entry().tick }.fmt(f),
}
}
}
impl RocEvent {
#[cfg(target_pointer_width = "64")]
pub fn tag(&self) -> RocEventTag {
self.tag
}
pub fn entry(&self) -> &RocEventEntry {
&self.entry
}
#[allow(non_snake_case)]
pub fn Resize(size: Bounds) -> Self {
Self {
tag: RocEventTag::Resize,
entry: RocEventEntry { resize: size },
}
}
#[allow(non_snake_case)]
pub fn KeyDown(keycode: RocKeyCode) -> Self {
Self {
tag: RocEventTag::KeyDown,
entry: RocEventEntry { key_down: keycode },
}
}
#[allow(non_snake_case)]
pub fn KeyUp(keycode: RocKeyCode) -> Self {
Self {
tag: RocEventTag::KeyUp,
entry: RocEventEntry { key_up: keycode },
}
}
#[allow(non_snake_case)]
pub fn Tick(duration: Duration) -> Self {
Self {
tag: RocEventTag::Tick,
entry: RocEventEntry {
tick: duration.as_nanos().to_ne_bytes(),
},
}
}
}
#[repr(u8)]
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RocKeyCode {
Down = 0,
Left,
Other,
Right,
Up,
}
impl From<VirtualKeyCode> for RocKeyCode {
fn from(keycode: VirtualKeyCode) -> Self {
use VirtualKeyCode::*;
match keycode {
Left => RocKeyCode::Left,
Right => RocKeyCode::Right,
Up => RocKeyCode::Up,
Down => RocKeyCode::Down,
_ => RocKeyCode::Other,
}
}
}
#[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(msg: *mut RocStr, tag_id: u32) {
match tag_id {
0 => {
eprintln!("Roc standard library hit a panic: {}", &*msg);
}
1 => {
eprintln!("Application hit a panic: {}", &*msg);
}
_ => unreachable!(),
}
std::process::exit(1);
}
#[no_mangle]
pub unsafe extern "C" fn roc_dbg(loc: *mut RocStr, msg: *mut RocStr, src: *mut RocStr) {
eprintln!("[{}] {} = {}", &*loc, &*src, &*msg);
}
#[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(C)]
pub union RocElemEntry {
pub rect: ManuallyDrop<RocRect>,
pub text: ManuallyDrop<RocText>,
}
#[repr(u8)]
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RocElemTag {
Rect = 0,
Text = 1,
}
#[repr(C)]
#[cfg(target_pointer_width = "64")] // on a 64-bit system, the tag fits in this pointer's spare 3 bits
pub struct RocElem {
entry: RocElemEntry,
tag: RocElemTag,
}
impl Debug for RocElem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use RocElemTag::*;
match self.tag() {
Rect => unsafe { &*self.entry().rect }.fmt(f),
Text => unsafe { &*self.entry().text }.fmt(f),
}
}
}
impl RocElem {
#[cfg(target_pointer_width = "64")]
pub fn tag(&self) -> RocElemTag {
self.tag
}
#[allow(unused)]
pub fn entry(&self) -> &RocElemEntry {
&self.entry
}
#[allow(unused)]
pub fn rect(styles: ButtonStyles) -> RocElem {
todo!("restore rect() method")
// let rect = RocRect { styles };
// let entry = RocElemEntry {
// rect: ManuallyDrop::new(rect),
// };
// Self::elem_from_tag(entry, RocElemTag::Rect)
}
#[allow(unused)]
pub fn text<T: Into<RocStr>>(into_roc_str: T) -> RocElem {
todo!("TODO restore text method")
// let entry = RocElemEntry {
// text: ManuallyDrop::new(into_roc_str.into()),
// };
// Self::elem_from_tag(entry, RocElemTag::Text)
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct RocRect {
pub color: Rgba,
// These must be in this order for alphabetization!
pub height: f32,
pub left: f32,
pub top: f32,
pub width: f32,
}
#[repr(C)]
#[derive(Debug, Clone)]
pub struct RocText {
pub text: RocStr,
pub color: Rgba,
pub left: f32,
pub size: f32,
pub top: f32,
}
impl Clone for RocElem {
fn clone(&self) -> Self {
unsafe {
match self.tag() {
RocElemTag::Rect => Self {
tag: RocElemTag::Rect,
entry: RocElemEntry {
rect: self.entry.rect.clone(),
},
},
RocElemTag::Text => Self {
tag: RocElemTag::Text,
entry: RocElemEntry {
text: self.entry.text.clone(),
},
},
}
}
}
}
impl Drop for RocElem {
fn drop(&mut self) {
unsafe {
match self.tag() {
RocElemTag::Rect => mem::drop(ManuallyDrop::take(&mut self.entry.rect)),
RocElemTag::Text => mem::drop(ManuallyDrop::take(&mut self.entry.text)),
}
}
}
}
#[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,
}
#[derive(Copy, Clone, Debug, Default)]
#[repr(C)]
pub struct Bounds {
pub height: f32,
pub width: f32,
}
type Model = c_void;
/// Call the app's init function, then render and return that result
pub fn init_and_render(bounds: Bounds) -> (*const Model, RocList<RocElem>) {
let closure_data_buf;
let closure_layout;
// Call init to get the initial model
let model = unsafe {
let ret_val_layout = Layout::array::<u8>(init_result_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model;
closure_layout = Layout::array::<u8>(init_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
closure_data_buf = std::alloc::alloc(closure_layout);
call_init(&bounds, closure_data_buf, ret_val_buf);
ret_val_buf
};
// Call render passing the model to get the initial Elems
let elems = unsafe {
let mut ret_val: MaybeUninit<RocList<RocElem>> = MaybeUninit::uninit();
// Reuse the buffer from the previous closure if possible
let closure_data_buf =
std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize);
call_render(model, closure_data_buf, ret_val.as_mut_ptr());
std::alloc::dealloc(closure_data_buf, closure_layout);
ret_val.assume_init()
};
(model, elems)
}
/// Call the app's update function, then render and return that result
pub fn update(model: *const Model, event: RocEvent) -> *const Model {
let closure_data_buf;
let closure_layout;
// Call update to get the new model
unsafe {
let ret_val_layout = Layout::array::<u8>(update_result_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model;
closure_layout = Layout::array::<u8>(update_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
closure_data_buf = std::alloc::alloc(closure_layout);
call_update(model, &event, closure_data_buf, ret_val_buf);
ret_val_buf
}
}
/// Call the app's update function, then render and return that result
pub fn update_and_render(model: *const Model, event: RocEvent) -> (*const Model, RocList<RocElem>) {
let closure_data_buf;
let closure_layout;
// Call update to get the new model
let model = unsafe {
let ret_val_layout = Layout::array::<u8>(update_result_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
let ret_val_buf = std::alloc::alloc(ret_val_layout) as *mut Model;
closure_layout = Layout::array::<u8>(update_size() as usize).unwrap();
// TODO allocate on the stack if it's under a certain size
closure_data_buf = std::alloc::alloc(closure_layout);
call_update(model, &event, closure_data_buf, ret_val_buf);
ret_val_buf
};
// Call render passing the model to get the initial Elems
let elems = unsafe {
let mut ret_val: MaybeUninit<RocList<RocElem>> = MaybeUninit::uninit();
// Reuse the buffer from the previous closure if possible
let closure_data_buf =
std::alloc::realloc(closure_data_buf, closure_layout, roc_render_size() as usize);
call_render(model, closure_data_buf, ret_val.as_mut_ptr());
std::alloc::dealloc(closure_data_buf, closure_layout);
ret_val.assume_init()
};
(model, elems)
}

View file

@ -1,3 +0,0 @@
The parser package example has been moved to [https://github.com/lukewilliamboswell/roc-parser](https://github.com/lukewilliamboswell/roc-parser).
The examples here use a release of this package.

View file

@ -1,81 +0,0 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.1/dCL3KsovvV-8A5D_W_0X_abynkcRcoAngsgF0xtvQsk.tar.br" }
import pf.Stdout
import pf.Stderr
import pf.Task exposing [Task]
import pf.Http exposing [Request, Response]
import pf.Utc
import pf.Env
main : Request -> Task Response []
main = \req ->
handleReq =
# Log the date, time, method, and url to stdout
{} <- logRequest req |> Task.await
# Read environment variable
url <- readUrlEnv "TARGET_URL" |> Task.await
# Fetch the Roc website
content <- fetchContent url |> Task.await
# Respond with the website content
respond 200 content
# Handle any application errors
handleReq |> Task.onErr handleErr
AppError : [
EnvURLNotFound,
HttpError Http.Error,
]
logRequest : Request -> Task {} AppError
logRequest = \req ->
dateTime <- Utc.now |> Task.map Utc.toIso8601Str |> Task.await
Stdout.line "$(dateTime) $(Http.methodToStr req.method) $(req.url)"
readUrlEnv : Str -> Task Str AppError
readUrlEnv = \target ->
Env.var target
|> Task.mapErr \_ -> EnvURLNotFound
fetchContent : Str -> Task Str AppError
fetchContent = \url ->
Http.getUtf8 url
|> Task.mapErr \err -> HttpError err
handleErr : AppError -> Task Response []
handleErr = \err ->
# Build error message
message =
when err is
EnvURLNotFound -> "TARGET_URL environment variable not set"
HttpError _ -> "Http error fetching content"
# Log error to stderr
{} <- Stderr.line "Internal Server Error: $(message)" |> Task.await
_ <- Stderr.flush |> Task.attempt
# Respond with Http 500 Error
Task.ok {
status: 500,
headers: [
{ name: "Content-Type", value: Str.toUtf8 "text/html; charset=utf-8" },
],
body: Str.toUtf8 "Error 500 Internal Server Error\n",
}
# Respond with the given status code and body
respond : U16, Str -> Task Response AppError
respond = \code, body ->
Task.ok {
status: code,
headers: [
{ name: "Content-Type", value: Str.toUtf8 "text/html; charset=utf-8" },
],
body: Str.toUtf8 body,
}

View file

@ -1,19 +0,0 @@
app [main] { pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.1/dCL3KsovvV-8A5D_W_0X_abynkcRcoAngsgF0xtvQsk.tar.br" }
import pf.Stdout
import pf.Task exposing [Task]
import pf.Http exposing [Request, Response]
import pf.Utc
main : Request -> Task Response []
main = \req ->
# Log request date, method and url
date <- Utc.now |> Task.map Utc.toIso8601Str |> Task.await
{} <- Stdout.line "$(date) $(Http.methodToStr req.method) $(req.url)" |> Task.await
# Respond with request body
when req.body is
EmptyBody -> Task.ok { status: 200, headers: [], body: [] }
Body internal -> Task.ok { status: 200, headers: [], body: internal.body }