lib/ruiby_gtk/dsl/canvas.rb
# Creative Commons BY-SA : Regis d'Aubarede <regis.aubarede@gmail.com>
# LGPL
module Ruiby_dsl
# Create a drawing area, for pixel/vectoriel draw
# for interactive actions see test.rb fo little example.
#
# @cv=canvas(width,height,opt) do
# on_canvas_draw { |w,ctx| myDraw(w,ctx) }
# on_canvas_button_press {|w,e| [e.x,e.y] } # must return a object which will given to next move/release callback
# on_canvas_button_motion {|w,e,o| n=[e.x,e.y] ; ... ; n }
# on_canvas_button_release {|w,e,o| ... }
# on_canvas_keypress {|w,key| ... }
# end
#
# for drawing in canvas, this commands are usable.
# basic gtk commands can still be uses ( move_to(), line_to()... )
# def myDraw(w,ctx)
# w.init_ctx(color_fg="#000000",color_bg="#FFFFFF",width=1)
# w.draw_point(x1,y1,color,width)
# w.draw_polygon([x,y,...],colorFill,colorStroke,widthStroke)
# w.draw_circle(cx,cy,rayon,colorFill,colorStroke,widthStroke)
# w.draw_rectangle(x0,y0,w,h, r,widthStroke,colorFill,w)
# cv.draw_rounded_rectangle(x0,y0,w,h,ar,colorStroke,colorFill,widthStroke)
# w.draw_pie(x,y,r,l_ratio_color_label)
# w.draw_arc(x,y,r,start,eend,width,color_stroke,color_fill=nil) # camenber
# w.draw_arc2(x,y,r,start,eend,width,color_stroke,color_fill=nil) # circle fraction
# w.draw_varbarr(x0,y0,x1,y1,vmin,vmax,l_date_value,width) {|value| color}
# w.draw_image(x,y,filename,sx,sy)
# cv.ctx_font(name,size) # choose font name and size for next draw_text...
# w.draw_text(x,y,text,scale,color,bgcolor=nil)
# w.draw_text_left(x,y,text,scale,color,bgcolor=nil)
# w.draw_text_center(x,y,text,scale,color,bgcolor=nil)
# lxy=w.translate(lxy,dx=0,dy=0) # move a list of points
# lxy=w.rotate(lxy,x0,y0,angle) # rotate a list of points
# cv.rotation(cx,cy,a) { draw... }
# w.scale(10,20,2) { w.draw_image(3,0,filename) }
# >> draw in a transladed/scaled coord system
# >> image will be draw at 16/20 (10+3*2)/(20+0*2)
# , and size doubled
#
# end
# gradient can be use for recangle and polygone, see samples/gradients.rb
# in place od String bg-color, say Array : #w{type direction color1 color2 ...}
# type = linear/radial direction : tb Top->Bottom, bu Bottom->Up , lr: Left->Right, ..., trb -> TopLeft -> BottomRight
def canvas(width,height,option={})
autoslot()
cv=DrawingArea.new()
cv.width_request=width
cv.height_request=height
cv.add_events(Gdk::EventMask::BUTTON_PRESS_MASK | Gdk::EventMask::BUTTON_MOTION_MASK | Gdk::EventMask::KEY_PRESS_MASK)
cv.can_focus = true
@currentCanvas=cv
@lcur << HandlerContainer.new
yield
@lcur.pop
@currentCanvas=nil
attribs(cv,option)
def cv.set_memo(memo) @memo=memo end
def cv.get_memo() @memo end
cv.set_memo(nil)
def cv.redraw()
self.queue_draw_area(0,0,self.allocation.width,self.allocation.height)
end
def cv.app_window() @app_window end
def cv.set_window(r) @app_window=r end
cv.set_window(self)
def cv.init_ctx(color_fg="#000000",color_bg="#FFFFFF",width=1)
w,cr=*@currentCanvasCtx
cr.set_line_join(Cairo::LINE_JOIN_ROUND)
cr.set_line_cap(Cairo::LINE_CAP_ROUND)
cr.set_line_width(width)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color_bg))
cr.paint
#cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color_fg))
@currentWidth=width
@currentColorFg=color_fg
@currentColorBg=color_bg
end
def cv.draw_line(lxy,color=nil,width=nil)
raise("odd number of coord for lxy") if !lxy || lxy.size==0 || lxy.size%2==1
if lxy.size==2
return draw_point(lxy.first,lxy.last,color,width)
end
_draw_poly(lxy,color|| @currentColorFg,nil,width)
end
def cv.draw_polygon(lxy,colorStroke=nil,colorFill=nil,widthStroke=nil)
raise("odd number of coord for lxy") if !lxy || lxy.size==0 || lxy.size%2==1
if lxy.size==2
return draw_point(lxy.first,lxy.last,colorStroke,widthStroke)
end
colorStroke=@currentColorFg if colorFill.nil? && colorStroke.nil?
_draw_poly(lxy,colorStroke,colorFill,widthStroke)
end
def cv._set_gradient(cv,cr,acolor,lxy)
type,sens,*data=acolor
return unless type && sens && data && data.size>=2
cr.set_source_rgba(*Ruiby_dsl.cv_color_html("#FFF"))
if type =~ /^g/
x0,y0,x1,y1=*bbox(lxy)
case sens
when "tb" then x0,y0,x1,y1 = x1/2,y0, x1/2,y1
when "bu" then x1,y1,x0,y0 = x1/2,y0, x1/2,y1
when "lr" then x0,y0,x1,y1 = 0,y1/2, x1,y1/2
when "rl" then x0,y0,x1,y1 = x1,y1/2, x0,y1/2
when "trb" then x0,y0,x1,y1 = x0,y0, x1,y1
when "tlb" then x0,y0,x1,y1 = x1,y0, x0,y1
else
error("unknown gradient : #{sens}")
end
#p [sens,x0,y0,x1,y1]
pattern = Cairo::LinearPattern.new(x0,y0,x1,y1)
last_color="#000"
data.each_with_index {|color,i|
pos= 1.0*i/(data.length-1)
#p [pos,Ruiby_dsl.cv_color_html(color)]
color=last_color if color=="-"
last_color=color
pattern.add_color_stop(pos, *(Ruiby_dsl.cv_color_html(color)[0..2]))
}
cr.set_source(pattern)
else
end
end
def cv.bbox(lxy)
xmin,ymin=lxy[0..1]
xmax,ymax=lxy[0..1]
lxy.each_slice(2) {|x,y|
xmin=x if x<xmin ;ymin=y if y<ymin
xmax=x if x>xmax ;ymax=y if y>ymax
}
[xmin,ymin,xmax,ymax]
end
def cv._draw_poly(lxy,color_fg,color_bg,width)
raise("odd number of coord for lxy") if !lxy || lxy.size==0 || lxy.size%2==1
w,cr=@currentCanvasCtx
cr.set_line_width(width) if width
x0,y0,*poly=*lxy
if color_bg
cr.move_to(x0,y0)
poly.each_slice(2) {|x,y| cr.line_to(x,y) }
if Array === color_bg
_set_gradient(w,cr,color_bg,lxy)
else
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color_bg))
end
cr.fill
end
if color_fg
#p lxy
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color_fg))
cr.move_to(x0.round,y0.round)
poly.each_slice(2) {|x,y| cr.line_to(x.round,y.round) }
cr.stroke()
end
end
def cv.draw_point(x,y,color=nil,width=nil)
width||=@currentWidth
draw_line([x,y-width/2.0, x,y+width/2.0],color,width)
end
def cv.draw_varbarr(x0,y0,x1,y1,dmin,dmax,lvalues0,width,&b)
ax=1.0*(x1-x0)/(dmax-dmin) ;bx= x0-ax*dmin
ay=1.0*(y1-y0)/(dmax-dmin) ;by= y0-ay*dmin
xconv=proc {|d| (x1==x0) ? x1 : (ax*d+bx) }
yconv=proc {|d| (y1==y0) ? y1 : (ay*d+by) }
w,cr=@currentCanvasCtx
lvalues=lvalues0.sort_by {|a| a.first}
l=[lvalues.first]+lvalues.each_cons(2).map {|(d,v),(d1,v1)|
(v1 && v!=v1) ? [d1,v1] : nil
}.compact+[lvalues.last]
#p l
cr.set_line_join(Cairo::LINE_JOIN_MITER)
cr.set_line_cap(Cairo::LINE_CAP_BUTT)
l.each_cons(2).map {|(d,v),(d1,v1)|
next unless v1
color= block_given? ? yield(v) : v.to_s
lxy=[xconv.call(d),yconv.call(d),xconv.call(d1),yconv.call(d1)]
#p " #{d},#{d1} ==> #{lxy.inspect}" if l.size>1
w.draw_line(lxy,color, width) if color
}
end
def cv.ctx_font(name,size)
w,cr=@currentCanvasCtx
fd=Pango::FontDescription.new(name)
cr.select_font_face(fd.family, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL)
cr.set_font_size(size)
end
def cv.draw_text(x,y,text,scale=1,color=nil,bgcolor=nil)
w,cr=@currentCanvasCtx
cr.set_line_width(1)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
scale(x,y,scale) {
if bgcolor
a=cr.text_extents(text)
w.draw_rectangle(0,1,a.width,-a.height,1,bgcolor,bgcolor,0)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
end
cr.move_to(0,0)
cr.show_text(text)
cr.fill
}
end
def cv.draw_text_left(x,y,text,scale=1,color=nil,bgcolor=nil)
w,cr=@currentCanvasCtx
cr.set_line_width(1)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
scale(x,y,scale) {
a=cr.text_extents(text)
if bgcolor
w.draw_rectangle(-a.width,-a.height,a.width,a.height+2,1,bgcolor,bgcolor,1)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
end
cr.move_to(-a.width,0)
cr.show_text(text)
cr.fill
}
end
def cv.draw_text_center(x,y,text,scale=1,color=nil,bgcolor=nil)
w,cr=@currentCanvasCtx
cr.set_line_width(1)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
scale(x,y,scale) {
a=cr.text_extents(text)
if bgcolor
w.draw_rectangle(-a.width/2,-1,a.width,-a.height-3,1,bgcolor,bgcolor,1)
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(color || @currentColorFg ))
end
cr.move_to(-a.width/2.0,0)
cr.show_text(text)
cr.fill
}
end
def cv.draw_rectangle(x0,y0,w,h,r=0,colorStroke=nil,colorFill=nil,widthStroke=nil)
return draw_rounded_rectangle(x0,y0,w,h,r,colorStroke,colorFill,widthStroke) if r.kind_of?(Array) || r>1
x1, y1 = x0+w, y0+h
colorStroke=@currentColorFg if colorFill.nil? && colorStroke.nil?
_draw_poly([x0,y0, x1,y0, x1,y1, x0,y1, x0,y0],colorStroke,colorFill,widthStroke)
end
def cv.draw_rounded_rectangle(x0,y0,w,h,ar,colorStroke,colorFill,widthStroke)
cv,cr=@currentCanvasCtx
pi=Math::PI
ar=[ar,ar,ar,ar] if ar.kind_of?(Numeric)
if Array === colorFill
_set_gradient(w,cr,colorFill,[x0,y0,x0+w,y0+h])
else
cr.set_source_rgba(*Ruiby_dsl.cv_color_html(colorFill ? colorFill : colorStroke))
end
cr.set_line_width( widthStroke )
r=ar[0]
cr.move_to(x0,y0+r)
cr.arc(x0+r,y0+r, r, -pi,-pi/2)
r=ar[1]
cr.line_to(x0+w-r,y0)
cr.arc(x0+w-r,y0+r, r, -pi/2, 0)
r=ar[2]
cr.line_to(x0+w,y0+h-r)
cr.arc(x0+w-r,y0+h-r, r, 0, pi/2)
r=ar[3]
cr.line_to(x0+r,y0+h)
cr.arc(x0+r,y0+h-r, r, pi/2,pi)
r=ar[0]
cr.line_to(x0,y0+r)
colorFill ? cr.fill : cr.stroke
end
def cv.draw_circle(x0,y0,r,color_bg=nil,color_fg=nil,width=nil)
w,cr=@currentCanvasCtx
cr.set_line_width(width || @currentWidth )
if color_bg
color=Ruiby_dsl.html_color(color_bg)
cr.set_source_rgba(color.red/65000.0, color.green/65000.0, color.blue/65000.0, 1)
cr.arc(x0,y0, r, width || @currentWidth , 3.0*Math::PI)
cr.fill
end
if color_fg
color=Ruiby_dsl.html_color(color_fg)
cr.set_source_rgba(color.red/65000.0, color.green/65000.0, color.blue/65000.0, 1)
cr.arc(x0,y0, r, 0 , 3.0*Math::PI)
cr.stroke
end
end
def cv.draw_pie(x0,y0,r,l_ratio_color_txt,with_legend=false)
lcolor=%w{#F00 #A00 #AA0 #AF0 #AAF #AAA #FAF #AFA #33F #044}
cv,ctx=@currentCanvasCtx
start=3.0*Math::PI/2.0
total=l_ratio_color_txt.inject(0.0) { |sum,(a)| sum+a }
h0=y0-r
p0=x0+r*1.2
l_ratio_color_txt.each_with_index { |(sweep0,coul,text),i|
coul=lcolor[i%lcolor.size] unless coul
sweep=sweep0/total
eend=start+Math::PI*2.0*sweep
mid=(eend+3*start)*0.25
if !with_legend && sweep>0.11 && r>=20 && text && text.size>0
dx=(mid>(Math::PI/2+Math::PI/4) && mid < Math::PI*3/2) ? text.size*7 : 0
cv.draw_line([x0,y0,x0+1.4*r*Math.cos(mid),y0+1.4*r*Math.sin(mid)],"#000",1)
cv.draw_text(x0+1.4*r*Math.cos(mid)-dx,y0+1.4*r*Math.sin(mid),text,1,coul)
end
if with_legend && text && text.size>0 && h0<y0+r
draw_rectangle(p0,h0,20,8,0,"#000",coul,1)
draw_text(p0+30,h0+10,text,1,"#000")
draw_text(p0+70,h0+10,": #{sweep0}",1,"#000")
h0+=10
end
ctx.move_to(x0,y0)
ctx.line_to(x0+r*Math.cos(start),y0+r*Math.sin(start))
ctx.arc( x0,y0, r, start, eend );
ctx.close_path
ctx.set_line_width( 1.0 )
ctx.set_source_rgba(*Ruiby_dsl.cv_color_html(coul))
ctx.fill
#ctx.set_source_rgba(*Ruiby_dsl.cv_color_html("#000"))
#ctx.stroke
start=eend
}
end
def cv.draw_arc2(x,y,r,start,eend,width,color_stroke,color_fill=nil)
w,ctx=@currentCanvasCtx
ctx.set_line_width( width )
ctx.set_source_rgba(*Ruiby_dsl.cv_color_html(color_fill ? color_fill : color_stroke))
ctx.arc( x,y, r, Math::PI*2.0*start, Math::PI*2.0*eend );
color_fill ? ctx.fill : ctx.stroke
end
def cv.draw_arc(x,y,r,start,eend,width,color_stroke,color_fill=nil)
w,ctx=@currentCanvasCtx
ctx.set_line_width( width )
ctx.set_source_rgba(*Ruiby_dsl.cv_color_html(color_fill ? color_fill : color_stroke))
ctx.move_to(x,y)
ctx.arc( x,y, r, Math::PI*2.0*start, Math::PI*2.0*eend );
ctx.close_path
color_fill ? ctx.fill : ctx.stroke
end
def cv.draw_image(x,y,filename,sx=1,sy=sx)
w,cr=@currentCanvasCtx
pxb=w.app_window.get_pixbuf(filename)
scale(x,y,sx,sy) { cr.set_source_pixbuf(pxb,0,0) ; cr.paint}
[pxb.width,pxb.height]
end
# draw in scale factor
# scale(20,100,2,4) { w.draw_line(10,10,20,20) ; ... }
# the line will be scaling by x/y factor 2 and 4 relative to center
# point x=10 y=100
def cv.scale(cx,cy,ax,ay=nil,&blk)
ay=ax unless ay
w,cr=@currentCanvasCtx
cr.translate(cx,cy)
cr.scale(ax,ay)
blk.call rescue Message.error $!
cr.scale(1.0/ax,1.0/ay)
cr.translate(-cx,-cy)
end
def cv.rotation(cx,cy,a,&blk)
w,cr=@currentCanvasCtx
cr.translate(cx,cy)
cr.rotate(Math::PI*2.0*a)
blk.call rescue Message.error $!
cr.rotate(-Math::PI*2.0*a)
cr.translate(-cx,-cy)
end
def cv.translate(lxy,dx=0,dy=0)
lxy.each_slice(2).inject([]) {|l,(x,y)| l <<x+dx; l <<y+dy}
end
def cv.rotate(lxy,x0,y0,angle)
sa,ca=Math.sin(angle),Math.cos(angle)
lxy.each_slice(2).inject([]) {|l,(x,y)| l << ((x-x0)*ca-(y-y0)*sa)+x0 ; l << ((x-x0)*sa+(y-y0)*ca)+y0}
end
cv
end
# update a canvas
def force_update(canvas) canvas.queue_draw unless canvas.destroyed? end
# define action on button_press
# action must return an object whici will be transmit to motion/release handler
def on_canvas_button_press(&blk)
_accept?(:handler)
@currentCanvas.signal_connect('button-press-event') { |w,e|
ret=w.set_memo(blk.call(w,e)) rescue error($!)
force_update(w)
ret
}
end
def on_canvas_resize(&blk)
_accept?(:handler)
@currentCanvas.signal_connect('configure_event') { |w,e|
ret=blk.call(w,e.width,e.height) rescue error($!)
force_update(w)
ret
}
end
# define action on mouse button press on current canvas definition
def on_canvas_button_release(&blk)
_accept?(:handler)
@currentCanvas.signal_connect('button_release_event') { |w,e|
ret=blk.call(w,e,w.get_memo) rescue error($!)
w.set_memo(nil)
force_update(w)
ret
}
end
# define action on mouse button motion on current canvas definition
def on_canvas_button_motion(&blk )
_accept?(:handler)
@currentCanvas.signal_connect('motion_notify_event') { |w,e|
next unless w.get_memo()
w.set_memo(blk.call(w,e,w.get_memo)) rescue error($!)
force_update(w)
}
end
# define action on keyboard press on current **window** definition
def on_canvas_key_press(&blk)
_accept?(:handler)
p "signal key press"
@currentCanvas.signal_connect('key_press_event') { |w,e|
p "signal key press ok #{e}"
blk.call(w,e,Gdk::Keyval.to_name(e.keyval)) rescue error($!)
force_update(w)
}
end
# define the drawing on current canvas definition
def on_canvas_draw(&blk)
_accept?(:handler)
@currentCanvas.signal_connect( 'draw' ) do |w,cr|
cr.set_line_join(Cairo::LINE_JOIN_ROUND)
cr.set_line_cap(Cairo::LINE_CAP_ROUND)
cr.set_line_width(2)
cr.set_source_rgba(1,1,1,1)
cr.paint
begin
w.instance_eval { @currentCanvasCtx=[w,cr] }
blk.call(w,cr)
w.set_memo(false)
#w.instance_eval { @currentCanvasCtx=nil }
rescue Exception => e
( after(1) { error(e) } ) if w.get_memo()!=true
w.draw_text(5,20,"Canvas ERROR...",1,"#EEE","#000")
w.draw_text(5,30,e.to_s,1,"#EEE","#000")
w.set_memo(true)
end
end
end
# DEPRECATED; Create a drawing area, for pixel draw
# option can define closure :mouse_down :mouse_up :mouse_move
# for interactive actions
def canvasOld(width,height,option={})
puts "*** DEPRECATED: use canvas do end in place of canvasOld ***"
autoslot()
w=DrawingArea.new()
w.width_request=width
w.height_request=height
w.events |= ( ::Gdk::EventMask::BUTTON_PRESS_MASK | ::Gdk::EventMask::POINTER_MOTION_MASK | ::Gdk::EventMask::BUTTON_RELEASE_MASK)
w.signal_connect( 'draw' ) { |w1,cr|
cr.save {
cr.set_line_join(Cairo::LINE_JOIN_ROUND)
cr.set_line_cap(Cairo::LINE_CAP_ROUND)
cr.set_line_width(2)
cr.set_source_rgba(1,1,1,1)
cr.paint
if option[:expose]
begin
option[:expose].call(w1,cr)
rescue Exception => e
bloc=option[:expose]
option.delete(:expose)
after(1) { error(e) }
after(3000) { puts "reset expose bloc" ;option[:expose] = nil }
end
end
}
}
@do=nil
w.signal_connect('button_press_event') { |wi,e| @do = option[:mouse_down].call(wi,e) rescue error($!) ; force_update(wi) } if option[:mouse_down]
w.signal_connect('button_release_event') { |wi,e| (option[:mouse_up].call(wi,e,@do) rescue error($!)) if @do ; @do=nil ; force_update(wi) if @do } if option[:mouse_up]
w.signal_connect('motion_notify_event') { |wi,e| (@do = option[:mouse_move].call(wi,e,@do) rescue error($!)) if @do ; force_update(wi) if @do } if option[:mouse_move]
w.signal_connect('key_press_event') { |wi,e| (option[:key_press].call(wi,e) rescue error($!)) ; force_update(wi) } if option[:key_press]
attribs(w,option)
def w.redraw()
self.queue_draw_area(0,0,self.width_request,self.height_request)
end
w
end
end