mdview2: расщепление монолита на модули (6 файлов, размер-нейтрально)
Вынос подсистем из mdview2.c в отдельные C-модули для читаемости и навигации. mdview2.c: 2740 → 1062 строк (-61%); ядро теперь чистая инфраструктура (cache-пул, EMM/fb, загрузка файла, doc-slot, loading-loop, build_doc, main). Модули (через EXTRA_SRCS, общий интерфейс в mdview2.h): - mdview2_help.c — диалог справки F1 - mdview2_table.c — выровненная отрисовка таблиц - mdview2_enc.c — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер - mdview2_view.c — отрисовка области/статус-бара/меню + прокрутка - mdview2_index.c — парсер/индексатор markdown (сердце приложения) (mdview2_raw.c был выделен ранее) Чистый перенос static→extern; приватное состояние подсистем осталось приватным. Размер: exe 28090 → 28112 (+22 Б / +0.08% — codegen-шум на двух сильно связанных модулях enc/index). Сборка чистая, smoke-тест ОК. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* mdview2_view.c — представление: отрисовка области документа из рендер-кэша,
|
||||
* статус-бар, нижнее меню и прокрутка (верт./гориз.).
|
||||
*
|
||||
* Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и
|
||||
* состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
|
||||
* парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша
|
||||
* через win_rest_remap (ремап 8-битных кодировок — в mdview2_enc.c).
|
||||
*/
|
||||
|
||||
#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();
|
||||
}
|
||||
Reference in New Issue
Block a user