diff --git a/sim/README.md b/sim/README.md
index 04a5e60ba783dd7bf5a916e579819306ea9179d3..22271a26c9222d9bafc1004c632314395b04613b 100644
--- a/sim/README.md
+++ b/sim/README.md
@@ -34,8 +34,6 @@ Known Issues
 
 Captouch petal input order is wrong.
 
-No support for three-way buttons yet.
-
 No support for audio yet.
 
 Hacking
diff --git a/sim/fakes/hardware.py b/sim/fakes/hardware.py
index dd65dc1e1917ef865ed5fc69f5710fd32b9b56ce..c28e68eabb539b3813060d30b6e1bac5cfa34090 100644
--- a/sim/fakes/hardware.py
+++ b/sim/fakes/hardware.py
@@ -14,19 +14,96 @@ bgpath = os.path.join(simpath, 'background.png')
 background = pygame.image.load(bgpath)
 
 
-class Simulation:
+class Input:
     """
-    Simulation implements the state and logic of the on-host pygame-based badge
-    simulator.
+    Input implements an input overlay (for petals or buttons) that can be
+    mouse-picked by the user, and in the future also keyboard-controlled.
     """
+    # Pixels positions of each marker.
+    POSITIONS = []
+    # Pixel size (diameter) of each marker.
+    MARKER_SIZE = 100
+
+    # Colors for various states (RGBA).
+    COLOR_HELD = (0x5b, 0x5b, 0x5b, 0xa0)
+    COLOR_HOVER = (0x6b, 0x6b, 0x6b, 0xa0)
+    COLOR_IDLE = (0x8b, 0x8b, 0x8b, 0x80)
+
+    def __init__(self):
+        self._state = [False for _ in self.POSITIONS]
+        self._mouse_hover = None
+        self._mouse_held = None
+
+    def state(self):
+        s = [ss for ss in self._state]
+        if self._mouse_held is not None:
+            s[self._mouse_held] = True
+        return s
+
+    def _mouse_coords_to_id(self, mouse_x, mouse_y):
+        for i, (x, y) in enumerate(self.POSITIONS):
+            x += self.MARKER_SIZE // 2
+            y += self.MARKER_SIZE // 2
+            dx = mouse_x - x
+            dy = mouse_y - y
+            if math.sqrt(dx**2 + dy**2) < self.MARKER_SIZE // 2:
+                return i
+        return None
 
