Morphic is the name given to Pharo's graphical interface. Morphic is written in Pharo, so it is fully portable between operating systems. As a consequence, Pharo looks exactly the same on Unix, MacOS and Windows. What distinguishes Morphic from most other user interface toolkits is that it does not have separate modes for composing and running the interface: all the graphical elements can be assembled and disassembled by the user, at any time. (We thank Hilaire Fernandes for permission to base this chapter on his original article in French.)

101. The history of Morphic

Morphic was developed by John Maloney and Randy Smith for the Self programming language, starting around 1993. Maloney later wrote a new version of Morphic for Squeak, but the basic ideas behind the Self version are still alive and well in Pharo Morphic: directness and liveness. Directness means that the shapes on the screen are objects that can be examined or changed directly, that is, by clicking on them using a mouse. Liveness means that the user interface is always able to respond to user actions: information on the screen is continuously updated as the world that it describes changes. A simple example of this is that you can detach a menu item and keep it as a button.

Bring up the World Menu and meta-click once on it to bring up its morphic halo, then meta-click again on a menu item you want to detach, to bring up that item's halo. (Recall that you should set halosEnabled in the Preferences browser.) Now drag that item elsewhere on the screen by grabbing the black handle (see Figure 0.107), as shown in Figure 0.108.

0.107. The grab handle.

0.108. Detaching a morph, here the Playground menu item, to make it an independent button.

All of the objects that you see on the screen when you run Pharo are Morphs, that is, they are instances of subclasses of class Morph. Morph itself is a large class with many methods; this makes it possible for subclasses to implement interesting behaviour with little code. You can create a morph to represent any object, although how good a representation you get depends on the object!

To create a morph to represent a string object, execute the following code in a Playground.

'Morph' asMorph openInWorld
0.66. Creation of a String Morph

This creates a Morph to represent the string 'Morph', and then opens it (that is, displays it) in the world, which is the name that Pharo gives to the screen. You should obtain a graphical element (a Morph), which you can manipulate by meta-clicking.

Of course, it is possible to define morphs that are more interesting graphical representations than the one that you have just seen. The method asMorph has a default implementation in class Object class that just creates a StringMorph. So, for example, Color tan asMorph returns a StringMorph labeled with the result of Color tan printString. Let's change this so that we get a coloured rectangle instead.

Open a browser on the Color class and add the following method to it:

Color >> asMorph
	^ Morph new color: self
0.67. Getting a morph for an instance of Color

Now execute Color orange asMorph openInWorld in a Playground. Instead of the string-like morph, you get an orange rectangle (see Figure 0.109)!

0.109. Color orange asMorph openInWorld with our new method.

102. Manipulating morphs

Morphs are objects, so we can manipulate them like any other object in Pharo: by sending messages, we can change their properties, create new subclasses of Morph, and so on.

Every morph, even if it is not currently open on the screen, has a position and a size. For convenience, all morphs are considered to occupy a rectangular region of the screen; if they are irregularly shaped, their position and size are those of the smallest rectangular box that surrounds them, which is known as the morph's bounding box, or just its bounds. The position method returns a Point that describes the location of the morph's upper left corner (or the upper left corner of its bounding box). The origin of the coordinate system is the screen's upper left corner, with y coordinates increasing down the screen and x coordinates increasing to the right. The extent method also returns a point, but this point specifies the width and height of the morph rather than a location.

Type the following code into a playground and Do it:

joe := Morph new color: Color blue.
joe openInWorld.
bill := Morph new color: Color red.
bill openInWorld.
0.68. Creation of two Color Morph

Then type joe position and then Print it. To move joe, execute joe position: (joe position + (10@3)) repeatedly (see Figure 0.110).

0.110. Bill and Joe after 10 moves.

It is possible to do a similar thing with size. joe extent answers joe's size; to have joe grow, execute joe extent: (joe extent * 1.1). To change the color of a morph, send it the color: message with the desired Color object as argument, for instance, joe color: Color orange. To add transparency, try joe color: (Color orange alpha: 0.5).

