An FRP-based GUI system

Một phần của tài liệu Manning functional reactive programming (Trang 272 - 284)

The implementation of a graphical user interface (GUI) system was one of the early use cases of object-oriented programming. Today, pretty much every GUI system has the same basic design—to the point that people have trouble imagining any other way of doing things.

We think FRP is a superior paradigm for this particular problem, and we’ll prove it.

We would love to see a full GUI system implemented using FRP, so we introduce Fridgets, a tiny GUI system implemented entirely in FRP. It handles the drawing of wid- gets, input handling, focus, and form layout.

12.7.1 Drawable

Fridgets draws its own widgets using 2D graphics. Things to be drawn are represented by a Drawable class like the one used in chapter 9. In the following listing, you can see that it’s a base class with a polymorphic draw() method. You can also append draw- ables together.

package fridgets;

import java.awt.Graphics;

public class Drawable {

public void draw(Graphics g) {}

public final Drawable append(Drawable second) { Drawable first = this;

return new Drawable() {

public void draw(Graphics g) { first.draw(g);

second.draw(g);

} };

} }

Listing 12.14 A thing that can be drawn

12.7.2 Fridget

Fridget is short for “FRP widget,” and Fridget is the base class for all fridgets. Figure 12.3 shows a fat FrButton fridget.

Fridget (listing 12.15) is a container for a function of five inputs and three outputs. The inputs are as follows:

■ Cell<Optional<Dimension>> size—The actual size of the fridget after layout.

This can be Optional.empty(), meaning the size isn’t yet known.

■ Stream<MouseEvent> sMouse—Mouse input events.

■ Stream<KeyEvent> sKey—Keyboard input events.

■ Cell<Long> focus—The ID of the fridget that currently has focus.

■ Supply idSupply—A supply of unique IDs.

And these are the outputs:

■ Cell<Drawable> drawable—How to draw the fridget.

■ Cell<Dimension> desiredSize—The size the fridget wants to be, which is the input to the layout algorithm.

■ Stream<Long> sChangeFocus—A request to change keyboard input focus to the fridget with the specified ID.

package fridgets;

import java.awt.Dimension;

import java.awt.event.KeyEvent;

import java.awt.event.MouseEvent;

import java.util.Optional;

import nz.sodium.*;

public abstract class Fridget { public static class Output { public Output(

Cell<Drawable> drawable, Cell<Dimension> desiredSize, Stream<Long> sChangeFocus) { this.drawable = drawable;

this.desiredSize = desiredSize;

this.sChangeFocus = sChangeFocus;

}

public Cell<Drawable> drawable;

public Cell<Dimension> desiredSize;

public Stream<Long> sChangeFocus;

}

public Fridget(Lambda5<

Cell<Optional<Dimension>>, Stream<MouseEvent>,

Stream<KeyEvent>, Cell<Long>, Supply, Output> reify_) { this.reify_ = reify_;

Listing 12.15 Fridget interface

Figure 12.3 FrButton fridget

251 An FRP-based GUI system

}

private final Lambda5<

Cell<Optional<Dimension>>, Stream<MouseEvent>,

Stream<KeyEvent>, Cell<Long>, Supply, Output> reify_;

public final Output reify(

Cell<Optional<Dimension>> size,

Stream<MouseEvent> sMouse, Stream<KeyEvent> sKey, Cell<Long> focus, Supply idSupply) {

return reify_.apply(size, sMouse, sKey, focus, idSupply);

} }

12.7.3 Your first fridget: FrButton

Listing 12.16 gives the code for the button fridget. It calculates its desired size based on measuring the label text but draws itself as it’s told to by the input size. In addition to meeting the requirements of the Fridget interface, it also exports a stream, sClicked, that fires when the button is clicked. It draws itself differently when the mouse is held down.

package fridgets;

import java.awt.*;

import java.awt.event.MouseEvent;

import java.util.Optional;

import nz.sodium.*;

