Move some common widgets and utilities to `devtools_app_shared` (#7979)

diff --git a/packages/devtools_app/lib/src/framework/home_screen.dart b/packages/devtools_app/lib/src/framework/home_screen.dart
index 32ddd14..229a738 100644
--- a/packages/devtools_app/lib/src/framework/home_screen.dart
+++ b/packages/devtools_app/lib/src/framework/home_screen.dart
@@ -14,7 +14,6 @@
 
 import '../shared/analytics/analytics.dart' as ga;
 import '../shared/analytics/constants.dart' as gac;
-import '../shared/common_widgets.dart';
 import '../shared/config_specific/import_export/import_export.dart';
 import '../shared/connection_info.dart';
 import '../shared/globals.dart';
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/root_selector.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/root_selector.dart
index 6639975..510c966 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/root_selector.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/project_root_selection/root_selector.dart
@@ -6,7 +6,6 @@
 import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
 
-import '../../../shared/common_widgets.dart';
 import '../../../shared/primitives/utils.dart';
 import '../../../shared/ui/utils.dart';
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/interval_dropdown.dart b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/interval_dropdown.dart
index dab307a..76bc517 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/interval_dropdown.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/chart/widgets/interval_dropdown.dart
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
 import '../../../../../shared/analytics/analytics.dart' as ga;
 import '../../../../../shared/analytics/constants.dart' as gac;
-import '../../../../../shared/common_widgets.dart';
 import '../controller/chart_pane_controller.dart';
 import '../data/primitives.dart';
 
diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
index 204b22e..631d249 100644
--- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
+++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart
@@ -7,7 +7,6 @@
 
 import '../../../../../shared/analytics/analytics.dart' as ga;
 import '../../../../../shared/analytics/constants.dart' as gac;
-import '../../../../../shared/common_widgets.dart';
 import '../../../../../shared/globals.dart';
 import '../../../../../shared/memory/classes.dart';
 import '../../../../../shared/primitives/utils.dart';
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart b/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
index 318c43e..4eb3d42 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/controls/cpu_profiler_controls.dart
@@ -7,7 +7,6 @@
 import 'package:devtools_app_shared/ui.dart';
 import 'package:flutter/material.dart';
 
-import '../../../../shared/common_widgets.dart';
 import '../../../../shared/globals.dart';
 import '../../../../shared/ui/filter.dart';
 import '../../cpu_profile_model.dart';
diff --git a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
index 8e03553..07f9c77 100644
--- a/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
+++ b/packages/devtools_app/lib/src/screens/vm_developer/object_inspector/object_inspector_view.dart
@@ -9,7 +9,6 @@
 import 'package:provider/provider.dart';
 
 import '../../../shared/analytics/constants.dart' as gac;
-import '../../../shared/common_widgets.dart';
 import '../../../shared/ui/drop_down_button.dart';
 import '../../debugger/program_explorer.dart';
 import '../../debugger/program_explorer_model.dart';
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart
index 9b76f3c..bd79b49 100644
--- a/packages/devtools_app/lib/src/shared/common_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -14,7 +14,6 @@
 import 'package:vm_service/vm_service.dart';
 
 import '../screens/debugger/debugger_controller.dart';
-import '../screens/inspector/layout_explorer/ui/theme.dart';
 import 'analytics/analytics.dart' as ga;
 import 'analytics/constants.dart' as gac;
 import 'config_specific/copy_to_clipboard/copy_to_clipboard.dart';
@@ -726,233 +725,6 @@
   }
 }
 
