#! /usr/bin/env fan
//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   10 Jun 08  Brian Frank  Creation
//

using gfx
using fwt

**
** FwtDemo displays the FWT sampler program.
**
class FwtDemo
{

  **
  ** Put the whole thing together in a tabbed pane
  **
  Void main()
  {
    Window
    {
      title = "FWT Demo"
      size = Size(1000, 600)
      menuBar = makeMenuBar
      content = EdgePane
      {
        top = makeToolBar
        center = TabPane
        {
          Tab { text = "Buttons";        InsetPane { makeButtons, }, },
          Tab { text = "Labels";         InsetPane { makeLabels, }, },
          Tab { text = "ProgessBar";     InsetPane { makeProgressBar, }, },
          Tab { text = "WebBrowser";     InsetPane { makeWebBrowser, }, },
          Tab { text = "Text";           InsetPane { makeText, }, },
          Tab { text = "BorderPane";     InsetPane { makeBorderPane, }, },
          Tab { text = "EdgePane";       InsetPane { makeEdgePane, }, },
          Tab { text = "GridPane";       InsetPane { makeGridPane, }, },
          Tab { text = "Tree and Table"; InsetPane { makeTreeAndTable, }, },
          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 menu bar
  **
  Menu makeMenuBar()
  {
    return Menu
    {
      Menu
      {
        text = "File";
        MenuItem { text = "Back";    image = backIcon;    onAction.add {browser.back} },
        MenuItem { text = "Next";    image = nextIcon;    onAction.add {browser.forward} },
        MenuItem { text = "Refresh"; image = refreshIcon; onAction.add {browser.refresh} },
        MenuItem { text = "Stop";    image = stopIcon;    onAction.add {browser.stop} },
        MenuItem { text = "Exit"; onAction.add |->| { Env.cur.exit } },
      },

      Menu
      {
        text = "Nested";
        Menu
        {
          text = "Alpha"
          image = folderIcon
          MenuItem { text = "Alpha.1"; onAction.add(cb) },
          Menu
          {
            text = "Alpha.2"
            MenuItem { text = "Alpha.2.I"; onAction.add(cb) },
            Menu
            {
              text = "Alpha.2.II"
              MenuItem { text = "Alpha.2.II.a"; onAction.add(cb) },
              MenuItem { text = "Alpha.2.II.b"; onAction.add(cb) },
            },
            MenuItem { text = "Alpha.2.III"; onAction.add(cb) },
          },
        },
        Menu
        {
          text = "Beta"
          MenuItem { text = "Beta.1"; onAction.add(cb) },
          MenuItem { text = "Beta.2"; onAction.add(cb) },
        },
      },

      Menu
      {
        text = "Modes"
        MenuItem { text = "Check 1"; accelerator=Key.f1; mode = MenuItemMode.check; onAction.add(cb) },
        MenuItem { text = "Check 2"; accelerator=Key.f2; mode = MenuItemMode.check; onAction.add(cb) },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "Radio 1"; accelerator=Key.num1+Key.alt; mode = MenuItemMode.radio; onAction.add(cb) },
        MenuItem { text = "Radio 2"; accelerator=Key.alt+Key.num2; mode = MenuItemMode.radio; onAction.add(cb); selected = true  },
      },

      Menu
      {
        text = "Dialogs"
        MenuItem { text = "Info"; onAction.add |Event e| { echo(Dialog.openInfo(e.window, "Test information!")) } },
        MenuItem { text = "Warn"; onAction.add |Event e| { echo(Dialog.openWarn(e.window, "Test warning!")) } },
        MenuItem { text = "Err"; onAction.add |Event e| { echo(Dialog.openErr(e.window, "Test error!")) } },
        MenuItem { text = "Question"; onAction.add |Event e| { echo(Dialog.openQuestion(e.window, "Test question?")) } },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "Ok/Cancel"; onAction.add |Event e| { echo(Dialog.openInfo(e.window, "OK/Cancel", Dialog.okCancel)) } },
        MenuItem { text = "Yes/No"; onAction.add |Event e| { echo(Dialog.openInfo(e.window, "Yes/No", Dialog.yesNo)) } },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "Details Err"; onAction.add |Event e| { echo(Dialog.openErr(e.window, "Something bad", ArgErr())) } },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "Prompt Str 1"; onAction.add |Event e| { echo("--> " + Dialog.openPromptStr(e.window, "Enter a string:")) } },
        MenuItem { text = "Prompt Str 2"; onAction.add |Event e| { echo("--> " + Dialog.openPromptStr(e.window, "Enter a string:", "123", 4)) } },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "Option A"; onAction.add |Event e| { echo((Dialog(e.window) {body="Str message"; commands=[Dialog.ok]}).open) } },
        MenuItem { text = "Option B"; onAction.add |Event e| { echo((Dialog(e.window) {body=Button { text="BIG!" }; commands=Dialog.okCancel}).open) } },
        MenuItem { mode = MenuItemMode.sep },
        MenuItem { text = "File Open";  onAction.add |Event e| { echo(FileDialog {}.open(e.window)) } },
        MenuItem { text = "Files Open"; onAction.add |Event e| { echo(FileDialog { dir=Env.cur.homeDir; mode=FileDialogMode.openFiles }.open(e.window)) } },
        MenuItem { text = "File Save";  onAction.add |Event e| { echo(FileDialog { name="foo.txt";  mode=FileDialogMode.saveFile }.open(e.window)) } },
        MenuItem { text = "Dir Open";   onAction.add |Event e| { echo(FileDialog { dir=Env.cur.homeDir; mode=FileDialogMode.openDir }.open(e.window)) } },
      },

    }
  }

  **
  ** Build the toolbar
  **
  Widget makeToolBar()
  {
    return ToolBar
    {
      Button { image = backIcon;    onAction.add {browser.back} },
      Button { image = nextIcon;    onAction.add {browser.forward} },
      Button { image = refreshIcon; onAction.add {browser.refresh} },
      Button { image = stopIcon;    onAction.add {browser.stop} },
      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 simple web browser
  **
  Widget makeWebBrowser()
  {
    url := Text { text=homeUri }
    url.onAction.add |->| { browser.load(url.text.toUri) }

    return EdgePane
    {
      top = EdgePane { center=url; right=Label{text="Enter to Go!"} }
      center = browser
    }
  }

  **
  ** 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 demo tree and table for file system
  **
  Widget makeTreeAndTable()
  {
    tree := Tree
    {
      multi = true
      model = DirTreeModel { demo = this }
      onAction.add |e| { echo(e) }
      onSelect.add |e| { echo(e); echo("selected=${e->widget->selected}") }
      onPopup.add |e|  { echo(e); e.popup = makePopup }
      // onMouseMove.add |e| { echo(e.pos + ": " + e->widget->nodeAt(e.pos)) }
      // hbar.onModify.add |e| { onScroll("Tree.hbar", e) }
      // vbar.onModify.add |e| { onScroll("Tree.vbar", e) }
    }

    table := Table
    {
      multi = true
      model = DirTableModel { demo = this; dir = File.os(".").list }
      onAction.add |e| { echo(e) }
      onSelect.add |e| { echo(e); echo("selected=${e->widget->selected}") }
      onPopup.add |e|  { echo(e); e.popup = makePopup }
      // onMouseMove.add |e| { Int? row := e->widget->rowAt(e.pos); Int? col := e->widget->colAt(e.pos); echo("Row: $row Col: $col " + ((row != null && col != null) ? e->widget->model->text(col, row) : "")) }
      // hbar.onModify.add |e| { onScroll("Tree.hbar", e) }
      // vbar.onModify.add |e| { onScroll("Tree.vbar", e) }
    }

    updateTable := |File dir| { table.model->dir = dir.list; table.refreshAll }
    tree.onAction.add  |e| { updateTable(e.data) }
    table.onAction.add |e| { updateTable(table.model->dir->get(e.index)) }

    return SashPane
    {
      weights = [1,3]
      tree,
      table,
    }
  }

  **
  ** 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")
  }

  WebBrowser browser := WebBrowser {}
  Str homeUri := "http://fantom.org/"

  File scriptDir  := File.make(this.typeof->sourceFile.toStr.toUri).parent

  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
**************************************************************************

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
**************************************************************************

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
**************************************************************************

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
**************************************************************************

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
**************************************************************************

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
**************************************************************************

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
**************************************************************************

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
  }
}