To make bill follow joe, you can repeatedly execute this code:

bill position: (joe position + (100@0))
0.69. Make bill follow joe

If you move joe using the mouse and then execute this code, bill will move so that it is 100 pixels to the right of joe.

You can see the result on Figure 0.111

0.111. Bill follows Joe.

103. Composing morphs

One way of creating new graphical representations is by placing one morph inside another. This is called composition; morphs can be composed to any depth.

You can place a morph inside another by sending the message addMorph: to the container morph.

Try adding a morph to another one:

balloon := BalloonMorph new color: Color yellow.
joe addMorph: balloon.
balloon position: joe position.
0.70. Create a Balloon inside Joe

The last line positions the balloon at the same coordinates as joe. Notice that the coordinates of the contained morph are still relative to the screen, not to the containing morph. There are many methods available to position a morph; browse the geometry protocol of class Morph to see for yourself. For example, to center the balloon inside joe, execute balloon center: joe center.

0.112. The balloon is contained inside joe, the translucent orange morph.

If you now try to grab the balloon with the mouse, you will find that you actually grab joe, and the two morphs move together: the balloon is embedded inside joe. It is possible to embed more morphs inside joe. In addition to doing this programmatically, you can also embed morphs by direct manipulation.

104. Creating and drawing your own morphs

While it is possible to make many interesting and useful graphical representations by composing morphs, sometimes you will need to create something completely different.

To do this you define a subclass of Morph and override the drawOn: method to change its appearance.

The morphic framework sends the message drawOn: to a morph when it needs to redisplay the morph on the screen. The parameter to drawOn: is a kind of Canvas; the expected behaviour is that the morph will draw itself on that canvas, inside its bounds. Let's use this knowledge to create a cross-shaped morph.

Using the browser, define a new class CrossMorph inheriting from Morph:

Morph subclass: #CrossMorph
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'PBE-Morphic'
0.71. Defining CrossMorph

We can define the drawOn: method like this:

CrossMorph >> drawOn: aCanvas
	| crossHeight crossWidth horizontalBar verticalBar |
	crossHeight := self height / 3.0.
	crossWidth := self width / 3.0.
	horizontalBar := self bounds insetBy: 0 @ crossHeight.
	verticalBar := self bounds insetBy: crossWidth @ 0.
	aCanvas fillRectangle: horizontalBar color: self color.
	aCanvas fillRectangle: verticalBar color: self color
0.72. Drawing a CrossMorph

0.113. A CrossMorph with its halo; you can resize it as you wish.

Sending the bounds message to a morph answers its bounding box, which is an instance of Rectangle. Rectangles understand many messages that create other rectangles of related geometry. Here, we use the insetBy: message with a point as its argument to create first a rectangle with reduced height, and then another rectangle with reduced width.

To test your new morph, execute CrossMorph new openInWorld.

The result should look something like Figure 0.113. However, you will notice that the sensitive zone — where you can click to grab the morph — is still the whole bounding box. Let's fix this.

When the Morphic framework needs to find out which Morphs lie under the cursor, it sends the message containsPoint: to all the morphs whose bounding boxes lie under the mouse pointer. So, to limit the sensitive zone of the morph to the cross shape, we need to override the containsPoint: method.

Define the following method in class CrossMorph:

CrossMorph >> containsPoint: aPoint
	| crossHeight crossWidth horizontalBar verticalBar |
	crossHeight := self height / 3.0.
	crossWidth := self width / 3.0.
	horizontalBar := self bounds insetBy: 0 @ crossHeight.
	verticalBar := self bounds insetBy: crossWidth @ 0.
	^ (horizontalBar containsPoint: aPoint) or: [ verticalBar containsPoint: aPoint ]
0.73. Shaping the sensitive zone of the CrossMorph

This method uses the same logic as drawOn:, so we can be confident that the points for which containsPoint: answers true are the same ones that will be colored in by drawOn. Notice how we leverage the containsPoint: method in class Rectangle to do the hard work.

There are two problems with the code in the two methods above.

