///|
pub struct SceneThreadLink {
id : String
scene_id : String
subject_kind : String
subject_id : String
thread_id : String
subject_ref : String
status : String
unread_count : Int
open_message : String
} derive(Debug, Eq)
///|
pub fn scene_thread_link(
id~ : String,
scene_id~ : String,
subject_kind~ : String,
subject_id~ : String,
thread_id~ : String,
subject_ref? : String = "",
status? : String = "",
unread_count? : Int = 0,
open_message? : String = "",
) -> SceneThreadLink {
{
id,
scene_id,
subject_kind,
subject_id,
thread_id,
subject_ref,
status,
unread_count,
open_message,
}
}
///|
pub fn scene_marker_thread_link(
id~ : String,
scene_id~ : String,
marker_id~ : String,
thread_id~ : String,
subject_ref? : String = "",
status? : String = "",
unread_count? : Int = 0,
open_message? : String = "",
) -> SceneThreadLink {
scene_thread_link(
id~,
scene_id~,
subject_kind="marker",
subject_id=marker_id,
thread_id~,
subject_ref~,
status~,
unread_count~,
open_message~,
)
}
///|
pub fn scene_region_thread_link(
id~ : String,
scene_id~ : String,
region_id~ : String,
thread_id~ : String,
subject_ref? : String = "",
status? : String = "",
unread_count? : Int = 0,
open_message? : String = "",
) -> SceneThreadLink {
scene_thread_link(
id~,
scene_id~,
subject_kind="region",
subject_id=region_id,
thread_id~,
subject_ref~,
status~,
unread_count~,
open_message~,
)
}
///|
pub fn scene_thread_overlay(links : Array[SceneThreadLink]) -> @core.Node {
windowed_scene_thread_overlay(links, total_count=links.length())
}
///|
pub fn windowed_scene_thread_overlay(
links : Array[SceneThreadLink],
total_count~ : Int,
offset? : Int = 0,
) -> @core.Node {
let rows : Array[@core.Node] = []
for item in links {
rows.push(scene_thread_badge(item, None))
}
scene_thread_overlay_node(rows, total_count, offset, false, None)
}
///|
pub fn anchored_scene_thread_overlay(
input : Scene,
links : Array[SceneThreadLink],
) -> @core.Node {
windowed_anchored_scene_thread_overlay(
input,
links,
total_count=links.length(),
)
}
///|
pub fn windowed_anchored_scene_thread_overlay(
input : Scene,
links : Array[SceneThreadLink],
total_count~ : Int,
offset? : Int = 0,
) -> @core.Node {
let rows : Array[@core.Node] = []
for item in links {
rows.push(scene_thread_badge(item, scene_thread_anchor(input, item)))
}
scene_thread_overlay_node(rows, total_count, offset, true, None)
}
///|
pub fn anchored_scene_thread_overlay_with_viewport(
input : Scene,
viewport : SceneViewport,
links : Array[SceneThreadLink],
) -> @core.Node {
windowed_anchored_scene_thread_overlay_with_viewport(
input,
viewport,
links,
total_count=links.length(),
)
}
///|
pub fn windowed_anchored_scene_thread_overlay_with_viewport(
input : Scene,
viewport : SceneViewport,
links : Array[SceneThreadLink],
total_count~ : Int,
offset? : Int = 0,
) -> @core.Node {
let rows : Array[@core.Node] = []
for item in links {
let anchor = scene_thread_anchor(input, item)
if scene_thread_anchor_in_viewport(anchor, viewport) {
rows.push(
scene_thread_badge(
item,
scene_thread_anchor_to_viewport(anchor, viewport),
),
)
}
}
scene_thread_overlay_node(rows, total_count, offset, true, Some(viewport))
}
///|
pub fn static_scene_with_threads(
input : Scene,
links : Array[SceneThreadLink],
) -> @core.Node {
@core.view_with_attrs([@core.class_name("bunnia-scene-with-threads")], [
static_scene_view(input),
anchored_scene_thread_overlay(input, links),
])
}
///|
pub fn static_scene_with_windowed_threads(
input : Scene,
links : Array[SceneThreadLink],
total_count~ : Int,
offset? : Int = 0,
) -> @core.Node {
@core.view_with_attrs([@core.class_name("bunnia-scene-with-threads")], [
static_scene_view(input),
windowed_anchored_scene_thread_overlay(input, links, total_count~, offset~),
])
}
///|
pub fn select_scene_thread(
scene_id : String,
subject_kind : String,
subject_id : String,
thread_id : String,
) -> @core.Patch {
@core.set_json(
"scenes.\{scene_id}.selectedThread",
"{\"subjectKind\":\"\{escape_json(subject_kind)}\",\"subjectId\":\"\{escape_json(subject_id)}\",\"threadId\":\"\{escape_json(thread_id)}\"}",
)
}
///|
pub fn select_scene_marker_thread(
scene_id : String,
marker_id : String,
thread_id : String,
) -> @core.Patch {
select_scene_thread(scene_id, "marker", marker_id, thread_id)
}
///|
pub fn select_scene_region_thread(
scene_id : String,
region_id : String,
thread_id : String,
) -> @core.Patch {
select_scene_thread(scene_id, "region", region_id, thread_id)
}
///|
pub fn set_scene_thread_status(
scene_id : String,
thread_id : String,
status : String,
) -> @core.Patch {
@core.set_string("scenes.\{scene_id}.threads.\{thread_id}.status", status)
}
///|
pub fn set_scene_thread_unread(
scene_id : String,
thread_id : String,
unread_count : Int,
) -> @core.Patch {
@core.set_json(
"scenes.\{scene_id}.threads.\{thread_id}.unreadCount",
unread_count.to_string(),
)
}
///|
pub fn append_scene_thread_link(
target_path : String,
link : SceneThreadLink,
) -> @core.Patch {
@core.append_json(target_path, scene_thread_link_json(link))
}
///|
fn scene_thread_overlay_node(
rows : Array[@core.Node],
total_count : Int,
offset : Int,
anchored : Bool,
viewport : SceneViewport?,
) -> @core.Node {
let class_name = if anchored {
"bunnia-scene-thread-overlay bunnia-scene-thread-overlay-anchored"
} else {
"bunnia-scene-thread-overlay"
}
let attrs = [
@core.class_name(class_name),
@core.data("bunnia-list-mode", "windowed"),
@core.data("thread-overlay-anchored", anchored.to_string()),
@core.data("total-count", total_count.to_string()),
@core.data("visible-start", offset.to_string()),
@core.data("visible-count", rows.length().to_string()),
]
match viewport {
Some(value) => {
attrs.push(@core.data("thread-overlay-viewport", "true"))
attrs.push(@core.data("overlay-viewport-x", value.x.to_string()))
attrs.push(@core.data("overlay-viewport-y", value.y.to_string()))
attrs.push(@core.data("overlay-viewport-width", value.width.to_string()))
attrs.push(
@core.data("overlay-viewport-height", value.height.to_string()),
)
}
None => attrs.push(@core.data("thread-overlay-viewport", "false"))
}
@core.node(
kind="view",
attrs~,
children=rows,
list_total_count=total_count,
list_visible_count=rows.length(),
)
}
///|
fn scene_thread_badge(
input : SceneThreadLink,
anchor : (Int, Int)?,
) -> @core.Node {
let events = if input.open_message == "" {
[]
} else {
[@core.tap(input.open_message)]
}
let anchor_class = match anchor {
Some(_) => "bunnia-scene-thread-anchored"
None => "bunnia-scene-thread-missing-anchor"
}
let attrs = [
@core.class_name(
"bunnia-scene-thread \{anchor_class} \{scene_thread_status_class(input.status)}",
),
@core.data("scene-thread-id", input.id),
@core.data("scene-id", input.scene_id),
@core.data("subject-kind", input.subject_kind),
@core.data("subject-id", input.subject_id),
@core.data("thread-id", input.thread_id),
@core.data("subject-ref", input.subject_ref),
@core.data("thread-status", input.status),
@core.data("unread-count", input.unread_count.to_string()),
]
match anchor {
Some((x, y)) => {
attrs.push(@core.attr("style", scene_thread_anchor_style(x, y)))
attrs.push(@core.data("anchor-x", x.to_string()))
attrs.push(@core.data("anchor-y", y.to_string()))
attrs.push(@core.data("anchor-state", "anchored"))
}
None => {
attrs.push(@core.data("anchor-x", ""))
attrs.push(@core.data("anchor-y", ""))
attrs.push(@core.data("anchor-state", "missing"))
}
}
@core.keyed(
input.id,
@core.node(kind="view", attrs~, events~, children=[
@core.text(input.unread_count.to_string()),
]),
)
}
///|
fn scene_thread_anchor(input : Scene, link : SceneThreadLink) -> (Int, Int)? {
if link.scene_id != input.id {
None
} else {
match link.subject_kind {
"marker" => scene_thread_marker_anchor(input, link.subject_id)
"region" => scene_thread_region_anchor(input, link.subject_id)
_ => None
}
}
}
///|
fn scene_thread_anchor_in_viewport(
anchor : (Int, Int)?,
viewport : SceneViewport,
) -> Bool {
match anchor {
Some((x, y)) =>
x >= viewport.x &&
y >= viewport.y &&
x < viewport.x + viewport.width &&
y < viewport.y + viewport.height
None => false
}
}
///|
fn scene_thread_anchor_to_viewport(
anchor : (Int, Int)?,
viewport : SceneViewport,
) -> (Int, Int)? {
match anchor {
Some((x, y)) => Some((x - viewport.x, y - viewport.y))
None => None
}
}
///|
fn scene_thread_marker_anchor(input : Scene, marker_id : String) -> (Int, Int)? {
for scene_layer in input.layers {
for scene_marker in scene_layer.markers {
if scene_marker.id == marker_id {
return Some(
(
scene_marker.x + scene_marker.hit_width / 2,
scene_marker.y + scene_marker.hit_height / 2,
),
)
}
}
}
None
}
///|
fn scene_thread_region_anchor(input : Scene, region_id : String) -> (Int, Int)? {
for scene_layer in input.layers {
for scene_region in scene_layer.regions {
if scene_region.id == region_id {
return Some(
(
scene_region.x + scene_region.width / 2,
scene_region.y + scene_region.height / 2,
),
)
}
}
}
None
}
///|
fn scene_thread_anchor_style(x : Int, y : Int) -> String {
"left: \{x}rpx; top: \{y}rpx;"
}
///|
fn scene_thread_link_json(input : SceneThreadLink) -> String {
"{" +
"\"id\":\"\{escape_json(input.id)}\"," +
"\"sceneId\":\"\{escape_json(input.scene_id)}\"," +
"\"subjectKind\":\"\{escape_json(input.subject_kind)}\"," +
"\"subjectId\":\"\{escape_json(input.subject_id)}\"," +
"\"threadId\":\"\{escape_json(input.thread_id)}\"," +
"\"subjectRef\":\"\{escape_json(input.subject_ref)}\"," +
"\"status\":\"\{escape_json(input.status)}\"," +
"\"unreadCount\":\{input.unread_count}," +
"\"openMessage\":\"\{escape_json(input.open_message)}\"" +
"}"
}
///|
fn scene_thread_status_class(status : String) -> String {
if status == "" {
"bunnia-scene-thread-default"
} else {
"bunnia-scene-thread-\{status}"
}
}
///|
fn escape_json(value : String) -> String {
value
.replace(old="\\", new="\\\\")
.replace(old="\"", new="\\\"")
.replace(old="\n", new="\\n")
.to_string()
}