Move backdrop filter to a canvas based solution

This commit is contained in:
Germain Souquet 2021-06-24 17:51:11 +01:00
parent f0ad70f0e7
commit 27ee7c5836
9 changed files with 129 additions and 49 deletions

View file

@ -43,7 +43,6 @@ $roomListCollapsedWidth: 68px;
// Note: The 'room list' in this context is actually everything that isn't the tag // Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc // panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel_roomListContainer { .mx_LeftPanel_roomListContainer {
background-color: $roomlist-bg-color;
flex: 1 0 0; flex: 1 0 0;
min-width: 0; min-width: 0;
// Create another flexbox (this time a column) for the room list components // Create another flexbox (this time a column) for the room list components

View file

@ -35,6 +35,16 @@ limitations under the License.
height: 100%; height: 100%;
} }
.mx_BackdropPanel {
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
z-index: 0;
opacity: .15;
}
.mx_MatrixToolbar { .mx_MatrixToolbar {
order: 1; order: 1;

View file

@ -27,6 +27,7 @@ $activeBorderColor: $secondary-fg-color;
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
padding: 0; padding: 0;
margin: 0; margin: 0;
position: relative;
// Create another flexbox so the Panel fills the container // Create another flexbox so the Panel fills the container
display: flex; display: flex;

View file

@ -4,27 +4,6 @@
// set the user avatar (if any) as a background so // set the user avatar (if any) as a background so
// it can be blurred by the tag panel and room list // 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 { .mx_RoomSublist_showNButton {
background-color: transparent !important; background-color: transparent !important;
} }

View file

@ -0,0 +1,50 @@
import React, { createRef } from "react";
interface IProps {
width?: number;
height?: number;
backgroundImage?: ImageBitmap;
blur?: string;
}
export default class BackdropPanel extends React.PureComponent<IProps> {
private canvasRef: React.RefObject<HTMLCanvasElement> = createRef();
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 destinationX = width - backgroundImage.width;
const destinationY = height - backgroundImage.height;
this.ctx.filter = `blur(${this.props.blur})`;
this.ctx.drawImage(
backgroundImage,
Math.min(destinationX, 0),
Math.min(destinationY, 0),
Math.max(width, backgroundImage.width),
Math.max(height, backgroundImage.height),
);
}
public render() {
return <canvas ref={this.canvasRef} className="mx_BackdropPanel" />;
}
}

View file