The most obvious is that we have duplicated code. This is a cardinal error: if we find that we need to change the way that horizontalBar or verticalBar are calculated, we are quite likely to forget to change one of the two occurrences. The solution is to factor out these calculations into two new methods, which we put in the private protocol:

CrossMorph >> horizontalBar
	| crossHeight |
	crossHeight := self height / 3.0.
	^ self bounds insetBy: 0 @ crossHeight
0.74. horizontalBar
CrossMorph >> verticalBar
	| crossWidth |
	crossWidth := self width / 3.0.
	^ self bounds insetBy: crossWidth @ 0
0.75. verticalBar

We can then define both drawOn: and containsPoint: using these methods:

CrossMorph >> drawOn: aCanvas
	aCanvas fillRectangle: self horizontalBar color: self color.
	aCanvas fillRectangle: self verticalBar color: self color
0.76. Refactored CrossMorph >> drawOn:
CrossMorph >> containsPoint: aPoint
	^ (self horizontalBar containsPoint: aPoint) or: [ self verticalBar containsPoint: aPoint ]
0.77. Refactored CrossMorph >> containsPoint:

This code is much simpler to understand, largely because we have given meaningful names to the private methods. In fact, it is so simple that you may have noticed the second problem: the area in the center of the cross, which is under both the horizontal and the vertical bars, is drawn twice. This doesn't matter when we fill the cross with an opaque colour, but the bug becomes apparent immediately if we draw a semi-transparent cross, as shown in Figure 0.114.

0.114. The center of the cross is filled twice with the color.

0.115. The cross-shaped morph, showing a row of unfilled pixels.

Execute the following code in a playground, line by line:

CrossMorph new openInWorld;
   bounds: (0@0 corner: 200@200);
   color: (Color blue alpha: 0.4)
0.78. Use this code to show a bug

The fix is to divide the vertical bar into three pieces, and to fill only the top and bottom. Once again we find a method in class Rectangle that does the hard work for us: r1 areasOutside: r2 answers an array of rectangles comprising the parts of r1 outside r2. Here is the revised code:

CrossMorph >> drawOn: aCanvas
	| topAndBottom |
	aCanvas fillRectangle: self horizontalBar color: self color.
	topAndBottom := self verticalBar areasOutside: self horizontalBar.
	topAndBottom do: [ :each | aCanvas fillRectangle: each color: self color ]
0.79. The revised CrossMorph >> drawOn: method, which fills the center of the cross once

This code seems to work, but if you try it on some crosses and resize them, you may notice that at some sizes, a one-pixel wide line separates the bottom of the cross from the remainder, as shown in Figure 0.115. This is due to rounding: when the size of the rectangle to be filled is not an integer, fillRectangle: color: seems to round inconsistently, leaving one row of pixels unfilled. We can work around this by rounding explicitly when we calculate the sizes of the bars.

CrossMorph >> horizontalBar
	| crossHeight |
	crossHeight := (self height / 3.0) rounded.
	^ self bounds insetBy: 0 @ crossHeight
0.80. CrossMorph >> horizontalBar with explicit rounding
CrossMorph >> verticalBar
	| crossWidth |
	crossWidth := (self width / 3.0) rounded.
	^ self bounds insetBy: crossWidth @ 0
0.81. CrossMorph >> verticalBar with explicit rounding

105. Interaction and animation

To build live user interfaces using morphs, we need to be able to interact with them using the mouse and keyboard. Moreover, the morphs need to be able respond to user input by changing their appearance and position — that is, by animating themselves.

105.1. Mouse events

When a mouse button is pressed, Morphic sends each morph under the mouse pointer the message handlesMouseDown:. If a morph answers true, then Morphic immediately sends it the mouseDown: message; it also sends the mouseUp: message when the user releases the mouse button. If all morphs answer false, then Morphic initiates a drag-and-drop operation. As we will discuss below, the mouseDown: and mouseUp: messages are sent with an argument — a MouseEvent object — that encodes the details of the mouse action.

Let's extend CrossMorph to handle mouse events. We start by ensuring that all crossMorphs answer true to the handlesMouseDown: message.

