Initial web studio functionality

This commit is contained in:
NGnius (Graham) 2023-03-09 21:16:29 -05:00
parent 82af952962
commit f25366e349
80 changed files with 5097 additions and 267 deletions

1
.gitignore vendored
View file

@ -46,6 +46,7 @@ yalc.lock
/backend/target
/backend/out
/bin
/**/target/
# packaged teasers
*.zip

460
backend/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ license = "MIT"
repository = "https://github.com/NGnius/kaylon"
[dependencies]
caylon-config = { version = "0.1.0", path = "./caylon-config" }
usdpl-back = { version = "0.10.0"}
clap = { version = "3.2", features = ["derive", "std"], default-features = false }

105
backend/caylon-config/Cargo.lock generated Normal file
View file

@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "caylon-config"
version = "0.1.0"
dependencies = [
"log",
"serde",
"serde_json",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "itoa"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "proc-macro2"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
[[package]]
name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"

View file

@ -0,0 +1,15 @@
[package]
name = "caylon-config"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
log = { version = "0.4" }
[dev-dependencies]

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct AboutConfig {
pub name: String,
pub version: String,

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "action")]
pub enum TopLevelActionConfig {
#[serde(rename = "sequence")]
@ -17,7 +17,7 @@ pub enum TopLevelActionConfig {
Json(JsonAction),
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "action")]
pub enum ActionConfig {
#[serde(rename = "command")]
@ -30,25 +30,25 @@ pub enum ActionConfig {
Json(JsonAction),
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct SequenceAction {
pub steps: Vec<ActionConfig>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct CommandAction {
pub run: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct MirrorAction;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JavascriptAction {
pub run: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JsonAction {
pub jmespath: String,
}

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::{ElementConfig, AboutConfig};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "api-version")]
pub enum BaseConfig {
#[serde(rename = "v0.0.0")]
@ -55,4 +55,8 @@ impl BaseConfig {
Self::V0 {items, ..} => items,
}
}
pub fn assemble(items: Vec<ElementConfig>, about: AboutConfig) -> Self {
Self::V0 { items, about }
}
}

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ButtonConfig {
pub title: String,
pub on_click: TopLevelActionConfig,

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig, EventDisplayConfig};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "element")]
pub enum ElementConfig {
#[serde(rename = "button")]

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct EventDisplayConfig {
pub title: String,
/// Type of event to listen for
@ -11,7 +11,7 @@ pub struct EventDisplayConfig {
pub on_event: TopLevelActionConfig,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub enum EventType {
#[serde(rename = "achievement")]
Achievement,

View file

@ -25,7 +25,7 @@ pub use transformer::{TransformAction, TransformTypeAction, ReplaceTransformActi
#[cfg(test)]
mod test {
use super::*;
#[test]
fn dump_test() {
let conf = BaseConfig::V0 {

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ReadingConfig {
pub title: String,
/// Period in milliseconds, or None/null for non-repeating actions

View file

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ResultDisplayConfig {
pub title: String,
/// Index of element who's action's result will be used

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct SliderConfig {
pub title: String,
pub min: u64,

View file

@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize};
use super::TopLevelActionConfig;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ToggleConfig {
pub title: String,
pub description: Option<String>,

View file

@ -2,12 +2,12 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct TransformAction {
pub transformer: TransformTypeAction,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "rule")]
pub enum TransformTypeAction {
#[serde(rename = "replace")]
@ -18,13 +18,13 @@ pub enum TransformTypeAction {
Log(LogTransformAction),
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ReplaceTransformAction {
/// Regex
pub patterns: Vec<PatternConfig>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct PatternConfig {
/// Regex
pub pattern: String,
@ -43,17 +43,17 @@ pub struct PatternConfig {
pub x: Option<bool>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct ExpandTransformAction {
pub format: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct LogTransformAction {
pub level: LogLevel,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub enum LogLevel {
DEBUG,
INFO,

2
backend/caylon-studio/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/dist/
/target/

2265
backend/caylon-studio/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
[package]
name = "caylon-studio"
version = "0.1.0"
edition = "2021"
description = "Template for starting a Yew project using Trunk"
readme = "README.md"
repository = "https://github.com/yewstack/yew-trunk-minimal-template"
license = "MIT"
keywords = ["yew", "trunk"]
categories = ["gui", "wasm", "web-programming"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "frontend"
[[bin]]
name = "backend"
[dependencies]
caylon-config = { version = "0.1.0", path = "../caylon-config" }
reqwest = { version = "0.11.8", features = ["json"] }
serde = { version = "1.0.132", features = ["derive"] }
serde_json = { version = "1.0" }
uuid = { version = "1.0.0", features = ["serde"] }
futures = "0.3"
bytes = "1.0"
log = "0.4"
[target.'cfg(target_arch = "wasm32")'.dependencies]
yew = { version = "0.20", features = [ "csr", "hydration" ] }
wasm-bindgen-futures = "0.4"
wasm-log = "0.3"
web-sys = { version = "0.3", features = [ "HtmlSelectElement", "HtmlInputElement" ] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
yew = { version = "0.20", features = [ "ssr" ] }
tokio = { version = "1", features = ["full"] }
actix-web = { version = "4.3" }
actix-files = { version = "0.6" }
clap = { version = "3.1.7", features = ["derive"] }
simplelog = { version = "0.12" }

View file

@ -0,0 +1,5 @@
# caylon-studio
## Running
Execute `run.sh` in your terminal. You will need cargo, Rust, and the wasm toolchain installed.

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Caylon Studio</title>
<link data-trunk rel="rust" data-bin="frontend" />
<link data-trunk rel="sass" href="index.scss" />
<!--<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css"
/>-->
</head>
</html>

View file

@ -0,0 +1,290 @@
html,
body {
height: 100%;
width: 98%;
margin: 0;
padding: 1%;
}
div.work-view {
padding: 0;
margin: 0;
display: inline-block;
width: 100%;
}
div.json-view {
padding: 0;
margin: auto 0;
display: inline-block;
width: 40%;
vertical-align: middle;
}
.json-input {
margin: auto;
width: 98%;
}
div.elements-view {
padding: 0;
margin: auto;
display: inline-block;
width: 60%;
text-align: center;
vertical-align: top;
}
/* Element styling */
// *
div.caylon-element {}
// actions/*
div.caylon-action-config {}
// actions/command.rs
div.caylon-command-action-edit {}
// actions/javascript.rs
div.caylon-javascript-action-edit {}
// actions/json.rs
div.caylon-json-action-edit {}
// actions/mirror.rs
div.caylon-mirror-action-edit {}
// actions/transform.rs
div.caylon-transformer-replace-item {}
div.caylon-transformer-replace {}
div.caylon-transformer-expand {}
div.caylon-transformer-log {}
div.caylon-transformer-action-edit {}
div.caylon-transformer-type-action-edit {}
// add_element.rs
div.add-element {}
button.add-element-button {}
// button.rs
div.caylon-button {}
// fake/button.rs
button.fake-button {}
// edit/*
div.caylon-edit {}
label.caylon-label-edit {}
input.caylon-input-editor {}
// edit/action_editor.rs
div.caylon-action-edit {}
div.caylon-sequence-action-edit {}
div.caylon-sequence-command-action {}
div.caylon-sequence-transform-action {}
div.caylon-sequence-javascript-action {}
div.caylon-sequence-json-action {}
div.caylon-sequence-action-edit-item {}
div.caylon-sequence-action-edit {}
div.caylon-action-config {}
div.caylon-editor {}
// edit/always_str.rs
div.caylon-always-str-edit {}
input.caylon-always-str-input {}
// edit/always_usize.rs
div.caylon-always-usize-edit {}
input.caylon-always-usize-input {}
// edit/optional_str.rs
div.caylon-option-str-edit {}
input.caylon-option-str-input {}
// edit/optional_u64.rs
div.caylon-option-u64-edit {}
input.caylon-option-u64-input {}
// elements.rs
div.elements-view {}
div.elements-toolbar {}
div.elements-list {}
div.elements-item {}
// event_display.rs
div.caylon-event-display {}
// reading_display.rs
div.caylon-reading-display {}
// reading_display.rs
div.caylon-reading-display {}
// remove_element.rs
div.remove-element {}
button.remove-element-button {}
// result_display.rs
div.caylon-result-display {}
// fake/display.rs
div.fake-slider {}
span.fake-slider-title {}
input.fake-slider-content {}
// slider.rs
div.caylon-slider {}
// fake/slider.rs
div.fake-slider {}
span.fake-slider-title {}
input.fake-slider-input {}
// toggle.rs
div.caylon-toggle {}
// fake/toggle.rs
// adapted from https://www.w3schools.com/howto/howto_css_switch.asp
.fake-toggle-button {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.fake-toggle-button input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-round {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.toggle-round:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .toggle-round {
background-color: #2196F3;
}
input:focus + .toggle-round {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .toggle-round:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.toggle-round {
border-radius: 34px;
}
.toggle-round:before {
border-radius: 50%;
}
/* End element styling */
div.footer {
width: 98%;
height: 5%;
margin: auto;
text-align: right;
}
.footer-elem {
display: inline-block;
padding: 0% 4%;
}
div.header {
width: 98%;
height: 10%;
margin: auto;
text-align: center;
}
.header-elem {
display: inline-block;
}
.hit-count {
margin: auto;
}

3
backend/caylon-studio/run.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
trunk build ./index.html && cargo run --bin backend

View file

@ -0,0 +1,9 @@
use actix_web::{get, web, Responder};
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) static INDEX_HITS: AtomicU64 = AtomicU64::new(0);
#[get("/stats/hits")]
pub async fn hits() -> impl Responder {
web::Json(INDEX_HITS.load(Ordering::Relaxed))
}

View file

@ -0,0 +1,43 @@
use actix_web::{get, web, HttpResponse, Responder};
use bytes::Bytes;
use futures::stream::{self, Stream, StreamExt};
type BoxedError = Box<dyn std::error::Error + 'static>;
pub struct IndexPage {
before: String,
after: String,
}
impl IndexPage {
async fn render(&self) -> impl Stream<Item = Result<Bytes, BoxedError>> + Send {
let renderer = yew::ServerRenderer::<crate::ui::App>::new();
let before = self.before.clone();
let after = self.after.clone();
stream::once(async move { before })
.chain(renderer.render_stream())
.chain(stream::once(async move { after }))
.map(|m| Result::<_, BoxedError>::Ok(m.into()))
}
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let index_html = std::fs::read_to_string(path.as_ref())?;
let (index_before, index_after) = index_html.split_once("<body>").unwrap().to_owned();
let (mut index_before, index_after) = (index_before.to_owned(), index_after.to_owned());
index_before.push_str("<body>");
log::trace!("<body> before: {}", index_before);
log::trace!("<body> after: {}", index_after);
Ok(Self {
before: index_before.to_owned(),
after: index_after.to_owned(),
})
}
}
#[get("/")]
pub async fn index(page: web::Data<IndexPage>) -> impl Responder {
super::get_hits::INDEX_HITS.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
HttpResponse::Ok().streaming(page.render().await)
}

View file

@ -0,0 +1,8 @@
use actix_web::{get, web, Responder};
#[get("/{name}")]
pub async fn resource(path: web::Path<String>) -> impl Responder {
//println!("GET resource {}", path);
let filepath = std::path::PathBuf::from("dist").join(&*path);
actix_files::NamedFile::open_async(filepath).await
}

View file

@ -0,0 +1,7 @@
pub(crate) mod get_hits;
mod get_index;
mod get_resources;
pub use get_hits::hits;
pub use get_index::{index, IndexPage};
pub use get_resources::resource;

View file

@ -0,0 +1,27 @@
use actix_web::{web, App, HttpServer};
use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode};
use caylon_studio::api::{hits, index, resource, IndexPage};
#[tokio::main]
async fn main() -> std::io::Result<()> {
TermLogger::init(
LevelFilter::Debug,
Default::default(),
TerminalMode::Mixed,
ColorChoice::Auto,
)
.unwrap();
log::info!("Starting HTTP server @ 127.0.0.1:8080");
HttpServer::new(|| {
App::new()
.app_data(web::Data::new(IndexPage::load("dist/index.html").unwrap()))
.service(index)
.service(resource)
.service(hits)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

View file

@ -0,0 +1,13 @@
use caylon_studio::ui::App;
#[cfg(target_arch = "wasm32")]
fn main() {
wasm_log::init(wasm_log::Config::default());
log::info!("Hydrating UI");
yew::Renderer::<App>::new().hydrate();
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
compile_error!("frontend is for browsers (wasm32)");
}

View file

@ -0,0 +1,3 @@
#[cfg(not(target_arch = "wasm32"))]
pub mod api;
pub mod ui;

View file

@ -0,0 +1,26 @@
use yew::prelude::*;
use super::element::ElementsComponent;
use super::FooterComponent;
use super::JsonViewComponent;
use super::TitleComponent;
use super::{JsonContext, JsonCtx};
#[function_component(App)]
pub fn app() -> Html {
let json_ctx = use_reducer(JsonCtx::init);
log::debug!("App render");
html! {
<main>
<TitleComponent />
<ContextProvider<JsonContext> context={json_ctx}>
<div class={classes!("work-view")}>
<JsonViewComponent />
<ElementsComponent />
</div>
//<h1>{ "Hello World!" }</h1>
</ContextProvider<JsonContext>>
<FooterComponent />
</main>
}
}

View file

@ -0,0 +1,194 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
#[derive(Properties, PartialEq)]
pub struct AddElementProps {
pub json_ctx: JsonContext,
}
pub enum AddElementMsg {
SelectCb(SelectedElementType),
AddClick,
ReDraw,
NoOp,
}
#[derive(PartialEq, Eq)]
pub enum SelectedElementType {
Button,
Toggle,
Slider,
ReadingDisplay,
ResultDisplay,
EventDisplay,
}
pub struct AddElementComponent {
selected: SelectedElementType,
}
impl Component for AddElementComponent {
type Message = AddElementMsg;
type Properties = AddElementProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
selected: SelectedElementType::EventDisplay, // should be last <option> in <select>
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AddElementMsg::SelectCb(elem) => {
self.selected = elem;
false
}
AddElementMsg::AddClick => {
let next_index = ctx.props().json_ctx.json.items().len();
let item = match &self.selected {
SelectedElementType::Button => {
caylon_config::ElementConfig::Button(caylon_config::ButtonConfig {
title: format!("Element {}", next_index),
on_click: default_top_level_action(),
})
}
SelectedElementType::Toggle => {
caylon_config::ElementConfig::Toggle(caylon_config::ToggleConfig {
title: format!("Element {}", next_index),
description: None, // TODO
on_toggle: default_top_level_action(),
})
}
SelectedElementType::Slider => {
caylon_config::ElementConfig::Slider(caylon_config::SliderConfig {
title: format!("Element {}", next_index),
min: 0,
max: 10,
notches: None,
on_set: default_top_level_action(),
})
}
SelectedElementType::ReadingDisplay => {
caylon_config::ElementConfig::ReadingDisplay(caylon_config::ReadingConfig {
title: format!("Element {}", next_index),
period_ms: None,
on_period: default_top_level_action(),
})
}
SelectedElementType::ResultDisplay => {
caylon_config::ElementConfig::ResultDisplay(
caylon_config::ResultDisplayConfig {
title: format!("Element {}", next_index),
result_of: 0,
},
)
}
SelectedElementType::EventDisplay => {
caylon_config::ElementConfig::EventDisplay(
caylon_config::EventDisplayConfig {
title: format!("Element {}", next_index),
event: caylon_config::EventType::GameStart,
on_event: default_top_level_action(),
},
)
}
};
ctx.props().json_ctx.dispatch(JsonCtxAction::InsertElement {
index: next_index,
item,
});
true
}
AddElementMsg::NoOp => false,
AddElementMsg::ReDraw => true,
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
log::debug!("Add element rendered");
let cb = ctx.link().callback(|#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::info!("Element dropdown select");
let elem = event.target_unchecked_into::<web_sys::HtmlSelectElement>();
let new_item = match &elem.value() as &str {
"button" => SelectedElementType::Button,
"toggle" => SelectedElementType::Toggle,
"slider" => SelectedElementType::Slider,
"reading-display" => SelectedElementType::ReadingDisplay,
"result-display" => SelectedElementType::ResultDisplay,
"event-display" => SelectedElementType::EventDisplay,
_ => SelectedElementType::ReadingDisplay,
};
AddElementMsg::SelectCb(new_item)
}
#[cfg(not(target_arch = "wasm32"))]
{
AddElementMsg::NoOp
}
});
html! {
<div class={classes!("add-element")} onload={ctx.link().callback(|_| {
log::info!("Add element dropdown loaded, redrawing");
AddElementMsg::ReDraw
})}>
<label>{"Add a new... "}</label>
<select onchange={cb} autocomplete={"off"}>
<option value={"button"}
selected={self.selected == SelectedElementType::Button}
>
{"Button"}
</option>
<option value={"toggle"}
selected={self.selected == SelectedElementType::Toggle}
>
{"Toggle"}
</option>
<option value={"slider"}
selected={self.selected == SelectedElementType::Slider}
>
{"Slider"}
</option>
<option value={"reading-display"}
selected={self.selected == SelectedElementType::ReadingDisplay}
>
{"Reading Display"}
</option>
<option value={"result-display"}
selected={self.selected == SelectedElementType::ResultDisplay}
>
{"Result Display"}
</option>
<option value={"event-display"}
selected={self.selected == SelectedElementType::EventDisplay}
>
{"Event Display"}
</option>
</select>
<button onclick={ctx.link()
.callback(
|_| AddElementMsg::AddClick
)} class={classes!("add-element-button")}>
{ "+" }
</button>
</div>
}
}
}
fn default_top_level_action() -> caylon_config::TopLevelActionConfig {
caylon_config::TopLevelActionConfig::Sequence(caylon_config::SequenceAction {
steps: vec![
caylon_config::ActionConfig::Command(caylon_config::CommandAction {
run: "echo \"Hello world!\"".to_owned(),
}),
]
})
}
/*fn default_action() -> caylon_config::ActionConfig {
caylon_config::ActionConfig::Command(
caylon_config::CommandAction { run: "echo \"Hello world!\"".to_owned() }
)
}*/

View file

@ -0,0 +1,72 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::ButtonConfig;
#[derive(Properties, PartialEq)]
pub struct ButtonProps {
pub index: usize,
pub config: ButtonConfig,
pub json_ctx: JsonContext,
}
pub struct ButtonComponent;
impl Component for ButtonComponent {
type Message = ElementMessage;
type Properties = ButtonProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(_desc) => false,
ElementMessage::SetAction(action) => {
new_config.on_click = action;
true
}
ElementMessage::SetPeriod(_) => false,
ElementMessage::SetResultOf(_) => false,
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::Button(new_config),
});
}
update_needed
}
fn view(&self, ctx: &Context<Self>) -> Html {
//let theme = &ctx.props().theme;
let props = ctx.props();
let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-button", "caylon-element")}>
// TODO editing
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::ActionComponent
index={props.index}
config={props.config.on_click.clone()}
{callback}
/>
<super::fake::FakeButtonComponent config={props.config.clone()} />
</div>
}
}
}

View file

@ -0,0 +1,230 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use super::super::ElementMessage;
use caylon_config::{TopLevelActionConfig, ActionConfig};
#[derive(PartialEq, Eq)]
enum SelectedActionConfig {
Command,
Transform,
Javascript,
Json
}
fn selected(act: &ActionConfig) -> SelectedActionConfig {
match act {
ActionConfig::Command(_) => SelectedActionConfig::Command,
ActionConfig::Transform(_) => SelectedActionConfig::Transform,
ActionConfig::Javascript(_) => SelectedActionConfig::Javascript,
ActionConfig::Json(_) => SelectedActionConfig::Json,
}
}
#[derive(Properties, PartialEq)]
pub struct ActionProps {
pub index: usize,
pub config: TopLevelActionConfig,
pub callback: super::EditCallback,
}
pub struct ActionComponent;
impl Component for ActionComponent {
type Message = ();
type Properties = ActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let inner_action = match &props.config {
TopLevelActionConfig::Sequence(seq) => {
let action_items = seq.steps
.clone()
.into_iter()
.enumerate()
.map(|(index, step)| {
let selected = selected(&step);
let item = match step {
ActionConfig::Command(cmd) =>
{
let cb = props.callback.clone();
let config = seq.clone();
html! {
<div class={classes!("caylon-sequence-command-action", "caylon-sequence-action-edit")}>
<super::actions::CommandActionComponent
config={cmd.clone()}
callback={
ctx.link().callback(
move |x| {
let mut new_seq = config.clone();
new_seq.steps[index] = ActionConfig::Command(x);
cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Sequence(new_seq)))
})
}
/>
</div>
}
},
ActionConfig::Transform(t) =>
{
let cb = props.callback.clone();
let config = seq.clone();
html! {
<div class={classes!("caylon-sequence-transform-action", "caylon-sequence-action-edit")}>
<super::actions::TransformActionComponent
config={t.clone()}
callback={
ctx.link().callback(
move |x| {
let mut new_seq = config.clone();
new_seq.steps[index] = ActionConfig::Transform(x);
cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Sequence(new_seq)))
})
}
/>
</div>
}
},
ActionConfig::Javascript(js) =>
{
let cb = props.callback.clone();
let config = seq.clone();
html! {
<div class={classes!("caylon-sequence-javascript-action", "caylon-sequence-action-edit")}>
<super::actions::JavascriptActionComponent
config={js.clone()}
callback={
ctx.link().callback(
move |x| {
let mut new_seq = config.clone();
new_seq.steps[index] = ActionConfig::Javascript(x);
cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Sequence(new_seq)))
})
}
/>
</div>
}
},
ActionConfig::Json(json) =>
{
let cb = props.callback.clone();
let config = seq.clone();
html! {
<div class={classes!("caylon-sequence-json-action", "caylon-sequence-action-edit")}>
<super::actions::JsonActionComponent
config={json.clone()}
callback={
ctx.link().callback(
move |x| {
let mut new_seq = config.clone();
new_seq.steps[index] = ActionConfig::Json(x);
cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Sequence(new_seq)))
})
}
/>
</div>
}
},
};
#[cfg(target_arch = "wasm32")]
let cb = props.callback.clone();
#[cfg(target_arch = "wasm32")]
let moved_seq = seq.clone();
let dropdown_cb = ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Transformer dropdown select");
let elem = event.target_unchecked_into::<web_sys::HtmlSelectElement>();
let new_sel = match &elem.value() as &str {
"command" => SelectedActionConfig::Command,
"transform" => SelectedActionConfig::Transform,
"javascript" => SelectedActionConfig::Javascript,
"json" => SelectedActionConfig::Json,
_ => SelectedActionConfig::Json,
};
let new_item = default_action_config(new_sel);
let mut new_conf = moved_seq.clone();
new_conf.steps[index] = new_item;
cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Sequence(new_conf)));
}
#[cfg(not(target_arch = "wasm32"))]
{ }
});
html! {
<div class={classes!("caylon-sequence-action-edit-item")}>
// dropdown to change action
<label>{"Action"}</label>
<select onchange={dropdown_cb} autocomplete={"off"}>
<option value={"command"}
selected={selected == SelectedActionConfig::Command}
>
{"Command"}
</option>
<option value={"transform"}
selected={selected == SelectedActionConfig::Transform}
>
{"Transform"}
</option>
<option value={"javascript"}
selected={selected == SelectedActionConfig::Javascript}
>
{"Javascript"}
</option><option value={"json"}
selected={selected == SelectedActionConfig::Json}
>
{"JSON"}
</option>
</select>
{item}
// TODO remove button
</div>
}
}).collect::<Html>();
// TODO add button
html! {
<div class={classes!("caylon-sequence-action-edit", "caylon-action-config")}>
{action_items}
</div>
}
},
_ => html! {<span>{"//TODO"}</span>},
/*TopLevelActionConfig::Command(cmd) =>
html! {<super::actions::CommandActionComponent config={cmd.clone()} callback={ctx.link().callback(move |x| cb.emit(ElementMessage::SetAction(TopLevelActionConfig::Command(x))))} />},
TopLevelActionConfig::Transform(t) => html! {<span>{"//TODO"}</span>},
TopLevelActionConfig::Mirror(mir) => html! {<span>{"//TODO"}</span>},
TopLevelActionConfig::Javascript(js) => html! {<span>{"//TODO"}</span>},
TopLevelActionConfig::Json(json) => html! {<span>{"//TODO"}</span>},*/
};
html! {
<div class={classes!("caylon-action-edit", "caylon-editor")}>
// TODO editing
{inner_action}
</div>
}
}
}
#[cfg(target_arch = "wasm32")]
fn default_action_config(selected: SelectedActionConfig) -> ActionConfig {
match selected {
SelectedActionConfig::Command => ActionConfig::Command(
caylon_config::CommandAction { run: "echo \"Hello caylon world!\"".to_owned() }
),
SelectedActionConfig::Transform => ActionConfig::Transform(caylon_config::TransformAction {
transformer: caylon_config::TransformTypeAction::Log(caylon_config::LogTransformAction {
level: caylon_config::LogLevel::INFO
})
}),
SelectedActionConfig::Javascript => ActionConfig::Javascript(
caylon_config::JavascriptAction { run: "console.log(\"Hello caylon world!\")".to_owned() }
),
SelectedActionConfig::Json => ActionConfig::Json(
caylon_config::JsonAction { jmespath: "".to_owned() }
),
}
}

View file

@ -0,0 +1,42 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use super::super::AlwaysStringComponent;
use caylon_config::CommandAction;
#[derive(Properties, PartialEq)]
pub struct CommandActionProps {
pub config: CommandAction,
pub callback: Callback<CommandAction>,
}
pub struct CommandActionComponent;
impl Component for CommandActionComponent {
type Message = ();
type Properties = CommandActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
//props.callback.emit(ElementMessage::NoOp);
let cb = props.callback.clone();
let config = props.config.clone();
html! {
<div class={classes!("caylon-command-action-edit", "caylon-action-config")}>
<AlwaysStringComponent
title={"Run"}
value={props.config.run.clone()}
callback={ctx.link().callback(move |run: String| {
let mut new_conf = config.clone();
new_conf.run = run;
cb.emit(new_conf)
})}
/>
</div>
}
}
}

