Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 88 additions & 7 deletions src/renderer/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,61 @@ body.ana-ruler-dragging .ana-ruler-view {
background: rgba(130, 150, 210, 0.55);
}

/* Floating "jump to this expanded run's top/bottom edge" buttons. They sit at
the right edge, vertically centred just left of the minimap, and only appear
while you browse inside an expanded fold whose edge is scrolled off-screen. */
.ana-fold-edges {
position: absolute;
right: 44px;
top: 50%;
z-index: 6;
display: flex;
flex-direction: column;
gap: 7px;
opacity: 0;
pointer-events: none;
transform: translateY(-50%) translateX(6px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.ana-fold-edges.show {
opacity: 1;
pointer-events: auto;
transform: translateY(-50%) translateX(0);
}
.ana-fold-edge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
border: 1px solid var(--border);
border-radius: 9px;
background: color-mix(in srgb, var(--panel) 86%, transparent);
color: var(--accent);
cursor: pointer;
box-shadow: 0 2px 9px rgba(0, 0, 0, 0.28);
-webkit-backdrop-filter: blur(4px);
backdrop-filter: blur(4px);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}
.ana-fold-edge:hover {
background: color-mix(in srgb, var(--accent) 20%, var(--panel));
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
.ana-fold-edge:active {
transform: scale(0.9);
}
.ana-fold-edge:disabled {
opacity: 0.32;
cursor: default;
box-shadow: none;
}
.ana-fold-edge:disabled:hover {
background: color-mix(in srgb, var(--panel) 86%, transparent);
border-color: var(--border);
}

/* ---------- In-viewer search (Ctrl+F) ---------- */
.ana-find {
position: absolute;
Expand Down Expand Up @@ -1412,23 +1467,49 @@ body.ana-ruler-dragging .ana-ruler-view {
background: color-mix(in srgb, var(--accent) 14%, transparent);
}

/* Pulse a just-expanded run's background once, slowly, to point it out. */
@keyframes anaFoldFlash {
/* Breathing-light cue: a single quick, smooth swell of the accent wash that
points out a just-folded/expanded run. The brightness follows a sine profile
(glides up, lingers softly at the peak, eases back down) and uses `linear`
timing so the curve comes purely from these stops — a crisp breath, not a
blink. */
@keyframes anaFoldBreath {
0% {
background: transparent;
}
10% {
background: color-mix(in srgb, var(--accent) 9%, transparent);
}
20% {
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
30% {
background: color-mix(in srgb, var(--accent) 24%, transparent);
}
40% {
background: color-mix(in srgb, var(--accent) 29%, transparent);
}
50% {
background: color-mix(in srgb, var(--accent) 34%, transparent);
background: color-mix(in srgb, var(--accent) 32%, transparent);
}
60% {
background: color-mix(in srgb, var(--accent) 29%, transparent);
}
70% {
background: color-mix(in srgb, var(--accent) 24%, transparent);
}
80% {
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
90% {
background: color-mix(in srgb, var(--accent) 9%, transparent);
}
100% {
background: transparent;
}
}
.ana-line.foldflash {
animation: anaFoldFlash 1.2s ease-in-out;
}
.ana-line.foldflash,
.ana-foldrow.foldflash {
animation: anaFoldFlash 1.2s ease-in-out;
animation: anaFoldBreath 0.9s linear;
}

.hl-error {
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@
"ana.fold": "收折",
"ana.fold.title": "收折未標記的內容,只顯示被標記的行",
"ana.fold.lines": "已收折 {n} 行,點擊展開",
"ana.fold.toBottom": "跳到此段最下練",
"ana.fold.toTop": "跳到此段最上練",
"ana.fold.toBottom": "跳到此段最下緣",
"ana.fold.toTop": "跳到此段最上緣",
"ana.hl.auto": "自動",
"ana.hl.none": "關閉",
"ana.copy": "複製",
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,14 @@ <h2 id="anaViewName" data-i18n="ana.viewer">檢視</h2>
<div class="ana-ruler-ticks" id="anaRulerTicks"></div>
<div class="ana-ruler-view" id="anaRulerView"></div>
</div>
<div class="ana-fold-edges" id="anaFoldEdges">
<button class="ana-fold-edge" id="btnAnaFoldTop" type="button" data-i18n-title="ana.fold.toTop" title="跳到此段最上緣" aria-label="跳到此段最上緣">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
</button>
<button class="ana-fold-edge" id="btnAnaFoldBottom" type="button" data-i18n-title="ana.fold.toBottom" title="跳到此段最下緣" aria-label="跳到此段最下緣">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
</div>
</section>
</div>
Expand Down
110 changes: 104 additions & 6 deletions src/renderer/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,9 @@ const ana = {
// Which collapsed runs are currently expanded, keyed by the run's first line
// index. Reset whenever a file is (re)rendered.
const anaFold = { open: new Set() };
// The expanded run currently under the viewport (start/end line indices) that
// the floating edge-jump buttons target; {-1, -1} when none is in view.
const anaFoldEdge = { start: -1, end: -1 };
const anaNav = { markers: [], targets: [], pos: -1, line: -1, levels: {} };
// Bookmarks: `lines` points at the active file's set; `store` keeps one set per
// file path so bookmarks survive switching files and coming back (per session).
Expand Down Expand Up @@ -1708,6 +1711,7 @@ const anaVirt = {
end: -1,
scheduled: false,
rows: null,
runs: null,
rowCount: 0,
lineRow: null,
lineDisplayRow: null,
Expand All @@ -1721,6 +1725,7 @@ const anaVirt = {
function anaBuildRows() {
const total = anaVirt.total;
const rows = [];
const runs = [];
const lineRow = total ? new Int32Array(total).fill(-1) : new Int32Array(0);
const dispRow = total ? new Int32Array(total) : new Int32Array(0);
const hl = anaVirt.hlSet;
Expand All @@ -1742,6 +1747,7 @@ function anaBuildRows() {
const a = i;
while (i < total && !hl.has(i)) i += 1;
const count = i - a;
runs.push({ start: a, end: i - 1 });
const open = anaFold.open.has(a);
if (open) {
// Expanded: a collapse bar above and below the revealed lines so the
Expand All @@ -1765,6 +1771,7 @@ function anaBuildRows() {
}
anaVirt.rows = rows;
anaVirt.rowCount = rows.length;
anaVirt.runs = runs;
anaVirt.lineRow = lineRow;
anaVirt.lineDisplayRow = dispRow;
}
Expand Down Expand Up @@ -1798,11 +1805,83 @@ function anaFoldApply(anchorLine) {
anaVirtRender();
anaBuildRuler(anaNav.markers, anaVirt.total);
anaUpdateRuler();
anaUpdateFoldEdges();
}

// Flash the lines of a just-expanded run (once, slowly) so the user can see
// which section opened. Best-effort: only the lines currently in the rendered
// window animate; off-screen lines are ignored.
// Line index -> its non-highlighted run {start, end} (binary search over the
// run table cached by anaBuildRows), or null when the line is highlighted.
function anaRunAtLine(idx) {
const runs = anaVirt.runs;
if (!runs || !runs.length || idx == null || idx < 0) return null;
let lo = 0;
let hi = runs.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const r = runs[mid];
if (idx < r.start) hi = mid - 1;
else if (idx > r.end) lo = mid + 1;
else return r;
}
return null;
}

// The expanded (currently open) run that contains line `idx`, or null.
function anaOpenRunAt(idx) {
if (!ana.fold) return null;
const r = anaRunAtLine(idx);
return r && anaFold.open.has(r.start) ? r : null;
}

// The expanded run filling the viewport now: prefer the line at the vertical
// centre (where the buttons sit), fall back to the first visible line.
function anaCurrentOpenRun() {
if (!ana.fold || !anaVirt.rows || !anaVirt.rowCount) return null;
const scroller = document.getElementById('anaScroll');
if (!scroller) return null;
const lineH = anaVirt.lineH || 19;
const rows = anaVirt.rows;
const lineAtPx = (px) => {
const r = Math.max(0, Math.min(rows.length - 1, Math.floor(px / lineH)));
const row = rows[r];
return row.fold ? row.start : row.line;
};
const mid = lineAtPx(scroller.scrollTop + scroller.clientHeight / 2);
return anaOpenRunAt(mid) || anaOpenRunAt(anaFirstVisibleLine());
}

// Show the floating top/bottom edge-jump buttons while an expanded run is in
// view and an edge is off-screen; disable the button whose edge is already
// visible. Remembers the target run for the click handlers.
function anaUpdateFoldEdges() {
const box = document.getElementById('anaFoldEdges');
if (!box) return;
const topBtn = document.getElementById('btnAnaFoldTop');
const bottomBtn = document.getElementById('btnAnaFoldBottom');
const run = anaCurrentOpenRun();
let upUseful = false;
let downUseful = false;
if (run) {
const scroller = document.getElementById('anaScroll');
const lineH = anaVirt.lineH || 19;
const lr = anaVirt.lineRow;
const topRow = lr && run.start < lr.length ? lr[run.start] : -1;
const botRow = lr && run.end < lr.length ? lr[run.end] : -1;
const firstVis = Math.floor(scroller.scrollTop / lineH);
const lastVis = Math.ceil((scroller.scrollTop + scroller.clientHeight) / lineH);
upUseful = topRow >= 0 && topRow < firstVis;
downUseful = botRow >= 0 && botRow > lastVis;
}
const show = upUseful || downUseful;
anaFoldEdge.start = show ? run.start : -1;
anaFoldEdge.end = show ? run.end : -1;
if (topBtn) topBtn.disabled = !upUseful;
if (bottomBtn) bottomBtn.disabled = !downUseful;
box.classList.toggle('show', show);
}

// Breathe the lines of a just-expanded run (a gentle accent pulse) so the user
// can see which section opened. Best-effort: only the lines currently in the
// rendered window animate; off-screen lines are ignored.
function anaFoldFlashRun(start) {
if (start == null || start < 0 || !anaVirt.hlSet) return;
let end = start;
Expand All @@ -1819,10 +1898,10 @@ function anaFoldFlashRun(start) {
const el = anaLineEl(i);
if (el) el.classList.remove('foldflash');
}
}, 1300);
}, 1100);
}

// Flash the collapsed fold bar of a just-folded run, so it's clear where the
// Breathe the collapsed fold bar of a just-folded run, so it's clear where the
// hidden lines went.
function anaFoldFlashBar(start) {
const el = document.getElementById('anaViewContent');
Expand All @@ -1832,7 +1911,7 @@ function anaFoldFlashBar(start) {
bar.classList.remove('foldflash');
void bar.offsetWidth; // restart the animation if the class lingered
bar.classList.add('foldflash');
window.setTimeout(() => bar.classList.remove('foldflash'), 1300);
window.setTimeout(() => bar.classList.remove('foldflash'), 1100);
}

// Toolbar toggle: fold/unfold the non-highlighted runs.
Expand Down Expand Up @@ -2005,6 +2084,7 @@ function anaRenderContent(text, rules) {
anaNavRebuild();
// Rebuild search highlights (e.g. after a highlight-type change) without moving.
if (anaFind.q) anaFindRun(anaFind.q, { keepPos: true, noScroll: true });
anaUpdateFoldEdges();
}


Expand Down Expand Up @@ -2078,6 +2158,7 @@ function anaScrollToLine(idx, opts) {
scroller.scrollTop = Math.max(0, rowIdx * lineH - scroller.clientHeight / 2 + lineH / 2);
anaVirtRender(); // render the window at the new position so the row exists
anaUpdateRuler();
anaUpdateFoldEdges();
if (opts && opts.noFlash) return;
const row = anaLineEl(idx);
if (!row) return;
Expand Down Expand Up @@ -2652,12 +2733,29 @@ function anaUpdateRulerThrottled() {
anaRulerRaf = 0;
anaVirtRender();
anaUpdateRuler();
anaUpdateFoldEdges();
});
}
if (document.getElementById('anaScroll')) {
document.getElementById('anaScroll').addEventListener('scroll', anaUpdateRulerThrottled, { passive: true });
}
window.addEventListener('resize', anaUpdateRulerThrottled);
// Floating edge-jump buttons: hop to the top / bottom edge of the expanded run
// currently in view (its fold bars may be scrolled off-screen).
if (document.getElementById('btnAnaFoldTop')) {
document.getElementById('btnAnaFoldTop').addEventListener('click', () => {
if (anaFoldEdge.start < 0) return;
anaScrollToLine(anaFoldEdge.start);
anaSetCurrentLine(anaFoldEdge.start);
});
}
if (document.getElementById('btnAnaFoldBottom')) {
document.getElementById('btnAnaFoldBottom').addEventListener('click', () => {
if (anaFoldEdge.end < 0) return;
anaScrollToLine(anaFoldEdge.end);
anaSetCurrentLine(anaFoldEdge.end);
});
}
$('#btnAnaPrev').addEventListener('click', () => anaNavGo(-1));
$('#btnAnaNext').addEventListener('click', () => anaNavGo(1));
$('#chipBm').addEventListener('click', () => anaBmGo(1));
Expand Down
Loading