# Build a desktop app

> Build a pomodoro timer with `std:ui` and `std:window`: a class with state and actions, a view in tags, styles and a theme, then show it on the web and in a window.

This is the Markdown version of https://osl-lang.org/build/desktop-app. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

You'll build a pomodoro timer: 25 minutes of focus, a 5 minute break, a count of sessions. It's one class that holds the state and the actions, and a `view` that draws it. The same class ends up in a native window and on a web page.

A window can't open on this page, so every step tests the class by calling its methods and printing, or by rendering it with `ui.html`, which gives the page's HTML without opening anything.

## Count down

A timer is a class with a field and two methods: `tick()` takes a second off and `label()` shows `mm:ss`. Nothing here is about drawing yet, so it's an ordinary program. The try-it tab ticks it 90 times.

```osl
def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500

  def tick() (
    if self.remaining > 0 (
      self.remaining -= 1
    )
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )
)

// try it
Timer timer = Timer()
log timer.label()
for _ in 1 to 90 (
  timer.tick()
)
log timer.label()
```

Output:

```text
25:00
23:30
```

**Try changing** the number of ticks in the try-it tab.

### Exercise: Format seconds

Make `clock` return `25:00`, `1:05` and `0:05`.

Starting code:

```osl
def clock(int seconds) string (
  return seconds ++ ":" ++ seconds
)

log clock(1500)
log clock(65)
log clock(5)
```

It should print:

```text
25:00
1:05
0:05
```

Hint 1: Minutes are `floor(seconds / 60).as<int>` and the rest is `seconds % 60`. `("" ++ rest).padStart("0", 2)` pads to two digits.

A solution:

```osl
def clock(int seconds) string (
  int minutes = floor(seconds / 60).as<int>
  string ss = ("" ++ (seconds % 60)).padStart("0", 2)
  return minutes ++ ":" ++ ss
)

log clock(1500)
log clock(65)
log clock(5)
```

## Start and pause

State is just fields. `running` decides whether `tick` does anything, and `toggle()` flips it.

```osl
def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )
)

// try it
Timer timer = Timer()
for _ in 1 to 5 (
  timer.tick()
)
log "idle:" timer.label()
timer.toggle()
for _ in 1 to 5 (
  timer.tick()
)
log "running:" timer.label()
timer.toggle()
log "paused:" timer.label() timer.running
```

Output:

```text
idle: 25:00
running: 24:55
paused: 24:55 false
```

### Exercise: Reset

Add a `reset()` method that puts the time back to 1500 and stops the timer.

Starting code:

```osl
class Timer (
  int remaining = 1500
  bool running = false

  def reset() (
    // put the time back and stop
    self.running = self.running
  )

  def toggle() (
    self.running = !self.running
  )
)

Timer timer = Timer()
timer.remaining = 10
timer.toggle()
timer.reset()
log timer.remaining timer.running
```

It should print:

```text
1500 false
```

Hint 1: It sets two fields.

A solution:

```osl
class Timer (
  int remaining = 1500
  bool running = false

  def reset() (
    self.remaining = 1500
    self.running = false
  )

  def toggle() (
    self.running = !self.running
  )
)

Timer timer = Timer()
timer.remaining = 10
timer.toggle()
timer.reset()
log timer.remaining timer.running
```

## Draw it

A component is a class with a `view()` method that returns tags. `<column>` stacks, `<text>` shows text and `${...}` inserts a value. `<button on:click=self.toggle>` calls the method when clicked, and the view runs again afterwards, so it always matches the state. `ui.html(timer)` renders the page for a test.

```osl
import "std:ui"

def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )

  def view() ui.node (
    return <column gap=12 pad=24 align=center>
      <text size=48 weight=bold>${self.label()}</text>
      <button on:click=self.toggle>${if self.running then "Pause" else "Start"}</button>
    </column>
  )
)

// try it
Timer timer = Timer()
string page = ui.html(timer)
log page.contains("25:00")
log page.contains("Start")

timer.toggle()
for _ in 1 to 3 (
  timer.tick()
)
page = ui.html(timer)
log page.contains("24:57")
log page.contains("Pause")
```

Output:

```text
true
true
true
true
```

**Quiz.** What happens after the button calls `toggle`?

- [ ] Nothing until you redraw by hand.
- [x] The view runs again, so the page matches the new state.
- [ ] The class is copied.

Answer: After any action every screen showing the component updates. Never keep state in the view: put it in fields.

### Exercise: Show the state

Make the view say `Paused` when `running` is false and `Running` when it's true.

Starting code:

```osl
import "std:ui"

class Badge (
  bool running = false

  def view() ui.node (
    return <text>Running</text>
  )
)

Badge badge = Badge()
log ui.html(badge).contains("Paused")
```

