use anyhow::Result;
use async_std::task;
use dashmap::DashMap;
use horrorshow::{html, owned_html, Raw, Render};
use serde::{de::DeserializeOwned, Deserialize};
use std::{cmp::Ordering, string::ToString, thread};
use tide::{Request, Response};
pub use const_tweaker_attribute::tweak;
#[doc(hidden)]
#[derive(Debug)]
pub enum Field {
F32 {
value: f32,
min: f64,
max: f64,
step: f64,
module: String,
file: String,
line: u32,
},
F64 {
value: f64,
min: f64,
max: f64,
step: f64,
module: String,
file: String,
line: u32,
},
Bool {
value: bool,
module: String,
file: String,
line: u32,
},
String {
value: String,
module: String,
file: String,
line: u32,
},
}
impl Field {
pub fn module_path(&self) -> &str {
match self {
Field::F32 { module, .. }
| Field::F64 { module, .. }
| Field::Bool { module, .. }
| Field::String { module, .. } => &*module,
}
}
pub fn file(&self) -> String {
match self {
Field::F32 { file, line, .. }
| Field::F64 { file, line, .. }
| Field::Bool { file, line, .. }
| Field::String { file, line, .. } => format!("{}:{}", file, line),
}
}
pub fn line_number(&self) -> u32 {
match self {
Field::F32 { line, .. }
| Field::F64 { line, .. }
| Field::Bool { line, .. }
| Field::String { line, .. } => *line,
}
}
pub fn to_html_widget(&self, key: &str) -> String {
match self {
Field::F32 {
value,
min,
max,
step,
..
} => Field::render_float(key, (*value).into(), *min, *max, *step, "f32").to_string(),
Field::F64 {
value,
min,
max,
step,
..
} => Field::render_float(key, *value, *min, *max, *step, "f64").to_string(),
Field::Bool { value, .. } => Field::render_bool(key, *value).to_string(),
Field::String { value, .. } => Field::render_string(key, value).to_string(),
}
}
fn render_float<'a>(
key: &'a str,
value: f64,
min: f64,
max: f64,
step: f64,
http_path: &'a str,
) -> impl Render + ToString + 'a {
owned_html! {
div (class="column") {
input (type="range",
id=key,
min=min,
max=max,
step=step,
defaultValue=value,
style="width: 100%",
oninput=send(key, "Number(this.value)", http_path))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key), class="is-small")
{ : value }
}
}
}
fn render_bool<'a>(key: &'a str, value: bool) -> impl Render + ToString + 'a {
owned_html! {
div (class="column") {
input (type="checkbox",
id=key,
value=value.to_string(),
onclick=send(key, "this.checked", "bool"))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key))
{ : value.to_string() }
}
}
}
fn render_string<'a>(key: &'a str, value: &'a str) -> impl Render + ToString + 'a {
owned_html! {
div (class="column") {
input (type="text",
id=key,
value=value,
style="width: 100%",
onchange=send(key, "this.value", "string"))
{ }
}
div (class="column is-narrow") {
span (id=format!("{}_label", key))
{ : value.to_string() }
}
}
}
}
#[derive(Debug, Deserialize)]
struct PostData<T> {
key: String,
value: T,
}
lazy_static::lazy_static! {
#[doc(hidden)]
pub static ref DATA: DashMap<&'static str, Field> = DashMap::new();
}
pub fn run() -> Result<()> {
thread::spawn(|| {
task::block_on(async {
let mut app = tide::new();
app.at("/").get(main_site);
app.at("/set/f32").post(|r| handle_set_value(r, set_f32));
app.at("/set/f64").post(|r| handle_set_value(r, set_f64));
app.at("/set/bool").post(|r| handle_set_value(r, set_bool));
app.at("/set/string")
.post(|r| handle_set_value(r, set_string));
app.listen("127.0.0.1:9938").await
})
.expect("Running web server failed");
});
Ok(())
}
async fn main_site(_: Request<()>) -> Response {
let body = html! {
style { : include_str!("bulma.css") }
style { : "* { font-family: sans-serif}" }
body {
div (class="container box") {
h1 (class="title is-1") { : "Const Tweaker Web Interface" }
}
: render_widgets();
div (class="container") {
div (class="notification is-danger") {
span(id="status") { }
}
}
}
script { : Raw(include_str!("send.js")) }
};
Response::new(200)
.body_string(format!("{}", body))
.set_header("content-type", "text/html;charset=utf-8")
}
fn render_widgets() -> impl Render {
owned_html! {
@for module in modules().into_iter() {
section (class="section") {
div (class="container box") {
h3 (class="title is-3") { : format!("Module: \"{}\"", module) }
: render_module(&module)
}
}
}
}
}
fn render_module<'a>(module: &'a str) -> impl Render + 'a {
let mut data = DATA
.iter()
.filter(|kv| kv.value().module_path() == module)
.collect::<Vec<_>>();
data.sort_by(|a, b| {
a.value()
.line_number()
.partial_cmp(&b.value().line_number())
.unwrap_or(Ordering::Equal)
});
owned_html! {
@for ref_multi in data.iter() {
: render_widget(ref_multi.key(), ref_multi.value())
}
}
}
fn render_widget<'a>(key: &'a str, field: &'a Field) -> impl Render + 'a {
owned_html! {
div (class="columns") {
div (class="column is-narrow") {
span (class="is-small") { : key }
br {}
span (class="tag") { : field.file() }
}
: Raw(field.to_html_widget(key))
}
}
}
fn send(key: &str, look_for: &str, data_type: &str) -> String {
format!(
"send('{}', {}, '{}')",
key.replace("\\", "\\\\"),
look_for,
data_type
)
}
async fn handle_set_value<T, F>(mut request: Request<()>, set_value: F) -> Response
where
T: DeserializeOwned,
F: Fn(&mut Field, T),
{
let post_data: PostData<T> = request.body_json().await.expect("Could not decode JSON");
set_value(
&mut DATA
.get_mut(&*post_data.key)
.expect("Could not get item from map"),
post_data.value,
);
Response::new(200)
}
fn set_f32(field: &mut Field, new_value: f32) {
match field {
Field::F32 { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_f64(field: &mut Field, new_value: f64) {
match field {
Field::F64 { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_bool(field: &mut Field, new_value: bool) {
match field {
Field::Bool { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn set_string(field: &mut Field, new_value: String) {
match field {
Field::String { ref mut value, .. } => {
*value = new_value;
}
_ => panic!("Unexpected type, please report an issue"),
}
}
fn modules() -> Vec<String> {
let mut modules: Vec<_> = DATA
.iter()
.map(|kv| kv.value().module_path().to_string())
.collect::<_>();
modules.sort();
modules.dedup();
modules
}