Add this method to CrossMorph:

CrossMorph >> handlesMouseDown: anEvent
	^ true
0.82. Declaring that CrossMorph will react to mouse clicks

Suppose that when we click on the cross, we want to change the color of the cross to red, and when we action-click on it, we want to change the color to yellow. This can be accomplished by the mouseDown: method as follows:

CrossMorph >> mouseDown: anEvent
	anEvent redButtonPressed
		ifTrue: [ self color: Color red ].	"click"
	anEvent yellowButtonPressed
		ifTrue: [ self color: Color yellow ].	"action-click"
	self changed
0.83. Reacting to mouse clicks by changing the morph's color

Notice that in addition to changing the color of the morph, this method also sends self changed. This makes sure that morphic sends drawOn: in a timely fashion.

Note also that once the morph handles mouse events, you can no longer grab it with the mouse and move it. Instead you have to use the halo: meta-click on the morph to make the halo appear and grab either the brown move handle (see Figure 0.116) or the black pickup handle (see Figure 0.117) at the top of the morph.

0.116. Move Handle button.

0.117. Grab Handle button.

The anEvent argument of mouseDown: is an instance of MouseEvent, which is a subclass of MorphicEvent. MouseEvent defines the redButtonPressed and yellowButtonPressed methods. Browse this class to see what other methods it provides to interrogate the mouse event.

105.2. Keyboard events

To catch keyboard events, we need to take three steps.

  1. Give the keyboard focus to a specific morph. For instance, we can give focus to our morph when the mouse is over it.
  2. Handle the keyboard event itself with the handleKeystroke: method. This message is sent to the morph that has keyboard focus when the user presses a key.
  3. Release the keyboard focus when the mouse is no longer over our morph.

Let's extend CrossMorph so that it reacts to keystrokes. First, we need to arrange to be notified when the mouse is over the morph. This will happen if our morph answers true to the handlesMouseOver: message

Declare that CrossMorph will react when it is under the mouse pointer.

CrossMorph >> handlesMouseOver: anEvent
0.84. We want to handle mouse over events

This message is the equivalent of handlesMouseDown: for the mouse position. When the mouse pointer enters or leaves the morph, the mouseEnter: and mouseLeave: messages are sent to it.

Define two methods so that CrossMorph catches and releases the keyboard focus, and a third method to actually handle the keystrokes.

CrossMorph >> mouseEnter: anEvent
	anEvent hand newKeyboardFocus: self
0.85. Getting the keyboard focus when the mouse enters the morph
CrossMorph >> mouseLeave: anEvent
	anEvent hand newKeyboardFocus: nil
0.86. Handing back the focus when the pointer goes away
CrossMorph >> handleKeystroke: anEvent
	| keyValue |
	keyValue := anEvent keyValue.
	keyValue = 30	 "up arrow"
		ifTrue: [self position: self position - (0 @ 1)].
	keyValue = 31	 "down arrow"
		ifTrue: [self position: self position + (0 @ 1)].
	keyValue = 29	 "right arrow"
		ifTrue: [self position: self position + (1 @ 0)].
	keyValue = 28	 "left arrow"
		ifTrue: [self position: self position - (1 @ 0)]
0.87. Receiving and handling keyboard events

We have written this method so that you can move the morph using the arrow keys. Note that when the mouse is no longer over the morph, the handleKeystroke: message is not sent, so the morph stops responding to keyboard commands. To discover the key values, you can open a Transcript window and add Transcript show: anEvent keyValue to the handleKeystroke: method.

The anEvent argument of handleKeystroke: is an instance of KeyboardEvent, another subclass of MorphicEvent. Browse this class to learn more about keyboard events.

105.3. Morphic animations

Morphic provides a simple animation system with two main methods: step is sent to a morph at regular intervals of time, while stepTime specifies the time in milliseconds between steps. stepTime is actually the minimum time between steps. If you ask for a stepTime of 1 ms, don't be surprised if Pharo is too busy to step your morph that often. In addition, startStepping turns on the stepping mechanism, while stopStepping turns it off again. isStepping can be used to find out whether a morph is currently being stepped.