View file

@ -0,0 +1,42 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use super::super::AlwaysStringComponent;
use caylon_config::JavascriptAction;
#[derive(Properties, PartialEq)]
pub struct JavascriptActionProps {
pub config: JavascriptAction,
pub callback: Callback<JavascriptAction>,
}
pub struct JavascriptActionComponent;
impl Component for JavascriptActionComponent {
type Message = ();
type Properties = JavascriptActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
//props.callback.emit(ElementMessage::NoOp);
let cb = props.callback.clone();
let config = props.config.clone();
html! {
<div class={classes!("caylon-javascript-action-edit", "caylon-action-config")}>
<AlwaysStringComponent
title={"Run"}
value={props.config.run.clone()}
callback={ctx.link().callback(move |run: String| {
let mut new_conf = config.clone();
new_conf.run = run;
cb.emit(new_conf)
})}
/>
</div>
}
}
}

View file

@ -0,0 +1,42 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use super::super::AlwaysStringComponent;
use caylon_config::JsonAction;
#[derive(Properties, PartialEq)]
pub struct JsonActionProps {
pub config: JsonAction,
pub callback: Callback<JsonAction>,
}
pub struct JsonActionComponent;
impl Component for JsonActionComponent {
type Message = ();
type Properties = JsonActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
//props.callback.emit(ElementMessage::NoOp);
let cb = props.callback.clone();
let config = props.config.clone();
html! {
<div class={classes!("caylon-json-action-edit", "caylon-action-config")}>
<AlwaysStringComponent
title={"JMESPath"}
value={props.config.jmespath.clone()}
callback={ctx.link().callback(move |run: String| {
let mut new_conf = config.clone();
new_conf.jmespath = run;
cb.emit(new_conf)
})}
/>
</div>
}
}
}

