NativeWindow3.tsx 10.5 Ko
Télécharger le fichier
import { CSSProperties, FC, ReactNode, useLayoutEffect, useRef, useState } from 'react';
import { NativeText } from '../../../common/native-text/NativeText';
import ubuntuAtlas from '../../../assets/mod-tools/skins/habbo-ubuntu-skin.png';
import blueAtlas from '../../../assets/mod-tools/skins/habbo-blue-skin.png';
import closeXml from '../../../assets/mod-tools/skins/skin-button_close_3.xml?raw';
import frameXml from '../../../assets/mod-tools/skins/skin-frame_3.xml?raw';
import thickXml from '../../../assets/mod-tools/skins/skin-button_shiny_thick.xml?raw';
import shinyXml from '../../../assets/mod-tools/skins/skin-button_shiny.xml?raw';
import '../../../css/mod-tools/NativeWindow3.css';
import { parseNativeSkin } from './NativeSkin';
import { NativeSkinView } from './NativeSkinView';
import { BaseButton } from '../../../common/BaseButton';
// Window-manager style 3 (the Ubuntu skin): frame_3 coloured 0xff418db0, close button, shiny thick button, Ubuntu text styles.
const SKINS = { frame: parseNativeSkin(frameXml), close: parseNativeSkin(closeXml), thick: parseNativeSkin(thickXml), shiny: parseNativeSkin(shinyXml) };
export const FRAME3_COLOR = 0x418db0;
/** The title bar of frame_3 after the frame colour multiplies the skin (the title text sits on it). */
export const FRAME3_TITLE_COLOR = 0x377998;
const rect = (x: number, y: number, width: number, height: number): CSSProperties => ({ left: x, top: y, width, height });
/** The title bar under another frame colour: the skin's title grey multiplied by that colour, scaled from the default pair. */
const titleColorFor = (color: number): number =>
[16, 8, 0].reduce((value, shift) => {
const base = ((FRAME3_TITLE_COLOR >> shift) & 0xff) / ((FRAME3_COLOR >> shift) & 0xff);
return value | (Math.min(255, Math.round(((color >> shift) & 0xff) * base)) << shift);
}, 0);
interface Native3FrameProps {
width: number;
height: number;
title: string;
onClose: () => void;
children?: ReactNode;
/** The frame colour of the layout (default 0xff418db0). */
color?: number;
}
/** Frame style 3: the 9-slice frame_3 skin, a 19x20 close button at (width - 29, 8) and the content area at the frame margins (6, 25). */
export const Native3Frame: FC<Native3FrameProps> = ({ width, height, title, onClose, children, color = FRAME3_COLOR }) => {
const [closeState, setCloseState] = useState<'default' | 'hovering' | 'pressed'>('default');
return (
<section aria-label={title} className="native3-frame" role="dialog" style={{ width, height }}>
<NativeSkinView atlas={ubuntuAtlas} color={color} height={height} layout="frame_3" skin={SKINS.frame} width={width} />
<div className="native3-title" style={{ left: 0, top: 4, width, height: 27 }}>
<NativeText background={color === FRAME3_COLOR ? FRAME3_TITLE_COLOR : titleColorFor(color)} overrides={{ color: 0xffffff }} text={title} textStyle="u_frame_title" />
</div>
<BaseButton
aria-label="Close"
className="native3-close"
type="button"
style={rect(width - 29, 8, 19, 20)}
onClick={onClose}
onPointerDown={() => setCloseState('pressed')}
onPointerEnter={() => setCloseState('hovering')}
onPointerLeave={() => setCloseState('default')}
onPointerUp={() => setCloseState('hovering')}
>
<NativeSkinView atlas={ubuntuAtlas} height={20} layout="button_close_3" skin={SKINS.close} state={closeState} width={19} />
</BaseButton>
<div className="native3-client" style={{ left: 6, top: 25 }}>
{children}
</div>
</section>
);
};
// The label is set over the bands of the shiny face (rows inside the 24px button, per state), each band against its own colour.
const FACE_BANDS: Record<string, { from: number; to: number; color: number }[]> = {
default: [{ from: 0, to: 4, color: 0xd9d9d9 }, { from: 4, to: 12, color: 0xf3f3f3 }, { from: 12, to: 24, color: 0xd9d9d9 }],
hovering: [{ from: 0, to: 12, color: 0xffffff }, { from: 12, to: 24, color: 0xebebeb }],
pressed: [{ from: 0, to: 4, color: 0xa3a3a3 }, { from: 4, to: 12, color: 0xe1e1e1 }, { from: 12, to: 24, color: 0xa3a3a3 }]
};
interface Native3ThickButtonProps {
x: number;
y: number;
width: number;
height: number;
label: string;
onClick?: () => void;
}
/** `button_thick` of style 3 (shiny thick): the label is Ubuntu bold, centred on whole pixels (measured against the native Ok button: 1px right, 3px down of the centred field). */
export const Native3ThickButton: FC<Native3ThickButtonProps> = ({ x, y, width, height, label, onClick }) => {
const [over, setOver] = useState(false);
const [down, setDown] = useState(false);
const state = down ? 'pressed' : over ? 'hovering' : 'default';
const labelRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ left: 0, top: 0 });
useLayoutEffect(() => {
const element = labelRef.current;
if (!element) return;
const place = () => setOffset({ left: Math.floor((width - element.offsetWidth) / 2) + 1, top: Math.floor((height - element.offsetHeight) / 2) + 3 });
place();
if (typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(place);
observer.observe(element);
return () => observer.disconnect();
}, [label, width, height]);
return (
<BaseButton type="button"
className="native3-box native3-button"
style={rect(x, y, width, height)}
onClick={onClick}
onPointerDown={() => setDown(true)}
onPointerEnter={() => setOver(true)}
onPointerLeave={() => {
setOver(false);
setDown(false);
}}
onPointerUp={() => setDown(false)}
>
<NativeSkinView atlas={blueAtlas} height={height} layout="button_shiny_thick" skin={SKINS.thick} state={state} width={width} />
<div ref={labelRef} className="native3-button__label" style={offset}>
<NativeText background={FACE_BANDS[state][0].color} text={label} textStyle="u_bold" style={{ visibility: 'hidden' }} />
</div>
{FACE_BANDS[state].map((band) => (
<div key={band.from} className="native3-button__band" style={{ top: band.from, height: band.to - band.from, width }}>
<div className="native3-button__label" style={{ left: offset.left, top: offset.top - band.from }}>
<NativeText background={band.color} text={label} textStyle="u_bold" />
</div>
</div>
))}
</BaseButton>
);
};
// The face of the 22px button_shiny per state, sampled from habbo_blue_skin; the disabled face is the translucent grey as it lands on the
// 0xe9e9e1 window body (0xc2c2c0, measured on the AIR pet commands window).
const SHINY_FACE_BANDS: Record<string, { from: number; to: number; color: number }[]> = {
default: [{ from: 0, to: 2, color: 0xd9d9d9 }, { from: 2, to: 11, color: 0xf3f3f3 }, { from: 11, to: 22, color: 0xd9d9d9 }],
hovering: [{ from: 0, to: 11, color: 0xffffff }, { from: 11, to: 18, color: 0xeeeeee }, { from: 18, to: 22, color: 0xffffff }],
pressed: [{ from: 0, to: 2, color: 0xa3a3a3 }, { from: 2, to: 11, color: 0xe1e1e1 }, { from: 11, to: 22, color: 0xa3a3a3 }],
disabled: [{ from: 0, to: 22, color: 0xc2c2c0 }]
};
interface Native3ButtonProps {
x: number;
y: number;
width: number;
label: string;
enabled?: boolean;
onClick?: () => void;
}
/**
* `button` of style 3 (button_shiny, 22px): habbo_window_layout_button_shiny sets the label in button_shiny_regular in a field 2px down and 8px in
* from both sides, centred in it; a label wider than the field starts at its left edge and the button cuts it. A disabled button draws the label
* at half alpha. Each band of the face gets the label drawn against its own colour. Measured on the AIR pet commands window: capitals start 5px
* down, "Libero" 27px in, an overflowing label 11px in.
*/
const LABEL_MARGIN = 8;
const LABEL_TOP = 2;
export const Native3Button: FC<Native3ButtonProps> = ({ x, y, width, label, enabled = true, onClick }) => {
const height = 22;
const [over, setOver] = useState(false);
const [down, setDown] = useState(false);
const state = !enabled ? 'disabled' : down ? 'pressed' : over ? 'hovering' : 'default';
const labelRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState({ left: 0, top: 0 });
useLayoutEffect(() => {
const element = labelRef.current;
if (!element) return;
const place = () => setOffset({ left: LABEL_MARGIN + 1 + Math.max(0, Math.floor((width - 2 * LABEL_MARGIN - element.offsetWidth) / 2)), top: LABEL_TOP });
place();
if (typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(place);
observer.observe(element);
return () => observer.disconnect();
}, [label, width]);
return (
<BaseButton
type="button"
className="native3-box native3-button"
disabled={!enabled}
style={rect(x, y, width, height)}
onClick={onClick}
onPointerDown={() => setDown(true)}
onPointerEnter={() => setOver(true)}
onPointerLeave={() => {
setOver(false);
setDown(false);
}}
onPointerUp={() => setDown(false)}
>
<NativeSkinView atlas={blueAtlas} height={height} layout="button_shiny" skin={SKINS.shiny} state={state} width={width} />
<div ref={labelRef} className="native3-button__label" style={offset}>
<NativeText background={SHINY_FACE_BANDS[state][0].color} text={label} textStyle="button_shiny_regular" style={{ visibility: 'hidden' }} />
</div>
{SHINY_FACE_BANDS[state].map((band) => (
<div key={band.from} className="native3-button__band" style={{ top: band.from, height: band.to - band.from, width }}>
<div className="native3-button__label" style={{ left: offset.left, top: offset.top - band.from, opacity: enabled ? 1 : 0.5 }}>
<NativeText background={band.color} text={label} textStyle="button_shiny_regular" />
</div>
</div>
))}
</BaseButton>
);
};