-    # Pixel coordinates of each petal 'marker'.
+    def process_event(self, ev):
+        prev_hover = self._mouse_hover
+        prev_state = self.state()
+
+        if ev.type == pygame.MOUSEMOTION:
+            x, y = ev.pos
+            self._mouse_hover = self._mouse_coords_to_id(x, y)
+        if ev.type == pygame.MOUSEBUTTONDOWN:
+            self._mouse_held = self._mouse_hover
+        if ev.type == pygame.MOUSEBUTTONUP:
+            self._mouse_held = None
+
+        if prev_hover != self._mouse_hover:
+            return True
+        if prev_state != self.state():
+            return True
+        return False
+
+    def render(self, surface):
+        s = self.state()
+        for i, (x, y) in enumerate(self.POSITIONS):
+            x += self.MARKER_SIZE // 2
+            y += self.MARKER_SIZE // 2
+            if s[i]:
+                pygame.draw.circle(surface, self.COLOR_HELD, (x, y), self.MARKER_SIZE//2)
+            elif i == self._mouse_hover:
+                pygame.draw.circle(surface, self.COLOR_HOVER, (x, y), self.MARKER_SIZE//2)
+            else:
+                pygame.draw.circle(surface, self.COLOR_IDLE, (x, y), self.MARKER_SIZE//2)
+
+
+class PetalsInput(Input):
     # TODO(q3k): document order
-    PETAL_POSITIONS = [
+    POSITIONS = [
         (114, 302), (163, 112), (204, 587), ( 49, 477), (504, 587), (352, 696), (602, 298), (660, 477), (356, 122), (547, 117),
     ]
-    # Size of each petal 'marker' rendered in overlay.
-    PETAL_MARKER_SIZE = 50
+
+
+class ButtonsInput(Input):
+    POSITIONS = [
+        ( 14, 230), ( 46, 230), ( 78, 230),
+        (714, 230), (746, 230), (778, 230),
+    ]
+    MARKER_SIZE = 20
+    COLOR_HELD = (0x80, 0x80, 0x80, 0xff)
+    COLOR_HOVER = (0x40, 0x40, 0x40, 0xff)
+    COLOR_IDLE = (0x20, 0x20, 0x20, 0xff)
+
+
+class Simulation:
+    """
+    Simulation implements the state and logic of the on-host pygame-based badge
+    simulator.
+    """
 
     # Pixel coordinates of each LED. The order is the same as the hardware
     # WS2812 chain, not the order as expected by the micropython API!
@@ -44,13 +121,8 @@ class Simulation:
         self.led_state_buf = [(0, 0, 0) for _ in self.LED_POSITIONS]
         # Actual LED state as rendered.
         self.led_state = [(0, 0, 0) for _ in self.LED_POSITIONS]
-        # Position of the simulation cursor in window pixel coordinates.
-        self.mouse_x, self.mouse_y = (0, 0)
-        # ID of petal being held (clicked), or None if no petal is being held.
-        self.petal_held = None
-        # ID of petal being hovered over, or None if no petal is being hovered
-        # over.
-        self.petal_hover = None
+        self.petals = PetalsInput()
+        self.buttons = ButtonsInput()
         # Timestamp of last GUI render. Used by the lazy render GUI
         # functionality.
         self.last_gui_render = None
@@ -95,61 +167,19 @@ class Simulation:
 
     def process_events(self):
         """
-        Process pygame events and update mouse_{x,y}, petal_held and
-        petal_hover.
+        Process pygame events and update mouse_{x,y}, {petal,button}_held and
+        {petal,button}_hover.
         """
-        prev_petal_hover = self.petal_hover
         evs = pygame.event.get()
         for ev in evs:
-            if ev.type == pygame.MOUSEMOTION:
-                self.mouse_x, self.mouse_y = ev.pos
-                self.petal_hover = self._mouse_coords_to_petal_id()
-            if ev.type == pygame.MOUSEBUTTONDOWN:
-                self._process_mouse_down()
-            if ev.type == pygame.MOUSEBUTTONUP:
-                self._process_mouse_up()
-
-        if prev_petal_hover != self.petal_hover:
-            self._petal_surface_dirty = True
-
-    def _mouse_coords_to_petal_id(self):
-        if self.mouse_x is None or self.mouse_y is None:
-            return None
-
-        for i, pos in enumerate(self.PETAL_POSITIONS):
-            (px, py) = pos
-            # TODO(q3k): pre-apply to PETAL_POSITIONS.
-            px += 50
-            py += 50
-            dx = self.mouse_x - px
-            dy = self.mouse_y - py
-            if math.sqrt(dx**2 + dy**2) < self.PETAL_MARKER_SIZE:
-                return i
-        return None
-
-    def _process_mouse_up(self):
-        if self.petal_held is not None:
-            self.petal_held = None
-            self._petal_surface_dirty = True
-
-    def _process_mouse_down(self):
-        if self.petal_held != self.petal_hover:
-            self.petal_held = self.petal_hover
-            self._petal_surface_dirty = True
+            if self.petals.process_event(ev):
+                self._petal_surface_dirty = True
+            if self.buttons.process_event(ev):
+                self._petal_surface_dirty = True
 
     def _render_petal_markers(self, surface):
-        for i, pos in enumerate(self.PETAL_POSITIONS):
-            x, y = pos
-            # TODO(q3k): pre-apply to PETAL_POSITIONS
-            x += 50
-            y += 50
-
-            if i == self.petal_held:
-                pygame.draw.circle(surface, (0x8b, 0x8b, 0x8b, 0x80), (x, y), 50)
-            elif i == self.petal_hover:
-                pygame.draw.circle(surface, (0x6b, 0x6b, 0x6b, 0xa0), (x, y), 50)
-            else:
-                pygame.draw.circle(surface, (0x5b, 0x5b, 0x5b, 0xa0), (x, y), 50)
+        self.petals.render(surface)
+        self.buttons.render(surface)
 
     def _render_leds(self, surface):
         for pos, state in zip(self.LED_POSITIONS, self.led_state):
@@ -342,10 +372,27 @@ def set_global_volume_dB(a):
 
 
 def get_button(a):
+    _sim.process_events()
+    _sim.render_gui_lazy()
+
+    state = _sim.buttons.state()
+    if a == 1:
+        sub = state[:3]
+    elif a == 0:
+        sub = state[3:6]
+    else:
+        return 0
+
+    if sub[0]:
+        return -1
+    elif sub[1]:
+        return 2
+    elif sub[2]:
+        return +1
     return 0
 
 
 def get_captouch(a):
     _sim.process_events()
     _sim.render_gui_lazy()
-    return _sim.petal_held == a
+    return _sim.petals.state()[a]