bar: allow hiding items and reordering (#379)

* feat: reorder bar, no popout yet

* chore: cleanup

* refactor: use DelegateChooser

* feat: popouts

* chore: cleanup

* better popout check + fix async stuff + bar interaction

+ a bunch of other fixes

* fix activewindow and bar vertical padding

* readme: add config opt

* bar: fix top/bottom padding

* bar: better wheel behaviour

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
This commit is contained in:
Laurens Duin 2025-08-13 06:31:48 +02:00 committed by GitHub
parent e34953f595
commit 3710df29f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 381 additions and 320 deletions

View file

@ -198,6 +198,44 @@ All configuration options are in `~/.config/caelestia/shell.json`.
}, },
"bar": { "bar": {
"dragThreshold": 20, "dragThreshold": 20,
"entries": [
{
"id": "logo",
"enabled": true
},
{
"id": "workspaces",
"enabled": true
},
{
"id": "spacer",
"enabled": true
},
{
"id": "activeWindow",
"enabled": true
},
{
"id": "spacer",
"enabled": true
},
{
"id": "tray",
"enabled": true
},
{
"id": "clock",
"enabled": true
},
{
"id": "statusIcons",
"enabled": true
},
{
"id": "power",
"enabled": true
}
],
"persistent": true, "persistent": true,
"showOnHover": true, "showOnHover": true,
"status": { "status": {

View file

@ -8,6 +8,45 @@ JsonObject {
property Status status: Status {} property Status status: Status {}
property Sizes sizes: Sizes {} property Sizes sizes: Sizes {}
property list<var> entries: [
{
id: "logo",
enabled: true
},
{
id: "workspaces",
enabled: true
},
{
id: "spacer",
enabled: true
},
{
id: "activeWindow",
enabled: true
},
{
id: "spacer",
enabled: true
},
{
id: "tray",
enabled: true
},
{
id: "clock",
enabled: true
},
{
id: "statusIcons",
enabled: true
},
{
id: "power",
enabled: true
},
]
component Workspaces: JsonObject { component Workspaces: JsonObject {
property int shown: 5 property int shown: 5
property bool rounded: true property bool rounded: true

View file

@ -1,5 +1,5 @@
import qs.components pragma ComponentBehavior: Bound
import qs.components.controls
import qs.services import qs.services
import qs.config import qs.config
import "popouts" as BarPopouts import "popouts" as BarPopouts
@ -7,170 +7,176 @@ import "components"
import "components/workspaces" import "components/workspaces"
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts
Item { ColumnLayout {
id: root id: root
required property ShellScreen screen required property ShellScreen screen
required property PersistentProperties visibilities required property PersistentProperties visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
readonly property int vPadding: Appearance.padding.large
function checkPopout(y: real): void { function checkPopout(y: real): void {
const spacing = Appearance.spacing.small; const ch = childAt(width / 2, y) as WrappedLoader;
const aw = activeWindow.child; if (!ch) {
const awy = activeWindow.y + aw.y;
const ty = tray.y;
const th = tray.implicitHeight;
const trayItems = tray.items;
// Check status icons hover areas
let statusIconFound = false;
for (const area of statusIconsInner.hoverAreas) {
if (!area.enabled)
continue;
const item = area.item;
const itemY = statusIcons.y + statusIconsInner.y + item.y - spacing / 2;
const itemHeight = item.implicitHeight + spacing;
if (y >= itemY && y <= itemY + itemHeight) {
popouts.currentName = area.name;
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + item.y + item.implicitHeight / 2);
popouts.hasCurrent = true;
statusIconFound = true;
break;
}
}
if (y >= awy && y <= awy + aw.implicitHeight) {
popouts.currentName = "activewindow";
popouts.currentCenter = Qt.binding(() => activeWindow.y + aw.y + aw.implicitHeight / 2);
popouts.hasCurrent = true;
} else if (y > ty && y < ty + th) {
const index = Math.floor(((y - ty) / th) * trayItems.count);
const item = trayItems.itemAt(index);
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => tray.y + item.y + item.implicitHeight / 2);
popouts.hasCurrent = true;
} else if (!statusIconFound) {
popouts.hasCurrent = false; popouts.hasCurrent = false;
return;
}
const id = ch.id;
const top = ch.y;
const item = ch.item;
const itemHeight = item.implicitHeight;
if (id === "statusIcons") {
const items = item.items;
const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y);
if (icon) {
popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y);
popouts.hasCurrent = true;
}
} else if (id === "tray") {
const index = Math.floor(((y - top) / itemHeight) * item.items.count);
const trayItem = item.items.itemAt(index);
if (trayItem) {
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y);
popouts.hasCurrent = true;
}
} else if (id === "activeWindow") {
popouts.currentName = id.toLowerCase();
popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y;
popouts.hasCurrent = true;
} }
} }
anchors.top: parent.top function handleWheel(y: real, angleDelta: point): void {
anchors.bottom: parent.bottom const ch = childAt(width / 2, y) as WrappedLoader;
anchors.left: parent.left if (ch?.id === "workspaces") {
// Workspace scroll
implicitWidth: child.implicitWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) * 2
Item {
id: child
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.max(osIcon.implicitWidth, workspaces.implicitWidth, activeWindow.implicitWidth, tray.implicitWidth, clock.implicitWidth, statusIcons.implicitWidth, power.implicitWidth)
OsIcon {
id: osIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Appearance.padding.large
}
StyledRect {
id: workspaces
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: osIcon.bottom
anchors.topMargin: Appearance.spacing.normal
radius: Appearance.rounding.full
color: Colours.tPalette.m3surfaceContainer
implicitWidth: workspacesInner.implicitWidth + Appearance.padding.small * 2
implicitHeight: workspacesInner.implicitHeight + Appearance.padding.small * 2
CustomMouseArea {
anchors.fill: parent
anchors.leftMargin: -Math.max(Appearance.padding.smaller, Config.border.thickness)
anchors.rightMargin: -Math.max(Appearance.padding.smaller, Config.border.thickness)
function onWheel(event: WheelEvent): void {
const activeWs = Hyprland.activeToplevel?.workspace?.name; const activeWs = Hyprland.activeToplevel?.workspace?.name;
if (activeWs?.startsWith("special:")) if (activeWs?.startsWith("special:"))
Hyprland.dispatch(`togglespecialworkspace ${activeWs.slice(8)}`); Hyprland.dispatch(`togglespecialworkspace ${activeWs.slice(8)}`);
else if (event.angleDelta.y < 0 || Hyprland.activeWsId > 1) else if (angleDelta.y < 0 || Hyprland.activeWsId > 1)
Hyprland.dispatch(`workspace r${event.angleDelta.y > 0 ? "-" : "+"}1`); Hyprland.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
} else if (y < screen.height / 2) {
// Volume scroll on top half
if (angleDelta.y > 0)
Audio.incrementVolume();
else if (angleDelta.y < 0)
Audio.decrementVolume();
} else {
// Brightness scroll on bottom half
const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + 0.1);
else if (angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - 0.1);
} }
} }
Workspaces { spacing: Appearance.spacing.normal
id: workspacesInner
anchors.centerIn: parent Repeater {
id: repeater
model: Config.bar.entries
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillHeight: enabled
} }
} }
DelegateChoice {
ActiveWindow { roleValue: "logo"
id: activeWindow delegate: WrappedLoader {
sourceComponent: OsIcon {}
anchors.horizontalCenter: parent.horizontalCenter }
anchors.top: workspaces.bottom }
anchors.bottom: tray.top DelegateChoice {
anchors.margins: Appearance.spacing.large roleValue: "workspaces"
delegate: WrappedLoader {
sourceComponent: Workspaces {}
}
}
DelegateChoice {
roleValue: "activeWindow"
delegate: WrappedLoader {
sourceComponent: ActiveWindow {
bar: root
monitor: Brightness.getMonitorForScreen(root.screen) monitor: Brightness.getMonitorForScreen(root.screen)
} }
Tray {
id: tray
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: clock.top
anchors.bottomMargin: Appearance.spacing.larger
}
Clock {
id: clock
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: statusIcons.top
anchors.bottomMargin: Appearance.spacing.normal
}
StyledRect {
id: statusIcons
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: power.top
anchors.bottomMargin: Appearance.spacing.normal
radius: Appearance.rounding.full
color: Colours.tPalette.m3surfaceContainer
implicitHeight: statusIconsInner.implicitHeight + Appearance.padding.normal * 2
StatusIcons {
id: statusIconsInner
anchors.centerIn: parent
} }
} }
DelegateChoice {
Power { roleValue: "tray"
id: power delegate: WrappedLoader {
sourceComponent: Tray {}
anchors.horizontalCenter: parent.horizontalCenter }
anchors.bottom: parent.bottom }
anchors.bottomMargin: Appearance.padding.large DelegateChoice {
roleValue: "clock"
delegate: WrappedLoader {
sourceComponent: Clock {}
}
}
DelegateChoice {
roleValue: "statusIcons"
delegate: WrappedLoader {
sourceComponent: StatusIcons {}
}
}
DelegateChoice {
roleValue: "power"
delegate: WrappedLoader {
sourceComponent: Power {
visibilities: root.visibilities visibilities: root.visibilities
} }
} }
} }
}
}
component WrappedLoader: Loader {
required property bool enabled
required property string id
required property int index
function findFirstEnabled(): Item {
const count = repeater.count;
for (let i = 0; i < count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
function findLastEnabled(): Item {
for (let i = repeater.count - 1; i >= 0; i--) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
Layout.alignment: Qt.AlignHCenter
Layout.leftMargin: root.padding
Layout.rightMargin: root.padding
// Cursed ahh thing to add padding to first and last enabled components
Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0
Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0
visible: enabled
active: enabled
}
}

View file

@ -14,19 +14,24 @@ Item {
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
readonly property int exclusiveZone: Config.bar.persistent || visibilities.bar ? content.implicitWidth : Config.border.thickness readonly property int exclusiveZone: Config.bar.persistent || visibilities.bar ? content.implicitWidth : Config.border.thickness
readonly property bool shouldBeVisible: Config.bar.persistent || visibilities.bar || isHovered
property bool isHovered property bool isHovered
function checkPopout(y: real): void { function checkPopout(y: real): void {
content.item?.checkPopout(y); content.item?.checkPopout(y);
} }
function handleWheel(y: real, angleDelta: point): void {
content.item?.handleWheel(y, angleDelta);
}
visible: width > Config.border.thickness visible: width > Config.border.thickness
implicitWidth: Config.border.thickness implicitWidth: Config.border.thickness
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
states: State { states: State {
name: "visible" name: "visible"
when: Config.bar.persistent || root.visibilities.bar || root.isHovered when: root.shouldBeVisible
PropertyChanges { PropertyChanges {
root.implicitWidth: content.implicitWidth root.implicitWidth: content.implicitWidth
@ -63,7 +68,7 @@ Item {
Loader { Loader {
id: content id: content
Component.onCompleted: active = Qt.binding(() => Config.bar.persistent || root.visibilities.bar || root.isHovered || root.visible) Component.onCompleted: active = Qt.binding(() => root.shouldBeVisible || root.visible)
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom

View file

@ -9,19 +9,17 @@ import QtQuick
Item { Item {
id: root id: root
required property var bar
required property Brightness.Monitor monitor required property Brightness.Monitor monitor
property color colour: Colours.palette.m3primary property color colour: Colours.palette.m3primary
readonly property Item child: child
implicitWidth: child.implicitWidth readonly property int maxHeight: {
implicitHeight: child.implicitHeight const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer");
const otherHeight = otherModules.reduce((acc, curr) => acc + curr.height, 0);
Item { // Length - 2 cause repeater counts as a child
id: child return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
}
property Item current: text1 property Title current: text1
anchors.centerIn: parent
clip: true clip: true
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
@ -30,11 +28,11 @@ Item {
MaterialIcon { MaterialIcon {
id: icon id: icon
anchors.horizontalCenter: parent.horizontalCenter
animate: true animate: true
text: Icons.getAppCategoryIcon(Hyprland.activeToplevel?.lastIpcObject.class, "desktop_windows") text: Icons.getAppCategoryIcon(Hyprland.activeToplevel?.lastIpcObject.class, "desktop_windows")
color: root.colour color: root.colour
anchors.horizontalCenter: parent.horizontalCenter
} }
Title { Title {
@ -52,22 +50,14 @@ Item {
font.pointSize: Appearance.font.size.smaller font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono font.family: Appearance.font.family.mono
elide: Qt.ElideRight elide: Qt.ElideRight
elideWidth: root.height - icon.height elideWidth: root.maxHeight - icon.height
onTextChanged: { onTextChanged: {
const next = child.current === text1 ? text2 : text1; const next = root.current === text1 ? text2 : text1;
next.text = elidedText; next.text = elidedText;
child.current = next; root.current = next;
}
onElideWidthChanged: child.current.text = elidedText
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
} }
onElideWidthChanged: root.current.text = elidedText
} }
Behavior on implicitHeight { Behavior on implicitHeight {
@ -77,7 +67,6 @@ Item {
easing.bezierCurve: Appearance.anim.curves.emphasized easing.bezierCurve: Appearance.anim.curves.emphasized
} }
} }
}
component Title: StyledText { component Title: StyledText {
id: text id: text
@ -89,7 +78,7 @@ Item {
font.pointSize: metrics.font.pointSize font.pointSize: metrics.font.pointSize
font.family: metrics.font.family font.family: metrics.font.family
color: root.colour color: root.colour
opacity: child.current === this ? 1 : 0 opacity: root.current === this ? 1 : 0
transform: Rotation { transform: Rotation {
angle: 90 angle: 90

View file

@ -2,24 +2,22 @@ import qs.components
import qs.services import qs.services
import qs.config import qs.config
import Quickshell import Quickshell
import QtQuick
MaterialIcon { Item {
id: root id: root
required property PersistentProperties visibilities required property PersistentProperties visibilities
text: "power_settings_new" implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
color: Colours.palette.m3error implicitHeight: icon.implicitHeight
font.bold: true
font.pointSize: Appearance.font.size.normal
StateLayer { StateLayer {
// Cursed workaround to make the height larger than the parent
anchors.fill: undefined anchors.fill: undefined
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: 1 implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
implicitWidth: parent.implicitHeight + Appearance.padding.small * 2
implicitHeight: implicitWidth
radius: Appearance.rounding.full radius: Appearance.rounding.full
@ -27,4 +25,16 @@ MaterialIcon {
root.visibilities.session = !root.visibilities.session; root.visibilities.session = !root.visibilities.session;
} }
} }
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
text: "power_settings_new"
color: Colours.palette.m3error
font.bold: true
font.pointSize: Appearance.font.size.normal
}
} }

View file

@ -10,51 +10,29 @@ import Quickshell.Services.UPower
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
Item { StyledRect {
id: root id: root
property color colour: Colours.palette.m3secondary property color colour: Colours.palette.m3secondary
readonly property alias items: iconColumn
readonly property list<var> hoverAreas: [ color: Colours.tPalette.m3surfaceContainer
{ radius: Appearance.rounding.full
name: "audio",
item: audioIcon,
enabled: Config.bar.status.showAudio
},
{
name: "network",
item: networkIcon,
enabled: Config.bar.status.showNetwork
},
{
name: "bluetooth",
item: bluetoothGroup,
enabled: Config.bar.status.showBluetooth
},
{
name: "battery",
item: batteryIcon,
enabled: Config.bar.status.showBattery
}
]
clip: true clip: true
implicitWidth: iconColumn.implicitWidth implicitWidth: iconColumn.implicitWidth + Appearance.padding.normal * 2
implicitHeight: iconColumn.implicitHeight implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2
ColumnLayout { ColumnLayout {
id: iconColumn id: iconColumn
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
spacing: Appearance.spacing.smaller / 2 spacing: Appearance.spacing.smaller / 2
// Audio icon // Audio icon
Loader { WrappedLoader {
id: audioIcon name: "audio"
asynchronous: true
active: Config.bar.status.showAudio active: Config.bar.status.showAudio
visible: active
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
animate: true animate: true
@ -65,8 +43,6 @@ Item {
// Keyboard layout icon // Keyboard layout icon
Loader { Loader {
id: kbLayout
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
asynchronous: true asynchronous: true
active: Config.bar.status.showKbLayout active: Config.bar.status.showKbLayout
@ -81,12 +57,9 @@ Item {
} }
// Network icon // Network icon
Loader { WrappedLoader {
id: networkIcon name: "network"
asynchronous: true
active: Config.bar.status.showNetwork active: Config.bar.status.showNetwork
visible: active
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
animate: true animate: true
@ -95,13 +68,10 @@ Item {
} }
} }
// Bluetooth section (grouped for hover area) // Bluetooth section
Loader { WrappedLoader {
id: bluetoothGroup name: "bluetooth"
asynchronous: true
active: Config.bar.status.showBluetooth active: Config.bar.status.showBluetooth
visible: active
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Appearance.spacing.smaller / 2 spacing: Appearance.spacing.smaller / 2
@ -157,12 +127,9 @@ Item {
} }
// Battery icon // Battery icon
Loader { WrappedLoader {
id: batteryIcon name: "battery"
asynchronous: true
active: Config.bar.status.showBattery active: Config.bar.status.showBattery
visible: active
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
animate: true animate: true
@ -194,6 +161,14 @@ Item {
Anim {} Anim {}
} }
component WrappedLoader: Loader {
required property string name
Layout.alignment: Qt.AlignHCenter
asynchronous: true
visible: active
}
component Anim: NumberAnimation { component Anim: NumberAnimation {
duration: Appearance.anim.durations.large duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline

View file

@ -2,10 +2,11 @@ pragma ComponentBehavior: Bound
import qs.services import qs.services
import qs.config import qs.config
import qs.components
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
Item { StyledRect {
id: root id: root
readonly property list<Workspace> workspaces: layout.children.filter(c => c.isWorkspace).sort((w1, w2) => w1.ws - w2.ws) readonly property list<Workspace> workspaces: layout.children.filter(c => c.isWorkspace).sort((w1, w2) => w1.ws - w2.ws)
@ -15,8 +16,16 @@ Item {
}, {}) }, {})
readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth + Appearance.padding.small * 2
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight + Appearance.padding.small * 2
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.full
Item {
id: inner
anchors.fill: parent
anchors.margins: Appearance.padding.small
ColumnLayout { ColumnLayout {
id: layout id: layout
@ -56,8 +65,8 @@ Item {
sourceComponent: ActiveIndicator { sourceComponent: ActiveIndicator {
workspaces: root.workspaces workspaces: root.workspaces
mask: layout mask: layout
maskWidth: root.width maskWidth: inner.width
maskHeight: root.height maskHeight: inner.height
groupOffset: root.groupOffset groupOffset: root.groupOffset
} }
} }
@ -72,3 +81,4 @@ Item {
} }
} }
} }
}

View file

@ -133,7 +133,6 @@ Variants {
visibilities: visibilities visibilities: visibilities
bar: bar bar: bar
} }
}
BarWrapper { BarWrapper {
id: bar id: bar
@ -148,3 +147,4 @@ Variants {
} }
} }
} }
}

View file

@ -45,18 +45,7 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (event.x < bar.implicitWidth) { if (event.x < bar.implicitWidth) {
if (event.y < screen.height / 2) { bar.handleWheel(event.y, event.angleDelta);
if (event.angleDelta.y > 0)
Audio.incrementVolume();
else if (event.angleDelta.y < 0)
Audio.decrementVolume();
} else {
const monitor = Brightness.getMonitorForScreen(screen);
if (event.angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + 0.1);
else if (event.angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - 0.1);
}
} }
} }

View file

@ -92,7 +92,7 @@ Item {
const diff = root.height - Math.floor(off + nonAnimHeight); const diff = root.height - Math.floor(off + nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return off; return Math.max(off, 0);
} }
} }