mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: keep widget corners and border even when the launcher crops the widget
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Home-screen widget edge, drawn over the widget by layout/rn_widget.xml. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/widget_frame" />
|
||||
<corners android:radius="@dimen/widget_radius" />
|
||||
</shape>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Home-screen widget root (layout/rn_widget.xml): its rounded outline is what the widget clips to. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_surface" />
|
||||
<corners android:radius="@dimen/widget_radius" />
|
||||
</shape>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Night twin of layout/rn_widget.xml, which explains the override. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@android:id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_surface"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="@drawable/widget_frame"
|
||||
tools:targetApi="31">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rn_widget_image_light"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:scaleType="matrix"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rn_widget_image_dark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:scaleType="matrix"
|
||||
android:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rn_widget_clickable_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rn_widget_collection_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Overrides react-native-android-widget's rn_widget.xml (same ids and children;
|
||||
layout-night/rn_widget.xml is its night twin).
|
||||
|
||||
The library draws each widget into one bitmap sized from the size the
|
||||
launcher reports, and pins it to the top left without scaling. When the real
|
||||
frame is smaller (after a rotation, or on a launcher that over-reports), the
|
||||
bitmap's bottom edge is cut off and takes the rounded corners and the border
|
||||
with it, so the bottom corners fall back to whatever the launcher clips to.
|
||||
This root always has the real size, so it owns the shape instead: it clips to
|
||||
a rounded outline (Android 12+; the launcher then leaves the corners to us),
|
||||
fills in where the bitmap falls short, and draws the frame on top.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@android:id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_surface"
|
||||
android:clipToOutline="true"
|
||||
android:foreground="@drawable/widget_frame"
|
||||
tools:targetApi="31">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rn_widget_image_light"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:scaleType="matrix"
|
||||
android:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rn_widget_image_dark"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:scaleType="matrix"
|
||||
android:visibility="gone" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rn_widget_clickable_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/rn_widget_collection_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
@@ -3,4 +3,6 @@
|
||||
<color name="splashscreen_background">#09090b</color>
|
||||
<!-- Matches the dark palette background (src/theme/tokens.ts). -->
|
||||
<color name="window_background">#09090b</color>
|
||||
<!-- DARK.bg in src/widgets/theme.ts. -->
|
||||
<color name="widget_surface">#0a0a0a</color>
|
||||
</resources>
|
||||
@@ -6,4 +6,8 @@
|
||||
<color name="colorPrimaryDark">#09090b</color>
|
||||
<!-- Behind the React root until the first frame; matches the light palette background (src/theme/tokens.ts). -->
|
||||
<color name="window_background">#ffffff</color>
|
||||
<!-- Home-screen widget surface (LIGHT.bg in src/widgets/theme.ts); it only shows where the drawn widget falls short of the launcher's frame. -->
|
||||
<color name="widget_surface">#ffffff</color>
|
||||
<!-- Home-screen widget edge, one colour for both schemes: the app can force a theme the launcher's night mode doesn't know about. -->
|
||||
<color name="widget_frame">#4d808080</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<!-- Home-screen widget corners; WIDGET_RADIUS in src/widgets/primitives.tsx. -->
|
||||
<dimen name="widget_radius">22dp</dimen>
|
||||
</resources>
|
||||
@@ -29,8 +29,6 @@ import type { EventItem, WidgetSnapshot } from '../snapshot';
|
||||
import { chipBackground, normalizeHex, type WidgetPalette } from '../theme';
|
||||
import type { Layout } from './types';
|
||||
|
||||
/** Surface's 1dp frame; Android adds a border to the padding, so it eats space. */
|
||||
const FRAME = 1;
|
||||
const HOUR = 3600000;
|
||||
const GRID_START_HOUR = 8;
|
||||
const GRID_END_HOUR = 18;
|
||||
@@ -207,7 +205,7 @@ export const AgendaLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
);
|
||||
}
|
||||
// Header bar plus its 1dp rule.
|
||||
const room = height - 2 * FRAME - headerHeight - 1;
|
||||
const room = height - headerHeight - 1;
|
||||
const { sections, hidden } = planAgenda(days, room);
|
||||
const rows: React.JSX.Element[] = [];
|
||||
sections.forEach((day, d) => {
|
||||
@@ -366,12 +364,12 @@ export const WeekLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
const events = s.calendar.events;
|
||||
const first = startOfWeek(now, s.weekStart);
|
||||
const days = Array.from({ length: 7 }, (_, i) => addDays(first, i));
|
||||
const colWidth = (width - 2 * FRAME - 2 * WEEK_PAD) / 7;
|
||||
const colWidth = (width - 2 * WEEK_PAD) / 7;
|
||||
const segments = allDaySegments(events, days);
|
||||
const strip = segments.some((seg) => seg.event !== null);
|
||||
const gridHeight = Math.max(
|
||||
0,
|
||||
height - 2 * FRAME - WEEK_PAD - WEEK_LABEL_HEIGHT - 2 - WEEK_DAY_SIZE - WEEK_GAP
|
||||
height - WEEK_PAD - WEEK_LABEL_HEIGHT - 2 - WEEK_DAY_SIZE - WEEK_GAP
|
||||
- (strip ? WEEK_STRIP_HEIGHT + WEEK_GAP : 0) - 1,
|
||||
);
|
||||
const windowStart = startOfDay(now) + GRID_START_HOUR * HOUR;
|
||||
@@ -566,11 +564,11 @@ export const MonthLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
const weeks = monthGrid(s.calendar.events, now, s.weekStart);
|
||||
const next = upcoming(s.calendar.events, now);
|
||||
const headerHeight = height >= 340 ? 44 : 38;
|
||||
const cellWidth = (width - 2 * FRAME - 2 * MONTH_PAD) / 7;
|
||||
const cellWidth = (width - 2 * MONTH_PAD) / 7;
|
||||
const chipsHeight = (n: number) =>
|
||||
n === 0 ? 0 : MONTH_CHIPS_TOP + n * MONTH_CHIP_HEIGHT + (n - 1) * 4 + MONTH_CHIPS_BOTTOM;
|
||||
const gridRoom = (n: number) =>
|
||||
height - 2 * FRAME - headerHeight - MONTH_LABEL_HEIGHT - MONTH_GRID_TOP - chipsHeight(n);
|
||||
height - headerHeight - MONTH_LABEL_HEIGHT - MONTH_GRID_TOP - chipsHeight(n);
|
||||
// Two chips when the weeks still get a comfortable row, else one, else none.
|
||||
let chips = Math.min(2, next.length);
|
||||
while (chips > 0 && gridRoom(chips) / weeks.length < MONTH_CELL_MIN) chips -= 1;
|
||||
@@ -700,7 +698,7 @@ function TodayColumn({
|
||||
const tomorrowList = eventsOnDay(events, addDays(today, 1));
|
||||
|
||||
// The "N more this week" line is always reserved; chips share the rest.
|
||||
const room = height - 2 * FRAME - 2 * TODAY_PAD - TODAY_LINE;
|
||||
const room = height - 2 * TODAY_PAD - TODAY_LINE;
|
||||
const row = TODAY_CHIP + TODAY_GAP;
|
||||
let withTomorrow = tomorrowList.length > 0;
|
||||
let slots = Math.floor((room - (withTomorrow ? TOMORROW_BLOCK : 0) + TODAY_GAP) / row);
|
||||
@@ -792,7 +790,7 @@ function TodayColumn({
|
||||
export const TodayEventsLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
// The date block is ~108dp tall at 40sp; a short 4x2 gets a smaller number
|
||||
// and, if still tight, loses the month line.
|
||||
const room = height - 2 * FRAME - 2 * TODAY_PAD;
|
||||
const room = height - 2 * TODAY_PAD;
|
||||
const big = room >= 108 ? 40 : 32;
|
||||
const showMonth = room >= 3 * 18 + Math.ceil(big * 1.33);
|
||||
return (
|
||||
@@ -820,7 +818,7 @@ export const NextEventLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
const when = running ? f.t('widgets.calendar.now', 'Now') : capitalize(f.relative(e.start, now), f.locale);
|
||||
// Status line, gap, one title line and the time range always show; then the
|
||||
// place, the calendar and a second title line, as long as they fit.
|
||||
const room = height - 2 * FRAME - 28;
|
||||
const room = height - 28;
|
||||
let used = 16 + 6 + 22 + 18;
|
||||
const place = placeOf(f, e);
|
||||
const showPlace = !!place && used + 16 <= room;
|
||||
@@ -888,7 +886,7 @@ function TwoLineChip({ f, e, now }: { f: Fmt; e: EventItem; now: number }) {
|
||||
}
|
||||
|
||||
export const DateNextLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
const room = height - 2 * FRAME - 2 * DATE_PAD - DATE_CIRCLE - 8;
|
||||
const room = height - 2 * DATE_PAD - DATE_CIRCLE - 8;
|
||||
const count = room >= 2 * DATE_CHIP + 4 ? 2 : 1;
|
||||
const next = s.calendar.supported ? upcoming(s.calendar.events, now).slice(0, count) : [];
|
||||
const chips: React.JSX.Element[] = [];
|
||||
|
||||
@@ -306,8 +306,8 @@ export const MailAndNextLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
height: 'match_parent',
|
||||
padding: pad,
|
||||
backgroundColor: unread > 0 ? p.unreadRow : p.bg,
|
||||
borderTopLeftRadius: WIDGET_RADIUS - 1,
|
||||
borderBottomLeftRadius: WIDGET_RADIUS - 1,
|
||||
borderTopLeftRadius: WIDGET_RADIUS,
|
||||
borderBottomLeftRadius: WIDGET_RADIUS,
|
||||
}}
|
||||
>
|
||||
<FlexWidget style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
|
||||
@@ -17,9 +17,14 @@ import { chipBackground, normalizeHex, type WidgetPalette } from './theme';
|
||||
import { filledIcon, icon, type FilledIconName, type IconName } from './icons';
|
||||
import type { WidgetClick } from './clicks';
|
||||
|
||||
/** Corner radius of the widget surface; launchers clip to roughly this on Android 12+. */
|
||||
/** Corner radius of the widget surface; `widget_radius` in android/app/src/main/res/values/dimens.xml. */
|
||||
export const WIDGET_RADIUS = 22;
|
||||
|
||||
/**
|
||||
* The widget's background. Its edge and the clip to rounded corners come from
|
||||
* the root layout (android/app/src/main/res/layout/rn_widget.xml), which has the
|
||||
* launcher's real size; this bitmap is drawn at the reported one and can be cut off.
|
||||
*/
|
||||
export function Surface({
|
||||
p,
|
||||
children,
|
||||
@@ -39,8 +44,6 @@ export function Surface({
|
||||
width: 'match_parent',
|
||||
backgroundColor: p.bg,
|
||||
borderRadius: WIDGET_RADIUS,
|
||||
borderWidth: 1,
|
||||
borderColor: p.frame,
|
||||
overflow: 'hidden',
|
||||
...style,
|
||||
}}
|
||||
|
||||
@@ -10,15 +10,16 @@ export type WidgetScheme = 'light' | 'dark';
|
||||
|
||||
export interface WidgetPalette {
|
||||
scheme: WidgetScheme;
|
||||
/** Widget surface (webmail `--color-background`). */
|
||||
/**
|
||||
* Widget surface (webmail `--color-background`). The edge around it is drawn
|
||||
* natively, as `widget_frame` in android/app/src/main/res/values/colors.xml.
|
||||
*/
|
||||
bg: ColorProp;
|
||||
fg: ColorProp;
|
||||
muted: ColorProp;
|
||||
/** Search pill, segmented track, quota ring track (webmail `--color-muted`). */
|
||||
mutedBg: ColorProp;
|
||||
border: ColorProp;
|
||||
/** Outer widget edge. */
|
||||
frame: ColorProp;
|
||||
primary: ColorProp;
|
||||
primaryFg: ColorProp;
|
||||
accent: ColorProp;
|
||||
@@ -66,7 +67,6 @@ export const LIGHT: WidgetPalette = {
|
||||
muted: '#64748b',
|
||||
mutedBg: '#f1f5f9',
|
||||
border: '#e2e8f0',
|
||||
frame: '#e2e8f0',
|
||||
primary: '#3b82f6',
|
||||
primaryFg: '#ffffff',
|
||||
accent: '#dbeafe',
|
||||
@@ -108,7 +108,6 @@ export const DARK: WidgetPalette = {
|
||||
muted: '#a3a3a3',
|
||||
mutedBg: '#262626',
|
||||
border: '#2d2d2d',
|
||||
frame: 'rgba(128, 128, 128, 0.3)',
|
||||
primary: '#fafafa',
|
||||
primaryFg: '#171717',
|
||||
accent: '#1e3a8a',
|
||||
|
||||
Reference in New Issue
Block a user