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:

select all
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 page
        injector.injectFantomMethod(MyJavascript#greet, ["Hello Mum!"])

        // let Duvet inject all it needs into a plain HTML shell
        return 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:

select all
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!

An FWT Dialog in a Web Browser

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:

select all
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 - triangle
    polygon := [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 rect
    g.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 brush
    g.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/colors
    y := 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 gradients
    g.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 box
    g.translate(50, 250)
    g.pen = Pen.defVal
    g.brush = Color.yellow
    g.fillRect(0, 0, 200, 100)

    // font metric box with ascent, descent, baseline
    g.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)

    // alpha
    g.translate(430, 80)
    // checkerboard bg
    g.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 color
    a := 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 alpha
    g.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 color
    g.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);
    // gradients
    g.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:

An FWT GUI running in a Web Browser

"Wow!"

Note the both DuvetDemo and DuvetFwtDemo have been tested with:

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.

Discuss