fixes and new examples

This commit is contained in:
Stefan Salewski 2019-09-14 12:31:27 +02:00
commit 8979188f4c
9 changed files with 888 additions and 185 deletions

View file

@ -1,6 +1,6 @@
= High level GTK3 (and GTK4) bindings for the Nim programming language
(c) Stefan Salewski
//Version 0.5.3
//Version 0.5.4
:experimental:
:imagesdir: http://ssalewski.de/tmp
:source-highlighter: pygments
@ -19,7 +19,7 @@ TIP: A more fancy copy of this document with dark source code background is avai
NOTE: 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.
NOTE: Starting with current release 0.5.3 we do not generate field entries for objects and we do
NOTE: 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
@ -1416,16 +1416,216 @@ proc main =
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]]
[source,c]
.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]]
[source,nim]
.css_colored_listview.nim
----
# nim c css_colored_listview.nim
import gintro/[gtk, 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 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)) # this cast is ugly, we should fix it in the bindings soon.
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))
## 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() # grid with only one occupied cell makes not much sense here -- 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.
== 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).
With the latest fix to Nim GTK3 bindings it seems to work fine, but some rewriting for optimized performance
may be needed, I have to investigate that. Cairo surface is currently manually freed, because GC may have a too large delay.
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]]
[source,nim]
.drawingarea.nim
@ -1445,6 +1645,8 @@ and move the mouse to first draw a selection rectangle and zoom into it when rel
# 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
@ -1483,7 +1685,7 @@ Scrolling: Scrollbars
Panning: Moving mouse while middle mouse button pressed
"""
# drawing area and scroll bars in 2x2 table (PDA == Plain Drawing Area)
# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area)
type
PosAdj = ref object of Adjustment
@ -1504,6 +1706,8 @@ type
selecting: bool
userZoom: float
surf: Surface
pattern: Pattern
cr: cairo.Context
darea: DrawingArea
hadjustment: PosAdj
vadjustment: PosAdj
@ -1520,23 +1724,24 @@ type
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.surf.isNil: return
cr.setSource(patternCreateForSurface(this.surf))
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)
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 SignalEventStopPropagation
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()
@ -1553,27 +1758,47 @@ proc updateAdjustments(this: PDA; dx, dy: float) =
updateVal(this.hadjustment, dx)
updateVal(this.vadjustment, dy)
# maybe for optimization we should only allocate a new surface when its size has changed.
proc paint(this: PDA) =
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)
let cr = newContext(this.surf)
cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center
# 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)
cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom)
cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5)
this.drawWorld(cr) # call the user provided drawing function
destroy(cr) # we can also manually destroy the context here, but GC would do it for us
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.updateAdjustments(0, 0)
(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)
@ -1590,37 +1815,45 @@ proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) =
proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool =
let state = getState(event)
let (x, y) = event.getCoords
if (state.contains(button1)): # selecting
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
var d = getScrollDirection(event)
var (x, y) = event.getCoords
if d == ScrollDirection.up:
case getScrollDirection(event)
of ScrollDirection.up:
this.userZoom *= ZoomFactorMouseWheel
elif d == ScrollDirection.down:
of ScrollDirection.down:
this.userZoom /= ZoomFactorMouseWheel
if (this.userZoom < 1):
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))
(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
@ -1631,60 +1864,66 @@ proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
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 =
var (x, y) = event.getCoords
var b = getButton(event)
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)
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(
((2 * this.hadjustment.value + x + this.lastButtonDownPosX) * z1 -
this.darea.allocatedWidth.float) * 0.5 - this.hadjustment.value,
((2 * this.vadjustment.value + y + this.lastButtonDownPosY) * z1 -
this.darea.allocatedHeight.float) * 0.5 - this.vadjustment.value)
((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.darea = newDrawingArea()
result.darea.setHExpand
result.darea.setVExpand
result.darea.connect("draw", drawingAreaDrawCb, result)
result.darea.connect("configure-event", dareaConfigureCallback, result)
result.darea.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease,
EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint})
result.darea.connect("motion-notify-event", onMotion, result)
result.darea.connect("scroll_event", scrollEvent, result)
result.darea.connect("button_press_event", buttonPressEvent, result)
result.darea.connect("button_release_event", buttonReleaseEvent, result)
result.hadjustment = newPosAdj()
result.hadjustment.handlerID = result.hadjustment.connect("value-changed",
onAdjustmentEvent, result)
result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result)
result.vadjustment = newPosAdj()
result.vadjustment.handlerID = result.vadjustment.connect("value-changed",
onAdjustmentEvent, result)
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 appActivate(app: Application; initData: ref PDA_Data) =
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
@ -1694,14 +1933,13 @@ proc appActivate(app: Application; initData: ref PDA_Data) =
proc newDisplay*(initData: PDA_Data) =
let app = newApplication("org.gtk.example")
var d = new (ref PDA_Data)
d[] = initData
connect(app, "activate", appActivate, d)
connect(app, "startup", appStartup)
connect(app, "activate", appActivate, initData)
discard run(app)
when isMainModule:
const # arbitrary locations for our data
const # arbitrary locations for our data
DataX = 150.0
DataY = 250.0
DataWidth = 200.0
@ -1721,21 +1959,16 @@ when isMainModule:
cr.setSource(0, 0, 0)
cr.setLineWidth(2)
var i = 0.0
while true:
if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0:
break
while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0:
cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i)
i += 10.0
i += 10
cr.stroke
proc test =
var data: PDA_Data
data.draw = drawWorld
data.extents = worldExtents
data.windowSize = (800, 600)
let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600))
newDisplay(data)
test()
test() # 337 lines
----
We can use this module as a library easily and get this simple drawing tool with full zoom and scroll support:

View file

@ -1,9 +1,9 @@
# nim c connect_args.nim
import gintro/[gtk, glib, gobject, gio]
import gintro/[gtk, gobject, gio]
type
O = object
i: int
i, j: int
proc b1Callback(button: Button; str: string) =
echo str
@ -12,7 +12,7 @@ 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
echo "Value of field i in ref to object o = ", r.i
proc b4Callback(button: Button; w: ApplicationWindow) =
if w.title == "Nim with GTK3":
@ -20,27 +20,23 @@ proc b4Callback(button: Button; w: ApplicationWindow) =
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)
proc appActivate(app: Application) =
let
o = O(i: 1234567)
r = (ref O)(i: 7654321)
window = newApplicationWindow(app)
box = newBox(Orientation.vertical, 0)
b1 = newButton("Nim with GTK3")
b2 = newButton("Passing an object from stack")
b3 = newButton("Passing an object from heap")
b4 = newButton("Passing a Widget")
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)
b1.connect("clicked", b1Callback, "is much fun.")
b2.connect("clicked", b2Callback, o)
b3.connect("clicked", b3Callback, r)
b4.connect("clicked", b4Callback, window)
for b in [b1, b2, b3, b4]:
box.add(b)
window.add(box)
window.showAll

View file

@ -0,0 +1,88 @@
## 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/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 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)) # this cast is ugly, we should fix it in the bindings soon.
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))
## 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() # grid with only one occupied cell makes not much sense here -- 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()

View file

@ -13,6 +13,8 @@
# 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
@ -51,7 +53,7 @@ Scrolling: Scrollbars
Panning: Moving mouse while middle mouse button pressed
"""
# drawing area and scroll bars in 2x2 table (PDA == Plain Drawing Area)
# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area)
type
PosAdj = ref object of Adjustment
@ -72,6 +74,8 @@ type
selecting: bool
userZoom: float
surf: Surface
pattern: Pattern
cr: cairo.Context
darea: DrawingArea
hadjustment: PosAdj
vadjustment: PosAdj
@ -88,17 +92,18 @@ type
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.surf.isNil: return
cr.setSource(patternCreateForSurface(this.surf))
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)
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)
@ -121,36 +126,47 @@ proc updateAdjustments(this: PDA; dx, dy: float) =
updateVal(this.hadjustment, dx)
updateVal(this.vadjustment, dy)
# maybe for optimization we should only allocate a new surface when its size has changed.
proc paint(this: PDA) =
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)
let cr = newContext(this.surf)
cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center
# 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)
cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom)
cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5)
this.drawWorld(cr) # call the user provided drawing function
destroy(cr) # we can also manually destroy the context here, but GC would do it for us
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.updateAdjustments(0, 0)
(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)
@ -167,37 +183,45 @@ proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) =
proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool =
let state = getState(event)
let (x, y) = event.getCoords
if (state.contains(button1)): # selecting
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
var d = getScrollDirection(event)
var (x, y) = event.getCoords
if d == ScrollDirection.up:
case getScrollDirection(event)
of ScrollDirection.up:
this.userZoom *= ZoomFactorMouseWheel
elif d == ScrollDirection.down:
of ScrollDirection.down:
this.userZoom /= ZoomFactorMouseWheel
if (this.userZoom < 1):
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))
(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
@ -208,49 +232,49 @@ proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
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 =
var (x, y) = event.getCoords
var b = getButton(event)
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)
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(
((2 * this.hadjustment.value + x + this.lastButtonDownPosX) * z1 -
this.darea.allocatedWidth.float) * 0.5 - this.hadjustment.value,
((2 * this.vadjustment.value + y + this.lastButtonDownPosY) * z1 -
this.darea.allocatedHeight.float) * 0.5 - this.vadjustment.value)
((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.darea = newDrawingArea()
result.darea.setHExpand
result.darea.setVExpand
result.darea.connect("draw", drawingAreaDrawCb, result)
result.darea.connect("configure-event", dareaConfigureCallback, result)
result.darea.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease,
EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint})
result.darea.connect("motion-notify-event", onMotion, result)
result.darea.connect("scroll_event", scrollEvent, result)
result.darea.connect("button_press_event", buttonPressEvent, result)
result.darea.connect("button_release_event", buttonReleaseEvent, result)
result.hadjustment = newPosAdj()
result.hadjustment.handlerID = result.hadjustment.connect("value-changed",
onAdjustmentEvent, result)
result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result)
result.vadjustment = newPosAdj()
result.vadjustment.handlerID = result.vadjustment.connect("value-changed",
onAdjustmentEvent, result)
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
@ -261,9 +285,13 @@ proc newPDA: PDA =
result.attach(result.vscrollbar, 1, 0, 1, 1)
result.attach(result.hscrollbar, 0, 1, 1, 1)
proc appActivate(app: Application; initData: ref PDA_Data) =
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
@ -273,14 +301,13 @@ proc appActivate(app: Application; initData: ref PDA_Data) =
proc newDisplay*(initData: PDA_Data) =
let app = newApplication("org.gtk.example")
var d = new (ref PDA_Data)
d[] = initData
connect(app, "activate", appActivate, d)
connect(app, "startup", appStartup)
connect(app, "activate", appActivate, initData)
discard run(app)
when isMainModule:
const # arbitrary locations for our data
const # arbitrary locations for our data
DataX = 150.0
DataY = 250.0
DataWidth = 200.0
@ -300,18 +327,13 @@ when isMainModule:
cr.setSource(0, 0, 0)
cr.setLineWidth(2)
var i = 0.0
while true:
if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0:
break
while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0:
cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i)
i += 10.0
i += 10
cr.stroke
proc test =
var data: PDA_Data
data.draw = drawWorld
data.extents = worldExtents
data.windowSize = (800, 600)
let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600))
newDisplay(data)
test()
test() # 337 lines

