diff --git a/samples/user-interface/appwidgets/src/main/AndroidManifest.xml b/samples/user-interface/appwidgets/src/main/AndroidManifest.xml
index f0bf0b19..c5501a42 100644
--- a/samples/user-interface/appwidgets/src/main/AndroidManifest.xml
+++ b/samples/user-interface/appwidgets/src/main/AndroidManifest.xml
@@ -38,6 +38,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Unit>,
+ spacing: Dp,
+ modifier: GlanceModifier = GlanceModifier.fillMaxHeight(),
+) {
+ val padding = spacing / 2 // split spacing between siblings
+
+ Row(modifier = modifier) {
+ Column(
+ modifier = GlanceModifier
+ .fillMaxHeight()
+ .defaultWeight()
+ ) {
+ items.forEachIndexed { index, item ->
+ val paddingModifier = when (index) {
+ // Only bottom padding
+ 0 -> GlanceModifier.padding(bottom = padding)
+
+ // Top and bottom padding
+ items.lastIndex -> GlanceModifier.padding(top = padding)
+
+ // Only top padding
+ else -> GlanceModifier.padding(
+ top = padding,
+ bottom = padding
+ )
+ }
+
+ Box(
+ modifier = paddingModifier
+ .fillMaxWidth()
+ .defaultWeight()
+ ) {
+ item()
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Arranges the provided [items] horizontally spaced by given [spacing].
+ */
+@Composable
+fun SpacedRow(
+ items: List<@Composable () -> Unit>,
+ spacing: Dp,
+ modifier: GlanceModifier = GlanceModifier.fillMaxWidth(),
+) {
+ val padding = spacing / 2 // split spacing between siblings
+
+ Column(modifier = modifier) {
+ Row(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .defaultWeight()
+ ) {
+ items.forEachIndexed { index, item ->
+ val paddingModifier = when (index) {
+ // Right padding only
+ 0 -> GlanceModifier.padding(end = padding)
+
+ // Left padding only
+ items.lastIndex -> GlanceModifier.padding(start = padding)
+
+ // Both left and right padding
+ else -> GlanceModifier.padding(start = padding, end = padding)
+ }
+
+ Box(
+ modifier = paddingModifier
+ .fillMaxHeight()
+ .defaultWeight()
+ ) {
+ item()
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Arranges given [items] in a grid of up to 2 rows spaced by [spacing] in the available space.
+ *
+ * Suitable for cases where there are multiple [items] that can be arranged in two rows.
+ */
+@Composable
+fun TwoRowGrid(
+ items: List<@Composable () -> Unit>,
+ spacing: Dp,
+ modifier: GlanceModifier = GlanceModifier.fillMaxSize(),
+) {
+ val middle = items.size / 2
+ val rowOneItems = items.subList(0, middle)
+ val rowTwoItems = items.subList(middle, items.size)
+
+ Column(modifier = modifier) {
+ if (rowOneItems.isNotEmpty()) {
+ SpacedRow(
+ items = rowOneItems,
+ spacing = spacing,
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .defaultWeight()
+ .padding(bottom = spacing / 2),
+ )
+ }
+ if (rowTwoItems.isNotEmpty()) {
+ SpacedRow(
+ items = rowTwoItems,
+ spacing = spacing,
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .padding(top = spacing / 2)
+ .defaultWeight()
+ )
+ }
+ }
+}
+
+/**
+ * Arranges the provided [items] in a grid of two rows spaced by [spacing] with a [sideBarItem] on
+ * the left.
+ */
+@Composable
+fun SideBarTwoRowGrid(
+ sideBarItem: @Composable () -> Unit,
+ items: List<@Composable () -> Unit>,
+ sideBarWidth: Dp,
+ spacing: Dp,
+ modifier: GlanceModifier = GlanceModifier.fillMaxSize(),
+) {
+ Row(modifier = modifier) {
+ Box(
+ modifier = GlanceModifier
+ .fillMaxHeight()
+ .width(sideBarWidth)
+ ) {
+ sideBarItem()
+ }
+ Spacer(
+ modifier = GlanceModifier
+ .fillMaxHeight()
+ .width(spacing)
+ )
+ TwoRowGrid(
+ items = items,
+ spacing = spacing,
+ modifier = GlanceModifier
+ .fillMaxHeight()
+ .defaultWeight()
+ )
+ }
+}
+
+/**
+ * Arranges the provided [items] in a grid of two rows spaced by [spacing] with a [headerItem] on the top.
+ */
+@Composable
+fun HeaderTwoRowGrid(
+ headerItem: @Composable () -> Unit,
+ items: List<@Composable () -> Unit>,
+ headerHeight: Dp,
+ spacing: Dp,
+ modifier: GlanceModifier = GlanceModifier.fillMaxSize(),
+) {
+ Column(modifier = modifier) {
+ Box(
+ modifier = GlanceModifier
+ .height(headerHeight)
+ .fillMaxWidth()
+ ) {
+ headerItem()
+ }
+ Spacer(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .height(spacing)
+ )
+ TwoRowGrid(
+ items = items,
+ spacing = spacing,
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .defaultWeight()
+ )
+ }
+}
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/SearchToolBarLayout.kt b/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/SearchToolBarLayout.kt
new file mode 100644
index 00000000..6e52dc4d
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/SearchToolBarLayout.kt
@@ -0,0 +1,417 @@
+/*
+ * Copyright 2024 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.
+ */
+package com.example.platform.ui.appwidgets.glance.layout.toolbars.layout
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.action.Action
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.background
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.layout.width
+import androidx.glance.preview.ExperimentalGlancePreviewApi
+import androidx.glance.preview.Preview
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.example.platform.ui.appwidgets.R
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.headerItemHeight
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.iconSize
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.itemsSpacing
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.minButtonSize
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.sideBarLeadingItemWidth
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutDimens.widgetPadding
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.canShowSearchText
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.canUseFilledButtons
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.Companion.numberOfItemsThatFit
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.HeaderTwoRowGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.HorizontalRow
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.SideBarTwoRowGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.TwoByTwoGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.SearchToolBarLayoutSize.VerticalColumn
+import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
+
+/**
+ * Layout focused on presenting a search entrypoint along with additional handy shortcuts that
+ * provide quick access to frequently used functions.
+ *
+ * This serves as an implementation suggestion, but should be customized to fit your product's
+ * needs.
+ *
+ * @param searchButton a button representing the search action in the widget displayed more
+ * prominently than the trailing buttons..
+ * @param trailingButtons list of 4 buttons representing handy shortcuts to frequently used
+ * functions in your app.
+ * @see SearchToolBarLayoutSize for supported breakpoints
+ */
+@Composable
+fun SearchToolBarLayout(
+ searchButton: SearchToolBarButton,
+ // 4 items, as a list here for convenience, that you might inline in your implementation.
+ trailingButtons: List,
+) {
+ val searchButtonItem: @Composable () -> Unit = {
+ if (canShowSearchText()) {
+ SearchBar(searchButton = searchButton)
+ } else {
+ SearchIconButton(
+ searchButton = searchButton,
+ filled = canUseFilledButtons()
+ )
+ }
+ }
+
+ val trailingButtonItems: List<@Composable () -> Unit> =
+ trailingButtons.map {
+ {
+ TrailingButton(
+ button = it,
+ filled = canUseFilledButtons()
+ )
+ }
+ }
+
+ Scaffold(
+ modifier = GlanceModifier.padding(vertical = widgetPadding),
+ horizontalPadding = widgetPadding
+ ) {
+ when (val layoutSize = SearchToolBarLayoutSize.fromLocalSize()) {
+ HorizontalRow, VerticalColumn -> {
+ val horizontal = (layoutSize == HorizontalRow)
+ val numberOfItems = numberOfItemsThatFit(
+ horizontal = horizontal,
+ minItemSize = minButtonSize,
+ spacing = itemsSpacing
+ )
+ val allItems = listOf(searchButtonItem) + trailingButtonItems
+ val finalItems = allItems.take(numberOfItems)
+
+ if (horizontal) {
+ SpacedRow(
+ items = finalItems,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ } else {
+ SpacedColumn(
+ items = finalItems,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+
+ TwoByTwoGrid -> TwoRowGrid(
+ // 4 items including search button
+ items = listOf(searchButtonItem) + trailingButtonItems.take(3),
+ spacing = itemsSpacing
+ )
+
+ SideBarTwoRowGrid -> SideBarTwoRowGrid(
+ sideBarItem = searchButtonItem,
+ items = trailingButtonItems.take(4),
+ sideBarWidth = sideBarLeadingItemWidth,
+ spacing = itemsSpacing
+ )
+
+ HeaderTwoRowGrid -> HeaderTwoRowGrid(
+ headerItem = searchButtonItem,
+ items = trailingButtonItems.take(4),
+ headerHeight = headerItemHeight,
+ spacing = itemsSpacing
+ )
+ }
+ }
+}
+
+/**
+ * Data class representing different buttons displayed in the search toolbar widget.
+ * @param iconRes Resource id of the icon button.
+ * @param contentDescription description about the button that can be used by the accessibility
+ * services.
+ * @param onClick action to perform on click of the button.
+ * @param text optional text that can be displayed if space is available.
+ */
+data class SearchToolBarButton(
+ @DrawableRes val iconRes: Int,
+ val contentDescription: String,
+ val onClick: Action,
+ val text: String? = null,
+)
+
+/**
+ * Search entrypoint in style of a rounded icon button.
+ */
+@Composable
+private fun SearchIconButton(
+ searchButton: SearchToolBarButton,
+ filled: Boolean,
+) {
+ RectangularIconButton(
+ imageProvider = ImageProvider(searchButton.iconRes),
+ contentDescription = searchButton.contentDescription,
+ backgroundColor = if (filled) {
+ GlanceTheme.colors.tertiary
+ } else {
+ ColorProvider(Color.Transparent, Color.Transparent)
+ },
+ contentColor = if (filled) {
+ GlanceTheme.colors.onTertiary
+ } else {
+ GlanceTheme.colors.onSecondaryContainer
+ },
+ iconSize = iconSize,
+ roundedCornerShape = RoundedCornerShape.FULL,
+ onClick = searchButton.onClick,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+}
+
+/**
+ * Search entrypoint in style of a search bar.
+ */
+@Composable
+private fun SearchBar(searchButton: SearchToolBarButton) {
+ Row(
+ horizontalAlignment = Alignment.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ .background(GlanceTheme.colors.secondaryContainer)
+ .cornerRadius(RoundedCornerShape.FULL.cornerRadius)
+ .semantics { this.contentDescription = searchButton.contentDescription }
+ .clickable(searchButton.onClick),
+ ) {
+ // Search or brand icon
+ Image(
+ provider = ImageProvider(searchButton.iconRes),
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.primary),
+ modifier = GlanceModifier.size(iconSize)
+ )
+ // Followed by text
+ searchButton.text?.let {
+ Spacer(GlanceModifier.width(8.dp))
+ Text(
+ text = it,
+ maxLines = 1,
+ style = TextStyle(
+ color = GlanceTheme.colors.onSecondaryContainer,
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Medium
+ ),
+ )
+ }
+ }
+}
+
+/**
+ * Rounded action buttons that support [filled] background when specified.
+ */
+@Composable
+private fun TrailingButton(
+ button: SearchToolBarButton,
+ filled: Boolean = true,
+) {
+ RectangularIconButton(
+ imageProvider = ImageProvider(button.iconRes),
+ contentDescription = button.contentDescription,
+ backgroundColor = if (filled) {
+ GlanceTheme.colors.secondaryContainer
+ } else {
+ ColorProvider(Color.Transparent, Color.Transparent)
+ },
+ contentColor = GlanceTheme.colors.onSecondaryContainer,
+ onClick = button.onClick,
+ iconSize = iconSize,
+ roundedCornerShape = RoundedCornerShape.MEDIUM,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+}
+
+// Breakpoints based on the UX design.
+private enum class SearchToolBarLayoutSize {
+ // Row of search button followed by action buttons that fit horizontally.
+ HorizontalRow,
+
+ // Column of search button and action buttons that fit vertically.
+ VerticalColumn,
+
+ // A two row, two column grid containing search button and 3 trailing buttons.
+ TwoByTwoGrid,
+
+ // A side bar for search followed by a 2x2 grid of trailing buttons.
+ SideBarTwoRowGrid,
+
+ // A header containing header bar followed by a 2x2 grid of trailing buttons.
+ HeaderTwoRowGrid;
+
+ companion object {
+ @Composable
+ fun fromLocalSize(): SearchToolBarLayoutSize {
+ val size = LocalSize.current
+ val height = size.height
+ val width = size.width
+
+ return if (height < 128.dp) {
+ HorizontalRow
+ } else if (width < 128.dp) {
+ VerticalColumn
+ } else if (height < 188.dp && width < 188.dp) {
+ TwoByTwoGrid
+ } else if (height < 188.dp) {
+ SideBarTwoRowGrid
+ } else {
+ HeaderTwoRowGrid
+ }
+ }
+
+ /**
+ * Helper to decide whether to show search text in current widget size.
+ */
+ @Composable
+ fun canShowSearchText(): Boolean {
+ val localSize = LocalSize.current
+
+ // Per breakpoints in the UX design
+ return localSize.width >= 184.dp && localSize.height >= 188.dp
+ }
+
+ /**
+ * Helper to decide whether to show filled icons vs without containers in current widget size.
+ */
+ @Composable
+ fun canUseFilledButtons(): Boolean {
+ val localSize = LocalSize.current
+
+ // Per breakpoints in the UX design
+ return localSize.height >= 72.dp && localSize.width >= 72.dp
+ }
+
+ /**
+ * Helper to decide how many items to show in the available space in given orientation.
+ *
+ * @param horizontal if its a horizontal orientation
+ * @param minItemSize min size to maintain for each item when identify how many to fit
+ * @param spacing spacing to between items
+ */
+ @Composable
+ fun numberOfItemsThatFit(horizontal: Boolean, minItemSize: Dp, spacing: Dp): Int {
+ val size = if (horizontal) {
+ LocalSize.current.width
+ } else {
+ LocalSize.current.height
+ }
+
+ // n buttons have n-1 content spacers, so, we add one to total width to make the width division
+ // simpler.
+ val normalizedWidth: Dp = size + spacing
+ val normalizedButtonWidth: Dp = minItemSize + spacing
+ // Number of equally wide buttons that fit in a row
+ return ((normalizedWidth / normalizedButtonWidth)).toInt()
+ }
+ }
+}
+
+// Various dimensions coming from the UX design
+private object SearchToolBarLayoutDimens {
+ /** Minimum size needed for buttons / clickable areas for accessibility. */
+ val minButtonSize = 48.dp
+
+ /** Padding around the content within the widget. */
+ val widgetPadding = 12.dp
+
+ /** Spacing between buttons in all layouts. */
+ val itemsSpacing = 8.dp
+
+ /** Size of icons in all buttons */
+ val iconSize = 24.dp
+
+ /** Height of side bar in the [SideBarTwoRowGrid] layout. */
+ val sideBarLeadingItemWidth = 52.dp
+
+ /** Height of header in the [HeaderTwoRowGrid] layout. */
+ val headerItemHeight = 52.dp
+}
+
+/**
+ * Previews for various breakpoints for this search toolbar layout.
+ */
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Preview(widthDp = 56, heightDp = 56) // only search icon
+@Preview(widthDp = 128, heightDp = 48) // reveals one additional button - no background
+@Preview(widthDp = 128, heightDp = 72) // w/ background colors
+@Preview(widthDp = 296, heightDp = 48) // more buttons - no background
+@Preview(widthDp = 296, heightDp = 72) // more buttons - w/ background colors
+@Preview(widthDp = 72, heightDp = 228) // vertical
+@Preview(widthDp = 128, heightDp = 128) // 2x2 grid
+@Preview(widthDp = 296, heightDp = 128) // search sidebar + 2x2 grid
+@Preview(widthDp = 128, heightDp = 228) // search on top (no text) + 2x2 grid
+@Preview(widthDp = 240, heightDp = 228) // search on top w/ text + 2x2 grid
+@Composable
+private fun SearchToolbarPreview() {
+ SearchToolBarLayout(
+ searchButton = SearchToolBarButton(
+ iconRes = R.drawable.sample_search_icon,
+ contentDescription = "Search notes",
+ text = "Search",
+ onClick = actionStartDemoActivity("search notes button")
+ ),
+ trailingButtons = listOf(
+ SearchToolBarButton(
+ iconRes = R.drawable.sample_mic_icon,
+ contentDescription = "audio",
+ onClick = actionStartDemoActivity("audio button")
+ ),
+ SearchToolBarButton(
+ iconRes = R.drawable.sample_videocam_icon,
+ contentDescription = "video note",
+ onClick = actionStartDemoActivity("video note button")
+ ),
+ SearchToolBarButton(
+ iconRes = R.drawable.sample_camera_icon,
+ contentDescription = "camera",
+ onClick = actionStartDemoActivity("camera button")
+ ),
+ SearchToolBarButton(
+ iconRes = R.drawable.sample_share_icon,
+ contentDescription = "share",
+ onClick = actionStartDemoActivity("share button")
+ ),
+ )
+ )
+}
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/ToolBarLayout.kt b/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/ToolBarLayout.kt
new file mode 100644
index 00000000..b2db4a15
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/java/com/example/platform/ui/appwidgets/glance/layout/toolbars/layout/ToolBarLayout.kt
@@ -0,0 +1,455 @@
+/*
+ * Copyright 2024 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.
+ */
+package com.example.platform.ui.appwidgets.glance.layout.toolbars.layout
+
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalSize
+import androidx.glance.action.Action
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.components.TitleBar
+import androidx.glance.color.ColorProvider
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.padding
+import androidx.glance.layout.size
+import androidx.glance.preview.ExperimentalGlancePreviewApi
+import androidx.glance.preview.Preview
+import com.example.platform.ui.appwidgets.R
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutDimens.iconSize
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutDimens.itemsSpacing
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutDimens.minButtonSize
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutDimens.widgetPadding
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.Companion.canShowHeaderTitle
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.Companion.canUseFilledButtons
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.Companion.numberOfItemsThatFit
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.Companion.numberOfContentButtonsInTwoRowGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.HeaderTwoRowGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.HorizontalRow
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.TwoRowGrid
+import com.example.platform.ui.appwidgets.glance.layout.toolbars.layout.ToolBarLayoutSize.VerticalColumn
+import com.example.platform.ui.appwidgets.glance.layout.utils.ActionUtils.actionStartDemoActivity
+import com.example.platform.ui.appwidgets.glance.layout.utils.MediumWidgetPreview
+import com.example.platform.ui.appwidgets.glance.layout.utils.SmallWidgetPreview
+
+/**
+ * A layout focused on presenting a brand icon, a most frequently used entrypoint along with 4
+ * additional entry points that provide quick access to various functions in your app.
+ *
+ * This serves as an implementation suggestion, but should be customized to fit your product's
+ * needs.
+ *
+ * @see appName a title for the toolbar e.g. app name that can be displayed at large sizes.
+ * @see appIconRes a brand icon to be displayed in the toolbar at all sizes.
+ * @param headerButton a button representing the most frequently used action of your app that is
+ * displayed more prominently than than the other [buttons].
+ * @param buttons list of additional 4 buttons for other frequently used functions in your app.
+ */
+@Composable
+fun ToolBarLayout(
+ appName: String,
+ @DrawableRes appIconRes: Int,
+ headerButton: ToolBarButton,
+ // 4 items, as a list here for convenience, that you might inline in your implementation.
+ buttons: List,
+) {
+ // Deconstructed header items shown along side other buttons in smaller widget sizes where
+ // a header isn't shown.
+ val appIconItem: @Composable () -> Unit = {
+ FluidHeaderAppIcon(iconRes = appIconRes)
+ }
+ val headerButtonItem: @Composable () -> Unit = {
+ // Unlike in the combined header case, this header button is fluid & fills the available space
+ // allowing us to display it along with other buttons.
+ FluidHeaderIconButton(
+ button = headerButton
+ )
+ }
+
+ // Combined header item (which shows app icon and header button in the title bar); shown at larger
+ // widget sizes.
+ val header: @Composable () -> Unit = {
+ Header(
+ appIconRes = appIconRes,
+ actionButton = headerButton,
+ title = if (canShowHeaderTitle()) {
+ appName
+ } else {
+ ""
+ },
+ )
+ }
+
+ // Other buttons
+ val buttonItems: List<@Composable () -> Unit> =
+ buttons.map { { FluidContentIconButton(it, filled = canUseFilledButtons()) } }
+
+ when (val layoutSize = ToolBarLayoutSize.fromLocalSize()) {
+ HorizontalRow, VerticalColumn -> {
+
+ Scaffold(
+ modifier = GlanceModifier.padding(vertical = widgetPadding),
+ horizontalPadding = widgetPadding,
+ ) {
+ val horizontal = layoutSize == HorizontalRow
+ val numberOfItems = numberOfItemsThatFit(
+ horizontal = horizontal,
+ minItemSize = minButtonSize,
+ spacing = itemsSpacing
+ )
+ val allItems = listOf(appIconItem, headerButtonItem) + buttonItems
+ val finalItems = allItems.take(numberOfItems)
+
+ if (horizontal) {
+ SpacedRow(
+ items = finalItems,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ } else {
+ SpacedColumn(
+ items = finalItems,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+ }
+
+ TwoRowGrid -> {
+ val contentButtonsToShow = buttonItems.take(
+ numberOfContentButtonsInTwoRowGrid()
+ )
+
+ Scaffold(
+ modifier = GlanceModifier.padding(vertical = widgetPadding),
+ horizontalPadding = widgetPadding
+ ) {
+ TwoRowGrid(
+ items = listOf(appIconItem, headerButtonItem) + contentButtonsToShow,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize(),
+ )
+ }
+ }
+
+ HeaderTwoRowGrid -> {
+ Scaffold(
+ modifier = GlanceModifier.padding(bottom = widgetPadding),
+ horizontalPadding = widgetPadding,
+ titleBar = header,
+ ) {
+ TwoRowGrid(
+ items = buttonItems,
+ spacing = itemsSpacing,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Title bar / header displayed at top at larger sizes.
+ *
+ * Displays brand icon, title, and a pill shaped filled action button.
+ * @see [HeaderTwoRowGrid]
+ */
+@Composable
+private fun Header(
+ appIconRes: Int,
+ title: String,
+ actionButton: ToolBarButton,
+) {
+ TitleBar(
+ startIcon = ImageProvider(appIconRes),
+ title = title,
+ iconColor = GlanceTheme.colors.primary,
+ actions = {
+ PillShapedButton(
+ iconImageProvider = ImageProvider(actionButton.iconRes),
+ contentDescription = actionButton.contentDescription,
+ iconSize = iconSize,
+ backgroundColor = if (canUseFilledButtons()) {
+ GlanceTheme.colors.tertiary
+ } else {
+ ColorProvider(Color.Transparent, Color.Transparent)
+ },
+ contentColor = GlanceTheme.colors.onTertiary,
+ onClick = actionButton.onClick,
+ modifier = GlanceModifier.padding(end = widgetPadding)
+ )
+ }
+ )
+}
+
+/**
+ * Brand icon displayed in the compact sizes where a title bar (header) cannot be shown.
+ *
+ * The background of this icon fills the available space.
+ *
+ * Using a separate icon enables us to equally space and size it with other buttons.
+ * @see [HorizontalRow], [VerticalColumn] & [TwoRowGrid]
+ */
+@Composable
+private fun FluidHeaderAppIcon(@DrawableRes iconRes: Int) {
+ Box(
+ modifier = GlanceModifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ provider = ImageProvider(iconRes),
+ contentDescription = null,
+ modifier = GlanceModifier.size(iconSize),
+ colorFilter = ColorFilter.tint(GlanceTheme.colors.onSurface)
+ )
+ }
+}
+
+/**
+ * A rounded icon button for the prominent header action displayed alongside other buttons in
+ * compact sizes where a title bar (header) cannot be shown.
+ *
+ * Using a separate icon button enables us to equally space and size it with other buttons.
+ * @see [HorizontalRow], [VerticalColumn] & [TwoRowGrid]
+ */
+@Composable
+private fun FluidHeaderIconButton(button: ToolBarButton) {
+ RectangularIconButton(
+ imageProvider = ImageProvider(button.iconRes),
+ contentDescription = button.contentDescription,
+ contentColor = if (canUseFilledButtons()) {
+ GlanceTheme.colors.onTertiary
+ } else {
+ GlanceTheme.colors.onSecondaryContainer
+ },
+ backgroundColor = if (canUseFilledButtons()) {
+ GlanceTheme.colors.tertiary
+ } else {
+ ColorProvider(Color.Transparent, Color.Transparent)
+ },
+ roundedCornerShape = RoundedCornerShape.FULL,
+ iconSize = iconSize,
+ modifier = GlanceModifier.fillMaxSize(),
+ onClick = button.onClick
+ )
+}
+
+/**
+ * Button meant for regular action buttons that appear after the header / deconstructed header
+ * items.
+ *
+ * Fills the given space, uses smaller radius, and supports filled / transparent backgrounds.
+ */
+@Composable
+private fun FluidContentIconButton(button: ToolBarButton, filled: Boolean = true) {
+ RectangularIconButton(
+ imageProvider = ImageProvider(button.iconRes),
+ contentDescription = button.contentDescription,
+ iconSize = iconSize,
+ roundedCornerShape = RoundedCornerShape.MEDIUM,
+ backgroundColor = if (filled) {
+ GlanceTheme.colors.secondaryContainer
+ } else {
+ ColorProvider(Color.Transparent, Color.Transparent)
+ },
+ contentColor = GlanceTheme.colors.onSecondaryContainer,
+ onClick = button.onClick,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+}
+
+/**
+ * Data class representing buttons displayed in the toolbar widget.
+ * @param iconRes Resource id of the icon button
+ * @param contentDescription description about the button that can be used by the accessibility
+ * services
+ * @param onClick action to perform on click of the button
+ * @param text optional text that will be displayed if space suffices.
+ */
+data class ToolBarButton(
+ @DrawableRes val iconRes: Int,
+ val contentDescription: String,
+ val onClick: Action,
+ val text: String? = null,
+)
+
+// Breakpoints from the UX design
+private enum class ToolBarLayoutSize {
+ // Row of app icon, featured button and regular action buttons that fit horizontally
+ HorizontalRow,
+
+ // Column of app icon, featured button and regular action buttons that fit vertically.
+ VerticalColumn,
+
+ // Two rows, 2-3 columns containing first column of app icon, featured action button and other
+ // columns displaying regular action buttons that fit.
+ TwoRowGrid,
+
+ // Header row (containing app icon + featured button) followed by 2 row grid containing the 4
+ // regular action buttons.
+ HeaderTwoRowGrid;
+
+ companion object {
+ @Composable
+ fun fromLocalSize(): ToolBarLayoutSize {
+ val size = LocalSize.current
+ val height = size.height
+ val width = size.width
+
+ return if (height < 128.dp) {
+ HorizontalRow
+ } else if (width < 128.dp) {
+ VerticalColumn
+ } else if (height < 172.dp) {
+ TwoRowGrid
+ } else {
+ HeaderTwoRowGrid
+ }
+ }
+
+ /**
+ * Indicates if buttons with background color can be displayed for the current widget size.
+ *
+ * Background is hidden when we are limited by height / width.
+ */
+ @Composable
+ fun canUseFilledButtons(): Boolean {
+ val localSize = LocalSize.current
+
+ return localSize.height >= 72.dp && localSize.width >= 72.dp
+ }
+
+ /**
+ * Returns how many items to show that would potentially fit in the given orientation
+ * (horizontal / vertical) if we were filling entire space.
+ * @see [HorizontalRow] & [VerticalColumn]
+ */
+ @Composable
+ fun numberOfItemsThatFit(
+ horizontal: Boolean,
+ minItemSize: Dp,
+ spacing: Dp,
+ ): Int {
+ val size = if (horizontal) {
+ LocalSize.current.width
+ } else {
+ LocalSize.current.height
+ }
+
+ // n buttons have n-1 content spacers, so, we add one to total width to make the width division
+ // simpler.
+ val normalizedWidth: Dp = size + spacing
+ val normalizedButtonWidth: Dp = minItemSize + spacing
+ // Number of equally wide buttons that fit in a row
+ return ((normalizedWidth / normalizedButtonWidth)).toInt()
+ }
+
+ /**
+ * Returns number of regular buttons that can fit in a 2-row grid where brand icon and a
+ * featured action button would also be shown.
+ *
+ * @see [TwoRowGrid]
+ */
+ @Composable
+ fun numberOfContentButtonsInTwoRowGrid() =
+ if (LocalSize.current.width >= 240.dp) { // from UX design
+ 4 // 1st column (app icon, featured button) and 2nd & 3rd column (4 regular buttons)
+ } else {
+ 2 // 1st column (app icon, featured button) and 2nd column (2 regular buttons)
+ }
+
+ /**
+ * Identifies if we should show or hide the title in the header at the current widget size.
+ */
+ @Composable
+ fun canShowHeaderTitle() =
+ LocalSize.current.width >= 240.dp && LocalSize.current.height >= 172.dp // from UX design
+ }
+}
+
+// Dimensions from UX design.
+private object ToolBarLayoutDimens {
+ /** Minimum size needed for buttons / clickable areas for accessibility. */
+ val minButtonSize = 48.dp
+
+ /** Padding around the content within the widget. */
+ val widgetPadding = 12.dp
+
+ /** Spacing between buttons in all layouts. */
+ val itemsSpacing = 8.dp
+
+ /** Size of icons in all buttons */
+ val iconSize = 24.dp
+}
+
+/**
+ * Previews for various breakpoints for this toolbar layout.
+ */
+@OptIn(ExperimentalGlancePreviewApi::class)
+@Preview(widthDp = 56, heightDp = 56) // only app icon
+@Preview(widthDp = 128, heightDp = 48) // reveals header button - no background
+@Preview(widthDp = 128, heightDp = 72) // w/ background colors
+@Preview(widthDp = 296, heightDp = 48) // more buttons - no background
+@Preview(widthDp = 296, heightDp = 72) // more buttons - w/ background colors
+@Preview(widthDp = 72, heightDp = 228) // vertical
+@Preview(widthDp = 128, heightDp = 128) // 2x2 grid
+@Preview(widthDp = 296, heightDp = 128) // 3x2 grid
+@Preview(widthDp = 128, heightDp = 228) // w/ title bar - no title
+@Preview(widthDp = 240, heightDp = 228) // w/ title bar and title
+@Composable
+private fun ToolbarPreview() {
+ ToolBarLayout(
+ appName = "App name",
+ appIconRes = R.drawable.sample_app_logo,
+ headerButton = ToolBarButton(
+ iconRes = R.drawable.sample_add_icon,
+ contentDescription = "add",
+ onClick = actionStartDemoActivity("add button")
+ ),
+ buttons = listOf(
+ ToolBarButton(
+ iconRes = R.drawable.sample_mic_icon,
+ contentDescription = "mic",
+ onClick = actionStartDemoActivity("mic button")
+ ),
+ ToolBarButton(
+ iconRes = R.drawable.sample_share_icon,
+ contentDescription = "share",
+ onClick = actionStartDemoActivity("share button")
+ ),
+ ToolBarButton(
+ iconRes = R.drawable.sample_videocam_icon,
+ contentDescription = "video",
+ onClick = actionStartDemoActivity("video button")
+ ),
+ ToolBarButton(
+ iconRes = R.drawable.sample_camera_icon,
+ contentDescription = "camera",
+ onClick = actionStartDemoActivity("camera button")
+ )
+ )
+ )
+}
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_search_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_search_toolbar_preview.png
new file mode 100644
index 00000000..5a0f17f8
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_search_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_toolbar_preview.png
new file mode 100644
index 00000000..b936bc46
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-night-nodpi/sample_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_search_toolbar.png b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_search_toolbar.png
new file mode 100644
index 00000000..b2989543
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_search_toolbar.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_toolbar.png b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_toolbar.png
new file mode 100644
index 00000000..ba29b178
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/cl_activity_row_toolbar.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_search_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_search_toolbar_preview.png
new file mode 100644
index 00000000..746bcf9b
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_search_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_toolbar_preview.png
new file mode 100644
index 00000000..b58959ee
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-nodpi/sample_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_search_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_search_toolbar_preview.png
new file mode 100644
index 00000000..ee68337b
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_search_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_toolbar_preview.png
new file mode 100644
index 00000000..09013f1d
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-night-nodpi/sample_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_search_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_search_toolbar_preview.png
new file mode 100644
index 00000000..d9fd29d5
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_search_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_toolbar_preview.png b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_toolbar_preview.png
new file mode 100644
index 00000000..5bc33277
Binary files /dev/null and b/samples/user-interface/appwidgets/src/main/res/drawable-xlarge-nodpi/sample_toolbar_preview.png differ
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable/sample_app_logo.xml b/samples/user-interface/appwidgets/src/main/res/drawable/sample_app_logo.xml
new file mode 100644
index 00000000..370ccd7e
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/drawable/sample_app_logo.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable/sample_camera_icon.xml b/samples/user-interface/appwidgets/src/main/res/drawable/sample_camera_icon.xml
new file mode 100644
index 00000000..8634a435
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/drawable/sample_camera_icon.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable/sample_mic_icon.xml b/samples/user-interface/appwidgets/src/main/res/drawable/sample_mic_icon.xml
new file mode 100644
index 00000000..7bb2cf5b
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/drawable/sample_mic_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable/sample_search_icon.xml b/samples/user-interface/appwidgets/src/main/res/drawable/sample_search_icon.xml
new file mode 100644
index 00000000..a8317a1f
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/drawable/sample_search_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/samples/user-interface/appwidgets/src/main/res/drawable/sample_videocam_icon.xml b/samples/user-interface/appwidgets/src/main/res/drawable/sample_videocam_icon.xml
new file mode 100644
index 00000000..8ea3b0d3
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/drawable/sample_videocam_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/samples/user-interface/appwidgets/src/main/res/values-xlarge/dimens.xml b/samples/user-interface/appwidgets/src/main/res/values-xlarge/dimens.xml
index a10bc021..d34c4941 100644
--- a/samples/user-interface/appwidgets/src/main/res/values-xlarge/dimens.xml
+++ b/samples/user-interface/appwidgets/src/main/res/values-xlarge/dimens.xml
@@ -2,6 +2,32 @@
+
+
+
+ 3
+ 1
+ 298dp
+ 64dp
+ 48dp
+ 48dp
+ 488dp
+ 672dp
+
+
+ 2
+ 2
+ 180dp
+ 184dp
+ 48dp
+ 48dp
+ 488dp
+ 672dp
+
2
diff --git a/samples/user-interface/appwidgets/src/main/res/values/dimens.xml b/samples/user-interface/appwidgets/src/main/res/values/dimens.xml
index 17c812c6..325980b7 100644
--- a/samples/user-interface/appwidgets/src/main/res/values/dimens.xml
+++ b/samples/user-interface/appwidgets/src/main/res/values/dimens.xml
@@ -22,6 +22,32 @@
+
+
+
+ 4
+ 1
+ 256dp
+ 48dp
+ 48dp
+ 48dp
+ 624dp
+ 422dp
+
+
+ 2
+ 2
+ 120dp
+ 115dp
+ 48dp
+ 48dp
+ 624dp
+ 422dp
+
2
diff --git a/samples/user-interface/appwidgets/src/main/res/values/strings.xml b/samples/user-interface/appwidgets/src/main/res/values/strings.xml
index f1e559a6..eaa048ed 100644
--- a/samples/user-interface/appwidgets/src/main/res/values/strings.xml
+++ b/samples/user-interface/appwidgets/src/main/res/values/strings.xml
@@ -86,6 +86,8 @@
Checklist
Action List
Image Grid
+ Toolbar
+ Search Toolbar
Add
@@ -107,5 +109,9 @@
Ideal for titles, status updates, short descriptions, or any scenario where a single line of text effectively conveys the message.
Checklist
The checklist layout is perfect for displaying tasks, providing clear tap targets for users to easily mark items as done.
+ Toolbar
+ Increase user engagement and streamline key workflows by providing instant access to your app\'s most important features with a toolbar widget.
+ Search Toolbar
+ Ideal for apps where search is paramount, this layout provides a dedicated search entry point while allowing for additional shortcuts based on available widget space.
\ No newline at end of file
diff --git a/samples/user-interface/appwidgets/src/main/res/xml/sample_search_toolbar_widget_info.xml b/samples/user-interface/appwidgets/src/main/res/xml/sample_search_toolbar_widget_info.xml
new file mode 100644
index 00000000..153d53c2
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/xml/sample_search_toolbar_widget_info.xml
@@ -0,0 +1,28 @@
+
+
diff --git a/samples/user-interface/appwidgets/src/main/res/xml/sample_toolbar_widget_info.xml b/samples/user-interface/appwidgets/src/main/res/xml/sample_toolbar_widget_info.xml
new file mode 100644
index 00000000..8b6af4a4
--- /dev/null
+++ b/samples/user-interface/appwidgets/src/main/res/xml/sample_toolbar_widget_info.xml
@@ -0,0 +1,28 @@
+
+