Add a sample UI for linux apps - lighting app (#24979)

* Initial version of a imgui UI in light app for linux.

Also fixed linux app shutdown to handle signals
by loop terminating instead of app killing.

* remove some fixme comments

* Fix unit tests ... with-ui variant was added

* Restyle

* Update text and remove some commented out code

* Restyle

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
diff --git a/.gitmodules b/.gitmodules
index b104291..d9f332d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -289,3 +289,7 @@
 	path = third_party/libwebsockets/repo
 	url = https://github.com/warmcat/libwebsockets
 	platforms = linux,darwin,tizen
+[submodule "third_party/imgui/repo"]
+	path = third_party/imgui/repo
+	url = https://github.com/ocornut/imgui
+	platforms = linux
diff --git a/examples/common/QRCode/BUILD.gn b/examples/common/QRCode/BUILD.gn
index d7918f5..b876f48 100644
--- a/examples/common/QRCode/BUILD.gn
+++ b/examples/common/QRCode/BUILD.gn
@@ -21,7 +21,10 @@
 static_library("QRCode") {
   output_name = "libqrcode-common"
 
-  sources = [ "repo/c/qrcodegen.c" ]
+  sources = [
+    "repo/c/qrcodegen.c",
+    "repo/c/qrcodegen.h",
+  ]
 
   public_configs = [ ":qrcode-common_config" ]
 
diff --git a/examples/lighting-app/linux/BUILD.gn b/examples/lighting-app/linux/BUILD.gn
index 721f408..3b20243 100644
--- a/examples/lighting-app/linux/BUILD.gn
+++ b/examples/lighting-app/linux/BUILD.gn
@@ -16,6 +16,7 @@
 
 import("${chip_root}/build/chip/tools.gni")
 import("${chip_root}/src/app/common_flags.gni")
+import("${chip_root}/third_party/imgui/imgui.gni")
 
 assert(chip_build_tools)
 
@@ -46,6 +47,18 @@
     "${chip_root}/src/lib",
   ]
 
+  if (chip_examples_enable_imgui_ui) {
+    deps += [
+      "${chip_root}/examples/common/QRCode",
+      "${chip_root}/third_party/imgui",
+    ]
+
+    sources += [
+      "ui.cpp",
+      "ui.h",
+    ]
+  }
+
   include_dirs = [ "include" ]
 
   if (chip_enable_pw_rpc) {
diff --git a/examples/lighting-app/linux/main.cpp b/examples/lighting-app/linux/main.cpp
index 7813c59..a74aa5d 100644
--- a/examples/lighting-app/linux/main.cpp
+++ b/examples/lighting-app/linux/main.cpp
@@ -26,6 +26,10 @@
 #include <lib/support/logging/CHIPLogging.h>
 #include <platform/Linux/NetworkCommissioningDriver.h>
 
+#if defined(CHIP_IMGUI_ENABLED) && CHIP_IMGUI_ENABLED
+#include "ui.h"
+#endif
+
 using namespace chip;
 using namespace chip::app;
 using namespace chip::app::Clusters;
@@ -81,7 +85,16 @@
     }
 
     LightingMgr().Init();
+
+#if defined(CHIP_IMGUI_ENABLED) && CHIP_IMGUI_ENABLED
+    example::Ui::Start();
+#endif
+
     ChipLinuxAppMainLoop();
 
+#if defined(CHIP_IMGUI_ENABLED) && CHIP_IMGUI_ENABLED
+    example::Ui::Stop();
+#endif
+
     return 0;
 }
