about summary refs log tree commit diff
path: root/corp
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2022-10-13T16·20+0300
committertazjin <tazjin@tvl.su>2022-10-16T12·26+0000
commita8f7383fcb151f76c9cda1235abf0577b1ce6720 (patch)
treeb3a11ef832314510df8cabee54a97d511a572fee /corp
parente6d9be32a2dc6c7752f658f5c8fe4e33024bc2d3 (diff)
refactor(tvixbolt): use details elements for toggling outputs r/5145
For optional outputs (runtime trace & AST) this has a slightly nicer
user experience.

Note that the code of this is a bit verbose because doing a naive
implementation hits dumb behaviours of browsers that result in
infinite loops.

Thanks Profpatsch for the suggestion.

Change-Id: I8945a8e722f0ad8735829807fb5e39e2101f378c
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7006
Reviewed-by: j4m3s <james.landrein@gmail.com>
Autosubmit: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
Diffstat (limited to 'corp')
-rw-r--r--corp/tvixbolt/Cargo.toml5
-rw-r--r--corp/tvixbolt/src/main.rs101
2 files changed, 70 insertions, 36 deletions
diff --git a/corp/tvixbolt/Cargo.toml b/corp/tvixbolt/Cargo.toml
index c448f9fe42c5..7182e88d83ae 100644
--- a/corp/tvixbolt/Cargo.toml
+++ b/corp/tvixbolt/Cargo.toml
@@ -10,7 +10,6 @@ yew = "0.19.3"
 yew-router = "0.16"
 codemap = "0.1.3"
 serde_urlencoded = "*" # pinned by yew
-web-sys = "*" # pinned by yew
 
 # needs to be in sync with nixpkgs
 wasm-bindgen = "= 0.2.83"
@@ -26,3 +25,7 @@ default-features = false
 [dependencies.serde]
 version = "*" # pinned by yew
 features = [ "derive" ]
+
+[dependencies.web-sys]
+version = "*" # pinned by yew
+features = [ "HtmlDetailsElement" ]
diff --git a/corp/tvixbolt/src/main.rs b/corp/tvixbolt/src/main.rs
index 6cef4851a59f..eaeffe38cb33 100644
--- a/corp/tvixbolt/src/main.rs
+++ b/corp/tvixbolt/src/main.rs
@@ -6,22 +6,31 @@ use serde::{Deserialize, Serialize};
 use tvix_eval::observer::TracingObserver;
 use tvix_eval::observer::{DisassemblingObserver, NoOpObserver};
 use tvix_eval::SourceCode;
-use web_sys::HtmlInputElement;
+use web_sys::HtmlDetailsElement;
 use web_sys::HtmlTextAreaElement;
 use yew::prelude::*;
 use yew::TargetCast;
 use yew_router::{prelude::*, AnyRoute};
 
+#[derive(Clone)]
 enum Msg {
     CodeChange(String),
     ToggleTrace(bool),
     ToggleDisplayAst(bool),
+
+    // Required because browsers are stupid and it's easy to get into
+    // infinite loops with `ontoggle` events.
+    NoOp,
 }
 
 #[derive(Clone, Serialize, Deserialize)]
 struct Model {
     code: String,
+
+    // #[serde(skip_serializing)]
     trace: bool,
+
+    // #[serde(skip_serializing)]
     display_ast: bool,
 }
 
@@ -118,6 +127,8 @@ impl Component for Model {
             Msg::CodeChange(new_code) => {
                 self.code = new_code;
             }
+
+            Msg::NoOp => {}
         }
 
         let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone());
