With the new Duvet release, this article looks at how easy it is to run Fantom code in your web browser.
Fantom is a JVM language that also compiles to Javascript. We all know that, because it's cool! But the core Fantom libraries never gave a simple means of delivering code to a web browser.
The Duvet library, together with the BedSheet web framework, hopes to fill that gap!
The new release of Duvet
comes with a small, yet very powerful additional method:
HtmlInjector.injectFantomMethod(...)
When you call that method, Duvet
then:
- calculates a dependency tree of Fantom pods that compile to Javascript,
- assembles them into RequireJS modules,
- computes client URLs to the modules,
- applies any asset caching strategies (see Cold Feet),
- serialises any method arguments into JSON,
- injects scripts to require the Fantom pod modules,
- injects a script to call the Fantom method.
Phew! That's a whole lotta work for a little method call - let's see it in action!
Duvet Demo
First make sure your Duvet
pod and Fantom environment is up to date:
fanr install -r http://eggbox.fantomfactory.org/fanr "afDuvet 1.1"
Next create a file called DuvetDemo.fan
and cut'n'paste the following into it:
using build using afIoc using afBedSheet using afDuvet using fwt::Dialog using fwt::Window** This class is compiled, delivered and run in the browser.@Js class MyJavascript { Void greet(Str name) { Dialog.openInfo(Window(), "Fantom says, '${name}'") } } class IndexPage { @Inject HtmlInjector? injector Text render() {// inject Fantom code into the web pageinjector.injectFantomMethod(MyJavascript#greet, ["Hello Mum!"])// let Duvet inject all it needs into a plain HTML shellreturn Text.fromHtml("<html><head></head><body><h1>Duvet by Alien-Factory</h1></body></html>") } } const class AppModule { @Contribute { serviceType=Routes# } static Void contributeRoutes(Configuration conf) { conf.add(Route(`/`, IndexPage#render)) } } class Build : BuildPod { new make() { podName = "duvetDemo" summary = "Run Fantom code in your browser!" meta = [ "proj.name" : "Duvet Demo", "afIoc.module" : "duvetDemo::AppModule", ] depends = [ "sys 1.0", "fwt 1.0", "build 1.0", "afIoc 3.0", "afBedSheet 1.5", "afDuvet 1.1" ] srcDirs = [`DuvetDemo.fan`] } }
Note that the MyJavascript
class has the @Js
facet. All classes to be loaded by the browser need to have this facet. It tells Fantom to compile it to Javascript.
Also, Fantom Javascript is served from pods. Because of this, the example needs to be compiled into a pod and can't be run as script.
Running the script will run the Build
class which compiles the source file into a pod called duvetDemo
:
C:\>fan DuvetDemo.fan compile [duvetDemo] Compile [duvetDemo] FindSourceFiles [1 files] CompileJs WritePod [file:/C:/Apps/fantom/fan/lib/fan/duvetDemo.pod] BUILD SUCCESS [302ms]!
Now start the BedSheet
application server, passing in our new duvetDemo
pod:
C:\> fan afBedSheet duvetDemo -port 8080 [web] WispService started on port 8080 [afBedSheet] Starting Bed App 'duvetDemo' on port 8080 [afBedSheet] Found pod 'duvetDemo' [afBedSheet] Found mod 'duvetDemo::AppModule' [afIoc] Adding module definitions from pod 'duvetDemo' [afIoc] Adding module definition for duvetDemo::AppModule [afIoc] Adding module definition for afBedSheet::BedSheetModule [afIoc] Adding module definition for afIocConfig::IocConfigModule [afIoc] Adding module definition for afIocEnv::IocEnvModule [afIoc] Adding module definition for afDuvet::DuvetModule ___ __ _____ _ / _ | / /_____ _____ / ___/__ ___/ /_________ __ __ / _ | / // / -_|/ _ /===/ __// _ \/ _/ __/ _ / __|/ // / /_/ |_|/_//_/\__|/_//_/ /_/ \_,_/__/\__/____/_/ \_, / Alien-Factory BedSheet v1.5.2, IoC v3.0.2 /___/ IoC Registry built in 137ms and started up in 752ms Bed App 'Duvet Demo' listening on http://localhost:8080/
Point your browser at http://localhost:8080/ and viola!
Ta daa! Yes, you've just run Fantom code in your browser!
"Nice! That's a fancy Javascript alert!"
That's because it isn't a Javascript alert!
If you look closely at the MyJavascript
class you'll notice it's using FWT code. (FWT stands for Fantom Windowing Toolkit, it is a wrapper around SWT from Eclipse.)
"Woah!!! An FWT Dialog!?? That's cool! What else can it do?"
Let's find out!
Duvet FWT Demo
Fantom has an FWT demo so let's take that and tweak it slightly to make it work in Javascript. We just need to add the @Js
facet to all the classes and remove any non-Javascript widgets such as WebBrowsers and FileDialogs.
Re-using the last example, we'll substitute the MyJavascript
class with our new FwtDemo
class. Cut'n'paste the following into a file called DuvetFwtDemo.fan
:
using gfx using fwt using build::BuildPod using afIoc using afBedSheet using afBedSheet::Text as BsText using afDuvet class IndexPage { @Inject HtmlInjector? injector BsText render() { injector.injectFantomMethod(FwtDemo#main, null, ["fwt.window.root": "fwt-window"]) return BsText.fromHtml("<html><head></head><body><div id='fwt-window' style='width:1000px; height:600px; position:relative;'></div></body></html>") } } const class AppModule { @Contribute { serviceType=Routes# } static Void contributeRoutes(Configuration conf) { conf.add(Route(`/`, IndexPage#render)) } } class Build : BuildPod { new make() { podName = "duvetFwtDemo" summary = "Run a Fantom FWT GUI in your browser!" meta = [ "proj.name" : "Duvet FWT Demo", "afIoc.module" : "duvetFwtDemo::AppModule", ] depends = [ "sys 1.0", "gfx 1.0", "fwt 1.0", "build 1.0", "afIoc 3.0", "afBedSheet 1.5", "afDuvet 1.1" ] srcDirs = [`DuvetFwtDemo.fan`] } }**** FwtDemo displays the FWT sampler program.**** This is a modified version of `http://fantom.org/doc/examples/fwt-demo.html`** that runs in Javascript.**** All classes now have the @Js facet and all references to WebBrowsers,** FileDialogs and Trees have been removed.@Js class FwtDemo {**** Put the whole thing together in a tabbed pane**Void main() { Window { title = "FWT Demo" size = Size(1000, 600) content = EdgePane { top = makeToolBar center = TabPane { Tab { text = "Buttons"; InsetPane { makeButtons, }, }, Tab { text = "Labels"; InsetPane { makeLabels, }, }, Tab { text = "ProgessBar"; InsetPane { makeProgressBar, }, }, Tab { text = "Text"; InsetPane { makeText, }, }, Tab { text = "BorderPane"; InsetPane { makeBorderPane, }, }, Tab { text = "EdgePane"; InsetPane { makeEdgePane, }, }, Tab { text = "GridPane"; InsetPane { makeGridPane, }, }, Tab { text = "Window"; InsetPane { makeWindow, }, }, Tab { text = "Serialization"; InsetPane { makeSerialization, }, }, Tab { text = "Eventing"; InsetPane { makeEventing, }, }, Tab { text = "Cursors"; InsetPane { makeCursors, }, }, Tab { text = "Graphics"; InsetPane { makeGraphics, }, }, } } }.open }**** Build the toolbar**Widget makeToolBar() { return ToolBar { Button { mode = ButtonMode.sep }, Button { image = sysIcon; mode = ButtonMode.check; onAction.add(cb) }, Button { image = prefsIcon; mode = ButtonMode.toggle; onAction.add(cb) }, Button { mode = ButtonMode.sep }, Button { image = audioIcon; mode = ButtonMode.radio; onAction.add(cb); selected = true }, Button { image = imageIcon; mode = ButtonMode.radio; onAction.add(cb); }, Button { image = videoIcon; mode = ButtonMode.radio; onAction.add(cb); }, } }**** Build a pane of various labels**Widget makeLabels() { return GridPane { numCols = 2 hgap = 20 halignCells = Halign.fill Label { text = "Text Only" }, Label { image = stopIcon }, Label { text = "Both"; image = folderIcon }, Label { text = "Monospace"; font = Desktop.sysFontMonospace }, Label { text = "Colors"; image = folderIcon; fg = Color.red; bg = Color.yellow }, Label { text = "Left"; halign = Halign.left }, Label { text = "Center"; halign = Halign.center }, Label { text = "Right"; halign = Halign.right }, } }**** Build a pane of various progress bars**Widget makeProgressBar() { return GridPane { numCols = 1 hgap = 20 halignCells = Halign.fill ProgressBar { val=25; }, ProgressBar { min=0; max=100; val=75; }, ProgressBar { min=-100; max=100; val=80; }, ProgressBar { min=-100; max=100; val=25; }, ProgressBar { indeterminate = true }, } }**** Build a pane of various buttons**Widget makeButtons() { return GridPane { numCols = 3 hgap = 20 Button { text = "B1"; image = stopIcon; onAction.add(cb) }, Button { text = "Monospace"; font = Desktop.sysFontMonospace; onAction.add(cb) }, Button { mode = ButtonMode.toggle; text = "Button 3"; onAction.add(cb) }, Button { mode = ButtonMode.check; text = "B4"; onAction.add(cb) }, Button { mode = ButtonMode.radio; text = "Button 5"; onAction.add(cb) }, Button { mode = ButtonMode.radio; text = "B6"; onAction.add(cb) }, Button { text = "Popup 1"; onAction.add {FwtDemo.popup(true, it)} }, Button { text = "Popup 2"; onAction.add {FwtDemo.popup(false, it)} }, Button { text = "Disabled"; enabled=false }, Button { text = "Invisible"; visible=false }, } }**** Build a pane of various text fields**Widget makeText() { area := Text { multiLine = true font = Desktop.sysFontMonospace text ="Press button above to serialize this entire demo here" } ecb := |Event e| { echo("onAction: \"${e.widget->text}\"") } ccb := |Event e| { echo("onModify: \"${e.widget->text}\"") } nums := ["One", "Two", "Three", "Four", "Five", "Six", "Seven" ] return EdgePane { left = GridPane { numCols = 2 Label { text="Single" }, Text { onAction.add(ecb); onModify.add(ccb) }, Label { text="Monospace"; }, Text { font = Desktop.sysFontMonospace; onAction.add(ecb); onModify.add(ccb) }, Label { text="Password" }, Text { password = true; onAction.add(ecb); onModify.add(ccb) }, Label { text="Combo" }, Combo { items=nums; onAction.add(ecb); onModify.add(ccb) }, Label { text="Combo editable=true" }, Combo { editable=true; items=nums; onAction.add(ecb); onModify.add(ccb) }, Label { text="Combo dropDown=false" }, Combo { dropDown=false; items=nums; onAction.add(ecb); onModify.add(ccb) }, Label { text="MultiLine" }, Button { text="Serialize Demo"; onAction.add {serializeTo(area)} }, } center = InsetPane.make(5) { content=area } } } Void serializeTo(Text area) { try { opts := ["indent":2, "skipDefaults":true, "skipErrors":true] buf := Buf.make.writeObj(area.window, opts) area.text = buf.flip.readAllStr } catch (Err e) { area.text = e.traceToStr } }**** Build a demo border pane**Widget makeBorderPane() { b := BorderPane { border = Border("#000") insets = Insets(10) content = Box { color = Color.blue } } borderText := Text { text = b.border.toStr } insetsText := Text { text = b.insets.toStr } bgText := Text { text = "" } update := |->| { b.border = Border(borderText.text) b.insets = Insets(insetsText.text) b.bg = bgText.text.isEmpty ? null : Color(bgText.text) b.relayout b.repaint } borderText.onAction.add(update) insetsText.onAction.add(update) bgText.onAction.add(update) controlPane := GridPane { numCols = 2 Label { text="border" }, borderText, Label { text="insets" }, insetsText, Label { text="bg" }, bgText, Button { text = "Update"; onAction.add(update) } } return EdgePane { left = controlPane center = BorderPane { bg = Color.white; insets = Insets(10); content = b } } }**** Build a demo edge pane**Widget makeEdgePane() { return EdgePane { top = Button { text = "top" } left = Button { text = "left" } right = Button { text = "right" } bottom = Button { text = "bottom" } center = Button { text = "center" } } }**** Build a demo grid pane using randomly sized boxes**Widget makeGridPane() { grid := GridPane { numCols = 5 hgap = 10 vgap = 10 Box { color = Color.red }, Box { color = Color.green }, Box { color = Color.yellow }, Box { color = Color.blue }, Box { color = Color.orange }, Box { color = Color.darkGray }, Box { color = Color.purple }, Box { color = Color.gray }, Box { color = Color.white }, } colors := [Color.red, Color.green, Color.yellow, Color.blue, Color.orange, Color.darkGray, Color.purple, Color.gray, Color.white] 15.times |Int i| { grid.add(Box { color=colors[i%colors.size] }) } controls := GridPane { numCols = 2 halignCells = Halign.fill Label { text="numCols" }, Text { text="5"; onModify.add {setInt(grid, "numCols", it)} }, Label { text="hgap" }, Text { text="10"; onModify.add {setInt(grid, "hgap", it)} }, Label { text="vgap" }, Text { text="10"; onModify.add {setInt(grid, "vgap", it)} }, Label { text="halignCells" }, Combo { items=Halign.vals; onModify.add {setEnum(grid, "halignCells", it)} }, Label { text="valignCells" }, Combo { items=Valign.vals; onModify.add {setEnum(grid, "valignCells", it)} }, Label { text="halignPane" }, Combo { items=Halign.vals; onModify.add {setEnum(grid, "halignPane", it)} }, Label { text="valignPane" }, Combo { items=Valign.vals; onModify.add {setEnum(grid, "valignPane", it)} }, Label { text="expandRow" }, Text { text="null"; onModify.add {setInt(grid, "expandRow", it)} }, Label { text="expandCol" }, Text { text="null"; onModify.add {setInt(grid, "expandCol", it)} }, Label { text="uniformCols" }, Combo { items=[false,true]; onModify.add {setBool(grid, "uniformCols", it)} }, Label { text="uniformRows" }, Combo { items=[false,true]; onModify.add {setBool(grid, "uniformRows", it)} }, } return EdgePane { left=controls; center=InsetPane { content=grid } } }**** Build a pane showing how the various window options work**Widget makeWindow() { mode := Combo { items = WindowMode.vals; editable=false } alwaysOnTop := Button { it.mode = ButtonMode.check; text = "alwaysOnTop" } resizable := Button { it.mode = ButtonMode.check; text = "resizable" } showTrim := Button { it.mode = ButtonMode.check; text = "showTrim"; selected = true } open := |->| { close := Button { text="Close Me" } w := Window(mode.window) { it.mode = mode.selected it.alwaysOnTop = alwaysOnTop.selected it.resizable = resizable.selected it.showTrim = showTrim.selected it.size = Size(200,200) GridPane { halignPane = Halign.center; valignPane = Valign.center; add(close) }, } close.onAction.add { w.close } w.open } return GridPane { mode, alwaysOnTop, resizable, showTrim, Button { text="Open"; onAction.add(open) }, } }**** Build a pane showing how to use serialization**Widget makeSerialization() { area := Text { multiLine = true font = Desktop.sysFontMonospace text = "fwt::EdgePane\n" + "{\n" + " top = fwt::Button { text=\"Top\" }\n" + " center = fwt::Button { text=\"Center\" }\n" + " bottom = fwt::Button { text=\"Bottom\" }\n" + "}\n" } test := InsetPane { Label { text="Press button to deserialize code on the left here" }, } return SashPane { EdgePane { center = area right = InsetPane { Button { text="=>"; onAction.add |->| { deserializeTo(area.text, test) } }, } }, test, } } Void deserializeTo(Str text, InsetPane test) { try { test.content = text.in.readObj } catch (Err e) { test.content = Text { it.multiLine = true; it.text = e.traceToStr } } test.relayout }**** Build a pane to trace events**Widget makeEventing() { return GridPane { numCols = 2 hgap = 36 it.onKeyDown.add |e| { echo("onKeyDown: $e.key") } GridPane { EventDemo { name = "A"; demo = this }, EventDemo { name = "B"; demo = this }, EventDemo { name = "C"; demo = this }, }, ConsumeEventDemo("container") { bg = Color.blue content = Label { text = "Text"; bg = Color.white } ConsumeEventDemo.listen(content, "label") }, } }**** Build a pane to trace events**Widget makeCursors() { return GridPane { numCols = 3 grid := it Cursor.predefined.each |Cursor c| { grid.add(CursorDemo { text = c.toStr(); cursor = c}) } grid.add(CursorDemo { text = "custom"; cursor = Cursor(refreshIcon, 8, 8)}) } }**** Build a pane showing how to use Graphics**Widget makeGraphics() { return ScrollPane { content=GraphicsDemo { demo = this } } } static Void setInt(Widget obj, Str field, Event e) { f := obj.typeof.field(field) Str text := e.widget->text int := text.toInt(10, false) if (int != null || text=="null") f.set(obj, int) obj.relayout } static Void setBool(Widget obj, Str field, Event e) { f := obj.typeof.field(field) Str text := e.widget->text b := text.toBool(false) if (b != null) f.set(obj, b) obj.relayout } static Void setEnum(Widget obj, Str field, Event e) { f := obj.typeof.field(field) en := f.get(obj)->fromStr(e.widget->text, false) if (en != null) f.set(obj, en) obj.relayout } static |Event e| cb() { return |Event e| { w := e.widget echo("${w->text} selected=${w->selected}") } } static Void popup(Bool withPos, Event event) { makePopup.open(event.widget, withPos ? Point.make(0, event.widget.size.h) : event.pos) } static Menu makePopup() { return Menu { MenuItem { text = "Popup 1"; onAction.add(cb) }, MenuItem { text = "Popup 2"; onAction.add(cb) }, MenuItem { text = "Popup 3"; onAction.add(cb) }, } } static Void onScroll(Str name, Event e) { ScrollBar sb := e.widget echo("-- onScroll $name $e [val=$sb.val min=$sb.min max=$sb.max thumb=$sb.thumb page=$sb.page orient=$sb.orientation") } Str homeUri := "http://fantom.org/" Image backIcon := Image(`fan://icons/x16/arrowLeft.png`) Image nextIcon := Image(`fan://icons/x16/arrowRight.png`) Image cutIcon := Image(`fan://icons/x16/cut.png`) Image copyIcon := Image(`fan://icons/x16/copy.png`) Image pasteIcon := Image(`fan://icons/x16/paste.png`) Image folderIcon := Image(`fan://icons/x16/folder.png`) Image fileIcon := Image(`fan://icons/x16/file.png`) Image audioIcon := Image(`fan://icons/x16/file.png`) Image imageIcon := Image(`fan://icons/x16/file.png`) Image videoIcon := Image(`fan://icons/x16/file.png`) Image sysIcon := Image(`fan://icons/x16/file.png`) Image prefsIcon := Image(`fan://icons/x16/file.png`) Image refreshIcon := Image(`fan://icons/x16/refresh.png`) Image stopIcon := Image(`fan://icons/x16/err.png`) Image cloudIcon := Image(`fan://icons/x16/cloud.png`) }**************************************************************************** DirTreeModel**************************************************************************@Js class DirTreeModel : TreeModel { FwtDemo? demo override Obj[] roots() { return Env.cur.homeDir.listDirs } override Str text(Obj node) { return node->name } override Image? image(Obj node) { return demo.folderIcon } override Obj[] children(Obj obj) { return obj->listDirs } }**************************************************************************** DirTableModel**************************************************************************@Js class DirTableModel : TableModel { FwtDemo? demo File[]? dir Str[] headers := ["Name", "Size", "Modified"] override Int numCols() { return 3 } override Int numRows() { return dir.size } override Str header(Int col) { return headers[col] } override Halign halign(Int col) { return col == 1 ? Halign.right : Halign.left } override Font? font(Int col, Int row) { return col == 2 ? Font {name=Desktop.sysFont.name; size=Desktop.sysFont.size-1} : null } override Color? fg(Int col, Int row) { return col == 2 ? Color("#666") : null } override Color? bg(Int col, Int row) { return col == 2 ? Color("#eee") : null } override Str text(Int col, Int row) { f := dir[row] switch (col) { case 0: return f.name case 1: return f.size?.toLocale("B") ?: "" case 2: return f.modified.toLocale default: return "?" } } override Int sortCompare(Int col, Int row1, Int row2) { a := dir[row1] b := dir[row2] switch (col) { case 1: return a.size <=> b.size case 2: return a.modified <=> b.modified default: return super.sortCompare(col, row1, row2) } } override Image? image(Int col, Int row) { if (col != 0) return null return dir[row].isDir ? demo.folderIcon : demo.fileIcon } }**************************************************************************** Box**************************************************************************@Js class Box : Canvas { Color color := Color.green override Size prefSize(Hints hints := Hints.defVal) { Size(Int.random(20..100), Int.random(20..80)) } override Void onPaint(Graphics g) { size := this.size g.brush = color g.fillRect(0, 0, size.w, size.h) g.brush = Color.black g.drawRect(0, 0, size.w-1, size.h-1) } }**************************************************************************** EventDemo**************************************************************************@Js class EventDemo : Canvas { new make() { d := |e| { dump(e) } onFocus.add(d) onBlur.add(d) onKeyUp.add(d) onKeyDown.add(d) onMouseUp.add(d) onMouseDown.add(d) onMouseEnter.add(d) onMouseExit.add(d) onMouseMove.add(d) onMouseHover.add(d) onMouseWheel.add(d) } override Size prefSize(Hints hints := Hints.defVal) { return Size.make(100, 100) } override Void onPaint(Graphics g) { w := size.w h := size.h g.brush = Color.white g.fillRect(0, 0, w, h) g.brush = Color.black g.drawRect(0, 0, w-1, h-1) g.drawText(name, 45, 40) if (hasFocus) { g.brush = Color.red g.drawRect(1, 1, w-3, h-3) g.drawRect(2, 2, w-5, h-5) } } Void dump(Event event) { if (event.id == EventId.focus || event.id == EventId.blur) repaint echo("$name> $event") } Str? name FwtDemo? demo }**************************************************************************** ConsumeEventDemo**************************************************************************@Js class ConsumeEventDemo : BorderPane { new make(Str name := "foo") { listen(this, name) insets = Insets(50) } static Void listen(Widget w, Str name) { d := |e| { dump(e, name) } w.onMouseUp.add(d) w.onMouseDown.add(d) w.onMouseEnter.add(d) w.onMouseExit.add(d) w.onMouseWheel.add(d) } static Void dump(Event event, Str name) { echo("$name> $event") event.consume() } }**************************************************************************** CursorDemo**************************************************************************@Js class CursorDemo : Canvas { new make() { d := |e| { dump(e) } onMouseEnter.add(d) onMouseExit.add(d) onMouseMove.add(d) } override Size prefSize(Hints hints := Hints.defVal) { return Size.make(150, 30) } override Void onPaint(Graphics g) { w := size.w; h := size.h font := Desktop.sysFont g.brush = Color.white g.fillRect(0, 0, w - 1, h - 1) g.brush = Color.black g.drawRect(0, 0, w - 1, h - 1) g.font = font g.drawText(text, (w - font.width(text)) / 2, (h - font.height()) / 2) if (p != null) { g.brush = Color.red g.drawLine(p.x - 10, p.y - 10, p.x + 10, p.y + 10) g.drawLine(p.x - 10, p.y + 10, p.x + 10, p.y - 10) } } Void dump(Event event) { p = event.id != EventId.mouseExit ? event.pos : null repaint } Str? text Point? p }**************************************************************************** GraphicsDemo**************************************************************************@Js class GraphicsDemo : Canvas { FwtDemo? demo override Size prefSize(Hints hints := Hints.defVal) { return Size.make(750, 450) } override Void onPaint(Graphics g) { w := size.w h := size.h g.antialias = true g.brush = Gradient("0% 0%, 100% 100%, #fff, #666") g.fillRect(0, 0, w, h) g.brush = Color.black; g.drawRect(0, 0, w-1, h-1) g.brush = Color.orange; g.fillRect(10, 10, 50, 60) g.brush = Color.blue; g.drawRect(10, 10, 50, 60) g.brush = Color("#80ffff00"); g.fillOval(40, 40, 120, 100) g.pen = Pen { width = 2; dash=[8,4].toImmutable } g.brush = Color.green; g.drawOval(40, 40, 120, 100) g.pen = Pen { width = 8; join = Pen.joinBevel } g.brush = Color.gray; g.drawRect(120, 120, 120, 90) g.brush = Color.orange; g.fillArc(120, 120, 120, 90, 45, 90) g.pen = Pen { width = 8; cap = Pen.capRound } g.brush = Color.blue; g.drawArc(120, 120, 120, 90, 45, 90) g.brush = Color.purple; g.drawText("Hello World!", 70, 50) g.font = Desktop.sysFontMonospace.toSize(16).toBold; g.drawText("Hello World!", 70, 70) g.pen = Pen { width = 2; join = Pen.joinBevel } g.brush = Color("#a00") g.drawPolyline([ Point(10, 380), Point(30, 420), Point(50, 380), Point(70, 420), Point(90, 380)])// polygon - trianglepolygon := [Point(180, 380), Point(140, 440), Point(220, 440)] g.pen = Pen("1") g.brush = Color("#f88"); g.fillPolygon(polygon) g.brush = Color("#800"); g.drawPolygon(polygon)// rounded rectg.brush = Color("#f88") g.fillRoundRect(240, 380, 100, 60, 30, 15) g.pen = Pen("2") g.brush = Color.blue g.drawRoundRect(240, 380, 100, 60, 30, 15) img := demo.folderIcon g.drawImage(img, 220, 20) g.copyImage(img, Rect(0, 0, img.size.w, img.size.h), Rect(250, 30, 64, 64)) g.drawImage(img.resize(Size(64, 64)), 320, 30) g.push try { g.alpha=128; g.drawImage(img, 220, 40) g.alpha=64; g.drawImage(img, 220, 60) } finally g.pop// image brushg.brush = Pattern(demo.cloudIcon) g.fillOval(390, 20, 80, 80) g.brush = Color.black g.pen = Pen { width = 1 } g.drawOval(390, 20, 80, 80)// system font/colorsy := 20 g.brush = Color.black g.font = Desktop.sysFont g.drawText("sysFont: $Desktop.sysFont.toStr", 480, y) g.font = Desktop.sysFontSmall g.drawText("sysFontSmall: $Desktop.sysFontSmall.toStr", 480, y+18) g.font = Desktop.sysFontView g.drawText("sysFontView: $Desktop.sysFontView.toStr", 480, y+30) y += 60 g.font = Font("9pt Arial") y = sysColor(g, y, Desktop.sysDarkShadow, "sysDarkShadow") y = sysColor(g, y, Desktop.sysNormShadow, "sysNormShadow") y = sysColor(g, y, Desktop.sysLightShadow, "sysLightShadow") y = sysColor(g, y, Desktop.sysHighlightShadow, "sysHighlightShadow") y = sysColor(g, y, Desktop.sysFg, "sysFg") y = sysColor(g, y, Desktop.sysBg, "sysBg") y = sysColor(g, y, Desktop.sysBorder, "sysBorder") y = sysColor(g, y, Desktop.sysListBg, "sysListBg") y = sysColor(g, y, Desktop.sysListFg, "sysListFg") y = sysColor(g, y, Desktop.sysListSelBg, "sysListSelBg") y = sysColor(g, y, Desktop.sysListSelFg, "sysListSelFg")// rect/text with gradientsg.brush = Gradient("260px 120px, 460px 320px, #00f, #f00") g.pen = Pen { width=20; join = Pen.joinRound } g.drawRect(270, 130, 180, 180) 6.times |Int i| { g.drawText("Gradients!", 300, 150+i*20) }// translate for font metric boxg.translate(50, 250) g.pen = Pen.defVal g.brush = Color.yellow g.fillRect(0, 0, 200, 100)// font metric box with ascent, descent, baselineg.font = Desktop.sysFont.toSize(20) tw := g.font.width("Font Metrics") tx := (200-tw)/2 ty := 30 g.brush = Color.gray g.drawLine(tx-10, ty, tx+10, ty) g.drawLine(tx, ty-10, tx, ty+10) g.brush = Color.orange my := ty+g.font.leading; g.drawLine(tx, my, tx+tw, my) g.brush = Color.green my += g.font.ascent; g.drawLine(tx, my, tx+tw, my) g.brush = Color.blue my += g.font.descent; g.drawLine(tx, my, tx+tw, my) g.brush = Color.black g.drawText("Font Metrics", tx, ty)// alphag.translate(430, 80)// checkerboard bgg.brush = Color.white g.fillRect(0, 0, 240, 120) g.brush = Color("#ccc") 12.times |Int by| { 24.times |Int bx| { if (bx.isEven.xor(by.isEven)) g.fillRect(bx*10, by*10, 10, 10) } }// change both alpha and colora := Color("#ffff0000") b := Color("#80ff0000") g.alpha=255; g.brush=a; g.fillRect(0, 0, 30, 30); g.brush=b; g.fillRect(30, 0, 30, 30) g.alpha=192; g.brush=a; g.fillRect(0, 30, 30, 30); g.brush=b; g.fillRect(30, 30, 30, 30) g.alpha=128; g.brush=a; g.fillRect(0, 60, 30, 30); g.brush=b; g.fillRect(30, 60, 30, 30) g.alpha=64; g.brush=a; g.fillRect(0, 90, 30, 30); g.brush=b; g.fillRect(30, 90, 30, 30)// change only alphag.brush = a g.alpha=255; g.fillRect(60, 0, 30, 30); g.alpha=192; g.fillRect(60, 30, 30, 30); g.alpha=128; g.fillRect(60, 60, 30, 30); g.alpha=64; g.fillRect(60, 90, 30, 30);// change only colorg.alpha = 128 g.brush = Color("#f00"); g.fillRect(90, 0, 30, 30); g.brush = Color("#ff0"); g.fillRect(90, 30, 30, 30); g.brush = Color("#0f0"); g.fillRect(90, 60, 30, 30); g.brush = Color("#00f"); g.fillRect(90, 90, 30, 30);// gradientsg.alpha = 255 g.brush = Gradient("0px 0px, 0px 120px, #f00, #fff"); g.fillRect(120, 0, 20, 120) g.brush = Gradient("0px 0px, 0px 120px, #f00, #80ffffff"); g.fillRect(140, 0, 20, 120) g.brush = Gradient("0px 0px, 0px 120px, #80ff0000, #80ffffff"); g.fillRect(160, 0, 20, 120) g.brush = Gradient("0px 0px, 0px 120px, #f00, #fff"); g.alpha = 128;/* set alpha after gradient */g.fillRect(180, 0, 20, 120) g.brush = Gradient("0px 0px, 0px 120px, #f00, #80ffffff"); g.fillRect(200, 0, 20, 120) g.brush = Gradient("0px 0px, 0px 120px, #80ff0000, #80ffffff"); g.fillRect(220, 0, 20, 120) g.translate(140, -350) g.alpha = 255 pathTurtle := |->GraphicsPath| { g.path .moveTo(40, 100) .curveTo(50, 30, 110, 30, 120, 100) .curveTo(170, 80, 170, 140, 120, 120) .lineTo(110, 120) .curveTo(115, 140, 95, 140, 100, 120) .lineTo(60, 120) .curveTo(65, 140, 45, 140, 50, 120) .lineTo(40, 120) .close } g.brush = Color("#0a0") pathTurtle().fill g.brush = Color("#7B3F00") g.pen = Pen("4") pathTurtle().draw } Int sysColor(Graphics g, Int y, Color c, Str name) { g.brush = c g.fillRect(480, y, 140, 20) g.brush = Color.green g.drawText(name, 490, y+3) return y + 20 } }
Yep, it's big! (Note the code is also available as BitBucket Snippet.)
Run the script to compile a new pod called duvetFwtDemo
:
C:\> fan DuvetFwtDemo.fan
Start BedSheet
:
C:\> fan afBedSheet duvetFwtDemo -port 8080
And refresh your browser:
"Wow!"
Note the both DuvetDemo
and DuvetFwtDemo
have been tested with:
- Fantom 1.0.68
- afIoc 3.0.2
- afBedSheet 1.5.2
- afDuvet 1.1.2
Go Further!
After that simple introduction, it's up to you to explore what else is possible!
Don't forget you can also interact with the browser's Window, Document and DOM objects by using the core Fantom DOM pod. And don't forget the new Web FWT pod!.
Note that when you instantiate an FWT window, it attaches itself to the whole browser window by default. If you wish to constrain the window to a particular element on the page, then pass the following environment variable to the injectFantomMethod()
call:
"fwt.window.root" : "<element-id>"
Where <element-id>
is the html ID of an element on the page. The element needs to specify a width, height and give a CSS position of relative
. This may either be done in CSS or defined on the element directly:
<div id="fwt-window" style="width: 640px; height:480px; position:relative;"></div>
See IndexPage
in the Duvet Fwt Demo for an example.
Have fun!
Edits
- 31 Jul 2016 - Updated example scripts to use Ioc 3.0, BedSheet 1.5, & Duvet 1.1.
- 9 Jul 2015 - Updated example scripts to use BedSheet 1.4.
- 7 Aug 2014 - Original article.