import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode import gleam/int import gleam/io import gleam/list import gleam/option.{type Option, Some} import gleam/result import lustre.{type App} import lustre/component import lustre/effect.{type Effect} import lustre/element.{type Element} import lustre/element/html import lustre/event import plinth/browser/document import plinth/browser/event as plinth_event import context pub const name: String = "viewer-controller" pub fn component() -> App(Nil, Model, Msg) { lustre.component(init, update, view, [ component.on_attribute_change("root-path", fn(value) { ParentChangedRootPath(value) |> Ok }), component.on_attribute_change("n-rows", fn(value) { int.parse(value) |> result.map(ParentChangedNRows) }), component.on_attribute_change("n-columns", fn(value) { int.parse(value) |> result.map(ParentChangedNColumns) }), component.on_attribute_change("root-path", fn(value) { ParentChangedRootPath(value) |> Ok }), ]) } pub type Model { Model( root_path: String, root_path_consumers: List(Dynamic), selected_row: Option(Int), selected_column: Option(Int), editing: Bool, n_rows: Int, n_columns: Int, ) } fn init(_) -> #(Model, Effect(Msg)) { #( Model( root_path: "", root_path_consumers: [], selected_row: option.None, selected_column: option.None, editing: False, n_rows: -1, n_columns: -1, ), effect.before_paint(fn(dispatch, _) -> Nil { document.add_event_listener("keydown", fn(ev) -> Nil { let key = plinth_event.key(ev) case key { "ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" -> dispatch(UserPressedDirectionalKey(key)) _ -> Nil } }) }), ) } pub type Msg { ParentChangedRootPath(String) ParentChangedNRows(Int) ParentChangedNColumns(Int) ChildRequestedRootPath(Dynamic, Bool) UserClickedCell(Int, Int) UserPressedDirectionalKey(String) } fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case msg { ParentChangedRootPath(root_path) -> #( Model(..model, root_path:), context.update_consumers( model.root_path_consumers, dynamic.string(root_path), ), ) ParentChangedNRows(n_rows) -> #(Model(..model, n_rows:), effect.none()) ParentChangedNColumns(n_columns) -> #( Model(..model, n_columns:), effect.none(), ) ChildRequestedRootPath(callback, subscribe) -> #( case subscribe { True -> Model(..model, root_path_consumers: [ callback, ..model.root_path_consumers ]) False -> model }, context.update_consumers([callback], dynamic.string(model.root_path)), ) UserClickedCell(row, column) -> #( Model( ..model, selected_row: option.Some(row), selected_column: option.Some(column), ), move_selection(to_row: option.Some(row), to_column: Some(column)), ) UserPressedDirectionalKey(key) -> { case model.editing, model.selected_row, model.selected_column { False, Some(selected_row), Some(selected_column) -> { let first_row_selected = selected_row == 0 let last_row_selected = selected_row == model.n_rows - 1 let first_col_selected = selected_column == 0 let last_col_selected = selected_column == model.n_columns - 1 case key, first_row_selected, last_row_selected, first_col_selected, last_col_selected { "ArrowLeft", _, _, False, _ -> #( Model(..model, selected_column: option.Some(selected_column - 1)), move_selection( to_row: model.selected_row, to_column: Some(selected_column - 1), ), ) "ArrowRight", _, _, _, False -> #( Model(..model, selected_column: option.Some(selected_column + 1)), move_selection( to_row: model.selected_row, to_column: Some(selected_column + 1), ), ) "ArrowUp", False, _, _, _ -> #( Model(..model, selected_row: option.Some(selected_row - 1)), move_selection( to_row: Some(selected_row - 1), to_column: model.selected_column, ), ) "ArrowDown", _, False, _, _ -> #( Model(..model, selected_row: option.Some(selected_row + 1)), move_selection( to_row: Some(selected_row + 1), to_column: model.selected_column, ), ) _, _, _, _, _ -> #(model, effect.none()) } } _, _, _ -> #(model, effect.none()) } } } } fn move_selection( to_row to_row: Option(Int), to_column to_column: Option(Int), ) -> Effect(msg) { use _dispatch, _root <- effect.before_paint() do_clear_selected_attrs() case to_row, to_column { option.Some(row), option.Some(column) -> do_set_selected_attr(row:, column:) _, _ -> Nil } } @external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs") fn do_clear_selected_attrs() -> Nil { Nil } @external(javascript, "./viewer_controller_component.ffi.mjs", "setSelectedAttr") fn do_set_selected_attr(row _row: Int, column _column: Int) -> Nil { Nil } fn view(_: Model) -> Element(Msg) { html.div( [ context.on_context_request( dynamic.string("root_path"), ChildRequestedRootPath, ), event.on("cell-click", { use msg <- decode.field("detail", { use row <- decode.field("row", decode.int) use column <- decode.field("column", decode.int) decode.success(UserClickedCell(row, column)) }) decode.success(msg) }), ], [component.default_slot([], [])], ) }