@@ -148,32 +159,10 @@ impl Component for Model {
                          id="code" cols="30" rows="10" value={self.code.clone()}>
                          </textarea>
                     </div>
-
-                    <div class="form-group">
-                      <label for="trace-runtime">{"Trace runtime:"}</label>
-                      <input
-                       id="trace-runtime" type="checkbox" checked={self.trace}
-                       onchange={link.callback(|e: Event| {
-                           let trace = e.target_unchecked_into::<HtmlInputElement>().checked();
-                           Msg::ToggleTrace(trace)
-                       })}
-                       />
-                    </div>
-
-                    <div class="form-group">
-                      <label for="display-ast">{"Display parsed AST:"}</label>
-                      <input
-                       id="display-ast" type="checkbox" checked={self.display_ast}
-                       onchange={link.callback(|e: Event| {
-                           let trace = e.target_unchecked_into::<HtmlInputElement>().checked();
-                           Msg::ToggleDisplayAst(trace)
-                       })}
-                       />
-                    </div>
                   </fieldset>
                 </form>
                 <hr />
-                {self.run()}
+                {self.run(ctx)}
                 {footer()}
             </div>
             </>
@@ -182,7 +171,7 @@ impl Component for Model {
 }
 
 impl Model {
-    fn run(&self) -> Html {
+    fn run(&self, ctx: &Context<Self>) -> Html {
         if self.code.is_empty() {
             return html! {
                 <p>
@@ -197,7 +186,7 @@ impl Model {
         html! {
             <>
               <h2>{"Result:"}</h2>
-            {eval(self.trace, self.display_ast, &self.code).display()}
+            {eval(self).display(ctx, self)}
             </>
         }
     }
@@ -228,8 +217,50 @@ fn maybe_show(title: &str, s: &str) -> Html {
     }
 }
 
+fn maybe_details(
+    ctx: &Context<Model>,
+    title: &str,
+    s: &str,
+    display: bool,
+    toggle: fn(bool) -> Msg,
+) -> Html {
+    let link = ctx.link();
+    if display {
+        let msg = toggle(false);
+        html! {
+            <details open=true
+                     ontoggle={link.callback(move |e: Event| {
+                         let details = e.target_unchecked_into::<HtmlDetailsElement>();
+                         if !details.open() {
+                             msg.clone()
+                         } else {
+                             Msg::NoOp
+                         }
+                     })}>
+
+              <summary><h3 style="display: inline;">{title}</h3></summary>
+              <pre>{s}</pre>
+            </details>
+        }
+    } else {
+        let msg = toggle(true);
+        html! {
+            <details ontoggle={link.callback(move |e: Event| {
+                         let details = e.target_unchecked_into::<HtmlDetailsElement>();
+                         if details.open() {
+                             msg.clone()
+                         } else {
+                             Msg::NoOp
+                         }
+                     })}>
+              <summary><h3 style="display: inline;">{title}</h3></summary>
+            </details>
+        }
+    }
+}
+
 impl Output {
-    fn display(self) -> Html {
+    fn display(self, ctx: &Context<Model>, model: &Model) -> Html {
         html! {
             <>
             {maybe_show("Parse errors:", &self.parse_errors)}
@@ -238,21 +269,21 @@ impl Output {
             {maybe_show("Compiler errors:", &self.compiler_errors)}
             {maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))}
             {maybe_show("Runtime errors:", &self.runtime_errors)}
-            {maybe_show("Runtime trace:", &String::from_utf8_lossy(&self.trace))}
-            {maybe_show("Parsed AST:", &self.ast)}
+            {maybe_details(ctx, "Runtime trace:", &String::from_utf8_lossy(&self.trace), model.trace, Msg::ToggleTrace)}
+            {maybe_details(ctx, "Parsed AST:", &self.ast, model.display_ast, Msg::ToggleDisplayAst)}
             </>
         }
     }
 }
 
-fn eval(trace: bool, display_ast: bool, code: &str) -> Output {
+fn eval(model: &Model) -> Output {
     let mut out = Output::default();
 
-    if code.is_empty() {
+    if model.code.is_empty() {
         return out;
     }
 
-    let parsed = rnix::ast::Root::parse(code);
+    let parsed = rnix::ast::Root::parse(&model.code);
     let errors = parsed.errors();
 
     if !errors.is_empty() {
@@ -269,12 +300,12 @@ fn eval(trace: bool, display_ast: bool, code: &str) -> Output {
         .expr()
         .expect("expression should exist if no errors occured");
 
-    if display_ast {
+    if model.display_ast {
         out.ast = tvix_eval::pretty_print_expr(&root_expr);
     }
 
     let source = SourceCode::new();
-    let file = source.add_file("nixbolt".to_string(), code.into());
+    let file = source.add_file("nixbolt".to_string(), model.code.clone());
 
     let mut compilation_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode);
 
@@ -309,7 +340,7 @@ fn eval(trace: bool, display_ast: bool, code: &str) -> Output {
         return out;
     }
 
-    let result = if trace {
+    let result = if model.trace {
         tvix_eval::run_lambda(
             Default::default(),
             &mut TracingObserver::new(&mut out.trace),