-class RoundedCornerOptions {
-  const RoundedCornerOptions({
-    this.showTopLeft = true,
-    this.showTopRight = true,
-    this.showBottomLeft = true,
-    this.showBottomRight = true,
-  });
-
-  /// Static constant instance with all borders hidden
-  static const empty = RoundedCornerOptions(
-    showTopLeft: false,
-    showTopRight: false,
-    showBottomLeft: false,
-    showBottomRight: false,
-  );
-
-  final bool showTopLeft;
-  final bool showTopRight;
-  final bool showBottomLeft;
-  final bool showBottomRight;
-}
-
-class RoundedDropDownButton<T> extends StatelessWidget {
-  const RoundedDropDownButton({
-    super.key,
-    this.value,
-    this.onChanged,
-    this.isDense = false,
-    this.isExpanded = false,
-    this.style,
-    this.selectedItemBuilder,
-    this.items,
-    this.roundedCornerOptions,
-  });
-
-  final T? value;
-
-  final ValueChanged<T?>? onChanged;
-
-  final bool isDense;
-
-  final bool isExpanded;
-
-  final TextStyle? style;
-
-  final DropdownButtonBuilder? selectedItemBuilder;
-
-  final List<DropdownMenuItem<T>>? items;
-
-  final RoundedCornerOptions? roundedCornerOptions;
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    final bgColor = theme.colorScheme.backgroundColorSelected;
-
-    Radius selectRadius(bool show) {
-      return show ? defaultRadius : Radius.zero;
-    }
-
-    final style = this.style ?? theme.regularTextStyle;
-    final showTopLeft = roundedCornerOptions?.showTopLeft ?? true;
-    final showTopRight = roundedCornerOptions?.showTopRight ?? true;
-    final showBottomLeft = roundedCornerOptions?.showBottomLeft ?? true;
-    final showBottomRight = roundedCornerOptions?.showBottomRight ?? true;
-
-    final button = Center(
-      child: SizedBox(
-        height: defaultButtonHeight - 2.0, // subtract 2.0 for width of border
-        child: DropdownButtonHideUnderline(
-          child: DropdownButton<T>(
-            padding: const EdgeInsets.only(
-              left: defaultSpacing,
-              right: borderPadding,
-            ),
-            value: value,
-            onChanged: onChanged,
-            isDense: isDense,
-            isExpanded: isExpanded,
-            borderRadius: BorderRadius.only(
-              topLeft: selectRadius(showTopLeft),
-              topRight: selectRadius(showTopRight),
-              bottomLeft: selectRadius(showBottomLeft),
-              bottomRight: selectRadius(showBottomRight),
-            ),
-            style: style,
-            selectedItemBuilder: selectedItemBuilder,
-            items: items,
-            focusColor: bgColor,
-          ),
-        ),
-      ),
-    );
-
-    if (roundedCornerOptions == RoundedCornerOptions.empty) return button;
-
-    return RoundedOutlinedBorder(
-      showTopLeft: showTopLeft,
-      showTopRight: showTopRight,
-      showBottomLeft: showBottomLeft,
-      showBottomRight: showBottomRight,
-      child: button,
-    );
-  }
-}
-
-class DevToolsClearableTextField extends StatelessWidget {
-  DevToolsClearableTextField({
-    super.key,
-    required this.labelText,
-    TextEditingController? controller,
-    this.hintText,
-    this.prefixIcon,
-    this.additionalSuffixActions = const <Widget>[],
-    this.onChanged,
-    this.onSubmitted,
-    this.autofocus = false,
-    this.enabled,
-    this.roundedBorder = false,
-  }) : controller = controller ?? TextEditingController();
-
-  final TextEditingController controller;
-  final String? hintText;
-  final Widget? prefixIcon;
-  final List<Widget> additionalSuffixActions;
-  final String labelText;
-  final void Function(String)? onChanged;
-  final void Function(String)? onSubmitted;
-  final bool autofocus;
-  final bool? enabled;
-  final bool roundedBorder;
-
-  static const _contentVerticalPadding = 6.0;
-
-  /// This is the default border radius used by the [OutlineInputBorder]
-  /// constructor.
-  static const _defaultInputBorderRadius =
-      BorderRadius.all(Radius.circular(4.0));
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    return SizedBox(
-      height: defaultTextFieldHeight,
-      child: TextField(
-        autofocus: autofocus,
-        controller: controller,
-        enabled: enabled,
-        onChanged: onChanged,
-        onSubmitted: onSubmitted,
-        style: theme.regularTextStyle,
-        decoration: InputDecoration(
-          isDense: true,
-          contentPadding: const EdgeInsets.only(
-            top: _contentVerticalPadding,
-            bottom: _contentVerticalPadding,
-            left: denseSpacing,
-            right: densePadding,
-          ),
-          constraints: BoxConstraints(
-            minHeight: defaultTextFieldHeight,
-            maxHeight: defaultTextFieldHeight,
-          ),
-          border: OutlineInputBorder(
-            borderRadius: roundedBorder
-                ? const BorderRadius.all(defaultRadius)
-                : _defaultInputBorderRadius,
-          ),
-          labelText: labelText,
-          labelStyle: theme.subtleTextStyle,
-          hintText: hintText,
-          hintStyle: theme.subtleTextStyle,
-          prefixIcon: prefixIcon,
-          suffix: SizedBox(
-            height: inputDecorationElementHeight,
-            child: Row(
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                clearInputButton(
-                  () {
-                    controller.clear();
-                    onChanged?.call('');
-                  },
-                ),
-                ...additionalSuffixActions,
-              ],
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-}
-
-Widget clearInputButton(VoidCallback onPressed) {
-  return inputDecorationSuffixButton(
-    icon: Icons.clear,
-    onPressed: onPressed,
-    tooltip: 'Clear',
-  );
-}
-
-Widget closeSearchDropdownButton(VoidCallback? onPressed) {
-  return inputDecorationSuffixButton(icon: Icons.close, onPressed: onPressed);
-}
-
-Widget inputDecorationSuffixButton({
-  required IconData icon,
-  required VoidCallback? onPressed,
-  String? tooltip,
-}) {
-  return maybeWrapWithTooltip(
-    tooltip: tooltip,
-    child: SizedBox(
-      height: inputDecorationElementHeight,
-      width: inputDecorationElementHeight + denseSpacing,
-      child: IconButton(
-        padding: EdgeInsets.zero,
-        onPressed: onPressed,
-        iconSize: defaultIconSize,
-        splashRadius: defaultIconSize,
-        icon: Icon(icon),
-      ),
-    ),
-  );
-}
-
 class OutlinedRowGroup extends StatelessWidget {
   const OutlinedRowGroup({super.key, required this.children, this.borderColor});
 
diff --git a/packages/devtools_app/lib/src/shared/file_import.dart b/packages/devtools_app/lib/src/shared/file_import.dart
index cb8510c..36a4ad1 100644
--- a/packages/devtools_app/lib/src/shared/file_import.dart
+++ b/packages/devtools_app/lib/src/shared/file_import.dart
@@ -185,7 +185,8 @@
             textAlign: TextAlign.left,
           ),
         ),
-        if (importedFile != null) clearInputButton(_clearFile),
+        if (importedFile != null)
+          InputDecorationSuffixButton.clear(onPressed: _clearFile),
       ],
     );
   }
diff --git a/packages/devtools_app/lib/src/shared/primitives/utils.dart b/packages/devtools_app/lib/src/shared/primitives/utils.dart
index c6272b6..b5e453c 100644
--- a/packages/devtools_app/lib/src/shared/primitives/utils.dart
+++ b/packages/devtools_app/lib/src/shared/primitives/utils.dart
@@ -950,32 +950,6 @@
     );
   }
 
-  /// Whether [query] is a case insensitive "fuzzy match" for this String.
-  ///
-  /// For example, the query "hwf" would be a fuzzy match for the String
-  /// "hello_world_file".
-  bool caseInsensitiveFuzzyMatch(String query) {
-    query = query.toLowerCase();
-    final lowercase = toLowerCase();
-    final it = query.characters.iterator;
-    var strIndex = 0;
-    while (it.moveNext()) {
-      final char = it.current;
-      var foundChar = false;
-      for (int i = strIndex; i < lowercase.length; i++) {
-        if (lowercase[i] == char) {
-          strIndex = i + 1;
-          foundChar = true;
-          break;
-        }
-      }
-      if (!foundChar) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   /// Whether [other] is a case insensitive match for this String.
   ///
   /// If [pattern] is a [RegExp], this method will return true if and only if
diff --git a/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart b/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
index 4b492cd..044b23b 100644
--- a/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
+++ b/packages/devtools_app/lib/src/shared/ui/drop_down_button.dart
@@ -7,7 +7,6 @@
 import 'package:flutter/material.dart';
 
 import '../analytics/analytics.dart' as ga;
-import '../common_widgets.dart';
 
 /// A [DropDownButton] implementation that reports selection changes to our
 /// analytics.
diff --git a/packages/devtools_app/lib/src/shared/ui/search.dart b/packages/devtools_app/lib/src/shared/ui/search.dart
index bce8588..508eb0c 100644
--- a/packages/devtools_app/lib/src/shared/ui/search.dart
+++ b/packages/devtools_app/lib/src/shared/ui/search.dart
@@ -1352,7 +1352,7 @@
     assert(supportsNavigation || onClose != null);
     return supportsNavigation
         ? SearchNavigationControls(controller, onClose: onClose)
-        : closeSearchDropdownButton(onClose);
+        : InputDecorationSuffixButton.close(onPressed: onClose);
   }
 }
 