Make CrossMorph blink by defining these methods as follows:

CrossMorph >> stepTime
	^ 100
0.88. Defining the animation time interval
CrossMorph >> step
	(self color diff: Color black) < 0.1
		ifTrue: [ self color: Color red ]
		ifFalse: [ self color: self color darker ]
0.89. Making a step in the animation

To start things off, you can open an inspector on a CrossMorph using the debug handle (see Figure 0.118) in the morphic halo, type self startStepping in the small playground pane at the bottom, and Do it.

0.118. The debug handle button.

Alternatively, you can modify the handleKeystroke: method so that you can use the + and - keys to start and stop stepping. Add the following code to the handleKeystroke: method:

	keyValue = $+ asciiValue
		ifTrue: [ self startStepping ].
	keyValue = $- asciiValue
		ifTrue: [ self stopStepping ].
0.90. Add the beginning and the end of the steps

106. Interactors

To prompt the user for input, the UIManager class provides a large number of ready to use dialog boxes. For instance, the request:initialAnswer: method returns the string entered by the user (Figure 0.119).

UIManager default request: 'What''s your name?' initialAnswer: 'no name'
0.91. Use the UIManager

0.119. An input dialog.

0.120. Pop-up menu.

To display a popup menu, use one of the various chooseFrom: methods (Figure 0.120):

UIManager default
	chooseFrom: #('circle' 'oval' 'square' 'rectangle' 'triangle')
	lines: #(2 4) message: 'Choose a shape'
0.92. Use the UIManager to open a popup

Browse the UIManager class and try out some of the interaction methods offered.

107. Drag-and-drop

Morphic also supports drag-and-drop. Let's examine a simple example with two morphs, a receiver morph and a dropped morph. The receiver will accept a morph only if the dropped morph matches a given condition: in our example, the morph should be blue. If it is rejected, the dropped morph decides what to do.

Let's first define the receiver morph:

Morph subclass: #ReceiverMorph
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'PBE-Morphic'
0.93. Defining a morph on which we can drop other morphs

Now define the initialization method in the usual way:

ReceiverMorph >> initialize
	super initialize.
	color := Color red.
	bounds := 0 @ 0 extent: 200 @ 200
0.94. Initializing ReceiverMorph

How do we decide if the receiver morph will accept or reject the dropped morph? In general, both of the morphs will have to agree to the interaction. The receiver does this by responding to wantsDroppedMorph:event:. Its first argument is the dropped morph, and the second the mouse event, so that the receiver can, for example, see if any modifier keys were held down at the time of the drop. The dropped morph is also given the opportunity to check and see if it likes the morph onto which it is being dropped, by responding to the message wantsToBeDroppedInto:. The default implementation of this method (in class Morph) answers true.

ReceiverMorph >> wantsDroppedMorph: aMorph event: anEvent
	^ aMorph color = Color blue
0.95. Accept dropped morphs based on their color

What happens to the dropped morph if the receiving morph doesn't want it? The default behaviour is for it to do nothing, that is, to sit on top of the receiving morph, but without interacting with it. A more intuitive behavior is for the dropped morph to go back to its original position. This can be achieved by the receiver answering true to the message repelsMorph:event: when it doesn't want the dropped morph:

ReceiverMorph >> repelsMorph: aMorph event: anEvent
	^ (self wantsDroppedMorph: aMorph event: anEvent) not
0.96. Changing the behaviour of the dropped morph when it is rejected

That's all we need as far as the receiver is concerned.

Create instances of ReceiverMorph and EllipseMorph in a playground:

ReceiverMorph new openInWorld;
    bounds: (100@100 corner: 200@200).
EllipseMorph new openInWorld.
0.97. Create an instance of ReceiverMorph

0.121. A ReceiverMorph and an EllipseMorph.

Try to drag and drop the yellow EllipseMorph onto the receiver. It will be rejected and sent back to its initial position.

To change this behaviour, change the color of the ellipse morph to the color blue (by sending it the message color: Color blue; right after new). Blue morphs should be accepted by the ReceiverMorph.

