From f67ac0c52205c1e8e4f9d094e2b45781386df64e Mon Sep 17 00:00:00 2001 From: EmileTrotignon Date: Sat, 9 May 2026 10:23:25 +0200 Subject: [PATCH 1/2] allow creating custom widgets --- bin/hotreload.available.ml | 2 +- lib/gamelle.mli | 188 +++++++++++++++++++++++++++++++------ lib/ui/layout.ml | 13 +++ lib/ui/layout.mli | 1 + lib/ui/text_input.ml | 19 ++-- lib/ui/ui.ml | 25 ++++- lib/ui/ui_backend.ml | 33 ++++++- lib/ui/vscroll.ml | 10 +- lib/ui/widgets.ml | 37 ++++---- 9 files changed, 266 insertions(+), 62 deletions(-) diff --git a/bin/hotreload.available.ml b/bin/hotreload.available.ml index 7600337..bb0ba7f 100644 --- a/bin/hotreload.available.ml +++ b/bin/hotreload.available.ml @@ -14,7 +14,7 @@ let ui_replay ~io (nb_past, target_clock, nb_future) = if total_events <= 0 then 0 else over [%ui] @@ fun () -> - draw [%ui] (fun ~io box -> + Ui.Custom.draw [%ui] (fun ~io box -> let module Box = Gamelle.Box in let size = Box.size box in let progress = diff --git a/lib/gamelle.mli b/lib/gamelle.mli index 1d51c41..6b4f943 100644 --- a/lib/gamelle.mli +++ b/lib/gamelle.mli @@ -867,39 +867,167 @@ capping its [max]. Note: reporting a [min] smaller than the child actually needs will cause it to be rendered in a box that is too small, resulting in clipping or overflow. *) - val draw : - ui -> - ?min_width:float -> - ?max_width:float -> - ?min_height:float -> - ?max_height:float -> - ?flex_width:float -> - ?flex_height:float -> - (io:io -> Box.t -> unit) -> - unit - (** {ocaml[ -draw [%ui] ?min_width ?max_width ?min_height ?max_height - ?flex_width ?flex_height begin fun ~io box -> - ... - end -]} - -The primitive leaf widget. Declares a box with the given layout constraints and -calls the drawing function with the allocated [~io] and {!Box.t}. + (** {1 Building custom widgets} -The code [...] should draw the desired inside the [box] its given by the layout. + Use [Ui.Custom] to build new interactive widgets. The primitives here are + the same ones used to implement {!button}, {!checkbox}, {!slider}, etc. -All size parameters default to [0.0] except [max_width] and [max_height] which -default to [infinity]. A [flex] value of [0.0] means the widget takes exactly -its [min] size; a positive value means it competes for extra space -proportionally to that value. - -No clipping is applied: drawing outside [box] is not prevented. If you want -clipping you can use {!View.clip}. - -You can combine this with {!over} to create "reskins" of other widgets. + A typical non-container widget follows this pattern: + {[ + open Ui.Custom + + module Store = State(struct type t = float end) + + let my_toggle ui (checked : bool) : bool = + let anim = Store.find [%ui] 0.0 in + on_click ui @@ fun st -> + update anim + (if st = `pressed then min 1.0 (value anim +. 0.05) else value anim); + let checked = if st = `clicked then not checked else checked in + draw ui ~min_width:40. ~min_height:20. (fun ~io box -> + (* render using checked and (value anim) *) + ()); + checked + ]} + *) -*) + module Custom : sig + type 'a state + (** Persistent per-instance internal state. Use {!get} and {!set} to + access it. The concrete representation is hidden. *) + + val value : 'a state -> 'a + val update : 'a state -> 'a -> unit + + val draw : + ui -> + ?min_width:float -> + ?max_width:float -> + ?min_height:float -> + ?max_height:float -> + ?flex_width:float -> + ?flex_height:float -> + (io:io -> Box.t -> unit) -> + unit + (** The primitive leaf widget. Declares a box with the given layout + constraints and calls the drawing function with the allocated [~io] + and {!Box.t}. + + All size parameters default to [0.0] except [max_width] and + [max_height] which default to [infinity]. A [flex] value of [0.0] + means the widget takes exactly its [min] size; a positive value means + it competes for extra space proportionally to that value. *) + + val get_io : ui -> io + (** Get the current [io]. Needed to read input events such as mouse + position inside [on_click] callbacks. *) + + module Layout : sig + type t + val horizontal : ?gap:float -> t list -> t + val vertical : ?gap:float -> t list -> t + val over : t list -> t + val pad : float -> t -> t + val center : t list -> t + val vclip : float -> t -> t + val hclip : float -> t -> t + val reshape : ?width:(constrain -> constrain) -> ?height:(constrain -> constrain) -> t -> t + end + (** Layout primitives for building container widgets. Pass a + [Layout.t list -> Layout.t] function to {!parent} to define how + children are arranged. *) + + val parent : + ui -> + (Layout.t list -> Layout.t) -> + (unit -> 'a) -> + 'a + (** Container primitive. [parent ui layout fn] runs [fn] to collect child + layout nodes, passes them to [layout], and inserts the result. All + built-in layout combinators ([horizontal], [vertical], etc.) are + wrappers around this. *) + + val parent1 : ui -> (Layout.t -> Layout.t) -> (unit -> 'a) -> 'a + (** Like {!parent} but asserts exactly one child. *) + + val horizontal : ui -> ?gap:float -> (unit -> 'a) -> 'a + val vertical : ui -> ?gap:float -> (unit -> 'a) -> 'a + val over : ui -> (unit -> 'a) -> 'a + + val vclip : ui -> ?offset:Vec.t -> Box.t -> (unit -> 'a) -> 'a + (** Clips content vertically to [box], scrolled by [offset]. Events outside + the clip box are suppressed. *) + + val hclip : ui -> ?offset:Vec.t -> Box.t -> (unit -> 'a) -> 'a + (** Clips content horizontally to [box], scrolled by [offset]. Events + outside the clip box are suppressed. *) + + val on_click : ui -> ([ `normal | `hover | `pressed | `clicked ] -> 'a) -> 'a + (** Track hover/press/click state for whatever is rendered inside the + callback. Internal button state is managed automatically. *) + + val with_box : ui -> (Box.t -> 'a) -> 'a + (** Provides the layout box allocated to the content. Useful when the box + is needed for hit-testing or positioning outside of a [draw] call. *) + + val boxed : + ?border:Color.t -> + ?bg:Color.t -> + ?pad:float -> + ui -> + (unit -> 'a) -> + 'a + (** Wraps content in a bordered rectangle with background fill. + Defaults to theme colors and [pad = 10.0]. *) + + val padding : ui -> float -> (unit -> 'a) -> 'a + (** Adds uniform padding around the content. *) + + val center : ui -> (unit -> 'a) -> 'a + (** Centers the content in the available space. *) + + module type Store = sig + type value + val find : ui -> value -> value state + end + (** Module type satisfied by the result of {!State}. Pass as a first-class + module to {!with_state}. *) + + module State : functor (V : sig + type t + end) -> Store with type value = V.t + (** [module Store = State(struct type t = my_type end)] creates a + persistent store for internal state of type [my_type]. Declare one + per internal-state type at module level. *) + + val with_state : + (module Store with type value = 'a) -> + ui -> + 'a -> + ('a -> 'a) -> + 'a + (** [with_state (module Store) [%ui] default (fun v -> ...)] looks up the + persistent state for this call-site, passes the current value [v] to + the callback, persists the returned value, and returns it. + + The state type ['a] can combine internal and external parts; the caller + is responsible for extracting whichever parts it needs from the result. + + {[ + open Ui.Custom + + module Store = State(struct type t = int * string end) + + let my_widget ui init_text = + let (_, text) = + with_state (module Store) [%ui] (0, init_text) @@ fun (cursor, text) -> + draw ui ~min_width:30. ~min_height:30. (fun ~io box -> + (* render *) ()); + (cursor + 1, text) + in + text + ]} *) + end (**/**) diff --git a/lib/ui/layout.ml b/lib/ui/layout.ml index 94c89f5..6659d73 100644 --- a/lib/ui/layout.ml +++ b/lib/ui/layout.ml @@ -215,6 +215,19 @@ let vclip height layout = in fn box ) ) +let hclip width layout = + let w_constraint, fn = layout in + ( C1.exact width, + fun actual_width -> + let h_constraint, fn = fn (max w_constraint.min actual_width) in + ( h_constraint, + fun box -> + let box = + Box.v (Box.top_left box) + (Size.v (max w_constraint.min (Box.width box)) (Box.height box)) + in + fn box ) ) + let center layout = let w, fn = layout in ( C1.max w C1.flex, diff --git a/lib/ui/layout.mli b/lib/ui/layout.mli index 9cf12ba..0ecd90c 100644 --- a/lib/ui/layout.mli +++ b/lib/ui/layout.mli @@ -25,6 +25,7 @@ val horizontal : ?gap:float -> t list -> t val vertical : ?gap:float -> t list -> t val over : t list -> t val vclip : float -> t -> t +val hclip : float -> t -> t val pad : float -> t -> t val center : t list -> t val solve : ?width:(float -> float) -> ?height:(float -> float) -> t -> unit diff --git a/lib/ui/text_input.ml b/lib/ui/text_input.ml index 5c350c5..bfde5e7 100644 --- a/lib/ui/text_input.ml +++ b/lib/ui/text_input.ml @@ -145,18 +145,21 @@ let update ~io { offset; cursor; focused; pressed_key } text box = ({ offset; cursor; focused; pressed_key }, text) module State = Ui_backend.State (struct - type t = state + type t = state * Text.t end) let v ui text = boxed ui @@ fun () -> with_box ui @@ fun box -> let io = get_io ui in - let text = Text.of_string text in - let state = State.find ui default_state in - let st, text = update ~io !state text box in - state := st; - let text_size = Text.size_t ~io text in - Ui_backend.draw ui ~min_width:30.0 ~flex_width:1.0 - ~min_height:(Size.height text_size) (render st text); + let default = (default_state, Text.of_string text) in + let _internal, text = + Ui_backend.with_state (module State) ui default begin fun (st, text) -> + let st, text = update ~io st text box in + let text_size = Text.size_t ~io text in + Ui_backend.draw ui ~min_width:30.0 ~flex_width:1.0 + ~min_height:(Size.height text_size) (render st text); + (st, text) + end + in Text.to_string text diff --git a/lib/ui/ui.ml b/lib/ui/ui.ml index 34eba58..a3092fe 100644 --- a/lib/ui/ui.ml +++ b/lib/ui/ui.ml @@ -24,6 +24,29 @@ let window ~io ?width ?height fn = type constrain = Layout.constrain = { min : float; flex : float; max : float } -let draw = Ui_backend.draw let update_loc = Ui_backend.update_loc let nest_loc = Ui_backend.nest_loc + +module Custom = struct + type 'a state = 'a Ui_backend.state + let value = Ui_backend.state_value + let update = Ui_backend.state_update + let get_io = Ui_backend.get_io + let draw = Ui_backend.draw + let parent = Ui_backend.parent + let parent1 = Ui_backend.parent1 + let vclip = Ui_backend.vclip + let hclip = Ui_backend.hclip + let horizontal = Widgets.horizontal + let vertical = Widgets.vertical + let over = Widgets.over + module Layout = Layout + let on_click = Widgets.on_click + let with_box = Widgets.with_box + let boxed = Widgets.boxed + let padding = Widgets.padding + let center = Widgets.center + module type Store = Ui_backend.Store + module State = Ui_backend.State + let with_state = Ui_backend.with_state +end diff --git a/lib/ui/ui_backend.ml b/lib/ui/ui_backend.ml index b3dc717..c6fc5c3 100644 --- a/lib/ui/ui_backend.ml +++ b/lib/ui/ui_backend.ml @@ -44,11 +44,17 @@ let id (ui, loc) = H.replace used_ids id (counter + 1); { id with counter } +type 'a state = 'a ref + +let state_value s = !s +let state_update s v = s := v + module State (Value : sig type t end) = struct - let state : Value.t ref H.t = H.create 16 + type value = Value.t + let state : value state H.t = H.create 16 let () = all_states := State state :: !all_states let find ui default = @@ -62,6 +68,17 @@ end type ui = t * string +module type Store = sig + type value + val find : ui -> value -> value state +end + +let with_state (type a) (module S : Store with type value = a) ui default fn = + let s = S.find ui default in + let result = fn (state_value s) in + state_update s result; + result + let get_io (ui, _loc) = !(ui.io) let push_renderer ~ui renderer = ui.renderers <- renderer :: ui.renderers let draw_layout (ui, _loc) layout = push_renderer ~ui layout @@ -119,3 +136,17 @@ let vclip (ui, _loc) ?(offset = Vec.zero) box f = in ui.io := io; result + +let hclip (ui, _loc) ?(offset = Vec.zero) box f = + let io = !(ui.io) in + ui.io := + View.clip_events true + @@ View.clip (Box.translate offset box) + @@ View.translate Vec.(-1.0 * offset) io; + let result = + parent (ui, _loc) + (function [ single ] -> Layout.hclip 0.0 single | _ -> assert false) + f + in + ui.io := io; + result diff --git a/lib/ui/vscroll.ml b/lib/ui/vscroll.ml index 0799290..6cbbbc9 100644 --- a/lib/ui/vscroll.ml +++ b/lib/ui/vscroll.ml @@ -52,15 +52,17 @@ let v ui ?(min_height = 100.0) fn = with_box ui @@ fun container_box -> horizontal ui ~gap:0.0 @@ fun () -> let state = State.find ui default in + let new_child_height = ref !state.child_height in let result = with_box ui @@ fun clip_box -> vclip ui clip_box ~offset:(Vec.v 0.0 !state.offset) @@ fun () -> with_box ui @@ fun child_box -> + new_child_height := Box.height child_box; padding ui 10.0 @@ fun () -> - state := { !state with child_height = Box.height child_box }; vertical ui fn in - let offset = vscrollbar ui ~min_height (Box.height container_box) !state in + let cur = { !state with child_height = !new_child_height } in + let offset = vscrollbar ui ~min_height (Box.height container_box) cur in let io = get_io ui in let offset = if @@ -69,9 +71,9 @@ let v ui ?(min_height = 100.0) fn = then max 0.0 (min - (!state.child_height -. Box.height container_box) + (cur.child_height -. Box.height container_box) (offset +. Event.wheel_delta ~io)) else offset in - state := { !state with offset }; + state := { cur with offset }; result diff --git a/lib/ui/widgets.ml b/lib/ui/widgets.ml index 980f84b..735636e 100644 --- a/lib/ui/widgets.ml +++ b/lib/ui/widgets.ml @@ -11,14 +11,14 @@ let padding ui p f = parent1 ui (Layout.pad p) f let reshape ui ?width ?height fn = parent1 ui (Layout.reshape ?width ?height) fn module Boxes = Ui_backend.State (struct - type t = Box.t + type t = Box.t ref end) let with_box ui f = over ui @@ fun () -> - let b = Boxes.find ui Box.zero in - draw ui (fun ~io:_ box -> b := box); - f !b + let box_ref = with_state (module Boxes) ui (ref Box.zero) Fun.id in + draw ui (fun ~io:_ box -> box_ref := box); + f !box_ref let label ui text = let size = Text.size ~io:(get_io ui) text in @@ -55,21 +55,24 @@ let on_click ui fn = let io = get_io ui in let mouse = Event.mouse_pos ~io in let hover = Event.handle_clip_events ~io true && Box.mem mouse box in - let state = Button_state.find ui `normal in let st = - match !state with - | `normal when hover && !focus = None -> `hover - | `hover when not hover -> `normal - | `hover when Event.is_down ~io `click_left -> - focus := Some (Ui_backend.id ui); - `pressed - | `pressed when Event.is_up ~io:(View.clip_events false io) `click_left -> - focus := None; - if hover then `clicked else `normal - | `clicked -> `normal - | other -> other + Ui_backend.with_state + (module Button_state) + ui `normal + (fun state -> + match state with + | `normal when hover && !focus = None -> `hover + | `hover when not hover -> `normal + | `hover when Event.is_down ~io `click_left -> + focus := Some (Ui_backend.id ui); + `pressed + | `pressed when Event.is_up ~io:(View.clip_events false io) `click_left + -> + focus := None; + if hover then `clicked else `normal + | `clicked -> `normal + | other -> other) in - state := st; fn st let button ui text = From 2e9fb942d30bbc5d1a32251a1bf0c97955b0f1c7 Mon Sep 17 00:00:00 2001 From: EmileTrotignon Date: Sat, 9 May 2026 16:06:59 +0200 Subject: [PATCH 2/2] fmt and change size settings for Ui.window --- bin/hotreload.available.ml | 78 +++++++++++----------- examples/ui/src/ui.ml | 14 ++-- lib/gamelle.mli | 128 ++++++++++++++++++++----------------- lib/ui/int_slider.ml | 4 +- lib/ui/layout.ml | 14 ++-- lib/ui/layout.mli | 2 +- lib/ui/ui.ml | 12 +++- lib/ui/ui_backend.ml | 2 + lib/ui/vscroll.ml | 3 +- 9 files changed, 138 insertions(+), 119 deletions(-) diff --git a/bin/hotreload.available.ml b/bin/hotreload.available.ml index bb0ba7f..92fcb75 100644 --- a/bin/hotreload.available.ml +++ b/bin/hotreload.available.ml @@ -2,45 +2,47 @@ let ui_replay ~io (nb_past, target_clock, nb_future) = let open Gamelle in let open Ui in let io = View.font_size 12 @@ View.color Color.white io in - window ~io - ~width:(fun _ -> Size.width (Window.size ~io)) - ~height:(fun h -> - Gamelle_common.ui_replay_height := h; - h) - @@ fun [%ui] -> - horizontal [%ui] @@ fun () -> - let total_events = nb_past + nb_future in - let present = - if total_events <= 0 then 0 - else - over [%ui] @@ fun () -> - Ui.Custom.draw [%ui] (fun ~io box -> - let module Box = Gamelle.Box in - let size = Box.size box in - let progress = - 8.0 - +. ((Size.width size -. 16.0) *. float nb_past /. float total_events) - in - let target = - 8.0 - +. (Size.width size -. 16.0) - *. float target_clock /. float total_events - in - let missing = - Box.v - Vec.(Box.top_left box + Vec.v progress 0.0) - (Size.v (target -. progress) (Size.height size)) - in - if Box.width missing > 0.0 then - Box.fill ~io ~color:Color.(with_alpha 0.2 red) missing); - int_of_float - @@ slider [%ui] ~min:0.0 ~max:(float total_events) (float target_clock) - in - let nb_future = - reshape [%ui] ~width:(fun _ -> { flex = 0.0; min = 60.0; max = 60.0 }) - @@ fun () -> if button [%ui] "CLEAR" then 0 else nb_future + let _, result = + window ~io ~at:Point.zero ~size:(fun s -> + Gamelle_common.ui_replay_height := Size.height s; + Size.v (Size.width (Window.size ~io)) (Size.height s)) + @@ fun [%ui] -> + horizontal [%ui] @@ fun () -> + let total_events = nb_past + nb_future in + let present = + if total_events <= 0 then 0 + else + over [%ui] @@ fun () -> + Ui.Custom.draw [%ui] (fun ~io box -> + let module Box = Gamelle.Box in + let size = Box.size box in + let progress = + 8.0 + +. (Size.width size -. 16.0) + *. float nb_past /. float total_events + in + let target = + 8.0 + +. (Size.width size -. 16.0) + *. float target_clock /. float total_events + in + let missing = + Box.v + Vec.(Box.top_left box + Vec.v progress 0.0) + (Size.v (target -. progress) (Size.height size)) + in + if Box.width missing > 0.0 then + Box.fill ~io ~color:Color.(with_alpha 0.2 red) missing); + int_of_float + @@ slider [%ui] ~min:0.0 ~max:(float total_events) (float target_clock) + in + let nb_future = + reshape [%ui] ~width:(fun _ -> { flex = 0.0; min = 60.0; max = 60.0 }) + @@ fun () -> if button [%ui] "CLEAR" then 0 else nb_future + in + (nb_past, present, nb_future) in - (nb_past, present, nb_future) + result let to_string = function | Inotify.Access -> "Access" diff --git a/examples/ui/src/ui.ml b/examples/ui/src/ui.ml index b796c95..d25a63b 100644 --- a/examples/ui/src/ui.ml +++ b/examples/ui/src/ui.ml @@ -26,16 +26,10 @@ let initial_state = let rec loop { text; check1; check2; slider1; slider2; rad } ~io = if Input.is_pressed ~io `escape then raise Exit; Window.show_cursor ~io true; - let wr = ref 0.0 and hr = ref 0.0 in - let state = + let panel_size, state = let open Ui in - window ~io - ~width:(fun w -> - wr := 1.1 *. w; - !wr) - ~height:(fun h -> - hr := 1.1 *. h; - !hr) + window ~io ~at:Point.zero ~size:(fun s -> + Size.v (Size.width s *. 1.5) (Size.height s *. 1.)) @@ fun [%ui] -> let text = text_input [%ui] text in text_area [%ui] @@ -109,7 +103,7 @@ let rec loop { text; check1; check2; slider1; slider2; rad } ~io = end; { text; check1; check2; slider1; slider2; rad } in - Window.set_size ~io (Size.v !wr !hr); + Window.set_size ~io panel_size; next_frame ~io; loop state ~io diff --git a/lib/gamelle.mli b/lib/gamelle.mli index 6b4f943..f6c37e6 100644 --- a/lib/gamelle.mli +++ b/lib/gamelle.mli @@ -751,11 +751,20 @@ module Ui : sig type ui val window : - io:io -> - ?width:(float -> float) -> - ?height:(float -> float) -> - (ui -> 'a) -> - 'a + io:io -> at:Point.t -> ?size:(Size.t -> Size.t) -> (ui -> 'a) -> Size.t * 'a + (** {ocaml[window ~io ~at ?size begin fun [%ui] -> + ... + end]} + + renders a UI panel with its top-left corner at [at] and returns + [(panel_size, result)] where [panel_size] is the final rendered size and + [result] is the value returned by the callback. + + The panel lays out its contents, then [size] is applied to the resulting + natural size to produce the final dimensions; it defaults to the identity, + meaning the panel is exactly as large as its contents require. Use [~size:(fun + _ -> Size.v w h)] to fix dimensions, or [~size:(fun s -> Size.v (Size.width s + *. 1.1) (Size.height s))] to scale one axis. *) val button : ui -> string -> bool (** [button [%ui] "click me"] display a button saying "click me", and return @@ -876,7 +885,9 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) {[ open Ui.Custom - module Store = State(struct type t = float end) + module Store = State (struct + type t = float + end) let my_toggle ui (checked : bool) : bool = let anim = Store.find [%ui] 0.0 in @@ -885,16 +896,15 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) (if st = `pressed then min 1.0 (value anim +. 0.05) else value anim); let checked = if st = `clicked then not checked else checked in draw ui ~min_width:40. ~min_height:20. (fun ~io box -> - (* render using checked and (value anim) *) - ()); + (* render using checked and (value anim) *) + ()); checked - ]} - *) + ]} *) module Custom : sig type 'a state - (** Persistent per-instance internal state. Use {!get} and {!set} to - access it. The concrete representation is hidden. *) + (** Persistent per-instance internal state. Use {!get} and {!set} to access + it. The concrete representation is hidden. *) val value : 'a state -> 'a val update : 'a state -> 'a -> unit @@ -910,20 +920,24 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) (io:io -> Box.t -> unit) -> unit (** The primitive leaf widget. Declares a box with the given layout - constraints and calls the drawing function with the allocated [~io] - and {!Box.t}. + constraints and calls the drawing function with the allocated [~io] and + {!Box.t}. - All size parameters default to [0.0] except [max_width] and - [max_height] which default to [infinity]. A [flex] value of [0.0] - means the widget takes exactly its [min] size; a positive value means - it competes for extra space proportionally to that value. *) + All size parameters default to [0.0] except [max_width] and [max_height] + which default to [infinity]. A [flex] value of [0.0] means the widget + takes exactly its [min] size; a positive value means it competes for + extra space proportionally to that value. *) val get_io : ui -> io - (** Get the current [io]. Needed to read input events such as mouse - position inside [on_click] callbacks. *) + (** Get the current [io]. Needed to read input events such as mouse position + inside [on_click] callbacks. *) + (** Layout primitives for building container widgets. Pass a + [Layout.t list -> Layout.t] function to {!parent} to define how children + are arranged. *) module Layout : sig type t + val horizontal : ?gap:float -> t list -> t val vertical : ?gap:float -> t list -> t val over : t list -> t @@ -931,17 +945,15 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) val center : t list -> t val vclip : float -> t -> t val hclip : float -> t -> t - val reshape : ?width:(constrain -> constrain) -> ?height:(constrain -> constrain) -> t -> t + + val reshape : + ?width:(constrain -> constrain) -> + ?height:(constrain -> constrain) -> + t -> + t end - (** Layout primitives for building container widgets. Pass a - [Layout.t list -> Layout.t] function to {!parent} to define how - children are arranged. *) - val parent : - ui -> - (Layout.t list -> Layout.t) -> - (unit -> 'a) -> - 'a + val parent : ui -> (Layout.t list -> Layout.t) -> (unit -> 'a) -> 'a (** Container primitive. [parent ui layout fn] runs [fn] to collect child layout nodes, passes them to [layout], and inserts the result. All built-in layout combinators ([horizontal], [vertical], etc.) are @@ -962,23 +974,19 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) (** Clips content horizontally to [box], scrolled by [offset]. Events outside the clip box are suppressed. *) - val on_click : ui -> ([ `normal | `hover | `pressed | `clicked ] -> 'a) -> 'a + val on_click : + ui -> ([ `normal | `hover | `pressed | `clicked ] -> 'a) -> 'a (** Track hover/press/click state for whatever is rendered inside the callback. Internal button state is managed automatically. *) val with_box : ui -> (Box.t -> 'a) -> 'a - (** Provides the layout box allocated to the content. Useful when the box - is needed for hit-testing or positioning outside of a [draw] call. *) + (** Provides the layout box allocated to the content. Useful when the box is + needed for hit-testing or positioning outside of a [draw] call. *) val boxed : - ?border:Color.t -> - ?bg:Color.t -> - ?pad:float -> - ui -> - (unit -> 'a) -> - 'a - (** Wraps content in a bordered rectangle with background fill. - Defaults to theme colors and [pad = 10.0]. *) + ?border:Color.t -> ?bg:Color.t -> ?pad:float -> ui -> (unit -> 'a) -> 'a + (** Wraps content in a bordered rectangle with background fill. Defaults to + theme colors and [pad = 10.0]. *) val padding : ui -> float -> (unit -> 'a) -> 'a (** Adds uniform padding around the content. *) @@ -986,29 +994,28 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) val center : ui -> (unit -> 'a) -> 'a (** Centers the content in the available space. *) + (** Module type satisfied by the result of {!State}. Pass as a first-class + module to {!with_state}. *) module type Store = sig type value + val find : ui -> value -> value state end - (** Module type satisfied by the result of {!State}. Pass as a first-class - module to {!with_state}. *) - module State : functor (V : sig - type t - end) -> Store with type value = V.t - (** [module Store = State(struct type t = my_type end)] creates a - persistent store for internal state of type [my_type]. Declare one - per internal-state type at module level. *) + (** [module Store = State(struct type t = my_type end)] creates a persistent + store for internal state of type [my_type]. Declare one per + internal-state type at module level. *) + module State : functor + (V : sig + type t + end) + -> Store with type value = V.t val with_state : - (module Store with type value = 'a) -> - ui -> - 'a -> - ('a -> 'a) -> - 'a + (module Store with type value = 'a) -> ui -> 'a -> ('a -> 'a) -> 'a (** [with_state (module Store) [%ui] default (fun v -> ...)] looks up the - persistent state for this call-site, passes the current value [v] to - the callback, persists the returned value, and returns it. + persistent state for this call-site, passes the current value [v] to the + callback, persists the returned value, and returns it. The state type ['a] can combine internal and external parts; the caller is responsible for extracting whichever parts it needs from the result. @@ -1016,13 +1023,16 @@ to be rendered in a box that is too small, resulting in clipping or overflow. *) {[ open Ui.Custom - module Store = State(struct type t = int * string end) + module Store = State (struct + type t = int * string + end) let my_widget ui init_text = - let (_, text) = - with_state (module Store) [%ui] (0, init_text) @@ fun (cursor, text) -> + let _, text = + with_state (module Store) [%ui] (0, init_text) + @@ fun (cursor, text) -> draw ui ~min_width:30. ~min_height:30. (fun ~io box -> - (* render *) ()); + (* render *) ()); (cursor + 1, text) in text diff --git a/lib/ui/int_slider.ml b/lib/ui/int_slider.ml index fb8b7ab..934823b 100644 --- a/lib/ui/int_slider.ml +++ b/lib/ui/int_slider.ml @@ -1,4 +1,6 @@ module Ui = Ui_backend let v [%ui] ~min ~max value = - int_of_float @@ Slider.v [%ui] ~min:(float_of_int min) ~max:(float_of_int max) (float_of_int value) + int_of_float + @@ Slider.v [%ui] ~min:(float_of_int min) ~max:(float_of_int max) + (float_of_int value) diff --git a/lib/ui/layout.ml b/lib/ui/layout.ml index 6659d73..385ac77 100644 --- a/lib/ui/layout.ml +++ b/lib/ui/layout.ml @@ -68,12 +68,16 @@ let width ?(min = 0.0) ?(max = infinity) ?(flex = 0.0) fn : t = let height ?(min = 0.0) ?(max = infinity) ?(flex = 0.0) fn : h = ({ min; flex; max }, fn) -let solve ?(width = fun w -> w) ?(height = fun h -> h) layout = +let solve ~at ?(size = fun s -> s) layout = let { min = w; _ }, fn = layout in - let w = max w (width w) in - let { min = h; _ }, fn = fn w in - let h = max h (height h) in - fn (Box.v Point.zero (Size.v w h)) + let { min = h; _ }, _ = fn w in + let s = size (Size.v w h) in + let w = max w (Size.width s) in + let h = max h (Size.height s) in + let _, render = fn w in + let s = Size.v w h in + render (Box.v at s); + s let v ?(min_width = 0.0) ?(flex_width = 0.0) ?(max_width = infinity) ?(min_height = 0.0) ?(flex_height = 0.0) ?(max_height = infinity) fn = diff --git a/lib/ui/layout.mli b/lib/ui/layout.mli index 0ecd90c..34d61af 100644 --- a/lib/ui/layout.mli +++ b/lib/ui/layout.mli @@ -28,7 +28,7 @@ val vclip : float -> t -> t val hclip : float -> t -> t val pad : float -> t -> t val center : t list -> t -val solve : ?width:(float -> float) -> ?height:(float -> float) -> t -> unit +val solve : at:Point.t -> ?size:(Size.t -> Size.t) -> t -> Size.t type constrain = { min : float; flex : float; max : float } diff --git a/lib/ui/ui.ml b/lib/ui/ui.ml index a3092fe..6b72044 100644 --- a/lib/ui/ui.ml +++ b/lib/ui/ui.ml @@ -7,7 +7,7 @@ let vscroll = Vscroll.v let slider = Slider.v let int_slider = Int_slider.v -let window ~io ?width ?height fn = +let window ~io ~at ?size fn = let t = { io = ref io; renderers = []; loc_stack = [] } in let ui = (t, "root") in let result = @@ -17,9 +17,9 @@ let window ~io ?width ?height fn = in match t.renderers with | [ single ] -> - Layout.solve ?width ?height single; + let final_size = Layout.solve ~at ?size single in Ui_backend.clean_old_states (); - result + (final_size, result) | _ -> assert false type constrain = Layout.constrain = { min : float; flex : float; max : float } @@ -29,6 +29,7 @@ let nest_loc = Ui_backend.nest_loc module Custom = struct type 'a state = 'a Ui_backend.state + let value = Ui_backend.state_value let update = Ui_backend.state_update let get_io = Ui_backend.get_io @@ -40,13 +41,18 @@ module Custom = struct let horizontal = Widgets.horizontal let vertical = Widgets.vertical let over = Widgets.over + module Layout = Layout + let on_click = Widgets.on_click let with_box = Widgets.with_box let boxed = Widgets.boxed let padding = Widgets.padding let center = Widgets.center + module type Store = Ui_backend.Store + module State = Ui_backend.State + let with_state = Ui_backend.with_state end diff --git a/lib/ui/ui_backend.ml b/lib/ui/ui_backend.ml index c6fc5c3..7fc87f7 100644 --- a/lib/ui/ui_backend.ml +++ b/lib/ui/ui_backend.ml @@ -54,6 +54,7 @@ module State (Value : sig end) = struct type value = Value.t + let state : value state H.t = H.create 16 let () = all_states := State state :: !all_states @@ -70,6 +71,7 @@ type ui = t * string module type Store = sig type value + val find : ui -> value -> value state end diff --git a/lib/ui/vscroll.ml b/lib/ui/vscroll.ml index 6cbbbc9..1b93848 100644 --- a/lib/ui/vscroll.ml +++ b/lib/ui/vscroll.ml @@ -58,8 +58,7 @@ let v ui ?(min_height = 100.0) fn = vclip ui clip_box ~offset:(Vec.v 0.0 !state.offset) @@ fun () -> with_box ui @@ fun child_box -> new_child_height := Box.height child_box; - padding ui 10.0 @@ fun () -> - vertical ui fn + padding ui 10.0 @@ fun () -> vertical ui fn in let cur = { !state with child_height = !new_child_height } in let offset = vscrollbar ui ~min_height (Box.height container_box) cur in