@@ -1403,15 +1403,16 @@
                     child: PaddedDivider.vertical(),
                   ),
                 ),
-                inputDecorationSuffixButton(
+                InputDecorationSuffixButton(
                   icon: Icons.keyboard_arrow_up,
                   onPressed: numMatches > 1 ? controller.previousMatch : null,
                 ),
-                inputDecorationSuffixButton(
+                InputDecorationSuffixButton(
                   icon: Icons.keyboard_arrow_down,
                   onPressed: numMatches > 1 ? controller.nextMatch : null,
                 ),
-                if (onClose != null) closeSearchDropdownButton(onClose),
+                if (onClose != null)
+                  InputDecorationSuffixButton.close(onPressed: onClose),
               ],
             );
           },
diff --git a/packages/devtools_app/test/memory/tracing/tracing_view_test.dart b/packages/devtools_app/test/memory/tracing/tracing_view_test.dart
index 5fc7286..7222328 100644
--- a/packages/devtools_app/test/memory/tracing/tracing_view_test.dart
+++ b/packages/devtools_app/test/memory/tracing/tracing_view_test.dart
@@ -9,6 +9,7 @@
 import 'package:devtools_app/src/screens/memory/framework/memory_tabs.dart';
 import 'package:devtools_app/src/screens/memory/panes/tracing/tracing_pane_controller.dart';
 import 'package:devtools_app/src/screens/memory/panes/tracing/tracing_tree.dart';
+import 'package:devtools_app_shared/ui.dart';
 import 'package:devtools_app_shared/utils.dart';
 import 'package:devtools_test/devtools_test.dart';
 import 'package:devtools_test/helpers.dart';
