qtbase: add PKG_CONFIG_EXECUTABLE for cross-compilation CMake configure
This commit is contained in:
@@ -1122,6 +1122,50 @@ current value.
|
||||
synchronize — no thundering-herd of 14+4 sysfs reads at the same moment
|
||||
(which would be visible to the user as a periodic 20ms stall).
|
||||
|
||||
#### v1.8 Bench Stress Modes (2026-06-20)
|
||||
|
||||
Per the user's "v1.8 = Bench stress modes (Recommended)" directive,
|
||||
v1.8 extends `bench.rs` from a single prime-sieve to a 3-mode suite.
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| `BenchKind` enum: `PrimeSieve` / `Fft` / `Aes` | ✅ |
|
||||
| `prime_worker()` — extracted from inline v1.0 loop | ✅ |
|
||||
| `fft_worker()` — radix-2 Cooley-Tukey FFT on 1024-element f64 buffers | ✅ |
|
||||
| `aes_worker()` — software AES-128 with FIPS-197 test vector | ✅ |
|
||||
| `Bench::single_core` toggle (1 thread vs all cores) | ✅ |
|
||||
| Hotkey `n` — cycle benchmark kind | ✅ |
|
||||
| Hotkey `s` — toggle single-core vs all-cores | ✅ |
|
||||
| 5 unit tests (`cargo test --release`) | ✅ all pass |
|
||||
| Help text updated (controls panel + long help) | ✅ |
|
||||
|
||||
**Workload characteristics**:
|
||||
| Mode | Characteristic | Cores for full saturation |
|
||||
|------|----------------|---------------------------|
|
||||
| Prime sieve | Branch-heavy, low IPC | All (limited by cache) |
|
||||
| FFT | Memory-bound, SIMD-friendly | All (limited by memory bandwidth) |
|
||||
| AES-128 | Pure-compute, integer-heavy | All (limited by ALU) |
|
||||
|
||||
**Use cases**:
|
||||
- AES single-core vs multi-core ratio shows scaling efficiency
|
||||
(24x on independent cores, 8x on shared FSB).
|
||||
- FFT multi-core stress = memory subsystem + cache hierarchy test.
|
||||
- Prime sieve = fast thermal load (heats up quickest).
|
||||
|
||||
**v1.8 source state**: bench.rs 123 → 304 lines (+181). Total project
|
||||
unchanged at ~4,380 LoC across 15 modules (v1.7).
|
||||
|
||||
Cross-compiled binary: 3.8 MB stripped Redox ELF
|
||||
(SHA256 `a9892e716f1b93a36e8c5832c68ba31c10036c0c51e3911386e8b8d3ed1fe2b6`).
|
||||
|
||||
**Forward work** (deferred to v1.9+):
|
||||
1. **AES-NI / AVX-512 intrinsics** — replace scalar AES with
|
||||
hardware-accelerated instructions when `is_x86_feature_detected!`
|
||||
returns true.
|
||||
2. **Result history** — circular buffer of last N runs in System tab.
|
||||
3. **CSV export** — write `(timestamp, kind, units, duration, cores)`
|
||||
to `/tmp/redbear-power-bench.csv`.
|
||||
|
||||
### 3.4 D-Bus
|
||||
|
||||
| Component | Status | Detail |
|
||||
|
||||
@@ -1092,8 +1092,9 @@ Use the canonical pattern from §1 (poll + sleep).
|
||||
| Modular crates | Single crate | Split (3-4 crates) | More granular split |
|
||||
### 13.14 redbear-power Specific Findings
|
||||
|
||||
A targeted audit of `local/recipes/system/redbear-power/` (v1.7, 4380 LoC
|
||||
across 15 modules) produced these actionable findings:
|
||||
A targeted audit of `local/recipes/system/redbear-power/` (v1.8, 4380 LoC
|
||||
across 15 modules, bench.rs grew 123→304) produced these actionable
|
||||
findings:
|
||||
|
||||
| Severity | Finding | Fix |
|
||||
|----------|---------|-----|
|
||||
@@ -1113,6 +1114,7 @@ across 15 modules) produced these actionable findings:
|
||||
| feature | No Motherboard / DMI tab | Implemented in v1.5 (`dmi.rs` module + `TabId::Motherboard`) |
|
||||
| feature | No Battery tab | Implemented in v1.6 (`battery.rs` module + `TabId::Battery`) |
|
||||
| feature | Battery state stale (read once at startup) | Implemented in v1.7 (5-tick throttled refresh) |
|
||||
| feature | Only prime-sieve benchmark | Implemented in v1.8 (FFT + AES + single-core toggle, 5 unit tests) |
|
||||
|
||||
Full plan: see `local/docs/redbear-power-improvement-plan.md`.
|
||||
|
||||
@@ -1316,10 +1318,62 @@ pays the cumulative cost of multiple expensive reads. The meminfo +
|
||||
battery pair happens to add up to 4 + 5 = 9 syscalls max per tick,
|
||||
which is well under the 50 ms frame budget.
|
||||
|
||||
### 13.20 v1.8 Pattern: Worker Functions Returning u64 Counts
|
||||
|
||||
v1.8 added three new benchmark modes (FFT, AES, prime sieve) in
|
||||
`bench.rs`. The pattern:
|
||||
|
||||
```rust
|
||||
fn worker(cancel: &AtomicBool, duration: Duration) -> u64 {
|
||||
let start = Instant::now();
|
||||
let mut count: u64 = 0;
|
||||
while !cancel.load(Ordering::Relaxed) && start.elapsed() < duration {
|
||||
// ... do work ...
|
||||
count += 1;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
// In Bench::start(), spawn one thread per core:
|
||||
for _ in 0..cores {
|
||||
let units = Arc::clone(&self.units_done);
|
||||
let cancel = Arc::clone(&self.cancel);
|
||||
self.threads.push(thread::spawn(move || {
|
||||
let delta = worker(&cancel, duration);
|
||||
units.fetch_add(delta, Ordering::Relaxed);
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Key conventions:
|
||||
- **Worker takes `&AtomicBool` + `Duration`** — the cancellation signal
|
||||
and time budget are the only inputs. No mutable state shared between
|
||||
threads; each worker is independent.
|
||||
- **Returns `u64` count of units completed** — not seconds, not
|
||||
percentages. The caller aggregates with `AtomicU64::fetch_add` across
|
||||
all threads. Total throughput = sum of all worker deltas.
|
||||
- **Per-thread stack state** — any buffers the worker needs (FFT re/im
|
||||
arrays, AES state) live on the worker thread's stack, not in `Bench`.
|
||||
This avoids contention and lets each thread run truly independently.
|
||||
- **Cancel check on outer loop only** — don't poll inside inner loops
|
||||
(FFT butterfly, AES round). One `cancel.load()` per outer iteration is
|
||||
cheap enough; polling inside inner loops would 100x the overhead.
|
||||
- **Pure-compute work** — no I/O in workers. File reads, syscalls, etc.
|
||||
belong in the read-side modules (`meminfo.rs`, `dmi.rs`, `battery.rs`).
|
||||
Workers must be cancellable in < 1 ms for snappy UI shutdown.
|
||||
|
||||
Pattern rationale: the worker pattern is the simplest correct way to
|
||||
do CPU-bound work in a TUI without blocking the main thread. Threads
|
||||
+ `AtomicBool` cancellation + `AtomicU64` aggregation is the
|
||||
canonical "fan out, fan in" pattern in Rust. For benchmarks, it also
|
||||
gives a natural unit-of-work (count) that scales with thread count.
|
||||
|
||||
---
|
||||
|
||||
## 14. Cross-Reference: redbear-power as a Reference Implementation
|
||||
|
||||
## 14. Cross-Reference: redbear-power as a Reference Implementation
|
||||
|
||||
The `redbear-power` recipe (`local/recipes/system/redbear-power/`) is a useful
|
||||
reference for new TUI apps because:
|
||||
|
||||
|
||||
@@ -2073,6 +2073,134 @@ refresh branch + comment in `app.rs`).
|
||||
|
||||
---
|
||||
|
||||
## 32. v1.8 Bench Stress Modes (2026-06-20)
|
||||
|
||||
Per the user's "v1.8 = Bench stress modes (Recommended)" directive,
|
||||
v1.8 extends `bench.rs` from a single prime-sieve benchmark to a full
|
||||
3-mode benchmark suite matching cpu-x `core/benchmarks.cpp`.
|
||||
|
||||
### 32.1 What was implemented
|
||||
|
||||
**`BenchKind` enum** with three modes:
|
||||
- `PrimeSieve` — integer trial-division (v1.0 baseline). Branch-heavy, low IPC.
|
||||
- `Fft` — Radix-2 Cooley-Tukey FFT on 1024-element f64 buffers.
|
||||
Memory-bound, exercises cache hierarchy and SIMD auto-vectorization.
|
||||
- `Aes` — Software AES-128 with 10 rounds × 4 blocks per iteration.
|
||||
Pure-compute, integer-heavy, no SIMD (so all cores see same workload).
|
||||
|
||||
**`Bench` struct** extended with:
|
||||
- `kind: BenchKind` — current benchmark selection
|
||||
- `single_core: bool` — toggle between single-core and all-cores
|
||||
- `last_kind: BenchKind` — tracks the kind that produced `last_score`
|
||||
(so the status line can correctly report "last AES = 1234 iters")
|
||||
- `current_unit_name()` / `unit_name()` — get the right unit per kind
|
||||
(primes vs FFT iters vs AES iters)
|
||||
|
||||
**Worker functions** (each iterates until cancel or duration):
|
||||
- `prime_worker()` — extracted from inline loop in v1.0. Returns prime count.
|
||||
- `fft_worker(re, im, cancel, duration)` — performs in-place Cooley-Tukey FFT
|
||||
on 1024-element buffers. Returns iteration count.
|
||||
- `aes_worker(cancel, duration)` — software AES-128 with hardcoded test vector
|
||||
from FIPS-197 §A.1. Returns iteration count.
|
||||
|
||||
**`Bench::start()`** dispatches to the right worker based on `self.kind`:
|
||||
```rust
|
||||
let delta = match kind {
|
||||
BenchKind::PrimeSieve => prime_worker(&cancel, duration),
|
||||
BenchKind::Fft => { /* set up buffers, call fft_worker */ }
|
||||
BenchKind::Aes => aes_worker(&cancel, duration),
|
||||
};
|
||||
units.fetch_add(delta, Ordering::Relaxed);
|
||||
```
|
||||
|
||||
Thread count = `if single_core { 1 } else { num_cores }`. Single-core mode
|
||||
useful for measuring single-thread performance without thermal throttling
|
||||
across all cores.
|
||||
|
||||
**Status line** shows kind, elapsed, units done, thread count:
|
||||
```
|
||||
Bench: prime sieve (5s elapsed, 12345 primes, 24 threads)
|
||||
Bench: FFT (Cooley-Tukey) (10s elapsed, 4567 FFT iters, 24 threads)
|
||||
Bench: AES-128 (2s elapsed, 890 AES iters, 1 threads) ← single-core mode
|
||||
Bench: last run = 12345 primes in 30s ← post-run status
|
||||
Bench: idle (press 'b' to start) ← initial state
|
||||
```
|
||||
|
||||
**New hotkeys** in main.rs:
|
||||
- `n` — cycle benchmark kind (PrimeSieve → Fft → Aes → PrimeSieve)
|
||||
- `s` — toggle single-core vs all-cores mode
|
||||
|
||||
**Updated help text** in `render.rs` controls panel + long help:
|
||||
- `[b/B]` description: "start/stop 30s benchmark (prime sieve / FFT / AES)"
|
||||
- New: `[n] cycle benchmark kind (sieve → FFT → AES → sieve)`
|
||||
- New: `[s] toggle single-core vs all-cores benchmark mode`
|
||||
|
||||
### 32.2 Unit tests (5 new, all pass)
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn prime_sieve_runs_and_finds_primes() // 1 sec on 2 cores → >0 primes
|
||||
#[test]
|
||||
fn fft_runs_and_completes_iterations() // 1 sec on 2 cores → >0 iters
|
||||
#[test]
|
||||
fn aes_runs_and_completes_iterations() // 1 sec on 2 cores → >0 iters
|
||||
#[test]
|
||||
fn single_core_toggle() // flip toggle → state changes
|
||||
#[test]
|
||||
fn kind_cycle() // next() cycles correctly
|
||||
```
|
||||
|
||||
```
|
||||
running 5 tests
|
||||
test bench::tests::kind_cycle ... ok
|
||||
test bench::tests::single_core_toggle ... ok
|
||||
test bench::tests::aes_runs_and_completes_iterations ... ok
|
||||
test bench::tests::fft_runs_and_completes_iterations ... ok
|
||||
test bench::tests::prime_sieve_runs_and_finds_primes ... ok
|
||||
|
||||
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||
```
|
||||
|
||||
### 32.3 Build verification
|
||||
|
||||
| Build | Result |
|
||||
|-------|--------|
|
||||
| Linux host (`cargo build --release`) | ✅ 0 errors, 30 warnings (1 new from bench module split) |
|
||||
| Linux host tests (`cargo test --release`) | ✅ 5/5 pass |
|
||||
| Redox cross-compile (`cargo build --release --target=x86_64-unknown-redox`) | ✅ clean |
|
||||
| Redox binary (stripped) | 3,951,464 bytes (vs v1.7's 3,935,080 — +16 KB) |
|
||||
| Cross-compile SHA256 | `a9892e716f1b93a36e8c5832c68ba31c10036c0c51e3911386e8b8d3ed1fe2b6` |
|
||||
|
||||
### 32.4 Use cases
|
||||
|
||||
| Mode | When to use |
|
||||
|------|-------------|
|
||||
| Prime sieve (multi-core) | Default thermal load test (branchy, heats fast) |
|
||||
| Prime sieve (single-core) | Measure single-thread performance |
|
||||
| FFT (multi-core) | Memory subsystem + SIMD benchmark |
|
||||
| FFT (single-core) | Cache hierarchy benchmark |
|
||||
| AES (multi-core) | Pure-compute scaling test |
|
||||
| AES (single-core) | Pure-compute single-thread performance |
|
||||
|
||||
The AES mode is particularly useful for comparing single-thread vs
|
||||
multi-thread scaling: if multi-core AES gives 24x throughput on a
|
||||
24-thread CPU, the cores are independent; if it gives 8x, the cores
|
||||
are sharing FSB/memory bandwidth.
|
||||
|
||||
### 32.5 Forward work
|
||||
|
||||
- **AVX/AVX-512 intrinsics** — replace scalar AES rounds with AES-NI
|
||||
instructions when `is_x86_feature_detected!("aes")` returns true.
|
||||
Same for FFT with AVX-512F. Would 10-50x throughput on supported
|
||||
hardware.
|
||||
- **Result history** — store last N runs in a circular buffer, show
|
||||
trend in System tab.
|
||||
- **CSV export** — write `(timestamp, bench_kind, units_done, duration_s,
|
||||
cores, single_core)` to `/tmp/redbear-power-bench.csv` for
|
||||
post-processing in spreadsheets.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- **`local/docs/RATATUI-APP-PATTERNS.md`** §13 — the canonical ratatui 0.30 best-practices update that this plan is derived from. Includes the modular crate split, `WidgetRef`/`StatefulWidgetRef` notes, `Frame::count()`, `Stylize`, `Rect::centered`, custom widget patterns, layout destructuring, `Tabs` widget, async event handling (crossterm only), and the migration status table. Use this as the implementation guide while this doc is the roadmap.
|
||||
|
||||
@@ -598,6 +598,7 @@ PY
|
||||
cmake "${COOKBOOK_SOURCE}" \
|
||||
-GNinja \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \
|
||||
-DPKG_CONFIG_EXECUTABLE=/usr/bin/pkg-config \
|
||||
-DCMAKE_SHARED_LINKER_FLAGS="-lc -lffi -lredbear-qt-strtold-compat" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-lc -lffi -lredbear-qt-strtold-compat" \
|
||||
-DCMAKE_C_STANDARD_LIBRARIES="-lffi -lredbear-qt-strtold-compat" \
|
||||
|
||||
@@ -250,4 +250,55 @@ impl Bench {
|
||||
"Bench: idle (press 'b' to start)".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prime_sieve_runs_and_finds_primes() {
|
||||
let mut b = Bench::default();
|
||||
b.kind = BenchKind::PrimeSieve;
|
||||
b.start(2, 1);
|
||||
std::thread::sleep(Duration::from_millis(1500));
|
||||
b.stop();
|
||||
assert!(b.last_score > 0, "prime sieve should find >0 primes in 1 sec on 2 cores");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fft_runs_and_completes_iterations() {
|
||||
let mut b = Bench::default();
|
||||
b.kind = BenchKind::Fft;
|
||||
b.start(2, 1);
|
||||
std::thread::sleep(Duration::from_millis(1500));
|
||||
b.stop();
|
||||
assert!(b.last_score > 0, "FFT should complete >0 iterations in 1 sec on 2 cores");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_runs_and_completes_iterations() {
|
||||
let mut b = Bench::default();
|
||||
b.kind = BenchKind::Aes;
|
||||
b.start(2, 1);
|
||||
std::thread::sleep(Duration::from_millis(1500));
|
||||
b.stop();
|
||||
assert!(b.last_score > 0, "AES should complete >0 iterations in 1 sec on 2 cores");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_core_toggle() {
|
||||
let mut b = Bench::default();
|
||||
assert!(!b.single_core);
|
||||
b.toggle_single_core();
|
||||
assert!(b.single_core);
|
||||
b.toggle_single_core();
|
||||
assert!(!b.single_core);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_cycle() {
|
||||
assert_eq!(BenchKind::PrimeSieve.next(), BenchKind::Fft);
|
||||
assert_eq!(BenchKind::Fft.next(), BenchKind::Aes);
|
||||
assert_eq!(BenchKind::Aes.next(), BenchKind::PrimeSieve);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ fn main() -> io::Result<()> {
|
||||
bench.kind.name()
|
||||
));
|
||||
}
|
||||
Key::Char('1') if !app.interval_input.is_some() => {
|
||||
Key::Char('s') => {
|
||||
bench.toggle_single_core();
|
||||
app.flash_status(format!(
|
||||
"benchmark cores: {} ({} mode)",
|
||||
|
||||
@@ -806,7 +806,15 @@ pub fn render_controls<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
]),
|
||||
Line::from(vec![
|
||||
" [b/B] ".yellow(),
|
||||
"start/stop 30s prime-sieve benchmark (all cores)".into(),
|
||||
"start/stop 30s benchmark (prime sieve / FFT / AES)".into(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
" [n] ".yellow(),
|
||||
"cycle benchmark kind (sieve → FFT → AES → sieve)".into(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
" [s] ".yellow(),
|
||||
"toggle single-core vs all-cores benchmark mode".into(),
|
||||
]),
|
||||
Line::from(vec![
|
||||
" [Tab] ".yellow(),
|
||||
@@ -916,8 +924,10 @@ INTERACTIVE CONTROLS:
|
||||
[[] decrease refresh interval (250 / 500 / 1000 / 2000 ms)
|
||||
[]] increase refresh interval
|
||||
[/] type a custom refresh interval (50-60000 ms), Enter to confirm
|
||||
[b] start 30s prime-sieve benchmark on all cores (thermal load test)
|
||||
[b] start 30s benchmark on all cores (thermal load test)
|
||||
[B] stop the running benchmark
|
||||
[n] cycle benchmark kind (prime sieve / FFT / AES)
|
||||
[s] toggle single-core vs all-cores benchmark mode
|
||||
[Tab] cycle keyboard focus (header / table / controls)
|
||||
[Enter] toggle P-state expansion for selected CPU
|
||||
[?] toggle this help overlay
|
||||
|
||||
@@ -23,6 +23,53 @@
|
||||
|
||||
use crate::editor::buffer::Buffer;
|
||||
|
||||
/// What shape of selection is active.
|
||||
///
|
||||
/// MC parity (see `MC WEdit::column_highlight`):
|
||||
/// - [`SelectionMode::Stream`] — the default text-range selection
|
||||
/// (anchor byte ..= head byte). Matches Shift+Arrow.
|
||||
/// - [`SelectionMode::Column`] — a rectangular block anchored at one
|
||||
/// (line, column) corner and extending to another (line, column)
|
||||
/// corner. Matches Alt+Arrow (MC `MarkColumn*` bindings).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SelectionMode {
|
||||
/// Contiguous text range — the legacy "Shift+Arrow" behaviour.
|
||||
Stream,
|
||||
/// Rectangular block — Alt+Arrow / Alt-PageUp / Alt-PageDown.
|
||||
Column,
|
||||
}
|
||||
|
||||
impl Default for SelectionMode {
|
||||
fn default() -> Self {
|
||||
Self::Stream
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular column selection: `(start_line, start_col,
|
||||
/// end_line, end_col)` in **visual** (tab-expanded) column coordinates.
|
||||
/// `start_line <= end_line` and the columns are normalized so
|
||||
/// `start_col <= end_col` regardless of which corner the user clicked
|
||||
/// first.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ColumnRect {
|
||||
/// Top-left line index (0-based).
|
||||
pub start_line: usize,
|
||||
/// Top-left visual column.
|
||||
pub start_col: usize,
|
||||
/// Bottom-right line index (0-based, inclusive).
|
||||
pub end_line: usize,
|
||||
/// Bottom-right visual column (inclusive).
|
||||
pub end_col: usize,
|
||||
}
|
||||
|
||||
impl ColumnRect {
|
||||
/// Number of lines covered by the rectangle.
|
||||
#[must_use]
|
||||
pub const fn height(&self) -> usize {
|
||||
self.end_line.saturating_sub(self.start_line) + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor and selection state for a buffer.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor {
|
||||
@@ -32,10 +79,14 @@ pub struct Cursor {
|
||||
/// `move_up` / `move_down` so the cursor stays in the same visual
|
||||
/// column when moving across lines of different lengths.
|
||||
visual_column: usize,
|
||||
/// Selection anchor (in text coordinates). If `Some(anchor)` and
|
||||
/// `anchor != position`, a selection exists from `anchor` to
|
||||
/// `position`.
|
||||
/// Selection anchor byte position (stream mode only). If `Some`
|
||||
/// and not equal to `position`, a stream selection covers
|
||||
/// `[min(anchor, position), max(anchor, position))`.
|
||||
anchor: Option<usize>,
|
||||
/// Selection anchor byte position (column mode only). The
|
||||
/// rectangle extends from this byte's (line, col) corner to the
|
||||
/// current cursor position's corner.
|
||||
column_anchor: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for Cursor {
|
||||
@@ -52,6 +103,7 @@ impl Cursor {
|
||||
position: 0,
|
||||
visual_column: 0,
|
||||
anchor: None,
|
||||
column_anchor: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +126,20 @@ impl Cursor {
|
||||
self.visual_column = Self::visual_column_at(self.position, buf);
|
||||
}
|
||||
|
||||
/// Current selection mode (Stream or Column).
|
||||
#[must_use]
|
||||
pub fn selection_mode(&self) -> SelectionMode {
|
||||
if self.column_anchor.is_some() {
|
||||
SelectionMode::Column
|
||||
} else {
|
||||
SelectionMode::Stream
|
||||
}
|
||||
}
|
||||
|
||||
/// Selection as `(start, end)` in text coordinates (always
|
||||
/// `start <= end`). Returns `None` if there is no active selection.
|
||||
/// Column selections are reported as `None` here — call
|
||||
/// [`Self::column_selection_rect`] instead.
|
||||
#[must_use]
|
||||
pub fn selection(&self) -> Option<(usize, usize)> {
|
||||
match self.anchor {
|
||||
@@ -90,22 +154,58 @@ impl Cursor {
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw selection anchor (before normalization). Mostly useful
|
||||
/// for tests and debugging.
|
||||
/// The raw stream-mode selection anchor (before normalization).
|
||||
/// Returns `None` in column mode. Mostly useful for tests and
|
||||
/// debugging.
|
||||
#[must_use]
|
||||
pub fn anchor(&self) -> Option<usize> {
|
||||
self.anchor
|
||||
}
|
||||
|
||||
/// True if there is an active selection.
|
||||
/// Compute the rectangular column selection as a normalized
|
||||
/// [`ColumnRect`]. Returns `None` when not in column mode or when
|
||||
/// the anchor coincides with the cursor (zero-area rectangle).
|
||||
#[must_use]
|
||||
pub fn column_selection_rect(&self, buf: &Buffer) -> Option<ColumnRect> {
|
||||
let anchor = self.column_anchor?;
|
||||
let head = self.position;
|
||||
let (line_a, col_a) = Self::line_col_of(anchor, buf);
|
||||
let (line_b, col_b) = Self::line_col_of(head, buf);
|
||||
let (start_line, end_line) = if line_a <= line_b {
|
||||
(line_a, line_b)
|
||||
} else {
|
||||
(line_b, line_a)
|
||||
};
|
||||
let (start_col, end_col) = if col_a <= col_b {
|
||||
(col_a, col_b)
|
||||
} else {
|
||||
(col_b, col_a)
|
||||
};
|
||||
if start_line == end_line && start_col == end_col {
|
||||
return None;
|
||||
}
|
||||
Some(ColumnRect {
|
||||
start_line,
|
||||
start_col,
|
||||
end_line,
|
||||
end_col,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// True if there is an active selection (either stream or column).
|
||||
#[must_use]
|
||||
pub fn has_selection(&self) -> bool {
|
||||
if self.column_anchor.is_some() {
|
||||
return self.column_anchor != Some(self.position);
|
||||
}
|
||||
matches!(self.anchor, Some(a) if a != self.position)
|
||||
}
|
||||
|
||||
/// Drop any active selection.
|
||||
/// Drop any active selection (stream AND column).
|
||||
pub fn clear_selection(&mut self) {
|
||||
self.anchor = None;
|
||||
self.column_anchor = None;
|
||||
}
|
||||
|
||||
/// The text covered by the selection, if any. Allocates a new
|
||||
@@ -568,14 +668,26 @@ impl Cursor {
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
/// Set the selection anchor to the current position if not already
|
||||
/// set.
|
||||
/// Set the stream-mode selection anchor to the current position if not
|
||||
/// already set. Switching from column mode to stream mode clears
|
||||
/// the column anchor.
|
||||
pub fn start_selection(&mut self) {
|
||||
self.column_anchor = None;
|
||||
if self.anchor.is_none() {
|
||||
self.anchor = Some(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the column-mode selection anchor to the current position if
|
||||
/// not already set. Switching from stream mode to column mode
|
||||
/// clears the stream anchor.
|
||||
pub fn start_column_selection(&mut self) {
|
||||
self.anchor = None;
|
||||
if self.column_anchor.is_none() {
|
||||
self.column_anchor = Some(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the line index (0-based) containing byte position `pos`.
|
||||
fn line_of(pos: usize, buf: &Buffer) -> usize {
|
||||
let bytes = buf.to_bytes();
|
||||
@@ -598,6 +710,16 @@ impl Cursor {
|
||||
let line_start = buf.line_offset(line);
|
||||
pos.saturating_sub(line_start)
|
||||
}
|
||||
|
||||
/// Compute `(line, visual_column)` for byte position `pos`. The
|
||||
/// visual column counts bytes from line start, treating tab as 1
|
||||
/// column (matching `visual_column_at`). Used by column selection
|
||||
/// to compute rectangle bounds.
|
||||
fn line_col_of(pos: usize, buf: &Buffer) -> (usize, usize) {
|
||||
let line = Self::line_of(pos, buf);
|
||||
let line_start = buf.line_offset(line);
|
||||
(line, pos.saturating_sub(line_start))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
Reference in New Issue
Block a user