View file

@ -12,4 +12,7 @@ nim c label.nim
nim c listview.nim
nim c menubar.nim
nim c t0.nim
nim c simpledrawingarea.nim
nim c css_colored_listview.nim
nim c gearsmenu.nim
nim c cairoImage.nim

View file

@ -0,0 +1,310 @@
# 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 is a simplified example, where all the drawing is done in
# the draw callback function. No separate paint proc with own buffer surface.
# 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
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 =
cr.save
cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center
this.vadjustment.upper * 0.5 - this.vadjustment.value)
cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom)
cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5)
this.drawWorld(cr) # call the user provided drawing function
cr.restore
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 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)
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.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.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() # 310 lines

View file

@ -1,6 +1,6 @@
# Package
version = "0.5.3"
version = "0.5.4"
author = "Stefan Salewski"
description = "High level GObject-Introspection based GTK3/GTK4 bindings"
license = "MIT"

View file

@ -1,7 +1,7 @@
# This is the high level cairo module for Nim -- based on the low level ngtk3 module, manually tuned.
# (c) S. Salewski 2017, cairo 1.15.6
# v0.4.18
# 24-APR-2019
# v0.5.04
# 14-SEP-2019
# Cairo does not really support gobject introspection and gobject's toggle references.
# So code for memory management is a bit different compared to other gtk modules.
@ -25,7 +25,8 @@
# result.impl = cairo_create(target.impl)
# discard cairo_set_user_data(result.impl, NUDK, cast[pointer](result), gcuref)
# We have to call GC_ref() because we store no direct reference to the proxy object.
# We have to call GC_ref() because we store no direct reference to the proxy object, while
# cairo_get_target() allows us to get the surface back from cairo context.
# For this example cairo_create() references the surface.
# When ref count of surface is decreased, maybe because context is destroyed, then gcuref is called
# which calls GC_unref() on proxy. When there is no other ref to proxy GC can free proxy and surface
@ -34,7 +35,8 @@
# But there is at least one design flaw:
# When we create for example a surface and GC releases it, then gcunref() is called on it while
# gc_ref() was newer called. When we consider that the cairo context is the only element which
# stores references to other objects, then we can simplify the code to
# stores references to other objects, then we can simplify the code to (Footnote: This is not really
# true, for example Pattern stores references to surface too -- but it should work similar.)
# proc newImageSurface*(format: Format; width, height: int): Surface =
# new(result, surfaceDestroy)
@ -55,7 +57,17 @@
# with
# proc gcuref(o: pointer) {.cdecl.} = GC_unref(cast[RootRef](o))
# cairo_image_surface_create() set refcount of the plain cairo surface to 1.
# cairo_create() increases refcount of surface (by 2), cairo destroy decreases
# surface refcount again. cairo_surface_set_user_data() ensures that
# Nim surface proxy element is unreffed when refcount of cairo surface
# drops to zero, so surfaceDestroy() frees the cairo memory.
# Note: For some other procs like cairo_set_source_surface() we also need code like
# discard cairo_surface_set_user_data(target.impl, NUDK, cast[pointer](target), gcuref)
# Currently this is all not really tested.
# One problem may be procs like cairo_surface_create_similar() when called frequently, maybe
# for each frame in animations. GC may be too slow in freeing the memory.
# We should try to avoid such usage, or we may add a way to manually free memory.
@ -290,9 +302,6 @@ proc newContext*(target: Surface): Context =
result.impl = cairo_create(target.impl)
discard cairo_surface_set_user_data(target.impl, NUDK, cast[pointer](target), gcuref)
proc cairo_reference*(cr: ptr Context00): ptr Context00 {.importc, libcairo.}
#
proc reference*(cr: Context): Context =
@ -330,7 +339,11 @@ proc pushGroup*(cr: Context; content: Content) =
proc cairo_pattern_destroy*(pattern: ptr Pattern00) {.importc, libcairo.}
#
proc patternDestroy*(pattern: Pattern) =
cairo_pattern_destroy(pattern.impl)
#cairo_pattern_destroy(pattern.impl)
if pattern != nil and pattern.impl != nil:
cairo_pattern_destroy(pattern.impl)
pattern.impl = nil
proc cairo_pattern_set_user_data*(pattern: ptr Pattern00; key: ptr UserDataKey; userData: pointer;
destroy: DestroyFunc00): Status {.importc, libcairo.}
@ -405,11 +418,9 @@ proc getReferenceCount*(surface: Surface): int =
proc cairo_set_source_surface*(cr: ptr Context00; surface: ptr Surface00; x, y: cdouble) {.importc, libcairo.}
#
proc setSourceSurface*(cr: Context; surface: Surface; x, y: float) =
#GC_ref(source) # we guess that surface is nor referenced, but a new intern pattern is created
let h = cairo_surface_get_reference_count(surface.impl)
GC_ref(surface)
cairo_set_source_surface(cr.impl, surface.impl, x.cdouble, y.cdouble)
assert(cairo_surface_get_reference_count(surface.impl) == h) # is our guess OK?
#
discard cairo_surface_set_user_data(surface.impl, NUDK, cast[pointer](surface), gcuref)
#const `sourceSurface=`* = setSourceSurface
proc cairo_set_tolerance*(cr: ptr Context00; tolerance: cdouble) {.importc, libcairo.}
@ -1378,15 +1389,22 @@ proc cairo_pattern_get_user_data*(pattern: ptr Pattern00; key: ptr UserDataKey):
proc getUserData*(pattern: Pattern; key: ptr UserDataKey): pointer =
cairo_pattern_get_user_data(pattern.impl, key)
proc cairo_pattern_reference*(pattern: ptr Pattern00): ptr Pattern00 {.importc, libcairo.}
#
proc patternReference*(pattern: Pattern): Pattern =
discard cairo_pattern_reference(pattern.impl)
return pattern
# Returns the current source pattern. This object is owned by cairo.
# To keep a reference to it, you must call cairo_pattern_reference().
proc cairo_get_source*(cr: ptr Context00): ptr Pattern00 {.importc, libcairo.}
#
proc getSource*(cr: Context): Pattern =
let h = cairo_get_source(cr.impl)
let d = cairo_pattern_get_user_data(h, NUDK)
if d.isNil:
#assert false # may this happen?
new(result, patternDestroy)
result.impl = h
result.impl = cairo_pattern_reference(h)
else:
result = cast[Pattern](d)
assert(result.impl == h)
@ -2077,11 +2095,7 @@ proc patternCreateMesh*(): Pattern =
result.impl = cairo_pattern_create_mesh()
### discard cairo_pattern_set_user_data(result.impl, NUDK, cast[pointer](result), gcuref)
proc cairo_pattern_reference*(pattern: ptr Pattern00): ptr Pattern00 {.importc, libcairo.}
#
proc patternReference*(pattern: Pattern): Pattern =
discard cairo_pattern_reference(pattern.impl)
return pattern
proc cairo_pattern_status*(pattern: ptr Pattern00): Status {.importc, libcairo.}
#
@ -2821,4 +2835,5 @@ when CAIRO_HAS_TEE_SURFACE:
discard cairo_tee_surface_index(surface.impl, index.cuint)
return surface
# 2786 lines
# 2851 lines

