bar: clock to bottom above power; drop Archie's ball

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-07 21:15:01 +01:00
parent 2c65cf1138
commit ee9e9ffbc7

View file

@ -1126,8 +1126,7 @@ in
// Archie a black and tan dachshund with a red collar, living // Archie a black and tan dachshund with a red collar, living
// along the bottom of the screen. He trots about, wags, naps in // along the bottom of the screen. He trots about, wags, naps in
// his bed, visits his bowl, and fetches a ball you can pick up // his bed and visits his bowl.
// and fling with the mouse.
// //
// The art is plain text, one character per pixel, so he can be // The art is plain text, one character per pixel, so he can be
// redrawn by editing the picture instead of editing code. He walks // redrawn by editing the picture instead of editing code. He walks
@ -1154,9 +1153,7 @@ in
"N": "#14100f", // nose and eye "N": "#14100f", // nose and eye
"B": "#96602f", // kibble "B": "#96602f", // kibble
"G": "#5a667a", // bed "G": "#5a667a", // bed
"D": "#3a4454", // bed cushion "D": "#3a4454" // bed cushion
"Y": "#c8d84a", // ball
"L": "#e4ee8c" // ball highlight
}) })
// Trotting: front leg forward, hind leg back, tail up. Paws // Trotting: front leg forward, hind leg back, tail up. Paws
@ -1206,52 +1203,6 @@ in
"........TTT..........TTT....................", "........TTT..........TTT....................",
"........TTTTT........TTTTT.................." "........TTTTT........TTTTT.................."
] ]
// Galloping for the ball. A bound, not a trot: reaching, with
// the hind legs thrown back and the front legs stretched out
// ahead then everything gathered under him. Same body and
// tail as walkB; it is entirely in the legs.
readonly property var runA: [
"...............................KKKKKKKK.....",
"..............................KKKKKKKKKK....",
"KKKK..........................KKKKKKKKKKK...",
"KKKK..........................KKKKK.KKKKK...",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.",
".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....",
"...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........",
"...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........",
"...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............",
".....KKKKKKKKKKKKKKKKKKKKKTTTT..............",
"....TTKKKKKKKKKKKKKKKKKKKTTTT...............",
".....TTTTT..............TTTT................",
"....TTTTT................TTTT...............",
"...TTTTT..................TTTT..............",
"..TTTTT....................TTTT.............",
"..TTTTT....................TTTT.............",
".TTTTTT....................TTTTTT..........."
]
readonly property var runB: [
"...............................KKKKKKKK.....",
"..............................KKKKKKKKKK....",
"KKKK..........................KKKKKKKKKKK...",
"KKKK..........................KKKKK.KKKKK...",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKK..TTKKKKK.",
".KKKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTTT.",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTTTTT..",
"..KKKKKKKKKKKKKKKKKKKKKKKKKKRRKKKKTTTTT.....",
"...KKKKKKKKKKKKKKKKKKKKKKKKKTTKKKK..........",
"...KKKKKKKKKKKKKKKKKKKKKKKKKTTTKKK..........",
"...KKKKKKKKKKKKKKKKKKKKKKKKTTTT.............",
".....KKKKKKKKKKKKKKKKKKKKKTTTT..............",
"....TTKKKKKKKKKKKKKKKKKKKTTTT...............",
".........TTTTT........TTTT..................",
"..........TTTTT......TTTT...................",
"...........TTTTT....TTTT....................",
"............TTTTT..TTTT.....................",
"............TTTTT..TTTT.....................",
"............TTTTTTTTTTTT...................."
]
// Standing: walkA's planted legs, walkB's tail. Flipping // Standing: walkA's planted legs, walkB's tail. Flipping
// between this and walkA is the wag. // between this and walkA is the wag.
readonly property var wag: [ readonly property var wag: [
@ -1363,15 +1314,6 @@ in
"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG",
"..GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG.." "..GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG.."
] ]
readonly property var ballArt: [
"..YYY..",
".YLYYY.",
"YLYYYYY",
"YYYYYYY",
"YYYYYYY",
".YYYYY.",
"..YYY.."
]
property var frame: walkA property var frame: walkA
@ -1395,79 +1337,18 @@ in
property int facing: 1 // 1 = right, -1 = left property int facing: 1 // 1 = right, -1 = left
property string mode: "walk" property string mode: "walk"
property real targetX: 0 property real targetX: 0
property string errand: "" // "" | "bowl" | "bed" | "ball" property string errand: "" // "" | "bowl" | "bed"
property int hold: 0 property int hold: 0
// Ticks since he last got up. Past `tired` he takes himself // Ticks since he last got up. Past `tired` he takes himself
// off to bed; the ball resets it, so a throw both wakes him // off to bed.
// and buys him a fresh stretch of being up.
property int awake: 0 property int awake: 0
readonly property int tired: 2700 // 90s on his feet readonly property int tired: 2700 // 90s on his feet
// Legs swap on distance covered, not on a tick count, so the // Legs swap on distance covered, not on a tick count, so the
// gait stays in step whether he is ambling or running for the // gait stays in step. 3 cells is exactly how far the feet
// ball. 3 cells is exactly how far the feet move in the art. // move in the art.
property real stride: 0 property real stride: 0
property bool stepped: false property bool stepped: false
// The ball
readonly property int ballSize: 7 * cell
property real ballX: 0
property real ballY: 0
property real ballVX: 0
property real ballVY: 0
property bool ballHeld: false // in your hand
property bool ballCarried: false // in his mouth
property bool ballThrown: false // he only fetches a fresh throw
property real lastBX: 0
property real lastBY: 0
property int carry: 0
// Where his mouth is, mirrored with him.
readonly property real mouthX: px + (facing > 0 ? 41 : 3) * cell
readonly property real mouthY: floorY - 13 * cell
// Exposed so Bar.qml can punch the ball out of the window's
// input mask without it the whole surface is click-through
// here and you could never grab the thing.
readonly property real hitX: ballX - 6
readonly property real hitY: ballY - 6
readonly property real hitW: primary ? ballSize + 12 : 0
// At rest on the floor after a throw: that is his cue.
readonly property bool ballLoose: ballThrown && !ballHeld && !ballCarried
&& Math.abs(ballVX) < 0.3 && Math.abs(ballVY) < 0.3
&& ballY >= floorY - ballSize - 1
function ballTick() {
if (ballCarried) {
ballX = mouthX - ballSize / 2;
ballY = mouthY - ballSize / 2;
return;
}
if (ballHeld) {
// Velocity comes from how fast you are dragging it, so
// letting go mid-swing throws it.
ballVX = Math.max(-45, Math.min(45, ballX - lastBX));
ballVY = Math.max(-45, Math.min(45, ballY - lastBY));
return;
}
ballVY += 0.55;
ballX += ballVX;
ballY += ballVY;
let ground = floorY - ballSize;
if (ballY >= ground) {
ballY = ground;
if (Math.abs(ballVY) > 1.2) ballVY = -ballVY * 0.55;
else ballVY = 0;
ballVX *= 0.92; // rolling friction
if (Math.abs(ballVX) < 0.08) ballVX = 0;
}
if (ballY < 0) { ballY = 0; ballVY = Math.abs(ballVY) * 0.4; }
let bl = Theme.barWidth;
let br = archie.width - Theme.frameWidth - ballSize;
if (ballX < bl) { ballX = bl; ballVX = -ballVX * 0.6; }
if (ballX > br) { ballX = br; ballVX = -ballVX * 0.6; }
}
// What he does next // What he does next
function wander() { function wander() {
mode = "walk"; mode = "walk";
@ -1475,7 +1356,7 @@ in
// Bed is not a random errand any more, it is what he does // Bed is not a random errand any more, it is what he does
// when he has been up long enough. A dog sleeps most of // when he has been up long enough. A dog sleeps most of
// the day; pottering about non-stop is the odd bit. // the day; pottering about non-stop is the odd bit.
if (awake > tired && !ballCarried) { if (awake > tired) {
errand = "bed"; errand = "bed";
targetX = bedLieX; targetX = bedLieX;
return; return;
@ -1483,9 +1364,8 @@ in
let dBowl = Math.abs(px - bowlStandX); let dBowl = Math.abs(px - bowlStandX);
// Near enough to be worth it, and not already stood there // Near enough to be worth it, and not already stood there
// otherwise he marches half a minute for a snack, or // otherwise he marches half a minute for a snack, or
// rolls "go to the bowl" with his head already in it. No // rolls "go to the bowl" with his head already in it.
// errands while he has the ball; he is busy. if (Math.random() < 0.3 && dBowl > 80 && dBowl < 900) {
if (!ballCarried && Math.random() < 0.3 && dBowl > 80 && dBowl < 900) {
errand = "bowl"; errand = "bowl";
targetX = bowlStandX; targetX = bowlStandX;
return; return;
@ -1500,24 +1380,8 @@ in
px + dir * (150 + Math.random() * 550))); px + dir * (150 + Math.random() * 550)));
} }
function chase() {
mode = "walk";
errand = "ball";
// Line his mouth up with it, approaching from whichever
// side he is already on.
let bc = ballX + ballSize / 2;
targetX = (px + cols * cell / 2 < bc) ? bc - 41 * cell : bc - 3 * cell;
targetX = Math.max(roamL - 60, Math.min(roamR + 60, targetX));
}
function arrive() { function arrive() {
if (errand === "ball") { if (errand === "bowl") {
ballCarried = true;
ballThrown = false;
carry = 400 + Math.random() * 500; // 13-30s of pride
mode = "idle";
hold = 40;
} else if (errand === "bowl") {
mode = "eat"; mode = "eat";
hold = 100 + Math.random() * 80; hold = 100 + Math.random() * 80;
facing = 1; facing = 1;
@ -1541,32 +1405,14 @@ in
if (placed || width <= 0) return; if (placed || width <= 0) return;
placed = true; placed = true;
px = roamL + Math.random() * (roamR - roamL); px = roamL + Math.random() * (roamR - roamL);
ballX = Math.min(roamR, px + 300);
ballY = floorY - ballSize;
wander(); wander();
} }
Timer { Timer {
interval: 33; running: archie.primary; repeat: true interval: 33; running: archie.primary; repeat: true
onTriggered: { onTriggered: {
archie.ballTick();
archie.lastBX = archie.ballX;
archie.lastBY = archie.ballY;
// A thrown ball beats whatever else he was doing,
// sleeping included that is how you wake him.
if (archie.ballLoose && archie.errand !== "ball") {
archie.awake = 0;
archie.chase();
}
archie.awake = archie.mode === "sleep" ? 0 : archie.awake + 1; archie.awake = archie.mode === "sleep" ? 0 : archie.awake + 1;
if (archie.ballCarried && --archie.carry <= 0) {
archie.ballCarried = false;
archie.ballVX = 0;
archie.ballVY = 0;
}
if (archie.mode === "walk") { if (archie.mode === "walk") {
// Arrival first: checking after the step makes him // Arrival first: checking after the step makes him
// stride backwards when he is already there. // stride backwards when he is already there.
@ -1574,21 +1420,15 @@ in
archie.arrive(); archie.arrive();
} else { } else {
let dir = archie.targetX > archie.px ? 1 : -1; let dir = archie.targetX > archie.px ? 1 : -1;
let run = archie.errand === "ball"; let sp = 1.5;
let sp = run ? 3.0 : 1.5;
archie.facing = dir; archie.facing = dir;
archie.px += dir * sp; archie.px += dir * sp;
archie.stride += sp; archie.stride += sp;
// A gallop covers more ground per stride than if (archie.stride >= 3 * archie.cell) {
// a trot, so the frames hold longer even though
// he is moving faster.
if (archie.stride >= (run ? 6 : 3) * archie.cell) {
archie.stride = 0; archie.stride = 0;
archie.stepped = !archie.stepped; archie.stepped = !archie.stepped;
} }
archie.frame = run archie.frame = archie.stepped ? archie.walkB : archie.walkA;
? (archie.stepped ? archie.runB : archie.runA)
: (archie.stepped ? archie.walkB : archie.walkA);
} }
} else { } else {
// Breathing: in for a third of the cycle, then // Breathing: in for a third of the cycle, then
@ -1659,39 +1499,6 @@ in
x: Math.round(archie.bowlX / archie.cell) * archie.cell x: Math.round(archie.bowlX / archie.cell) * archie.cell
y: archie.floorY - height y: archie.floorY - height
} }
Sprite {
art: archie.ballArt; w: 7; h: 7
x: Math.round(archie.ballX / archie.cell) * archie.cell
y: Math.round(archie.ballY / archie.cell) * archie.cell
}
// Grab handle for the ball, a little larger than the ball so
// it is catchable while it rolls.
MouseArea {
x: archie.hitX; y: archie.hitY
width: archie.hitW; height: archie.hitW
cursorShape: Qt.PointingHandCursor
property real grabDX: 0
property real grabDY: 0
onPressed: (m) => {
let p = mapToItem(archie, m.x, m.y);
grabDX = p.x - archie.ballX;
grabDY = p.y - archie.ballY;
archie.ballCarried = false; // you may take it off him
archie.ballHeld = true;
}
onPositionChanged: (m) => {
if (!archie.ballHeld) return;
let p = mapToItem(archie, m.x, m.y);
archie.ballX = p.x - grabDX;
archie.ballY = p.y - grabDY;
}
onReleased: {
archie.ballHeld = false;
archie.ballThrown = true;
}
}
} }
''; '';
}; };
@ -2011,16 +1818,6 @@ in
width: launcherPanel.visible ? launcherPanel.width : 0 width: launcherPanel.visible ? launcherPanel.width : 0
height: launcherPanel.visible ? launcherPanel.height : 0 height: launcherPanel.visible ? launcherPanel.height : 0
} }
${lib.optionalString (!isMacbook) ''
// Archie's ball. The rest of this surface is click-through,
// so without a hole here you could never pick it up.
Region {
x: archieDog.hitX
y: archieDog.hitY
width: archieDog.hitW
height: archieDog.hitW
}
''}
} }
Item { Item {
@ -4198,8 +3995,7 @@ in
precision: SystemClock.Minutes precision: SystemClock.Minutes
} }
// Top cluster: logo, clock, workspaces the three groups // Top cluster: logo, workspaces split by a hairline.
// that belong to "this machine, right now", split by hairlines.
// The server glance is NOT here: it anchors above the bottom // The server glance is NOT here: it anchors above the bottom
// cluster instead, so adding a workspace grows this column // cluster instead, so adding a workspace grows this column
// downward into empty space rather than shoving the stats. // downward into empty space rather than shoving the stats.
@ -4244,58 +4040,6 @@ in
BarSep {} BarSep {}
// Clock HH over mm; "HH:mm" does not fit a 46px column.
Item {
id: clockText
width: Theme.barWidth
height: clockCol.height
property date now: sysClock.date
Column {
id: clockCol
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
SText {
anchors.horizontalCenter: parent.horizontalCenter
text: clockText.now.toLocaleTimeString(Qt.locale(), "HH")
font.pixelSize: 15
font.weight: Font.Medium
}
SText {
anchors.horizontalCenter: parent.horizontalCenter
text: clockText.now.toLocaleTimeString(Qt.locale(), "mm")
font.pixelSize: 15
font.weight: Font.Medium
color: Theme.base04
}
}
// Anchors to the clock like every other dropdown
// anchors to its widget; the clamp in BarDropdown
// pulls it back on screen when it overhangs.
function openCalendar() {
bar.toggleDropdown(calPopup, function() {
let pos = clockText.mapToItem(bar.contentItem, 0, clockText.height / 2);
calPopup.dropdownY = pos.y;
calPopup.resetView();
});
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: clockText.openCalendar()
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== calPopup) clockText.openCalendar();
}
}
}
}
BarSep {}
// Workspace dots: accent pill for the focused workspace, // Workspace dots: accent pill for the focused workspace,
// dim dots otherwise. Stacked vertically now, so the // dim dots otherwise. Stacked vertically now, so the
// focused pill grows along HEIGHT rather than width. // focused pill grows along HEIGHT rather than width.
@ -4537,7 +4281,7 @@ in
} }
} }
// Bottom cluster: status icons, tray, power button. Anchored // Bottom cluster: status icons, tray, clock, power button. Anchored
// to the bottom edge; the server glance sits just above it and // to the bottom edge; the server glance sits just above it and
// the OSD borrows that same slot while it is on screen. // the OSD borrows that same slot while it is on screen.
Column { Column {
@ -5005,6 +4749,56 @@ in
// as three clusters instead of one clump. // as three clusters instead of one clump.
BarSep {} BarSep {}
// Clock HH over mm; "HH:mm" does not fit a 46px column.
Item {
id: clockText
width: Theme.barWidth
height: clockCol.height
property date now: sysClock.date
Column {
id: clockCol
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
SText {
anchors.horizontalCenter: parent.horizontalCenter
text: clockText.now.toLocaleTimeString(Qt.locale(), "HH")
font.pixelSize: 15
font.weight: Font.Medium
}
SText {
anchors.horizontalCenter: parent.horizontalCenter
text: clockText.now.toLocaleTimeString(Qt.locale(), "mm")
font.pixelSize: 15
font.weight: Font.Medium
color: Theme.base04
}
}
// Anchors to the clock like every other dropdown
// anchors to its widget; the clamp in BarDropdown
// pulls it back on screen when it overhangs.
function openCalendar() {
bar.toggleDropdown(calPopup, function() {
let pos = clockText.mapToItem(bar.contentItem, 0, clockText.height / 2);
calPopup.dropdownY = pos.y;
calPopup.resetView();
});
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: clockText.openCalendar()
onEntered: {
if (bar.activeDropdown) {
if (bar.activeDropdown !== calPopup) clockText.openCalendar();
}
}
}
}
// Power menu opener bottom-left corner. Opens the same // Power menu opener bottom-left corner. Opens the same
// session menu as Super+L, which now unfolds from here. // session menu as Super+L, which now unfolds from here.
Item { Item {