public class FrButton extends Fridget { public FrButton(Cell<String> label) { this(label, new StreamLoop<Unit>());

}

private FrButton(Cell<String> label, StreamLoop<Unit> sClicked) { super((size, sMouse, sKey, focus, idSupply) -> {

Stream<Unit> sPressed = Stream.filterOptional(

sMouse.snapshot(size, (e, osz) ->

osz.isPresent() &&

e.getID() == MouseEvent.MOUSE_PRESSED

&& e.getX() >= 2 && e.getX() < osz.get().width-2 && e.getY() >= 2 && e.getY() < osz.get().height-2 ? Optional.of(Unit.UNIT)

: Optional.empty() )

);

Stream<Unit> sReleased = Stream.filterOptional(

sMouse.map(e -> e.getID() == MouseEvent.MOUSE_RELEASED ? Optional.of(Unit.UNIT)

: Optional.empty()));

Cell<Boolean> pressed = sPressed.map(u -> true)

.orElse(sReleased.map(u -> false)) .hold(false);

sClicked.loop(sReleased.gate(pressed));

Font font = new Font("Helvetica", Font.PLAIN, 13);

Canvas c = new Canvas();

Listing 12.16 FrButton, the button fridget

FontMetrics fm = c.getFontMetrics(font);

Cell<Dimension> desiredSize = label.map(label_ ->

new Dimension(

fm.stringWidth(label_) + 14, fm.getHeight() + 10));

return new Output(

label.lift(size, pressed,

(label_, osz, pressed_) -> new Drawable() { public void draw(Graphics g) {

if (osz.isPresent()) { Dimension sz = osz.get();

int w = fm.stringWidth(label_);

g.setColor(pressed_ ? Color.darkGray : Color.lightGray);

g.fillRect(3, 3, sz.width-6, sz.height-6);

g.setColor(Color.black);

g.drawRect(2, 2, sz.width-5, sz.height-5);

int centerX = sz.width / 2;

g.setFont(font);

g.drawString(label_, (sz.width - w)/2,

(sz.height - fm.getHeight())/2 + fm.getAscent());

} } } ),

desiredSize, new Stream<Long>() );

});

this.sClicked = sClicked;

}

public final Stream<Unit> sClicked;

}

Note that each fridget sees the world in such a way that its top left is at the origin (0,0). Mouse input events and drawables are adjusted to create this illusion. That way, the fridget doesn’t need to care where it is in the window.

Listing 12.17 shows how you construct the widgets. The way you do this is similar to the SWidgets used in chapters 1 and 2. FrView converts a Fridget into a Swing compo- nent so you can attach it as the content of your application’s Swing frame. You’ll see the code for that shortly.

import fridgets.*;

import javax.swing.*;

import nz.sodium.*;

public class button {

public static void main(String[] args) { Listing 12.17 Button fridget example

253 An FRP-based GUI system

JFrame frame = new JFrame("button");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setContentPane(Transaction.run(() -> { FrButton b = new FrButton(new Cell<>("OK"));

Listener l = b.sClicked.listen(

u -> System.out.println("clicked!"));

return new FrView(frame, b) {

public void removeNotify() { super.removeNotify();

l.unlisten();

} };

}));

frame.setSize(360,120);

frame.setVisible(true);

} }

To run this, check it out if you haven’t done so already, and then run it like this:

git clone https://github.com/SodiumFRP/sodium cd sodium/book/fridgets/java

mvn test -Pbutton or ant button

12.7.4 Bringing a Fridget to life with FrView

Listing 12.18 shows how you interface a Fridget to Java Swing. The code basically feeds in the mouse, keyboard, and window resize events and tells the Fridget to take up the entire window. You don’t do any layout here.

Handling of current focus is trivially simple. You hold() what the fridget sets it to and feed that back in.

package fridgets;

import java.awt.event.ComponentAdapter;

import java.awt.event.ComponentEvent;

import java.awt.event.KeyAdapter;

import java.awt.event.KeyEvent;

import java.awt.event.MouseAdapter;

import java.awt.event.MouseMotionListener;

import java.awt.event.MouseEvent;

import java.awt.*;

import javax.swing.*;

import java.util.Optional;

import nz.sodium.*;

public class FrView extends JPanel {

public FrView(JFrame frame, Fridget fr) {

StreamSink<MouseEvent> sMouse = new StreamSink<>();

StreamSink<KeyEvent> sKey = new StreamSink<>();

Listing 12.18 Viewing a Fridget as a Swing component

Javaism to stop listening when the component is disposed

addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { sMouse.send(e);

}

public void mouseReleased(MouseEvent e) { sMouse.send(e);

} });