Let's create a specific subclass of Morph, named DroppedMorph, so we can experiment a bit more:

Morph subclass: #DroppedMorph
	instanceVariableNames: ''
	classVariableNames: ''
	package: 'PBE-Morphic'
0.98. Defining a morph we can drag-and-drop onto ReceiverMorph
DroppedMorph >> initialize
	super initialize.
	color := Color blue.
	self position: 250 @ 100
0.99. Initializing DroppedMorph

Now we can specify what the dropped morph should do when it is rejected by the receiver; here it will stay attached to the mouse pointer:

DroppedMorph >> rejectDropMorphEvent: anEvent
	| h |
	h := anEvent hand.
	WorldState addDeferredUIMessage: [ h grabMorph: self ].
	anEvent wasHandled: true
0.100. Reacting when the morph was dropped but rejected

Sending the hand message to an event answers the hand, an instance of HandMorph that represents the mouse pointer and whatever it holds. Here we tell the World that the hand should grab self, the rejected morph.

Create two instances of DroppedMorph, and then drag and drop them onto the receiver.

ReceiverMorph new openInWorld.
morph := (DroppedMorph new color: Color blue) openInWorld.
morph position: (morph position + (70@0)).
(DroppedMorph new color: Color green) openInWorld.
0.101. Open a now DroppedMorph

0.122. Creation of DroppedMorph and ReceiverMorph.

The green morph is rejected and therefore stays attached to the mouse pointer.

108. A complete example

Let's design a morph to roll a die. Clicking on it will display the values of all sides of the die in a quick loop, and another click will stop the animation.

0.123. The die in Morphic

Define the die as a subclass of BorderedMorph instead of Morph, because we will make use of the border.

BorderedMorph subclass: #DieMorph
	instanceVariableNames: 'faces dieValue isStopped'
	classVariableNames: ''
	package: 'PBE-Morphic'
0.102. Defining the die morph

The instance variable faces records the number of faces on the die; we allow dice with up to 9 faces! dieValue records the value of the face that is currently displayed, and isStopped is true if the die animation has stopped running. To create a die instance, we define the faces: n method on the class side of DieMorph to create a new die with n faces.

DieMorph class >> faces: aNumber
	^ self new faces: aNumber
0.103. Creating a new die with the number of faces we like

The initialize method is defined on the instance side in the usual way; remember that new automatically sends initialize to the newly-created instance.

DieMorph >> initialize
	super initialize.
	self extent: 50 @ 50.
		borderWidth: 2;
	self setBorderStyle: #complexRaised.
	self fillStyle direction: self extent.
	self color: Color green.
	dieValue := 1.
	faces := 6.
	isStopped := false
0.104. Initializing instances of DieMorph

We use a few methods of BorderedMorph to give a nice appearance to the die: a thick border with a raised effect, rounded corners, and a color gradient on the visible face. We define the instance method faces: to check for a valid parameter as follows:

DieMorph >> faces: aNumber
	"Set the number of faces"

	((aNumber isInteger and: [ aNumber > 0 ]) and: [ aNumber <= 9 ])
		ifTrue: [ faces := aNumber ]
0.105. Setting the number of faces of the die

It may be good to review the order in which the messages are sent when a die is created. For instance, if we start by evaluating DieMorph faces: 9:

Before defining drawOn:, we need a few methods to place the dots on the displayed face:

DieMorph >> face1
	^ {(0.5 @ 0.5)}
0.106. Nine methods for placing points on the faces of the die
DieMorph >> face2
	^{0.25@0.25 . 0.75@0.75}
DieMorph >> face3
	^{0.25@0.25 . 0.75@0.75 . 0.5@0.5}
DieMorph >> face4
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75}
DieMorph >> face5
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.5@0.5}
DieMorph >> face6
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5}
DieMorph >> face7
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5}
DieMorph >> face8
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5 . 0.5@0.25}
DieMorph >> face9
	^{0.25@0.25 . 0.75@0.25 . 0.75@0.75 . 0.25@0.75 . 0.25@0.5 . 0.75@0.5 . 0.5@0.5 . 0.5@0.25 . 0.5@0.75}

