Split out FlKeyboardLayout into its own class (#55816)

diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index ef13edb..4b7568d 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -44806,6 +44806,9 @@
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_handler.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_handler.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_handler_test.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_layout.cc + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_layout.h + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_layout_test.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_pending_event.cc + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_pending_event.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/shell/platform/linux/fl_keyboard_view_delegate.cc + ../../../flutter/LICENSE
@@ -47713,6 +47716,9 @@
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_handler.cc
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_handler.h
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_handler_test.cc
+FILE: ../../../flutter/shell/platform/linux/fl_keyboard_layout.cc
+FILE: ../../../flutter/shell/platform/linux/fl_keyboard_layout.h
+FILE: ../../../flutter/shell/platform/linux/fl_keyboard_layout_test.cc
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_pending_event.cc
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_pending_event.h
 FILE: ../../../flutter/shell/platform/linux/fl_keyboard_view_delegate.cc
diff --git a/shell/platform/linux/BUILD.gn b/shell/platform/linux/BUILD.gn
index 9cbcc9f..22c192e 100644
--- a/shell/platform/linux/BUILD.gn
+++ b/shell/platform/linux/BUILD.gn
@@ -117,6 +117,7 @@
     "fl_key_event.cc",
     "fl_key_responder.cc",
     "fl_keyboard_handler.cc",
+    "fl_keyboard_layout.cc",
     "fl_keyboard_pending_event.cc",
     "fl_keyboard_view_delegate.cc",
     "fl_message_codec.cc",
@@ -215,6 +216,7 @@
     "fl_key_channel_responder_test.cc",
     "fl_key_embedder_responder_test.cc",
     "fl_keyboard_handler_test.cc",
+    "fl_keyboard_layout_test.cc",
     "fl_message_codec_test.cc",
     "fl_method_channel_test.cc",
     "fl_method_codec_test.cc",
diff --git a/shell/platform/linux/fl_keyboard_handler.cc b/shell/platform/linux/fl_keyboard_handler.cc
index 0b825cb..2901693 100644
--- a/shell/platform/linux/fl_keyboard_handler.cc
+++ b/shell/platform/linux/fl_keyboard_handler.cc
@@ -11,6 +11,7 @@
 
 #include "flutter/shell/platform/linux/fl_key_channel_responder.h"
 #include "flutter/shell/platform/linux/fl_key_embedder_responder.h"
+#include "flutter/shell/platform/linux/fl_keyboard_layout.h"
 #include "flutter/shell/platform/linux/fl_keyboard_pending_event.h"
 #include "flutter/shell/platform/linux/key_mapping.h"
 #include "flutter/shell/platform/linux/public/flutter_linux/fl_method_channel.h"
@@ -25,8 +26,6 @@
 
 /* Declarations of private classes */
 
-#define FL_TYPE_KEYBOARD_HANDLER_USER_DATA \
-  fl_keyboard_handler_user_data_get_type()
 G_DECLARE_FINAL_TYPE(FlKeyboardHandlerUserData,
                      fl_keyboard_handler_user_data,
                      FL,
@@ -37,20 +36,6 @@
 
 namespace {
 
-// The maxiumum keycode in a derived layout.
-//
-// Although X supports higher keycodes, Flutter only cares about standard keys,
-// which are below this.
-constexpr size_t kLayoutSize = 128;
-// Describes the derived layout of a keyboard group.
-//
-// Maps from keycode to logical key. Value being 0 stands for empty.
-typedef std::array<uint64_t, kLayoutSize> DerivedGroupLayout;
-// Describes the derived layout of the entire keyboard.
-//
-// Maps from group ID to group layout.
-typedef std::map<guint8, DerivedGroupLayout> DerivedLayout;
-
 // Context variables for the foreach call used to dispatch events to responders.
 typedef struct {
   FlKeyEvent* event;
@@ -58,7 +43,7 @@
   FlKeyboardHandlerUserData* user_data;
 } DispatchToResponderLoopContext;
 
-bool is_eascii(uint16_t character) {
+static bool is_eascii(uint16_t character) {
   return character < 256;
 }
 
@@ -89,21 +74,6 @@
 
 }  // namespace
 
-static uint64_t get_logical_key_from_layout(FlKeyEvent* event,
-                                            const DerivedLayout& layout) {
-  guint8 group = fl_key_event_get_group(event);
-  guint16 keycode = fl_key_event_get_keycode(event);
-  if (keycode >= kLayoutSize) {
-    return 0;
-  }
-
-  auto found_group_layout = layout.find(group);
-  if (found_group_layout != layout.end()) {
-    return found_group_layout->second[keycode];
-  }
-  return 0;
-}
-
 /* Define FlKeyboardHandlerUserData */
 
 /**
@@ -192,11 +162,13 @@
   // It is cleared when the platform reports a layout switch. Each entry,
   // which corresponds to a group, is only initialized on the arrival of the
   // first event for that group that has a goal keycode.
-  std::unique_ptr<DerivedLayout> derived_layout;
+  FlKeyboardLayout* derived_layout;
+
   // A static map from keycodes to all layout goals.
   //
   // It is set up when the handler is initialized and is not changed ever after.
   std::unique_ptr<std::map<uint16_t, const LayoutGoal*>> keycode_to_goals;
+
   // A static map from logical keys to all mandatory layout goals.
   //
   // It is set up when the handler is initialized and is not changed ever after.
@@ -322,7 +294,7 @@
 // if the event contains a goal keycode.
 static void guarantee_layout(FlKeyboardHandler* self, FlKeyEvent* event) {
   guint8 group = fl_key_event_get_group(event);
-  if (self->derived_layout->find(group) != self->derived_layout->end()) {
+  if (fl_keyboard_layout_has_group(self->derived_layout, group)) {
     return;
   }
   if (self->keycode_to_goals->find(fl_key_event_get_keycode(event)) ==
@@ -330,8 +302,6 @@
     return;
   }
 
-  DerivedGroupLayout& layout = (*self->derived_layout)[group];
-
   // Clone all mandatory goals. Each goal is removed from this cloned map when
   // fulfilled, and the remaining ones will be assigned to a default position.
   std::map<uint64_t, const LayoutGoal*> remaining_mandatory_goals =
@@ -371,8 +341,10 @@
       auto matching_goal = remaining_mandatory_goals.find(clue);
       if (matching_goal != remaining_mandatory_goals.end()) {
         // Found a key that produces a mandatory char. Use it.
-        g_return_if_fail(layout[keycode] == 0);
-        layout[keycode] = clue;
+        g_return_if_fail(fl_keyboard_layout_get_logical_key(
+                             self->derived_layout, group, keycode) == 0);
+        fl_keyboard_layout_set_logical_key(self->derived_layout, group, keycode,
+                                           clue);
         remaining_mandatory_goals.erase(matching_goal);
         break;
       }
@@ -380,10 +352,14 @@
     bool has_any_eascii =
         is_eascii(this_key_clues[0]) || is_eascii(this_key_clues[1]);
     // See if any produced char meets the requirement as a logical key.
-    if (layout[keycode] == 0 && !has_any_eascii) {
+    if (fl_keyboard_layout_get_logical_key(self->derived_layout, group,
+                                           keycode) == 0 &&
+        !has_any_eascii) {
       auto found_us_layout = self->keycode_to_goals->find(keycode);
       if (found_us_layout != self->keycode_to_goals->end()) {
-        layout[keycode] = found_us_layout->second->logical_key;
+        fl_keyboard_layout_set_logical_key(
+            self->derived_layout, group, keycode,
+            found_us_layout->second->logical_key);
       }
     }
   }
@@ -391,7 +367,8 @@
   // Ensure all mandatory goals are assigned.
   for (const auto mandatory_goal_iter : remaining_mandatory_goals) {
     const LayoutGoal* goal = mandatory_goal_iter.second;
-    layout[goal->keycode] = goal->logical_key;
+    fl_keyboard_layout_set_logical_key(self->derived_layout, group,
+                                       goal->keycode, goal->logical_key);
   }
 }
 
@@ -460,7 +437,6 @@
     self->view_delegate = nullptr;
   }
 
-  self->derived_layout.reset();
   self->keycode_to_goals.reset();
   self->logical_to_mandatory_goals.reset();
 
@@ -468,6 +444,7 @@
   g_ptr_array_set_free_func(self->pending_responds, g_object_unref);
   g_ptr_array_free(self->pending_responds, TRUE);
   g_ptr_array_free(self->pending_redispatches, TRUE);
+  g_clear_object(&self->derived_layout);
 
   G_OBJECT_CLASS(fl_keyboard_handler_parent_class)->dispose(object);
 }
@@ -477,7 +454,7 @@
 }
 
 static void fl_keyboard_handler_init(FlKeyboardHandler* self) {
-  self->derived_layout = std::make_unique<DerivedLayout>();
+  self->derived_layout = fl_keyboard_layout_new();
 
   self->keycode_to_goals =
       std::make_unique<std::map<uint16_t, const LayoutGoal*>>();
@@ -529,7 +506,10 @@
                       fl_keyboard_view_delegate_get_messenger(view_delegate))));
 
   fl_keyboard_view_delegate_subscribe_to_layout_change(
-      self->view_delegate, [self]() { self->derived_layout->clear(); });
+      self->view_delegate, [self]() {
+        g_clear_object(&self->derived_layout);
+        self->derived_layout = fl_keyboard_layout_new();
+      });
 
   // Setup the flutter/keyboard channel.
   g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
@@ -561,8 +541,9 @@
       self, fl_keyboard_pending_event_get_sequence_id(pending));
   DispatchToResponderLoopContext data{
       .event = event,
-      .specified_logical_key =
-          get_logical_key_from_layout(event, *self->derived_layout),
+      .specified_logical_key = fl_keyboard_layout_get_logical_key(
+          self->derived_layout, fl_key_event_get_group(event),
+          fl_key_event_get_keycode(event)),
       .user_data = user_data,
   };
   g_ptr_array_foreach(self->responder_list, dispatch_to_responder, &data);
diff --git a/shell/platform/linux/fl_keyboard_layout.cc b/shell/platform/linux/fl_keyboard_layout.cc
new file mode 100644
index 0000000..5ac5d87
--- /dev/null
+++ b/shell/platform/linux/fl_keyboard_layout.cc
@@ -0,0 +1,80 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_keyboard_layout.h"
+
+// The maxiumum keycode in a derived layout.
+//
+// Although X supports higher keycodes, Flutter only cares about standard keys,
+// which are below this.
+constexpr size_t kLayoutSize = 128;
+
+struct _FlKeyboardLayout {
+  GObject parent_instance;
+
+  // Each keycode->logical key mapping per group.
+  GHashTable* groups;
+};
+
+G_DEFINE_TYPE(FlKeyboardLayout, fl_keyboard_layout, G_TYPE_OBJECT)
+
+static void fl_keyboard_layout_dispose(GObject* object) {
+  FlKeyboardLayout* self = FL_KEYBOARD_LAYOUT(object);
+
+  g_clear_pointer(&self->groups, g_hash_table_unref);
+
+  G_OBJECT_CLASS(fl_keyboard_layout_parent_class)->dispose(object);
+}
+
+static void fl_keyboard_layout_class_init(FlKeyboardLayoutClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_keyboard_layout_dispose;
+}
+
+static void fl_keyboard_layout_init(FlKeyboardLayout* self) {
+  self->groups = g_hash_table_new_full(
+      g_direct_hash, g_direct_equal, nullptr,
+      reinterpret_cast<GDestroyNotify>(g_hash_table_unref));
+}
+
+FlKeyboardLayout* fl_keyboard_layout_new() {
+  return FL_KEYBOARD_LAYOUT(
+      g_object_new(fl_keyboard_layout_get_type(), nullptr));
+}
+
+gboolean fl_keyboard_layout_has_group(FlKeyboardLayout* self, uint8_t group) {
+  return g_hash_table_lookup(self->groups, GINT_TO_POINTER(group)) != nullptr;
+}
+
+void fl_keyboard_layout_set_logical_key(FlKeyboardLayout* self,
+                                        uint8_t group,
+                                        uint16_t keycode,
+                                        uint64_t logical_key) {
+  GHashTable* group_layout = static_cast<GHashTable*>(
+      g_hash_table_lookup(self->groups, GINT_TO_POINTER(group)));
+  if (group_layout == nullptr) {
+    group_layout =
+        g_hash_table_new_full(g_direct_hash, g_direct_equal, nullptr, nullptr);
+    g_hash_table_insert(self->groups, GINT_TO_POINTER(group), group_layout);
+  }
+
+  g_hash_table_insert(group_layout, GINT_TO_POINTER(keycode),
+                      GINT_TO_POINTER(logical_key));
+}
+
+uint64_t fl_keyboard_layout_get_logical_key(FlKeyboardLayout* self,
+                                            uint8_t group,
+                                            uint16_t keycode) {
+  if (keycode >= kLayoutSize) {
+    return 0;
+  }
+
+  GHashTable* group_layout = static_cast<GHashTable*>(
+      g_hash_table_lookup(self->groups, GINT_TO_POINTER(group)));
+  if (group_layout == nullptr) {
+    return 0;
+  }
+
+  return GPOINTER_TO_INT(
+      g_hash_table_lookup(group_layout, GINT_TO_POINTER(keycode)));
+}
diff --git a/shell/platform/linux/fl_keyboard_layout.h b/shell/platform/linux/fl_keyboard_layout.h
new file mode 100644
index 0000000..1338ab5
--- /dev/null
+++ b/shell/platform/linux/fl_keyboard_layout.h
@@ -0,0 +1,73 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_LINUX_FL_KEYBOARD_LAYOUT_H_
+#define FLUTTER_SHELL_PLATFORM_LINUX_FL_KEYBOARD_LAYOUT_H_
+
+#include <glib-object.h>
+#include <stdint.h>
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE(FlKeyboardLayout,
+                     fl_keyboard_layout,
+                     FL,
+                     KEYBOARD_LAYOUT,
+                     GObject);
+
+/**
+ * FlKeyboardLayout:
+ * Tracks keycode to to logical key mappings for #FlKeyboardHandler
+ */
+
+/**
+ * fl_keyboard_layout_new:
+ *
+ * Create a new #FlKeyboardLayout.
+ *
+ * Returns: a new #FlKeyboardLayout.
+ */
+FlKeyboardLayout* fl_keyboard_layout_new();
+
+/**
+ * fl_keyboard_layout_has_group:
+ * @layout: a #FlKeyboardLayout.
+ * @group: a key group.
+ *
+ * Checks if a group is present in this layout.
+ *
+ * Returns: %TRUE if this group is present.
+ */
+gboolean fl_keyboard_layout_has_group(FlKeyboardLayout* layout, uint8_t group);
+
+/**
+ * fl_keyboard_layout_has_group:
+ * @layout: a #FlKeyboardLayout.
+ * @group: a key group.
+ * @logical_key: a logical keycode.
+ *
+ * Sets the logical key for a given group and keycode.
+ */
+void fl_keyboard_layout_set_logical_key(FlKeyboardLayout* layout,
+                                        uint8_t group,
+                                        uint16_t keycode,
+                                        uint64_t logical_key);
+
+/**
+ * fl_keyboard_layout_get_logical_key:
+ * @layout: a #FlKeyboardLayout.
+ * @group: a key group.
+ * @keycode: a keycode.
+ *
+ * Gets the logical key for the given group and keycode.
+ *
+ * Returns: the logical keycode or 0 if not set.
+ */
+uint64_t fl_keyboard_layout_get_logical_key(FlKeyboardLayout* layout,
+                                            uint8_t group,
+                                            uint16_t keycode);
+
+G_END_DECLS
+
+#endif  // FLUTTER_SHELL_PLATFORM_LINUX_FL_KEYBOARD_LAYOUT_H_
diff --git a/shell/platform/linux/fl_keyboard_layout_test.cc b/shell/platform/linux/fl_keyboard_layout_test.cc
new file mode 100644
index 0000000..0d6680ce
--- /dev/null
+++ b/shell/platform/linux/fl_keyboard_layout_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/shell/platform/linux/fl_keyboard_layout.h"
+
+#include "gtest/gtest.h"
+
+TEST(FlKeyboardLayoutTest, SetLogicalKey) {
+  g_autoptr(FlKeyboardLayout) layout = fl_keyboard_layout_new();
+
+  EXPECT_EQ(fl_keyboard_layout_get_logical_key(layout, 0, 42),
+            static_cast<uint64_t>(0));
+
+  fl_keyboard_layout_set_logical_key(layout, 0, 42, 1234);
+
+  EXPECT_EQ(fl_keyboard_layout_get_logical_key(layout, 0, 42),
+            static_cast<uint64_t>(1234));
+}
+
+TEST(FlKeyboardLayoutTest, MaxValues) {
+  g_autoptr(FlKeyboardLayout) layout = fl_keyboard_layout_new();
+
+  EXPECT_EQ(fl_keyboard_layout_get_logical_key(layout, 255, 127),
+            static_cast<uint64_t>(0));
+
+  fl_keyboard_layout_set_logical_key(layout, 255, 127, 12345678);
+
+  EXPECT_EQ(fl_keyboard_layout_get_logical_key(layout, 255, 127),
+            static_cast<uint64_t>(12345678));
+}
+
+TEST(FlKeyboardLayoutTest, HasGroup) {
+  g_autoptr(FlKeyboardLayout) layout = fl_keyboard_layout_new();
+
+  EXPECT_FALSE(fl_keyboard_layout_has_group(layout, 42));
+
+  fl_keyboard_layout_set_logical_key(layout, 42, 11, 22);
+
+  EXPECT_TRUE(fl_keyboard_layout_has_group(layout, 42));
+}