fixes for builder get() procs and cairo

This commit is contained in:
Stefan Salewski 2019-04-27 09:02:37 +02:00
commit 4c93a26d04
7 changed files with 3212 additions and 267 deletions

View file

@ -843,7 +843,10 @@ main()
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.
Later we may use Destructors for that.
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.
[[drawingarea.nim]]
[source,nim]
@ -851,46 +854,26 @@ Later we may use Destructors for that.
----
# Plain demo for zooming, panning, scrolling with GTK DrawingArea
# (c) S. Salewski, 21-DEC-2010 (initial Ruby version)
# Nim version April 2019 -- well first draft :-)
# 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.
import gintro/[gtk, gdk, glib, gobject, gio, cairo]
const
DataX = 150.0
DataY = 250.0
DataWidth = 200.0
DataHeight = 120.0
# The following two functions are intended to be user defined -- we have still
# to investigate how to do that best.
# bounding box of user data -- x, y, w, h -- top left corner, width, height
proc getWorldExtends(): (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)
cr.rectangle(DataX, DataY, DataWidth, DataHeight)
var i = 10.0
while true:
if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0:
break
cr.rectangle(DataX + i, DataY + i, DataWidth - 2.0 * i,
DataHeight - 2.0 * i)
i += 10.0
cr.stroke
const
ZOOM_FACTOR_MOUSE_WHEEL = 1.1
ZOOM_FACTOR_SELECT_MAX = 10 # ignore zooming in tiny selection
ZOOM_NEAR_MOUSEPOINTER = true # mouse wheel zooming -- to mousepointer or center
SELECT_RECT_COL = [0.0, 0, 1, 0.5] # blue with transparency
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...
@ -919,7 +902,7 @@ 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
Panning: Moving mouse while middle mouse button pressed
"""
# drawing area and scroll bars in 2x2 table (PDA == Plain Drawing Area)
@ -931,6 +914,12 @@ type
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
@ -953,6 +942,8 @@ type
lastMousePosY: float
zoomRectX1: float
zoomRectY1: float
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
@ -967,7 +958,8 @@ proc drawingAreaDrawCb(darea: DrawingArea; cr: Context; this: PDA): bool =
cr.setSource(0, 0, 0)
cr.setLineWidth(2)
cr.stroke
return true # TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further.
return SignalEventStopPropagation
#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) =
@ -977,35 +969,30 @@ proc updateVal(adj: PosAdj; d: float) =
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.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)
# Maybe we should fix this for latest GTK3, see
# https://developer.gnome.org/gdk3/stable/gdk3-Cairo-Interaction.html
# so use gdk_drawing_context_get_cairo_context ()
# or maybe even better do all drwing from within "draw" callback?
# maybe for optimization we should only allocate a new surface when its size has changed.
proc paint(this: PDA) =
if this.surf != nil:
cairosurfaceDestroy(this.surf.impl) # we may have to fix that later for automatically destroy
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
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)
drawWorld(cr) # call the user provided drawing fonction
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
proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure;
this: PDA): bool =
proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure; this: PDA): bool =
this.updateAdjustments(0, 0)
(this.dataX, this.dataY, this.dataWidth,
this.dataHeight) = getWorldExtends() # query user defined size
this.dataHeight) = this.extents() # query user defined size
this.fullScale = min(this.darea.allocatedWidth.float / this.dataWidth,
this.darea.allocatedHeight.float / this.dataHeight)
this.paint
@ -1013,8 +1000,7 @@ proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure;
proc updateAdjustmentsAndPaint(this: PDA; dx, dy: float) =
this.updateAdjustments(dx, dy)
this.paint
this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth,
this.darea.allocatedHeight)
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) =
@ -1026,16 +1012,13 @@ 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
#(x, y) = this.getUserCoordinates(x, y)
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)
this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight)
elif button2 in state: # panning
this.updateAdjustmentsAndPaint(this.lastMousePosX - x,
this.lastMousePosY - y)
this.updateAdjustmentsAndPaint(this.lastMousePosX - x, this.lastMousePosY - y)
this.lastMousePosX = x
this.lastMousePosY = y
#event.request # request more motion events ?
@ -1048,25 +1031,20 @@ proc scrollEvent(darea: DrawingArea; event: EventScroll; this: PDA): bool =
var d = getScrollDirection(event)
var (x, y) = event.getCoords
if d == ScrollDirection.up:
this.userZoom *= ZOOM_FACTOR_MOUSE_WHEEL
this.userZoom *= ZoomFactorMouseWheel
elif d == ScrollDirection.down:
this.userZoom /= ZOOM_FACTOR_MOUSE_WHEEL
this.userZoom /= ZoomFactorMouseWheel
if (this.userZoom < 1):
this.userZoom = 1
if this.zoomNearMousepointer:
this.updateAdjustmentsAndPaint((this.hadjustment.value + x) * (
this.userZoom / z0 - 1),
(this.vadjustment.value + y) * (
this.userZoom / z0 - 1))
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))
(this.vadjustment.value + this.darea.allocatedHeight.float * 0.5) * (this.userZoom / z0 - 1))
proc buttonPressEvent(darea: DrawingArea; event: EventButton;
this: PDA): bool =
proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
var (x, y) = event.getCoords
this.lastMousePosX = x
this.lastMousePosY = y
@ -1077,8 +1055,7 @@ proc buttonPressEvent(darea: DrawingArea; event: EventButton;
echo "User coordinates: ", x, ' ', y, "\n" # to verify getUserCoordinates()
# zoom into selected rectangle and center it
proc buttonReleaseEvent(darea: DrawingArea; event: EventButton;
this: PDA): bool =
proc buttonReleaseEvent(darea: DrawingArea; event: EventButton; this: PDA): bool =
var (x, y) = event.getCoords
var b = getButton(event)
if b == 1:
@ -1086,7 +1063,7 @@ proc buttonReleaseEvent(darea: DrawingArea; event: EventButton;
let z1 = min(this.darea.allocatedWidth.float / (
this.lastButtonDownPosX - x).abs, this.darea.allocatedHeight.float / (
this.lastButtonDownPosY - y).abs)
if z1 < ZOOM_FACTOR_SELECT_MAX: # else selection rectangle will persist, we may output a message...
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 -
@ -1096,24 +1073,19 @@ proc buttonReleaseEvent(darea: DrawingArea; event: EventButton;
proc onAdjustmentEvent(this: PosAdj; pda: PDA) =
pda.paint
pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth,
pda.darea.allocatedHeight)
pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth, pda.darea.allocatedHeight)
proc newPDA: PDA =
initGrid(result)
result.zoomNearMousepointer = ZOOM_NEAR_MOUSEPOINTER # mouse wheel zooming
result.selecting = false
result.zoomNearMousepointer = ZoomNearMousepointer # mouse wheel zooming
result.userZoom = 1.0
result.surf = nil
result.darea = newDrawingArea()
result.darea.setHExpand#(true)
result.darea.setVExpand#(true)
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,
EventFlag.button1Motion, EventFlag.button2Motion,
EventFlag.pointerMotionHint})
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)
@ -1126,25 +1098,98 @@ proc newPDA: PDA =
onAdjustmentEvent, result)
result.hscrollbar = newScrollbar(Orientation.horizontal, result.hadjustment)
result.vscrollbar = newScrollbar(Orientation.vertical, result.vadjustment)
result.hscrollbar.setHExpand#(true)
result.vscrollbar.setVExpand#(true)
result.hscrollbar.setHExpand
result.vscrollbar.setVExpand
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) =
proc appActivate(app: Application; initData: ref PDA_Data) =
let window = newApplicationWindow(app)
window.title = "Drawing example"
window.defaultSize = (400, 400)
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 main =
proc newDisplay*(initData: PDA_Data) =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
var d = new (ref PDA_Data)
d[] = initData
connect(app, "activate", appActivate, d)
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 true:
if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0:
break
cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i)
i += 10.0
cr.stroke
proc test =
var data: PDA_Data
data.draw = drawWorld
data.extents = worldExtents
data.windowSize = (800, 600)
newDisplay(data)
test()
----
We can use this module as a library easily and get this simple drawing tool with full zoom and scroll support:
[[darea_test.nim]]
[source,nim]
.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()
----