View file

@ -0,0 +1,28 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use caylon_config::MirrorAction;
#[derive(Properties, PartialEq)]
pub struct MirrorActionProps {
pub config: MirrorAction,
pub callback: Callback<MirrorAction>,
}
pub struct MirrorActionComponent;
impl Component for MirrorActionComponent {
type Message = ();
type Properties = MirrorActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<div class={classes!("caylon-mirror-action-edit", "caylon-action-config")}>
</div>
}
}
}

View file

@ -0,0 +1,11 @@
mod command;
mod javascript;
mod json;
mod mirror;
mod transform;
pub use command::{CommandActionProps, CommandActionComponent};
pub use javascript::{JavascriptActionProps, JavascriptActionComponent};
pub use json::{JsonActionProps, JsonActionComponent};
pub use mirror::{MirrorActionProps, MirrorActionComponent};
pub use transform::{TransformActionProps, TransformActionComponent};

View file

@ -0,0 +1,232 @@
use yew::prelude::*;
//use super::super::super::{JsonContext, JsonCtxAction};
use super::super::AlwaysStringComponent;
use caylon_config::{TransformAction, TransformTypeAction};
#[cfg(target_arch = "wasm32")]
use caylon_config::PatternConfig;
#[derive(PartialEq, Eq)]
enum SelectedTransformer {
Replace,
Expand,
Log,
}
fn selected(trans: &TransformTypeAction) -> SelectedTransformer {
match trans {
TransformTypeAction::Replace(_) => SelectedTransformer::Replace,
TransformTypeAction::Expand(_) => SelectedTransformer::Expand,
TransformTypeAction::Log(_) => SelectedTransformer::Log,
}
}
#[derive(Properties, PartialEq)]
pub struct TransformActionProps {
pub config: TransformAction,
pub callback: Callback<TransformAction>,
}
pub struct TransformActionComponent;
impl Component for TransformActionComponent {
type Message = ();
type Properties = TransformActionProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
//props.callback.emit(ElementMessage::NoOp);
#[cfg(target_arch = "wasm32")]
let cb = props.callback.clone();
let config = props.config.clone();
let selected = selected(&config.transformer);
let dropdown_cb = ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Transformer dropdown select");
let elem = event.target_unchecked_into::<web_sys::HtmlSelectElement>();
let mut new_config = config.clone();
new_config.transformer = match &elem.value() as &str {
"replace" => default_replace_transformer(),
"expand" => default_expand_transformer(),
"log" => default_log_transformer(),
_ => default_log_transformer(),
};
cb.emit(new_config);
}
#[cfg(not(target_arch = "wasm32"))]
{ }
});
//let config = props.config.clone();
let editor = match &props.config.transformer {
TransformTypeAction::Replace(rep) => {
// TODO allow for more than one pattern
//let moved_rep = rep.clone();
let items = rep.patterns.iter()
.enumerate()
.map(|(index, pattern)| {
let cb_for_p = props.callback.clone();
let cb_for_f = props.callback.clone();
let moved_rep_p = rep.clone();
let moved_rep_f = rep.clone();
html! {
<div class={classes!("caylon-transformer-replace-item")}>
<AlwaysStringComponent
title={"Regex pattern"}
value={pattern.pattern.clone()}
callback={ctx.link().callback(move |val: String| {
let mut new_conf = moved_rep_p.clone();
new_conf.patterns[index].pattern = val;
cb_for_p.emit(TransformAction {
transformer: TransformTypeAction::Replace(new_conf)
})
})}
/>
<AlwaysStringComponent
title={"Format"}
value={pattern.pattern.clone()}
callback={ctx.link().callback(move |val: String| {
let mut new_conf = moved_rep_f.clone();
new_conf.patterns[index].format = val;
cb_for_f.emit(TransformAction {
transformer: TransformTypeAction::Replace(new_conf)
})
})}
/>
</div>
}
}).collect::<Html>();
html! {
<div class={classes!("caylon-transformer-replace")}>
{items}
</div>
}
}
TransformTypeAction::Expand(exp) => {
let cb = props.callback.clone();
let moved_exp = exp.clone();
html! {
<div class={classes!("caylon-transformer-expand")}>
<AlwaysStringComponent
title={"Format"}
value={exp.format.clone()}
callback={ctx.link().callback(move |val: String| {
let mut new_conf = moved_exp.clone();
new_conf.format = val;
cb.emit(TransformAction {
transformer: TransformTypeAction::Expand(new_conf)
})
})}
/>
</div>
}
}
TransformTypeAction::Log(log) => {
#[cfg(target_arch = "wasm32")]
let cb = props.callback.clone();
#[cfg(target_arch = "wasm32")]
let moved_log = log.clone();
let log_cb = ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Transformer log dropdown select");
let elem = event.target_unchecked_into::<web_sys::HtmlSelectElement>();
let mut new_conf = moved_log.clone();
new_conf.level = match &elem.value() as &str {
"debug" => caylon_config::LogLevel::DEBUG,
"info" => caylon_config::LogLevel::INFO,
"warn" => caylon_config::LogLevel::WARN,
"error" => caylon_config::LogLevel::ERROR,
_ => caylon_config::LogLevel::ERROR,
};
cb.emit(TransformAction {
transformer: TransformTypeAction::Log(new_conf)
})
}
#[cfg(not(target_arch = "wasm32"))]
{ }
});
html! {
<div class={classes!("caylon-transformer-log")}>
<label>{"Log level"}</label>
<select onchange={log_cb} autocomplete={"off"}>
<option value={"debug"}
selected={log.level == caylon_config::LogLevel::DEBUG}
>
{"Debug"}
</option>
<option value={"info"}
selected={log.level == caylon_config::LogLevel::INFO}
>
{"Info"}
</option>
<option value={"warn"}
selected={log.level == caylon_config::LogLevel::WARN}
>
{"Warn"}
</option><option value={"error"}
selected={log.level == caylon_config::LogLevel::ERROR}
>
{"Error"}
</option>
</select>
</div>
}
}
};
html! {
<div class={classes!("caylon-transformer-action-edit", "caylon-action-config")}>
<label>{"Type"}</label>
<select onchange={dropdown_cb} autocomplete={"off"}>
<option value={"replace"}
selected={selected == SelectedTransformer::Replace}
>
{"Replace"}
</option>
<option value={"expand"}
selected={selected == SelectedTransformer::Expand}
>
{"Expand"}
</option>
<option value={"log"}
selected={selected == SelectedTransformer::Log}
>
{"Log"}
</option>
</select>
<div class={classes!("caylon-transformer-type-action-edit")}>
{editor}
</div>
</div>
}
}
}
#[cfg(target_arch = "wasm32")]
fn default_replace_transformer() -> TransformTypeAction {
TransformTypeAction::Replace(caylon_config::ReplaceTransformAction { patterns: vec![
PatternConfig {
pattern: "regex".to_owned(),
format: "$1".to_owned(),
i: None,
m: None,
s: None,
u: None,
x: None,
}
] })
}
#[cfg(target_arch = "wasm32")]
fn default_expand_transformer() -> TransformTypeAction {
TransformTypeAction::Expand(caylon_config::ExpandTransformAction { format: "$CAYLON_VALUE".into() })
}
#[cfg(target_arch = "wasm32")]
fn default_log_transformer() -> TransformTypeAction {
TransformTypeAction::Log(caylon_config::LogTransformAction { level: caylon_config::LogLevel::INFO })
}

