ee87ae1bd9
- mdview2_view.c → mdview2_md.c: модуль уже содержит отрисовку MD-документа из рендер-кэша + скролл + статус-бар, т.е. это парный к mdview2_raw.c вид (MD ↔ RAW). Переименование делает пару явной. - mdview2.c: обновлён устаревший заголовок-комментарий («Фаза 0 — копия mdview.c») на описание ядра + карту модулей; убраны осиротевшие после выноса комментарии; нормализованы баннеры секций (рендер-кэш / примитивы экрана+EMM / загрузка файла / doc-slots / loading-loop / точка входа). - mdview2.h: освежён заголовок-комментарий под текущую раскладку модулей. Только переименование и комментарии/баннеры — поведение и размер не меняются (exe 28115, как до). Сборка чистая. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
249 lines
12 KiB
C
249 lines
12 KiB
C
/*
|
||
* mdview2_md.c — представление MD-документа (peer к mdview2_raw.c): отрисовка
|
||
* области из рендер-кэша, статус-бар, нижнее меню и прокрутка (верт./гориз.).
|
||
*
|
||
* Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и
|
||
* состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
|
||
* парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша
|
||
* через win_rest_remap (ремап 8-битных кодировок — в mdview2_enc.c).
|
||
*
|
||
* Пара видов: mdview2_md.c (MD, из кэша) ↔ mdview2_raw.c (RAW, из fb()).
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include <stdio.h> /* dec8/dec16 */
|
||
#include <conio.h> /* wrchar/gotoxy/cputs/textattr/scroll/COLOR */
|
||
#include <bios/text.h> /* bios_set_place/bios_fillcharattr */
|
||
#include "mdview2.h"
|
||
|
||
/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
|
||
* единого обращения к fb()/исходному файлу. Горизонтальный скролл для
|
||
* nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2
|
||
* байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся
|
||
* строка лежит в одной странице, так что off+vx*2 не пересекает
|
||
* границу страницы); не nowrap-строки игнорируют viewport_x, как и
|
||
* раньше в живом render_line() (там это называлось effective_vx). */
|
||
static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
|
||
{
|
||
cache_rec_t rec;
|
||
cache_dir_get(line_idx, &rec);
|
||
|
||
bios_set_place(row, 0);
|
||
bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W);
|
||
|
||
/* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код,
|
||
* таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая
|
||
* строки короче 80 (короткая строка, ушедшая за левый край, просто
|
||
* показывает маркер '<' на пустом месте). HR и границы fence не имеют
|
||
* IF_HSCROLL и не сдвигаются. */
|
||
uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0;
|
||
|
||
if (rec.len > vx) {
|
||
uint16_t remaining = (uint16_t)(rec.len - vx);
|
||
uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining;
|
||
uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u);
|
||
win_rest_remap(row, w, rec.page, off);
|
||
if (remaining > SCREEN_W) {
|
||
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
|
||
}
|
||
}
|
||
if (vx > 0) {
|
||
wrchar(0, row, '<', ATTR_TRUNC);
|
||
}
|
||
}
|
||
|
||
/* Перерисовывает все VIEW_H строк окна из кэша — основной путь
|
||
* перерисовки видимой области после загрузки файла (Фаза 3-4). */
|
||
void draw_viewport_from_cache(void)
|
||
{
|
||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||
draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
|
||
}
|
||
}
|
||
|
||
/* Число строк, готовых к показу. Во время загрузки последняя эмитированная
|
||
* строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */
|
||
uint16_t drawable_lines(void)
|
||
{
|
||
if (g_loading) return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0;
|
||
return n_lines;
|
||
}
|
||
|
||
/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */
|
||
static uint8_t calc_pct(void)
|
||
{
|
||
uint16_t total = drawable_lines();
|
||
uint8_t pct;
|
||
if (total <= VIEW_H)
|
||
pct = 100;
|
||
else
|
||
pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H));
|
||
return pct;
|
||
}
|
||
|
||
/* Рисует динамическую часть статус-бара без printf.
|
||
* Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */
|
||
void render_status_numbers(void)
|
||
{
|
||
uint16_t total = drawable_lines();
|
||
uint16_t last = top_line + VIEW_H;
|
||
if (last > total) last = total;
|
||
uint8_t pct = calc_pct();
|
||
|
||
textattr(ATTR_BAR);
|
||
bios_set_place(0, 45);
|
||
bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45));
|
||
|
||
wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
|
||
gotoxy(47, 0);
|
||
cputs("L ");
|
||
dec16(top_line + 1);
|
||
cputs("-");
|
||
dec16(last);
|
||
cputs(" / ");
|
||
dec16(total);
|
||
if (g_loading) cputs("..."); /* ещё грузится */
|
||
|
||
wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
|
||
gotoxy( 73, 0);
|
||
dec8(pct);
|
||
cputs("%");
|
||
}
|
||
|
||
/* Полностью перерисовывает статус-бар (заголовок + числа). */
|
||
void render_full_status(void)
|
||
{
|
||
|
||
fill_row(0, ATTR_BAR);
|
||
/* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */
|
||
put_str_attr(1, 0, "MDVIEW", ATTR_BAR);
|
||
put_str_attr(10, 0, filename, ATTR_BAR);
|
||
put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */
|
||
render_status_numbers();
|
||
}
|
||
|
||
/* Обновляет только изменяемую числовую часть статус-бара. */
|
||
void render_updated_status(void)
|
||
{
|
||
render_status_numbers();
|
||
}
|
||
|
||
/* Отрисовывает нижнюю строку меню. */
|
||
void render_menu(void)
|
||
{
|
||
/* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль
|
||
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
|
||
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст —
|
||
* только у задействованных и доступных сейчас. */
|
||
fill_row(MENU_ROW, ATTR_MENU_T);
|
||
|
||
char num[3]; num[2] = 0;
|
||
for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */
|
||
num[0] = ' '; num[1] = (char)('1' + i);
|
||
put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K);
|
||
}
|
||
put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */
|
||
|
||
put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T);
|
||
#if WITH_RAW
|
||
if (g_ready) /* RAW доступен после постройки документа */
|
||
put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T);
|
||
if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */
|
||
put_str_attr(2 * 8 + 2, MENU_ROW,
|
||
(g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T);
|
||
#endif
|
||
if (g_f8_enabled) /* смена кодировки сейчас возможна */
|
||
put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T);
|
||
put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T);
|
||
}
|
||
|
||
/* ==================================================================
|
||
* Прокрутка
|
||
* ================================================================== */
|
||
|
||
/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */
|
||
void clamp_top(void)
|
||
{
|
||
if (n_lines <= VIEW_H) {
|
||
top_line = 0;
|
||
} else if (top_line > n_lines - VIEW_H) {
|
||
top_line = (uint16_t)(n_lines - VIEW_H);
|
||
}
|
||
}
|
||
|
||
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
|
||
void scroll_up(uint16_t n)
|
||
{
|
||
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
|
||
if( new_top_line != top_line) {
|
||
top_line = new_top_line;
|
||
if (n == 1) {
|
||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0);
|
||
draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW));
|
||
} else {
|
||
draw_viewport_from_cache();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
|
||
void scroll_down(uint16_t n)
|
||
{
|
||
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
|
||
(uint16_t)(top_line + n) : n_lines - VIEW_H;
|
||
if( new_top_line != top_line) {
|
||
top_line = new_top_line;
|
||
if (n == 1) {
|
||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0);
|
||
draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
|
||
} else {
|
||
clamp_top();
|
||
draw_viewport_from_cache();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
|
||
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране.
|
||
* Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже
|
||
* посчитанная реальная длина в ячейках после рендера), без единого
|
||
* обращения к fb()/seg_off()/исходному файлу. */
|
||
void scroll_h(int8_t delta)
|
||
{
|
||
uint16_t maxw = 0;
|
||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||
uint16_t li = (uint16_t)(top_line + i);
|
||
if (li >= n_lines) break;
|
||
cache_rec_t rec;
|
||
cache_dir_get(li, &rec);
|
||
if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */
|
||
if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */
|
||
}
|
||
if (maxw == 0) return; /* нет строк шире экрана */
|
||
|
||
/* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */
|
||
uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
|
||
if (over > 248u) over = 248u;
|
||
uint8_t max_vx = (uint8_t)over;
|
||
|
||
int16_t nx = (int16_t)viewport_x + delta;
|
||
if (nx < 0) nx = 0;
|
||
if (nx > (int16_t)max_vx) nx = max_vx;
|
||
|
||
/* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже
|
||
* сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для
|
||
* `if (new_vx != viewport_x) viewport_x = new_vx;` такой код:
|
||
* ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a
|
||
* — т.е. для записи переиспользует регистр A, испорченный вычитанием в
|
||
* сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx).
|
||
* (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно
|
||
* наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем
|
||
* портящий sub с пути записи; сравнение ниже только решает, перерисовывать
|
||
* ли экран. */
|
||
uint8_t new_vx = (uint8_t)nx;
|
||
uint8_t old_vx = viewport_x;
|
||
viewport_x = new_vx;
|
||
if (new_vx != old_vx)
|
||
draw_viewport_from_cache();
|
||
}
|