/* * mdview2_md.c — представление MD-документа (peer к mdview2_raw.c): отрисовка * области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key(). * Статус-бар, меню и спиннер — в mdview2_status.c (общие для MD/RAW). * * Работает поверх готового рендер-кэша (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 #include /* wrchar/scroll/COLOR */ #include /* 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); /* Строка-обрыв (исчерпан контент-кэш / лимит строк): фикс. текст вживую, * без обращения к контент-кэшу (см. index_lines финализацию). */ if (rec.flags & IF_TRUNC_MSG) { const char *msg; switch (rec.reserved) { case TRUNC_LINES: msg = "*** Line limit reached - document truncated ***"; break; case TRUNC_FILE: msg = "*** File too large - truncated at 256 KB ***"; break; default: msg = "*** Content cache exhausted - document truncated ***"; break; } bios_set_place(row, 0); bios_fillcharattr(' ', ATTR_WARN, SCREEN_W); put_str_attr(2, row, msg, ATTR_WARN); return; } 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)); } } /* ================================================================== * Прокрутка * ================================================================== */ /* Ограничивает 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 md_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 md_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 md_scroll_horizon(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(); } /* Процент прокрутки (0..100) относительно доступного диапазона. */ static uint8_t calc_md_pct(void) { uint16_t total = drawable_lines(); if (total <= VIEW_H) return 100; return (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H)); } /* ---- навигация MD после загрузки (peer к raw_key) ---------------- */ /* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного * цикла). Сама перерисовывает область/статус. Возврат: 1 — обработано, * 0 — не наша клавиша (F1/F8/F10 разбирает main). */ uint8_t md_key(uint8_t scan) { switch (scan) { case KEY_UP: md_scroll_up(1); break; case KEY_DOWN: md_scroll_down(1); break; case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); break; case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); break; case KEY_PGUP: md_scroll_up(VIEW_H); break; case KEY_PGDN: md_scroll_down(VIEW_H); break; case KEY_HOME: if(top_line != 0 || viewport_x != 0 ) { top_line = 0; viewport_x = 0; draw_viewport_from_cache(); } break; case KEY_END: uint16_t new_top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; if(top_line != new_top_line || viewport_x != 0 ) { top_line = new_top_line; viewport_x = 0; draw_viewport_from_cache(); } break; default: return 0; /* не наша клавиша */ } render_percent_progress(calc_md_pct()); render_md_status_numbers(); return 1; }