diff --git a/examples/lighting-app/linux/ui.cpp b/examples/lighting-app/linux/ui.cpp
new file mode 100644
index 0000000..32ff5f4
--- /dev/null
+++ b/examples/lighting-app/linux/ui.cpp
@@ -0,0 +1,370 @@
+/*
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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
+ *
+ *        http://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 "ui.h"
+
+#include <Options.h> // examples/platform/linux/Options.h
+#include <app/server/OnboardingCodesUtil.h>
+#include <lib/support/logging/CHIPLogging.h>
+#include <platform/CHIPDeviceLayer.h>
+#include <platform/PlatformManager.h>
+#include <setup_payload/QRCodeSetupPayloadGenerator.h>
+#include <setup_payload/SetupPayload.h>
+
+#include <app-common/zap-generated/ids/Attributes.h>
+#include <app-common/zap-generated/ids/Clusters.h>
+#include <app/util/attribute-storage.h>
+
+#include <SDL.h>
+#include <SDL_opengl.h>
+#include <imgui.h>
+#include <imgui_impl_opengl3.h>
+#include <imgui_impl_sdl2.h>
+#include <qrcodegen.h>
+
+#include <atomic>
+#include <chrono>
+#include <semaphore.h>
+#include <thread>
+
+namespace example {
+namespace Ui {
+
+namespace {
+
+std::atomic<bool> gUiRunning{ false };
+
+class DeviceState
+{
+public:
+    DeviceState() { sem_init(&mChipLoopWaitSemaphore, 0 /* shared */, 0); }
+    ~DeviceState() { sem_destroy(&mChipLoopWaitSemaphore); }
+
+    // Initialize. MUST be called within the CHIP main loop as it
+    // loads startup data.
+    void Init();
+
+    // Use ImgUI to show the current state
+    void ShowUi();
+
+    // Fetches the current state from Ember
+    void UpdateState();
+
+private:
+    static constexpr int kQRCodeVersion   = qrcodegen_VERSION_MAX;
+    static constexpr int kMaxQRBufferSize = qrcodegen_BUFFER_LEN_FOR_VERSION(kQRCodeVersion);
+
+    sem_t mChipLoopWaitSemaphore;
+
+    bool mHasQRCode                   = false;
+    uint8_t mQRData[kMaxQRBufferSize] = { 0 };
+
+    // light data:
+    bool mOnOff = false;
+
+    // Updates the data (run in the chip event loop)
+    void ChipLoopUpdate();
+
+    void InitQRCode();
+
+    // Run in CHIPMainLoop to access ember in a single threaded
+    // fashion
+    static void ChipLoopUpdateCallback(intptr_t self);
+};
+
+DeviceState gDeviceState;
+
+void DeviceState::Init()
+{
+    InitQRCode();
+}
+
+void DeviceState::InitQRCode()
+{
+
+    chip::PayloadContents payload = LinuxDeviceOptions::GetInstance().payload;
+    if (!payload.isValidQRCodePayload())
+    {
+        return;
+    }
+
+    char payloadBuffer[chip::QRCodeBasicSetupPayloadGenerator::kMaxQRCodeBase38RepresentationLength + 1];
+    chip::MutableCharSpan qrCode(payloadBuffer);
+
+    CHIP_ERROR err = GetQRCode(qrCode, payload);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(AppServer, "Failed to load QR code: %" CHIP_ERROR_FORMAT, err.Format());
+        return;
+    }
+
+    if (qrCode.size() > kMaxQRBufferSize)
+    {
+        ChipLogError(AppServer, "Insufficient qr code buffer size to encode");
+        return;
+    }
+
+    uint8_t tempAndData[kMaxQRBufferSize];
+    memcpy(tempAndData, qrCode.data(), qrCode.size());
+
+    mHasQRCode = qrcodegen_encodeBinary(tempAndData, qrCode.size(), mQRData, qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
+                                        qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
+
+    if (!mHasQRCode)
+    {
+        ChipLogError(AppServer, "Failed to encode QR code");
+        return;
+    }
+}
+
+inline ImVec2 operator+(const ImVec2 & a, const ImVec2 & b)
+{
+    return ImVec2(a.x + b.x, a.y + b.y);
+}
+
+void DeviceState::ShowUi()
+{
+    ImGui::Begin("Light app");
+    ImGui::Text("Here is the current ember device state:");
+    ImGui::Checkbox("Light is ON", &mOnOff);
+    ImGui::End();
+
+    if (mHasQRCode)
+    {
+        ImGui::Begin("QR Code.");
+
+        ImDrawList * drawList = ImGui::GetWindowDrawList();
+
+        constexpr int kBorderSize    = 35;
+        constexpr int kMinWindowSize = 200;
+        const int kQRCodeSize        = qrcodegen_getSize(mQRData);
+
+        ImVec2 pos  = ImGui::GetWindowPos();
+        ImVec2 size = ImGui::GetWindowSize();
+
+        if (size.y < kMinWindowSize)
+        {
+            size = ImVec2(kMinWindowSize, kMinWindowSize);
+            ImGui::SetWindowSize(size);
+        }
+
+        // Fill the entire window white, then figure out borders
+        drawList->AddRectFilled(pos, pos + size, IM_COL32_WHITE);
+
+        // add a border
+        if (size.x >= 2 * kBorderSize && size.y >= 2 * kBorderSize)
+        {
+            size.x -= 2 * kBorderSize;
+            size.y -= 2 * kBorderSize;
+            pos.x += kBorderSize;
+            pos.y += kBorderSize;
+        }
+
+        // create a square rectangle: keep only the smaller side and adjust the
+        // other
+        if (size.x > size.y)
+        {
+            pos.x += (size.x - size.y) / 2;
+            size.x = size.y;
+        }
+        else if (size.y > size.x)
+        {
+            pos.y += (size.y - size.x) / 2;
+            size.y = size.x;
+        }
+
+        const ImVec2 squareSize = ImVec2(size.x / static_cast<float>(kQRCodeSize), size.y / static_cast<float>(kQRCodeSize));
+
+        for (int y = 0; y < kQRCodeSize; ++y)
+        {
+            for (int x = 0; x < kQRCodeSize; ++x)
+            {
+                if (qrcodegen_getModule(mQRData, x, y))
+                {
+                    ImVec2 placement =
+                        ImVec2(pos.x + static_cast<float>(x) * squareSize.x, pos.y + static_cast<float>(y) * squareSize.y);
+                    drawList->AddRectFilled(placement, placement + squareSize, IM_COL32_BLACK);
+                }
+            }
+        }
+
+        ImGui::End();
+    }
+}
+
+void DeviceState::ChipLoopUpdate()
+{
+    // This will contain a dimmable light
+    static constexpr chip::EndpointId kLightEndpointId = 1;
+
+    // TODO:
+    //    - consider error checking
+    //    - add more attributes to the display (color? brightness?)
+    {
+        uint8_t value;
+        emberAfReadServerAttribute(kLightEndpointId, chip::app::Clusters::OnOff::Id,
+                                   chip::app::Clusters::OnOff::Attributes::OnOff::Id, &value, sizeof(value));
+        mOnOff = (value != 0);
+    }
+}
+
+void DeviceState::ChipLoopUpdateCallback(intptr_t self)
+{
+    DeviceState * _this = reinterpret_cast<DeviceState *>(self);
+    _this->ChipLoopUpdate();
+    sem_post(&_this->mChipLoopWaitSemaphore); // notify complete
+}
+
+void DeviceState::UpdateState()
+{
+    chip::DeviceLayer::PlatformMgr().ScheduleWork(&ChipLoopUpdateCallback, reinterpret_cast<intptr_t>(this));
+    // ensure update is done when existing
+    if (sem_trywait(&mChipLoopWaitSemaphore) != 0)
+    {
+        if (!gUiRunning.load())
+        {
+            // UI should stop, no need to wait, probably chip main loop is stopped
+            return;
+        }
+        std::this_thread::yield();
+    }
+}
+
+void UiInit(SDL_GLContext * gl_context, SDL_Window ** window)
+{
+    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0)
+    {
+        ChipLogError(AppServer, "SDL Init Error: %s\n", SDL_GetError());
+        return;
+    }
+
+    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
+    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
+    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
+#ifdef SDL_HINT_IME_SHOW_UI
+    SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
+#endif
+
+    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
+    SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
+    *window     = SDL_CreateWindow("Dear ImGui SDL2+OpenGL3 example", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1280, 720,
+                               window_flags);
+    *gl_context = SDL_GL_CreateContext(*window);
+    SDL_GL_MakeCurrent(*window, *gl_context);
+    SDL_GL_SetSwapInterval(1); // Enable vsync
+
+    // Setup Dear ImGui context
+    IMGUI_CHECKVERSION();
+    ImGui::CreateContext();
+    ImGuiIO & io = ImGui::GetIO();
+    (void) io;
+    // io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
+    // io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls
+
+    // Setup Dear ImGui style
+    ImGui::StyleColorsDark();
+    // ImGui::StyleColorsLight();
+
+    // Setup Platform/Renderer backends
+    ImGui_ImplSDL2_InitForOpenGL(*window, *gl_context);
+    ImGui_ImplOpenGL3_Init("#version 130");
+}
+
+void UiShutdown(SDL_GLContext * gl_context, SDL_Window ** window)
+{
+    ImGui_ImplOpenGL3_Shutdown();
+    ImGui_ImplSDL2_Shutdown();
+    ImGui::DestroyContext();
+
+    SDL_GL_DeleteContext(*gl_context);
+    SDL_DestroyWindow(*window);
+    SDL_Quit();
+}
+
+void UiLoop()
+{
+    SDL_GLContext gl_context;
+    SDL_Window * window = nullptr;
+
+    UiInit(&gl_context, &window);
+
+    ImGuiIO & io = ImGui::GetIO();
+
+    while (gUiRunning.load())
+    {
+        SDL_Event event;
+        while (SDL_PollEvent(&event))
+        {
+            ImGui_ImplSDL2_ProcessEvent(&event);
+            if (event.type == SDL_QUIT)
+            {
+                chip::DeviceLayer::PlatformMgr().StopEventLoopTask();
+            }
+            if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE &&
+                event.window.windowID == SDL_GetWindowID(window))
+            {
+                chip::DeviceLayer::PlatformMgr().StopEventLoopTask();
+            }
+        }
+
+        ImGui_ImplOpenGL3_NewFrame();
+        ImGui_ImplSDL2_NewFrame();
+        ImGui::NewFrame();
+
+        gDeviceState.UpdateState();
+        gDeviceState.ShowUi();
+
+        // rendering
+        ImGui::Render();
+        glViewport(0, 0, (int) io.DisplaySize.x, (int) io.DisplaySize.y);
+        glClearColor(0, 0, 0, 0);
+        glClear(GL_COLOR_BUFFER_BIT);
+        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+        SDL_GL_SwapWindow(window);
+    }
+
+    UiShutdown(&gl_context, &window);
+
+    ChipLogProgress(AppServer, "UI thread Stopped...");
+}
+
+static std::thread gUiThread;
+
+} // namespace
+
+void Start()
+{
+    // Init inside the "main" thread, so that it can access globals
+    // proparly (for QR code and such)
+    gDeviceState.Init();
+
+    gUiRunning = true;
+    std::thread uiThread(&UiLoop);
+    gUiThread.swap(uiThread);
+}
+
+void Stop()
+{
+    gUiRunning = false;
+    gUiThread.join();
+}
+
+} // namespace Ui
+} // namespace example
diff --git a/examples/lighting-app/linux/ui.h b/examples/lighting-app/linux/ui.h
new file mode 100644
index 0000000..85bf2aa
--- /dev/null
+++ b/examples/lighting-app/linux/ui.h
@@ -0,0 +1,28 @@
+/*
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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
+ *
+ *        http://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
+
+namespace example {
+namespace Ui {
+
+void Start();
+
+void Stop();
+
+} // namespace Ui
+} // namespace example
diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp
index 876e0da..05b8066 100644
--- a/examples/platform/linux/AppMain.cpp
+++ b/examples/platform/linux/AppMain.cpp
@@ -124,6 +124,11 @@
     // TODO(16968): Lifecycle management of storage-using components like GroupDataProvider, etc
 }
 
+void StopSignalHandler(int signal)
+{
+    DeviceLayer::PlatformMgr().StopEventLoopTask();
+}
+
 } // namespace
 
 #if CHIP_DEVICE_CONFIG_ENABLE_WPA
@@ -389,6 +394,8 @@
 
     ApplicationInit();
 
+    signal(SIGTERM, StopSignalHandler);
+    signal(SIGINT, StopSignalHandler);
     DeviceLayer::PlatformMgr().RunEventLoop();
 
 #if CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE
diff --git a/scripts/build/build/targets.py b/scripts/build/build/targets.py
index 2a6b170..db7053e 100755
--- a/scripts/build/build/targets.py
+++ b/scripts/build/build/targets.py
@@ -148,6 +148,7 @@
     target.AppendModifier('clang', use_clang=True)
     target.AppendModifier('test', extra_tests=True)
     target.AppendModifier('rpc', enable_rpcs=True)
+    target.AppendModifier('with-ui', imgui_ui=True)
 
     return target
 
diff --git a/scripts/build/builders/host.py b/scripts/build/builders/host.py
index b05a034..b3bfbaa 100644
--- a/scripts/build/builders/host.py
+++ b/scripts/build/builders/host.py
@@ -229,6 +229,7 @@
                  use_coverage=False, use_dmalloc=False,
                  minmdns_address_policy=None,
                  minmdns_high_verbosity=False,
+                 imgui_ui=False,
                  crypto_library: HostCryptoLibrary = None):
         super(HostBuilder, self).__init__(
             root=os.path.join(root, 'examples', app.ExamplePath()),
@@ -282,6 +283,9 @@
         if use_libfuzzer:
             self.extra_gn_options.append('is_libfuzzer=true')
 
+        if imgui_ui:
+            self.extra_gn_options.append('chip_examples_enable_imgui_ui=true')
+
         self.use_coverage = use_coverage
         if use_coverage:
             self.extra_gn_options.append('use_coverage=true')
diff --git a/scripts/build/testdata/all_targets_linux_x64.txt b/scripts/build/testdata/all_targets_linux_x64.txt
index 7b7ae21..77c780e 100644
--- a/scripts/build/testdata/all_targets_linux_x64.txt
+++ b/scripts/build/testdata/all_targets_linux_x64.txt
@@ -8,7 +8,7 @@
 esp32-{m5stack,c3devkit,devkitc,qemu}-{all-clusters,all-clusters-minimal,ota-provider,ota-requestor,shell,light,lock,bridge,temperature-measurement,ota-requestor,tests}[-rpc][-ipv6only]
 genio-lighting-app
 linux-fake-tests[-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-coverage][-dmalloc][-clang]
-linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,minmdns,light,lock,shell,ota-provider,ota-requestor,python-bindings,tv-app,tv-casting-app,bridge,dynamic-bridge,tests,chip-cert,address-resolve-tool}[-nodeps][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-coverage][-dmalloc][-clang][-test][-rpc]
+linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,minmdns,light,lock,shell,ota-provider,ota-requestor,python-bindings,tv-app,tv-casting-app,bridge,dynamic-bridge,tests,chip-cert,address-resolve-tool}[-nodeps][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-coverage][-dmalloc][-clang][-test][-rpc][-with-ui]
 linux-x64-efr32-test-runner[-clang]
 imx-{chip-tool,lighting-app,thermostat,all-clusters-app,all-clusters-minimal-app,ota-provider-app}[-release]
 infineon-psoc6-{lock,light,all-clusters,all-clusters-minimal}[-ota][-updateimage]
diff --git a/third_party/imgui/BUILD.gn b/third_party/imgui/BUILD.gn
new file mode 100644
index 0000000..362e964
--- /dev/null
+++ b/third_party/imgui/BUILD.gn
@@ -0,0 +1,63 @@
+# Copyright (c) 2023 Project CHIP 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
+#
+# http://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/build.gni")
+import("//build_overrides/chip.gni")
+
+config("imgui_config") {
+  include_dirs = [
+    "repo",
+    "repo/backends",
+  ]
+  defines = [ "CHIP_IMGUI_ENABLED=1" ]
+
+  # FUTURE: these should be parsed from `sdl2-config --cflags`
+  include_dirs += [ "/usr/include/SDL2" ]
+  defines += [ "_REENTRANT" ]
+}
+
+source_set("imgui") {
+  sources = [
+    "repo/imconfig.h",
+    "repo/imgui.cpp",
+    "repo/imgui.h",
+    "repo/imgui_draw.cpp",
+    "repo/imgui_internal.h",
+    "repo/imgui_tables.cpp",
+    "repo/imgui_widgets.cpp",
+    "repo/imstb_rectpack.h",
+    "repo/imstb_textedit.h",
+    "repo/imstb_truetype.h",
+  ]
+
+  # SDL2 + OPENGL3 backend enabled directly here since
+  # the includes are circular (backend includes imgui)
+  sources += [
+    "repo/backends/imgui_impl_opengl3.cpp",
+    "repo/backends/imgui_impl_opengl3.h",
+    "repo/backends/imgui_impl_sdl2.cpp",
+    "repo/backends/imgui_impl_sdl2.h",
+  ]
+
+  # FUTURE: SDL2 libs should be from `sdl2-config --libs`
+  #         Also different platforms may require different seettings (e.g. on mac this
+  #         seems to need `-framework OpenGl -framework CoreFoundation`
+  libs = [
+    "SDL2",
+    "GL",
+    "dl",
+  ]
+
+  public_configs = [ ":imgui_config" ]
+}
diff --git a/third_party/imgui/imgui.gni b/third_party/imgui/imgui.gni
new file mode 100644
index 0000000..6db9204
--- /dev/null
+++ b/third_party/imgui/imgui.gni
@@ -0,0 +1,23 @@
+# Copyright (c) 2023 Project CHIP 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
+#
+# http://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/build.gni")
+import("//build_overrides/chip.gni")
+
+import("${chip_root}/src/platform/device.gni")
+
+declare_args() {
+  # Enable UI functionality for example apps
+  chip_examples_enable_imgui_ui = false
+}
diff --git a/third_party/imgui/repo b/third_party/imgui/repo
new file mode 160000
index 0000000..85395b7
--- /dev/null
+++ b/third_party/imgui/repo
@@ -0,0 +1 @@
+Subproject commit 85395b76b08111961aa6e0ff026fd5152d48aa15