diff --git a/README.adoc b/README.adoc index 80b131d..ce864a0 100644 --- a/README.adoc +++ b/README.adoc @@ -1524,7 +1524,7 @@ And this is the Nim version, created with c2nim and some manual tuning: .css_colored_listview.nim ---- # nim c css_colored_listview.nim -import gintro/[gtk, gobject] +import gintro/[gtk, glib, gobject] import gintro/gdk except Window # there is a problem with gdk.Window -- we have to investigate! const # maybe we should use Nim's enum here? Id = 0 @@ -1552,6 +1552,26 @@ proc toBoolVal(b: bool): Value = discard init(result, gtype) setBoolean(result, b) +# we need the following two procs for now -- later we will not use that ugly cast... +proc typeTest(o: gobject.Object; s: string): bool = + let gt = g_type_from_name(s) + return g_type_check_instance_is_a(cast[ptr TypeInstance00](o.impl), gt).toBool + +proc listStore(o: gobject.Object): gtk.ListStore = + assert(typeTest(o, "GtkListStore")) + cast[gtk.ListStore](o) + +proc updateRow(renderer: CellRendererText; path: cstring; newText: cstring; tree: TreeView) = + var iter: TreeIter + var value: Value + let gtype = typeFromName("gchararray") + discard init(value, gtype) + let store = listStore(tree.getModel()) + value.setString(newText) + let treePath = newTreePathFromString(path) + discard store.getIter(iter, treePath) + store.setValue(iter, 1, value) + # we use the old gtk style with init() as is used in the C original -- maybe better use modern app sytle proc main() = gtk.init() @@ -1562,8 +1582,9 @@ proc main() = window.borderWidth = 20 connect(window, "destroy", bye) var iter: TreeIter - var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), typeFromName("gchararray")] - var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # this cast is ugly, we should fix it in the bindings soon. + var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), + typeFromName("gchararray")] + var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # cast is ugly, we should fix it in bindings. let progNames = ["Gedit", "Gimp", "Inkscape", "Firefox", "Calculator", "Devhelp"] for i, n in progNames: store.append(iter) # currently we have to use setValue() as there is no varargs proc as in C original @@ -1581,6 +1602,7 @@ proc main() = setProperty(renderer1, "editable", toBoolVal(false)) var renderer2 = newCellRendererText() setProperty(renderer2, "editable", toBoolVal(true)) + connect(renderer2, "edited", updateRow, tree) ## Bind the Color column to the "cell-background" property. var column1 = newTreeViewColumn() column1.setTitle("ID") @@ -1594,12 +1616,12 @@ proc main() = column1.addAttribute(renderer2, "text", Program) column1.addAttribute(renderer2, "cell-background", Color2) discard tree.appendColumn(column2) - var grid = newGrid() # grid with only one occupied cell makes not much sense here -- but so we can add more widgets later + var grid = newGrid() # only one occupied cell makes no sense -- but so we can add more widgets later grid.attach(tree, 0, 0, 1, 1) window.add(grid) const cssString = # note: big font selected intentionally - """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); - color: rgba(0,0,255,1.0);}""" + """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: + rgba(255,255,0,1.0); color: rgba(0,0,255,1.0);}""" var provider = newCssProvider() discard provider.loadFromData(cssString) addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION) @@ -1611,7 +1633,10 @@ main() When you compile with `nim c -d:release -d:danger --passC:-flto css_colored_listview.nim` you will get an executable size of 80k, which is big compared with the 20k of the C version, but -not too bad. +not too bad. You may note that I have added the updateRow() proc, which is necessary to +make editing the program name entries permanent. That proc needs cstring parametes, which +may be surprising, as we generally use Nim strings. Not a big problem, maybe intended, we may have to +check the connect() macro in gimpl.nim. == A more advanced example for cairo drawing with zooming, panning, scrolling diff --git a/README.html b/README.html new file mode 100644 index 0000000..dde01dc --- /dev/null +++ b/README.html @@ -0,0 +1,3082 @@ + + + + + + + + +High level GTK3 (and GTK4) bindings for the Nim programming language + + + + + + + +
+
+
+
+ + + + + +
+ + +A more fancy copy of this document with dark source code background is available at GIntro README +
+
+
+ + + + + +
+ + +This work is partly based on earlier works of J. Mansour and has been supported by A. Rumpf and other Nim and GTK/Gnome developers. +The combinatorics module was kindly provided by R. Behrends. +
+
+
+ + + + + +
+ + +Starting with release 0.5.3 we do not generate field entries for objects and we do +not generate class structs and private objects. Also we stopped exporting the low level functions +like gtk_button_new(). For a real high-level binding we should not need these. If that is a serious +limitation for you, then use release 0.5.2 for now and create an github issue for your use case, we +will try to fix it, maybe undo these changes. Also starting with v0.5.3 we try to support array +parameters like TargetEntryArray, PageRangeArray and KeymapKeyArray. Use of these array parameters is rare, +if you will use functions with these parameters you may inspect the source code first, as the +code is auto-generated and still untested. +
+
+
+ + + + + +
+ + +Starting with release 0.5.0 we also support GTK4. GTK4 is still work in progress and not intended for +end users yet, but it is good to have it available for migration testing. For GTK4 we have a new module gsk, and +new versions of modules gtk, gdk and gdkX11, which are not backward compatible with the old once of GTK3. The other +modules can be used by GTK3 and GTK4 in parallel. Due to this fact we use a single nimble package which can be used for +GTK3 and GTK4 development. To archive this, we have named the new modules gtk4, gdk4 and gdkX114 — the +old once are named gtk, gdk and gdkX11 still. So for existing GTK3 software no code changes are necessary. +For GTK4 an example is provided — it imports gtk4 instead of gtk now, and instead of window.showAll() +window.show() is needed. More GTK4 examples may follow eventually, see GTK4 migration page at +https://developer.gnome.org/gtk4/stable/gtk-migrating-3-to-4.html. The gintro package tries to install +the GTK4 modules when GTK4 is available on the local computer and skips it if not available. +For successful detection of GTK4 the typelibs must be found. For example, if you have installed +GTK4 from sources on /opt/gtk as described in https://developer.gnome.org/gtk4/3.96/gtk-building.html, then +you may have to execute "export GI_TYPELIB_PATH=/opt/gtk/lib64/girepository-1.0" in your shell before you +do "nimble install gintro". Currently gtksourceview and vte is not available for GTK4. GTK4 provides a +official test program called gtk4-demo — of course that one should work fine on your box before you +consider testing Nim with GTK4. +
+
+
+

Nim is a modern universal programming language.

+
+
+

GTK, also known as the Gimp Tool Kit and now sometimes called Gnome Tool Kit, is a Graphical User Interface library.

+
+
+ + + + + +
+ + +Later we will insert at this location a nice picture of a fancy Nim GTK3 GUI. Such a picture is fine to attract users and indeed is a good motivation. +But such pictures are no real evidence for the quality of a GUI toolkit — the concrete example may look nice, while the toolkit +looks much worse in other environments and offers by far not all that what is needed in real life. +
+
+
+

While GTK was initially designed and advertised as cross platform GUI toolkit, it is currently mostly used on Linux and other Unix like operation systems. +Most Linux distributions include it, and some use it for their default desktop environment, often with the Gnome environment or other window managers. +While GTK2 applications like GIMP are still used on Windows, there seems to exist currently only very few GTK3 applications for Windows or MacOSX. +When you develop primary free open source software (FOSS) for Linux or other Unix like operating systems, then GTK3 is a good choice for you. With some +effort you should be even able to port your application to the proprietary Windows or MacOSX operating systems. But when your primary target platforms +are Windows and MacOSX and you desire a real native look and feel there, then you may find better suited ones in the Nim software repository. +Also, when you only need a minimal restricted GUI which is very easy to install on Windows and MacOSX, then you may find better suited packages +in the Nim package repository. Android OS is currently not supported by GTK at all.

+
+
+ + + + + +
+ + +At least for Windows 10 it seems to be not that hard to install GTK3 libraries, as was recently reported in +https://github.com/StefanSalewski/gintro/issues/24 by user zetashift: +
+
+
+
+
  Sketch of GTK3 install for Windows 10:
+  For the GTK libs I did according these instructions(https://www.gtk.org/download/windows.php):
+  Install MSYS2
+  In the msys2 cmd I entered:
+  pacman -S mingw-w64-x86_64-gtk3
+  Then for some other necessary depencies(girepository.dll) you need to do:
+  pacman -S mingw-w64-x86_64-python3-gobject
+
+  Additional, you have to install the separate GtkSourceView lib in a similar manner from
+  https://github.com/Alexpux/MINGW-packages/blob/master/mingw-w64-gtksourceview3/
+
+
+
+

While low level Nim bindings for GTK3 are already available since a few years, this one is an attempt to +provide real high level bindings with full type safety, full Garbage Collector (GC) support and an idiomatic +Application Programming Interface (API).

+
+
+

Currently there are at least 3 sources of GTK3 bindings for Nim:

+
+ +
+

ngtk3 was the first attempt to provide GTK3 support for Nim. It contained single repositories for all the GTK related libraries and +was not supported by nimble package manager. It was created from GTK 3.20 headers and is now deprecated.

+
+
+

oldgtk3 is the port of ngtk3 to GTK 3.22 — joining all libraries and providing nimble support. Some people may still prefer +using oldgtk3. As it is generated with the Nim tool c2nim directly from the C header files without much manual intervention, +it should be complete and contain not that much bugs. Missing Garbage Collector support is generally not really a problem, as +widgets are generally put into containers and were automatically deleted together with its parents due to GTK’s reference counting.

+
+
+

Still there can be some demand for really high level bindings — so this gintro repository tries to provide them.

+
+
+

High level GTK3 bindings, as available for many other programming languages like C++, Python, Ruby or D already, +have these advantages:

+
+
+
    +
  • +

    full Garbage Collector or Destructor support — you should never have to free resources manually

    +
  • +
  • +

    Widgets are Nim objects, so inheritance and sub-classing can be used

    +
  • +
  • +

    full type safety — no needs for casts or other unsafe and dangerous operations

    +
  • +
+
+
+

These high level bindings are based on GObject-Introspection, an XML based database like interface description. Compared to the C header +files this description gives us more and deeper information about data types and function calls, for example ownership transfer of objects and +in or out direction of procedure variables, which makes writing the glue code much easier. +And it should work with minimal +modifications also for the upcoming GTK4.

+
+
+

Unfortunately there are also some drawbacks:

+
+
+
    +
  • +

    The Application Programming Interface (API) will be different from what is known from C API, so using C examples or C tutorials is not really straight forward

    +
  • +
  • +

    The high level source code will differ from available C examples, so there would be a big demand for tutorials

    +
  • +
  • +

    We need a lot of glue code, which has much room for bugs. So much testing is necessary.

    +
  • +
  • +

    There is some overhead due to indirect calls, leading to some code size increase and minimal +performance loss.

    +
  • +
+
+
+ + + + + +
+ + +The new package name is gintro, short for GObject-Introspection. The previous name was nim-gi, but the hyphen is deprecated for package names, as is the +nim prefix. +
+
+
+
+
+

Current state of these bindings

+
+
+

We are still in an early stage, but it is already more than a proof of concept. GTK and related libraries have many thousand of +callable functions and nearly as many data types. Testing all that is nearly impossible for a small team with limited resources. +The initial approach was to generate low level +bindings, which looked similar to the ones generated by the c2nim tool from the C headers. After that was done, we have associated all +the C structs and GObject data types with Nim proxy objects. A well defined relation between these proxy object and the low level C data types +should ensure fully automatic garbage collection. This is supported by smart type conversion, for example C strings returned by glib library +are assigned to newly created Nim strings, while the memory of the C strings is automatically freed. For most cases this seems to work. But there +exists a few more complicated cases, for example functions may return whole arrays of C strings or other non elementary data types, +or function arguments or results may be so called glists, +list structures of glib library. These cases can not be processed automatically but needs carefully manual investigations. And there may be still functions and data +types missing: GObject-Introspection query gives us many thousand lines of Nim interface code, and it is not really obvious if and what is missing. +Some functions and data types are missing for sure — at least some low level ones, which are considered unneeded for high level bindings by GObject-Introspection. +But maybe more is missing, we have to investigate that. Until now these bindings have been tested only for 64 bit Linux systems with GTK 3.22.

+
+
+

These basic libraries are already partly tested:

+
+
+

Gtk, Gdk, GLib, GObject, Gio, GdkPixbuf, GtkSource, Pango, PangoCairo, PangoFT2, GModule, Rsvg, fontconfig, freetype2, xlib, Atk, Vte, cairo

+
+
+

In best case it should be possible to add more GObject based libraries to this list without larger modifications of the generator source code. +Unfortunately the bindings for the cairo drawing library provided by GObject-Introspection was only a minimal stub — we have extend it manually.

+
+
+
+
+

How to try it out

+
+
+

Of course you will need a working Nim installation with a recent compiler version and you have to ensure that GTK and related libraries are installed on your system. For some Linux +distributions which provide mainly pre-compiled software you may have to also install some GTK related developer files.

+
+
+

With a recent nimble version (>= v0.8.10) you only have to type in a shell window:

+
+
+
+
nimble install gintro
+
+
+
+ + + + + +
+ + +Latest version of gintro package uses some files from oldgtk3 package for bootstrapping. We assume that +users of gintro generally are not interested in low level oldgtk3 package, so we try to download only 3 single files +from oldgtk3 package. That should work if wget or nimgrab executables are available. If it fails you should +get a longer error message which may help you to solve the issue. +
+
+
+ + + + + +
+ + +Nimble prepare should run for about 20 seconds, it compiles and executes the generator program gen.nim. +Unfortunately we can not guarantee that the generator command will be able to really build all the +desired modules. The built process highly depends on your OS and installed GTK version. For 64 bit Linux systems +with GTK 3.22 and all required dependencies installed it should work. For never GTK versions it may fail, when that GTK +release introduces for example new unknown data types like array containers. In that case manual fixes may be necessary. +The GObject-Introspection based built process generates bindings customized to the OS where the generator is executed, +so for older GTK releases or a 32 bit system different files are created. Later we may also provide pre-generated +files for various OS and GTK versions, but building locally is preferred when possible. +
+
+
+
+
+

A few basic examples

+
+
+ + + + + +
+ + +Currently we do not install the example programs. If you want to try them, you have to copy the source code of the +examples from https://github.com/StefanSalewski/gintro/tree/master/examples to your local computer, maybe to /tmp/gintro/examples directory. +
+
+
+

Then you can compile and run them from shell with commands like

+
+
+
+
cd /tmp/gintro/examples/
+nim c app0.nim
+./app0
+
+
+
+

or you may open the source files in your favorite Nim IDE or editor. Taking the source code from this Readme file is not +really recommended, as these source code listings may be not the latest versions.

+
+
+

GTK3 programs can use still the old GTK2 design, where you first initialize the GTK library, create your widgets and finally enter the GTK main loop. +This style is still used in many tutorials as in Zetcode tutorial or in the GTK book of A. Krause. +Or you can use the new GTK3 App style, this is generally recommended by newer original GTK documentation. +Unfortunately the GTK3 original documentation is mostly restricted to the GTK3 API documentation, which is generally very good, but makes +it not really easy for beginners to start with GTK. API docs and some basic introduction is available here:

+
+ +
+ + + + + +
+ + +If you should decide to continue developing software with GTK, then you may consider installing the so called +devhelp tool. It gives you easy and fast access to the GTK API docs. For example, if you want to use a Button Widget in your +GUI and wants to learn more about related functions and signals, you just enter Button in that tool and are guided to +all the relevant information. +
+
+
+

We start with a minimal traditional old style example, which should be familiar to most of us:

+
+
+
t0.nim
+
+
# nim c t0.nim
+import gintro/[gtk, gobject]
+
+proc bye(w: Window) =
+  mainQuit()
+  echo "Bye..."
+
+proc main =
+  gtk.init()
+  let window = newWindow()
+  window.title = "First Test"
+  window.connect("destroy", bye)
+  window.showAll
+  gtk.main()
+
+main()
+
+
+
+

This is the traditional layout of GTK2 programs. When using this style then it is important to initialize the GTK library by calling gtk.init() +at the very beginning. Then we create the desired widgets, connect signals, show all widgets and finally enter the GTK main loop +by calling gtk.main. About connecting signals we will learn more soon, for now it is only important that we have to connect to +the destroy signal here to enable the user to terminate program execution by clicking the window close button.

+
+
+

Now a really minimal but complete App style example, which displays an empty window.

+
+
+ + + + + +
+ + +The source text of all these examples is contained in the examples directory. Unfortunately github +seems to not allow to include that sources directly into this document, so there may be minimal +differences between the source code displayed here and the sources in examples directory. +
+
+
+
app0.nim
+
+
# app0.nim -- minimal application style example
+# nim c app0.nim
+import gintro/[gtk, glib, gobject, gio]
+
+proc appActivate(app: Application) =
+  let window = newApplicationWindow(app)
+  window.title = "GTK3 & Nim"
+  window.defaultSize = (200, 200)
+  showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+

In the main proc we create a new application and connect the activate signal to our activate proc, which then creates and displays +the still empty window.

+
+
+ + + + + +
+ + +We are importing modules gtk and gio. Initially both modules had a data type called Application (gtk.Application +extends indeed the gio.Application), so we would have to use module name prefixes, or we could import from gio only +what is really needed (from gio import …​) or use the form (import gio exept …​). But as gio.Application is generally +not needed often, we have no renamed gio.Application to GApplication. No more name clashes. +
+
+
+

Various ways to set widget parameters are supported — the number 1 to 6 refer to the comments below:

+
+
+
+
setDefaultSize(window, 200, 200) (1)
+gtk.setDefaultSize(window, 200, 200) (2)
+window.setDefaultSize(200, 200) (3)
+window.setDefaultSize(width = 200, height = 200) (4)
+window.defaultSize = (200, 200) (5)
+window.defaultSize = (width: 200, height: 200) (6)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
1proc call syntax
2optional qualified with module name prefix
3method call syntax
4named parameters
5tupel assignment
6tupel assignment with named members
+
+
+

Well, that empty window is really not very interesting. The GTK and Gnome team provides some GTK examples +at https://developer.gnome.org/gnome-devel-demos/. +The C demos seems to be most actual and complete, +and are easy to port to Nim. So we start with these, +but if you are familiar with the other listed languages, then you can try to port them to Nim as well. +Let us start with https://developer.gnome.org/gnome-devel-demos/3.22/button.c.html.en as it is +still short and easy to understand, but shows already some interesting topics.

+
+
+
+NimGTK3Button +
+
+
+

The C code looks like this:

+
+
+
button.c
+
+
#include <gtk/gtk.h>
+
+/*This is the callback function. It is a handler function which
+reacts to the signal. In this case, it will cause the button label's
+string to reverse.*/
+static void
+button_clicked (GtkButton *button,
+                gpointer   user_data)
+{
+  const char *old_label;
+  char *new_label;
+
+  old_label = gtk_button_get_label (button);
+  new_label = g_utf8_strreverse (old_label, -1);
+
+  gtk_button_set_label (button, new_label);
+  g_free (new_label);
+}
+
+static void
+activate (GtkApplication *app,
+          gpointer        user_data)
+{
+  GtkWidget *window;
+  GtkWidget *button;
+
+  /*Create a window with a title and a default size*/
+  window = gtk_application_window_new (app);
+  gtk_window_set_title (GTK_WINDOW (window), "GNOME Button");
+  gtk_window_set_default_size (GTK_WINDOW (window), 250, 50);
+
+  /*Create a button with a label, and add it to the window*/
+  button = gtk_button_new_with_label ("Click Me");
+  gtk_container_add (GTK_CONTAINER (window), button);
+
+  /*Connecting the clicked signal to the callback function*/
+  g_signal_connect (GTK_BUTTON (button),
+                    "clicked",
+                    G_CALLBACK (button_clicked),
+                    G_OBJECT (window));
+
+  gtk_widget_show_all (window);
+}
+
+int
+main (int argc, char **argv)
+{
+  GtkApplication *app;
+  int status;
+
+  app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);
+  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
+  status = g_application_run (G_APPLICATION (app), argc, argv);
+  g_object_unref (app);
+
+  return status;
+}
+
+
+
+