It should print:

```text
true
```

Hint 1: Put an `if ... then ... else` inside `${{}}`.

A solution:

```osl
import "std:ui"

class Badge (
  bool running = false

  def view() ui.node (
    return <text>${if self.running then "Running" else "Paused"}</text>
  )
)

Badge badge = Badge()
log ui.html(badge).contains("Paused")
```

## Style and theme

Layout uses three sizing rules: elements hug their content by default, `width=fill` takes the space left, and a number is pixels. `gap`, `pad` and `align` place children. A `style` is a named set of attributes, spread into a tag with `...card`. A theme holds shared values: on the web they become CSS variables, so changing `theme.accent` recolours every view.

```osl
import "std:ui"

*ui.Theme theme = ui.newTheme({accent: "#d9480f", surface: "#fff4e6", muted: "#868e96"})

style card = (pad: 32, gap: 16, radius: 16, bg: white)

def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )

  def view() ui.node (
    return <column width=fill height=fill align=center bg=theme.surface>
      <column ...card align=center>
        <text size=64 weight=bold color=theme.accent>${self.label()}</text>
        <row gap=8>
          <button
          on:click=self.toggle
          bg=theme.accent
        >${if self.running then "Pause" else "Start"}</button>
        </row>
      </column>
    </column>
  )
)

// try it
Timer timer = Timer()
string before = ui.html(timer)
log before.contains("#d9480f")
log before.contains("#fff4e6")
theme.accent = "#1c7ed6"
string after = ui.html(timer)
log after.contains("#1c7ed6")
```

Output:

```text
true
true
true
```

**Try changing** the accent colour in `newTheme`.

### Exercise: Use the theme

`Greeting` ignores the theme. Colour its text with the theme's accent so the page contains `#d9480f`.

Starting code:

```osl
import "std:ui"

*ui.Theme theme = ui.newTheme({accent: "#d9480f"})

class Greeting (
  def view() ui.node (
    return <text>Hello</text>
  )
)

log ui.html(Greeting()).contains("#d9480f")
```

It should print:

```text
true
```

Hint 1: Give the tag `color=theme.accent`, as the timer does.

A solution:

```osl
import "std:ui"

*ui.Theme theme = ui.newTheme({accent: "#d9480f"})

class Greeting (
  def view() ui.node (
    return <text color=theme.accent>Hello</text>
  )
)

log ui.html(Greeting()).contains("#d9480f")
```

## Focus and break

When the time hits zero, `next()` flips between focus and break, resets the time, and counts a session when focus ends. The view shows which mode it's in. In the try-it tab, three ticks run a 3 second focus to the end and start a 5 minute break.

```osl
import "std:ui"

*ui.Theme theme = ui.newTheme({accent: "#d9480f", surface: "#fff4e6", muted: "#868e96"})

style card = (pad: 32, gap: 16, radius: 16, bg: white)

def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false
  bool onBreak = false
  int sessions = 0

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
    if self.remaining == 0 (
      self.next()
    )
  )

  def next() (
    if !self.onBreak (
      self.sessions += 1
    )
    self.onBreak = !self.onBreak
    self.remaining = if self.onBreak then 300 else 1500
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )

  def view() ui.node (
    return <column width=fill height=fill align=center bg=theme.surface>
      <column ...card align=center>
        <text color=theme.muted>${if self.onBreak then "Break" else "Focus"}</text>
        <text size=64 weight=bold color=theme.accent>${self.label()}</text>
        <button
          on:click=self.toggle
          bg=theme.accent
        >${if self.running then "Pause" else "Start"}</button>
        <text color=theme.muted>${self.sessions} sessions done</text>
      </column>
    </column>
  )
)

// try it
Timer timer = Timer()
timer.remaining = 3
timer.toggle()
for _ in 1 to 3 (
  timer.tick()
)
log timer.onBreak timer.label() timer.sessions
timer.remaining = 1
timer.tick()
log timer.onBreak timer.label() timer.sessions
log ui.html(timer).contains("1 sessions done")
```

Output:

```text
true 05:00 1
false 25:00 1
true
```

### Exercise: Longer breaks

Every 4th break should be 15 minutes. Write the rule: `lengthOf(mode, sessions)` returns 25 for focus, 15 for a break after a multiple of 4 sessions, else 5.

Starting code:

```osl
def lengthOf(string mode, int sessions) int (
  return 25
)

log lengthOf("focus", 1)
log lengthOf("break", 1)
log lengthOf("break", 4)
```

It should print:

```text
25
5
15
```

Hint 1: `sessions % 4 == 0`.

A solution:

