[ANN] Bogue, the OCaml GUI

Yeah, but having a dedicated library for beginner that hides this would be useful in my opinion.

Hi @Regis_Smith the new version in opam should fix this issue.
(version 20220115)

It should also work on Mac Retina screens, thanks to the help of @nilsbecker!

EDIT: the doc about Theming is here: Bogue.Theme

1 Like

Hi, some new developments. I have implemented a new Sdl_area widget where one can conveniently issue any SDL function (from the SDL Renderer API).

Here is (below) the new ‘labelled graph’ example. In this example I am using regular “label” widgets for creating the nodes, and I am using an Sdl_area for drawing the lines.

The nice things for labels to be regular widgets is that one can click on them. To demonstrate this, in this example they react to a click by jumping to another random location (with animation).

graph-click

open Bogue
module W = Widget
module L = Layout

let n = 15 (* number of discs *)
let radius = 20
let width = 800
let height = 600

let c = Draw.find_color "#e5b92c"
let cb = Draw.find_color "#7b6b35"
let disc_style = Style.(
    create ~border:(
      mk_border ~radius (mk_line ~color:Draw.(opaque c) ~width:1 ~style:Solid ()))
      ~background:(color_bg Draw.(opaque cb)) ())

let background = L.style_bg Style.(
    of_bg (gradient ~angle:45. Draw.[opaque grey; opaque black]))

let fg = Draw.(opaque white)

let create_disc i (x,y) =
  let w = 2*radius + 1 in
  let bg = Box.create ~style:disc_style ~width:w ~height:w () in
  W.label ~fg (string_of_int i)
  |> L.resident ~background:(L.box_bg bg) ~x:(x-radius) ~y:(y-radius) ~w ~h:w

let move_disc (x,y) d =
  let (x0, y0) = L.xpos d, L.ypos d in
  L.animate_x d (Avar.fromto x0 x);
  L.animate_y d (Avar.fromto y0 y)

let random_center _ =
  radius + Random.int (width - 2*radius),
  radius + Random.int (height - 2*radius)

let area =
  let sdlw = W.sdl_area ~w:width ~h:height () in
  let sdla = W.get_sdl_area sdlw in
  let centers = Array.init n random_center in
  let color = Draw.(opaque grey) in
  let draw_lines renderer = let open Draw in
    for i = 0 to n - 2 do
      let x0, y0 = to_pixels centers.(i) in
      let x1, y1 = to_pixels centers.(i+1) in
      line renderer ~color ~thick:6 ~x0 ~y0 ~x1 ~y1
    done in
  Sdl_area.add sdla draw_lines;
  let discs = Array.mapi create_disc centers |> Array.to_list in
  (* move the disc when click on it *)
  List.iteri (fun i d ->
      W.on_click ~click:(fun _ ->
          centers.(i) <- random_center 0;
          Sdl_area.update sdla;
          let x,y = centers.(i) in
          move_disc (x - radius, y - radius) d) (L.widget d))
    discs;
  L.superpose ~w:width ~h:height ~background (L.resident sdlw :: discs)


let board = Bogue.make [] [area]

let () = Bogue.run board
8 Likes

Thank you so much, I look forward to implementing this in my current project!
I’ll let you know how it goes

Cool. Is it possible with this API to wait with drawing the new connecting lines to the randomized label until the animation has finished?

Yes, it is possible, because Avar.fromto has an ending parameter.
You would simply have to replace the line containing L.animate_x by

 L.animate_x d (Avar.fromto x0 x ~ending:(fun () ->
      Sdl_area.update sdla;
      Update.push sdlw));

add sdla and sdlw as parameters to the move_disc function (in fact only sdlw is necessary), and remove the line with Sdl_area.update sdla below.

2 Likes

Hi!
I’m just starting to use Bogue in a game I’m making, and I was wondering it if was possible to send data from a game to another ml file?
I have variables representing the “last clicked coordinates” and when I reference them in another file, their values don’t update until the Bogue window is closed.
Any advice would be appreciated, thanks!

Hi! I’m not sure I understand your question. Do you want to save data in a config file for keeping them after the player quits the game, and read them back when she starts again,

or do you want to dynamically use your data (the “last clicked coordinates”) in some other function while the game is running?

Hi! Some friends and I are currently working on implementing a small game in OCaml. Most of our code is pure OCaml – we have a separate game loop and terminal interface that updates with each “action” we perform. Recently, we wanted to use a real GUI library and came across Bogue! Unfortunately, our current implementation involves passing the state into the GUI code which writes a static, unmodifiable visual representation of the state, so we end up creating a brand new window with each change to the game’s state. This isn’t exactly what we intended.

We wanted to know if there was a way to rerender specific components and then pass them into the window so that the entire window wouldn’t rerender; instead, only the updated components would change. Thanks for your time!

yes of course; that’s the way Bogue works. every component in fine is rendered into a texture, and if the component hasn’t changed, the texture is re-used directly, so it is very fast to render. The idea is that you don’t need to care about this, this is done automatically by Bogue. For a small game written with bogue, you may have a look at snoke:

2 Likes

for instance if you have a window full of widgets, and if you want to modify the text of one of them, you should just call Widget.set_text on that widget. There is no need to recreate all the widgets!

Similarly if you want to change the position of a Layout, use Layout.setx or Layout.sety, see Bogue.Layout, don’t recreate the layout!

