Introduce Icon component to abstract icons

Recent change aosp/2470800 broke a couple of things where I relied on
variations of icons to be visually distinguishable (e.g. 'star' and
'star_outline'). Abstracting a component for the icon seems to be a
reasonable way to mitigate those problems in the future; this CL
introduces such component and uses it in query history.

Change-Id: I0c0d401c1274fa9596da7cc5dffcc9174b10fcdd
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index d621810..9e26d09 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -897,7 +897,7 @@
   button {
     margin: 0 0.5rem;
 
-    i.material-icons {
+    i.material-icons, i.material-icons-filled {
       font-size: 18px;
     }
   }
diff --git a/ui/src/frontend/query_history.ts b/ui/src/frontend/query_history.ts
index 35aa9bf..e2e503c 100644
--- a/ui/src/frontend/query_history.ts
+++ b/ui/src/frontend/query_history.ts
@@ -26,6 +26,7 @@
   ValidatedType,
 } from '../controller/validators';
 import {assertTrue} from '../base/logging';
+import {Icon} from './widgets/icon';
 
 const QUERY_HISTORY_KEY = 'queryHistory';
 
@@ -67,8 +68,7 @@
                 globals.rafScheduler.scheduleFullRedraw();
               },
             },
-            m('i.material-icons',
-              vnode.attrs.entry.starred ? 'star' : 'star_outline')),
+            m(Icon, {icon: 'star', filled: vnode.attrs.entry.starred})),
           m('button',
             {
               onclick: () => {
@@ -76,7 +76,7 @@
                     {queryId: 'analyze-page-query', query}));
               },
             },
-            m('i.material-icons', 'play_arrow')),
+            m(Icon, {icon: 'play_arrow'})),
           m('button',
             {
               onclick: () => {
@@ -84,7 +84,7 @@
                 globals.rafScheduler.scheduleFullRedraw();
               },
             },
-            m('i.material-icons', 'delete'))),
+            m(Icon, {icon: 'delete'}))),
         m('pre', query));
   }
 }
diff --git a/ui/src/frontend/widgets/icon.ts b/ui/src/frontend/widgets/icon.ts
new file mode 100644
index 0000000..209db85
--- /dev/null
+++ b/ui/src/frontend/widgets/icon.ts
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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 * as m from 'mithril';
+
+export interface IconAttrs {
+  icon: string;
+  filled?: boolean;
+}
+
+export class Icon implements m.ClassComponent<IconAttrs> {
+  view(vnode: m.Vnode<IconAttrs>): m.Child {
+    return m(
+        vnode.attrs.filled ? 'i.material-icons-filled' : 'i.material-icons',
+        vnode.attrs.icon);
+  }
+}
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index 041e663..94930df 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -21,6 +21,7 @@
 import {Button} from './widgets/button';
 import {Checkbox} from './widgets/checkbox';
 import {EmptyState} from './widgets/empty_state';
+import {Icon} from './widgets/icon';
 import {Popup, PopupPosition} from './widgets/popup';
 import {Portal} from './widgets/portal';
 import {TextInput} from './widgets/text_input';
@@ -325,6 +326,10 @@
           renderWidget: (opts) => m(ControlledPopup, opts),
           initialOpts: {},
         }),
-    );
+        m('h2', 'Icon'),
+        m(WidgetShowcase, {
+          renderWidget: (opts) => m(Icon, {icon: 'star', ...opts}),
+          initialOpts: {filled: false},
+        }));
   },
 });