Converting it to Nim is straight forward with some basic C and Nim knowledge, and Nim does not force us +to convert its shape into all the classes known from pure Object Orientated (OO) languages. We can either use the +Nim tool c2nim to help us with the conversion, or do it manually. Indeed c2nim can be very helpful by +converting C sources to Nim. Most of the time it works well. Personally I generally pre-process C files, for example +by removing too strange macros and defines, or by replacing strange constructs, like C `for loops, to simpler +ones like while loops. Then I apply c2nim to the C file and finally manually compare the result line by line and +fine tune the Nim code. But for this short source text we may do all that manually and finally get something like +this:

+
+
+
button.nim
+
+
# nim c button.nim
+import gintro/[gtk, glib, gobject, gio]
+
+proc buttonClicked (button: Button) =
+  button.label = utf8Strreverse(button.label, -1)
+
+proc appActivate (app: Application) =
+  let window = newApplicationWindow(app)
+  window.title = "GNOME Button"
+  window.defaultSize = (250, 50)
+  let button = newButton("Click Me")
+  window.add(button)
+  button.connect("clicked",  buttonClicked)
+  window.showAll
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard app.run
+
+main()
+
+
+
+

Again we have the basic shape already known from app0.nim example: Main proc creates the application, connect +to the activate signal and finally runs the application. When GTK launches the application and emits the activate signal, then +our activate proc is called, which creates a main window containing a button widget. That button is again connected with a +signal, in this case named clicked. That signal is emitted by GTK whenever that button is clicked with the mouse and results +in a call of our provided buttonClicked() proc. The procs connected to signals are called callbacks and generally got the widget +on which the signal was emitted as first parameter. They can also get a second optional parameter of arbitrary type — we will +see that in a later example. This callback here gets only the button itself as parameter, and it’s task is to reverse the +text displayed by the button. Not very interesting basically, but we are indeed using the glib function utf8Strreverse() +for this task. While that function internally works with cstrings, and in C we have to free the memory of the returned cstring, +in our Nim example that is done automatically by Nim’s Garbage Collector. When you compare our example carefully with the C code, +then you may notice a difference. The C code passes the window containing the button as an additional parameter to the +callback function, but that parameter is not really used. We simple ignore it here, as it is not used at all. +In one of the following examples you will learn how passing (nearly) arbitrary parameters in a type safe way is done. +Another difference is, that the C code returns an integer status value returned by g_application_run() to the OS. We +could do the same by using the quit() proc of Nim’s OS module, but as that would give us no additional benefit, we simply ignore it.

+
+
+ + + + + +
+ + +The command nim c sourcetext.nim generates an executable which contains code for runtime checks and debugging, +which increases executable size and decreases performance. +After you have tested your software carefully, you may give the additional parameter -d:release to avoid this. For the gcc backend +you may additional enable Link Time Optimization (LTO), which reduces executable size further. To enable LTO you may put +a nim.cfg file in your sources directory with content like +
+
+
+
+
path:"$projectdir"
+nimcache:"/tmp/$projectdir"
+gcc.options.speed = "-march=native -O3 -flto -fstrict-aliasing"
+
+
+
+

With that optimization, your executable sizes should be in the range of about 50 kB only!

+
+
+
+
+

Optional, type safe parameters for callbacks

+
+
+

The next example shows, how we can pass (nearly) arbitrary parameters to our connect procs. +We pass a string, an object from the stack, a reference to an object allocated on the heap +and finally a widget (in this case the application window itself, you may also try passing +another button). As the main window itself is a so called GTK bin and can contain only one +single child widget, we create a container widget, a vertical box in this case, fill that box with +some buttons, and add that box to the window.

+
+
+

Compile and start this example from the command line and watch what +happens when you click on the buttons.

+
+
+
connect_args.nim
+
+
# nim c connect_args.nim
+import gintro/[gtk, glib, gobject, gio]
+
+type
+  O = object
+    i: int
+
+proc b1Callback(button: Button; str: string) =
+  echo str
+
+proc b2Callback(button: Button; o: O) =
+  echo "Value of field i in object o = ", o.i
+
+proc b3Callback(button: Button; r: ref O) =
+  echo "Value of field i in ref to object O = ", r.i
+
+proc b4Callback(button: Button; w: ApplicationWindow) =
+  if w.title == "Nim with GTK3":
+    w.title = "GTK3 with Nim"
+  else:
+    w.title = "Nim with GTK3"
+
+proc appActivate (app: Application) =
+  var o: O
+  var r: ref O
+  new r
+  o.i = 1234567
+  r.i = 7654321
+  let window = newApplicationWindow(app)
+  let box = newBox(Orientation.vertical, 0)
+  window.title = "Parameters for callbacks"
+  let b1 = newButton("Nim with GTK3")
+  let b2 = newButton("Passing an object from stack")
+  let b3 = newButton("Passing an object from heap")
+  let b4 = newButton("Passing a Widget")
+  b1.connect("clicked",  b1Callback, "is much fun.")
+  b2.connect("clicked",  b2Callback, o)
+  b3.connect("clicked",  b3Callback, r)
+  b4.connect("clicked",  b4Callback, window)
+  box.add(b1)
+  box.add(b2)
+  box.add(b3)
+  box.add(b4)
+  window.add(box)
+  window.showAll
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard app.run
+
+main()
+
+
+
+

To prove type safety, we may modify one of the callback procs and watch the compiler output:

+
+
+
+
proc b1Callback(button: Button; str: int) =
+  discard # echo str
+
+
+
+
+
connect_args.nim(37, 5) template/generic instantiation from here
+gtk.nim(-15021, 10) Error: type mismatch: got (ref Button:ObjectType, string)
+but expected one of:
+proc b1Callback(button: Button; str: int)
+
+
+
+

It may be not always really obvious what the compiler wants to tell us, but at least we +are told that it got a string and expected an int.

+
+
+

Currently the connect function is realized by a Nim type safe macro. Connect accepts two or three +arguments — the widget, the signal name and the optional argument. When the optional argument +is a ref (reference to objects on the heap) then it is passed as a reference, otherwise a deep copy +of the argument is passed. For the above code this means, that r and the window variables are passed +as references, while the string and the stack object are deep copied. Currently it is not possible +to release the memory of passed arguments again. This should be no real problem, as in most +cases no arguments are passed at all, and when arguments are passed, then they are general +small in size like plain numbers or strings, or maybe references to widgets which could not be freed +at all, as they are part of the GUI. Later we may add more variants of that connect macro.

+
+
+ + + + + +
+ + +Navigation can be hard for beginners. You may have basic knowledge of GTK and want +to build a GUI for your application. But how to find what you need. Well, we offer no separate +automatically generated API documentation currently, as that is not really helpful. In most cases +it is easy to just guess Nim symbol names, proc parameters and all that. Using a smart editor +with good nimsuggest support further supports navigation — for example NEd shows us +all the needed proc parameters when we move the cursor on a proc name, or we press Ctrl+W and jump +to the definition of that symbol. For unknown stuff the original C function name is often a good starting point. +Assume you don’t know much about GTK’s buttons, but you know that you want to have a button in +your GUI application. GTK generally offers generator functions containing the string new in their name. +So it is easy to guess that there exists a C function named gtk_button_new. That name is also +contained in the bindings files, in this case in gtk.nim. So we open that file in a text editor and search for +that term. So it is really easy to find first starting points for related procs and data types. Most data types +are located near by their related functions, so you should be able to find all relevant information fast. +Remember the GTK devhelp tool, and use also grep or the nimgrep variant. +
+
+
+
+
+

Extending or sub-classing Widgets

+
+
+

I may occur that we want to attach additional information to GTK widgets +by extending or subclassing them. Doing this is supported +by providing for each widget class not only a corresponding new() proc which returns +the newly created widget, but also +a init() proc, which gets an uninitialized variable of the (extended) widget type as argument and +initializes that variable with a newly created +GTK widget . Initializing the added fields is +done separately by the user. The following code shows a GTK button, which is +extended with a counter member field. That counter is decreased for +each button click. The amount of decrease (5) is passed to the callback as a int parameter.

+
+
+
count_button.nim
+
+
# nim c count_button.nim
+import gintro/[gtk, glib, gobject, gio]
+
+type
+  CountButton = ref object of Button
+    counter: int
+
+proc buttonClicked (button: CountButton; decrement: int) =
+  dec(button.counter, decrement)
+  button.label = "Counter: " & $button.counter
+  echo "Counter is now: ", button.counter
+
+proc appActivate (app: Application) =
+  var button: CountButton
+  let window = newApplicationWindow(app)
+  window.title = "Count Button"
+  initButton(button, "Counting down from 100 by 5")
+  button.counter = 100
+  window.add(button)
+  button.connect("clicked",  buttonClicked, 5)
+  window.showAll
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard app.run
+
+main()
+
+
+
+

In this example we have to define our new widget type first, then we have to +declare a variable of that type and pass that variable to the init() proc.

+
+
+
+
+

CSS styles, GErrors and Exceptions

+
+
+
+NimGTK3Label +
+
+
+

Often GTK beginners ask how one can apply custom styles to GTK widgets, for example custom colors. +While in most cases the use of custom colors gives just ugly results, as the custom colors generally do +not match well with the default color scheme, it is good to know how we can do it. For GTK3 styles are +applied to widgets by using Cascading Style Sheets (CSS). You may find C example code similar to this:

+
+
+
label.c
+
+
// https://stackoverflow.com/questions/30791670/how-to-style-a-gtklabel-with-css
+// gcc `pkg-config gtk+-3.0 --cflags` test.c -o test `pkg-config --libs gtk+-3.0`
+#include <gtk/gtk.h>
+int main(int argc, char *argv[]) {
+    gtk_init(&argc, &argv);
+    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+    GtkWidget *label = gtk_label_new("Label");
+    GtkCssProvider *cssProvider = gtk_css_provider_new();
+    char *data = "label {color: green;}";
+    gtk_css_provider_load_from_data(cssProvider, data, -1, NULL);
+    gtk_style_context_add_provider(gtk_widget_get_style_context(window),
+                                   GTK_STYLE_PROVIDER(cssProvider),
+                                   GTK_STYLE_PROVIDER_PRIORITY_USER);
+    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
+    gtk_container_add(GTK_CONTAINER(window), label);
+    gtk_widget_show_all(window);
+    gtk_main();
+}
+
+
+
+

Converting that to Nim is again straight forward:

+
+
+
label.nim
+
+
# nim c label.nim
+import gintro/[gtk, glib, gobject, gio]
+
+proc appActivate(app: Application) =
+  let window = newApplicationWindow(app)
+  let label = newLabel("Yellow text on green background")
+  let cssProvider = newCssProvider()
+  let data = "label {color: yellow; background: green;}"
+  #discard cssProvider.loadFromPath("doesnotexist")
+  discard cssProvider.loadFromData(data)
+  let styleContext = label.getStyleContext
+  assert styleContext != nil
+  addProvider(styleContext, cssProvider, STYLE_PROVIDER_PRIORITY_USER)
+  window.add(label)
+  showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+

For this example we create a plain label widget with some text. To colorize it, we generate a +CssProvider and load it with a textual description of our desired colors. Then we extract the +style context from the label and add our CssProvider to it.

+
+
+

The last parameter of the C function gtk_css_provider_load_from_data() is of type GError and can +be used in C code to detect runtime errors. The C code above just passes NULL to ignore this error. +For Nim we map that GError argument to exceptions. To test what happens in Nim when an GError would +report an error condition, you may uncomment function loadFromPath() in the code above. As the specified path +does not exist, we should get an exception with a message telling us the problem. Of course in your real +code you may catch such exceptions with Nim’s try: blocks. (You may also modify the data variable above to +an illegal CSS statement — if the statement is seriously wrong, then you should get an exception from +loadFromData().

+
+
+
+
+

GTK Builder — user interfaces created with the glade tool

+
+
+

As C code can be very verbose, some people prefer outsourcing the GUI layout +in XML files which can be created and modified with the glade GUI creator program. +For high level languages like Python or Nim the program source code is generally +short and clean, so that use of XML files may not have much benefit. But of course +we can use GTK builder from Nim. We follow the example from +https://developer.gnome.org/gtk3/stable/ch01s03.html +but we modify it to use the new GTK3 app style: For the XML file we have to change only +class="GtkWindow" into class="GtkApplicationWindow". Our Nim program has +the well known application shape, with one addition: We have to +explicitly set the application for the main window. Of course you can also +use the traditional program structure with Nim and Builder, for that case +you can straight follow the linked page or other examples. Here is the XML file and the Nim code:

+
+
+
builder.ui
+
+
<interface>
+  <object id="window" class="GtkApplicationWindow">
+    <property name="visible">True</property>
+    <property name="title">Grid</property>
+    <property name="border-width">10</property>
+    <child>
+      <object id="grid" class="GtkGrid">
+        <property name="visible">True</property>
+        <child>
+          <object id="button1" class="GtkButton">
+            <property name="visible">True</property>
+            <property name="label">Button 1</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object id="button2" class="GtkButton">
+            <property name="visible">True</property>
+            <property name="label">Button 2</property>
+          </object>
+          <packing>
+            <property name="left-attach">1</property>
+            <property name="top-attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object id="quit" class="GtkButton">
+            <property name="visible">True</property>
+            <property name="label">Quit</property>
+          </object>
+          <packing>
+            <property name="left-attach">0</property>
+            <property name="top-attach">1</property>
+            <property name="width">2</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+      </packing>
+    </child>
+  </object>
+</interface>
+
+
+
+
builder.nim
+
+
 https://developer.gnome.org/gtk3/stable/ch01s03.html
+# builder.nim -- application style example using builder/glade xml file for user interface
+# nim c builder.nim
+import gintro/[gtk, glib, gobject, gio]
+
+proc hello(b: Button; msg: string) =
+  echo "Hello", msg
+
+proc quitApp(b: Button; app: Application) =
+  echo "Bye"
+  quit(app)
+
+proc appActivate(app: Application) =
+  let builder = newBuilder()
+  discard builder.addFromFile("builder.ui")
+  let window = builder.getApplicationWindow("window")
+  window.setApplication(app)
+  var button = builder.getButton("button1")
+  button.connect("clicked", hello, "")
+  button = builder.getButton("button2")
+  button.connect("clicked", hello, " again...")
+  button = builder.getButton("quit")
+  button.connect("clicked", quitApp, app)
+  #showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+

For each builder component gintro provides a typesafe access proc like +getApplicationWindow() and getButton() in this example.

+
+
+

Generally it is possible to use resource files merged with the executable program +instead of an external XML files, we have to investigate how we can do that in Nim. +And it may be possible to connect the signal handlers to handler procs from within +the XML file — this is also work in progress…​

+
+
+
+
+

GAction

+
+
+

GAction represents a single named action and is for GTK3 the prefered way to do +user interactions. GAction works with button, menus and keyboard shortcuts.

+
+
+

The following example is based on

+
+ +
+
gaction.nim
+
+
# https://wiki.gnome.org/HowDoI/GAction
+# nim c gaction.nim
+import gintro/[gtk, glib, gobject, gio]
+
+proc saveCb(action: SimpleAction; v: Variant) =
+  echo "saveCb"
+
+proc appActivate(app: Application) =
+  let window = newApplicationWindow(app)
+  let action = newSimpleAction("save")
+  discard action.connect("activate", saveCB)
+  window.actionMap.addAction(action)
+  let button = newButton()
+  button.label = "Save"
+  window.add(button)
+  button.setActionName("win.save")
+  setAccelsForAction(app, "win.save", "<Control><Shift>S")
+  showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+

GtkApplicationWindow provides an interface to GActionMap. As +the interface itself and the interface provider are defined in different modules, +automatic conversion is not possible, so we have to convert the ApplicationWindow +to ActionMap. (We could use a converter to do the conversion for us, but as +these conversions are rare, and because gintro use no converters at all still, we use +an explicit proc.) The use of cstringArray as third parameter for proc setAccelsForAction() +is a bit ugly, we have to fix that later.

+
+
+
+
+

GMenu with GActions

+
+
+

The following example shows how we can define GActions and bind them to Menus, Buttons +and Keyboard shortcuts. Examples for stateless actions (quit), for toggle actions (spellcheck) +and for statefull actions (text justify) are provided.

+
+
+

Note that the following code is not a direct translation of an existing example, but +a collections of informations from various sources, so +it may contain bugs or not fully optimal code.

+
+ +
+

We can easily modify the above example to get the more modern look with +a HeaderBar and the "Gears" MenuButtons:

+
+
+
gearsmenu.nim
+
+
# https://developer.gnome.org/glib/stable/glib-GVariant.html
+# https://developer.gnome.org/glib/stable/glib-GVariantType.html
+# https://wiki.gnome.org/HowDoI/GMenu
+# https://wiki.gnome.org/HowDoI/GAction
+# https://developer.gnome.org/gnome-devel-demos/stable/menubutton.c.html.en
+# nim c gearsmenu.nim
+import gintro/[gtk, glib, gobject, gio]
+import strformat
+
+# https://github.com/GNOME/glib/blob/master/gio/tests/gapplication-example-actions.c
+proc activateToggleAction(action: SimpleAction; parameter: Variant; app: Application) =
+  app.hold # hold/release taken over from C example, there may be reasons...
+  block:
+    echo fmt"action {action.name} activated"
+    let state: Variant = action.state
+    let b = state.getBoolean
+    action.state = newVariantBoolean(not b)
+    echo fmt"state change {b} -> {not b}"
+  app.release
+
+proc activateStatefulAction(action: SimpleAction; parameter: Variant; app: Application) =
+  app.hold
+  block:
+    echo fmt"action {action.name} activated"
+    let state: Variant = action.state
+    var l: uint64
+    let oldState = state.getString(l) # yes uint64 parameter is a bit ugly
+    let newState = parameter.getString(l)
+    action.state = newVariantString(newState)
+    echo fmt"state change {oldState} -> {newState}"
+  app.release
+
+proc quitProgram(action: SimpleAction; parameter: Variant; app: Application) =
+  quit(app)
+
+proc appStartup(app: Application) =
+  echo "appStartup"
+  let quit = newSimpleAction("quit") # here we create the actions for whole app
+  connect(quit, "activate", quitProgram, app)
+  app.addAction(quit)
+
+proc appActivate(app: Application) =
+  echo "appActivate"
+  let window = newApplicationWindow(app)
+  # window.title = "GTK3 App with Headerbar and Gears Menu" # unused due to HeaderBar
+  window.defaultSize = (500, 200)
+  window.position = WindowPosition.center
+
+  let menu = gio.newMenu() # root of all menus
+  block: # plain stateless menu
+    let subMenu = gio.newMenu()
+    menu.appendSubMenu("Application", submenu)
+    # let section = gio.newMenu() # no separating section needed here
+    # submenu.appendSection(nil, section)
+    # section.append("Quit", "app.quit")
+    submenu.append("Quit", "app.quit")
+
+  block: #stateful menu with radio items
+    let subMenu = gio.newMenu()
+    menu.appendSubMenu("Layout", submenu)
+    let subMenu2 = gio.newMenu()
+    submenu.appendSubMenu("justify", submenu2)
+    let section = gio.newMenu()
+    submenu2.appendSection(nil, section)
+    section.append("left", "win.justify::left")
+    section.append("center", "win.justify::center")
+    section.append("right", "win.justify::right")
+
+  block: # and finally a toggle menu
+    let subMenu = gio.newMenu()
+    menu.appendSubMenu("Spelling", submenu)
+    let section = gio.newMenu()
+    submenu.appendSection(nil, section)
+    section.append("Check", "win.toggleSpellCheck")
+
+  let headerBar = newHeaderBar()
+  headerBar.setShowCloseButton
+  headerBar.setTitle("Title")
+  headerBar.setSubtitle("Subtitle")
+  window.setTitlebar (headerBar)
+
+  let menubar = newMenuButton()
+  # menubar.setDirection(ArrowType.none) # show the gears Icon
+  # let image = newImageFromIconName("open-menu-symbolic", IconSize.menu.ord)
+  let image = newImageFromIconName("document-save", IconSize.dialog.ord) # dialog is really big!
+  menubar.setImage(image) # this is only an example for a custom image
+  # menubar.setIconName("open-menu-symbolic") # only gtk4
+  headerBar.packEnd(menubar)
+  menubar.setMenuModel(menu)
+
+  block: # creat the window related actions
+    let v = newVariantBoolean(true)
+    let spellCheck = newSimpleActionStateful("toggleSpellCheck", nil, v)
+    connect(spellCheck, "activate", activateToggleAction, app)
+    window.actionMap.addAction(spellCheck)
+  block:
+    let v = newVariantString("left") # default value and
+    let vt = newVariantType("s") # string (value type)
+    let justifyAction = newSimpleActionStateful("justify", vt, v)
+    connect(justifyAction, "activate", activateStatefulAction, app)
+    window.actionMap.addAction(justifyAction)
+  let button = newButton()
+  button.label = "Justify Center"
+  button.setDetailedActionName("win.justify::center")
+  #button.setActionName("app.quit") # for a stateless action
+  setAccelsForAction(app, "win.justify::right", "<Control><Shift>R")
+  window.add(button)
+  showAll(window)
+
+proc main =
+  let app = newApplication("app.example")
+  connect(app, "startup", appStartup)
+  connect(app, "activate", appActivate)
+  echo fmt"GTK Version {majorVersion()}.{minorVersion()}.{microVersion()}"
+  let status = run(app)
+  quit(status)
+
+main()
+
+
+
+

While in the previous example we create only a single menu instance in proc appStartup() +for all of our application windows, here we create a new menu for all of our instances +in proc appActivate(). That seems to work fine, so I assume it is correct.

+
+
+
+
+

GSettings

+
+
+

GSettings provides a convenient way to permanently storing configuration data, +and to bind them to properties of widgets.

+
+ +
+

For using GSettings in our own programs, we have first to create a XML file +which defines names and type of each configuration entry, and additional +provides default value and a description. The file name of such xml files +must always end with ".gschema.xml". +The following example has only one +field called like-nim of type boolean (b). For a real application program +we would install the configuration on our computer — unfortunately we +would need root access for this. We could do it this way:

+
+
+
+
# For making gsettings available system wide one method is, as root
+# https://developer.gnome.org/gio/stable/glib-compile-schemas.html
+# echo $XDG_DATA_DIRS
+# /usr/share/gnome:/usr/local/share:/usr/share:/usr/share/gdm
+# cd /usr/local/share/glib-2.0/schemas
+# cp test.gschema.xml .
+# glib-compile-schemas .
+#
+
+
+
+

For testing there is an easier method available:

+
+
+

Create a directory and copy the xml file and the test program below into it.

+
+
+

Then do, as ordinary user:

+
+
+
+
glib-compile-schemas .
+nim c gsettings.nim
+GSETTINGS_SCHEMA_DIR="." ./gsettings
+
+
+
+

This is the xml file and the test program:

+
+
+
test.gschema.xml
+
+
<schemalist>
+  <schema path="/org/gnome/recipes/"
+         id="org.gnome.Recipes">
+    <key type="b" name="like-nim">
+      <default>false</default>
+      <summary>I like Nim</summary>
+      <description>
+        I like or like not
+        the Nim programming language.
+      </description>
+    </key>
+  </schema>
+</schemalist>
+
+
+
+
gsettings.nim
+
+
# gsettings.nim -- basic use of gsettings
+# nim c gsettings.nim
+# https://blog.gtk.org/2017/05/01/first-steps-with-gsettings/
+# https://mail.gnome.org/archives/gtk-list/2016-December/msg00003.html
+import gintro/[gtk, glib, gobject, gio]
+
+# unused
+proc toggle(b: CheckButton) =
+  echo b.active
+  let s = newSettings("org.gnome.Recipes")
+  discard s.setBoolean("like-nim", b.active)
+
+proc appActivate(app: Application) =
+  let window = newApplicationWindow(app)
+  window.title = "GTK3, Nim and GSettings"
+  window.defaultSize = (200, 200)
+  let b = newCheckButton()
+  b.halign = Align.center
+  b.label = "I like Nim"
+  #b.connect("toggled", toggle) # we don't need this for plain binding!
+  let s = newSettings("org.gnome.Recipes")
+  if s.getBoolean("like-nim"):
+    echo "I like Nim language"
+  `bind`(s, "like-nim", b, "active", {SettingsBindFlag.get, SettingsBindFlag.set})
+  window.add(b)
+  showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+

The command "glib-compile-schemas ." compiles all schemas in the current directory. And +"GSETTINGS_SCHEMA_DIR="." ./gsettings" launches our test program with the environment +variable GSETTINGS_SCHEMA_DIR pointing to the current directory, containing the compiled schema.

+
+
+

Note that a system tool with same name as our test program exists — that one can be used +to get or set configuration data — for example you may query the current state of field +"like-nim" with

+
+
+
+
gsettings --schemadir "." get org.gnome.Recipes like-nim
+
+
+
+

Or test program first creates a window with a check button. Then our settings file is +opened and we print the current value of the boolean variable. After that the +bind procedure binds the active property (checkmark state) of our widget to the +"like-nim" entry of our settings file. The result of this binding is, that +our checkmark state is automatically made persistent, that is when we terminate +and restart our test program, the checkmark will have the last state again.

+
+
+

These bindings works for booleans, integers, floats, strings. The type of the property of the +widget must be identical with the corresponding type of the entry in the settings xml file.

+
+
+

On Linux you may permanently set the gsetting directory by adding the statement

+
+
+
+
export GSETTINGS_SCHEMA_DIR="pathToMyProg"
+
+
+
+

to your .bashrc file — of course after replacing pathToMyProg with the actual path.

+
+
+

For more informations about gsettings see

+
+ + +
+
+
+

Drawing with Cairo graphics library

+
+
+

The next example shows how we can use the cairo graphics library for drawing on a DrawingArea widget, +and at the same time uses glib timeoutAdd() function to create a timer which periodically calls the +drawing function to create some animations. The code is based on a recent post to the cairo mailing list +and shows a sine wave which is continuously moving to the left.

+
+
+ + + + + +
+ + +The gobject-introspection generated cairo module was only a minimal stub, because cairo +library does not really support introspection. Now we are using a cairo module which is generated +directly from the cairo C header files with the tool c2nim and then modified to support a high level +API. +
+
+
+
cairo_anim.nim
+
+
# https://lists.cairographics.org/archives/cairo/2016-October/027791.html
+# Nim version of that plain cairo animation example
+
+import gintro/[gtk, glib, gobject, gio, cairo]
+import math
+
+const
+  NumPoints = 1000
+  Period = 100.0
+
+proc invalidateCb(w: Widget): bool =
+  queueDraw(w)
+  return SOURCE_CONTINUE
+
+proc sineToPoint(x, width, height: int): float =
+  math.sin(x.float * math.TAU / Period) * height.float * 0.5 + height.float * 0.5
+
+proc drawingAreaDrawCb(widget: DrawingArea; context: Context): bool =
+  var redrawNumber {.global.} : int
+  let width = getAllocatedWidth(widget)
+  let height = getAllocatedHeight(widget)
+  for i in 1 ..< NumPoints:
+    context.lineTo(i.float , sineToPoint(i + redrawNumber, width, height))
+  context.stroke
+  inc(redrawNumber)
+  return true # TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further.
+
+proc appActivate(app: Application) =
+  let window = newApplicationWindow(app)
+  window.title = "Drawing example"
+  window.defaultSize = (400, 400)
+  let drawingArea = newDrawingArea()
+  window.add(drawingArea)
+  showAll(window)
+  discard timeoutAdd(1000 div 60, invalidateCb, drawingArea)
+  connect(drawingArea, "draw", drawingAreaDrawCb)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+
+
+

A simple ListView example

+
+
+
+NimGTK3ListView +
+
+
+

Recently someone reported about some problems porting a GTK2 application to Nim GTK3, so I will give a small example +which may help using ListViews and TreeViews. These two widget types are the most complicated widget types in GTK — I can remember that I had some trouble myself when I used Ruby-GTK some years ago. As I can currently not remember +details about use of ListView widgets, I decided to take an example code from zetcode.com as starting point. Of course +porting is straight forward, but when I tried to compile the result I noticed some bugs and restrictions of current +gintro package. Of course not really surprising, as the package is not really tested yet. I will try to fix these bugs later. +First problem is, that we store a ListStore as model in our TreeView, and we need to extract that ListStore from the TreeView +for some operations. But module gtk.nim offers currently only a function to extract the model itself, which is of type TreeModel. +In the C code an upcast is used to get the ListStore from the retrieved TreeModel. To avoid casting in our Nim code, I have just copied +the getModel() proc and modified it to return a ListStore. Second problem was, that module gio export a ListStore datatype also. +To avoid prefixing all ListStore types with gtk prefix, I excluded gio.ListStore from import list. And finally a real bug: +Proc newListStore() expects currently a plain pointer as last parameter, while we know that it should be the address of a list of GTypes. +So we have to use an ugly cast for now. For populating the ListStore currently GValues are used. That is not very convenient, and +for that we need the correct GType of our string list. In C one would use the macro G_TYPE_STRING, which is not provided by +gobject-introspection. So we use typeFromName() to get the correct GType, which works fine when we know that the string name is "gchararray". +Later we will provide a higher level function for this process.

+
+
+

I will try to give more and better explained ListView and TreeView examples later…​

+
+
+
listview.nim
+
+
# http://zetcode.com/gui/gtk2/gtktreeview/
+# dynamiclistview.c
+
+import gintro/[glib, gobject, gtk]
+import gintro/gio except ListStore
+
+const
+  LIST_ITEM = 0
+  N_COLUMNS = 1
+
+var list: TreeView
+
+# this is copied from gtk.nim
+#proc getModel*(self: TreeView): TreeModel =
+#  new(result)
+#  result.impl = gtk_tree_view_get_model(cast[ptr TreeView00](self.impl))
+
+proc getListStore(self: TreeView): ListStore =
+  new(result)
+  result.impl = gtk_tree_view_get_model(cast[ptr TreeView00](self.impl))
+
+proc appendItem(widget: Button; entry: Entry) =
+  var
+    val: Value
+    iter: TreeIter
+  let store = getListStore(list)
+  let gtype = typeFromName("gchararray")
+  discard gValueInit(val, gtype)
+  gValueSetString(val, entry.text)
+  store.append(iter)
+  store.setValue(iter, LIST_ITEM, val)
+  entry.text = ""
+
+proc removeItem(widget: Button; selection: TreeSelection) =
+  var
+    ls: ListStore
+    iter: TreeIter
+  let store = getListStore(list)
+  if not store.getIterFirst(iter):
+      return
+  if getSelected(selection, ls, iter):
+    discard store.remove(iter)
+
+proc onRemoveAll(widget: Button; selection: TreeSelection) =
+  var
+    iter: TreeIter
+  let store = getListStore(list)
+  if not store.getIterFirst(iter):
+    return
+  clear(store)
+
+proc initList(list: TreeView) =
+  let renderer = newCellRendererText()
+  let column = newTreeViewColumn()
+  column.title = "List Item"
+  column.packStart(renderer, true)
+  column.addAttribute(renderer, "text", LIST_ITEM)
+  discard list.appendColumn(column)
+  let gtype = typeFromName("gchararray")
+  let store = newListStore(N_COLUMNS, cast[pointer]( unsafeaddr gtype)) # cast due to bug in gtk.nim
+  list.setModel(store)
+
+proc appActivate(app: Application) =
+  let
+    window = newApplicationWindow(app)
+    sw = newScrolledWindow()
+    hbox = newBox(Orientation.horizontal, 5)
+    vbox = newBox(Orientation.vertical, 0)
+    add = newButton("Add")
+    remove = newButton("Remove")
+    removeAll = newButton("Remove All")
+    entry = newEntry()
+  window. title = "List view"
+  window.position = WindowPosition.center
+  window.borderWidth = 10
+  window.setSizeRequest(370, 270)
+  list = newTreeView()
+  sw.add(list)
+  sw.setPolicy(PolicyType.automatic, PolicyType.automatic)
+  sw.setShadowType(ShadowType.etchedIn)
+  list.setHeadersVisible(false)
+  vbox.packStart(sw, true, true, 5)
+  entry.setSizeRequest(120, -1)
+  hbox.packStart(add, false, true, 3)
+  hbox.packStart(entry, false, true, 3)
+  hbox.packStart(remove, false, true, 3)
+  hbox.packStart(removeAll, false, true, 3)
+  vbox.packStart(hbox, false, true, 3)
+  window.add(vbox)
+  initList(list)
+  let selection = getSelection(list)
+  connect(add, "clicked", listview.appendItem, entry)
+  connect(remove, "clicked", listview.removeItem, selection)
+  connect(removeAll, "clicked", listview.onRemoveAll, selection)
+  showAll(window)
+
+proc main =
+  let app = newApplication("org.gtk.example")
+  connect(app, "activate", appActivate)
+  discard run(app)
+
+main()
+
+
+
+
+
+

A ListView example with CSS styling

+
+
+

Recently C. Eric Cashon provided this example at https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/3

+
+
+

I will show his original code here too, so we can compare it better with the Nim version. +We see that Nim code has currently some disadvantages still, for example we have no +varargs procs implemented, so setting of properties and attributes is done using GValues, +which is typesafe, but not really compact. That is not too bad, but we may consider +creating macros to support a more dense, but still typesafe way similar to C’s varargs functions.

+
+
+
cell_color1.c
+
+
// gcc -Wall cell_color1.c -o cell_color1 `pkg-config --cflags --libs gtk+-3.0`
+// https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/4
+// C. Eric Cashon
+
+#include<gtk/gtk.h>
+
+enum
+{
+   ID,
+   PROGRAM,
+   COLOR1,
+   COLOR2,
+   COLUMNS
+};
+
+int main(int argc, char *argv[])
+  {
+    gtk_init(&argc, &argv);
+
+    GtkWidget *window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
+    gtk_window_set_title(GTK_WINDOW(window), "Select Cell");
+    gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
+    gtk_window_set_default_size(GTK_WINDOW(window), 500, 500);
+    gtk_container_set_border_width(GTK_CONTAINER(window), 20);
+    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
+
+    GtkTreeIter iter;
+    GtkListStore *store=gtk_list_store_new(COLUMNS, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 0, PROGRAM, "Gedit", COLOR1, "DarkCyan", COLOR2, "cyan", -1);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 1, PROGRAM, "Gimp", COLOR1,  "LightSlateGray", COLOR2, "cyan", -1);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 2, PROGRAM, "Inkscape", COLOR1, "DarkCyan", COLOR2, "cyan", -1);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 3, PROGRAM, "Firefox", COLOR1, "LightSlateGray", COLOR2, "cyan", -1);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 4, PROGRAM, "Calculator", COLOR1, "DarkCyan", COLOR2, "cyan", -1);
+    gtk_list_store_append(store, &iter);
+    gtk_list_store_set(store, &iter, ID, 5, PROGRAM, "Devhelp", COLOR1, "LightSlateGray", COLOR2, "cyan", -1);
+
+    GtkWidget *tree=gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
+    gtk_widget_set_hexpand(tree, TRUE);
+    gtk_widget_set_vexpand(tree, TRUE);
+    g_object_set(tree, "activate-on-single-click", TRUE, NULL);
+
+    GtkTreeSelection *selection=gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
+    gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
+
+    GtkCellRenderer *renderer1=gtk_cell_renderer_text_new();
+    g_object_set(renderer1, "editable", FALSE, NULL);
+
+    GtkCellRenderer *renderer2=gtk_cell_renderer_text_new();
+    g_object_set(renderer2, "editable", TRUE, NULL);
+
+    //Bind the COLOR column to the "cell-background" property.
+    GtkTreeViewColumn *column1=gtk_tree_view_column_new_with_attributes("ID", renderer1, "text", ID, "cell-background", COLOR1, NULL);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column1);
+    GtkTreeViewColumn *column2 = gtk_tree_view_column_new_with_attributes("Program", renderer2, "text", PROGRAM, "cell-background", COLOR2, NULL);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column2);
+
+    GtkWidget *grid=gtk_grid_new();
+    gtk_grid_attach(GTK_GRID(grid), tree, 0, 0, 1, 1);
+
+    gtk_container_add(GTK_CONTAINER(window), grid);
+
+    gchar *css_string=g_strdup("treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); color: rgba(0,0,255,1.0);}");
+    GError *css_error=NULL;
+    GtkCssProvider *provider=gtk_css_provider_new();
+    gtk_css_provider_load_from_data(provider, css_string, -1, &css_error);
+    gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+    if(css_error!=NULL)
+      {
+        g_print("CSS loader error %s\n", css_error->message);
+        g_error_free(css_error);
+      }
+    g_object_unref(provider);
+    g_free(css_string);
+
+    gtk_widget_show_all(window);
+
+    gtk_main();
+    return 0;
+  }
+
+
+
+

And this is the Nim version, created with c2nim and some manual tuning:

+
+
+
css_colored_listview.nim
+
+
# nim c css_colored_listview.nim
+import gintro/[gtk, glib, gobject]
+import gintro/gdk except Window # there is a problem with gdk.Window -- we have to investigate!
+const # maybe we should use Nim's enum here?
+  Id = 0
+  Program = 1
+  Color1 = 2
+  Color2 = 3
+  Columns = 4
+
+proc bye(w: Window) =
+  mainQuit()
+  echo "Bye..."
+
+proc toStringVal(s: string): Value =
+  let gtype = typeFromName("gchararray")
+  discard init(result, gtype)
+  setString(result, s)
+
+proc toUIntVal(i: int): Value =
+  let gtype = typeFromName("guint")
+  discard init(result, gtype)
+  setUint(result, i)
+
+proc toBoolVal(b: bool): Value =
+  let gtype = typeFromName("gboolean")
+  discard init(result, gtype)
+  setBoolean(result, b)
+
+# we need the following two procs for now -- later we will not use that ugly cast...
+proc typeTest(o: gobject.Object; s: string): bool =
+  let gt = g_type_from_name(s)
+  return g_type_check_instance_is_a(cast[ptr TypeInstance00](o.impl), gt).toBool
+
+proc listStore(o: gobject.Object): gtk.ListStore =
+  assert(typeTest(o, "GtkListStore"))
+  cast[gtk.ListStore](o)
+
+proc updateRow(renderer: CellRendererText; path: cstring; newText: cstring; tree: TreeView) =
+  var iter: TreeIter
+  var value: Value
+  let gtype = typeFromName("gchararray")
+  discard init(value, gtype)
+  let store = listStore(tree.getModel())
+  value.setString(newText)
+  let treePath = newTreePathFromString(path)
+  discard store.getIter(iter, treePath)
+  store.setValue(iter, 1, value)
+
+# we use the old gtk style with init() as is used in the C original -- maybe better use modern app sytle
+proc main() =
+  gtk.init()
+  let window = newWindow()
+  window.title = "Select Cell"
+  window.position = WindowPosition.center
+  window.defaultSize = (500, 500)
+  window.borderWidth = 20
+  connect(window, "destroy", bye)
+  var iter: TreeIter
+  var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"),
+    typeFromName("gchararray")]
+  var store = newListStore(Columns,  cast[pointer]( unsafeaddr h)) # cast is ugly, we should fix it in bindings.
+  let progNames = ["Gedit", "Gimp", "Inkscape", "Firefox", "Calculator", "Devhelp"]
+  for i, n in progNames:
+    store.append(iter) # currently we have to use setValue() as there is no varargs proc as in C original
+    store.setValue(iter, Id, toUIntVal(i))
+    store.setValue(iter, Program, toStringVal(n))
+    store.setValue(iter, Color1, toStringVal(if (i and 1) != 0: "LightSlateGray" else: "DarkCyan"))
+    store.setValue(iter, Color2, toStringVal("cyan"))
+  var tree  = newTreeViewWithModel(store)
+  tree.setHexpand
+  tree.setVexpand
+  setProperty(tree, "activate-on-single-click", toBoolVal(true))
+  var selection = tree.getSelection()
+  selection.setMode(SelectionMode.single)
+  var renderer1 = newCellRendererText()
+  setProperty(renderer1, "editable", toBoolVal(false))
+  var renderer2 = newCellRendererText()
+  setProperty(renderer2, "editable", toBoolVal(true))
+  connect(renderer2, "edited", updateRow, tree)
+  ## Bind the Color column to the "cell-background" property.
+  var column1 = newTreeViewColumn()
+  column1.setTitle("ID")
+  column1.packStart(renderer1, true)
+  column1.addAttribute(renderer1, "text", Id)
+  column1.addAttribute(renderer1, "cell-background", Color1)
+  discard tree.appendColumn(column1)
+  var column2  = newTreeViewColumn()
+  column1.setTitle("Program")
+  column1.packStart(renderer2, true)
+  column1.addAttribute(renderer2, "text", Program)
+  column1.addAttribute(renderer2, "cell-background", Color2)
+  discard tree.appendColumn(column2)
+  var grid = newGrid() # only one occupied cell makes no sense -- but so we can add more widgets later
+  grid.attach(tree, 0, 0, 1, 1)
+  window.add(grid)
+  const cssString = # note: big font selected intentionally
+    """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color:
+    rgba(255,255,0,1.0); color: rgba(0,0,255,1.0);}"""
+  var provider  = newCssProvider()
+  discard provider.loadFromData(cssString)
+  addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION)
+  window.showAll
+  gtk.main()
+
+main()
+
+
+
+

When you compile with nim c -d:release -d:danger --passC:-flto css_colored_listview.nim +you will get an executable size of 80k, which is big compared with the 20k of the C version, but +not too bad. You may note that I have added the updateRow() proc, which is necessary to +make editing the program name entries permanent. That proc needs cstring parametes, which +may be surprising, as we generally use Nim strings. Not a big problem, maybe intended, we may have to +check the connect() macro in gimpl.nim.

+
+
+
+
+

A more advanced example for cairo drawing with zooming, panning, scrolling

+
+
+

The following code is a plain Nim version of a drawing demo which I wrote some years ago in Ruby (http://ssalewski.de/PetEd-Demo.html.en). +Cairo surface is currently manually freed, because GC may have a too large delay.

+
+
+

You can resize the window and zoom in with the mouse wheel. When zoomed in scroll bars appear. You +can hold the middle mouse button pressed while moving the mouse for panning, and you can press left mouse button +and move the mouse to first draw a selection rectangle and zoom into it when releasing the mouse button.

+
+
+

In the examples directory there is also a simplified version called simpledrawingarea.nim which does all +the drawings in the draw callback, without using a buffering surface. This is generally preferable for +plain applications.

+
+
+
drawingarea.nim
+
+
# Plain demo for zooming, panning, scrolling with GTK DrawingArea
+# (c) S. Salewski, 21-DEC-2010 (initial Ruby version)
+# Nim version April 2019
+# License MIT
+
+# This version of the demo program uses a separate proc paint()
+# which allocates a custom surface for buffered drawing.
+# That may be not really necessary, for simple drawings doing all
+# the drawing in the "draw" call back is easier and faster. But for
+# more complicated drawing operations, for example when using a
+# background grid, which is a bit larger than the window size and
+# is reused when scrolling, a custom surface may be useful.
+# And finally that custom surface and custom cairo context is an
+# important test for the language bindings.
+
+# https://discourse.gnome.org/t/problem-with-gtkscrollbar-gtk-window-resize-and-gtk-adjustment-set-value/1081
+
+import gintro/[gtk, gdk, glib, gobject, gio, cairo]
+
+const
+  ZoomFactorMouseWheel = 1.1
+  ZoomFactorSelectMax = 10 # ignore zooming in tiny selection
+  ZoomNearMousepointer = true # mouse wheel zooming -- to mouse-pointer or center
+  SelectRectCol = [0.0, 0, 1, 0.5] # blue with transparency
+
+discard """
+Zooming, scrolling, panning...
+
+|-------------------------|
+|<-------- A ------------>|
+|                         |
+|  |---------------|      |
+|  | <---- a ----->|      |
+|  |    visible    |      |
+|  |---------------|      |
+|                         |
+|                         |
+|-------------------------|
+
+a is the visible, zoomed in area == darea.allocatedWidth
+A is the total data range
+A/a == userZoom >= 1
+For horizontal adjustment we use
+hadjustment.setUpper(darea.allocatedWidth * userZoom) == A
+hadjustment.setPageSize(darea.allocatedWidth) == a
+So hadjustment.value == left side of visible area
+
+Initially, we set userZoom = 1, scale our data to fit into darea.allocatedWidth
+and translate the origin of our data to (0, 0)
+
+Zooming: Mouse wheel or selecting a rectangle with left mouse button pressed
+Scrolling: Scrollbars
+Panning: Moving mouse while middle mouse button pressed
+"""
+
+# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area)
+
+type
+  PosAdj = ref object of Adjustment
+    handlerID: uint64
+
+proc newPosAdj: PosAdj =
+  initAdjustment(result, 0, 0, 1, 1, 10, 1)
+
+type
+  PDA_Data* = object
+    draw*: proc (cr: Context)
+    extents*: proc (): tuple[x, y, w, h: float]
+    windowSize*: tuple[w, h: int]
+
+type
+  PDA = ref object of Grid
+    zoomNearMousepointer: bool
+    selecting: bool
+    userZoom: float
+    surf: Surface
+    pattern: Pattern
+    cr: cairo.Context
+    darea: DrawingArea
+    hadjustment: PosAdj
+    vadjustment: PosAdj
+    hscrollbar: Scrollbar
+    vscrollbar: Scrollbar
+    fullScale: float
+    dataX: float
+    dataY: float
+    dataWidth: float
+    dataHeight: float
+    lastButtonDownPosX: float
+    lastButtonDownPosY: float
+    lastMousePosX: float
+    lastMousePosY: float
+    zoomRectX1: float
+    zoomRectY1: float
+    oldSizeX: int
+    oldSizeY: int
+    drawWorld: proc (cr: Context)
+    extents: proc (): tuple[x, y, w, h: float]
+
+proc drawingAreaDrawCb(darea: DrawingArea; cr: Context; this: PDA): bool =
+  if this.pattern.isNil: return
+  cr.setSource(this.pattern)
+  cr.paint
+  if this.selecting:
+    cr.rectangle(this.lastButtonDownPosX, this.lastButtonDownPosY,
+      this.zoomRectX1 - this.lastButtonDownPosX, this.zoomRectY1 - this.lastButtonDownPosY)
+    cr.setSource(0, 0, 1, 0.5) # SELECT_RECT_COL) # 0, 0, 1, 0.5
+    cr.fillPreserve
+    cr.setSource(0, 0, 0)
+    cr.setLineWidth(2)
+    cr.stroke
+  return gdk.EVENT_STOP # EVENT_PROPAGATE
+  #return true # TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further.
+
+# clamp to correct values, 0 <= value <= (adj.upper - adj.pageSize), block calling onAdjustmentEvent()
+proc updateVal(adj: PosAdj; d: float) =
+  adj.signalHandlerBlock(adj.handlerID)
+  adj.setValue(max(0.0, min(adj.value + d, adj.upper - adj.pageSize)))
+  adj.signalHandlerUnblock(adj.handlerID)
+
+proc updateAdjustments(this: PDA; dx, dy: float) =
+  this.hadjustment.setUpper(this.darea.allocatedWidth.float * this.userZoom)
+  this.vadjustment.setUpper(this.darea.allocatedHeight.float * this.userZoom)
+  this.hadjustment.setPageSize(this.darea.allocatedWidth.float)
+  this.vadjustment.setPageSize(this.darea.allocatedHeight.float)
+  updateVal(this.hadjustment, dx)
+  updateVal(this.vadjustment, dy)
+
+proc paint(this: PDA) =
+  # echo "paint"
+  this.cr.save
+  this.cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center
+    this.vadjustment.upper * 0.5 - this.vadjustment.value)
+  this.cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom)
+  this.cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5)
+  this.drawWorld(this.cr) # call the user provided drawing function
+  this.cr.restore
+
+proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure; this: PDA): bool =
+  (this.dataX, this.dataY, this.dataWidth,
+    this.dataHeight) = this.extents() # query user defined size
+  this.fullScale = min(this.darea.allocatedWidth.float / this.dataWidth,
+      this.darea.allocatedHeight.float / this.dataHeight)
+  if this.surf != nil:
+    destroy(this.surf) # manually destroy surface -- GC would do it for us, but GC is slow...
+  this.surf = this.darea.window.createSimilarSurface(Content.color,
+      this.darea.allocatedWidth, this.darea.allocatedHeight)
+  if this.pattern != nil:
+    patternDestroy(this.pattern)
+  if this.cr != nil:
+    destroy(this.cr)
+  this.pattern = patternCreateForSurface(this.surf) # pattern now owns the surface!
+  this.cr = newContext(this.surf) # this function references target!
+  this.paint
+  return gdk.EVENT_STOP
+
+proc hscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) =
+  pda.hadjustment.setUpper(r.width.float * pda.userZoom)
+  pda.hadjustment.setPageSize(r.width.float)
+  if pda.oldSizeX != 0: # this fix is not exact, as fullScale can ...
+    updateVal(pda.hadjustment, (r.width - pda.oldSizeX).float * 0.5)
+  pda.oldSizeX = r.width
+
+proc vscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) =
+  pda.vadjustment.setUpper(r.height.float * pda.userZoom)
+  pda.vadjustment.setPageSize(r.height.float)
+  if pda.oldSizeY != 0: # ... change when window is rezized. But it's good enough!
+    updateVal(pda.vadjustment, (r.height - pda.oldSizeY).float * 0.5)
+  pda.oldSizeY = r.height
+
+proc updateAdjustmentsAndPaint(this: PDA; dx, dy: float) =
+  this.updateAdjustments(dx, dy)
+  this.paint
+  this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight)
+
+# event coordinates to user space
+proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) =
+  ((eventX - this.hadjustment.upper * 0.5 + this.hadjustment.value) / (
+      this.fullScale * this.userZoom) + this.dataX + this.dataWidth * 0.5,
+   (eventY - this.vadjustment.upper * 0.5 + this.vadjustment.value) / (
+       this.fullScale * this.userZoom) + this.dataY + this.dataHeight * 0.5)
+
+proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool =
+  let state = getState(event)
+  let (x, y) = event.getCoords
+  if state.contains(button1): # selecting
+    this.selecting = true
+    this.zoomRectX1 = x
+    this.zoomRectY1 = y
+    this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight)
+  elif button2 in state: # panning
+    this.updateAdjustmentsAndPaint(this.lastMousePosX - x, this.lastMousePosY - y)
+  else:
+    return gdk.EVENT_PROPAGATE
+  this.lastMousePosX = x
+  this.lastMousePosY = y
+  return gdk.EVENT_STOP
+  #event.request # request more motion events ?
+
+# zooming with mouse wheel -- data near mouse pointer should not move if possible!
+# hadjustment.value + event.x is the position in our zoomed_in world, (userZoom / z0 - 1)
+# is the relative movement caused by zooming
+# In other words, this is the delta-move d of a point at position P from zooming:
+# d = newPos - P = P * scale - P = P * (z/z0) - P = P * (z/z0 - 1). We have to compensate for this d.
+proc scrollEvent(darea: DrawingArea; event: EventScroll; this: PDA): bool =
+  let z0 = this.userZoom
+  case getScrollDirection(event)
+  of ScrollDirection.up:
+    this.userZoom *= ZoomFactorMouseWheel
+  of ScrollDirection.down:
+    this.userZoom /= ZoomFactorMouseWheel
+    if this.userZoom < 1:
+      this.userZoom = 1
+  else:
+    return gdk.EVENT_PROPAGATE
+  if this.zoomNearMousepointer:
+    let (x, y) = event.getCoords
+    this.updateAdjustmentsAndPaint((this.hadjustment.value + x) * (this.userZoom / z0 - 1),
+      (this.vadjustment.value + y) * (this.userZoom / z0 - 1))
+  else: # zoom to center
+    this.updateAdjustmentsAndPaint((this.hadjustment.value +
+        this.darea.allocatedWidth.float * 0.5) * (this.userZoom / z0 - 1),
+        (this.vadjustment.value + this.darea.allocatedHeight.float * 0.5) * (this.userZoom / z0 - 1))
+  return gdk.EVENT_STOP
+
+proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
+  var (x, y) = event.getCoords
+  this.lastMousePosX = x
+  this.lastMousePosY = y
+  this.lastButtonDownPosX = x
+  this.lastButtonDownPosY = y
+  echo "buttonPressEvent", x, " ", y
+  (x, y) = this.getUserCoordinates(x, y)
+  echo "User coordinates: ", x, ' ', y, "\n" # to verify getUserCoordinates()
+  return gdk.EVENT_STOP
+
+# zoom into selected rectangle and center it
+# math: we first center the selection rectangle, and then compensate for translation due to scale
+proc buttonReleaseEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
+  let (x, y) = event.getCoords
+  let b = getButton(event)
+  if b == 1:
+    this.selecting = false
+    let z1 = min(this.darea.allocatedWidth.float / (this.lastButtonDownPosX - x).abs,
+      this.darea.allocatedHeight.float / (this.lastButtonDownPosY - y).abs)
+    if z1 < ZoomFactorSelectMax: # else selection rectangle will persist, we may output a message...
+      this.userZoom *= z1
+      this.updateAdjustmentsAndPaint(
+        ((x + this.lastButtonDownPosX) * z1 - this.darea.allocatedWidth.float) * 0.5 + this.hadjustment.value * (z1 - 1),
+        ((y + this.lastButtonDownPosY) * z1 - this.darea.allocatedHeight.float) * 0.5 + this.vadjustment.value * (z1 - 1))
+    return gdk.EVENT_STOP
+  return gdk.EVENT_PROPAGATE
+
+proc onAdjustmentEvent(this: PosAdj; pda: PDA) =
+  pda.paint
+  pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth, pda.darea.allocatedHeight)
+
+proc newPDA: PDA =
+  initGrid(result)
+  let da = newDrawingArea()
+  result.darea = da
+  da.setHExpand
+  da.setVExpand
+  da.connect("draw", drawingAreaDrawCb, result)
+  da.connect("configure-event", dareaConfigureCallback, result)
+  da.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease,
+      EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint})
+  da.connect("motion-notify-event", onMotion, result)
+  da.connect("scroll_event", scrollEvent, result)
+  da.connect("button_press_event", buttonPressEvent, result)
+  da.connect("button_release_event", buttonReleaseEvent, result)
+  result.zoomNearMousepointer = ZoomNearMousepointer # mouse wheel zooming
+  result.userZoom = 1.0
+  result.hadjustment = newPosAdj()
+  result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result)
+  result.vadjustment = newPosAdj()
+  result.vadjustment.handlerID = result.vadjustment.connect("value-changed", onAdjustmentEvent, result)
+  result.hscrollbar = newScrollbar(Orientation.horizontal, result.hadjustment)
+  result.vscrollbar = newScrollbar(Orientation.vertical, result.vadjustment)
+  result.hscrollbar.setHExpand
+  result.vscrollbar.setVExpand
+  result.hscrollbar.connect("size-allocate", hscrollbarSizeAllocateCallback, result)
+  result.vscrollbar.connect("size-allocate", vscrollbarSizeAllocateCallback, result)
+  result.attach(result.darea, 0, 0, 1, 1)
+  result.attach(result.vscrollbar, 1, 0, 1, 1)
+  result.attach(result.hscrollbar, 0, 1, 1, 1)
+
+proc appStartup(app: Application) =
+  echo "appStartup"
+
+proc appActivate(app: Application; initData: PDA_Data) =
+  let window = newApplicationWindow(app)
+  window.title = "Drawing example"
+  # window.defaultSize = initData.windowSize
+  window.defaultSize = (initData.windowSize[0], initData.windowSize[1])
+  let pda = newPDA()
+  pda.drawWorld = initData.draw
+  pda.extents = initData.extents
+  window.add(pda)
+  showAll(window)
+
+proc newDisplay*(initData: PDA_Data) =
+  let app = newApplication("org.gtk.example")
+  connect(app, "startup", appStartup)
+  connect(app, "activate", appActivate, initData)
+  discard run(app)
+
+when isMainModule:
+
+  const # arbitrary locations for our data
+    DataX = 150.0
+    DataY = 250.0
+    DataWidth = 200.0
+    DataHeight = 120.0
+
+  # we need two user defined functions -- one gives the extent of the graphics,
+  # and the other does the cairo drawing using a cairo context.
+
+  # bounding box of user data -- x, y, w, h -- top left corner, width, height
+  proc worldExtents(): (float, float, float, float) =
+    (DataX, DataY, DataWidth, DataHeight) # current extents of our user world
+
+  # draw to cairo context
+  proc drawWorld(cr: cairo.Context) =
+    cr.setSource(1, 1, 1)
+    cr.paint
+    cr.setSource(0, 0, 0)
+    cr.setLineWidth(2)
+    var i = 0.0
+    while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0:
+      cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i)
+      i += 10
+    cr.stroke
+
+  proc test =
+    let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600))
+    newDisplay(data)
+
+  test() # 337 lines
+
+
+
+

We can use this module as a library easily and get this simple drawing tool with full zoom and scroll support:

+
+
+
darea_test.nim
+
+
import gintro/cairo
+import drawingarea
+from math import PI
+
+proc extents(): (float, float, float, float) =
+  (0.0, 0.0, 100.0, 100.0) # ugly float literals
+
+# draw to cairo context
+proc draw(cr: cairo.Context) =
+  cr.setSource(1, 1, 1) # set background color and paint
+  cr.paint
+  cr.setSource(0, 0, 0) # forground color
+  cr.arc(20, 30, 10, 0, 5) # nearly a circle
+  cr.newSubPath # do not join the two arcs
+  cr.arc(70, 60, 20, 0, math.PI)
+  cr.stroke # finally do it
+
+proc main =
+  var data: PDA_Data
+  data.draw = draw
+  data.extents = extents
+  data.windowSize = (800, 600)
+  newDisplay(data)
+
+main()
+
+
+
+
+
+

One more cairo example

+
+
+

Recently Mr. C. Eric Cashon provided an example code for working with a large bitmap image. +His example writes the image to disk, loads it again and displays the image allowing +zooming and translation. As examples are rare in these days, and that example is not to large, +I used c2nim to convert it to Nim. Below is the code with a few manually fixes. Note, the +current shipped cairo.nim module contains an assert statement, which prevents running this example. +If you really intent running this code, you will have to fix that single line in cairo.nim. I +have to do some more fixes in the cairo module and may ship a new version eventually. This +example is really low level, as alloc() is used directly.

+
+
+
cairoImage.nim
+
+
# https://discourse.gnome.org/t/proper-zoom-pan-image-approach-for-large-images/1497/6
+# Nim version of the C example of C. Eric Cashon
+import gintro/[gtk, gobject, glib, cairo]
+from math import TAU
+import strutils
+
+const
+  Width = 5000
+  Height = 5000
+  CFormat = cairo.Format.argb32
+
+var
+  Key: cairo.UserDataKey
+  translateX: float
+  translateY: float
+  scale = 1.0
+  ## Store data from file.
+  bigSurfaceData*: ptr cuchar = nil
+
+proc translateXSpinChanged(spinButton: SpinButton; data: DrawingArea) =
+  translateX = spinButton.value
+  data.queueDraw
+
+proc translateYSpinChanged(spinButton: SpinButton; data: DrawingArea) =
+  translateY = spinButton.getValue
+  data.queueDraw
+
+proc scaleSpinChanged(spinButton: SpinButton; data: DrawingArea) =
+  scale = spinButton.value
+  data.queueDraw
+
+proc saveBigSurface =
+  ## Use gdk_cairo_surface_create_from_pixbuf() to read in a pixbuf. Try a test surface here.
+  let bigSurface = imageSurfaceCreate(CFormat, Width, Height)
+  let cr = newContext(bigSurface)
+  ## Paint the background.
+  cr.setSource(1, 1, 1)
+  cr.paint
+  ## Draw a circle.
+  cr.setSource(0, 0, 1)
+  cr.arc(250, 250, 50, 0, math.TAU)
+  cr.fill
+  ## Draw some test grid lines.
+  cr.setSource(0, 1, 0)
+  for i in countup(0, 4900, 100):
+    cr.moveTo(0, i.float)
+    cr.lineTo(5000, i.float)
+    cr.stroke
+  for i in countup(0, 4900, 100):
+    cr.moveTo(i.float, 0)
+    cr.lineTo(i.float, 5000)
+    cr.stroke
+  cr.setSource(0, 0, 1)
+  cr.setLineWidth(10)
+  for i in 0 ..< 10:
+    cr.moveTo(0, i.float * 500.0)
+    cr.lineTo(5000, i.float * 500.0)
+    cr.stroke
+  for i in 0 ..< 10:
+    cr.moveTo(i.float * 500.0, 0)
+    cr.lineTo(i.float * 500.0, 5000)
+    cr.stroke
+  ## Outside box.
+  cr.setLineWidth(20)
+  cr.setSource(1, 0, 1)
+  cr.rectangle(0, 0, 5000, 5000)
+  cr.stroke
+  ## Save surface data to file.
+  let f: File = open("big_surface.s", fmWrite)
+  let p: ptr cuchar = cairo_image_surface_get_data(bigSurface.impl)
+  let len = writeBuffer(f, p, cairo_format_stride_for_width(CFormat, Width) * Height)
+  echo("write $1\n" % $len)
+  close(f)
+
+proc myDealloc(data: pointer) {.cdecl.} =
+  system.dealloc(data)
+
+proc getBigSurface(): Surface =
+  let f: File = open("big_surface.s", fmRead)
+  # setFilePos(f, 0)
+  # https://www.cairographics.org/manual/cairo-Image-Surfaces.html#cairo-format-stride-for-width
+  let stride = cairo_format_stride_for_width(CFormat, Width)
+  bigSurfaceData = cast[ptr cuchar](malloc((stride * Height).uint64))
+  var len = readBuffer(f, bigSurfaceData, stride * Height)
+  echo("read $1" % $len)
+  close(f)
+  let bigSurface: Surface = new Surface # this is a temporary fix, we will support this later in cairo modul
+  bigSurface.impl = cairo_image_surface_create_for_data(bigSurfaceData, CFormat, Width, Height, stride)
+  discard setUserData(bigSurface, addr(Key), bigSurfaceData, myDealloc) # automatic deallocation
+  # flush(bigSurface)
+  echo("open $1" % bigSurface.status.statusToString)
+  return bigSurface
+
+proc daDrawing*(da: DrawingArea; cr: Context; bigSurface: Surface): bool =
+  var
+    width = da.getAllocatedWidth.float
+    height = da.getAllocatedHeight.float
+    originX = translateX
+    originY = translateY
+  ## Some constraints.
+  if translateX > 5000.0 - width:
+    originX = 5000.0 - width / scale
+  if translateY > 5000.0 - height:
+    originY = 5000.0 - height / scale
+  cr.setSource(0, 0, 0)
+  cr.paint
+  ## Partition the big surface.
+  var littleSurface: Surface = cairo.surfaceCreateForRectangle(bigSurface,
+      originX, originY, width / scale, height / scale)
+  cr.scale(scale, scale)
+  cr.setSourceSurface(littleSurface, 0, 0)
+  setFilter(getSource(cr), cairo.Filter.bilinear)
+  cr.paint
+  return true
+
+proc bye(w: Window) =
+  mainQuit()
+  echo "Bye..."
+
+proc main =
+  gtk.init()
+  let window = newWindow()
+  window.setTitle("Big Surface2")
+  window.setDefaultSize(500, 500)
+  window.setPosition(gtk.WindowPosition.center)
+  window.connect("destroy", bye)
+  ## Get a test surface.
+  saveBigSurface()
+  let bigSurface = getBigSurface()
+  let da: DrawingArea = newDrawingArea()
+  da.setHexpand
+  da.setVexpand
+  da.connect("draw", daDrawing, bigSurface)
+  let
+    translateXAdj = newAdjustment(0, 0, 5000, 20, 0, 0)
+    translateYAdj = newAdjustment(0, 0, 5000, 20, 0, 0)
+    scaleAdj = newAdjustment(1, 1, 5, 0.1, 0, 0)
+    translateXLabel = newLabel("translate x")
+    translateXSpin= newSpinButton(translateXAdj, 50, 1)
+  connect(translateXSpin, "value-changed", translateXSpinChanged, da)
+  let translateYLabel = newLabel("translate y")
+  let translateYSpin = newSpinButton(translateYAdj, 50, 1)
+  connect(translateYSpin, "value-changed", translateYSpinChanged, da)
+  let scaleLabel = newLabel("Scale")
+  let scaleSpin = newSpinButton(scaleAdj, 0.2, 1)
+  connect(scaleSpin, "value-changed", scaleSpinChanged, da)
+  let grid = newGrid()
+  grid.attach(da, 0, 0, 3, 1)
+  grid.attach(translateXLabel, 0, 1, 1, 1)
+  grid.attach(translateYLabel, 1, 1, 1, 1)
+  grid.attach(scaleLabel, 2, 1, 1, 1)
+  grid.attach(translateXSpin, 0, 2, 1, 1)
+  grid.attach(translateYSpin, 1, 2, 1, 1)
+  grid.attach(scaleSpin, 2, 2, 1, 1)
+  add(window, grid)
+  showAll(window)
+  gtk.main()
+
+main()
+
+
+
+ + + + + +
+ + +Related work: https://github.com/jdmansour/nim-smartgi +
+
+
+
+
+ + + \ No newline at end of file diff --git a/examples/gtk3/css_colored_listview.nim b/examples/gtk3/css_colored_listview.nim index 6099e9f..6a73c62 100644 --- a/examples/gtk3/css_colored_listview.nim +++ b/examples/gtk3/css_colored_listview.nim @@ -1,9 +1,8 @@ -## gcc -Wall cell_color1.c -o cell_color1 `pkg-config --cflags --libs gtk+-3.0` ## https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/4 ## C. Eric Cashon # nim c css_colored_listview.nim -import gintro/[gtk, gobject] +import gintro/[gtk, glib, gobject] import gintro/gdk except Window # there is a problem with gdk.Window -- we have to investigate! const # maybe we should use Nim's enum here? Id = 0 @@ -31,6 +30,26 @@ proc toBoolVal(b: bool): Value = discard init(result, gtype) setBoolean(result, b) +# we need the following two procs for now -- later we will not use that ugly cast... +proc typeTest(o: gobject.Object; s: string): bool = + let gt = g_type_from_name(s) + return g_type_check_instance_is_a(cast[ptr TypeInstance00](o.impl), gt).toBool + +proc listStore(o: gobject.Object): gtk.ListStore = + assert(typeTest(o, "GtkListStore")) + cast[gtk.ListStore](o) + +proc updateRow(renderer: CellRendererText; path: cstring; newText: cstring; tree: TreeView) = + var iter: TreeIter + var value: Value + let gtype = typeFromName("gchararray") + discard init(value, gtype) + let store = listStore(tree.getModel()) + value.setString(newText) + let treePath = newTreePathFromString(path) + discard store.getIter(iter, treePath) + store.setValue(iter, 1, value) + # we use the old gtk style with init() as is used in the C original -- maybe better use modern app sytle proc main() = gtk.init() @@ -41,8 +60,9 @@ proc main() = window.borderWidth = 20 connect(window, "destroy", bye) var iter: TreeIter - var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), typeFromName("gchararray")] - var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # this cast is ugly, we should fix it in the bindings soon. + var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), + typeFromName("gchararray")] + var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # cast is ugly, we should fix it in bindings. let progNames = ["Gedit", "Gimp", "Inkscape", "Firefox", "Calculator", "Devhelp"] for i, n in progNames: store.append(iter) # currently we have to use setValue() as there is no varargs proc as in C original @@ -60,6 +80,7 @@ proc main() = setProperty(renderer1, "editable", toBoolVal(false)) var renderer2 = newCellRendererText() setProperty(renderer2, "editable", toBoolVal(true)) + connect(renderer2, "edited", updateRow, tree) ## Bind the Color column to the "cell-background" property. var column1 = newTreeViewColumn() column1.setTitle("ID") @@ -73,16 +94,16 @@ proc main() = column1.addAttribute(renderer2, "text", Program) column1.addAttribute(renderer2, "cell-background", Color2) discard tree.appendColumn(column2) - var grid = newGrid() # grid with only one occupied cell makes not much sense here -- but so we can add more widgets later + var grid = newGrid() # only one occupied cell makes no sense -- but so we can add more widgets later grid.attach(tree, 0, 0, 1, 1) window.add(grid) const cssString = # note: big font selected intentionally - """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); - color: rgba(0,0,255,1.0);}""" + """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: + rgba(255,255,0,1.0); color: rgba(0,0,255,1.0);}""" var provider = newCssProvider() discard provider.loadFromData(cssString) addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION) window.showAll gtk.main() -main() \ No newline at end of file +main()