View file

@ -0,0 +1,45 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct AlwaysStringProps {
pub title: Option<&'static str>,
pub value: String,
pub callback: Callback<String>,
}
pub struct AlwaysStringComponent;
impl Component for AlwaysStringComponent {
type Message = ();
type Properties = AlwaysStringProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = props.callback.clone();
html! {
<div class={classes!("caylon-always-str-edit", "caylon-editor")}>
{props.title.map(|t| html!{ <label class={classes!("caylon-label-edit")}>{t}</label>})}
<input type="text"
value={props.value.clone()}
onchange={ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Always str input change");
let elem = event.target_unchecked_into::<web_sys::HtmlInputElement>();
callback.emit(elem.value());
}
#[cfg(not(target_arch = "wasm32"))]
{
callback.emit("[UNREACHABLE]".into())
}
})}
class={classes!("caylon-always-str-input", "caylon-input-editor")}
/>
</div>
}
}
}

View file

@ -0,0 +1,50 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct AlwaysUsizeProps {
pub title: Option<&'static str>,
pub value: usize,
pub callback: Callback<usize>,
}
pub struct AlwaysUsizeComponent;
impl Component for AlwaysUsizeComponent {
type Message = ();
type Properties = AlwaysUsizeProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = props.callback.clone();
html! {
<div class={classes!("caylon-always-usize-edit", "caylon-editor")}>
{props.title.map(|t| html!{ <label class={classes!("caylon-label-edit")}>{t}</label>})}
<input type="number"
value={props.value.to_string()}
onchange={ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Always usize input change");
let elem = event.target_unchecked_into::<web_sys::HtmlInputElement>();
let result: Result<usize, _> = elem.value().parse();
match result {
Ok(value) => callback.emit(value),
Err(e) => log::warn!("Failed to parse always usize: {}", e),
}
}
#[cfg(not(target_arch = "wasm32"))]
{
callback.emit(usize::MAX)
}
})}
class={classes!("caylon-always-usize-input", "caylon-input-editor")}
/>
</div>
}
}
}