diff --git a/packages/devtools_app/test/shared/primitives/utils_test.dart b/packages/devtools_app/test/shared/primitives/utils_test.dart
index 6daea5c..2b5fe47 100644
--- a/packages/devtools_app/test/shared/primitives/utils_test.dart
+++ b/packages/devtools_app/test/shared/primitives/utils_test.dart
@@ -1144,22 +1144,6 @@
     });
 
     group('StringExtension', () {
-      test('fuzzyMatch', () {
-        const str = 'hello_world_file';
-        expect(str.caseInsensitiveFuzzyMatch('h'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('o_'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('hw'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('hwf'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('_e'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('HWF'), isTrue);
-        expect(str.caseInsensitiveFuzzyMatch('_E'), isTrue);
-
-        expect(str.caseInsensitiveFuzzyMatch('hwfh'), isFalse);
-        expect(str.caseInsensitiveFuzzyMatch('hfw'), isFalse);
-        expect(str.caseInsensitiveFuzzyMatch('gello'), isFalse);
-        expect(str.caseInsensitiveFuzzyMatch('files'), isFalse);
-      });
-
       test('caseInsensitiveContains', () {
         const str = 'This is a test string with a path/to/uri';
         expect(str.caseInsensitiveContains('test'), isTrue);
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index fa87159..cc1c775 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -1,4 +1,9 @@
-## 0.2.0
+## 0.2.2-wip
+* Add `caseInsensitiveFuzzyMatch` extension method on `String`.
+* Add common widgets `DevToolsClearableTextField`, `InputDecorationSuffixButton`,
+and `RoundedDropDownButton`.
+
+## 0.2.1
 * Add `navigateToCode` utility method for jumping to code in IDEs.
 * Add `FlutterEvent` and `DeveloperServiceEvent` constants.
 * Add `connectedAppPackageRoot`, `rootPackageDirectoryForMainIsolate`, and
@@ -21,11 +26,11 @@
 URLs when in an embedded VS Code view.
 
 ## 0.1.1
-* Update `package:dtd` to `^2.1.0`
+* Update `package:dtd` to `^2.1.0`.
 * Add `DTDManager.projectRoots` method.
 * Bump the minimum Dart and Flutter SDK versions to `3.4.0-282.1.beta` and
 `3.22.0-0.1.pre` respectively.
-* Bump `devtools_shared` to ^8.1.1-dev.0
+* Bump `devtools_shared` to ^8.1.1-dev.0.
 
 ## 0.1.0
 * Remove deprecated `background` and `onBackground` values for `lightColorScheme`
diff --git a/packages/devtools_app_shared/lib/src/ui/buttons.dart b/packages/devtools_app_shared/lib/src/ui/buttons.dart
new file mode 100644
index 0000000..12e3072
--- /dev/null
+++ b/packages/devtools_app_shared/lib/src/ui/buttons.dart
@@ -0,0 +1,475 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'common.dart';
+import 'theme/theme.dart';
+
+/// A button with default DevTools styling and analytics handling.
+///
+/// * `onPressed`: The callback to be called upon pressing the button.
+/// * `minScreenWidthForTextBeforeScaling`: The minimum width the button can be before the text is
+///    omitted.
+class DevToolsButton extends StatelessWidget {
+  const DevToolsButton({
+    super.key,
+    required this.onPressed,
+    this.icon,
+    this.label,
+    this.tooltip,
+    this.color,
+    this.minScreenWidthForTextBeforeScaling,
+    this.elevated = false,
+    this.outlined = true,
+    this.tooltipPadding,
+  }) : assert(
+          label != null || icon != null,
+          'Either icon or label must be specified.',
+        );
+
+  factory DevToolsButton.iconOnly({
+    required IconData icon,
+    String? tooltip,
+    VoidCallback? onPressed,
+    bool outlined = true,
+  }) {
+    return DevToolsButton(
+      icon: icon,
+      outlined: outlined,
+      tooltip: tooltip,
+      onPressed: onPressed,
+    );
+  }
+
+  final IconData? icon;
+
+  final String? label;
+
+  final String? tooltip;
+
+  final Color? color;
+
+  final VoidCallback? onPressed;
+
+  final double? minScreenWidthForTextBeforeScaling;
+
+  /// Whether this icon label button should use an elevated button style.
+  final bool elevated;
+
+  /// Whether this icon label button should use an outlined button style.
+  final bool outlined;
+
+  final EdgeInsetsGeometry? tooltipPadding;
+
+  @override
+  Widget build(BuildContext context) {
+    var tooltip = this.tooltip;
+
+    if (label == null) {
+      return SizedBox(
+        // This is required to force the button size.
+        height: defaultButtonHeight,
+        width: defaultButtonHeight,
+        child: maybeWrapWithTooltip(
+          tooltip: tooltip,
+          tooltipPadding: tooltipPadding,
+          child: outlined
+              ? IconButton.outlined(
+                  onPressed: onPressed,
+                  iconSize: defaultIconSize,
+                  icon: Icon(icon),
+                )
+              : IconButton(
+                  onPressed: onPressed,
+                  iconSize: defaultIconSize,
+                  icon: Icon(icon),
+                ),
+        ),
+      );
+    }
+    final colorScheme = Theme.of(context).colorScheme;
+    var textColor = color;
+    if (textColor == null && elevated) {
+      textColor =
+          onPressed == null ? colorScheme.onSurface : colorScheme.onPrimary;
+    }
+    final iconLabel = MaterialIconLabel(
+      label: label!,
+      iconData: icon,
+      minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
+      color: textColor,
+    );
+
+    // If we hid the label due to a small screen width and the button does not
+    // have a tooltip, use the label as a tooltip.
+    final labelHidden =
+        !isScreenWiderThan(context, minScreenWidthForTextBeforeScaling);
+    if (labelHidden && tooltip == null) {
+      tooltip = label;
+    }
+
+    if (elevated) {
+      return SizedBox(
+        // This is required to force the button size.
+        height: defaultButtonHeight,
+        child: maybeWrapWithTooltip(
+          tooltip: tooltip,
+          tooltipPadding: tooltipPadding,
+          child: ElevatedButton(
+            onPressed: onPressed,
+            child: iconLabel,
+          ),
+        ),
+      );
+    }
+    // TODO(kenz): this SizedBox wrapper should be unnecessary once
+    // https://github.com/flutter/flutter/issues/79894 is fixed.
+    return maybeWrapWithTooltip(
+      tooltip: tooltip,
+      tooltipPadding: tooltipPadding,
+      child: SizedBox(
+        height: defaultButtonHeight,
+        width: !isScreenWiderThan(context, minScreenWidthForTextBeforeScaling)
+            ? buttonMinWidth
+            : null,
+        child: outlined
+            ? OutlinedButton(
+                style: denseAwareOutlinedButtonStyle(
+                  context,
+                  minScreenWidthForTextBeforeScaling,
+                ),
+                onPressed: onPressed,
+                child: iconLabel,
+              )
+            : TextButton(
+                onPressed: onPressed,
+                style: denseAwareTextButtonStyle(
+                  context,
+                  minScreenWidthForTextBeforeScaling:
+                      minScreenWidthForTextBeforeScaling,
+                ),
+                child: iconLabel,
+              ),
+      ),
+    );
+  }
+}
+
+final class DevToolsToggleButtonGroup extends StatelessWidget {
+  const DevToolsToggleButtonGroup({
+    super.key,
+    required this.children,
+    required this.selectedStates,
+    required this.onPressed,
+    this.fillColor,
+    this.selectedColor,
+    this.borderColor,
+  });
+
+  final List<Widget> children;
+
+  final List<bool> selectedStates;
+
+  final void Function(int)? onPressed;
+
+  final Color? fillColor;
+
+  final Color? selectedColor;
+
+  final Color? borderColor;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return SizedBox(
+      height: defaultButtonHeight,
+      child: ToggleButtons(
+        borderRadius: defaultBorderRadius,
+        fillColor: fillColor,
+        selectedColor: selectedColor,
+        borderColor: borderColor,
+        textStyle: theme.textTheme.bodyMedium,
+        constraints: BoxConstraints(
+          minWidth: defaultButtonHeight,
+          minHeight: defaultButtonHeight,
+        ),
+        isSelected: selectedStates,
+        onPressed: onPressed,
+        children: children,
+      ),
+    );
+  }
+}
+
+final class DevToolsToggleButton extends StatelessWidget {
+  const DevToolsToggleButton({
+    super.key,
+    required this.onPressed,
+    required this.isSelected,
+    required this.message,
+    required this.icon,
+    this.outlined = true,
+    this.label,
+    this.shape,
+  });
+
+  final String message;
+
+  final VoidCallback onPressed;
+
+  final bool isSelected;
+
+  final IconData icon;
+
+  final String? label;
+
+  final OutlinedBorder? shape;
+
+  final bool outlined;
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsToggleButtonGroup(
+      borderColor: outlined || isSelected
+          ? Theme.of(context).focusColor
+          : Colors.transparent,
+      selectedStates: [isSelected],
+      onPressed: (_) => onPressed(),
+      children: [
+        DevToolsTooltip(
+          message: message,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
+            child: MaterialIconLabel(
+              iconData: icon,
+              label: label,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+/// A group of buttons that share a common border.
+///
+/// This widget ensures the buttons are displayed with proper borders on the
+/// interior and exterior of the group. The attirbutes for each button can be
+/// defined by [ButtonGroupItemData] and included in [items].
+final class RoundedButtonGroup extends StatelessWidget {
+  const RoundedButtonGroup({
+    super.key,
+    required this.items,
+    this.minScreenWidthForTextBeforeScaling,
+  });
+
+  final List<ButtonGroupItemData> items;
+  final double? minScreenWidthForTextBeforeScaling;
+
+  @override
+  Widget build(BuildContext context) {
+    Widget buildButton(int index) {
+      final itemData = items[index];
+      Widget button = _ButtonGroupButton(
+        buttonData: itemData,
+        roundedLeftBorder: index == 0,
+        roundedRightBorder: index == items.length - 1,
+        minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
+      );
+      if (index != 0) {
+        button = Container(
+          decoration: BoxDecoration(
+            border: Border(
+              left: BorderSide(
+                color: Theme.of(context).focusColor,
+              ),
+            ),
+          ),
+          child: button,
+        );
+      }
+      return button;
+    }
+
+    return SizedBox(
+      height: defaultButtonHeight,
+      child: RoundedOutlinedBorder(
+        child: Row(
+          children: [
+            for (int i = 0; i < items.length; i++) buildButton(i),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+final class _ButtonGroupButton extends StatelessWidget {
+  const _ButtonGroupButton({
+    required this.buttonData,
+    this.roundedLeftBorder = false,
+    this.roundedRightBorder = false,
+    this.minScreenWidthForTextBeforeScaling,
+  });
+
+  final ButtonGroupItemData buttonData;
+  final bool roundedLeftBorder;
+  final bool roundedRightBorder;
+  final double? minScreenWidthForTextBeforeScaling;
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsTooltip(
+      message: buttonData.tooltip,
+      child: OutlinedButton(
+        autofocus: buttonData.autofocus,
+        style: OutlinedButton.styleFrom(
+          padding: const EdgeInsets.symmetric(horizontal: densePadding),
+          side: BorderSide.none,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.horizontal(
+              left: roundedLeftBorder ? defaultRadius : Radius.zero,
+              right: roundedRightBorder ? defaultRadius : Radius.zero,
+            ),
+          ),
+        ),
+        onPressed: buttonData.onPressed,
+        child: MaterialIconLabel(
+          label: buttonData.label,
+          iconData: buttonData.icon,
+          minScreenWidthForTextBeforeScaling:
+              minScreenWidthForTextBeforeScaling,
+        ),
+      ),
+    );
+  }
+}
+
+final class ButtonGroupItemData {
+  const ButtonGroupItemData({
+    this.label,
+    this.icon,
+    String? tooltip,
+    this.onPressed,
+    this.autofocus = false,
+  })  : tooltip = tooltip ?? label,
+        assert(label != null || icon != null);
+
+  final String? label;
+  final IconData? icon;
+  final String? tooltip;
+  final VoidCallback? onPressed;
+  final bool autofocus;
+}
+
+final class DevToolsFilterButton extends StatelessWidget {
+  const DevToolsFilterButton({
+    super.key,
+    required this.onPressed,
+    required this.isFilterActive,
+    this.message = 'Filter',
+    this.outlined = true,
+  });
+
+  final VoidCallback onPressed;
+  final bool isFilterActive;
+  final String message;
+  final bool outlined;
+
+  @override
+  Widget build(BuildContext context) {
+    return DevToolsToggleButton(
+      onPressed: onPressed,
+      isSelected: isFilterActive,
+      message: message,
+      icon: Icons.filter_list,
+      outlined: outlined,
+    );
+  }
+}
+
+/// A DevTools-styled dropdown button.
+final class RoundedDropDownButton<T> extends StatelessWidget {
+  const RoundedDropDownButton({
+    super.key,
+    this.value,
+    this.onChanged,
+    this.isDense = false,
+    this.isExpanded = false,
+    this.style,
+    this.selectedItemBuilder,
+    this.items,
+    this.roundedCornerOptions,
+  });
+
+  final T? value;
+
+  final ValueChanged<T?>? onChanged;
+
+  final bool isDense;
+
+  final bool isExpanded;
+
+  final TextStyle? style;
+
+  final DropdownButtonBuilder? selectedItemBuilder;
+
+  final List<DropdownMenuItem<T>>? items;
+
+  final RoundedCornerOptions? roundedCornerOptions;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+
+    Radius selectRadius(bool show) {
+      return show ? defaultRadius : Radius.zero;
+    }
+
+    final style = this.style ?? theme.regularTextStyle;
+    final showTopLeft = roundedCornerOptions?.showTopLeft ?? true;
+    final showTopRight = roundedCornerOptions?.showTopRight ?? true;
+    final showBottomLeft = roundedCornerOptions?.showBottomLeft ?? true;
+    final showBottomRight = roundedCornerOptions?.showBottomRight ?? true;
+
+    final button = Center(
+      child: SizedBox(
+        height: defaultButtonHeight - 2.0, // subtract 2.0 for width of border
+        child: DropdownButtonHideUnderline(
+          child: DropdownButton<T>(
+            padding: const EdgeInsets.only(
+              left: defaultSpacing,
+              right: borderPadding,
+            ),
+            value: value,
+            onChanged: onChanged,
+            isDense: isDense,
+            isExpanded: isExpanded,
+            borderRadius: BorderRadius.only(
+              topLeft: selectRadius(showTopLeft),
+              topRight: selectRadius(showTopRight),
+              bottomLeft: selectRadius(showBottomLeft),
+              bottomRight: selectRadius(showBottomRight),
+            ),
+            style: style,
+            selectedItemBuilder: selectedItemBuilder,
+            items: items,
+            focusColor: theme.colorScheme.surface,
+          ),
+        ),
+      ),
+    );
+
+    if (roundedCornerOptions == RoundedCornerOptions.empty) return button;
+
+    return RoundedOutlinedBorder(
+      showTopLeft: showTopLeft,
+      showTopRight: showTopRight,
+      showBottomLeft: showBottomLeft,
+      showBottomRight: showBottomRight,
+      child: button,
+    );
+  }
+}
diff --git a/packages/devtools_app_shared/lib/src/ui/common.dart b/packages/devtools_app_shared/lib/src/ui/common.dart
index 3306447..77db9c2 100644
--- a/packages/devtools_app_shared/lib/src/ui/common.dart
+++ b/packages/devtools_app_shared/lib/src/ui/common.dart
@@ -312,158 +312,8 @@
   }
 }
 
-/// A button with default DevTools styling and analytics handling.
-///
-/// * `onPressed`: The callback to be called upon pressing the button.
-/// * `minScreenWidthForTextBeforeScaling`: The minimum width the button can be before the text is
-///    omitted.
-class DevToolsButton extends StatelessWidget {
-  const DevToolsButton({
-    super.key,
-    required this.onPressed,
-    this.icon,
-    this.label,
-    this.tooltip,
-    this.color,
-    this.minScreenWidthForTextBeforeScaling,
-    this.elevated = false,
-    this.outlined = true,
-    this.tooltipPadding,
-  }) : assert(
-          label != null || icon != null,
-          'Either icon or label must be specified.',
-        );
-
-  factory DevToolsButton.iconOnly({
-    required IconData icon,
-    String? tooltip,
-    VoidCallback? onPressed,
-    bool outlined = true,
-  }) {
-    return DevToolsButton(
-      icon: icon,
-      outlined: outlined,
-      tooltip: tooltip,
-      onPressed: onPressed,
-    );
-  }
-
-  final IconData? icon;
-
-  final String? label;
-
-  final String? tooltip;
-
-  final Color? color;
-
-  final VoidCallback? onPressed;
-
-  final double? minScreenWidthForTextBeforeScaling;
-
-  /// Whether this icon label button should use an elevated button style.
-  final bool elevated;
-
-  /// Whether this icon label button should use an outlined button style.
-  final bool outlined;
-
-  final EdgeInsetsGeometry? tooltipPadding;
-
-  @override
-  Widget build(BuildContext context) {
-    var tooltip = this.tooltip;
-
-    if (label == null) {
-      return SizedBox(
-        // This is required to force the button size.
-        height: defaultButtonHeight,
-        width: defaultButtonHeight,
-        child: maybeWrapWithTooltip(
-          tooltip: tooltip,
-          tooltipPadding: tooltipPadding,
-          child: outlined
-              ? IconButton.outlined(
-                  onPressed: onPressed,
-                  iconSize: defaultIconSize,
-                  icon: Icon(icon),
-                )
-              : IconButton(
-                  onPressed: onPressed,
-                  iconSize: defaultIconSize,
-                  icon: Icon(icon),
-                ),
-        ),
-      );
-    }
-    final colorScheme = Theme.of(context).colorScheme;
-    var textColor = color;
-    if (textColor == null && elevated) {
-      textColor =
-          onPressed == null ? colorScheme.onSurface : colorScheme.onPrimary;
-    }
-    final iconLabel = MaterialIconLabel(
-      label: label!,
-      iconData: icon,
-      minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
-      color: textColor,
-    );
-
-    // If we hid the label due to a small screen width and the button does not
-    // have a tooltip, use the label as a tooltip.
-    final labelHidden =
-        !isScreenWiderThan(context, minScreenWidthForTextBeforeScaling);
-    if (labelHidden && tooltip == null) {
-      tooltip = label;
-    }
-
-    if (elevated) {
-      return SizedBox(
-        // This is required to force the button size.
-        height: defaultButtonHeight,
-        child: maybeWrapWithTooltip(
-          tooltip: tooltip,
-          tooltipPadding: tooltipPadding,
-          child: ElevatedButton(
-            onPressed: onPressed,
-            child: iconLabel,
-          ),
-        ),
-      );
-    }
-    // TODO(kenz): this SizedBox wrapper should be unnecessary once
-    // https://github.com/flutter/flutter/issues/79894 is fixed.
-    return maybeWrapWithTooltip(
-      tooltip: tooltip,
-      tooltipPadding: tooltipPadding,
-      child: SizedBox(
-        height: defaultButtonHeight,
-        width: !isScreenWiderThan(context, minScreenWidthForTextBeforeScaling)
-            ? buttonMinWidth
-            : null,
-        child: outlined
-            ? OutlinedButton(
-                style: denseAwareOutlinedButtonStyle(
-                  context,
-                  minScreenWidthForTextBeforeScaling,
-                ),
-                onPressed: onPressed,
-                child: iconLabel,
-              )
-            : TextButton(
-                onPressed: onPressed,
-                style: denseAwareTextButtonStyle(
-                  context,
-                  minScreenWidthForTextBeforeScaling:
-                      minScreenWidthForTextBeforeScaling,
-                ),
-                child: iconLabel,
-              ),
-      ),
-    );
-  }
-}
-
-/// A widget, commonly used for icon buttons, that provides a tooltip with a
-/// common delay before the tooltip is shown.
+/// A widget that provides a tooltip with a common delay before the tooltip is
+/// shown.
 final class DevToolsTooltip extends StatelessWidget {
   const DevToolsTooltip({
     super.key,
@@ -511,239 +361,6 @@
   }
 }
 
-final class DevToolsToggleButtonGroup extends StatelessWidget {
-  const DevToolsToggleButtonGroup({
-    super.key,
-    required this.children,
-    required this.selectedStates,
-    required this.onPressed,
-    this.fillColor,
-    this.selectedColor,
-    this.borderColor,
-  });
-
-  final List<Widget> children;
-
-  final List<bool> selectedStates;
-
-  final void Function(int)? onPressed;
-
-  final Color? fillColor;
-
-  final Color? selectedColor;
-
-  final Color? borderColor;
-
-  @override
-  Widget build(BuildContext context) {
-    final theme = Theme.of(context);
-    return SizedBox(
-      height: defaultButtonHeight,
-      child: ToggleButtons(
-        borderRadius: defaultBorderRadius,
-        fillColor: fillColor,
-        selectedColor: selectedColor,
-        borderColor: borderColor,
-        textStyle: theme.textTheme.bodyMedium,
-        constraints: BoxConstraints(
-          minWidth: defaultButtonHeight,
-          minHeight: defaultButtonHeight,
-        ),
-        isSelected: selectedStates,
-        onPressed: onPressed,
-        children: children,
-      ),
-    );
-  }
-}
-
-final class DevToolsToggleButton extends StatelessWidget {
-  const DevToolsToggleButton({
-    super.key,
-    required this.onPressed,
-    required this.isSelected,
-    required this.message,
-    required this.icon,
-    this.outlined = true,
-    this.label,
-    this.shape,
-  });
-
-  final String message;
-
-  final VoidCallback onPressed;
-
-  final bool isSelected;
-
-  final IconData icon;
-
-  final String? label;
-
-  final OutlinedBorder? shape;
-
-  final bool outlined;
-
-  @override
-  Widget build(BuildContext context) {
-    return DevToolsToggleButtonGroup(
-      borderColor: outlined || isSelected
-          ? Theme.of(context).focusColor
-          : Colors.transparent,
-      selectedStates: [isSelected],
-      onPressed: (_) => onPressed(),
-      children: [
-        DevToolsTooltip(
-          message: message,
-          child: Container(
-            padding: const EdgeInsets.symmetric(horizontal: denseSpacing),
-            child: MaterialIconLabel(
-              iconData: icon,
-              label: label,
-            ),
-          ),
-        ),
-      ],
-    );
-  }
-}
-
-/// A group of buttons that share a common border.
-///
-/// This widget ensures the buttons are displayed with proper borders on the
-/// interior and exterior of the group. The attirbutes for each button can be
-/// defined by [ButtonGroupItemData] and included in [items].
-final class RoundedButtonGroup extends StatelessWidget {
-  const RoundedButtonGroup({
-    super.key,
-    required this.items,
-    this.minScreenWidthForTextBeforeScaling,
-  });
-
-  final List<ButtonGroupItemData> items;
-  final double? minScreenWidthForTextBeforeScaling;
-
-  @override
-  Widget build(BuildContext context) {
-    Widget buildButton(int index) {
-      final itemData = items[index];
-      Widget button = _ButtonGroupButton(
-        buttonData: itemData,
-        roundedLeftBorder: index == 0,
-        roundedRightBorder: index == items.length - 1,
-        minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
-      );
-      if (index != 0) {
-        button = Container(
-          decoration: BoxDecoration(
-            border: Border(
-              left: BorderSide(
-                color: Theme.of(context).focusColor,
-              ),
-            ),
-          ),
-          child: button,
-        );
-      }
-      return button;
-    }
-
-    return SizedBox(
-      height: defaultButtonHeight,
-      child: RoundedOutlinedBorder(
-        child: Row(
-          children: [
-            for (int i = 0; i < items.length; i++) buildButton(i),
-          ],
-        ),
-      ),
-    );
-  }
-}
-
-final class _ButtonGroupButton extends StatelessWidget {
-  const _ButtonGroupButton({
-    required this.buttonData,
-    this.roundedLeftBorder = false,
-    this.roundedRightBorder = false,
-    this.minScreenWidthForTextBeforeScaling,
-  });
-
-  final ButtonGroupItemData buttonData;
-  final bool roundedLeftBorder;
-  final bool roundedRightBorder;
-  final double? minScreenWidthForTextBeforeScaling;
-
-  @override
-  Widget build(BuildContext context) {
-    return DevToolsTooltip(
-      message: buttonData.tooltip,
-      child: OutlinedButton(
-        autofocus: buttonData.autofocus,
-        style: OutlinedButton.styleFrom(
-          padding: const EdgeInsets.symmetric(horizontal: densePadding),
-          side: BorderSide.none,
-          shape: RoundedRectangleBorder(
-            borderRadius: BorderRadius.horizontal(
-              left: roundedLeftBorder ? defaultRadius : Radius.zero,
-              right: roundedRightBorder ? defaultRadius : Radius.zero,
-            ),
-          ),
-        ),
-        onPressed: buttonData.onPressed,
-        child: MaterialIconLabel(
-          label: buttonData.label,
-          iconData: buttonData.icon,
-          minScreenWidthForTextBeforeScaling:
-              minScreenWidthForTextBeforeScaling,
-        ),
-      ),
-    );
-  }
-}
-
-final class ButtonGroupItemData {
-  const ButtonGroupItemData({
-    this.label,
-    this.icon,
-    String? tooltip,
-    this.onPressed,
-    this.autofocus = false,
-  })  : tooltip = tooltip ?? label,
-        assert(label != null || icon != null);
-
-  final String? label;
-  final IconData? icon;
-  final String? tooltip;
-  final VoidCallback? onPressed;
-  final bool autofocus;
-}
-
-final class DevToolsFilterButton extends StatelessWidget {
-  const DevToolsFilterButton({
-    super.key,
-    required this.onPressed,
-    required this.isFilterActive,
-    this.message = 'Filter',
-    this.outlined = true,
-  });
-
-  final VoidCallback onPressed;
-  final bool isFilterActive;
-  final String message;
-  final bool outlined;
-
-  @override
-  Widget build(BuildContext context) {
-    return DevToolsToggleButton(
-      onPressed: onPressed,
-      isSelected: isFilterActive,
-      message: message,
-      icon: Icons.filter_list,
-      outlined: outlined,
-    );
-  }
-}
-
 /// Label including an image icon and optional text.
 final class ImageIconLabel extends StatelessWidget {
   const ImageIconLabel(
@@ -923,3 +540,25 @@
   final String display;
   final String url;
 }
+
+class RoundedCornerOptions {
+  const RoundedCornerOptions({
+    this.showTopLeft = true,
+    this.showTopRight = true,
+    this.showBottomLeft = true,
+    this.showBottomRight = true,
+  });
+
+  /// Static constant instance with all borders hidden
+  static const empty = RoundedCornerOptions(
+    showTopLeft: false,
+    showTopRight: false,
+    showBottomLeft: false,
+    showBottomRight: false,
+  );
+
+  final bool showTopLeft;
+  final bool showTopRight;
+  final bool showBottomLeft;
+  final bool showBottomRight;
+}
diff --git a/packages/devtools_app_shared/lib/src/ui/text_field.dart b/packages/devtools_app_shared/lib/src/ui/text_field.dart
new file mode 100644
index 0000000..2839a00
--- /dev/null
+++ b/packages/devtools_app_shared/lib/src/ui/text_field.dart
@@ -0,0 +1,148 @@
+// Copyright 2024 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'common.dart';
+import 'theme/theme.dart';
+
+/// A DevTools-styled text field with a suffix action to clear the search field.
+final class DevToolsClearableTextField extends StatelessWidget {
+  DevToolsClearableTextField({
+    super.key,
+    required this.labelText,
+    TextEditingController? controller,
+    this.hintText,
+    this.prefixIcon,
+    this.additionalSuffixActions = const <Widget>[],
+    this.onChanged,
+    this.onSubmitted,
+    this.autofocus = false,
+    this.enabled,
+    this.roundedBorder = false,
+  }) : controller = controller ?? TextEditingController();
+
+  final TextEditingController controller;
+  final String? hintText;
+  final Widget? prefixIcon;
+  final List<Widget> additionalSuffixActions;
+  final String labelText;
+  final void Function(String)? onChanged;
+  final void Function(String)? onSubmitted;
+  final bool autofocus;
+  final bool? enabled;
+  final bool roundedBorder;
+
+  static const _contentVerticalPadding = 6.0;
+
+  /// This is the default border radius used by the [OutlineInputBorder]
+  /// constructor.
+  static const _defaultInputBorderRadius =
+      BorderRadius.all(Radius.circular(4.0));
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return SizedBox(
+      height: defaultTextFieldHeight,
+      child: TextField(
+        autofocus: autofocus,
+        controller: controller,
+        enabled: enabled,
+        onChanged: onChanged,
+        onSubmitted: onSubmitted,
+        style: theme.regularTextStyle,
+        decoration: InputDecoration(
+          isDense: true,
+          contentPadding: const EdgeInsets.only(
+            top: _contentVerticalPadding,
+            bottom: _contentVerticalPadding,
+            left: denseSpacing,
+            right: densePadding,
+          ),
+          constraints: BoxConstraints(
+            minHeight: defaultTextFieldHeight,
+            maxHeight: defaultTextFieldHeight,
+          ),
+          border: OutlineInputBorder(
+            borderRadius: roundedBorder
+                ? const BorderRadius.all(defaultRadius)
+                : _defaultInputBorderRadius,
+          ),
+          labelText: labelText,
+          labelStyle: theme.subtleTextStyle,
+          hintText: hintText,
+          hintStyle: theme.subtleTextStyle,
+          prefixIcon: prefixIcon,
+          suffix: SizedBox(
+            height: inputDecorationElementHeight,
+            child: Row(
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                InputDecorationSuffixButton.clear(
+                  onPressed: () {
+                    controller.clear();
+                    onChanged?.call('');
+                  },
+                ),
+                ...additionalSuffixActions,
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// A DevTools-styled icon action button intended to be used as an
+/// [InputDecoration.suffix] widget.
+final class InputDecorationSuffixButton extends StatelessWidget {
+  const InputDecorationSuffixButton({
+    super.key,
+    required this.icon,
+    required this.onPressed,
+    this.tooltip,
+  });
+
+  factory InputDecorationSuffixButton.clear({
+    required VoidCallback? onPressed,
+  }) =>
+      InputDecorationSuffixButton(
+        icon: Icons.clear,
+        onPressed: onPressed,
+        tooltip: 'Clear',
+      );
+
+  factory InputDecorationSuffixButton.close({
+    required VoidCallback? onPressed,
+  }) =>
+      InputDecorationSuffixButton(
+        icon: Icons.close,
+        onPressed: onPressed,
+        tooltip: 'Close',
+      );
+
+  final IconData icon;
+  final VoidCallback? onPressed;
+  final String? tooltip;
+
+  @override
+  Widget build(BuildContext context) {
+    return maybeWrapWithTooltip(
+      tooltip: tooltip,
+      child: SizedBox(
+        height: inputDecorationElementHeight,
+        width: inputDecorationElementHeight + denseSpacing,
+        child: IconButton(
+          padding: EdgeInsets.zero,
+          onPressed: onPressed,
+          iconSize: defaultIconSize,
+          splashRadius: defaultIconSize,
+          icon: Icon(icon),
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_app_shared/lib/src/utils/utils.dart b/packages/devtools_app_shared/lib/src/utils/utils.dart
index cd6623b..c4c21f6 100644
--- a/packages/devtools_app_shared/lib/src/utils/utils.dart
+++ b/packages/devtools_app_shared/lib/src/utils/utils.dart
@@ -115,3 +115,31 @@
   String hex(int val) => val.toRadixString(16).padLeft(2, '0');
   return '#${hex(color.red)}${hex(color.green)}${hex(color.blue)}${hex(color.alpha)}';
 }
+
+extension StringUtilities on String {
+  /// Whether [query] is a case insensitive "fuzzy match" for this String.
+  ///
+  /// For example, the query "hwf" would be a fuzzy match for the String
+  /// "hello_world_file".
+  bool caseInsensitiveFuzzyMatch(String query) {
+    query = query.toLowerCase();
+    final lowercase = toLowerCase();
+    final it = query.characters.iterator;
+    var strIndex = 0;
+    while (it.moveNext()) {
+      final char = it.current;
+      var foundChar = false;
+      for (int i = strIndex; i < lowercase.length; i++) {
+        if (lowercase[i] == char) {
+          strIndex = i + 1;
+          foundChar = true;
+          break;
+        }
+      }
+      if (!foundChar) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/packages/devtools_app_shared/lib/ui.dart b/packages/devtools_app_shared/lib/ui.dart
index b110f5d..c5f6d38 100644
--- a/packages/devtools_app_shared/lib/ui.dart
+++ b/packages/devtools_app_shared/lib/ui.dart
@@ -2,10 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+export 'src/ui/buttons.dart';
 export 'src/ui/common.dart';
 export 'src/ui/dialogs.dart';
 export 'src/ui/flex_split_column.dart';
 export 'src/ui/split_pane.dart';
+export 'src/ui/text_field.dart';
 export 'src/ui/theme/ide_theme.dart';
 export 'src/ui/theme/theme.dart';
 export 'src/ui/ui_utils.dart';
diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml
index 9ad4b19..a3e760d 100644
--- a/packages/devtools_app_shared/pubspec.yaml
+++ b/packages/devtools_app_shared/pubspec.yaml
@@ -1,6 +1,6 @@
 name: devtools_app_shared
 description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions.
-version: 0.2.1
+version: 0.2.2-wip
 repository: https://github.com/flutter/devtools/tree/master/packages/devtools_app_shared
 
 environment:
diff --git a/packages/devtools_app_shared/test/utils/utils_test.dart b/packages/devtools_app_shared/test/utils/utils_test.dart
index 0af7e76..21c3f47 100644
--- a/packages/devtools_app_shared/test/utils/utils_test.dart
+++ b/packages/devtools_app_shared/test/utils/utils_test.dart
@@ -94,4 +94,22 @@
       expect(toCssHexColor(const Color(0xFFAABBCC)), equals('#aabbccff'));
     });
   });
+
+  group('StringExtension', () {
+    test('fuzzyMatch', () {
+      const str = 'hello_world_file';
+      expect(str.caseInsensitiveFuzzyMatch('h'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('o_'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('hw'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('hwf'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('_e'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('HWF'), isTrue);
+      expect(str.caseInsensitiveFuzzyMatch('_E'), isTrue);
+
+      expect(str.caseInsensitiveFuzzyMatch('hwfh'), isFalse);
+      expect(str.caseInsensitiveFuzzyMatch('hfw'), isFalse);
+      expect(str.caseInsensitiveFuzzyMatch('gello'), isFalse);
+      expect(str.caseInsensitiveFuzzyMatch('files'), isFalse);
+    });
+  });
 }