addMouseMotionListener(new MouseMotionListener() { public void mouseDragged(MouseEvent e) { sMouse.send(e);

}

public void mouseMoved(MouseEvent e) { sMouse.send(e);

} });

size = new CellSink<Optional<Dimension>>(Optional.empty());

addComponentListener(new ComponentAdapter() {

public void componentResized(ComponentEvent e) { if (e.getID() == ComponentEvent.COMPONENT_RESIZED) size.send(Optional.of(getSize()));

} });

frame.addKeyListener(new KeyAdapter() { public void keyTyped(KeyEvent e) { sKey.send(e);

} });

CellLoop<Long> focus = new CellLoop<>();

Fridget.Output fo = fr.reify(size, sMouse, sKey, focus, new Supply());

focus.loop(fo.sChangeFocus.hold(-1l));

this.drawable = fo.drawable;

l = l.append(Operational.updates(drawable).listen(d -> { repaint();

}));

}

private Listener l = new Listener();

private final CellSink<Optional<Dimension>> size;

private final Cell<Drawable> drawable;

public void paintComponent(Graphics g) { super.paintComponent(g);

drawable.sample().draw(g);

}

public void removeNotify() { l.unlisten();

super.removeNotify();

}

public void handleKeys(JFrame frame) { }

}

255 An FRP-based GUI system

12.7.5 Layout

Layout of widgets in the window is handled by a FrFlow fridget. It takes as input a direction, HORIZONTAL or VERTICAL, and a list of child fridgets to lay out. It lays them out one after another, horizontally or vertically according to each fridget’s requested size. Figure 12.4 shows an example of FrFlow; the code is in the next listing.

package fridgets;

import java.awt.Dimension;

import java.util.Collection;

import java.util.Optional;

import nz.sodium.*;

public class FrFlow extends Fridget {

public enum Direction { HORIZONTAL, VERTICAL };

public FrFlow(Direction dir, Collection<Fridget> fridgets) { super((size, sMouse, sKey, focus, idSupply) -> {

Cell<Dimension> desiredSize = new Cell<>(new Dimension(0,0));

Cell<Drawable> drawable = new Cell<>(new Drawable());

Stream<Long> sChangeFocus = new Stream<Long>();

for (Fridget fridget : fridgets) {

CellLoop<Optional<Dimension>> childSz = new CellLoop<>();

Fridget.Output fo = new FrTranslate(fridget, dir == Direction.HORIZONTAL

? desiredSize.map(dsz -> new Dimension(dsz.width, 0)) : desiredSize.map(dsz -> new Dimension(0, dsz.height))) .reify(childSz, sMouse, sKey, focus,

idSupply.child1());

idSupply = idSupply.child2();

childSz.loop(

size.lift(fo.desiredSize, (osz, foDsz) ->

osz.isPresent()

? Optional.of(dir == Direction.HORIZONTAL ? new Dimension(foDsz.width, osz.get().height) : new Dimension(osz.get().width, foDsz.height)) : Optional.empty()

) );

desiredSize = desiredSize.lift(fo.desiredSize, dir == Direction.HORIZONTAL

? (dsz, foDsz) -> new Dimension(

dsz.width + foDsz.width, Listing 12.19 Widget layout with FrFlow

Figure 12.4 Laying out two buttons horizontally with FrFlow

Translates the child’s coordinate space(see listing 12.20)

Peels off a unique ID for the child widget

The width is the child’s desired width.

The height of the child is your height.

Desired width:

sum of children

dsz.height > foDsz.height ? dsz.height : foDsz.height) : (dsz, foDsz) -> new Dimension(

dsz.width > foDsz.width ? dsz.width : foDsz.width, dsz.height + foDsz.height));

drawable = drawable.lift(fo.drawable, (drA, drB) -> drA.append(drB));

sChangeFocus = sChangeFocus.orElse(fo.sChangeFocus);

}

return new Fridget.Output(drawable, desiredSize, sChangeFocus);

});

} }

NOTE FrFlow takes a static collection of fridgets to display, but you could enhance this code to make it dynamic by passing Cell<Collection<Fridget>>

