Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Move backdrop filter to a canvas based solution #6262

Merged
merged 41 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
27ee7c5
Move backdrop filter to a canvas based solution
germain-gg Jun 24, 2021
652ad36
Backdrop filter compatibility for Firefox and Safari
germain-gg Jun 25, 2021
e628cac
Improve image drawing fill strategy
germain-gg Jun 25, 2021
6d293d1
Merge branch 'develop' into gsouquet/fix-backdrop-filter
germain-gg Jun 25, 2021
31a363d
mock canvas filter polyfill in tests
germain-gg Jun 25, 2021
bf89aa9
Add BackdropPanel copyright header
germain-gg Jun 25, 2021
a17b13f
Add drawable copyright header
germain-gg Jun 25, 2021
533d5ad
Specify return type for drawable promise
germain-gg Jun 25, 2021
5932b93
make backdrop closer to what is happening in production
germain-gg Jun 25, 2021
edae9a4
Merge branch 'develop' into gsouquet/fix-backdrop-filter
germain-gg Jul 6, 2021
7e6d378
Update benchmark action for lockfile diff
germain-gg Jul 6, 2021
dc17bfe
Update benchmark action for lockfile diff
germain-gg Jul 6, 2021
fbac730
Merge branch 'develop' into gsouquet/fix-backdrop-filter
germain-gg Jul 6, 2021
d6c3a22
fix lockfiles
germain-gg Jul 6, 2021
36ba65b
Merge branch 'develop' into gsouquet/fix-backdrop-filter
germain-gg Jul 12, 2021
8f345dc
Rework backdrop to draw one image with two different level of blur
germain-gg Jul 12, 2021
5f9b55e
Merge remote-tracking branch 'origin/develop' into gsouquet/fix-backd…
Palid Aug 13, 2021
7f58a21
Improve BackdropPanel performance by ignoring React
Palid Aug 13, 2021
94a5013
temp
Palid Aug 13, 2021
8bd5441
Merge branch 'develop' into gsouquet/fix-backdrop-filter
Palid Aug 16, 2021
582b5c9
Properly sepearate left column from timeline
Palid Aug 16, 2021
bad37e6
Add missing position:relative;
Palid Aug 16, 2021
a279779
Add proper glass-like look
Palid Aug 16, 2021
bdb5f3b
Refactor GroupFilterPanel to typescript
Palid Aug 17, 2021
a999cad
Properly cache blur effect
Palid Aug 17, 2021
2ee26d0
Make the blur as-close to the css one as possible
Palid Aug 17, 2021
de2eb5b
Satisfy linter
Palid Aug 17, 2021
515fc24
Revert useless change
Palid Aug 17, 2021
78d48b1
Fix weird code style
Palid Aug 17, 2021
eb24204
Fix missing background tonality without avatar
Palid Aug 17, 2021
3fd95ad
Fix missing coma
Palid Aug 17, 2021
a7bda2b
Better blur values for nicer experience
Palid Aug 17, 2021
b1c724a
Fix invalid width for blurred sidebar
Palid Aug 17, 2021
6310949
Fix background blur not being shown on first load
Palid Aug 17, 2021
b5178a3
Fix fonts for inputs being of wrong colors
Palid Aug 17, 2021
e83722e
Fix missing background color in GroupFilterPanel
Aug 18, 2021
455a914
Merge remote-tracking branch 'origin/develop' into gsouquet/fix-backd…
Aug 19, 2021
d65f6dd
Fix bad merge
Aug 19, 2021
468bb5c
Fix mr comments
Aug 19, 2021
fe1d0e6
Remove useless constructor
Aug 19, 2021
fa03b10
Fix missing type for state
Aug 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.29.3",
"context-filter-polyfill": "^0.2.4",
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
Expand Down
2 changes: 1 addition & 1 deletion res/css/structures/_LeftPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ $roomListCollapsedWidth: 68px;
// Create a row-based flexbox for the GroupFilterPanel and the room list
display: flex;
contain: content;
position: relative;

