shoko 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters::Output::Ui::Components
|
|
5
|
+
# Standard interface contract for all UI components.
|
|
6
|
+
# Defines the required methods and behavior for component lifecycle.
|
|
7
|
+
module ComponentInterface
|
|
8
|
+
# Called once when component is first rendered
|
|
9
|
+
def mount
|
|
10
|
+
raise NotImplementedError, 'Components must implement #mount'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Called when component is removed from the UI
|
|
14
|
+
def unmount
|
|
15
|
+
raise NotImplementedError, 'Components must implement #unmount'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Render this component into the given surface within bounds
|
|
19
|
+
# @param surface [Surface] Terminal surface wrapper
|
|
20
|
+
# @param bounds [Rect] Local bounds for this component
|
|
21
|
+
def render(surface, bounds)
|
|
22
|
+
raise NotImplementedError, 'Components must implement #render'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Handle input key for this component
|
|
26
|
+
# @param key [String] Input key
|
|
27
|
+
# @return [Symbol] :handled or :pass_through
|
|
28
|
+
def handle_input(_key)
|
|
29
|
+
:pass_through
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Component height calculation for layout
|
|
33
|
+
# @param available_height [Integer] Total height available from parent
|
|
34
|
+
# @return [Integer, Symbol] Height requirement
|
|
35
|
+
def preferred_height(_available_height)
|
|
36
|
+
:flexible
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Component width calculation for layout
|
|
40
|
+
# @param available_width [Integer] Total width available from parent
|
|
41
|
+
# @return [Integer, Symbol] Width requirement
|
|
42
|
+
def preferred_width(_available_width)
|
|
43
|
+
:flexible
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Check if component needs re-rendering
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def needs_update?
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Mark component as updated (no longer needs re-render)
|
|
53
|
+
def mark_updated
|
|
54
|
+
# Default implementation - override if needed
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Validate that a class properly implements the interface
|
|
58
|
+
def self.validate_implementation!(klass)
|
|
59
|
+
required_methods = %i[mount unmount render]
|
|
60
|
+
missing = required_methods.reject { |method| klass.method_defined?(method) }
|
|
61
|
+
|
|
62
|
+
return if missing.empty?
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "#{klass} missing required methods: #{missing.join(', ')}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Helper method to ensure a component follows the interface
|
|
68
|
+
def self.included(base)
|
|
69
|
+
base.extend(ClassMethods)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Class-level helpers mixed into components implementing the interface.
|
|
73
|
+
module ClassMethods
|
|
74
|
+
def validate_interface!
|
|
75
|
+
ComponentInterface.validate_implementation!(self)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative 'surface'
|
|
5
|
+
require_relative 'reading/view_renderer_factory'
|
|
6
|
+
require_relative 'reading/help_renderer'
|
|
7
|
+
|
|
8
|
+
module Shoko
|
|
9
|
+
module Adapters::Output::Ui::Components
|
|
10
|
+
# ContentComponent coordinates the main reading content area.
|
|
11
|
+
# It switches between help and the active view renderer based on state.
|
|
12
|
+
class ContentComponent < BaseComponent
|
|
13
|
+
def initialize(controller)
|
|
14
|
+
super(controller&.dependencies) # Initialize BaseComponent with dependencies
|
|
15
|
+
@controller = controller
|
|
16
|
+
@view_renderer = nil
|
|
17
|
+
deps = controller&.dependencies
|
|
18
|
+
@help_renderer = Reading::HelpRenderer.new(deps)
|
|
19
|
+
|
|
20
|
+
state = @controller.state
|
|
21
|
+
# Observe core fields that affect content rendering via StateStore paths
|
|
22
|
+
state.add_observer(self, %i[reader current_chapter], %i[reader left_page], %i[reader right_page],
|
|
23
|
+
%i[reader single_page], %i[reader current_page_index], %i[reader mode], %i[config view_mode])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Observer callback triggered by ObserverStateStore
|
|
27
|
+
def state_changed(path, old_value, new_value)
|
|
28
|
+
# Reset renderer for mode changes or view mode changes
|
|
29
|
+
@view_renderer = nil if [%i[reader mode], %i[config view_mode]].include?(path)
|
|
30
|
+
|
|
31
|
+
# Call parent invalidate to properly trigger re-rendering
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fill remaining space after fixed components
|
|
36
|
+
def preferred_height(_available_height)
|
|
37
|
+
:fill
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def do_render(surface, bounds)
|
|
41
|
+
state = @controller.state
|
|
42
|
+
|
|
43
|
+
case state.get(%i[reader mode])
|
|
44
|
+
when :help
|
|
45
|
+
@help_renderer.render(surface, bounds)
|
|
46
|
+
else
|
|
47
|
+
view_renderer.render(surface, bounds)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def view_renderer
|
|
54
|
+
return @view_renderer if @view_renderer
|
|
55
|
+
|
|
56
|
+
@view_renderer = Reading::ViewRendererFactory.create(@controller.state,
|
|
57
|
+
@controller.dependencies)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative '../../terminal/text_metrics.rb'
|
|
5
|
+
require_relative '../../../../core/models/selection_anchor.rb'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters::Output::Ui::Components
|
|
9
|
+
# Enhanced popup menu that uses the coordinate service for consistent positioning
|
|
10
|
+
# and integrates with the clipboard service for reliable copy functionality.
|
|
11
|
+
class EnhancedPopupMenu < BaseComponent
|
|
12
|
+
include Adapters::Output::Ui::Constants::UI
|
|
13
|
+
|
|
14
|
+
attr_reader :visible, :selected_index, :x, :y, :width, :height
|
|
15
|
+
|
|
16
|
+
def initialize(selection_range, available_actions = nil, coordinate_service = nil,
|
|
17
|
+
clipboard_service = nil, rendered_lines = nil)
|
|
18
|
+
super()
|
|
19
|
+
@coordinate_service = coordinate_service
|
|
20
|
+
@clipboard_service = clipboard_service
|
|
21
|
+
@rendered_lines = rendered_lines || {}
|
|
22
|
+
|
|
23
|
+
@selection_range = @coordinate_service.normalize_selection_range(selection_range, @rendered_lines)
|
|
24
|
+
|
|
25
|
+
unless @selection_range
|
|
26
|
+
@visible = false
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@available_actions = available_actions || default_actions
|
|
31
|
+
@items = @available_actions.map { |action| action[:label] }
|
|
32
|
+
@selected_index = 0
|
|
33
|
+
@visible = true
|
|
34
|
+
@width = calculate_width
|
|
35
|
+
@height = @items.length
|
|
36
|
+
|
|
37
|
+
# Ensure we have at least one item before proceeding
|
|
38
|
+
unless @items.any?
|
|
39
|
+
@visible = false
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Calculate optimal position using coordinate service
|
|
44
|
+
position = popup_anchor_position(@selection_range[:end])
|
|
45
|
+
@x = position[:x]
|
|
46
|
+
@y = position[:y]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def do_render(surface, bounds)
|
|
50
|
+
return unless @visible
|
|
51
|
+
|
|
52
|
+
@items.each_with_index do |item, i|
|
|
53
|
+
render_menu_item(surface, bounds, item, i)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def handle_key(key)
|
|
58
|
+
return nil unless @visible
|
|
59
|
+
|
|
60
|
+
if Adapters::Input::KeyDefinitions::NAVIGATION[:up].include?(key)
|
|
61
|
+
move_selection(-1)
|
|
62
|
+
{ type: :selection_change }
|
|
63
|
+
elsif Adapters::Input::KeyDefinitions::NAVIGATION[:down].include?(key)
|
|
64
|
+
move_selection(1)
|
|
65
|
+
{ type: :selection_change }
|
|
66
|
+
elsif Adapters::Input::KeyDefinitions::ACTIONS[:confirm].include?(key)
|
|
67
|
+
execute_selected_action
|
|
68
|
+
elsif Adapters::Input::KeyDefinitions::ACTIONS[:cancel].include?(key)
|
|
69
|
+
{ type: :cancel }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Align with ComponentInterface naming; delegate to existing logic
|
|
74
|
+
def handle_input(key)
|
|
75
|
+
handle_key(key)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_click(click_x, click_y)
|
|
79
|
+
return nil unless @visible && contains?(click_x, click_y)
|
|
80
|
+
|
|
81
|
+
clicked_index = click_y - @y
|
|
82
|
+
return nil unless clicked_index >= 0 && clicked_index < @items.length
|
|
83
|
+
|
|
84
|
+
@selected_index = clicked_index
|
|
85
|
+
execute_selected_action
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hide
|
|
89
|
+
@visible = false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def contains?(col, row)
|
|
93
|
+
bounds = Shoko::Adapters::Output::Ui::Components::Rect.new(x: @x, y: @y, width: @width, height: @height)
|
|
94
|
+
@coordinate_service.within_bounds?(col, row, bounds)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def default_actions
|
|
100
|
+
actions = []
|
|
101
|
+
|
|
102
|
+
# Always offer annotation creation
|
|
103
|
+
actions << {
|
|
104
|
+
label: 'Create Annotation',
|
|
105
|
+
action: :create_annotation,
|
|
106
|
+
icon: '',
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Only offer clipboard if available
|
|
110
|
+
if @clipboard_service&.available?
|
|
111
|
+
actions << {
|
|
112
|
+
label: 'Copy to Clipboard',
|
|
113
|
+
action: :copy_to_clipboard,
|
|
114
|
+
icon: '',
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
actions
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def calculate_width
|
|
122
|
+
max_label_width = @items.map { |item| Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(item) }.max || 0
|
|
123
|
+
max_label_width + 6 # Padding for icon and spacing
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def move_selection(direction)
|
|
127
|
+
return if @items.empty?
|
|
128
|
+
|
|
129
|
+
@selected_index = (@selected_index + direction) % @items.length
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def execute_selected_action
|
|
133
|
+
action = @available_actions[@selected_index]
|
|
134
|
+
return { type: :cancel } unless action
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
type: :action,
|
|
138
|
+
action: action[:action],
|
|
139
|
+
data: {
|
|
140
|
+
selection_range: @selection_range,
|
|
141
|
+
action_config: action,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def popup_anchor_position(anchor_hash)
|
|
147
|
+
anchor = Shoko::Core::Models::SelectionAnchor.from(anchor_hash)
|
|
148
|
+
geometry = geometry_for_anchor(anchor)
|
|
149
|
+
return { x: 1, y: 1 } unless geometry
|
|
150
|
+
|
|
151
|
+
x = if anchor.cell_index >= geometry.cells.length
|
|
152
|
+
geometry.column_origin + geometry.visible_width
|
|
153
|
+
else
|
|
154
|
+
cell = geometry.cells[anchor.cell_index]
|
|
155
|
+
geometry.column_origin + cell.screen_x
|
|
156
|
+
end
|
|
157
|
+
y = geometry.row
|
|
158
|
+
|
|
159
|
+
@coordinate_service.calculate_popup_position({ x:, y: }, @width, @height)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def geometry_for_anchor(anchor)
|
|
163
|
+
return nil unless anchor
|
|
164
|
+
|
|
165
|
+
return nil unless @rendered_lines
|
|
166
|
+
|
|
167
|
+
entry = @rendered_lines[anchor.geometry_key]
|
|
168
|
+
(entry && entry[:geometry]) || nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def render_menu_item(surface, bounds, item, index)
|
|
172
|
+
item_y = @y + index
|
|
173
|
+
is_selected = (index == @selected_index)
|
|
174
|
+
action = @available_actions[index]
|
|
175
|
+
|
|
176
|
+
# Colors
|
|
177
|
+
bg = is_selected ? TOOLTIP_BG_SELECTED : TOOLTIP_BG_DEFAULT
|
|
178
|
+
fg = is_selected ? TOOLTIP_FG_SELECTED : TOOLTIP_FG_DEFAULT
|
|
179
|
+
|
|
180
|
+
# Background
|
|
181
|
+
surface.write_abs(bounds, item_y, @x, "#{bg}#{' ' * @width}#{Terminal::ANSI::RESET}")
|
|
182
|
+
|
|
183
|
+
# Content with icon
|
|
184
|
+
indicator = is_selected ? '❯' : ' '
|
|
185
|
+
icon = action[:icon] || ' '
|
|
186
|
+
line_text = Shoko::Adapters::Output::Terminal::TextMetrics.pad_right("#{indicator}#{icon} #{item} ", @width)
|
|
187
|
+
surface.write_abs(bounds, item_y, @x, "#{bg}#{fg}#{line_text}#{Terminal::ANSI::RESET}")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative 'surface'
|
|
5
|
+
require_relative '../../terminal/text_metrics.rb'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters::Output::Ui::Components
|
|
9
|
+
# Renders the bottom status area (page info + transient message).
|
|
10
|
+
class FooterComponent < BaseComponent
|
|
11
|
+
def initialize(view_model_provider = nil)
|
|
12
|
+
super()
|
|
13
|
+
@view_model_provider = view_model_provider
|
|
14
|
+
@cached_view_model = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def preferred_height(_available_height)
|
|
18
|
+
vm = resolve_view_model
|
|
19
|
+
return 0 unless vm
|
|
20
|
+
|
|
21
|
+
renderable_page_info?(vm) ? 1 : 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def do_render(surface, bounds)
|
|
25
|
+
vm = resolve_view_model
|
|
26
|
+
return unless vm
|
|
27
|
+
|
|
28
|
+
return unless renderable_page_info?(vm)
|
|
29
|
+
|
|
30
|
+
render_page_info(surface, bounds, vm, bounds.height)
|
|
31
|
+
ensure
|
|
32
|
+
@cached_view_model = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def resolve_view_model
|
|
38
|
+
return nil unless @view_model_provider
|
|
39
|
+
|
|
40
|
+
@cached_view_model ||= @view_model_provider.call
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def renderable_page_info?(view_model)
|
|
46
|
+
disallowed_modes = %i[help]
|
|
47
|
+
return false if view_model.respond_to?(:mode) && disallowed_modes.include?(view_model.mode)
|
|
48
|
+
return false unless view_model.respond_to?(:show_page_numbers) && view_model.show_page_numbers
|
|
49
|
+
|
|
50
|
+
info = view_model.page_info
|
|
51
|
+
info && !info.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_page_info(surface, bounds, view_model, row)
|
|
55
|
+
ui = Shoko::Adapters::Output::Ui::Constants::UI
|
|
56
|
+
width = bounds.width
|
|
57
|
+
info = view_model.page_info
|
|
58
|
+
|
|
59
|
+
if view_model.view_mode == :split && info[:left]
|
|
60
|
+
render_split_page_info(surface, bounds, info, width, row, ui)
|
|
61
|
+
else
|
|
62
|
+
render_single_page_info(surface, bounds, info, width, row, ui)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_single_page_info(surface, bounds, info, width, row, ui_constants)
|
|
67
|
+
current = info[:current].to_i
|
|
68
|
+
total = info[:total].to_i
|
|
69
|
+
return if current.zero? && total.zero?
|
|
70
|
+
|
|
71
|
+
label = page_label(current, total)
|
|
72
|
+
col = center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(label))
|
|
73
|
+
write_colored(surface, bounds, row, col, label, ui_constants::COLOR_TEXT_PRIMARY)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_split_page_info(surface, bounds, info, width, row, ui_constants)
|
|
77
|
+
left = info[:left]
|
|
78
|
+
right = info[:right]
|
|
79
|
+
return unless left
|
|
80
|
+
|
|
81
|
+
left_label = page_label(left[:current].to_i, left[:total].to_i)
|
|
82
|
+
left_col = quarter_center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(left_label), :left)
|
|
83
|
+
unless left_label.empty?
|
|
84
|
+
write_colored(surface, bounds, row, left_col, left_label,
|
|
85
|
+
ui_constants::COLOR_TEXT_PRIMARY)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
return unless right
|
|
89
|
+
|
|
90
|
+
right_label = page_label(right[:current].to_i, right[:total].to_i)
|
|
91
|
+
right_col = quarter_center_col(width, Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(right_label), :right)
|
|
92
|
+
return if right_label.empty?
|
|
93
|
+
|
|
94
|
+
write_colored(surface, bounds, row, right_col, right_label,
|
|
95
|
+
ui_constants::COLOR_TEXT_PRIMARY)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ----- helpers -----
|
|
99
|
+
|
|
100
|
+
def page_label(current, total)
|
|
101
|
+
total.positive? ? "#{current} / #{total}" : "Page #{current}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def center_col(width, text_len)
|
|
105
|
+
[(width - text_len) / 2, 1].max
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def quarter_center_col(width, text_len, side)
|
|
109
|
+
half = text_len / 2
|
|
110
|
+
return [(width / 4) - half, 1].max if side == :left
|
|
111
|
+
|
|
112
|
+
[(3 * width / 4) - half, 1].max
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_colored(surface, bounds, row, col, text, color_prefix)
|
|
116
|
+
surface.write(bounds, row, col, color_prefix + text + Terminal::ANSI::RESET)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_component'
|
|
4
|
+
require_relative 'surface'
|
|
5
|
+
require_relative '../constants/ui_constants'
|
|
6
|
+
require_relative '../../terminal/text_metrics.rb'
|
|
7
|
+
|
|
8
|
+
module Shoko
|
|
9
|
+
module Adapters::Output::Ui::Components
|
|
10
|
+
# Renders the top header row (document title).
|
|
11
|
+
class HeaderComponent < BaseComponent
|
|
12
|
+
include Adapters::Output::Ui::Constants::UI
|
|
13
|
+
|
|
14
|
+
def initialize(view_model_provider = nil, theme = :dark)
|
|
15
|
+
super()
|
|
16
|
+
@view_model_provider = view_model_provider
|
|
17
|
+
@theme = theme
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def preferred_height(_available_height)
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def do_render(surface, bounds)
|
|
25
|
+
return unless @view_model_provider
|
|
26
|
+
|
|
27
|
+
view_model = @view_model_provider.call
|
|
28
|
+
return unless view_model.respond_to?(:document_title)
|
|
29
|
+
|
|
30
|
+
render_centered_title(surface, bounds, view_model.document_title.to_s)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def render_centered_title(surface, bounds, title)
|
|
36
|
+
return if title.empty?
|
|
37
|
+
|
|
38
|
+
reset = Terminal::ANSI::RESET
|
|
39
|
+
width = bounds.width
|
|
40
|
+
title_width = Shoko::Adapters::Output::Terminal::TextMetrics.visible_length(title)
|
|
41
|
+
col = [(width - title_width) / 2, 1].max
|
|
42
|
+
surface.write(bounds, 1, col, "#{COLOR_TEXT_PRIMARY}#{title}#{reset}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../base_component'
|
|
4
|
+
require_relative '../rect'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Output::Ui::Components
|
|
8
|
+
module Layouts
|
|
9
|
+
# Horizontal layout that splits children left-to-right
|
|
10
|
+
# Supports collapsible left sidebar with dynamic width allocation
|
|
11
|
+
class Horizontal < BaseComponent
|
|
12
|
+
def initialize(left_child, right_child)
|
|
13
|
+
super(nil)
|
|
14
|
+
@left_child = left_child
|
|
15
|
+
@right_child = right_child
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def do_render(surface, bounds)
|
|
19
|
+
return unless @left_child && @right_child
|
|
20
|
+
|
|
21
|
+
# Cache frequently used bounds
|
|
22
|
+
x = bounds.x
|
|
23
|
+
y = bounds.y
|
|
24
|
+
w = bounds.width
|
|
25
|
+
h = bounds.height
|
|
26
|
+
|
|
27
|
+
# Calculate widths based on left child's preferred width
|
|
28
|
+
left_width = calculate_left_width(w)
|
|
29
|
+
right_width = w - left_width
|
|
30
|
+
|
|
31
|
+
# Render left child (sidebar)
|
|
32
|
+
if left_width.positive?
|
|
33
|
+
left_bounds = Rect.new(x: x, y: y, width: left_width, height: h)
|
|
34
|
+
@left_child.render(surface, left_bounds)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render right child (content)
|
|
38
|
+
return unless right_width.positive?
|
|
39
|
+
|
|
40
|
+
right_x = x + left_width
|
|
41
|
+
right_bounds = Rect.new(x: right_x, y: y, width: right_width, height: h)
|
|
42
|
+
@right_child.render(surface, right_bounds)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def calculate_left_width(total_width)
|
|
48
|
+
return 0 unless @left_child.respond_to?(:preferred_width)
|
|
49
|
+
|
|
50
|
+
pref = @left_child.preferred_width(total_width)
|
|
51
|
+
case pref
|
|
52
|
+
when Integer
|
|
53
|
+
[pref, total_width].min
|
|
54
|
+
when :flexible
|
|
55
|
+
total_width / 3 # Default to 1/3 width
|
|
56
|
+
else
|
|
57
|
+
0
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../base_component'
|
|
4
|
+
require_relative '../rect'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::Output::Ui::Components
|
|
8
|
+
module Layouts
|
|
9
|
+
# Simple vertical layout that stacks children top-to-bottom
|
|
10
|
+
# Respects preferred heights; assigns remaining space to first flexible child
|
|
11
|
+
class Vertical < BaseComponent
|
|
12
|
+
def initialize(children)
|
|
13
|
+
super(nil)
|
|
14
|
+
@children = children
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def do_render(surface, bounds)
|
|
18
|
+
return if @children.nil? || @children.empty?
|
|
19
|
+
|
|
20
|
+
# Calculate heights using new contract
|
|
21
|
+
heights = calculate_child_heights(bounds.height)
|
|
22
|
+
|
|
23
|
+
# Render children
|
|
24
|
+
cursor_y = bounds.y
|
|
25
|
+
@children.each_with_index do |child, i|
|
|
26
|
+
height = heights[i] || 0
|
|
27
|
+
next if height <= 0
|
|
28
|
+
|
|
29
|
+
child_bounds = Rect.new(x: bounds.x, y: cursor_y, width: bounds.width, height: height)
|
|
30
|
+
child.render(surface, child_bounds)
|
|
31
|
+
cursor_y += height
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def calculate_child_heights(total_height)
|
|
38
|
+
heights = []
|
|
39
|
+
remaining = total_height
|
|
40
|
+
fill_children = []
|
|
41
|
+
|
|
42
|
+
# First pass: allocate fixed heights
|
|
43
|
+
@children.each_with_index do |child, i|
|
|
44
|
+
pref = child.respond_to?(:preferred_height) ? child.preferred_height(total_height) : :flexible
|
|
45
|
+
|
|
46
|
+
case pref
|
|
47
|
+
when Integer
|
|
48
|
+
# Fixed height
|
|
49
|
+
height = [pref, remaining].min
|
|
50
|
+
heights[i] = height
|
|
51
|
+
remaining -= height
|
|
52
|
+
when :fill
|
|
53
|
+
# Fill remaining space (calculated in second pass)
|
|
54
|
+
fill_children << i
|
|
55
|
+
heights[i] = nil
|
|
56
|
+
else
|
|
57
|
+
# :flexible and any unknown values default to minimum space
|
|
58
|
+
heights[i] = 0
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Second pass: distribute remaining space to fill children
|
|
63
|
+
if fill_children.any?
|
|
64
|
+
target_height = remaining.positive? ? (remaining / fill_children.size) : 0
|
|
65
|
+
fill_children.each { |i| heights[i] = target_height }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
heights
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|