diff --git a/src/tardis.gleam b/src/tardis.gleam index 1a9d1a6..fe20a85 100644 --- a/src/tardis.gleam +++ b/src/tardis.gleam @@ -38,11 +38,12 @@ //// } //// ``` +import gleam/bool import gleam/dynamic.{type Dynamic} import gleam/int import gleam/io import gleam/list -import gleam/option.{Some} +import gleam/option.{None, Some} import gleam/pair import gleam/result import lustre.{type Action, type App} @@ -127,7 +128,6 @@ fn start_lustre(lustre_root, application) { /// It can be skipped when using [`single`](#single). pub fn setup() { let #(shadow_root, lustre_root) = setup.mount_shadow_node() - sketch.cache(strategy: sketch.Ephemeral) |> result.map(sl.compose(sl.shadow(shadow_root), view, _)) |> result.map(lustre.application(init, update, _)) @@ -138,8 +138,7 @@ pub fn setup() { /// Directly creates a tardis instance for a single application. /// Replaces `setup` and `application` in a single application context. pub fn single(name: String) { - setup() - |> result.map(application(_, name)) + result.map(setup(), application(_, name)) } /// Creates the application debugger from the tardis. Should be run once, @@ -152,12 +151,12 @@ pub fn application(instance: Tardis, name: String) -> Instance { } fn init(_) { - colors.choose_color_scheme() - |> Model( + let color_scheme = colors.choose_color_scheme() + Model( debuggers: [], frozen: False, opened: False, - color_scheme: _, + color_scheme:, selected_debugger: option.None, ) |> pair.new(effect.none()) @@ -165,162 +164,180 @@ fn init(_) { fn update(model: Model, msg: Msg) { case msg { - msg.ToggleOpen -> #(Model(..model, opened: !model.opened), effect.none()) - - msg.Restart(debugger_) -> { - let restart_effect = - model.debuggers - |> list.filter_map(fn(d_) { - let d = pair.second(d_) - let fst_step = list.first(d.steps) - use step <- result.try(fst_step) - d.dispatcher - |> option.map(fn(d) { d(step.model) }) - |> option.to_result(Nil) - }) - |> effect.batch() + msg.LustreAddedApplication(debugger_, dispatcher) -> { + model.debuggers + |> debugger_.replace(debugger_, debugger_.add_dispatcher(_, dispatcher)) + |> model.set_debuggers(model, _) + |> pair.new(effect.none()) + } + msg.LustreRanStep(debugger_, m, m_) -> { model.debuggers - |> debugger_.replace(debugger_, debugger_.unselect) - |> fn(ds) { Model(..model, frozen: False, debuggers: ds) } - |> pair.new(restart_effect) + |> debugger_.replace(debugger_, debugger_.add_step(_, m, m_)) + |> model.set_debuggers(model, _) + |> model.optional_set_selected_debugger(debugger_) + |> pair.new(effect.none()) + } + + msg.TardisPrintDebug(message:) -> { + io.debug(message) + #(model, effect.none()) } - msg.UpdateColorScheme(cs) -> + msg.UserChangedColorScheme(cs) -> { Model(..model, color_scheme: cs) |> pair.new(colors.save_color_scheme(cs)) + } - msg.AddApplication(debugger_, dispatcher) -> + msg.UserClickedStep(debugger_, item) -> { model.debuggers - |> debugger_.replace(debugger_, debugger_.add_dispatcher(_, dispatcher)) - |> fn(d) { Model(..model, debuggers: d) } - |> pair.new(effect.none()) + |> debugger_.replace(debugger_, debugger_.select(_, Some(item.index))) + |> model.set_debuggers(model, _) + |> model.freeze + |> pair.new({ + let debugger_ = debugger_.get(model.debuggers, debugger_) + use debugger_ <- result.try(debugger_) + debugger_.dispatcher + |> option.map(fn(dispatcher) { dispatcher(item.model) }) + |> option.to_result(Nil) + }) + |> pair.map_second(result.unwrap(_, effect.none())) + } - msg.BackToStep(debugger_, item) -> { - let selected_step = option.Some(item.index) - let model_effect = - model.debuggers - |> debugger_.get(debugger_) - |> result.try(fn(d) { - d.dispatcher - |> option.map(fn(d) { d(item.model) }) + msg.UserResumedApplication(debugger_) -> { + model.debuggers + |> debugger_.replace(debugger_, debugger_.unselect) + |> model.set_debuggers(model, _) + |> model.unfreeze + |> pair.new({ + effect.batch({ + use #(_name, debugger_) <- list.filter_map(model.debuggers) + use step <- result.try(list.first(debugger_.steps)) + debugger_.dispatcher + |> option.map(fn(dispatcher) { dispatcher(step.model) }) |> option.to_result(Nil) }) - |> result.unwrap(effect.none()) - - model.debuggers - |> debugger_.replace(debugger_, debugger_.select(_, selected_step)) - |> fn(d) { Model(..model, frozen: True, debuggers: d) } - |> pair.new(model_effect) - } - - msg.Debug(value) -> { - io.debug(value) - #(model, effect.none()) + }) } - msg.SelectDebugger(debugger_) -> - Model(..model, selected_debugger: option.Some(debugger_)) + msg.UserSelectedDebugger(debugger_) -> { + model.set_selected_debugger(model, debugger_) |> pair.new(effect.none()) + } - msg.AddStep(debugger_, m, m_) -> { - model.debuggers - |> debugger_.replace(debugger_, debugger_.add_step(_, m, m_)) - |> fn(d) { Model(..model, debuggers: d) } - |> model.optional_set_debugger(debugger_) + msg.UserToggledPanel -> { + Model(..model, opened: !model.opened) |> pair.new(effect.none()) } } } +fn view(model: Model) { + let #(panel, header) = select_panel_options(model.opened) + let debugger_ = model.get_selected_debugger(model) + panel_wrapper(model, [], [ + panel([], [ + header([], [ + s.title([], [ + h.div_([], [h.text("Debugger")]), + view_color_scheme_selector(model), + view_restart_button(model), + ]), + view_debugger_actions(model, debugger_), + ]), + debugger_ + |> result.map(view_debugger_body(model, _)) + |> result.lazy_unwrap(el.none), + ]), + ]) +} + fn select_panel_options(panel_opened: Bool) { case panel_opened { - True -> #(s.panel(), s.bordered_header(), "Close") - False -> #(s.panel_closed(), s.header(), "Open") + True -> #(s.panel, s.bordered_header) + False -> #(s.panel_closed, s.header) } } -fn on_cs_input(content) { - let cs = colors.cs_from_string(content) - msg.UpdateColorScheme(cs) +fn panel_wrapper(model: Model, attrs, children) { + let color_scheme_class = colors.get_color_scheme_class(model.color_scheme) + let frozen_panel = select_frozen_panel(model) + let classes = [color_scheme_class, frozen_panel] + let panel_class = sketch.class(list.map(classes, sketch.compose)) + h.div(panel_class, [a.class("debugger_"), ..attrs], children) } -fn on_debugger_input(content) { - msg.SelectDebugger(content) +fn select_frozen_panel(model: Model) { + case model.frozen { + True -> s.frozen_panel() + False -> sketch.class([]) + } } -fn color_scheme_selector(model: Model) { - case model.opened { - False -> el.none() - True -> - h.select(s.select_cs(), [event.on_input(on_cs_input)], { - use item <- list.map(colors.themes()) - let as_s = colors.cs_to_string(item) - let selected = model.color_scheme == item - h.option_([a.value(as_s), a.selected(selected)], [h.text(as_s)]) - }) - } +fn view_color_scheme_selector(model: Model) { + use <- bool.lazy_guard(when: !model.opened, return: el.none) + s.select_cs([event.on_input(on_cs_input)], { + use item <- list.map(colors.themes()) + let as_s = colors.cs_to_string(item) + let selected = model.color_scheme == item + h.option_([a.value(as_s), a.selected(selected)], [h.text(as_s)]) + }) +} + +fn on_cs_input(content) { + let cs = colors.cs_from_string(content) + msg.UserChangedColorScheme(cs) } -fn restart_button(model: Model) { - case model.frozen, model.selected_debugger { - True, Some(debugger_) -> - h.button(s.select_cs(), [event.on_click(msg.Restart(debugger_))], [ +fn view_restart_button(model: Model) { + use <- bool.lazy_guard(when: !model.frozen, return: el.none) + case model.selected_debugger { + None -> el.none() + Some(debugger_) -> + s.button([event.on_click(msg.UserResumedApplication(debugger_))], [ h.text("Restart"), ]) - _, _ -> el.none() } } -fn view(model: Model) { - let color_scheme_class = colors.get_color_scheme_class(model.color_scheme) - let #(panel, header, button_txt) = select_panel_options(model.opened) - let frozen_panel = case model.frozen { - True -> s.frozen_panel() - False -> sketch.class([]) +fn view_debugger_actions(model, debugger_) { + case debugger_ { + Error(_) -> el.none() + Ok(debugger_) -> { + s.actions_section([], [ + view_debugger_selection(model), + view_debugger_step_counter(debugger_), + view_debugger_toggle_panel_button(model), + ]) + } } - let debugger_ = - model.selected_debugger - |> option.unwrap("") - |> debugger_.get(model.debuggers, _) - let panel_class = - [color_scheme_class, frozen_panel] - |> list.map(sketch.compose) - |> sketch.class - let title_class = - [s.flex(), s.debugger_title()] - |> list.map(sketch.compose) - |> sketch.class - h.div(panel_class, [a.class("debugger_")], [ - h.div(panel, [], [ - h.div(header, [], [ - h.div(title_class, [], [ - h.div_([], [h.text("Debugger")]), - color_scheme_selector(model), - restart_button(model), - ]), - case debugger_ { - Error(_) -> el.none() - Ok(debugger_) -> - h.div(s.actions_section(), [], [ - h.select(s.select_cs(), [event.on_input(on_debugger_input)], { - use #(item, _) <- list.map(model.keep_active_debuggers(model)) - let selected = model.selected_debugger == Some(item) - h.option_([a.value(item), a.selected(selected)], [h.text(item)]) - }), - h.div_([], [ - h.text(int.to_string(debugger_.count - 1) <> " Steps"), - ]), - h.button(s.toggle_button(), [event.on_click(msg.ToggleOpen)], [ - h.text(button_txt), - ]), - ]) - }, - ]), - case debugger_, model.selected_debugger { - Ok(debugger_), Some(d) -> v.view_model(model.opened, d, debugger_) - _, _ -> el.none() - }, - ]), +} + +fn view_debugger_selection(model: Model) { + s.select_cs([event.on_input(msg.UserSelectedDebugger)], { + use #(item, _) <- list.map(model.keep_active_debuggers(model)) + let selected = model.selected_debugger == Some(item) + h.option_([a.value(item), a.selected(selected)], [h.text(item)]) + }) +} + +fn view_debugger_step_counter(debugger_: debugger_.Debugger) { + let count = int.to_string(debugger_.count - 1) + h.div_([], [h.text(count <> " Steps")]) +} + +fn view_debugger_toggle_panel_button(model: Model) { + s.toggle_button([event.on_click(msg.UserToggledPanel)], [ + h.text(case model.opened { + True -> "Close" + False -> "Open" + }), ]) } + +fn view_debugger_body(model: Model, debugger_: debugger_.Debugger) { + case model.selected_debugger { + Some(name) -> v.view_model(model.opened, name, debugger_) + None -> el.none() + } +} diff --git a/src/tardis/internals/data/model.gleam b/src/tardis/internals/data/model.gleam index 777eac5..fbd621b 100644 --- a/src/tardis/internals/data/model.gleam +++ b/src/tardis/internals/data/model.gleam @@ -2,11 +2,14 @@ import gleam/list import gleam/option.{type Option, Some} import gleam/pair import tardis/internals/data/colors -import tardis/internals/data/debugger.{type Debugger} +import tardis/internals/data/debugger.{type Debugger} as debugger_ + +pub type Debuggers = + List(#(String, Debugger)) pub type Model { Model( - debuggers: List(#(String, Debugger)), + debuggers: Debuggers, color_scheme: colors.ColorScheme, frozen: Bool, opened: Bool, @@ -14,7 +17,7 @@ pub type Model { ) } -pub fn optional_set_debugger(model: Model, debugger_: String) { +pub fn optional_set_selected_debugger(model: Model, debugger_: String) { let selected = option.or(model.selected_debugger, Some(debugger_)) Model(..model, selected_debugger: selected) } @@ -24,3 +27,25 @@ pub fn keep_active_debuggers(model: Model) { let steps = pair.second(debugger_).steps !list.is_empty(steps) } + +pub fn set_debuggers(model: Model, debuggers: Debuggers) { + Model(..model, debuggers:) +} + +pub fn freeze(model: Model) { + Model(..model, frozen: True) +} + +pub fn unfreeze(model: Model) { + Model(..model, frozen: False) +} + +pub fn get_selected_debugger(model: Model) { + model.selected_debugger + |> option.unwrap("") + |> debugger_.get(model.debuggers, _) +} + +pub fn set_selected_debugger(model: Model, debugger_) { + Model(..model, selected_debugger: Some(debugger_)) +} diff --git a/src/tardis/internals/data/msg.gleam b/src/tardis/internals/data/msg.gleam index 7c182e9..eb0ffc7 100644 --- a/src/tardis/internals/data/msg.gleam +++ b/src/tardis/internals/data/msg.gleam @@ -4,14 +4,12 @@ import tardis/internals/data/colors.{type ColorScheme} import tardis/internals/data/step.{type Step} pub type Msg { - // Panel - ToggleOpen - UpdateColorScheme(ColorScheme) - Debug(String) - SelectDebugger(String) - // Debugger - AddApplication(String, fn(Dynamic) -> Effect(Msg)) - AddStep(String, Dynamic, Dynamic) - BackToStep(String, Step) - Restart(String) + LustreAddedApplication(name: String, dispatcher: fn(Dynamic) -> Effect(Msg)) + LustreRanStep(name: String, model: Dynamic, msg: Dynamic) + TardisPrintDebug(message: String) + UserChangedColorScheme(color_scheme: ColorScheme) + UserClickedStep(name: String, step: Step) + UserResumedApplication(name: String) + UserSelectedDebugger(debugger: String) + UserToggledPanel } diff --git a/src/tardis/internals/setup.gleam b/src/tardis/internals/setup.gleam index b6f8d18..8b6c35c 100644 --- a/src/tardis/internals/setup.gleam +++ b/src/tardis/internals/setup.gleam @@ -92,7 +92,7 @@ pub fn create_model_updater( let msg = runtime.Debug(runtime.ForceModel(model)) coerce(dispatcher)(msg) } - |> msg.AddApplication(application, _) + |> msg.LustreAddedApplication(application, _) |> lustre.dispatch |> dispatch } @@ -103,7 +103,7 @@ pub fn step_adder( name: String, ) { fn(model, msg) { - msg.AddStep(name, model, msg) + msg.LustreRanStep(name, model, msg) |> lustre.dispatch() |> dispatch() } diff --git a/src/tardis/internals/styles.gleam b/src/tardis/internals/styles.gleam index 569c951..7c706fa 100644 --- a/src/tardis/internals/styles.gleam +++ b/src/tardis/internals/styles.gleam @@ -1,4 +1,5 @@ import sketch +import sketch/lustre/element/html as h import sketch/size.{percent, px} fn panel_() { @@ -21,17 +22,19 @@ fn panel_() { ]) } -pub fn panel() { +pub fn panel(attrs, children) { panel_() + |> h.div(attrs, children) } -pub fn panel_closed() { +pub fn panel_closed(attrs, children) { sketch.class([ sketch.compose(panel_()), sketch.width(px(400)), // sketch.min_height(px(60)), sketch.justify_content("center"), ]) + |> h.div(attrs, children) } fn grid_header_() { @@ -66,12 +69,14 @@ fn header_() { ]) } -pub fn header() { +pub fn header(attrs, children) { header_() + |> h.div(attrs, children) } -pub fn bordered_header() { +pub fn bordered_header(attrs, children) { sketch.class([sketch.compose(header_())]) + |> h.div(attrs, children) } pub fn body() { @@ -132,16 +137,17 @@ pub fn step_model() { ]) } -pub fn actions_section() { +pub fn actions_section(attrs, children) { sketch.class([ sketch.display("flex"), sketch.gap(px(12)), sketch.align_items("center"), sketch.white_space("nowrap"), ]) + |> h.div(attrs, children) } -pub fn toggle_button() { +pub fn toggle_button(attrs, children) { sketch.class([ sketch.appearance("none"), sketch.border("none"), @@ -150,6 +156,7 @@ pub fn toggle_button() { sketch.property("cursor", "pointer"), sketch.color("var(--button)"), ]) + |> h.button(attrs, children) } pub fn keyword_color() { @@ -162,12 +169,13 @@ pub fn flex() { |> sketch.class() } -pub fn debugger_title() { +pub fn title(attrs, children) { sketch.class([ - sketch.display("flex"), + sketch.compose(flex()), sketch.align_items("center"), sketch.gap(px(18)), ]) + |> h.div(attrs, children) } pub fn text_color(color: String) { @@ -183,7 +191,7 @@ pub fn subgrid_header() { ]) } -pub fn select_cs() { +pub fn button_base() { sketch.class([ sketch.appearance("none"), sketch.background("transparent"), @@ -200,6 +208,16 @@ pub fn select_cs() { ]) } +pub fn button(attrs, children) { + button_base() + |> h.button(attrs, children) +} + +pub fn select_cs(attrs, children) { + button_base() + |> h.select(attrs, children) +} + pub fn frozen_panel() { sketch.class([ sketch.position("fixed"), diff --git a/src/tardis/internals/view.gleam b/src/tardis/internals/view.gleam index b9aea33..d4db3b8 100644 --- a/src/tardis/internals/view.gleam +++ b/src/tardis/internals/view.gleam @@ -1,3 +1,4 @@ +import gleam/bool import gleam/list import gleam/option.{type Option} import gleam/pair @@ -13,16 +14,13 @@ import tardis/internals/styles as s pub fn view_model(opened: Bool, debugger_: String, model: Debugger) { let selected = model.selected_step - case opened { - False -> element.none() - True -> - element.keyed(h.div(s.body(), [], _), { - model.steps - |> list.take(100) - |> list.map(fn(i) { #(i.index, view_step(debugger_, selected, i)) }) - |> list.prepend(#("header", view_grid_header(opened, model))) - }) - } + use <- bool.lazy_guard(when: !opened, return: element.none) + element.keyed(h.div(s.body(), [], _), { + model.steps + |> list.take(100) + |> list.map(fn(i) { #(i.index, view_step(debugger_, selected, i)) }) + |> list.prepend(#("header", view_grid_header(opened, model))) + }) } fn view_step(debugger_: String, selected_step: Option(String), item: Step) { @@ -31,7 +29,7 @@ fn view_step(debugger_: String, selected_step: Option(String), item: Step) { True -> s.selected_details() False -> s.details() } - h.div(class, [event.on_click(msg.BackToStep(debugger_, item))], [ + h.div(class, [event.on_click(msg.UserClickedStep(debugger_, item))], [ h.div(s.step_index(), [], [h.text(index)]), h.div(s.step_msg(), [], view_data(data.inspect(msg), 0, "")), h.div(s.step_model(), [], view_data(data.inspect(model), 0, "")),