2025-07-22 00:21:54 -07:00
|
|
|
import gleam/dynamic.{type Dynamic}
|
2025-07-25 15:01:31 -07:00
|
|
|
import gleam/dynamic/decode
|
|
|
|
|
import gleam/int
|
|
|
|
|
import gleam/io
|
|
|
|
|
import gleam/list
|
|
|
|
|
import gleam/option.{type Option, Some}
|
|
|
|
|
import gleam/result
|
2025-07-22 00:21:54 -07:00
|
|
|
import lustre.{type App}
|
|
|
|
|
import lustre/component
|
|
|
|
|
import lustre/effect.{type Effect}
|
|
|
|
|
import lustre/element.{type Element}
|
|
|
|
|
import lustre/element/html
|
2025-07-25 15:01:31 -07:00
|
|
|
import lustre/event
|
|
|
|
|
import plinth/browser/document
|
|
|
|
|
import plinth/browser/event as plinth_event
|
2025-07-22 00:21:54 -07:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}),
|
2025-07-25 15:01:31 -07:00
|
|
|
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
|
|
|
|
|
}),
|
2025-07-22 00:21:54 -07:00
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type Model {
|
2025-07-25 15:01:31 -07:00
|
|
|
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,
|
|
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn init(_) -> #(Model, Effect(Msg)) {
|
2025-07-25 15:01:31 -07:00
|
|
|
#(
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub type Msg {
|
|
|
|
|
ParentChangedRootPath(String)
|
2025-07-25 15:01:31 -07:00
|
|
|
ParentChangedNRows(Int)
|
|
|
|
|
ParentChangedNColumns(Int)
|
2025-07-22 00:21:54 -07:00
|
|
|
ChildRequestedRootPath(Dynamic, Bool)
|
2025-07-25 15:01:31 -07:00
|
|
|
UserClickedCell(Int, Int)
|
|
|
|
|
UserPressedDirectionalKey(String)
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
)
|
2025-07-25 15:01:31 -07:00
|
|
|
ParentChangedNRows(n_rows) -> #(Model(..model, n_rows:), effect.none())
|
|
|
|
|
ParentChangedNColumns(n_columns) -> #(
|
|
|
|
|
Model(..model, n_columns:),
|
|
|
|
|
effect.none(),
|
|
|
|
|
)
|
2025-07-22 00:21:54 -07:00
|
|
|
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)),
|
|
|
|
|
)
|
2025-07-25 15:01:31 -07:00
|
|
|
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
|
2025-07-22 00:21:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 15:01:31 -07:00
|
|
|
@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
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 00:21:54 -07:00
|
|
|
fn view(_: Model) -> Element(Msg) {
|
|
|
|
|
html.div(
|
|
|
|
|
[
|
|
|
|
|
context.on_context_request(
|
|
|
|
|
dynamic.string("root_path"),
|
|
|
|
|
ChildRequestedRootPath,
|
|
|
|
|
),
|
2025-07-25 15:01:31 -07:00
|
|
|
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)
|
|
|
|
|
}),
|
2025-07-22 00:21:54 -07:00
|
|
|
],
|
|
|
|
|
[component.default_slot([], [])],
|
|
|
|
|
)
|
|
|
|
|
}
|