View file

@ -58,6 +58,8 @@ macro mconnect(widget: gobject.Object; signal: string; p: typed; arg: typed; ign
inc(ProcID)
let wt = getType(widget)
let at = getTypeInst(arg)
#echo at.repr
#echo at.typeKind == ntyRef
let wts = ($(wt[1].toStrLit)).replace(":ObjectType")
let ats = $at.toStrLit
let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names
@ -74,7 +76,10 @@ proc $1$2 {.cdecl.} =
ahl = "(self: gobject.Object)"
r1s.add(" $3(cast[$4](h)")
if not ignoreArg.boolVal:
r1s.add(", cast[$5](xdata)")
if getTypeInst(arg).typeKind == ntyRef:
r1s.add(", cast[$5](xdata)")
else:
r1s.add(", cast[ptr $5](xdata)[]")
r1s.add(")\n")
else: # signals with multiple arguments and maybe using interface providers
(sn, wid, num, ahl, all) = sci.split(RecSep)
@ -136,16 +141,19 @@ proc $1$2 {.cdecl.} =
r1s.add(" if " & names[i] & "1" & ".isNil:\n")
r1s.add(" new " & names[i] & "1" & "\n")
r1s.add(" " & names[i] & "1.impl = " & names[i] & "\n")
if not ignoreArg.boolVal:
resu.add(", cast[$5](data)")
# resu.add(", cast[$5](data)")
if getTypeInst(arg).typeKind == ntyRef:
resu.add(", cast[$5](data)")
else:
resu.add(", cast[ptr $5](data)[]")
resu.add(")")
if resl == "gboolean":
resu.add(".ord.gboolean")
r1s.add(resu & "\n")
all = all.replace(")", "; data: pointer)")
r1s = r1s % [$procNameCdecl, all, $p, wts, ats]
#echo r1s
# echo r1s
result = parseStmt(r1s)
if not ignoreArg.boolVal:
ahl = ahl.replace(")", "; arg: " & ats & ")")
@ -154,7 +162,7 @@ proc $1$2 {.cdecl.} =
else:
ahl = "(self: " & wts & ")" & ahl.split(")", 1)[1]
let r2s =
let r2sunused =
if ignoreArg.boolVal:
"""
proc $1(self: $2; p: proc $3): culong {.discardable.} =
@ -164,7 +172,7 @@ $1($6, $7)
else:
"""
proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} =
when a is RootRef:
when a is ref:
GC_ref(a)
sc$5(self, $6, cast[pointer](a))
else:
@ -172,10 +180,38 @@ proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} =
new(ar)
deepCopy(ar[], a)
GC_ref(ar)
sc$5(self, $6, cast[pointer](ar[]))
# sc$5(self, $6, cast[pointer](ar[]))
sc$5(self, $6, cast[pointer](ar))
$1($7, $8, $9)
""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)]
#echo r2s
# maybe we write this better this way:
let r2s =
if ignoreArg.boolVal:
"""
proc $1(self: $2; p: proc $3): culong {.discardable.} =
sc$4(self, $5, nil)
$1($6, $7)
""" % [$procName, wts, ahl, signalName, $procNameCdecl, $(widget.toStrLit), $p]
elif getTypeInst(arg).typeKind == ntyRef:
"""
proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} =
GC_ref(a)
sc$5(self, $6, cast[pointer](a))
$1($7, $8, $9)
""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)]
else:
"""
proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} =
var ar: ref $4
new(ar)
deepCopy(ar[], a)
GC_ref(ar)
# sc$5(self, $6, cast[pointer](ar[]))
sc$5(self, $6, cast[pointer](ar))
$1($7, $8, $9)
""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)]
# echo r2s
result.add(parseStmt(r2s))
template connect*(widget: gobject.Object; signal: string; p: typed; arg: typed): untyped =