View file

@ -0,0 +1,15 @@
mod action_editor;
mod always_str;
mod always_usize;
mod optional_str;
mod optional_u64;
pub mod actions;
pub use action_editor::{ActionComponent, ActionProps};
pub use always_str::{AlwaysStringComponent, AlwaysStringProps};
pub use always_usize::{AlwaysUsizeComponent, AlwaysUsizeProps};
pub use optional_str::{OptionStringComponent, OptionStringProps};
pub use optional_u64::{OptionU64Component, OptionU64Props};
type EditCallback = yew::prelude::Callback<super::ElementMessage>;

View file

@ -0,0 +1,46 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct OptionStringProps {
pub title: Option<&'static str>,
pub value: Option<String>,
pub callback: Callback<Option<String>>,
}
pub struct OptionStringComponent;
impl Component for OptionStringComponent {
type Message = ();
type Properties = OptionStringProps;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = props.callback.clone();
html! {
<div class={classes!("caylon-option-str-edit", "caylon-editor")}>
{props.title.map(|t| html!{ <label class={classes!("caylon-label-edit")}>{t}</label>})}
<input type="text"
value={props.value.clone().unwrap_or("".into())}
onchange={ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Option str input change");
let elem = event.target_unchecked_into::<web_sys::HtmlInputElement>();
let value = elem.value();
callback.emit(if value.is_empty() { None } else { Some(value) });
}
#[cfg(not(target_arch = "wasm32"))]
{
callback.emit(Some("[UNREACHABLE]".into()))
}
})}
class={classes!("caylon-option-str-input", "caylon-input-editor")}
/>
</div>
}
}
}

View file

@ -0,0 +1,46 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct OptionU64Props {
pub title: Option<&'static str>,
pub value: Option<u64>,
pub callback: Callback<Option<u64>>,
}
pub struct OptionU64Component;
impl Component for OptionU64Component {
type Message = ();
type Properties = OptionU64Props;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = props.callback.clone();
html! {
<div class={classes!("caylon-option-u64-edit", "caylon-editor")}>
{props.title.map(|t| html!{ <label class={classes!("caylon-label-edit")}>{t}</label>})}
<input type="number"
value={props.value.unwrap_or(0).to_string()}
onchange={ctx.link().callback(move |#[allow(unused_variables)] event: Event| {
#[cfg(target_arch = "wasm32")]
{
log::debug!("Option u64 input change");
let elem = event.target_unchecked_into::<web_sys::HtmlInputElement>();
let value: Option<u64> = elem.value().parse().ok();
callback.emit(value);
}
#[cfg(not(target_arch = "wasm32"))]
{
callback.emit(Some(u64::MAX))
}
})}
class={classes!("caylon-option-u64-input", "caylon-input-editor")}
/>
</div>
}
}
}

View file

@ -0,0 +1,65 @@
use yew::prelude::*;
use super::super::JsonContext;
use caylon_config::ElementConfig;
#[function_component]
pub fn ElementsComponent() -> Html {
log::debug!("Elements render");
let json_ctx = use_context::<JsonContext>().expect("Missing JSON context");
let elem_view = if json_ctx.json.items().is_empty() {
html! {
{"--- No elements ---"}
}
} else {
let elements_html = json_ctx
.json
.items()
.iter()
.enumerate()
.map(|(i, elem)| element_impl(i, elem, &json_ctx))
.collect::<Html>();
html! {
{elements_html}
}
};
html! {
<div class={classes!("elements-view")}>
<div class={classes!("elements-toolbar")}>
<super::AddElementComponent {json_ctx}/>
</div>
<div class={classes!("elements-list")}>
{elem_view}
</div>
</div>
}
}
fn element_impl(index: usize, elem: &ElementConfig, ctx: &JsonContext) -> Html {
let inner = match elem {
ElementConfig::Button(button) => html! {
<super::button::ButtonComponent {index} config={button.to_owned()} json_ctx={ctx.to_owned()}/>
},
ElementConfig::Toggle(toggle) => {
html! {<super::toggle::ToggleComponent {index} config={toggle.to_owned()} json_ctx={ctx.to_owned()} />}
}
ElementConfig::Slider(slider) => {
html! {<super::slider::SliderComponent {index} config={slider.to_owned()} json_ctx={ctx.to_owned()} />}
}
ElementConfig::ReadingDisplay(disp) => {
html! {<super::reading_display::ReadingDisplayComponent {index} config={disp.to_owned()} json_ctx={ctx.to_owned()} />}
}
ElementConfig::ResultDisplay(disp) => {
html! {<super::result_display::ResultDisplayComponent {index} config={disp.to_owned()} json_ctx={ctx.to_owned()} />}
}
ElementConfig::EventDisplay(disp) => {
html! {<super::event_display::EventDisplayComponent {index} config={disp.to_owned()} json_ctx={ctx.to_owned()} />}
} //_ => html!{{format!("elem #{} //TODO", index)}},
};
html! {
<div class={classes!("elements-item")}>
{inner}
<super::RemoveElementComponent {index} />
</div>
}
}

