const_tweaker/
lib.rs

1//! # Runtime `const` tweaking
2//!
3//! This library starts a web server at `http://127.0.0.1:9938` where you can change the values of `const` variables in your crate.
4//!
5//! `f64` & `bool` are the types that are currently supported.
6//!
7//! ## Example
8//! ```rust
9//! // Tweak `VALUE` when running in debug mode
10//! // This will render a slider in the web GUI because the type here is a `f64`
11//! #[const_tweaker::tweak]
12//! const VALUE: f64 = 0.0;
13//!
14//! // Enter a GUI/Game loop
15//! loop {
16//!     // ...
17//!
18//!     // Print the constant value that can be changed from the website.
19//!     println!("VALUE: {}", VALUE);
20//! #   break;
21//! }
22//! ```
23//!
24//! Some widgets have customizable options, as seen in the examples below:
25//!
26//! `f64`:
27//! ```rust
28//! // Spawns a slider
29//! #[const_tweaker::tweak]
30//! const DEFAULT_VALUE: f64 = 0.0;
31//!
32//! // Spawns a slider with 10 steps from 0-1
33//! #[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.1)]
34//! const CUSTOM_VALUE: f64 = 0.0;
35//! ```
36//!
37//! `bool`:
38//! ```rust
39//! // Spawns a checkbox
40//! #[const_tweaker::tweak]
41//! const DEFAULT_VALUE: bool = true;
42//! ```
43//!
44//! `&str`
45//! ```rust
46//! // Spaws a textbox
47//! #[const_tweaker::tweak]
48//! const DEFAULT_VALUE: &str = "Hi";
49//! ```
50
51// Ignore the lazy_static warning about the mutex
52#![allow(clippy::mutex_atomic)]
53
54use async_std::task;
55use dashmap::DashMap;
56use horrorshow::{html, owned_html, Raw, Render};
57use serde::{de::DeserializeOwned, Deserialize};
58use std::{cmp::Ordering, fmt::Display, string::ToString, sync::Mutex, thread};
59use tide::{Request, Response};
60
61pub use const_tweaker_attribute::tweak;
62#[doc(hidden)]
63pub use ctor::ctor;
64
65/// Type representing the const field with metadata.
66#[doc(hidden)]
67#[derive(Debug)]
68pub enum Field {
69    F32 {
70        value: f32,
71        /// Minimum value of slider.
72        min: f32,
73        /// Maximum value of slider.
74        max: f32,
75        /// Step increase of slider.
76        step: f32,
77
78        /// Rust module location.
79        module: String,
80        /// Rust file location.
81        file: String,
82        /// Rust line number in file.
83        line: u32,
84    },
85    F64 {
86        value: f64,
87        /// Minimum value of slider.
88        min: f64,
89        /// Maximum value of slider.
90        max: f64,
91        /// Step increase of slider.
92        step: f64,
93
94        /// Rust module location.
95        module: String,
96        /// Rust file location.
97        file: String,
98        /// Rust line number in file.
99        line: u32,
100    },
101    I8 {
102        value: i8,
103        /// Minimum value of slider.
104        min: i8,
105        /// Maximum value of slider.
106        max: i8,
107        /// Step increase of slider.
108        step: i8,
109
110        /// Rust module location.
111        module: String,
112        /// Rust file location.
113        file: String,
114        /// Rust line number in file.
115        line: u32,
116    },
117    U8 {
118        value: u8,
119        /// Minimum value of slider.
120        min: u8,
121        /// Maximum value of slider.
122        max: u8,
123        /// Step increase of slider.
124        step: u8,
125
126        /// Rust module location.
127        module: String,
128        /// Rust file location.
129        file: String,
130        /// Rust line number in file.
131        line: u32,
132    },
133    I16 {
134        value: i16,
135        /// Minimum value of slider.
136        min: i16,
137        /// Maximum value of slider.
138        max: i16,
139        /// Step increase of slider.
140        step: i16,
141
142        /// Rust module location.
143        module: String,
144        /// Rust file location.
145        file: String,
146        /// Rust line number in file.
147        line: u32,
148    },
149    U16 {
150        value: u16,
151        /// Minimum value of slider.
152        min: u16,
153        /// Maximum value of slider.
154        max: u16,
155        /// Step increase of slider.
156        step: u16,
157
158        /// Rust module location.
159        module: String,
160        /// Rust file location.
161        file: String,
162        /// Rust line number in file.
163        line: u32,
164    },
165    I32 {
166        value: i32,
167        /// Minimum value of slider.
168        min: i32,
169        /// Maximum value of slider.
170        max: i32,
171        /// Step increase of slider.
172        step: i32,
173
174        /// Rust module location.
175        module: String,
176        /// Rust file location.
177        file: String,
178        /// Rust line number in file.
179        line: u32,
180    },
181    U32 {
182        value: u32,
183        /// Minimum value of slider.
184        min: u32,
185        /// Maximum value of slider.
186        max: u32,
187        /// Step increase of slider.
188        step: u32,
189
190        /// Rust module location.
191        module: String,
192        /// Rust file location.
193        file: String,
194        /// Rust line number in file.
195        line: u32,
196    },
197    I64 {
198        value: i64,
199        /// Minimum value of slider.
200        min: i64,
201        /// Maximum value of slider.
202        max: i64,
203        /// Step increase of slider.
204        step: i64,
205
206        /// Rust module location.
207        module: String,
208        /// Rust file location.
209        file: String,
210        /// Rust line number in file.
211        line: u32,
212    },
213    U64 {
214        value: u64,
215        /// Minimum value of slider.
216        min: u64,
217        /// Maximum value of slider.
218        max: u64,
219        /// Step increase of slider.
220        step: u64,
221
222        /// Rust module location.
223        module: String,
224        /// Rust file location.
225        file: String,
226        /// Rust line number in file.
227        line: u32,
228    },
229    Usize {
230        value: usize,
231        /// Minimum value of slider.
232        min: usize,
233        /// Maximum value of slider.
234        max: usize,
235        /// Step increase of slider.
236        step: usize,
237
238        /// Rust module location.
239        module: String,
240        /// Rust file location.
241        file: String,
242        /// Rust line number in file.
243        line: u32,
244    },
245    Bool {
246        value: bool,
247
248        /// Rust module location.
249        module: String,
250        /// Rust file location.
251        file: String,
252        /// Rust line number in file.
253        line: u32,
254    },
255    String {
256        value: String,
257
258        /// Rust module location.
259        module: String,
260        /// Rust file location.
261        file: String,
262        /// Rust line number in file.
263        line: u32,
264    },
265}
266
267impl Field {
268    /// The full module path where the constant lives.
269    pub fn module_path(&self) -> &str {
270        match self {
271            Field::F32 { module, .. }
272            | Field::F64 { module, .. }
273            | Field::I8 { module, .. }
274            | Field::U8 { module, .. }
275            | Field::I16 { module, .. }
276            | Field::U16 { module, .. }
277            | Field::I32 { module, .. }
278            | Field::U32 { module, .. }
279            | Field::I64 { module, .. }
280            | Field::U64 { module, .. }
281            | Field::Usize { module, .. }
282            | Field::Bool { module, .. }
283            | Field::String { module, .. } => &*module,
284        }
285    }
286
287    /// The file with line number.
288    pub fn file(&self) -> String {
289        match self {
290            Field::F32 { file, line, .. }
291            | Field::F64 { file, line, .. }
292            | Field::I8 { file, line, .. }
293            | Field::U8 { file, line, .. }
294            | Field::I16 { file, line, .. }
295            | Field::U16 { file, line, .. }
296            | Field::I32 { file, line, .. }
297            | Field::U32 { file, line, .. }
298            | Field::I64 { file, line, .. }
299            | Field::U64 { file, line, .. }
300            | Field::Usize { file, line, .. }
301            | Field::Bool { file, line, .. }
302            | Field::String { file, line, .. } => format!("{}:{}", file, line),
303        }
304    }
305
306    /// Just the line number in the file.
307    pub fn line_number(&self) -> u32 {
308        match self {
309            Field::F32 { line, .. }
310            | Field::F64 { line, .. }
311            | Field::I8 { line, .. }
312            | Field::U8 { line, .. }
313            | Field::I16 { line, .. }
314            | Field::U16 { line, .. }
315            | Field::I32 { line, .. }
316            | Field::U32 { line, .. }
317            | Field::I64 { line, .. }
318            | Field::U64 { line, .. }
319            | Field::Usize { line, .. }
320            | Field::Bool { line, .. }
321            | Field::String { line, .. } => *line,
322        }
323    }
324
325    /// Create a HTML widget from this field with it's metadata.
326    pub fn to_html_widget(&self, key: &str) -> String {
327        match self {
328            Field::F32 {
329                value,
330                min,
331                max,
332                step,
333                ..
334            } => Field::render_slider(key, *value, *min, *max, *step, "f32").to_string(),
335            Field::F64 {
336                value,
337                min,
338                max,
339                step,
340                ..
341            } => Field::render_slider(key, *value, *min, *max, *step, "f64").to_string(),
342            Field::I8 {
343                value,
344                min,
345                max,
346                step,
347                ..
348            } => Field::render_slider(key, *value, *min, *max, *step, "i8").to_string(),
349            Field::U8 {
350                value,
351                min,
352                max,
353                step,
354                ..
355            } => Field::render_slider(key, *value, *min, *max, *step, "u8").to_string(),
356            Field::I16 {
357                value,
358                min,
359                max,
360                step,
361                ..
362            } => Field::render_slider(key, *value, *min, *max, *step, "i16").to_string(),
363            Field::U16 {
364                value,
365                min,
366                max,
367                step,
368                ..
369            } => Field::render_slider(key, *value, *min, *max, *step, "u16").to_string(),
370            Field::I32 {
371                value,
372                min,
373                max,
374                step,
375                ..
376            } => Field::render_slider(key, *value, *min, *max, *step, "i32").to_string(),
377            Field::U32 {
378                value,
379                min,
380                max,
381                step,
382                ..
383            } => Field::render_slider(key, *value, *min, *max, *step, "u32").to_string(),
384            Field::I64 {
385                value,
386                min,
387                max,
388                step,
389                ..
390            } => Field::render_slider(key, *value, *min, *max, *step, "i64").to_string(),
391            Field::U64 {
392                value,
393                min,
394                max,
395                step,
396                ..
397            } => Field::render_slider(key, *value, *min, *max, *step, "u64").to_string(),
398            Field::Usize {
399                value,
400                min,
401                max,
402                step,
403                ..
404            } => Field::render_slider(key, *value, *min, *max, *step, "usize").to_string(),
405            Field::Bool { value, .. } => Field::render_bool(key, *value).to_string(),
406            Field::String { value, .. } => Field::render_string(key, value).to_string(),
407        }
408    }
409
410    /// Render a slider widget for the number types.
411    fn render_slider<'a, T>(
412        key: &'a str,
413        value: T,
414        min: T,
415        max: T,
416        step: T,
417        http_path: &'a str,
418    ) -> impl Render + ToString + 'a
419    where
420        T: Display + 'a,
421    {
422        owned_html! {
423            div (class="column") {
424                input (type="range",
425                    id=key.to_string(),
426                    min=min.to_string(),
427                    max=max.to_string(),
428                    step=step.to_string(),
429                    defaultValue=value.to_string(),
430                    style="width: 100%",
431                    // The value is a string, convert it to a number so it can be properly
432                    // deserialized by serde
433                    oninput=send(key, "Number(this.value)", http_path))
434                { }
435            }
436            div (class="column is-narrow") {
437                span (id=format!("{}_label", key), class="is-small")
438                { : value.to_string() }
439            }
440        }
441    }
442
443    /// Render the bool widget.
444    fn render_bool<'a>(key: &'a str, value: bool) -> impl Render + ToString + 'a {
445        owned_html! {
446            div (class="column") {
447                input (type="checkbox",
448                    id=key,
449                    value=value.to_string(),
450                    onclick=send(key, "this.checked", "bool"))
451                { }
452            }
453            div (class="column is-narrow") {
454                span (id=format!("{}_label", key))
455                { : value.to_string() }
456            }
457        }
458    }
459
460    /// Render the string widget.
461    fn render_string<'a>(key: &'a str, value: &'a str) -> impl Render + ToString + 'a {
462        owned_html! {
463            div (class="column") {
464                input (type="text",
465                    id=key,
466                    value=value,
467                    style="width: 100%",
468                    onchange=send(key, "this.value", "string"))
469                { }
470            }
471            div (class="column is-narrow") {
472                span (id=format!("{}_label", key))
473                { : value.to_string() }
474            }
475        }
476    }
477}
478
479/// A struct used for deserializing POST request JSON data.
480#[derive(Debug, Deserialize)]
481struct PostData<T> {
482    key: String,
483    value: T,
484}
485
486lazy_static::lazy_static! {
487    /// The list of fields with their data.
488    #[doc(hidden)]
489    pub static ref DATA: DashMap<&'static str, Field> = DashMap::new();
490    /// The last known size of the DATA map, used to detect whether the page should refresh.
491    static ref LAST_MAP_SIZE: Mutex<usize> = Mutex::new(0);
492}
493
494/// Launch the `const` tweaker web service.
495///
496/// This will launch a web server at `http://127.0.01:9938`.
497#[ctor::ctor]
498fn run() {
499    // Run a blocking web server in a new thread
500    thread::spawn(|| {
501        task::block_on(async {
502            let mut app = tide::new();
503            // The main site
504            app.at("/").get(main_site);
505            // Whether the page should be refreshed or not
506            app.at("/should_refresh").get(should_refresh);
507
508            // Setting the data
509            app.at("/set/f32").post(|r| handle_set_value(r, set_f32));
510            app.at("/set/f64").post(|r| handle_set_value(r, set_f64));
511            app.at("/set/bool").post(|r| handle_set_value(r, set_bool));
512            app.at("/set/string")
513                .post(|r| handle_set_value(r, set_string));
514            app.listen("127.0.0.1:9938").await
515        })
516        .expect("Running web server failed");
517    });
518}
519
520/// Build the actual site.
521async fn main_site(_: Request<()>) -> Response {
522    // Set LAST_MAP_SIZE to it's initial value
523    let mut last_map_size = LAST_MAP_SIZE.lock().unwrap();
524    *last_map_size = DATA.len();
525
526    let body = html! {
527        style { : include_str!("bulma.css") }
528        style { : "* { font-family: sans-serif}" }
529        body {
530            // Title
531            section (class="hero is-primary") {
532                div (class="hero-body") {
533                    div (class="container") {
534                        h1 (class="title is-1") { : "Const Tweaker Web Interface" }
535                    }
536                }
537            }
538            // All the widgets
539            : render_widgets();
540            // The error message
541            div (class="container") {
542                div (class="notification is-danger") {
543                    span(id="status") { }
544                }
545            }
546        }
547        script { : Raw(include_str!("send.js")) }
548    };
549
550    Response::new(200)
551        .body_string(format!("{}", body))
552        .set_header("content-type", "text/html;charset=utf-8")
553}
554
555/// Render all widgets.
556fn render_widgets() -> impl Render {
557    owned_html! {
558        // All modules go in their own panels
559        @for module in modules().into_iter() {
560            section (class="section") {
561                div (class="container box") {
562                    h3 (class="title is-3") { : format!("Module: \"{}\"", module) }
563                    : render_module(&module)
564                }
565
566                // The textbox to copy the output from
567                div (class="container box") {
568                    h4 (class="title is-4") { : "Changes" }
569                    div (class="columns") {
570                        div (class="column") {
571                            textarea (
572                                class="textarea",
573                                style="font-family: monospace",
574                                id=format!("{}_output", module.replace("::", "_")),
575                                readonly,
576                                placeholder="No changes")
577                        }
578                        div (class="column is-narrow control") {
579                            button (class="button is-link", onclick=format!("copy_text(\"{}\")", module)) {
580                                : "Copy"
581                            }
582                        }
583                    }
584                }
585            }
586        }
587    }
588}
589
590/// Render a module of widgets.
591fn render_module<'a>(module: &'a str) -> impl Render + 'a {
592    let mut data = DATA
593        .iter()
594        .filter(|kv| kv.value().module_path() == module)
595        .collect::<Vec<_>>();
596
597    data.sort_by(|a, b| {
598        a.value()
599            .line_number()
600            .partial_cmp(&b.value().line_number())
601            .unwrap_or(Ordering::Equal)
602    });
603
604    owned_html! {
605        // All widgets go into their own column box
606        @for ref_multi in data.iter() {
607            : render_widget(ref_multi.key(), ref_multi.value())
608        }
609    }
610}
611
612/// Render a single widget.
613fn render_widget<'a>(key: &'a str, field: &'a Field) -> impl Render + 'a {
614    owned_html! {
615        div (class="columns") {
616            div (class="column is-narrow") {
617                // module::CONSTANT
618                span (class="is-small") { : key }
619
620                br {}
621                // file:line
622                span (class="tag") { : field.file() }
623            }
624            : Raw(field.to_html_widget(key))
625        }
626    }
627}
628
629/// The javascript call to send the updated data.
630fn send(key: &str, look_for: &str, data_type: &str) -> String {
631    format!(
632        "send('{}', {}, '{}')",
633        key.replace("\\", "\\\\"),
634        look_for,
635        data_type
636    )
637}
638
639/// Whether the webpage should refresh itself or not.
640async fn should_refresh(_request: Request<()>) -> Response {
641    let mut last_map_size = LAST_MAP_SIZE.lock().unwrap();
642
643    if *last_map_size == DATA.len() {
644        // Don't need to do anything, just send an empty response
645        Response::new(200)
646    } else {
647        // There is a size mismatch of the map, reload the page
648        *last_map_size = DATA.len();
649
650        Response::new(200).body_string("refresh".to_string())
651    }
652}
653
654/// Handle setting of values.
655async fn handle_set_value<T, F>(mut request: Request<()>, set_value: F) -> Response
656where
657    T: DeserializeOwned,
658    F: Fn(&mut Field, T),
659{
660    let post_data: PostData<T> = request.body_json().await.expect("Could not decode JSON");
661    set_value(
662        &mut DATA
663            .get_mut(&*post_data.key)
664            .expect("Could not get item from map"),
665        post_data.value,
666    );
667
668    Response::new(200)
669}
670
671/// Set a f32 value when the field matches the proper variant.
672fn set_f32(field: &mut Field, new_value: f32) {
673    match field {
674        Field::F32 { ref mut value, .. } => {
675            *value = new_value;
676        }
677        _ => panic!("Unexpected type, please report an issue"),
678    }
679}
680
681/// Set a f64 value when the field matches the proper variant.
682fn set_f64(field: &mut Field, new_value: f64) {
683    match field {
684        Field::F64 { ref mut value, .. } => {
685            *value = new_value;
686        }
687        _ => panic!("Unexpected type, please report an issue"),
688    }
689}
690
691/// Set a bool value when the field matches the proper variant.
692fn set_bool(field: &mut Field, new_value: bool) {
693    match field {
694        Field::Bool { ref mut value, .. } => {
695            *value = new_value;
696        }
697        _ => panic!("Unexpected type, please report an issue"),
698    }
699}
700
701/// Set a string value when the field matches the proper variant.
702fn set_string(field: &mut Field, new_value: String) {
703    match field {
704        Field::String { ref mut value, .. } => {
705            *value = new_value;
706        }
707        _ => panic!("Unexpected type, please report an issue"),
708    }
709}
710
711/// Get a list of all modules.
712fn modules() -> Vec<String> {
713    let mut modules: Vec<_> = DATA
714        .iter()
715        .map(|kv| kv.value().module_path().to_string())
716        .collect::<_>();
717
718    // Remove duplicate entries
719    modules.sort();
720    modules.dedup();
721
722    modules
723}