instead. There’s an exercise for you if you’d like one.

Listing 12.19 used FrTranslate, which we give in the next listing. It intercepts incom- ing mouse events and outgoing drawables, translating their coordinate space accord- ing to a specified (x,y) offset.

package fridgets;

import java.awt.Dimension;

import java.awt.Graphics;

import java.awt.event.MouseEvent;

import nz.sodium.*;

public class FrTranslate extends Fridget {

public FrTranslate(Fridget fr, Cell<Dimension> offset) { super((size, sMouse, sKey, focus, idSupply) -> { Stream<MouseEvent> sMouseNew =

sMouse.snapshot(offset, (e, o) ->

new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), e.getModifiers(),

e.getX() - o.width, e.getY() - o.height, e.getClickCount(), e.isPopupTrigger()));

Fridget.Output fo = fr.reify(size, sMouseNew, sKey, focus, idSupply);

Cell<Drawable> drawableNew = fo.drawable.lift(offset, (dr, o) -> new Drawable() {

public void draw(Graphics g) { g.translate(o.width, o.height);

dr.draw(g);

g.translate(-o.width, -o.height);

} });

return new Fridget.Output(drawableNew, fo.desiredSize, fo.sChangeFocus);

});

} }

Listing 12.20 FrTranslate: translating a fridget’s coordinate space Desired height:

maximum of children

Combines drawables Combines focus

requests

257 An FRP-based GUI system

The next listing shows the main program flow that demonstrates the use of FrFlow.

import fridgets.*;

import javax.swing.*;

import java.util.ArrayList;

import nz.sodium.*;

public class flow {

public static void main(String[] args) { JFrame frame = new JFrame("flow");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setContentPane(Transaction.run(() -> { FrButton ok = new FrButton(new Cell<>("OK"));

FrButton cancel = new FrButton(new Cell<>("Cancel"));

ArrayList<Fridget> fridgets = new ArrayList<>();

fridgets.add(ok);

fridgets.add(cancel);

Fridget dialog = new FrFlow(FrFlow.Direction.HORIZONTAL, fridgets);

Listener l =

ok.sClicked.listen(

u -> System.out.println("OK")) .append(

cancel.sClicked.listen(

u -> System.out.println("Cancel") )

);

return new FrView(frame, dialog) { public void removeNotify() { super.removeNotify();

l.unlisten();

} };

}));

frame.setSize(360,120);

frame.setVisible(true);

} }

To run this, check it out if you haven’t done so already, and then run it like this:

git clone https://github.com/SodiumFRP/sodium cd sodium/book/fridgets/java

mvn test -Pflow or ant flow

12.7.6 A form with text fields

Now let’s look at a more complex exam- ple: a form with two text fields and two buttons. See figure 12.5.

Figure 12.5 A complex example:

two text fields and two buttons Listing 12.21 flow example main program

The next listing shows FrTextField. It’s a lot of code, but think about how much work it does. Yet it’s not much more complicated than FrButton.

package fridgets;

import java.awt.*;

import java.awt.event.MouseEvent;

import java.util.Optional;

import nz.sodium.*;

class TextUpdate {

TextUpdate(String txt, int newX) { this.txt = txt;

this.newX = newX;

}

String txt;

int newX;

};