View file

@ -0,0 +1,66 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::EventDisplayConfig;
#[derive(Properties, PartialEq)]
pub struct EventDisplayProps {
pub index: usize,
pub config: EventDisplayConfig,
pub json_ctx: JsonContext,
}
pub struct EventDisplayComponent;
impl Component for EventDisplayComponent {
type Message = ElementMessage;
type Properties = EventDisplayProps;
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(_desc) => false,
ElementMessage::SetAction(action) => {
new_config.on_event = action;
true
}
ElementMessage::SetPeriod(_) => false,
ElementMessage::SetResultOf(_) => false,
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::EventDisplay(new_config),
});
}
update_needed
}
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-event-display", "caylon-element")}>
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::ActionComponent index={props.index} config={props.config.on_event.clone()} {callback}/>
<super::fake::FakeDisplayComponent title={props.config.title.clone()} />
</div>
}
}
}

View file

@ -0,0 +1,15 @@
use yew::prelude::*;
use caylon_config::ButtonConfig;
#[derive(Properties, PartialEq)]
pub struct FakeButtonProps {
pub config: ButtonConfig,
}
#[function_component]
pub fn FakeButtonComponent(props: &FakeButtonProps) -> Html {
html! {
<button type="button" class={classes!("fake-button")}>{props.config.title.clone()}</button>
}
}

View file

@ -0,0 +1,19 @@
use yew::prelude::*;
//use caylon_config::DisplayConfig;
#[derive(Properties, PartialEq)]
pub struct FakeDisplayProps {
pub title: String,
pub content: Option<String>,
}
#[function_component]
pub fn FakeDisplayComponent(props: &FakeDisplayProps) -> Html {
html! {
<div class={classes!("fake-display")}>
<span class={classes!("fake-display-title")}>{props.title.clone()}</span>
<span class={classes!("fake-display-content")}>{props.content.clone().unwrap_or_else(|| "[info]".into())}</span>
</div>
}
}

View file

@ -0,0 +1,9 @@
mod button;
mod display;
mod slider;
mod toggle;
pub use button::*;
pub use display::*;
pub use slider::*;
pub use toggle::*;

View file

@ -0,0 +1,18 @@
use yew::prelude::*;
use caylon_config::SliderConfig;
#[derive(Properties, PartialEq)]
pub struct FakeSliderProps {
pub config: SliderConfig,
}
#[function_component]
pub fn FakeSliderComponent(props: &FakeSliderProps) -> Html {
html! {
<div class={classes!("fake-slider")}>
<span class={classes!("fake-slider-title")}>{props.config.title.clone()}</span>
<input type="range" min={props.config.min.to_string()} max={props.config.max.to_string()} value={0} class={classes!("fake-slider-input")} />
</div>
}
}

View file

@ -0,0 +1,24 @@
use yew::prelude::*;
use caylon_config::ToggleConfig;
#[derive(Properties, PartialEq)]
pub struct FakeToggleProps {
pub config: ToggleConfig,
}
#[function_component]
pub fn FakeToggleComponent(props: &FakeToggleProps) -> Html {
html! {
<div class={classes!("fake-toggle")}>
<span class={classes!("fake-toggle-title")}>{props.config.title.clone()}</span>
{props.config.description.clone().map(|desc| html! {
<span class={classes!("fake-toggle-description")}>{desc}</span>
})}
<label class={classes!("fake-toggle-button")}>
<input type={"checkbox"} />
<span class={classes!("toggle-round")}></span>
</label>
</div>
}
}

View file

@ -0,0 +1,22 @@
//! HTML versions of UI elements that Caylon supports.
//! These do not aim to exactly clone the Steam Deck style,
//! as long as they give the right impression.
mod add_element;
mod button;
mod elements;
mod event_display;
mod msg_common;
mod reading_display;
mod remove_element;
mod result_display;
mod slider;
mod toggle;
pub mod fake;
pub mod edit;
pub use add_element::AddElementComponent;
pub use elements::ElementsComponent;
pub use msg_common::{ElementMessage, ElementCtx, ElementContext};
pub use remove_element::{RemoveElementComponent, RemoveElementProps};

View file

@ -0,0 +1,105 @@
use yew::prelude::*;
use caylon_config::{ElementConfig, TopLevelActionConfig};
use std::rc::Rc;
pub enum ElementMessage {
SetTitle(String),
SetDescription(Option<String>),
SetAction(TopLevelActionConfig),
SetPeriod(Option<u64>),
SetResultOf(usize),
NoOp,
}
// unused (it's a bad idea)
#[derive(PartialEq)]
pub struct ElementCtx(ElementConfig);
pub type ElementContext = UseReducerHandle<ElementCtx>;
impl Eq for ElementCtx {}
impl ElementCtx {
pub fn init(element: ElementConfig) -> Self {
Self(element)
}
}
impl Reducible for ElementCtx {
type Action = ElementMessage;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let new_config = match self.0.clone() {
ElementConfig::Button(mut button) => {
match action {
ElementMessage::SetTitle(title) => button.title = title,
ElementMessage::SetDescription(_) => {},
ElementMessage::SetAction(action) => button.on_click = action,
ElementMessage::SetPeriod(_) => {},
ElementMessage::SetResultOf(_) => {},
ElementMessage::NoOp => {},
}
ElementConfig::Button(button)
},
ElementConfig::Toggle(mut toggle) => {
match action {
ElementMessage::SetTitle(title) => toggle.title = title,
ElementMessage::SetDescription(desc) => toggle.description = desc,
ElementMessage::SetAction(action) => toggle.on_toggle = action,
ElementMessage::SetPeriod(_) => {},
ElementMessage::SetResultOf(_) => {},
ElementMessage::NoOp => {},
}
ElementConfig::Toggle(toggle)
},
ElementConfig::Slider(mut slider) => {
match action {
ElementMessage::SetTitle(title) => slider.title = title,
ElementMessage::SetDescription(_) => {},
ElementMessage::SetAction(action) => slider.on_set = action,
ElementMessage::SetPeriod(_) => {},
ElementMessage::SetResultOf(_) => {},
ElementMessage::NoOp => {},
}
ElementConfig::Slider(slider)
},
ElementConfig::ReadingDisplay(mut disp) => {
match action {
ElementMessage::SetTitle(title) => disp.title = title,
ElementMessage::SetDescription(_) => {},
ElementMessage::SetAction(action) => disp.on_period = action,
ElementMessage::SetPeriod(period) => disp.period_ms = period,
ElementMessage::SetResultOf(_) => {},
ElementMessage::NoOp => {},
}
ElementConfig::ReadingDisplay(disp)
},
ElementConfig::ResultDisplay(mut disp) => {
match action {
ElementMessage::SetTitle(title) => disp.title = title,
ElementMessage::SetDescription(_) => {},
ElementMessage::SetAction(_) => {},
ElementMessage::SetPeriod(_) => {},
ElementMessage::SetResultOf(result) => disp.result_of = result,
ElementMessage::NoOp => {},
}
ElementConfig::ResultDisplay(disp)
},
ElementConfig::EventDisplay(mut disp) => {
match action {
ElementMessage::SetTitle(title) => disp.title = title,
ElementMessage::SetDescription(_) => {},
ElementMessage::SetAction(action) => disp.on_event = action,
ElementMessage::SetPeriod(_) => {},
ElementMessage::SetResultOf(_) => {},
ElementMessage::NoOp => {},
}
ElementConfig::EventDisplay(disp)
},
};
Rc::new(Self::init(new_config))
}
}

View file

@ -0,0 +1,71 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::ReadingConfig;
#[derive(Properties, PartialEq)]
pub struct ReadingDisplayProps {
pub index: usize,
pub config: ReadingConfig,
pub json_ctx: JsonContext,
}
pub struct ReadingDisplayComponent;
impl Component for ReadingDisplayComponent {
type Message = ElementMessage;
type Properties = ReadingDisplayProps;
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(_desc) => false,
ElementMessage::SetAction(_action) => false,
ElementMessage::SetPeriod(period) => {
new_config.period_ms = period;
true
},
ElementMessage::SetResultOf(_) => false,
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::ReadingDisplay(new_config),
});
}
update_needed
}
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-reading-display", "caylon-element")}>
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::OptionU64Component
title={"Period"}
value={props.config.period_ms}
callback={ctx.link().callback(|n_period| ElementMessage::SetPeriod(n_period))}
/>
<super::edit::ActionComponent index={props.index} config={props.config.on_period.clone()} {callback}/>
<super::fake::FakeDisplayComponent title={ctx.props().config.title.clone()} />
</div>
}
}
}

