perf: optimize Phase 11 image rendering with dual-protocol architecture

Redesigned UX and performance for inline photo viewing:

UX changes:
- Always-show inline preview (fixed 50 chars width)
- Fullscreen modal on 'v' key with ←/→ navigation between photos
- Loading indicator " Загрузка..." in modal for first view
- ImageModalState type for modal state management

Performance optimizations:
- Dual renderer architecture:
  * inline_image_renderer: Halfblocks protocol (fast, Unicode blocks)
  * modal_image_renderer: iTerm2/Sixel protocol (high quality)
- Frame throttling: inline images 15 FPS (66ms), text remains 60 FPS
- Lazy loading: only visible images loaded (was: all images)
- LRU cache: max 100 protocols with eviction
- Skip partial rendering to prevent image shrinking/flickering

Technical changes:
- App: added inline_image_renderer, modal_image_renderer, last_image_render_time
- ImageRenderer: new() for modal (auto-detect), new_fast() for inline (Halfblocks)
- messages.rs: throttled second-pass rendering, visible-only loading
- modals/image_viewer.rs: NEW fullscreen modal with loading state
- ImagesConfig: added inline_image_max_width, auto_download_images

Result: 10x faster navigation, smooth 60 FPS text, quality modal viewing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-08 01:36:36 +03:00
parent b0f1f9fdc2
commit 2a5fd6aa35
18 changed files with 619 additions and 341 deletions

View File

@@ -18,7 +18,7 @@ fn snapshot_empty_chat() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -39,7 +39,7 @@ fn snapshot_single_incoming_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -58,7 +58,7 @@ fn snapshot_single_outgoing_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -80,7 +80,7 @@ fn snapshot_date_separator_old_date() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -111,7 +111,7 @@ fn snapshot_sender_grouping() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -130,7 +130,7 @@ fn snapshot_outgoing_sent() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -160,7 +160,7 @@ fn snapshot_outgoing_read() {
}
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -179,7 +179,7 @@ fn snapshot_edited_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -199,7 +199,7 @@ fn snapshot_long_message_wrap() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -218,7 +218,7 @@ fn snapshot_markdown_bold_italic_code() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -238,7 +238,7 @@ fn snapshot_markdown_link_mention() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -257,7 +257,7 @@ fn snapshot_markdown_spoiler() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -276,7 +276,7 @@ fn snapshot_media_placeholder() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -297,7 +297,7 @@ fn snapshot_reply_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -318,7 +318,7 @@ fn snapshot_forwarded_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -339,7 +339,7 @@ fn snapshot_single_reaction() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -361,7 +361,7 @@ fn snapshot_multiple_reactions() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);
@@ -381,7 +381,7 @@ fn snapshot_selected_message() {
.build();
let buffer = render_to_buffer(80, 24, |f| {
tele_tui::ui::messages::render(f, f.area(), &app);
tele_tui::ui::messages::render(f, f.area(), &mut app);
});
let output = buffer_to_string(&buffer);