public class FrTextField extends Fridget { public FrTextField(String initText) { this(initText, new CellLoop<String>());

}

private FrTextField(String initText, CellLoop<String> text) { super((size, sMouse, sKey, focus, idSupply) -> {

Stream<Integer> sPressed = Stream.filterOptional(

sMouse.snapshot(size, (e, osz) ->

osz.isPresent() &&

e.getID() == MouseEvent.MOUSE_PRESSED

&& e.getX() >= 2 && e.getX() < osz.get().width-2 && e.getY() >= 2 && e.getY() < osz.get().height-2 ? Optional.of(e.getX() - 2)

: Optional.empty() )

);

CellLoop<Integer> x = new CellLoop<>();

long myId = idSupply.get();

Cell<Boolean> haveFocus = focus.map(f_id -> f_id == myId);

Font font = new Font("Helvetica", Font.PLAIN, 13);

Canvas c = new Canvas();

FontMetrics fm = c.getFontMetrics(font);

Stream<TextUpdate> sTextUpdate = Stream.filterOptional(

sKey.gate(haveFocus) .snapshot(text, (key, txt) -> {

int x_ = x.sample();

if (key.getKeyChar() == (char)8) { if (x_ > 0)

return Optional.of(new TextUpdate(

txt.substring(0,x_-1)+

txt.substring(x_), x_-1));

else

return Optional.empty();

Listing 12.22 FrTextField fridget

X position in the text field where the mouse was clicked

Do you have focus?

Ignores keypresses if no focus Updates the text

contents according to keypress, current text, and cursor position

259 An FRP-based GUI system

} else {

char[] keyChs = new char[1];

keyChs[0] = key.getKeyChar();

return Optional.of(new TextUpdate(

txt.substring(0, x_)+

new String(keyChs)+

txt.substring(x_), x_ + 1));

} }) );

x.loop(sPressed.snapshot(text, (xCoord, txt) -> {

for (int x_ = 1; x_ <= txt.length(); x_++)

if (xCoord < fm.stringWidth(txt.substring(0, x_))) return x_-1;

return txt.length();

})

.orElse(sTextUpdate.map(tu -> tu.newX)) .hold(0));

text.loop(sTextUpdate.map(tu -> tu.txt).hold(initText));

Cell<Dimension> desiredSize = text.map(txt ->

new Dimension(

fm.stringWidth(txt) + 14, fm.getHeight() + 10));

return new Output(

text.lift(x, haveFocus, size,

(txt, x_, haveFocus_, osz) -> new Drawable() { public void draw(Graphics g) {

if (osz.isPresent()) { Dimension sz = osz.get();

g.setColor(Color.white);

g.fillRect(3, 3, sz.width-6, sz.height-6);

g.setColor(Color.black);

g.drawRect(2, 2, sz.width-5, sz.height-5);

int centerX = sz.width / 2;

g.setFont(font);

int cursorX = fm.stringWidth(

txt.substring(0, x_));

g.drawString(txt, 4,

(sz.height - fm.getHeight())/2 + fm.getAscent());

if (haveFocus_) { g.setColor(Color.red);

g.drawLine(4 + cursorX, 4,

4 + cursorX, sz.height - 5);

} } } }), desiredSize,

sPressed.map(xCoord -> myId) );

});

Moves the cursor on a mouse click

Moves the cursor after a keypress Holds text

changes

Draws the cursor only if you have focus

this.text = text;

}

public final Cell<String> text;

}

Following is the main program for textfield.

import fridgets.*;

import javax.swing.*;

import java.util.ArrayList;

import nz.sodium.*;

public class textfield {

public static void main(String[] args) { JFrame frame = new JFrame("button");

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setContentPane(Transaction.run(() -> { FrTextField firstName = new FrTextField("Joe");

FrTextField lastName = new FrTextField("Bloggs");

FrButton ok = new FrButton(new Cell<>("OK"));

FrButton cancel = new FrButton(new Cell<>("Cancel"));

ArrayList<Fridget> fridgets = new ArrayList<>();

fridgets.add(ok);

fridgets.add(cancel);

Fridget buttons = new FrFlow(FrFlow.Direction.HORIZONTAL, fridgets);

fridgets = new ArrayList<>();

fridgets.add(firstName);

fridgets.add(lastName);

fridgets.add(buttons);

Fridget dialog =

new FrFlow(FrFlow.Direction.VERTICAL, fridgets);

Listener l = ok.sClicked

.map(u -> firstName.text.sample()+" "+

lastName.text.sample())

.listen(name -> System.out.println("OK: "+name)) .append(

cancel.sClicked.listen(

u -> System.out.println("Cancel") )

);

return new FrView(frame, dialog) { public void removeNotify() { super.removeNotify();

l.unlisten();

} };

}));

frame.setSize(360,120);

frame.setVisible(true);

} }

Listing 12.23 textfield example main program

261 Summary

To run this, check it out if you haven’t done so already, and then run it like this:

git clone https://github.com/SodiumFRP/sodium cd sodium/book/fridgets/java

mvn test -Ptextfield or ant textfield

Một phần của tài liệu Manning functional reactive programming (Trang 272 - 284)

Tải bản đầy đủ (PDF)

(362 trang)