View file

@ -0,0 +1,22 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
#[derive(Properties, PartialEq)]
pub struct RemoveElementProps {
pub index: usize,
}
#[function_component]
pub fn RemoveElementComponent(props: &RemoveElementProps) -> Html {
let json_ctx = use_context::<JsonContext>().expect("Missing JSON context");
let index = props.index;
html! {
<div class={classes!("remove-element")}>
<button onclick={Callback::from(move |_| json_ctx.dispatch(JsonCtxAction::RemoveElement { index }))} class={classes!("remove-element-button")}>
{ "-" }
</button>
</div>
}
}

View file

@ -0,0 +1,71 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::ResultDisplayConfig;
#[derive(Properties, PartialEq)]
pub struct ResultDisplayProps {
pub index: usize,
pub config: ResultDisplayConfig,
pub json_ctx: JsonContext,
}
pub struct ResultDisplayComponent;
impl Component for ResultDisplayComponent {
type Message = ElementMessage;
type Properties = ResultDisplayProps;
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(_desc) => false,
ElementMessage::SetAction(_action) => false,
ElementMessage::SetPeriod(_) => false,
ElementMessage::SetResultOf(result) => {
new_config.result_of = result;
true
},
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::ResultDisplay(new_config),
});
}
update_needed
}
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
//let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-result-display", "caylon-element")}>
// TODO editing
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::AlwaysUsizeComponent
title={"Result Of"}
value={props.config.result_of}
callback={ctx.link().callback(|n_result| ElementMessage::SetResultOf(n_result))}
/>
<super::fake::FakeDisplayComponent title={ctx.props().config.title.clone()} content={"[result]".to_owned()}/>
</div>
}
}
}

View file

@ -0,0 +1,67 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::SliderConfig;
#[derive(Properties, PartialEq)]
pub struct SliderProps {
pub index: usize,
pub config: SliderConfig,
pub json_ctx: JsonContext,
}
pub struct SliderComponent;
impl Component for SliderComponent {
type Message = ElementMessage;
type Properties = SliderProps;
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(_desc) => false,
ElementMessage::SetAction(action) => {
new_config.on_set = action;
true
}
ElementMessage::SetPeriod(_) => false,
ElementMessage::SetResultOf(_) => false,
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::Slider(new_config),
});
}
update_needed
}
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-slider", "caylon-element")}>
// TODO editing
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::ActionComponent index={props.index} config={props.config.on_set.clone()} {callback}/>
<super::fake::FakeSliderComponent config={ctx.props().config.clone()} />
</div>
}
}
}

View file

@ -0,0 +1,70 @@
use yew::prelude::*;
use super::super::{JsonContext, JsonCtxAction};
use super::ElementMessage;
use caylon_config::ToggleConfig;
#[derive(Properties, PartialEq)]
pub struct ToggleProps {
pub index: usize,
pub config: ToggleConfig,
pub json_ctx: JsonContext,
}
pub struct ToggleComponent;
impl Component for ToggleComponent {
type Message = ElementMessage;
type Properties = ToggleProps;
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let mut new_config = ctx.props().config.clone();
let index = ctx.props().index;
let update_needed = match msg {
ElementMessage::SetTitle(title) => {
new_config.title = title;
true
}
ElementMessage::SetDescription(desc) => {
new_config.description = desc;
true
}
ElementMessage::SetAction(action) => {
new_config.on_toggle = action;
true
}
ElementMessage::SetPeriod(_) => false,
ElementMessage::SetResultOf(_) => false,
ElementMessage::NoOp => false,
//_ => false,
};
if update_needed {
ctx.props().json_ctx.dispatch(JsonCtxAction::UpdateElement {
index,
new_item: caylon_config::ElementConfig::Toggle(new_config),
});
}
update_needed
}
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
let callback = ctx.link().callback(|msg: Self::Message| msg);
html! {
<div class={classes!("caylon-toggle", "caylon-element")}>
// TODO editing
<super::edit::AlwaysStringComponent
title={"Title"}
value={props.config.title.clone()}
callback={ctx.link().callback(|n_title: String| ElementMessage::SetTitle(n_title))}
/>
<super::edit::ActionComponent index={props.index} config={props.config.on_toggle.clone()} {callback}/>
<super::fake::FakeToggleComponent config={ctx.props().config.clone()} />
</div>
}
}
}

View file

@ -0,0 +1,27 @@
use yew::prelude::*;
#[function_component]
pub fn FooterComponent() -> Html {
log::debug!("Footer render");
html! {
<div class={classes!("footer")}>
<span class={classes!("footer-elem")}>
{"Javascript required (though it's mostly WASM)"}
</span>
<span class={classes!("footer-elem")}>
{" Made for "}
<a href={"https://github.com/NGnius/caylon"}>{"Caylon"}</a>
{" by "}
<a href={"http://ngni.us"}>{"NGnius"}</a>
</span>
<span class={classes!("footer-elem")}>
<a href={"https://liberapay.com/NGnius"}>{"Donate"}</a>
</span>
<span class={classes!("footer-elem")}>
<Suspense fallback={super::hit_counter::fallback()}>
<super::HitCounterComponent />
</Suspense>
</span>
</div>
}
}

View file

@ -0,0 +1,26 @@
use yew::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
async fn fetch_hits() -> u64 {
// URL only valid server-side, after the TLS reverse-proxy
//let resp = reqwest::get("http://localhost:8080/stats/hits").await.unwrap();
//let hits = resp.json::<u64>().await.unwrap();
let hits = crate::api::get_hits::INDEX_HITS.load(std::sync::atomic::Ordering::Relaxed);
hits
}
#[function_component]
pub fn HitCounterComponent() -> HtmlResult {
let hits = use_prepared_state!(async move |_| -> u64 { fetch_hits().await }, ())?.unwrap();
Ok(html! {
<span class={classes!("hit-count")}>{"Hit #"}{hits}</span>
})
}
pub fn fallback() -> Html {
html! {
<span class={classes!("hit-count")}>{"Hit ..."}</span>
}
}

View file

@ -0,0 +1,74 @@
use std::rc::Rc;
use yew::prelude::*;
pub enum JsonCtxAction {
InsertElement {
item: caylon_config::ElementConfig,
index: usize,
},
RemoveElement {
index: usize,
},
UpdateElement {
new_item: caylon_config::ElementConfig,
index: usize,
},
}
#[derive(Clone, PartialEq)]
pub struct JsonCtx {
pub json: caylon_config::BaseConfig,
}
pub type JsonContext = UseReducerHandle<JsonCtx>;
impl Eq for JsonCtx {}
impl JsonCtx {
pub fn init() -> Self {
Self {
json: minimal_config(),
}
}
}
impl Reducible for JsonCtx {
type Action = JsonCtxAction;
fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
let json_base = match action {
JsonCtxAction::InsertElement { item, index } => {
let mut items = self.json.items().to_owned();
items.insert(index, item);
caylon_config::BaseConfig::assemble(items, self.json.get_about().to_owned())
}
JsonCtxAction::RemoveElement { index } => {
let mut items = self.json.items().to_owned();
items.remove(index);
caylon_config::BaseConfig::assemble(items, self.json.get_about().to_owned())
}
JsonCtxAction::UpdateElement { new_item, index } => {
let mut items = self.json.items().to_owned();
if items.len() > index {
items[index] = new_item;
}
caylon_config::BaseConfig::assemble(items, self.json.get_about().to_owned())
}
};
Rc::new(Self { json: json_base })
}
}
fn minimal_config() -> caylon_config::BaseConfig {
caylon_config::BaseConfig::V0 {
items: vec![],
about: caylon_config::AboutConfig {
name: env!("CARGO_PKG_NAME").to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
description: "Studio-generated UI layout for Caylon".to_owned(),
url: Some("https://caylon.ngni.us".to_owned()),
authors: vec!["NGnius".to_owned()],
license: Some("MIT".to_owned()),
},
}
}

View file

@ -0,0 +1,20 @@
use serde_json::to_string_pretty;
use yew::prelude::*;
#[function_component]
pub fn JsonViewComponent() -> Html {
log::debug!("Json view render");
let json_ctx = use_context::<super::JsonContext>().expect("Missing JSON context");
let pretty_json = to_string_pretty(&json_ctx.json).expect("Invalid JSON");
let line_count = pretty_json.chars().filter(|&c| c == '\n').count() + 2;
/*html! {
<pre>
{pretty_json.clone()}
</pre>
}*/
html! {
<div class={classes!("json-view")}>
<textarea class={classes!("json-input")} rows={line_count.to_string()} value={pretty_json} readonly={true}/>
</div>
}
}