```osl
def lengthOf(string mode, int sessions) int (
  if mode == "focus" return 25
  if sessions % 4 == 0 return 15
  return 5
)

log lengthOf("focus", 1)
log lengthOf("break", 1)
log lengthOf("break", 4)
```

## Put it on the web

`std:serve` can show the same class. `app.UI(path, target, options)` makes a page that updates live over a WebSocket, and still works as a plain form without JavaScript. An object target, like `timer`, is one state shared by every visitor. A function that returns a new `Timer()` gives each visitor their own. The try-it tab fetches the page.

```osl
import "std:ui"
import "std:serve"
import "std:requests"

*ui.Theme theme = ui.newTheme({accent: "#d9480f", surface: "#fff4e6", muted: "#868e96"})

style card = (pad: 32, gap: 16, radius: 16, bg: white)

def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false
  bool onBreak = false
  int sessions = 0

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
    if self.remaining == 0 (
      self.next()
    )
  )

  def next() (
    if !self.onBreak (
      self.sessions += 1
    )
    self.onBreak = !self.onBreak
    self.remaining = if self.onBreak then 300 else 1500
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )

  def view() ui.node (
    return <column width=fill height=fill align=center bg=theme.surface>
      <column ...card align=center>
        <text color=theme.muted>${if self.onBreak then "Break" else "Focus"}</text>
        <text size=64 weight=bold color=theme.accent>${self.label()}</text>
        <button
          on:click=self.toggle
          bg=theme.accent
        >${if self.running then "Pause" else "Start"}</button>
        <text color=theme.muted>${self.sessions} sessions done</text>
      </column>
    </column>
  )
)

Timer timer = Timer()

*serve.Router app = serve.new()
app.UI("/", timer, {title: "Pomodoro"})

// try it
app.start("127.0.0.1:8755").unwrap()
auto r = requests.get("http://127.0.0.1:8755/").unwrap()
string body = r.body
log r.status
log body.contains("<title>Pomodoro</title>")
log body.contains("25:00")
app.stop().unwrap()
```

Output:

```text
200
true
true
```

**Quiz.** You pass `def() -> Timer()` to `app.UI` instead of `timer`. What changes?

- [ ] Nothing.
- [x] Each visitor gets their own timer.
- [ ] The page stops updating.

Answer: An object is shared by everyone it's given to. A factory is called once per visitor or window.

## Open a window

`window.create` opens a window and `main.show(timer)` puts the component in it. The window and the web page share the one `timer`. Something has to tick it: an `async` function loops, sleeps a second, ticks and calls `ui.refresh()` to redraw every screen. Windows use SDL2, which needs its development files to build.

```osl
import "std:ui"
import "std:serve"
import "std:window"
import "std:thread"

*ui.Theme theme = ui.newTheme({accent: "#d9480f", surface: "#fff4e6", muted: "#868e96"})

style card = (pad: 32, gap: 16, radius: 16, bg: white)

def two(int n) string -> ("" ++ n).padStart("0", 2)

class Timer (
  int remaining = 1500
  bool running = false
  bool onBreak = false
  int sessions = 0

  def tick() (
    if !self.running return
    if self.remaining > 0 (
      self.remaining -= 1
    )
    if self.remaining == 0 (
      self.next()
    )
  )

  def next() (
    if !self.onBreak (
      self.sessions += 1
    )
    self.onBreak = !self.onBreak
    self.remaining = if self.onBreak then 300 else 1500
  )

  def toggle() (
    self.running = !self.running
  )

  def label() string (
    int minutes = floor(self.remaining / 60).as<int>
    return two(minutes) ++ ":" ++ two(self.remaining % 60)
  )

  def view() ui.node (
    return <column width=fill height=fill align=center bg=theme.surface>
      <column ...card align=center>
        <text color=theme.muted>${if self.onBreak then "Break" else "Focus"}</text>
        <text size=64 weight=bold color=theme.accent>${self.label()}</text>
        <button
          on:click=self.toggle
          bg=theme.accent
        >${if self.running then "Pause" else "Start"}</button>
        <text color=theme.muted>${self.sessions} sessions done</text>
      </column>
    </column>
  )
)

Timer timer = Timer()

async clock(Timer t) (
  while true (
    thread.sleep(1000)
    t.tick()
    ui.refresh()
  )
)
clock(timer)

*serve.Router app = serve.new()
app.UI("/", timer, {title: "Pomodoro"})
app.start(":8095").unwrap()

*window.Window main = window.create({title: "Pomodoro", width: 360, height: 360}).unwrap()
main.show(timer)
window.run()
```

Save it as `pomodoro.osl` and run it with `osl run pomodoro.osl`: the window opens and `http://localhost:8095` shows the same timer. Next: [ui](/packages/ui.md), [window](/packages/window.md), or a [game](/build/game.md).