.mx_LeftPanel_GroupFilterPanelContainer {
flex-grow: 0;
Expand All @@ -43,7 +44,6 @@ $roomListCollapsedWidth: 68px;
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel_roomListContainer {
background-color: $roomlist-bg-color;
flex: 1 0 0;
min-width: 0;
// Create another flexbox (this time a column) for the room list components
Expand Down
10 changes: 10 additions & 0 deletions res/css/structures/_MatrixChat.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ limitations under the License.
height: 100%;
}

.mx_BackdropPanel {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
z-index: 0;
opacity: .15;
}

.mx_MatrixToolbar {
order: 1;

Expand Down
1 change: 1 addition & 0 deletions res/css/structures/_SpacePanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ $activeBorderColor: $secondary-fg-color;
background-color: $groupFilterPanel-bg-color;
padding: 0;
margin: 0;
position: relative;

// Create another flexbox so the Panel fills the container
display: flex;
Expand Down
21 changes: 0 additions & 21 deletions res/themes/light/css/_mods.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,6 @@
// set the user avatar (if any) as a background so
// it can be blurred by the tag panel and room list

@supports (backdrop-filter: none) {
.mx_LeftPanel {
background-image: var(--avatar-url, unset);
background-repeat: no-repeat;
background-size: cover;
background-position: left top;
}

.mx_GroupFilterPanel {
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
}

.mx_SpacePanel {
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
}

.mx_LeftPanel .mx_LeftPanel_roomListContainer {
backdrop-filter: blur($roomlist-background-blur-amount);
}
}

.mx_RoomSublist_showNButton {
background-color: transparent !important;
}
Expand Down
71 changes: 71 additions & 0 deletions src/components/structures/BackdropPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { createRef } from "react";
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
import "context-filter-polyfill";

interface IProps {
width?: number;
height?: number;
backgroundImage?: CanvasImageSource;
blur?: string;
}


export default class BackdropPanel extends React.PureComponent<IProps> {
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
private canvasRef: React.RefObject<HTMLCanvasElement> = createRef();
germain-gg marked this conversation as resolved.
Show resolved Hide resolved
private ctx: CanvasRenderingContext2D;

static defaultProps = {
blur: "60px",
}

public componentDidMount() {
this.ctx = this.canvasRef.current.getContext("2d");
}

public componentDidUpdate() {
if (this.props.backgroundImage) {
requestAnimationFrame(this.refreshBackdropImage);
}
}

private refreshBackdropImage = (): void => {
const { width, height, backgroundImage } = this.props;
this.canvasRef.current.width = width;
this.canvasRef.current.height = height;

const imageWidth = (backgroundImage as ImageBitmap).width
|| (backgroundImage as HTMLImageElement).naturalWidth;
const imageHeight = (backgroundImage as ImageBitmap).height
|| (backgroundImage as HTMLImageElement).naturalHeight;

const contentRatio = imageWidth / imageHeight;
const containerRatio = width / height;
let resultHeight;
let resultWidth;
if (contentRatio > containerRatio) {
resultHeight = height;
resultWidth = height * contentRatio;
} else {
resultWidth = width;
resultHeight = width / contentRatio;
}

const x = (width - resultWidth) / 2;
const y = (height - resultHeight) / 2;

this.ctx.filter = `blur(${this.props.blur})`;
this.ctx.drawImage(
backgroundImage,
x,
y,
resultWidth,
resultHeight,
);
}

public render() {
return <canvas
ref={this.canvasRef}
className="mx_BackdropPanel"
/>;
}
}
34 changes: 10 additions & 24 deletions src/components/structures/LeftPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import BackdropPanel from "./BackdropPanel";

interface IProps {
isMinimized: boolean;
resizeNotifier: ResizeNotifier;
backgroundImage?: CanvasImageSource;
}

interface IState {
Expand Down Expand Up @@ -85,16 +85,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {

BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.bgImageWatcherRef = SettingsStore.watchSetting(
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
});
}

public componentDidMount() {
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
// Using the passive option to not block the main thread
Expand All @@ -104,10 +102,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {

public componentWillUnmount() {
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
Expand Down Expand Up @@ -144,23 +140,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
}
};

private onBackgroundImageUpdate = () => {
// Note: we do this in the LeftPanel as it uses this variable most prominently.
const avatarSize = 32; // arbitrary
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
if (settingBgMxc) {
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
}

const avatarUrlProp = `url(${avatarUrl})`;
if (!avatarUrl) {
document.body.style.removeProperty("--avatar-url");
} else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) {
document.body.style.setProperty("--avatar-url", avatarUrlProp);
}
};

private handleStickyHeaders(list: HTMLDivElement) {
if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true;
Expand Down Expand Up @@ -453,8 +432,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
"mx_AutoHideScrollbar",
);

const panelDimensions = UIStore.instance.getElementDimensions("LeftPanel");

return (
<div className={containerClasses} ref={this.ref}>
<BackdropPanel
backgroundImage={this.props.backgroundImage}
width={panelDimensions?.width}
height={panelDimensions?.height}
/>
Palid marked this conversation as resolved.
Show resolved Hide resolved
{leftLeftPanel}
<aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()}
Expand Down
18 changes: 17 additions & 1 deletion src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
import { UPDATE_EVENT } from "../../stores/AsyncStore";


// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
Expand Down Expand Up @@ -120,6 +123,7 @@ interface IState {
usageLimitEventTs?: number;
useCompactLayout: boolean;
activeCalls: Array<MatrixCall>;
backgroundImage?: CanvasImageSource;
}

/**
Expand Down Expand Up @@ -198,6 +202,8 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();

OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
}

componentWillUnmount() {
Expand All @@ -206,10 +212,17 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
this.resizer.detach();
}

private refreshBackgroundImage = async (): Promise<void> => {
this.setState({
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
});
}

private onCallsChanged = () => {
this.setState({
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
Expand Down Expand Up @@ -633,10 +646,13 @@ class LoggedInView extends React.Component<IProps, IState> {
>
<ToastContainer />
<div ref={this._resizeContainer} className={bodyClasses}>
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
{ SettingsStore.getValue("feature_spaces")
? <SpacePanel backgroundImage={this.state.backgroundImage} />
: null }
<LeftPanel
isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier}
backgroundImage={this.state.backgroundImage}
/>
<ResizeHandle />
{ pageElement }
Expand Down
25 changes: 23 additions & 2 deletions src/components/views/spaces/SpacePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useRef, useState } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
Expand Down Expand Up @@ -43,6 +43,8 @@ import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import SettingsStore from "../../../settings/SettingsStore";
import BackdropPanel from "../../structures/BackdropPanel";
import UIStore from "../../../stores/UIStore";

interface IButtonProps {
space?: Room;
Expand Down Expand Up @@ -178,7 +180,11 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
</div>;
});

const SpacePanel = () => {
interface IProps {
backgroundImage?: CanvasImageSource;
}

const SpacePanel = (props: IProps) => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
Expand Down Expand Up @@ -261,6 +267,15 @@ const SpacePanel = () => {
openMenu();
};

const ref: React.RefObject<HTMLUListElement> = useRef(null);
useEffect(() => {
UIStore.instance.trackElementDimensions("SpacePanel", ref.current);
return () => {
UIStore.instance.stopTrackingElementDimensions("SpacePanel");
}
}, []);
const panelDimensions = UIStore.instance.getElementDimensions("SpacePanel");

return (
<DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list
Expand All @@ -271,7 +286,13 @@ const SpacePanel = () => {
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler}
ref={ref}
>
<BackdropPanel
backgroundImage={props.backgroundImage}
width={panelDimensions?.width}
height={panelDimensions?.height}
/>
<Droppable droppableId="top-level-spaces">
{(provided, snapshot) => (
<AutoHideScrollbar
Expand Down
20 changes: 19 additions & 1 deletion src/stores/OwnProfileStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { User } from "matrix-js-sdk/src/models/user";
import { throttle } from "lodash";
import { memoize, throttle } from "lodash";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler";
import {mediaFromMxc} from "../customisations/Media";
import SettingsStore from "../settings/SettingsStore";
import { getDrawable } from "../utils/drawable";

interface IState {
displayName?: string;
Expand Down Expand Up @@ -137,6 +139,22 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
};

public async getAvatarBitmap(avatarSize = 32): Promise<CanvasImageSource> {
let avatarUrl = this.getHttpAvatarUrl(avatarSize);
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
if (settingBgMxc) {
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
}

if (avatarUrl) {
return await this.buildBitmap(avatarUrl);
} else {
return null;
}
}

private buildBitmap = memoize(getDrawable);

private onStateEvents = throttle(async (ev: MatrixEvent) => {
const myUserId = MatrixClientPeg.get().getUserId();
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
Expand Down
Loading