@ -36,18 +36,18 @@ import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import RoomListNumResults from "../views/rooms/RoomListNumResults"; import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
import BackdropPanel from "./BackdropPanel";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
backgroundImage?: ImageBitmap;
} }
interface IState { interface IState {
@ -85,16 +85,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_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); 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.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
}); });
} }
public componentDidMount() { public componentDidMount() {
UIStore.instance.trackElementDimensions("LeftPanel", this.ref.current);
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
UIStore.instance.on("ListContainer", this.refreshStickyHeaders); UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
// Using the passive option to not block the main thread // Using the passive option to not block the main thread
@ -104,10 +102,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_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); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.stopTrackingElementDimensions("ListContainer");
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
@ -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) { private handleStickyHeaders(list: HTMLDivElement) {
if (this.isDoingStickyHeaders) return; if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true; this.isDoingStickyHeaders = true;
@ -453,8 +432,15 @@ export default class LeftPanel extends React.Component<IProps, IState> {
"mx_AutoHideScrollbar", "mx_AutoHideScrollbar",
); );
const panelDimensions = UIStore.instance.getElementDimensions("LeftPanel");
return ( return (
<div className={containerClasses} ref={this.ref}> <div className={containerClasses} ref={this.ref}>
<BackdropPanel
backgroundImage={this.props.backgroundImage}
width={panelDimensions?.width}
height={panelDimensions?.height}
/>
{leftLeftPanel} {leftLeftPanel}
<aside className="mx_LeftPanel_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} {this.renderHeader()}

View file

@ -59,6 +59,10 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
import CallHandler, { CallHandlerEvent } from '../../CallHandler'; import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall'; import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
import { OwnProfileStore } from '../../stores/OwnProfileStore';
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { mediaFromMxc } from "../../customisations/Media";
// We need to fetch each pinned message individually (if we don't already have it) // 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. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -120,6 +124,7 @@ interface IState {
usageLimitEventTs?: number; usageLimitEventTs?: number;
useCompactLayout: boolean; useCompactLayout: boolean;
activeCalls: Array<MatrixCall>; activeCalls: Array<MatrixCall>;
backgroundImage?: ImageBitmap;
} }
/** /**
@ -198,6 +203,8 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer = this._createResizer(); this.resizer = this._createResizer();
this.resizer.attach(); this.resizer.attach();
this._loadResizerPreferences(); this._loadResizerPreferences();
OwnProfileStore.instance.on(UPDATE_EVENT, this.refreshBackgroundImage);
} }
componentWillUnmount() { componentWillUnmount() {
@ -206,10 +213,17 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
OwnProfileStore.instance.off(UPDATE_EVENT, this.refreshBackgroundImage);
SettingsStore.unwatchSetting(this.compactLayoutWatcherRef); SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
this.resizer.detach(); this.resizer.detach();
} }
private refreshBackgroundImage = async (): Promise<void> => {
this.setState({
backgroundImage: await OwnProfileStore.instance.getAvatarBitmap(),
});
}
private onCallsChanged = () => { private onCallsChanged = () => {
this.setState({ this.setState({
activeCalls: CallHandler.sharedInstance().getAllActiveCalls(), activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
@ -633,10 +647,13 @@ class LoggedInView extends React.Component<IProps, IState> {
> >
<ToastContainer /> <ToastContainer />
<div ref={this._resizeContainer} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null } { SettingsStore.getValue("feature_spaces")
? <SpacePanel backgroundImage={this.state.backgroundImage} />
: null }
<LeftPanel <LeftPanel
isMinimized={this.props.collapseLhs || false} isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
backgroundImage={this.state.backgroundImage}
/> />
<ResizeHandle /> <ResizeHandle />
{ pageElement } { pageElement }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import classNames from "classnames"; import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -43,6 +43,8 @@ import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NotificationState } from "../../../stores/notifications/NotificationState"; import { NotificationState } from "../../../stores/notifications/NotificationState";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import BackdropPanel from "../../structures/BackdropPanel";
import UIStore from "../../../stores/UIStore";
interface IButtonProps { interface IButtonProps {
space?: Room; space?: Room;
@ -178,7 +180,11 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCo
</div>; </div>;
}); });
const SpacePanel = () => { interface IProps {
backgroundImage?: ImageBitmap;
}
const SpacePanel = (props: IProps) => {
// We don't need the handle as we position the menu in a constant location // We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>(); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
@ -261,6 +267,15 @@ const SpacePanel = () => {
openMenu(); 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 ( return (
<DragDropContext onDragEnd={result => { <DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list if (!result.destination) return; // dropped outside the list
@ -271,7 +286,13 @@ const SpacePanel = () => {
<ul <ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })} className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
ref={ref}
> >
<BackdropPanel
backgroundImage={props.backgroundImage}
width={panelDimensions?.width}
height={panelDimensions?.height}
/>
<Droppable droppableId="top-level-spaces"> <Droppable droppableId="top-level-spaces">
{(provided, snapshot) => ( {(provided, snapshot) => (
<AutoHideScrollbar <AutoHideScrollbar

View file

@ -23,6 +23,7 @@ import { throttle } from "lodash";
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import {mediaFromMxc} from "../customisations/Media"; import {mediaFromMxc} from "../customisations/Media";
import SettingsStore from "../settings/SettingsStore";
interface IState { interface IState {
displayName?: string; displayName?: string;
@ -137,6 +138,22 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url}); await this.updateState({displayName: profileInfo.displayname, avatarUrl: profileInfo.avatar_url});
}; };
public async getAvatarBitmap(avatarSize = 32): Promise<ImageBitmap> {
let avatarUrl = this.getHttpAvatarUrl(avatarSize);
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
if (settingBgMxc) {
avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize);
}
if (avatarUrl) {
const response = await fetch(avatarUrl);
const blob = await response.blob();
return await createImageBitmap(blob);
} else {
return null;
}
}
private onStateEvents = throttle(async (ev: MatrixEvent) => { private onStateEvents = throttle(async (ev: MatrixEvent) => {
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) { if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {