lib/kudzu_buttons: imgui, null and pi4ioe5v6416 backends

Also update badge/main.cc with button controls for:
- dpad: KUDZU sprite wave offsets
- start: reset wave to default
- B: toggle background color animation
- A: toggle nametag display

Change-Id: I27d52074c91c07edf49fdf895b1974be0a72ca4a
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/kudzu/+/193210
Reviewed-by: Kayce Basques <kayce@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/applications/app_common/BUILD.gn b/applications/app_common/BUILD.gn
index 64bfca0..e5d5d60 100644
--- a/applications/app_common/BUILD.gn
+++ b/applications/app_common/BUILD.gn
@@ -30,6 +30,7 @@
     "$dir_pw_display",
     "$dir_pw_status",
     "$dir_pw_thread:thread",
+    "//lib/kudzu_buttons",
     "//lib/kudzu_imu",
     "//lib/pw_touchscreen",
   ]
diff --git a/applications/app_common/public/app_common/common.h b/applications/app_common/public/app_common/common.h
index 6596189..e9c568f 100644
--- a/applications/app_common/public/app_common/common.h
+++ b/applications/app_common/public/app_common/common.h
@@ -13,6 +13,7 @@
 // the License.
 #pragma once
 
+#include "kudzu_buttons/buttons.h"
 #include "kudzu_imu/imu.h"
 #include "pw_display/display.h"
 #include "pw_status/status.h"
@@ -40,6 +41,8 @@
   // Return an initialized touchscreen.
   static pw::touchscreen::Touchscreen& GetTouchscreen();
 
+  static kudzu::Buttons& GetButtons();
+
   // Provides thread options for the display thread.
   static const pw::thread::Options& DisplayDrawThreadOptions();
 
diff --git a/applications/app_common_impl/BUILD.gn b/applications/app_common_impl/BUILD.gn
index 3d53560..ab29dba 100644
--- a/applications/app_common_impl/BUILD.gn
+++ b/applications/app_common_impl/BUILD.gn
@@ -108,6 +108,7 @@
   deps += [
     "$dir_pw_display_driver_st7789",
     "$dir_pw_pixel_pusher_rp2040_pio",
+    "//lib/kudzu_buttons_pi4ioe5v6416",
     "//lib/pw_touchscreen_ft6236",
   ]
   sources = [ "common_pico.cc" ]
@@ -123,6 +124,7 @@
     "$dir_pw_thread:thread",
     "$dir_pw_thread_stl:thread",
     "//applications/app_common:app_common.facade",
+    "//lib/kudzu_buttons_imgui",
     "//lib/kudzu_imu_imgui",
     "//lib/pw_touchscreen_imgui",
   ]
@@ -141,6 +143,7 @@
     "$dir_pw_thread:thread",
     "$dir_pw_thread_stl:thread",
     "//applications/app_common:app_common.facade",
+    "//lib/kudzu_buttons_null",
     "//lib/pw_touchscreen_null",
   ]
   sources = [ "common_host_null.cc" ]
diff --git a/applications/app_common_impl/common_host_imgui.cc b/applications/app_common_impl/common_host_imgui.cc
index b9d3675..c9e02e2 100644
--- a/applications/app_common_impl/common_host_imgui.cc
+++ b/applications/app_common_impl/common_host_imgui.cc
@@ -12,11 +12,13 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 #include "app_common/common.h"
+#include "kudzu_buttons_imgui/buttons.h"
 #include "kudzu_imu_imgui/imu.h"
 #include "pw_color/color.h"
 #include "pw_display_driver_imgui/display_driver.h"
 #include "pw_display_imgui/display.h"
 #include "pw_framebuffer_pool/framebuffer_pool.h"
+#include "pw_status/status.h"
 #include "pw_status/try.h"
 #include "pw_thread/thread.h"
 #include "pw_thread_stl/options.h"
@@ -28,6 +30,7 @@
 using pw::framebuffer_pool::FramebufferPool;
 
 using Touchscreen = pw::touchscreen::TouchscreenImGui;
+using Buttons = kudzu::ButtonsImgui;
 
 namespace {
 
@@ -58,7 +61,17 @@
 // static
 Status Common::EndOfFrameCallback() { return pw::OkStatus(); }
 
-Status Common::Init() { return s_display_driver.Init(); }
+Status Common::Init() {
+  auto status = s_display_driver.Init();
+  if (!status.ok()) {
+    return status;
+  }
+  status = GetButtons().Init();
+  if (!status.ok()) {
+    return status;
+  }
+  return pw::OkStatus();
+}
 
 // static
 pw::display::Display& Common::GetDisplay() {
@@ -72,6 +85,11 @@
   return s_touchscreen;
 }
 
+kudzu::Buttons& Common::GetButtons() {
+  static Buttons s_buttons = Buttons(s_display_driver);
+  return s_buttons;
+}
+
 kudzu::imu::PollingImu& Common::GetImu() {
   static kudzu::imu::PollingImuImGui s_imu = kudzu::imu::PollingImuImGui();
   return s_imu;
diff --git a/applications/app_common_impl/common_host_null.cc b/applications/app_common_impl/common_host_null.cc
index 11c9011..283c1e7 100644
--- a/applications/app_common_impl/common_host_null.cc
+++ b/applications/app_common_impl/common_host_null.cc
@@ -12,6 +12,7 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 #include "app_common/common.h"
+#include "kudzu_buttons_null/buttons.h"
 #include "pw_display/display.h"
 #include "pw_display_driver_null/display_driver.h"
 #include "pw_status/try.h"
@@ -23,6 +24,8 @@
 using pw::framebuffer::PixelFormat;
 using pw::framebuffer_pool::FramebufferPool;
 
+using Buttons = kudzu::ButtonsNull;
+
 namespace {
 
 constexpr pw::math::Size<uint16_t> kDisplaySize = {DISPLAY_WIDTH,
@@ -57,6 +60,11 @@
   return s_touchscreen;
 }
 
+kudzu::Buttons& Common::GetButtons() {
+  static Buttons s_buttons = Buttons();
+  return s_buttons;
+}
+
 const pw::thread::Options& Common::DisplayDrawThreadOptions() {
   static pw::thread::stl::Options display_draw_thread_options;
   return display_draw_thread_options;
@@ -65,4 +73,4 @@
 const pw::thread::Options& Common::TouchscreenThreadOptions() {
   static pw::thread::stl::Options display_draw_thread_options;
   return display_draw_thread_options;
-}
\ No newline at end of file
+}
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index 5701c02..c3d6cf1 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -24,6 +24,7 @@
 #include "hardware/pwm.h"
 #include "hardware/vreg.h"
 #include "icm42670p/device.h"
+#include "kudzu_buttons_pi4ioe5v6416/buttons.h"
 #include "kudzu_imu_icm42670p/imu.h"
 #include "max17048/device.h"
 #include "pi4ioe5v6416/device.h"
@@ -60,6 +61,7 @@
 #endif
 
 using Touchscreen = pw::touchscreen::TouchscreenFT6236;
+using Buttons = kudzu::ButtonsPI4IOE5V6416;
 
 using pw::Status;
 using pw::digital_io::Rp2040Config;
@@ -277,7 +279,7 @@
 }  // namespace
 
 Status Common::EndOfFrameCallback() {
-  // touch_screen_controller.LogControllerInfo();
+  touch_screen_controller.LogControllerInfo();
 
   if (io_expander.Probe() == pw::OkStatus()) {
     io_expander.LogControllerInfo();
@@ -383,6 +385,11 @@
   return s_touchscreen;
 }
 
+kudzu::Buttons& Common::GetButtons() {
+  static Buttons s_buttons = Buttons(&io_expander);
+  return s_buttons;
+}
+
 kudzu::imu::PollingImu& Common::GetImu() {
   static kudzu::imu::PollingImuICM42670P s_imu(&imu);
   return s_imu;
diff --git a/applications/badge/main.cc b/applications/badge/main.cc
index 669a8a8..ed48f08 100644
--- a/applications/badge/main.cc
+++ b/applications/badge/main.cc
@@ -22,6 +22,7 @@
 #include "graphics/surface.hpp"
 #include "heart_8x8.h"
 #include "hello_my_name_is65x42.h"
+#include "kudzu_buttons/buttons.h"
 #include "kudzu_isometric_text_sprite.h"
 #include "libkudzu/framecounter.h"
 #include "libkudzu/random.h"
@@ -46,6 +47,7 @@
 #include "pw_thread/detached_thread.h"
 #include "pw_touchscreen/touchscreen.h"
 
+using kudzu::Buttons;
 using pw::color::color_rgb565_t;
 using pw::color::colors_pico8_rgb565;
 using pw::display::Display;
@@ -62,6 +64,7 @@
 float angle = 0;
 
 bool show_nametag = false;
+bool show_background = false;
 
 // Draw the a waving text banner.
 // Returns the bottom Y coordinate of the bottommost pixel set.
@@ -123,13 +126,15 @@
   }
 }
 
-void DrawKudzu(Framebuffer& framebuffer) {
+void DrawKudzu(Framebuffer& framebuffer,
+               float y_scale_offset = 0.0,
+               float x_scale_offset = 0.0) {
   Vector2<int> tl = {0, 16};
 
   const float y_scale = 12.0;
   const float x_scale = 1.0;
-  const float max_x_offset = 4.0;
-  const float max_y_offset = 16.0;
+  const float max_x_offset = 4.0 + x_scale_offset;
+  const float max_y_offset = 16.0 + y_scale_offset;
 
   // X offsets between each letter
   std::array<int, 5> x_offsets = {32, 22, 26, 30, 0};
@@ -205,7 +210,6 @@
   tag_position += blit::Point(4, name_rect_y_offset);
   blit::Size name_size(screen.bounds.w - 8,
                        screen.bounds.h - name_rect_y_offset - 4);
-  PW_LOG_DEBUG("Tag size: %d, %d", name_size.w, name_size.h);
 
   blit::Rect name_rect(tag_position, name_size);
   screen.pen = blit::Pen(0xff, 0xff, 0xff);
@@ -215,6 +219,19 @@
       framebuffer, tag_position.x, tag_position.y, &name_tag_sprite_sheet, 1);
 }
 
+void DrawBackgroundColors(Framebuffer& framebuffer) {
+  static color_rgb565_t base_color = 0;
+  static uint16_t magic = 27;
+
+  uint16_t* p = static_cast<uint16_t*>(framebuffer.data());
+  for (int y = 0; y < framebuffer.size().height; y++) {
+    for (int x = 0; x < framebuffer.size().width; x++) {
+      *p++ = base_color + magic * (x ^ y);
+    }
+  }
+  base_color += 0x0021;
+}
+
 void MainTask(void*) {
   kudzu::FrameCounter frame_counter = kudzu::FrameCounter();
 
@@ -237,7 +254,14 @@
   Touchscreen& touchscreen = Common::GetTouchscreen();
   pw::touchscreen::TouchEvent last_touch_event;
 
+  Buttons& kudzu_buttons = Common::GetButtons();
+
   uint32_t frame_start_millis = pw::spin_delay::Millis();
+
+  float x_scale_offset = 0.0;
+  float y_scale_offset = 0.0;
+  const float x_scale_increment = 0.7;
+  const float y_scale_increment = 0.7;
   // The display loop.
   while (1) {
     frame_counter.StartFrame();
@@ -263,6 +287,28 @@
     blit::Point button_position(screen.bounds.w - button_size.w, 0);
     blit::Rect mode_button_rect(button_position, button_size);
 
+    kudzu_buttons.Update();
+    if (kudzu_buttons.Pressed(kudzu::button::a)) {
+      show_nametag = !show_nametag;
+    }
+    if (kudzu_buttons.Pressed(kudzu::button::b)) {
+      show_background = !show_background;
+    }
+    if (kudzu_buttons.Pressed(kudzu::button::start)) {
+      x_scale_offset = 0;
+      y_scale_offset = 0;
+    }
+    if (kudzu_buttons.Held(kudzu::button::left)) {
+      y_scale_offset += y_scale_increment;
+    } else if (kudzu_buttons.Held(kudzu::button::right)) {
+      y_scale_offset -= y_scale_increment;
+    }
+    if (kudzu_buttons.Held(kudzu::button::up)) {
+      x_scale_offset += x_scale_increment;
+    } else if (kudzu_buttons.Held(kudzu::button::down)) {
+      x_scale_offset -= x_scale_increment;
+    }
+
     if (show_nametag) {
       DrawNametag(framebuffer, screen);
       // Draw button
@@ -273,7 +319,10 @@
                   true,
                   blit::TextAlign::top_right);
     } else {
-      DrawKudzu(framebuffer);
+      if (show_background) {
+        DrawBackgroundColors(framebuffer);
+      }
+      DrawKudzu(framebuffer, x_scale_offset, y_scale_offset);
       DrawGreeting(framebuffer, screen);
 
       // Draw button
diff --git a/lib/kudzu_buttons/BUILD.gn b/lib/kudzu_buttons/BUILD.gn
new file mode 100644
index 0000000..d5482a3
--- /dev/null
+++ b/lib/kudzu_buttons/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("public_includes") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_buttons") {
+  public_configs = [ ":public_includes" ]
+  public = [ "public/kudzu_buttons/buttons.h" ]
+  public_deps = [
+    "$dir_pw_chrono:system_clock",
+    "$dir_pw_result",
+    "$dir_pw_status",
+  ]
+  deps = [ "$dir_pw_log" ]
+  sources = [ "buttons.cc" ]
+}
diff --git a/lib/kudzu_buttons/buttons.cc b/lib/kudzu_buttons/buttons.cc
new file mode 100644
index 0000000..6740bc7
--- /dev/null
+++ b/lib/kudzu_buttons/buttons.cc
@@ -0,0 +1,64 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "kudzu_buttons/buttons.h"
+
+#include <chrono>
+#include <cstdint>
+
+#define PW_LOG_MODULE_NAME "kudzu_buttons"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "pw_log/log.h"
+
+namespace kudzu {
+
+pw::Status Buttons::Update() {
+  // Get the new button bits.
+  pw::Result<std::bitset<kButtonCount>> update_result = DoUpdate();
+  if (!update_result.ok()) {
+    return update_result.status();
+  }
+
+  update_time_previous_ = update_time_;
+  update_time_ = pw::chrono::SystemClock::now();
+  pw::chrono::SystemClock::duration update_delta =
+      update_time_ - update_time_previous_;
+
+  button_bits_previous_ = button_bits_;
+  button_bits_ = update_result.value();
+
+  // Log if buttons changed.
+  if (button_bits_previous_ != button_bits_) {
+    std::string button_str = button_bits_.to_string();
+    PW_LOG_DEBUG("Buttons: %s", button_str.data());
+  }
+
+  // Track how long each button has been held.
+  for (int i = 0; i < kButtonCount; i++) {
+    if (Pressed(kudzu::button::ButtonName(i))) {
+      button_hold_duration_[i] = pw::chrono::SystemClock::duration(0);
+      PW_LOG_DEBUG("Button %d pressed", i);
+    } else if (Released(kudzu::button::ButtonName(i))) {
+      button_hold_duration_[i] = pw::chrono::SystemClock::duration(0);
+      PW_LOG_DEBUG("Button %d released", i);
+    } else if (Held(kudzu::button::ButtonName(i))) {
+      button_hold_duration_[i] += update_delta;
+    }
+  }
+
+  return pw::OkStatus();
+};
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons/public/kudzu_buttons/buttons.h b/lib/kudzu_buttons/public/kudzu_buttons/buttons.h
new file mode 100644
index 0000000..463f7d9
--- /dev/null
+++ b/lib/kudzu_buttons/public/kudzu_buttons/buttons.h
@@ -0,0 +1,96 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <bitset>
+#include <chrono>
+#include <cstdint>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace kudzu::button {
+
+enum ButtonName {
+  up = 0,
+  right = 1,
+  left = 2,
+  down = 3,
+  select = 4,
+  start = 5,
+  b = 6,
+  a = 7,
+};
+
+}  // namespace kudzu::button
+
+namespace kudzu {
+
+constexpr int kButtonCount = 8;
+
+class Buttons {
+ public:
+  virtual ~Buttons() = default;
+  virtual pw::Status Init() = 0;
+  /// Fetch the latest button states and update held times.
+  pw::Status Update();
+
+  /// Function to be implemented that returns a bitset of length kButtonCount. A
+  /// button press corresponds to a bit == 1 and a release == 0.
+  virtual pw::Result<std::bitset<kButtonCount>> DoUpdate() = 0;
+
+  /// Returns true if the button was just pressed. This only fires once on the
+  /// transition from not pressed to pressed.
+  inline bool Pressed(kudzu::button::ButtonName button_name) {
+    return button_bits_[button_name] && !button_bits_previous_[button_name];
+  }
+
+  /// Returns true if the button was just released. This only fires once on the
+  /// transition from held to released.
+  inline bool Released(kudzu::button::ButtonName button_name) {
+    return !button_bits_[button_name] && button_bits_previous_[button_name];
+  }
+
+  /// Returns true if the button is being held down.
+  inline bool Held(kudzu::button::ButtonName button_name) {
+    return button_bits_[button_name] && button_bits_previous_[button_name];
+  }
+
+  /// Button hold duration accessor.
+  ///
+  /// @code
+  ///   #include "pw_chrono/system_clock.h"
+  ///   using namespace std::chrono_literals;
+  ///
+  ///   if (buttons.HeldDuration(kudzu::button::up) >
+  ///       pw::chrono::SystemClock::for_at_least(1000ms)) {
+  ///     PW_LOG_INFO("Up button held for one second.");
+  ///   }
+  /// @endcode
+  inline pw::chrono::SystemClock::duration HeldDuration(
+      kudzu::button::ButtonName button_name) {
+    return button_hold_duration_[button_name];
+  }
+
+ private:
+  pw::chrono::SystemClock::time_point update_time_previous_;
+  pw::chrono::SystemClock::time_point update_time_;
+  std::bitset<kButtonCount> button_bits_;
+  std::bitset<kButtonCount> button_bits_previous_;
+  std::array<pw::chrono::SystemClock::duration, kButtonCount>
+      button_hold_duration_;
+};
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_imgui/BUILD.gn b/lib/kudzu_buttons_imgui/BUILD.gn
new file mode 100644
index 0000000..143ee23
--- /dev/null
+++ b/lib/kudzu_buttons_imgui/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_buttons_imgui") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/kudzu_buttons_imgui/buttons.h" ]
+  deps = [
+    "$dir_pigweed_experimental/third_party/glfw",
+    "$dir_pigweed_experimental/third_party/imgui",
+    "$dir_pw_display_driver_imgui",
+    "$dir_pw_log",
+    "//lib/kudzu_buttons:kudzu_buttons",
+  ]
+  sources = [ "buttons.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+  if (host_os == "linux") {
+    remove_configs += [ "$dir_pw_toolchain/host_clang:linux_sysroot" ]
+  }
+}
diff --git a/lib/kudzu_buttons_imgui/buttons.cc b/lib/kudzu_buttons_imgui/buttons.cc
new file mode 100644
index 0000000..8764e59
--- /dev/null
+++ b/lib/kudzu_buttons_imgui/buttons.cc
@@ -0,0 +1,83 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "kudzu_buttons_imgui/buttons.h"
+
+#include "kudzu_buttons/buttons.h"
+
+#define PW_LOG_MODULE_NAME "kudzu_buttons_imgui"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "pw_display_driver_imgui/display_driver.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+namespace kudzu {
+
+namespace {
+
+std::bitset<kButtonCount> current_buttons;
+
+// GLFW keyboard event handler.
+// See also: https://www.glfw.org/docs/3.3/input_guide.html#input_key
+void key_callback(
+    GLFWwindow* window, int key, int scancode, int action, int mods) {
+  bool state;
+  if (action == GLFW_PRESS) {
+    state = true;
+  } else if (action == GLFW_RELEASE) {
+    state = false;
+  } else {
+    // Don't handle actions other than press or release.
+    return;
+  }
+
+  // GLFW Keycodes: https://www.glfw.org/docs/3.3/group__keys.html
+  if (key == GLFW_KEY_W || key == GLFW_KEY_UP) {
+    current_buttons[kudzu::button::up] = state;
+  } else if (key == GLFW_KEY_A || key == GLFW_KEY_LEFT) {
+    current_buttons[kudzu::button::left] = state;
+  } else if (key == GLFW_KEY_S || key == GLFW_KEY_DOWN) {
+    current_buttons[kudzu::button::down] = state;
+  } else if (key == GLFW_KEY_D || key == GLFW_KEY_RIGHT) {
+    current_buttons[kudzu::button::right] = state;
+  } else if (key == GLFW_KEY_ENTER || key == GLFW_KEY_C) {
+    current_buttons[kudzu::button::start] = state;
+  } else if (key == GLFW_KEY_TAB || key == GLFW_KEY_V) {
+    current_buttons[kudzu::button::select] = state;
+  } else if (key == GLFW_KEY_COMMA || key == GLFW_KEY_Z) {
+    current_buttons[kudzu::button::b] = state;
+  } else if (key == GLFW_KEY_PERIOD || key == GLFW_KEY_X) {
+    current_buttons[kudzu::button::a] = state;
+  }
+}
+
+}  // namespace
+
+ButtonsImgui::ButtonsImgui(
+    pw::display_driver::DisplayDriverImgUI& display_driver)
+    : display_driver_(display_driver) {}
+
+pw::Status ButtonsImgui::Init() {
+  glfwSetKeyCallback(display_driver_.GetGlfwWindow(), key_callback);
+  current_buttons.reset();
+
+  return pw::OkStatus();
+}
+
+pw::Result<std::bitset<kButtonCount>> ButtonsImgui::DoUpdate() {
+  return current_buttons;
+}
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_imgui/public/kudzu_buttons_imgui/buttons.h b/lib/kudzu_buttons_imgui/public/kudzu_buttons_imgui/buttons.h
new file mode 100644
index 0000000..8f06fa6
--- /dev/null
+++ b/lib/kudzu_buttons_imgui/public/kudzu_buttons_imgui/buttons.h
@@ -0,0 +1,32 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "kudzu_buttons/buttons.h"
+#include "pw_display_driver_imgui/display_driver.h"
+#include "pw_status/status.h"
+
+namespace kudzu {
+
+class ButtonsImgui : public Buttons {
+ public:
+  ButtonsImgui(pw::display_driver::DisplayDriverImgUI& display_driver);
+  pw::Status Init() override;
+  pw::Result<std::bitset<kButtonCount>> DoUpdate() override;
+
+ private:
+  pw::display_driver::DisplayDriverImgUI& display_driver_;
+};
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_null/BUILD.gn b/lib/kudzu_buttons_null/BUILD.gn
new file mode 100644
index 0000000..cd81f2a
--- /dev/null
+++ b/lib/kudzu_buttons_null/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_buttons_null") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/kudzu_buttons_null/buttons.h" ]
+  deps = [
+    "$dir_pw_log",
+    "//lib/kudzu_buttons:kudzu_buttons",
+  ]
+  sources = [ "buttons.cc" ]
+}
diff --git a/lib/kudzu_buttons_null/buttons.cc b/lib/kudzu_buttons_null/buttons.cc
new file mode 100644
index 0000000..97bb7b1
--- /dev/null
+++ b/lib/kudzu_buttons_null/buttons.cc
@@ -0,0 +1,30 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "kudzu_buttons_null/buttons.h"
+
+#include "pw_status/status.h"
+
+namespace kudzu {
+
+ButtonsNull::ButtonsNull() {}
+
+pw::Status ButtonsNull::Init() { return pw::Status::Unimplemented(); }
+
+pw::Result<std::bitset<kButtonCount>> ButtonsNull::DoUpdate() {
+  std::bitset<kButtonCount> new_bits = 0;
+  return new_bits;
+}
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_null/public/kudzu_buttons_null/buttons.h b/lib/kudzu_buttons_null/public/kudzu_buttons_null/buttons.h
new file mode 100644
index 0000000..3884c2a
--- /dev/null
+++ b/lib/kudzu_buttons_null/public/kudzu_buttons_null/buttons.h
@@ -0,0 +1,28 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "kudzu_buttons/buttons.h"
+#include "pw_status/status.h"
+
+namespace kudzu {
+
+class ButtonsNull : public Buttons {
+ public:
+  ButtonsNull();
+  pw::Status Init() override;
+  pw::Result<std::bitset<kButtonCount>> DoUpdate() override;
+};
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_pi4ioe5v6416/BUILD.gn b/lib/kudzu_buttons_pi4ioe5v6416/BUILD.gn
new file mode 100644
index 0000000..ec87769
--- /dev/null
+++ b/lib/kudzu_buttons_pi4ioe5v6416/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2024 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_buttons_pi4ioe5v6416") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/kudzu_buttons_pi4ioe5v6416/buttons.h" ]
+  deps = [
+    "$dir_pw_log",
+    "$dir_pw_result",
+    "//lib/kudzu_buttons:kudzu_buttons",
+    "//lib/pi4ioe5v6416",
+  ]
+  sources = [ "buttons.cc" ]
+}
diff --git a/lib/kudzu_buttons_pi4ioe5v6416/buttons.cc b/lib/kudzu_buttons_pi4ioe5v6416/buttons.cc
new file mode 100644
index 0000000..76602ac
--- /dev/null
+++ b/lib/kudzu_buttons_pi4ioe5v6416/buttons.cc
@@ -0,0 +1,49 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "kudzu_buttons/buttons.h"
+
+#include <bitset>
+
+#define PW_LOG_MODULE_NAME "kudzu_buttons_pi4ioe5v6416"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "kudzu_buttons_pi4ioe5v6416/buttons.h"
+#include "pi4ioe5v6416/device.h"
+#include "pw_log/log.h"
+
+namespace kudzu {
+
+ButtonsPI4IOE5V6416::ButtonsPI4IOE5V6416(pw::pi4ioe5v6416::Device* controller)
+    : controller_(controller) {}
+
+pw::Status ButtonsPI4IOE5V6416::Init() {
+  controller_->Enable();
+  return pw::OkStatus();
+}
+
+pw::Result<std::bitset<kButtonCount>> ButtonsPI4IOE5V6416::DoUpdate() {
+  pw::Result<uint8_t> result = controller_->ReadPort0();
+  if (!result.ok()) {
+    return result.status();
+  }
+  std::bitset<kButtonCount> new_bits = result.value();
+  // The button pins use the GPIO expanders internal pull up resistors so a
+  // press is 0 and a 1 is released. Flip the bits so 1 is a press and 0 is a
+  // release.
+  new_bits.flip();
+  return new_bits;
+}
+
+}  // namespace kudzu
diff --git a/lib/kudzu_buttons_pi4ioe5v6416/public/kudzu_buttons_pi4ioe5v6416/buttons.h b/lib/kudzu_buttons_pi4ioe5v6416/public/kudzu_buttons_pi4ioe5v6416/buttons.h
new file mode 100644
index 0000000..4749c50
--- /dev/null
+++ b/lib/kudzu_buttons_pi4ioe5v6416/public/kudzu_buttons_pi4ioe5v6416/buttons.h
@@ -0,0 +1,32 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "kudzu_buttons/buttons.h"
+#include "pi4ioe5v6416/device.h"
+#include "pw_status/status.h"
+
+namespace kudzu {
+
+class ButtonsPI4IOE5V6416 : public Buttons {
+ public:
+  ButtonsPI4IOE5V6416(pw::pi4ioe5v6416::Device* controller);
+  pw::Status Init() override;
+  pw::Result<std::bitset<kButtonCount>> DoUpdate() override;
+
+ private:
+  pw::pi4ioe5v6416::Device* controller_;
+};
+
+}  // namespace kudzu