View file

@ -0,0 +1,14 @@
mod app;
pub mod element;
mod footer_bar;
mod hit_counter;
mod json_context;
mod json_view;
mod title_bar;
pub use app::App;
pub use footer_bar::FooterComponent;
pub use hit_counter::HitCounterComponent;
pub use json_context::{JsonContext, JsonCtx, JsonCtxAction};
pub use json_view::JsonViewComponent;
pub use title_bar::TitleComponent;

View file

@ -0,0 +1,13 @@
use yew::prelude::*;
#[function_component]
pub fn TitleComponent() -> Html {
log::debug!("Header render");
html! {
<div class={classes!("header")}>
<span class={classes!("header-elem")}>
<h1>{"Caylon Studio"}</h1>
</span>
</div>
}
}

View file

@ -1,6 +1,5 @@
mod api;
mod cli;
mod config;
mod consts;
mod runtime;
@ -20,7 +19,7 @@ fn main() -> Result<(), ()> {
let filepath = cli_args.config.unwrap_or(consts::FILEPATH.into());
let kaylon_conf = config::BaseConfig::load(&filepath);
let kaylon_conf = caylon_config::BaseConfig::load(&filepath);
let (executor, sender) = runtime::RuntimeExecutor::new(kaylon_conf, filepath);
log::info!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);

View file

@ -1,6 +1,6 @@
use usdpl_back::core::serdes::Primitive;
use crate::config::{ElementConfig, ActionConfig, TopLevelActionConfig};
use caylon_config::{ElementConfig, ActionConfig, TopLevelActionConfig};
pub type ActError = String;

View file

@ -2,7 +2,7 @@ use std::process::Command;
use usdpl_back::core::serdes::Primitive;
use crate::config::CommandAction;
use caylon_config::CommandAction;
use super::{SeqAct, ActError};
/// Runs a CLI command in Bash

View file

@ -2,7 +2,7 @@ use std::sync::mpsc::{Sender, self};
use usdpl_back::core::serdes::Primitive;
use crate::config::JavascriptAction;
use caylon_config::JavascriptAction;
use super::{SeqAct, ActError};
use crate::runtime::{RuntimeIO, JavascriptCommand, Javascript};

View file

@ -2,7 +2,7 @@ use usdpl_back::core::serdes::Primitive;
use jmespath::{Expression, Variable};
use crate::config::JsonAction;
use caylon_config::JsonAction;
use super::{SeqAct, ActError};
pub struct JsonActor {

View file

@ -2,7 +2,7 @@ use std::time::Duration;
use usdpl_back::core::serdes::Primitive;
use crate::config::ReadingConfig;
use caylon_config::ReadingConfig;
use super::{Act, SeqAct, ActError, TopLevelActorType};
use crate::runtime::{RouterCommand, RuntimeIO};

View file

@ -1,6 +1,6 @@
use usdpl_back::core::serdes::Primitive;
use crate::config::SequenceAction;
use caylon_config::SequenceAction;
use super::{SeqAct, ActError, ActorType};
pub struct SequenceActor {

View file

@ -3,7 +3,7 @@ use usdpl_back::core::serdes::Primitive;
use crate::runtime::primitive_utils;
use crate::config::{TransformAction, TransformTypeAction, ReplaceTransformAction, ExpandTransformAction, LogTransformAction, LogLevel, PatternConfig};
use caylon_config::{TransformAction, TransformTypeAction, ReplaceTransformAction, ExpandTransformAction, LogTransformAction, LogLevel, PatternConfig};
use super::{SeqAct, ActError};
/// Changes the output or input of an act

View file

@ -3,7 +3,7 @@ use std::sync::mpsc::Sender;
use usdpl_back::core::serdes::Primitive;
use crate::api::SteamEvent;
use crate::config::{AboutConfig, ElementConfig};
use caylon_config::{AboutConfig, ElementConfig};
/// An API operation for the executor to perform
pub enum QueueAction {

View file

@ -4,7 +4,7 @@ use std::sync::mpsc::{self, Receiver, Sender};
use usdpl_back::core::serdes::Primitive;
use crate::config::{BaseConfig, ElementConfig};
use caylon_config::{BaseConfig, ElementConfig};
use crate::api::SteamEvent;
use super::{QueueItem, QueueAction, Act, SeqAct};
use super::{ResultRouter, RouterCommand, JavascriptRouter, JavascriptCommand};

14
main.py
View file

@ -11,6 +11,18 @@ class Plugin:
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
async def _main(self):
# startup
self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend", "--config", "./caylon.json"])
self.backend_proc = subprocess.Popen(
[PARENT_DIR + "/bin/backend", "--config", "./caylon.json"],
env = dict(os.environ))
while True:
await asyncio.sleep(1)
async def _unload(self):
# shutdown
if self.backend_proc is not None:
self.backend_proc.terminate()
try:
self.backend_proc.wait(timeout=5) # 5 seconds timeout
except subprocess.TimeoutExpired:
self.backend_proc.kill()
self.backend_proc = None

View file

@ -38,7 +38,7 @@
"typescript": "^4.9.5"
},
"dependencies": {
"decky-frontend-lib": "~3.18.10",
"decky-frontend-lib": "~3.18.11",
"react-icons": "^4.7.1",
"usdpl-front": "file:src/usdpl_front"
},

View file

@ -8,7 +8,7 @@ specifiers:
'@rollup/plugin-typescript': ^8.5.0
'@types/react': 16.14.0
'@types/webpack': ^5.28.0
decky-frontend-lib: ~3.18.10
decky-frontend-lib: ~3.18.11
react-icons: ^4.7.1
rollup: ^2.79.1
rollup-plugin-import-assets: ^1.1.1
@ -18,7 +18,7 @@ specifiers:
usdpl-front: file:src/usdpl_front
dependencies:
decky-frontend-lib: 3.18.10
decky-frontend-lib: 3.18.11
react-icons: 4.7.1
usdpl-front: file:src/usdpl_front
@ -380,8 +380,8 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001456
electron-to-chromium: 1.4.302
caniuse-lite: 1.0.30001457
electron-to-chromium: 1.4.305
node-releases: 2.0.10
update-browserslist-db: 1.0.10_browserslist@4.21.5
dev: true
@ -395,8 +395,8 @@ packages:
engines: {node: '>=6'}
dev: true
/caniuse-lite/1.0.30001456:
resolution: {integrity: sha512-XFHJY5dUgmpMV25UqaD4kVq2LsiaU5rS8fb0f17pCoXQiQslzmFgnfOxfvo1bTpTqf7dwG/N/05CnLCnOEKmzA==}
/caniuse-lite/1.0.30001457:
resolution: {integrity: sha512-SDIV6bgE1aVbK6XyxdURbUE89zY7+k1BBBaOwYwkNCglXlel/E7mELiHC64HQ+W0xSKlqWhV9Wh7iHxUjMs4fA==}
dev: true
/chrome-trace-event/1.0.3:
@ -420,8 +420,8 @@ packages:
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
dev: true
/decky-frontend-lib/3.18.10:
resolution: {integrity: sha512-2mgbA3sSkuwQR/FnmhXVrcW6LyTS95IuL6muJAmQCruhBvXapDtjk1TcgxqMZxFZwGD1IPnemPYxHZll6IgnZw==}
/decky-frontend-lib/3.18.11:
resolution: {integrity: sha512-SLb3qWJc6CLNRqcbNyJsA8D6sXf7aix8/h6VqQTWjVmel6c3SkOR6b9KMvgFRzXumfRt7XGasBnZJmyCwH4BCQ==}
dev: false
/deepmerge/4.3.0:
@ -429,8 +429,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/electron-to-chromium/1.4.302:
resolution: {integrity: sha512-Uk7C+7aPBryUR1Fwvk9VmipBcN9fVsqBO57jV2ZjTm+IZ6BMNqu7EDVEg2HxCNufk6QcWlFsBkhQyQroB2VWKw==}
/electron-to-chromium/1.4.305:
resolution: {integrity: sha512-WETy6tG0CT5gm1O+xCbyapWNsCcmIvrn4NHViIGYo2AT8FV2qUCXdaB+WqYxSv/vS5mFqhBYnfZAAkVArjBmUg==}
dev: true
/enhanced-resolve/5.12.0:

View file

@ -21,7 +21,7 @@ export async function initBackend() {
// init usdpl
await init_embedded();
init_usdpl(USDPL_PORT);
console.log("USDPL started for framework: " + target_usdpl());
console.log("CAYLON: USDPL started for framework: " + target_usdpl());
//setReady(true);
}