2 Likes

Thank you for your quick response!!

As a piece of constructive criticism (or maybe I will be proven wrong), I tried out Bogue but I needed to make my own widget (a game board) as the existing ones did not suit my needs. To that occasion I discovered that Widget.t is in fact a variant, a sum of all possible types of widgets. This has the consequence that to add a custom widget type, one has to fork Bogue, which seems like a big modularity extensibility* issue to me.

The general idea is to construct new (possibly complicated) Layouts by assembling simple Widgets. I’m not sure what you are trying to achieve exactly, but could you construct your game board this way?

To group several widgets in a Layout, see for instance this part of the tutorials

2 Likes

@ivg gave a great solution to this specific (expression) problem at The shape design problem - #39 by ivg

2 Likes

I needed to arrange squares in a grid, with the squares evenly distributed and separated by small gutters and rows of interstitial space. Clicking the squares should do something (select them), but clicking the interstitial space also should do something (deselect the selected square, if any). That didn’t seem possible with Bogue.

More generally, relying on composition of existing functionalities can only go so far when there is no possibility for extension. I don’t think it is tenable to require users to fork/ask every time they need a feature, which is why I see extensibility as a must-have.

That’s really easy to do. Just use empty widgets for the interstitial spaces (or images, if you want them to look more fancy)

I’m not opposed to having an extension mechanism, but for this case (and it can be generalized) I don’t see the point. Building a layout out of widgets is the way Bogue works, and if you try to build a new widget of your own, it will be probably more difficult (because programming widgets is more low-level).

In other words, the philosophy here is that widgets are your lego bricks, and they should be generic enough to let you build many marvelous things. Do you view your “game board” as a generic brick that can be used to build other things?

Maybe the word “widget” was not so appropriately chosen and leads to confusion. (In some sense, Bogue’s Layouts are also widgets, in the usual GUI terminology.)

1 Like

You can try this

#require "bogue";;

open Bogue
module W = Widget
module L = Layout

let unselect button =
  W.set_state button false

let main () =
  let row1 = ["A"; "B"; "C"]
             |> List.map (W.button ~kind:Button.Switch) in
  let row2 = ["X"; "Y"; "Z"]
             |> List.map (W.button ~kind:Button.Switch) in
  let buttons = L.tower ~sep:50
                  [ L.flat_of_w ~sep:50 row1;
                    L.flat_of_w ~sep:50 row2 ] in
  let bg = W.empty ~w:300 ~h:300 () in
  W.on_click ~click:(fun _ ->
      List.iter unselect row1;
      List.iter unselect row2) bg;

  let layout = L.superpose [ L.resident bg; buttons] in

  let board = Bogue.of_layout layout in
  Bogue.run board

The letters can be clicked to select them. If you click on the background, this unselects them.

1 Like

I’m not sure if it’s an idea you want to consider @sanette but with my GUI library (which might not be ready for public use ever :sweat_smile: and is still not ready for private use) I think I have an okay-ish solution to the extensibility problem.

Simplifying it a bit, the main widget (= drawable) type is defined like this:

type 'model drawable =
  | Empty (* No widget here, so do nothing; for example, you might give a button a child or not *)
  | Widget of {
      f_draw : (* Minimum type signature required for draw function *);
      f_size : (* Minimum type signature required for getting the size, if you want to stack one widget on top of another in a column or row for example *);
      mouse_update : (* Minimum type signature for returning a new 'model given mouse events *);
      key_update : (* Minimum type signature for returning a new 'model given keyboard events *);
    }

This method lets me implement new widgets without having to extend the drawable type. The only things I need in order to create a new widget are four functions in a Widget { … } that satisfy the Widget case. If a widget needs more information (like a margin will need to know the width and height around a child widget), you can use partial application to build up those four minimum type signatures, just like in here:
https://github.com/hummy123/reylm/blob/af0a0bd531c946b9e2d3e207175acf063f0f682e/lib/layout/margin.ml#L44 .

The event loop functions (size, draw, mouse/key update) can supply the rest of the needed to execute the Widget case in just a few simple lines of code (see here).

I think of Widget { … } as being similar to an interface in OOP land (specifying the parameters and return type of whatever functions you need) and the actual widgets like Margin which I linked before as being like object constructors which may require various parameters not essential to the interface itself (but essential to this particular object).

Here’s a very very simple (runnable but non-interactive) program showing how widgets compose for the user. I find that it works for me and it might work for you as well (or maybe the approach you already have is fine which would be cool; I also thought today about disallowing extensibility in my library to make widget state management easier for an end-user as described here but still thinking about it).

1 Like

Hi @hummy123 thanks for your message and good luck for your GUI project!

To be honest I thought about the extension issue a few years ago, I considered functors, first class modules, or simply records like the one your are using, (and even Objects, but that was a long time ago :wink: ) but finally I decided not to do it, mainly for two reasons:

  • I didn’t find a compelling use case where this would be necessary (in Bogue, complicated “widgets” like a menu bar are in fact of type Layout, not Widget)
  • I realized that the priority was to make the code robust, and old style OCaml variants (fixed union of types) might sound rigid, but they also help reducing the possibility of bugs

But of course I’m not opposed to changes by principle; if people using the library have clear cases where extension of low-level widgets are necessary, let’s discuss it again!

1 Like