///|
/// UI contracts.
///
/// Reference:
/// - mizchi/layout
///|
pub struct UINodeId {
value : Int
} derive(Debug)
///|
pub impl Show for UINodeId with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub struct LayoutConstraint {
min_width : Double
min_height : Double
max_width : Double
max_height : Double
} derive(Debug)
///|
pub impl Show for LayoutConstraint with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub struct LayoutRect {
x : Double
y : Double
width : Double
height : Double
} derive(Debug)
///|
pub impl Show for LayoutRect with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub struct LayoutResult {
node_id : UINodeId
rect : LayoutRect
} derive(Debug)
///|
pub impl Show for LayoutResult with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub enum UIEvent {
PointerDown(Double, Double)
PointerUp(Double, Double)
PointerMove(Double, Double)
Scroll(Double, Double)
KeyDown(Int)
KeyUp(Int)
} derive(Debug)
///|
pub impl Show for UIEvent with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub trait LayoutEngine {
fn compute_layout(Self, root : UINodeId, constraint : LayoutConstraint) -> Array[
LayoutResult,
] raise
}
///|
pub trait UIInputAdapter {
fn from_input_snapshot(Self, snapshot : @core.InputSnapshot) -> Array[UIEvent]
}
///|
pub trait UIRenderAdapter {
fn build_draw_commands(
Self,
frame : @core.FrameBudget,
layouts : Array[LayoutResult],
target : @gfx.ImageHandle,
) -> Array[@gfx.DrawTrianglesCommand] raise
}
///|
pub fn default_layout_constraint(
width : Double,
height : Double,
) -> LayoutConstraint {
{ min_width: 0.0, min_height: 0.0, max_width: width, max_height: height, }
}
///|
pub fn point_in_rect(rect : LayoutRect, x : Double, y : Double) -> Bool {
x >= rect.x &&
x < rect.x + rect.width &&
y >= rect.y &&
y < rect.y + rect.height
}
///|
/// Find the topmost node that contains the given point.
///
/// `LayoutResult` carries no z, so the array order is the only ordering there
/// is, and `compute_layout` emits **post-order**: every child before the
/// container that holds it, the root last. The innermost node containing the
/// point is therefore the *first* match, and the last match is always the
/// outermost container -- for a full-screen root, every on-screen point.
///
/// This used to take the last match, which meant that feeding
/// `compute_layout`'s own output straight into `hit_test` returned the root
/// background for every point on screen. The regression test at the bottom of
/// `contracts_wbtest.mbt` pins the two halves together.
pub fn hit_test(
layouts : Array[LayoutResult],
x : Double,
y : Double,
) -> UINodeId? {
for layout in layouts {
if point_in_rect(layout.rect, x, y) {
return Some(layout.node_id)
}
}
None
}
///|
/// Every node containing the point, innermost first -- the same order
/// `hit_test` picks its answer from. A caller looking for "the deepest node
/// that is focusable" takes the first match it likes.
pub fn hit_test_all(
layouts : Array[LayoutResult],
x : Double,
y : Double,
) -> Array[UINodeId] {
let result : Array[UINodeId] = []
for layout in layouts {
if point_in_rect(layout.rect, x, y) {
result.push(layout.node_id)
}
}
result
}
///|
pub(all) enum UIFocusEvent {
FocusGained(UINodeId)
FocusLost(UINodeId)
} derive(Debug)
///|
pub impl Show for UIFocusEvent with fn output(self, logger) {
logger.write_object(Repr(self))
}
///|
pub struct UIFocusManager {
mut focused : UINodeId?
mut focusable_ids : Array[UINodeId]
}
///|
pub fn UIFocusManager::new() -> UIFocusManager {
{ focused: None, focusable_ids: [], }
}
///|
pub fn UIFocusManager::set_focusable(
self : UIFocusManager,
ids : Array[UINodeId],
) -> Unit {
self.focusable_ids = ids
}
///|
pub fn UIFocusManager::current_focus(self : UIFocusManager) -> UINodeId? {
self.focused
}
///|
/// Focus a specific node. Returns focus/blur events.
pub fn UIFocusManager::focus(
self : UIFocusManager,
id : UINodeId,
) -> Array[UIFocusEvent] {
let events : Array[UIFocusEvent] = []
match self.focused {
Some(prev) =>
if prev.value != id.value {
events.push(UIFocusEvent::FocusLost(prev))
events.push(UIFocusEvent::FocusGained(id))
self.focused = Some(id)
}
None => {
events.push(UIFocusEvent::FocusGained(id))
self.focused = Some(id)
}
}
events
}
///|
/// Remove focus. Returns blur event if something was focused.
pub fn UIFocusManager::blur(self : UIFocusManager) -> Array[UIFocusEvent] {
let events : Array[UIFocusEvent] = []
match self.focused {
Some(prev) => {
events.push(UIFocusEvent::FocusLost(prev))
self.focused = None
}
None => ()
}
events
}
///|
fn UIFocusManager::find_focus_index(self : UIFocusManager) -> Int {
match self.focused {
None => -1
Some(id) => {
for i, fid in self.focusable_ids {
if fid.value == id.value {
return i
}
}
-1
}
}
}
///|
/// Move focus to the next focusable node (tab order).
pub fn UIFocusManager::focus_next(self : UIFocusManager) -> Array[UIFocusEvent] {
let n = self.focusable_ids.length()
if n == 0 {
return []
}
let current = self.find_focus_index()
let next_idx = (current + 1) % n
self.focus(self.focusable_ids[next_idx])
}
///|
/// Move focus to the previous focusable node (shift-tab order).
pub fn UIFocusManager::focus_prev(self : UIFocusManager) -> Array[UIFocusEvent] {
let n = self.focusable_ids.length()
if n == 0 {
return []
}
let current = self.find_focus_index()
let prev_idx = if current <= 0 { n - 1 } else { current - 1 }
self.focus(self.focusable_ids[prev_idx])
}
///|
/// Handle a pointer down event: focus the hit node, or blur if no hit.
pub fn UIFocusManager::handle_pointer_down(
self : UIFocusManager,
layouts : Array[LayoutResult],
x : Double,
y : Double,
) -> Array[UIFocusEvent] {
match hit_test(layouts, x, y) {
Some(id) => {
for fid in self.focusable_ids {
if fid.value == id.value {
return self.focus(id)
}
}
self.blur()
}
None => self.blur()
}
}
///|
/// Convert inpututil edge state to UIEvents for the current frame.
/// Uses just-pressed/just-released from the edge detection to generate
/// PointerDown/Up and KeyDown/Up events.
pub fn ui_events_from_input_edge(
key_state : @inputstate.KeyInputState,
mouse_state : @inputstate.MouseButtonInputState,
cursor_x : Double,
cursor_y : Double,
) -> Array[UIEvent] {
let events : Array[UIEvent] = []
let just_pressed_keys = @inputstate.append_just_pressed_keys(key_state, [])
for key in just_pressed_keys {
events.push(UIEvent::KeyDown(key))
}
let just_released_keys = @inputstate.append_just_released_keys(key_state, [])
for key in just_released_keys {
events.push(UIEvent::KeyUp(key))
}
let just_pressed_buttons = @inputstate.append_just_pressed_mouse_buttons(
mouse_state,
[],
)
for _ in just_pressed_buttons {
events.push(UIEvent::PointerDown(cursor_x, cursor_y))
}
let just_released_buttons = @inputstate.append_just_released_mouse_buttons(
mouse_state,
[],
)
for _ in just_released_buttons {
events.push(UIEvent::PointerUp(cursor_x, cursor_y))
}
events
}
///|
pub extend UINodeId with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend UINodeId with Show::{to_string, output}
///|
pub extend LayoutConstraint with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend LayoutConstraint with Show::{to_string, output}
///|
pub extend LayoutRect with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend LayoutRect with Show::{to_string, output}
///|
pub extend LayoutResult with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend LayoutResult with Show::{to_string, output}
///|
pub extend UIEvent with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend UIEvent with Show::{to_string, output}
///|
pub extend UIFocusEvent with @moonbitlang/core/debug.Debug::{to_repr}
///|
pub extend UIFocusEvent with Show::{to_string, output}