These methods define collections of the coordinates of dots for each face. The coordinates are in a square of size 1x1; we will simply need to scale them to place the actual dots.

The drawOn: method does two things: it draws the die background with the super-send, and then draws the dots.

DieMorph >> drawOn: aCanvas
	super drawOn: aCanvas.
	(self perform: ('face', dieValue asString) asSymbol)
		do: [:aPoint | self drawDotOn: aCanvas at: aPoint]
0.107. Drawing the die morph

The second part of this method uses the reflective capacities of Pharo. Drawing the dots of a face is a simple matter of iterating over the collection given by the faceX method for that face, sending the drawDotOn:at: message for each coordinate. To call the correct faceX method, we use the perform: method which sends a message built from a string, ('face', dieValue asString) asSymbol. You will encounter this use of perform: quite regularly.

DieMorph >> drawDotOn: aCanvas at: aPoint
		fillOval: (Rectangle
			center: self position + (self extent * aPoint)
			extent: self extent / 6)
		color: Color black
0.108. Drawing a single dot on a face

Since the coordinates are normalized to the [0:1] interval, we scale them to the dimensions of our die: self extent * aPoint.

We can already create a die instance from a playground (see result on Figure 0.124):

(DieMorph faces: 6) openInWorld.
0.109. Create a Die 6

0.124. A new die 6 with (DieMorph faces: 6) openInWorld

To change the displayed face, we create an accessor that we can use as myDie dieValue: 5:

DieMorph >> dieValue: aNumber
	((aNumber isInteger and: [ aNumber > 0 ]) and: [ aNumber <= faces ])
		ifTrue: [
			dieValue := aNumber.
			self changed ]
0.110. Setting the current value of the die

0.125. Result of (DieMorph faces: 6) openInWorld; dieValue: 5.

Now we will use the animation system to show quickly all the faces:

DieMorph >> stepTime
	^ 100
0.111. Animating the die
DieMorph >> step
	isStopped ifFalse: [self dieValue: (1 to: faces) atRandom]

Now the die is rolling!

To start or stop the animation by clicking, we will use what we learned previously about mouse events. First, activate the reception of mouse events:

DieMorph >> handlesMouseDown: anEvent
	^ true
0.112. Handling mouse clicks to start and stop the animation
DieMorph >> mouseDown: anEvent
	anEvent redButtonPressed
		ifTrue: [isStopped := isStopped not]

Now the die will roll or stop rolling when we click on it.

109. More about the canvas

The drawOn: method has an instance of Canvas as its sole argument; the canvas is the area on which the morph draws itself. By using the graphics methods of the canvas you are free to give the appearance you want to a morph. If you browse the inheritance hierarchy of the Canvas class, you will see that it has several variants. The default variant of Canvas is FormCanvas, and you will find the key graphics methods in Canvas and FormCanvas. These methods can draw points, lines, polygons, rectangles, ellipses, text, and images with rotation and scaling.

It is also possible to use other kinds of canvas, for example to obtain transparent morphs, more graphics methods, antialiasing, and so on. To use these features you will need an AlphaBlendingCanvas or a BalloonCanvas. But how can you obtain such a canvas in a drawOn: method, when drawOn: receives an instance of FormCanvas as its argument? Fortunately, you can transform one kind of canvas into another.

To use a canvas with a 0.5 alpha-transparency in DieMorph, redefine drawOn: like this:

DieMorph >> drawOn: aCanvas
	| theCanvas |
	theCanvas := aCanvas asAlphaBlendingCanvas: 0.5.
	super drawOn: theCanvas.
	(self perform: ('face', dieValue asString) asSymbol)
		do: [:aPoint | self drawDotOn: theCanvas at: aPoint]
0.113. Drawing a translucent die

That's all you need to do!

0.126. The die displayed with alpha-transparency

110. Chapter summary

Morphic is a graphical framework in which graphical interface elements can be dynamically composed.