Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Canvas Widget to Use a Drawing Stack #383

Merged
merged 76 commits into from Jun 12, 2018
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
9847b3a
Create dict of contexts each with a stack of draw operations for Gtk+
danyeaw Feb 25, 2018
b0f0a96
Move the cairo context to one of the list values of the context dict
danyeaw Feb 25, 2018
38f2783
Update core widget to remove on_draw and add 'remove' parameters
danyeaw Feb 25, 2018
6e57a3c
Update tests and dummy implementation to remove on_draw and add remove
danyeaw Feb 25, 2018
068eed2
Remove native.context from context_dict, use on_draw handler in Gtk+
danyeaw Feb 26, 2018
772568c
Switch append and remove to be methods on the implementation
danyeaw Feb 27, 2018
119fe0b
Wrap function calls with parameters with lambdas and cleanup names
danyeaw Feb 28, 2018
8d2f185
Fix draw tiberius function not called
danyeaw Feb 28, 2018
07cb34d
Cleanup dummy implementation
danyeaw Feb 28, 2018
63ba1f3
Initial change of interface to make drawing objects classes
danyeaw Mar 28, 2018
cb0c3ba
Update context on interface with drawing_objects added to stack as list
danyeaw Mar 31, 2018
4634c7f
Change from using a default context object to a root context stack
danyeaw Apr 1, 2018
718bd51
Stop function calls while adding to stack and add root context traversal
danyeaw Apr 2, 2018
7143693
Fix stroked lines not all same width
danyeaw Apr 2, 2018
83f3f18
Add support for implementations with no drawing callback
danyeaw Apr 2, 2018
503fc10
Fix closed_path missing 2 positional arguments
danyeaw Apr 2, 2018
a99e997
Update dummy implementation and tests for new stack based Canvas
danyeaw Apr 3, 2018
09f7b71
Update tutorial 4 to match stack based Canvas implementation
danyeaw Apr 3, 2018
8b86b9e
Add ability to modify all values of a drawing object
danyeaw Apr 7, 2018
bfff265
Use travertino color parsing methods
danyeaw Apr 7, 2018
d5eb4f7
Fix tests using float numbers for rgb values
danyeaw Apr 8, 2018
c19e059
Replace create_context method with the context method yielding a context
danyeaw Apr 8, 2018
9382102
Update Canvas docs with new stack based API proposal
danyeaw Apr 9, 2018
572173d
Merge master
danyeaw Apr 12, 2018
f320a11
Update docs to require context yields to be named
danyeaw Apr 12, 2018
1264fdb
Update tests to require context yields to be named
danyeaw Apr 12, 2018
480b001
Update Canvas docs to match tutorial4 and align with new context design
danyeaw Apr 13, 2018
c64bda8
Add mixin class and context tree to allow explicit drawing on contexts
danyeaw Apr 15, 2018
3cd1f94
Remove inherited members from drawing object classes
danyeaw Apr 15, 2018
4cd33e0
Define representation of drawing object Classes to show attributes
danyeaw Apr 16, 2018
6ab42f5
Add multiple context support on the implementation layer
danyeaw Apr 19, 2018
0c9a57f
Remove fill, stroke, and closed path as implementation contexts
danyeaw Apr 20, 2018
79b750c
Update dummy implementation to match updated stack design
danyeaw Apr 21, 2018
4ccef5a
Fix tests for actions now performed by the Canvas
danyeaw May 7, 2018
e81fce3
Fix context objects not being traversed properly
danyeaw May 9, 2018
9e8a3b8
Merge dev9 master
danyeaw May 16, 2018
0473854
Update canvas docs to add more details and make use of autodoc
danyeaw May 16, 2018
ecc54e4
Change title to keyword argument in MainWindow
danyeaw May 17, 2018
cd6d8d7
Cleanup Canvas mixin to pass in args and remove extra property methods
danyeaw May 18, 2018
b7c1bb3
Move transformations from implementation to interface layer
danyeaw May 21, 2018
e3251e4
Change to setting a single root context on the implementation layer
danyeaw May 22, 2018
c5a0a8d
Fix Gtk+ ellipse by replacing previous transformations with native calls
danyeaw May 22, 2018
98c3536
Change to passing Gtk+ context through drawing object calls
danyeaw May 23, 2018
e57c4c8
Add matrix multiply method for transformations
danyeaw May 24, 2018
f3e2eeb
Remove duplicate methods in Gtk+ implementation
danyeaw May 27, 2018
474c60e
Fix arc missing native_context when creating ellipse
danyeaw May 27, 2018
3bee912
Correct transform parameter definition
danyeaw May 27, 2018
dc41e78
Update canvas example so that it works using updated API
danyeaw May 28, 2018
7b4fff5
Update Canvas class repr to match calling the class
danyeaw May 30, 2018
8ca82f3
Change canvas drawing class modify methods to default to current values
danyeaw May 31, 2018
3b9d47e
Add implicit drawing object access for Fill and Closed Path
danyeaw May 31, 2018
6ac44c8
Fix Stroke class arguments have wrong name
danyeaw May 31, 2018
0547001
Improve canvas text coverage and remove matrix multiply test
danyeaw Jun 2, 2018
8a0f97c
Fix correct default values for optional parameters
danyeaw Jun 2, 2018
c76a0d4
Change transformation operations to apply to the canvas
danyeaw Jun 3, 2018
36adc08
Update code formatting
danyeaw Jun 3, 2018
97f5604
Add ability to call with native_context for transformation classes
danyeaw Jun 3, 2018
8745508
Changed drawing stack to a flat list instead of nested lists
danyeaw Jun 3, 2018
ba997c4
Update Tutorial 4 to use built-in Toga colors and font definitions
danyeaw Jun 3, 2018
d3a5a0e
Simplify interface method return statements and fix Gtk+ constant
danyeaw Jun 4, 2018
945c138
Remove Gtk+ static methods and restore traversal
danyeaw Jun 4, 2018
6b8e86f
Restore nested drawing stack, remove child context propagation
danyeaw Jun 5, 2018
46730bf
Remove debug statement and change to default else in radius calculation
danyeaw Jun 5, 2018
b710538
Undo find and replace error
danyeaw Jun 6, 2018
f279823
Change to setting drawing object properties without modify methods
danyeaw Jun 7, 2018
cb6eafd
Move traversal to interface and make contexts draw their own objects
danyeaw Jun 9, 2018
a00769f
Limit redraw events to exiting a context and drawing directly to canvas
danyeaw Jun 9, 2018
c661832
Move transformation operations to the Canvas class
danyeaw Jun 10, 2018
dfe7193
Replace __call__ with draw method in order to draw drawing objects
danyeaw Jun 10, 2018
0b5e443
Remove closure around Gtk+ draw callback
danyeaw Jun 10, 2018
7fddf73
Use color properties directly during Context init
danyeaw Jun 10, 2018
f466f93
Update color format in tutorial docs
danyeaw Jun 10, 2018
ab669ae
Refactor add_canvas_to_child method to @canvas.setter
danyeaw Jun 10, 2018
d3f6732
Change class hierarchy to remove the CanvasContextMixin
danyeaw Jun 10, 2018
da7a5d5
Cleanup extra variable initialization
danyeaw Jun 10, 2018
368921e
Cleanup public API by making draw protected and renaming methods
danyeaw Jun 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 55 additions & 1 deletion docs/reference/api/widgets/canvas.rst
Expand Up @@ -15,16 +15,70 @@ The canvas is used for creating a blank widget that you can draw on.
Usage
-----

Simple usage to draw a black circle on the screen using the arc drawing object:

.. code-block:: Python

import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.fill() as fill:
fill.arc(50, 50, 15)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor note - this isn't drawing an "image", it's drawing a filled circle. Explaining exactly what is going to be drawn (and why) would be helpful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, more detailed added to what is supposed to be drawn.


More advanced usage for something like a vector drawing app where you would
want to modify the parameters of the drawing objects. Here we draw a black
circle and black rectangle. We then change the size of the circle, move the
rectangle, and finally delete the rectangle.

.. code-block:: Python

import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.fill() as fill:
arc1 = fill.arc(x=50, y=50, radius=15)
rect1 = fill.rect(x=50, y=50, width=15, height=15)

arc1.x, arc1.y, arc1.radius = (25, 25, 5)
rect1.x = 75
fill.remove(rect1)

Use of drawing contexts, for example with a platformer game. Here you would
want to modify the x/y coordinate of a drawing context that draws each
character on the canvas. First, we create a hero context. Next, we create a
black circle and a black outlined rectangle for the hero's body. Finally, we
move the hero by 10 on the x-axis.

.. code-block:: Python

import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.context() as hero:
with hero.fill() as body:
body.arc(50, 50, 15)
with hero.stroke() as outline:
outline.rect(50, 50, 15, 15)

hero.translate(10, 0)

canvas = toga.Canvas()

Reference
---------

Main Interface
^^^^^^^^^^^^^^

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might it help to use automodule here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, I updated to use the automodule with exclude-members.

.. autoclass:: toga.widgets.canvas.Canvas
:members:
:undoc-members:
:inherited-members:
:exclude-members: canvas, propogate_canvas, add_child, redraw,
add_drawing_object

Lower-Level Classes
^^^^^^^^^^^^^^^^^^^

.. automodule:: toga.widgets.canvas
:members:
:exclude-members: Canvas
35 changes: 18 additions & 17 deletions docs/tutorial/tutorial-4.rst
Expand Up @@ -12,33 +12,37 @@ One of the main capabilities needed to create many types of GUI applications is
the ability to draw and manipulate lines, shapes, text, and other graphics. To
do this in Toga, we use the Canvas Widget.

Utilizing the Canvas is easy as determining the drawing operations you want to
perform, placing them in a function, and then creating a new Canvas while
passing the function to the on_draw parameter.
Utilizing the Canvas is as easy as determining the drawing operations you want to
perform and then creating a new Canvas. All drawing objects that are created
with one of the drawing operations are returned so that they can be modified or
removed.

1. We first define the drawing operations we want to perform in a new function::

def draw_tiberius(self, canvas, context):
self.canvas.set_context(context)
self.fill_head()
def draw_eyes(self):
with self.canvas.fill(color='rgba(255, 255, 255, 1)') as eye_whites:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this color syntax still current?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think so. This is a valid format in the color method in travertino. Would you prefer that we use named colors in the examples like WHITE and BLACK?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I swapped to using the built-in Toga color definitions, and the rgb() definition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've changed to travertino rgb elsewhere, but not in this specific example :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping this one in case it's been missed.

eye_whites.arc(58, 92, 15)
eye_whites.arc(88, 92, 15, math.pi, 3 * math.pi)

The function you want to draw with should also be defined to include canvas and
context arguments, and make use of the set_context method.
Notice that we also created and used a new fill context called eye_whites. The
"with" keyword that is used for the fill operation causes everything draw using
the context to be filled with a color. In this example we filled two circular
eyes with the color white.

2. Next we create a new Canvas, and pass in the draw_tiberius method::
2. Next we create a new Canvas::

self.canvas = toga.Canvas(on_draw=self.draw_tiberius)
self.canvas = toga.Canvas(style=Pack(flex=1))

That's all there is to! In this example we also add our canvas to the MainWindow
through use of the Box Widget::

box = toga.Box(children=[self.canvas])
self.main_window.content = box

You'll also notice in the full example below that some of the drawing operations
use the "with" keyword to utilize context managers including closed_path, fill,
and stroke. This reduces the repetition of commands while utilizing these basic
drawing capabilities.
You'll also notice in the full example below that the drawing operations utilize
contexts in addition to fill including context, closed_path, and stroke. This
reduces the repetition of commands as well as groups drawing operations so that
they can be modified together.

.. image:: screenshots/tutorial-4.png

Expand All @@ -47,7 +51,4 @@ Here's the source code
.. literalinclude:: /../examples/tutorial4/tutorial/app.py
:language: python

Although not shown in this tutorial, it is also possible to directly do simple
draw operations without passing in the on_draw callable function.

In this example, we see a new Toga widget - :class:`.Canvas`.
7 changes: 4 additions & 3 deletions examples/canvas/canvas/app.py
Expand Up @@ -15,9 +15,10 @@ def startup(self):
# Show the main window
self.main_window.show()

with canvas.stroke():
with canvas.closed_path(50, 50):
canvas.line_to(100, 100)
with canvas.stroke() as stroker:
with stroker.closed_path(50, 50) as closer:
closer.line_to(100, 100)
closer.line_to(100, 50)


def main():
Expand Down
139 changes: 64 additions & 75 deletions examples/tutorial4/tutorial/app.py
@@ -1,17 +1,20 @@
import math

import toga
from toga.color import WHITE, rgb
from toga.font import SANS_SERIF
from toga.style import Pack


class StartApp(toga.App):
def startup(self):
# Window class
# Main window of the application with title and size
# Main window of the application with title and size
self.main_window = toga.MainWindow(title=self.name, size=(148, 200))

self.canvas = toga.Canvas(on_draw=self.draw_tiberius, style=Pack(flex=1))
# Create canvas and draw tiberius on it
self.canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[self.canvas])
self.draw_tiberius()

# Add the content on the main window
self.main_window.content = box
Expand All @@ -20,91 +23,77 @@ def startup(self):
self.main_window.show()

def fill_head(self):
with self.canvas.fill():
self.canvas.fill_style('rgba(149.0, 119, 73, 1)')
self.canvas.move_to(112, 103)
self.canvas.line_to(112, 113)
self.canvas.ellipse(73, 114, 39, 47, 0, 0, math.pi)
self.canvas.line_to(35, 84)
self.canvas.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
self.canvas.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)
with self.canvas.fill(color=rgb(149, 119, 73)) as head_filler:
head_filler.move_to(112, 103)
head_filler.line_to(112, 113)
head_filler.ellipse(73, 114, 39, 47, 0, 0, math.pi)
head_filler.line_to(35, 84)
head_filler.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
head_filler.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)

def stroke_head(self):
with self.canvas.stroke():
with self.canvas.closed_path(112, 103):
self.canvas.line_width(4.0)
self.canvas.stroke_style()
self.canvas.line_to(112, 113)
self.canvas.ellipse(73, 114, 39, 47, 0, 0, math.pi)
self.canvas.line_to(35, 84)
self.canvas.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
self.canvas.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)
with self.canvas.stroke(line_width=4.0) as head_stroker:
with head_stroker.closed_path(112, 103) as closed_head:
closed_head.line_to(112, 113)
closed_head.ellipse(73, 114, 39, 47, 0, 0, math.pi)
closed_head.line_to(35, 84)
closed_head.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
closed_head.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)

def draw_eyes(self):
self.canvas.line_width(4.0)
with self.canvas.fill():
self.canvas.fill_style('rgba(255, 255, 255, 1)')
self.canvas.arc(58, 92, 15)
self.canvas.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.stroke():
self.canvas.stroke_style('rgba(0, 0, 0, 1)')
self.canvas.arc(58, 92, 15)
self.canvas.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.fill():
self.canvas.arc(58, 97, 3)
self.canvas.arc(88, 97, 3)
with self.canvas.fill(color=WHITE) as eye_whites:
eye_whites.arc(58, 92, 15)
eye_whites.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.stroke(line_width=4.0) as eye_outline:
eye_outline.arc(58, 92, 15)
eye_outline.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.fill() as eye_pupils:
eye_pupils.arc(58, 97, 3)
eye_pupils.arc(88, 97, 3)

def draw_horns(self):
# Right horn
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(112, 99)
self.canvas.quadratic_curve_to(145, 65, 139, 36)
self.canvas.quadratic_curve_to(130, 60, 109, 75)
with self.canvas.stroke():
self.canvas.stroke_style()
self.canvas.move_to(112, 99)
self.canvas.quadratic_curve_to(145, 65, 139, 36)
self.canvas.quadratic_curve_to(130, 60, 109, 75)
# Left horn
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(35, 99)
self.canvas.quadratic_curve_to(2, 65, 6, 36)
self.canvas.quadratic_curve_to(17, 60, 37, 75)
with self.canvas.stroke():
self.canvas.stroke_style()
self.canvas.move_to(35, 99)
self.canvas.quadratic_curve_to(2, 65, 6, 36)
self.canvas.quadratic_curve_to(17, 60, 37, 75)
with self.canvas.context() as r_horn:
with r_horn.fill(color=rgb(212, 212, 212)) as r_horn_filler:
r_horn_filler.move_to(112, 99)
r_horn_filler.quadratic_curve_to(145, 65, 139, 36)
r_horn_filler.quadratic_curve_to(130, 60, 109, 75)
with r_horn.stroke(line_width=4.0) as r_horn_stroker:
r_horn_stroker.move_to(112, 99)
r_horn_stroker.quadratic_curve_to(145, 65, 139, 36)
r_horn_stroker.quadratic_curve_to(130, 60, 109, 75)
with self.canvas.context() as l_horn:
with l_horn.fill(color=rgb(212, 212, 212)) as l_horn_filler:
l_horn_filler.move_to(35, 99)
l_horn_filler.quadratic_curve_to(2, 65, 6, 36)
l_horn_filler.quadratic_curve_to(17, 60, 37, 75)
with l_horn.stroke(line_width=4.0) as l_horn_stroker:
l_horn_stroker.move_to(35, 99)
l_horn_stroker.quadratic_curve_to(2, 65, 6, 36)
l_horn_stroker.quadratic_curve_to(17, 60, 37, 75)

def draw_nostrils(self):
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(45, 145)
self.canvas.bezier_curve_to(51, 123, 96, 123, 102, 145)
self.canvas.ellipse(73, 114, 39, 47, 0, math.pi / 4, 3 * math.pi / 4)
with self.canvas.fill():
self.canvas.fill_style()
self.canvas.arc(63, 140, 3)
self.canvas.arc(83, 140, 3)
with self.canvas.stroke():
self.canvas.move_to(45, 145)
self.canvas.bezier_curve_to(51, 123, 96, 123, 102, 145)
with self.canvas.fill(color=rgb(212, 212, 212)) as nose_filler:
nose_filler.move_to(45, 145)
nose_filler.bezier_curve_to(51, 123, 96, 123, 102, 145)
nose_filler.ellipse(73, 114, 39, 47, 0, math.pi / 4, 3 * math.pi / 4)
with self.canvas.fill() as nostril_filler:
nostril_filler.arc(63, 140, 3)
nostril_filler.arc(83, 140, 3)
with self.canvas.stroke(line_width=4.0) as nose_stroker:
nose_stroker.move_to(45, 145)
nose_stroker.bezier_curve_to(51, 123, 96, 123, 102, 145)

def draw_text(self):
x = 32
y = 185
font = toga.Font(family='sans-serif', size=20)
font = toga.Font(family=SANS_SERIF, size=20)
width, height = font.measure('Tiberius', tight=True)
with self.canvas.stroke():
self.canvas.rect(x - 10, y - height + 2, width, height + 2)
with self.canvas.fill():
self.canvas.fill_style('rgba(149.0, 119, 73, 1)')
self.canvas.write_text('Tiberius', x, y, font)

def draw_tiberius(self, canvas, context):
self.canvas.set_context(context)
with self.canvas.stroke(line_width=4.0) as rect_stroker:
rect_stroker.rect(x - 10, y - height + 2, width, height + 2)
with self.canvas.fill(color=rgb(149, 119, 73)) as text_filler:
text_filler.write_text('Tiberius', x, y, font)

def draw_tiberius(self):
self.fill_head()
self.draw_eyes()
self.draw_horns()
Expand Down