From 22b53e9e9e6cf43d9176fdf6641209880d91a440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 16:18:15 -0400 Subject: [PATCH] Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI --- AGENTS.md | 25 +- README.md | 23 +- .../accent_color_picker.go | 32 +- .../accent_color_picker_test.go | 4 +- .../account_switcher.go | 39 +-- .../account_switcher_test.go | 2 +- .../api_key_manager.go | 58 ++-- .../api_key_manager_test.go | 2 +- coachmark.go => account/coachmark.go | 27 +- .../coachmark_test.go | 10 +- account/doc.go | 2 + account/example_account_test.go | 242 +++++++++++++++ .../feedback_widget.go | 34 ++- .../feedback_widget_test.go | 10 +- account/helpers_test.go | 24 ++ .../keyboard_shortcuts_list.go | 17 +- .../keyboard_shortcuts_list_test.go | 2 +- login_form.go => account/login_form.go | 28 +- .../login_form_test.go | 2 +- oauth_buttons.go => account/oauth_buttons.go | 11 +- .../oauth_buttons_test.go | 2 +- .../onboarding_wizard.go | 31 +- .../onboarding_wizard_test.go | 6 +- .../profile_editor.go | 33 ++- .../profile_editor_test.go | 2 +- session_list.go => account/session_list.go | 32 +- .../session_list_test.go | 2 +- .../settings_layout.go | 38 +-- .../settings_layout_test.go | 11 +- .../settings_search.go | 22 +- .../settings_search_test.go | 6 +- .../shortcut_recorder.go | 36 +-- .../shortcut_recorder_test.go | 6 +- .../theme_selector.go | 33 ++- .../theme_selector_test.go | 16 +- .../two_factor_input.go | 22 +- .../two_factor_input_test.go | 2 +- usage_quota.go => account/usage_quota.go | 32 +- .../usage_quota_test.go | 2 +- user_menu.go => account/user_menu.go | 25 +- .../user_menu_test.go | 2 +- .../whats_new_dialog.go | 34 ++- .../whats_new_dialog_test.go | 2 +- .../workspace_switcher.go | 29 +- .../workspace_switcher_test.go | 2 +- agent_diff.go => agent/agent_diff.go | 35 +-- agent_parts.go => agent/agent_parts.go | 69 ++--- agent_plan.go => agent/agent_plan.go | 48 +-- .../agent_plan_test.go | 6 +- agent_progress.go => agent/agent_progress.go | 13 +- .../agent_progress_test.go | 10 +- agent_status.go => agent/agent_status.go | 11 +- .../agent_status_test.go | 10 +- .../agent_step_list.go | 21 +- .../agent_step_list_test.go | 6 +- artifact_panel.go => agent/artifact_panel.go | 37 +-- .../artifact_panel_test.go | 8 +- .../artifact_version_switcher.go | 21 +- .../artifact_version_switcher_test.go | 6 +- .../checkpoint_list.go | 36 +-- .../checkpoint_list_test.go | 6 +- .../command_execution_card.go | 44 +-- .../command_execution_card_test.go | 6 +- agent/doc.go | 2 + agent/example_agent_test.go | 244 +++++++++++++++ .../file_change_card.go | 26 +- .../file_change_card_test.go | 6 +- agent/helpers_test.go | 30 ++ .../human_input_request.go | 36 +-- .../human_input_request_test.go | 6 +- .../mcp_server_list.go | 36 +-- .../mcp_server_list_test.go | 6 +- memory_panel.go => agent/memory_panel.go | 41 +-- .../memory_panel_test.go | 6 +- .../multi_file_diff_review.go | 50 ++-- .../multi_file_diff_review_test.go | 6 +- .../permission_prompt.go | 23 +- .../permission_prompt_test.go | 11 +- sandbox_status.go => agent/sandbox_status.go | 33 ++- .../sandbox_status_test.go | 8 +- .../screenshot_stream.go | 50 ++-- .../screenshot_stream_test.go | 6 +- sub_agent_tree.go => agent/sub_agent_tree.go | 25 +- .../sub_agent_tree_test.go | 8 +- .../tool_approval_dialog.go | 21 +- .../tool_approval_dialog_test.go | 11 +- tool_call_card.go => agent/tool_call_card.go | 38 +-- .../tool_call_card_test.go | 14 +- .../tool_call_group.go | 26 +- .../tool_call_group_test.go | 10 +- area_chart.go => chart/area_chart.go | 10 +- .../area_chart_test.go | 2 +- bar_chart.go => chart/bar_chart.go | 41 +-- bar_chart_test.go => chart/bar_chart_test.go | 6 +- box_plot.go => chart/box_plot.go | 27 +- box_plot_test.go => chart/box_plot_test.go | 2 +- bullet_chart.go => chart/bullet_chart.go | 27 +- .../bullet_chart_test.go | 6 +- .../calendar_heatmap.go | 50 ++-- .../calendar_heatmap_test.go | 21 +- .../candle_countdown.go | 11 +- .../candle_countdown_test.go | 12 +- .../candlestick_chart.go | 49 +-- .../candlestick_chart_test.go | 16 +- .../candlestick_paint.go | 15 +- .../chart_annotation.go | 27 +- .../chart_annotation_test.go | 10 +- chart_axis.go => chart/chart_axis.go | 9 +- .../chart_axis_test.go | 13 +- chart_brush.go => chart/chart_brush.go | 66 +++-- .../chart_brush_test.go | 8 +- .../chart_crosshair.go | 12 +- .../chart_crosshair_test.go | 9 +- .../chart_empty_state.go | 11 +- .../chart_empty_state_test.go | 17 +- chart_export.go => chart/chart_export.go | 20 +- .../chart_export_test.go | 15 +- chart_frame.go => chart/chart_frame.go | 72 ++--- .../chart_frame_test.go | 2 +- chart_grid.go => chart/chart_grid.go | 7 +- .../chart_grid_test.go | 9 +- chart_legend.go => chart/chart_legend.go | 25 +- .../chart_legend_test.go | 17 +- chart_series.go => chart/chart_series.go | 81 ++--- chart_sync.go => chart/chart_sync.go | 25 +- .../chart_sync_test.go | 9 +- chart_tooltip.go => chart/chart_tooltip.go | 17 +- .../chart_tooltip_test.go | 15 +- chart_tree.go => chart/chart_tree.go | 15 +- .../chart_type_switcher.go | 15 +- .../chart_type_switcher_test.go | 9 +- chord_diagram.go => chart/chord_diagram.go | 25 +- .../chord_diagram_test.go | 6 +- .../compare_symbol_test.go | 8 +- density_plot.go => chart/density_plot.go | 14 +- .../density_plot_test.go | 6 +- depth_chart.go => chart/depth_chart.go | 44 +-- .../depth_chart_test.go | 6 +- chart/doc.go | 2 + .../drawing_toolbar.go | 31 +- .../drawing_toolbar_test.go | 9 +- drawing_tools.go => chart/drawing_tools.go | 8 +- .../drawing_tools_paint.go | 11 +- .../drawing_tools_test.go | 19 +- chart/example_charts1_test.go | 222 ++++++++++++++ chart/example_charts2_test.go | 183 ++++++++++++ chart/example_markets_test.go | 23 ++ .../example_mktchart_test.go | 170 +++++------ funnel_chart.go => chart/funnel_chart.go | 21 +- .../funnel_chart_test.go | 6 +- gauge.go => chart/gauge.go | 23 +- gauge_test.go => chart/gauge_test.go | 6 +- heatmap_chart.go => chart/heatmap_chart.go | 27 +- .../heatmap_chart_test.go | 6 +- .../heikin_ashi_chart_test.go | 15 +- chart/helpers_test.go | 70 +++++ histogram.go => chart/histogram.go | 19 +- histogram_test.go => chart/histogram_test.go | 6 +- .../indicator_overlay.go | 18 +- .../indicator_overlay_test.go | 12 +- indicator_pane.go => chart/indicator_pane.go | 57 ++-- .../indicator_pane_test.go | 11 +- .../indicator_selector.go | 39 +-- .../indicator_selector_test.go | 11 +- indicators.go => chart/indicators.go | 2 +- .../indicators_test.go | 2 +- .../interval_selector.go | 39 +-- .../interval_selector_test.go | 9 +- line_chart.go => chart/line_chart.go | 55 ++-- .../line_chart_test.go | 15 +- .../line_quote_chart.go | 45 +-- .../line_quote_chart_test.go | 12 +- .../market_boundary_test.go | 8 +- market_data.go => chart/market_data.go | 2 +- market_heatmap.go => chart/market_heatmap.go | 108 +++---- .../market_heatmap_test.go | 5 +- market_input.go => chart/market_input.go | 11 +- .../market_keyboard_test.go | 11 +- .../market_large_test.go | 11 +- market_layers.go => chart/market_layers.go | 36 +-- .../market_regression_test.go | 10 +- .../market_review_regression_test.go | 35 ++- .../market_states_test.go | 18 +- .../multi_chart_layout.go | 26 +- .../multi_chart_layout_test.go | 9 +- network_graph.go => chart/network_graph.go | 21 +- .../network_graph_test.go | 6 +- .../ohlc_chart_test.go | 6 +- .../parallel_coordinates.go | 51 ++-- .../parallel_coordinates_test.go | 6 +- pie_chart.go => chart/pie_chart.go | 33 ++- pie_chart_test.go => chart/pie_chart_test.go | 10 +- .../price_alert_line_test.go | 6 +- radar_chart.go => chart/radar_chart.go | 29 +- .../radar_chart_test.go | 6 +- realtime_chart.go => chart/realtime_chart.go | 23 +- .../realtime_chart_test.go | 2 +- sankey_chart.go => chart/sankey_chart.go | 25 +- .../sankey_chart_test.go | 6 +- scatter_chart.go => chart/scatter_chart.go | 35 +-- .../scatter_chart_test.go | 6 +- spark_bar.go => chart/spark_bar.go | 9 +- spark_bar_test.go => chart/spark_bar_test.go | 26 +- sparkline.go => chart/sparkline.go | 38 +-- sparkline_test.go => chart/sparkline_test.go | 13 +- sunburst.go => chart/sunburst.go | 29 +- sunburst_test.go => chart/sunburst_test.go | 6 +- .../time_range_selector.go | 30 +- .../time_range_selector_test.go | 13 +- treemap.go => chart/treemap.go | 29 +- treemap_test.go => chart/treemap_test.go | 6 +- .../trend_indicator.go | 22 +- .../trend_indicator_test.go | 16 +- violin_plot.go => chart/violin_plot.go | 29 +- .../violin_plot_test.go | 6 +- volume_chart.go => chart/volume_chart.go | 34 ++- .../volume_chart_test.go | 8 +- volume_profile.go => chart/volume_profile.go | 36 +-- .../volume_profile_test.go | 8 +- .../waterfall_chart.go | 25 +- .../waterfall_chart_test.go | 6 +- artifact_card.go => chat/artifact_card.go | 34 ++- .../artifact_card_test.go | 17 +- attachment_chip.go => chat/attachment_chip.go | 25 +- .../attachment_chip_test.go | 10 +- audio_message.go => chat/audio_message.go | 23 +- .../audio_message_test.go | 10 +- .../branch_navigator.go | 26 +- .../branch_navigator_test.go | 8 +- .../capability_cards.go | 17 +- .../capability_cards_test.go | 6 +- chat_container.go => chat/chat_container.go | 9 +- .../chat_container_test.go | 9 +- chat/chat_empty_state.go | 60 ++++ .../chat_empty_state_test.go | 6 +- .../chatsession_fixture_test.go | 4 +- citation_badge.go => chat/citation_badge.go | 52 ++-- .../citation_badge_test.go | 14 +- code_block.go => chat/code_block.go | 41 +-- code_block_test.go => chat/code_block_test.go | 6 +- context_chips.go => chat/context_chips.go | 29 +- .../context_chips_test.go | 10 +- .../context_mention_menu.go | 19 +- .../context_mention_menu_test.go | 8 +- .../context_window_meter.go | 25 +- .../context_window_meter_test.go | 11 +- .../conversation_export.go | 42 +-- .../conversation_export_test.go | 6 +- .../conversation_item.go | 50 ++-- .../conversation_item_test.go | 12 +- .../conversation_list.go | 34 ++- .../conversation_list_test.go | 6 +- .../conversation_search.go | 37 +-- .../conversation_search_test.go | 2 +- cost_estimator.go => chat/cost_estimator.go | 27 +- .../cost_estimator_test.go | 6 +- chat/date_separator.go | 71 +++++ chat/date_separator_test.go | 67 +++++ chat/doc.go | 2 + .../drag_drop_overlay.go | 27 +- .../drag_drop_overlay_test.go | 10 +- error_message.go => chat/error_message.go | 24 +- .../error_message_test.go | 10 +- chat/example_chatinput_test.go | 242 +++++++++++++++ .../example_chatmsg_test.go | 99 ++++--- chat/example_chatsession_test.go | 232 +++++++++++++++ feedback_form.go => chat/feedback_form.go | 26 +- .../feedback_form_test.go | 2 +- file_message.go => chat/file_message.go | 21 +- .../file_message_test.go | 10 +- chat/helpers_test.go | 36 +++ image_grid.go => chat/image_grid.go | 21 +- image_grid_test.go => chat/image_grid_test.go | 10 +- .../link_preview_card.go | 17 +- .../link_preview_card_test.go | 10 +- markdown_view.go => chat/markdown_view.go | 17 +- .../markdown_view_test.go | 10 +- message_actions.go => chat/message_actions.go | 13 +- .../message_actions_test.go | 10 +- message_bubble.go => chat/message_bubble.go | 17 +- .../message_bubble_test.go | 13 +- message_editor.go => chat/message_editor.go | 13 +- .../message_editor_test.go | 6 +- message_header.go => chat/message_header.go | 19 +- .../message_header_test.go | 10 +- message_list.go => chat/message_list.go | 21 +- .../message_list_test.go | 8 +- mode_selector.go => chat/mode_selector.go | 21 +- .../mode_selector_test.go | 14 +- model_selector.go => chat/model_selector.go | 33 ++- .../model_selector_test.go | 15 +- parameter_panel.go => chat/parameter_panel.go | 40 +-- .../parameter_panel_test.go | 14 +- .../paste_image_preview.go | 15 +- .../paste_image_preview_test.go | 10 +- .../project_knowledge_panel.go | 40 +-- .../project_knowledge_panel_test.go | 6 +- project_list.go => chat/project_list.go | 38 +-- .../project_list_test.go | 2 +- prompt_composer.go => chat/prompt_composer.go | 26 +- .../prompt_composer_test.go | 13 +- prompt_library.go => chat/prompt_library.go | 34 ++- .../prompt_library_test.go | 2 +- quote_reply.go => chat/quote_reply.go | 17 +- .../quote_reply_test.go | 8 +- regenerate_menu.go => chat/regenerate_menu.go | 20 +- .../regenerate_menu_test.go | 2 +- .../scroll_to_bottom_button.go | 21 +- .../scroll_to_bottom_button_test.go | 8 +- search_progress.go => chat/search_progress.go | 32 +- .../search_progress_test.go | 10 +- send_button.go => chat/send_button.go | 19 +- .../send_button_test.go | 6 +- .../shared_conversation_view.go | 39 +-- .../shared_conversation_view_test.go | 6 +- .../slash_command_menu.go | 31 +- .../slash_command_menu_test.go | 10 +- source_card.go => chat/source_card.go | 13 +- .../source_card_test.go | 8 +- sources_panel.go => chat/sources_panel.go | 19 +- .../sources_panel_test.go | 12 +- .../stop_generating_button.go | 26 +- .../stop_generating_button_test.go | 2 +- streaming_text.go => chat/streaming_text.go | 8 +- .../streaming_text_test.go | 6 +- .../suggestion_chips.go | 15 +- .../suggestion_chips_test.go | 6 +- .../system_prompt_editor.go | 28 +- .../system_prompt_editor_test.go | 2 +- table_block.go => chat/table_block.go | 27 +- .../table_block_test.go | 8 +- thinking_block.go => chat/thinking_block.go | 24 +- .../thinking_block_test.go | 10 +- .../thinking_indicator.go | 15 +- .../thinking_indicator_test.go | 2 +- token_counter.go => chat/token_counter.go | 17 +- .../token_counter_test.go | 8 +- .../tool_toggle_menu.go | 37 +-- .../tool_toggle_menu_test.go | 6 +- .../typing_indicator.go | 15 +- .../typing_indicator_test.go | 8 +- .../voice_input_button.go | 29 +- .../voice_input_button_test.go | 10 +- voice_waveform.go => chat/voice_waveform.go | 15 +- .../voice_waveform_test.go | 8 +- welcome_screen.go => chat/welcome_screen.go | 13 +- .../welcome_screen_test.go | 6 +- chat_empty_state.go | 58 ---- breakpoint_list.go => code/breakpoint_list.go | 35 ++- .../breakpoint_list_test.go | 6 +- call_stack.go => code/call_stack.go | 27 +- call_stack_test.go => code/call_stack_test.go | 6 +- code_shared.go => code/code_shared.go | 82 +++--- code_viewer.go => code/code_viewer.go | 46 +-- .../code_viewer_test.go | 13 +- command_history.go => code/command_history.go | 32 +- .../command_history_test.go | 8 +- completion_menu.go => code/completion_menu.go | 31 +- .../completion_menu_test.go | 9 +- debug_toolbar.go => code/debug_toolbar.go | 13 +- .../debug_toolbar_test.go | 6 +- code/doc.go | 2 + code/example_code_test.go | 278 ++++++++++++++++++ find_widget.go => code/find_widget.go | 28 +- .../find_widget_test.go | 6 +- flamegraph.go => code/flamegraph.go | 47 +-- flamegraph_test.go => code/flamegraph_test.go | 12 +- code/helpers_test.go | 24 ++ hex_viewer.go => code/hex_viewer.go | 29 +- hex_viewer_test.go => code/hex_viewer_test.go | 6 +- log_viewer.go => code/log_viewer.go | 42 +-- log_viewer_test.go => code/log_viewer_test.go | 10 +- minimap.go => code/minimap.go | 19 +- minimap_test.go => code/minimap_test.go | 6 +- output_panel.go => code/output_panel.go | 28 +- .../output_panel_test.go | 12 +- problems_panel.go => code/problems_panel.go | 28 +- .../problems_panel_test.go | 6 +- process_list.go => code/process_list.go | 53 ++-- .../process_list_test.go | 6 +- search_panel.go => code/search_panel.go | 37 +-- .../search_panel_test.go | 9 +- symbol_outline.go => code/symbol_outline.go | 31 +- .../symbol_outline_test.go | 6 +- terminal.go => code/terminal.go | 11 +- terminal_search.go => code/terminal_search.go | 19 +- .../terminal_search_test.go | 6 +- terminal_state.go => code/terminal_state.go | 4 +- terminal_tabs.go => code/terminal_tabs.go | 35 ++- .../terminal_tabs_test.go | 8 +- terminal_test.go => code/terminal_test.go | 6 +- variables_panel.go => code/variables_panel.go | 31 +- .../variables_panel_test.go | 6 +- .../base_disabled_test.go | 2 +- core/choice_result.go | 17 ++ datetime.go => core/datetime.go | 105 +++---- docs_test.go => core/docs_test.go | 12 +- core/example_feedback_test.go | 15 + core/geometry.go | 28 ++ i18n.go => core/i18n.go | 10 +- core/icon_action.go | 23 ++ .../media_helpers_test.go | 2 +- core/media_icons.go | 53 ++++ .../messages_account.go | 2 +- messages_agent.go => core/messages_agent.go | 2 +- .../messages_calendar.go | 8 +- .../messages_charts1.go | 2 +- .../messages_charts2.go | 2 +- .../messages_chatinput.go | 2 +- .../messages_chatmsg.go | 2 +- .../messages_chatsession.go | 2 +- messages_choice.go => core/messages_choice.go | 2 +- messages_code.go => core/messages_code.go | 2 +- messages_data.go => core/messages_data.go | 2 +- .../messages_datetime.go | 2 +- .../messages_devtools.go | 2 +- .../messages_display.go | 2 +- .../messages_essentials.go | 2 +- .../messages_feedback.go | 2 +- messages_files.go => core/messages_files.go | 2 +- messages_form.go => core/messages_form.go | 2 +- messages_git.go => core/messages_git.go | 2 +- messages_input.go => core/messages_input.go | 2 +- messages_layout.go => core/messages_layout.go | 2 +- .../messages_markets.go | 2 +- messages_media.go => core/messages_media.go | 2 +- .../messages_messaging.go | 11 +- .../messages_mktchart.go | 2 +- messages_motion.go => core/messages_motion.go | 2 +- .../messages_navigation.go | 2 +- .../messages_overlay.go | 2 +- .../messages_projects.go | 2 +- .../messages_trading.go | 2 +- .../motion_delivery_test.go | 8 +- overlay_layers.go => core/overlay_layers.go | 24 +- core/overlay_motion.go | 44 +++ core/severity.go | 59 ++++ core/snapshot_test.go | 35 +++ spinner.go => core/spinner.go | 6 +- theme.go => core/theme.go | 23 +- theme_test.go => core/theme_test.go | 4 +- core/trend.go | 96 ++++++ accordion.go => data/accordion.go | 21 +- accordion_test.go => data/accordion_test.go | 8 +- card.go => data/card.go | 13 +- card_test.go => data/card_test.go | 26 +- carousel.go => data/carousel.go | 38 +-- carousel_test.go => data/carousel_test.go | 10 +- collapsible.go => data/collapsible.go | 25 +- .../collapsible_test.go | 19 +- data_table.go => data/data_table.go | 26 +- .../data_table_columns.go | 24 +- data_table_test.go => data/data_table_test.go | 7 +- .../description_list.go | 22 +- .../description_list_test.go | 10 +- data/doc.go | 2 + .../example_data_test.go | 78 ++--- data/helpers_test.go | 24 ++ image_viewer.go => data/image_viewer.go | 34 ++- .../image_viewer_test.go | 6 +- list.go => data/list.go | 90 +++--- list_test.go => data/list_test.go | 9 +- statistic.go => data/statistic.go | 37 +-- statistic_test.go => data/statistic_test.go | 14 +- timeline.go => data/timeline.go | 27 +- timeline_test.go => data/timeline_test.go | 6 +- tree.go => data/tree.go | 80 ++--- tree_table.go => data/tree_table.go | 33 ++- tree_table_test.go => data/tree_table_test.go | 26 +- tree_test.go => data/tree_test.go | 29 +- date_separator.go | 70 ----- date_separator_test.go | 65 ---- agenda_view.go => datetime/agenda_view.go | 43 +-- .../agenda_view_test.go | 6 +- attendee_list.go => datetime/attendee_list.go | 32 +- .../attendee_list_test.go | 6 +- .../availability_picker.go | 39 +-- .../availability_picker_test.go | 8 +- calendar.go => datetime/calendar.go | 95 +++--- .../calendar_day_view.go | 20 +- .../calendar_day_view_test.go | 10 +- .../calendar_events.go | 23 +- .../calendar_month_view.go | 67 ++--- .../calendar_month_view_test.go | 12 +- calendar_test.go => datetime/calendar_test.go | 54 ++-- .../calendar_time_grid.go | 53 ++-- .../calendar_week_view.go | 41 +-- .../calendar_week_view_test.go | 8 +- countdown.go => datetime/countdown.go | 17 +- .../countdown_test.go | 10 +- cron_editor.go => datetime/cron_editor.go | 50 ++-- .../cron_editor_test.go | 10 +- .../current_time_indicator.go | 17 +- .../current_time_indicator_test.go | 15 +- date_picker.go => datetime/date_picker.go | 72 ++--- .../date_picker_test.go | 45 +-- .../date_range_picker.go | 76 ++--- .../date_range_picker_test.go | 12 +- .../date_time_picker.go | 62 ++-- .../date_time_picker_test.go | 14 +- datetime_grid.go => datetime/datetime_grid.go | 35 +-- datetime/datetime_result.go | 25 ++ .../datetime_review2_test.go | 34 ++- .../datetime_review_test.go | 19 +- datetime/doc.go | 2 + .../duration_picker.go | 23 +- .../duration_picker_test.go | 6 +- event_chip.go => datetime/event_chip.go | 23 +- .../event_chip_test.go | 13 +- event_editor.go => datetime/event_editor.go | 51 ++-- .../event_editor_test.go | 8 +- event_popover.go => datetime/event_popover.go | 34 ++- .../event_popover_test.go | 13 +- datetime/example_calendar_test.go | 255 ++++++++++++++++ datetime/example_datetime_test.go | 111 +++++++ datetime/example_helpers_test.go | 15 + datetime/helpers_test.go | 24 ++ month_picker.go => datetime/month_picker.go | 22 +- .../month_picker_test.go | 6 +- .../recurrence_editor.go | 80 ++--- .../recurrence_editor_test.go | 10 +- relative_time.go => datetime/relative_time.go | 14 +- .../relative_time_test.go | 14 +- .../reminder_picker.go | 25 +- .../reminder_picker_test.go | 8 +- stopwatch.go => datetime/stopwatch.go | 32 +- .../stopwatch_test.go | 6 +- time_picker.go => datetime/time_picker.go | 45 +-- .../time_picker_popup.go | 15 +- .../time_picker_test.go | 6 +- .../time_range_picker.go | 19 +- .../time_range_picker_test.go | 6 +- .../timezone_select.go | 36 +-- .../timezone_select_test.go | 6 +- datetime/week_picker.go | 97 ++++++ .../week_picker_test.go | 10 +- year_picker.go => datetime/year_picker.go | 16 +- .../year_picker_test.go | 6 +- year_view.go => datetime/year_view.go | 51 ++-- .../year_view_test.go | 23 +- device_selector.go | 78 ----- alert_list.go => devtools/alert_list.go | 33 ++- .../alert_list_test.go | 6 +- .../api_request_builder.go | 45 +-- .../api_request_builder_test.go | 10 +- .../dashboard_filter_bar.go | 46 +-- .../dashboard_filter_bar_test.go | 8 +- devtools/devtools_parts.go | 32 ++ devtools/doc.go | 2 + devtools/example_devtools_test.go | 221 ++++++++++++++ devtools/helpers_test.go | 44 +++ incident_card.go => devtools/incident_card.go | 34 ++- .../incident_card_test.go | 10 +- json_editor.go => devtools/json_editor.go | 37 +-- .../json_editor_test.go | 8 +- json_tree.go => devtools/json_tree.go | 2 +- json_viewer.go => devtools/json_viewer.go | 41 +-- .../json_viewer_test.go | 10 +- .../key_value_input.go | 41 +-- .../key_value_input_test.go | 8 +- log_stream.go => devtools/log_stream.go | 68 +++-- .../log_stream_test.go | 51 ++-- .../refresh_interval_selector.go | 24 +- .../refresh_interval_selector_test.go | 8 +- .../resource_gauge.go | 37 ++- .../resource_gauge_test.go | 19 +- .../response_viewer.go | 59 ++-- .../response_viewer_test.go | 19 +- schema_tree.go => devtools/schema_tree.go | 35 ++- .../schema_tree_test.go | 10 +- .../service_status.go | 33 ++- .../service_status_test.go | 10 +- stat_card.go => devtools/stat_card.go | 50 ++-- .../stat_card_test.go | 25 +- devtools/system_monitor.go | 87 ++++++ .../system_monitor_test.go | 26 +- .../trace_waterfall.go | 45 +-- .../trace_waterfall_test.go | 8 +- uptime_bar.go => devtools/uptime_bar.go | 26 +- .../uptime_bar_test.go | 26 +- devtools_parts.go | 30 -- avatar.go => display/avatar.go | 9 +- avatar_group.go => display/avatar_group.go | 17 +- .../avatar_group_test.go | 14 +- avatar_test.go => display/avatar_test.go | 6 +- badge.go => display/badge.go | 11 +- badge_test.go => display/badge_test.go | 12 +- display/doc.go | 2 + display/example_display_test.go | 137 +++++++++ heading.go => display/heading.go | 15 +- heading_test.go => display/heading_test.go | 10 +- display/helpers_test.go | 24 ++ icon.go => display/icon.go | 11 +- icon_test.go => display/icon_test.go | 10 +- image.go => display/image.go | 25 +- .../image_state_test.go | 11 +- image_test.go => display/image_test.go | 18 +- kbd.go => display/kbd.go | 21 +- kbd_test.go => display/kbd_test.go | 12 +- link.go => display/link.go | 15 +- link_test.go => display/link_test.go | 12 +- tag.go => display/tag.go | 19 +- tag_test.go => display/tag_test.go | 10 +- text.go => display/text.go | 21 +- text_test.go => display/text_test.go | 30 +- docs/components/001-app-shell.md | 4 +- docs/components/002-title-bar.md | 4 +- docs/components/003-status-bar.md | 6 +- docs/components/004-container.md | 2 +- docs/components/005-stack.md | 8 +- docs/components/006-grid.md | 4 +- docs/components/007-scroll-area.md | 2 +- docs/components/008-split-pane.md | 4 +- docs/components/009-aspect-ratio.md | 2 +- docs/components/010-divider.md | 4 +- docs/components/011-text.md | 8 +- docs/components/012-heading.md | 6 +- docs/components/013-icon.md | 6 +- docs/components/014-image.md | 4 +- docs/components/015-avatar.md | 6 +- docs/components/016-avatar-group.md | 6 +- docs/components/017-badge.md | 8 +- docs/components/018-tag.md | 6 +- docs/components/019-link.md | 6 +- docs/components/020-kbd.md | 6 +- docs/components/021-button.md | 2 +- docs/components/022-icon-button.md | 2 +- docs/components/023-button-group.md | 4 +- docs/components/024-text-input.md | 2 +- docs/components/025-text-area.md | 2 +- docs/components/026-number-input.md | 2 +- docs/components/027-password-input.md | 2 +- docs/components/028-search-input.md | 2 +- docs/components/029-checkbox.md | 4 +- docs/components/030-radio-group.md | 4 +- docs/components/031-switch.md | 4 +- docs/components/032-toggle.md | 4 +- docs/components/033-toggle-group.md | 6 +- docs/components/034-slider.md | 4 +- docs/components/035-range-slider.md | 4 +- docs/components/036-select.md | 6 +- docs/components/037-multi-select.md | 6 +- docs/components/038-combobox.md | 4 +- docs/components/039-cascader.md | 10 +- docs/components/040-tree-select.md | 8 +- docs/components/041-form.md | 10 +- docs/components/042-form-field.md | 6 +- docs/components/043-input-group.md | 6 +- docs/components/044-pin-input.md | 4 +- docs/components/045-tags-input.md | 4 +- docs/components/046-rating.md | 4 +- docs/components/047-color-picker.md | 4 +- docs/components/048-file-picker.md | 4 +- docs/components/049-file-drop-zone.md | 6 +- docs/components/050-transfer.md | 6 +- docs/components/051-calendar.md | 6 +- docs/components/052-date-picker.md | 10 +- docs/components/053-date-range-picker.md | 6 +- docs/components/054-time-picker.md | 8 +- docs/components/055-time-range-picker.md | 8 +- docs/components/056-date-time-picker.md | 6 +- docs/components/057-month-picker.md | 8 +- docs/components/058-year-picker.md | 6 +- docs/components/059-tabs.md | 4 +- docs/components/060-segmented-control.md | 4 +- docs/components/061-breadcrumb.md | 2 +- docs/components/062-pagination.md | 2 +- docs/components/063-steps.md | 4 +- docs/components/064-sidebar.md | 8 +- docs/components/065-navigation-menu.md | 8 +- docs/components/066-menubar.md | 14 +- docs/components/067-toolbar.md | 12 +- docs/components/068-command-palette.md | 4 +- docs/components/069-card.md | 6 +- docs/components/070-list.md | 6 +- docs/components/071-data-table.md | 10 +- docs/components/072-tree.md | 6 +- docs/components/073-tree-table.md | 8 +- docs/components/074-accordion.md | 6 +- docs/components/075-collapsible.md | 4 +- docs/components/076-timeline.md | 8 +- docs/components/077-description-list.md | 6 +- docs/components/078-statistic.md | 4 +- docs/components/079-image-viewer.md | 4 +- docs/components/080-carousel.md | 4 +- docs/components/081-dialog.md | 10 +- docs/components/082-alert-dialog.md | 6 +- docs/components/083-drawer.md | 6 +- docs/components/084-popover.md | 6 +- docs/components/085-tooltip.md | 6 +- docs/components/086-hover-card.md | 6 +- docs/components/087-dropdown-menu.md | 14 +- docs/components/088-context-menu.md | 8 +- docs/components/089-popconfirm.md | 6 +- docs/components/090-overlay.md | 4 +- docs/components/091-alert.md | 4 +- docs/components/092-toast.md | 6 +- docs/components/093-progress.md | 6 +- docs/components/094-spinner.md | 2 +- docs/components/095-skeleton.md | 4 +- docs/components/096-empty-state.md | 4 +- docs/components/097-result.md | 4 +- docs/components/098-status-indicator.md | 4 +- docs/components/099-notification-center.md | 2 +- docs/components/100-loading-overlay.md | 4 +- docs/components/101-line-chart.md | 6 +- docs/components/102-area-chart.md | 6 +- docs/components/103-bar-chart.md | 6 +- docs/components/104-pie-chart.md | 6 +- docs/components/105-scatter-chart.md | 8 +- docs/components/106-radar-chart.md | 6 +- docs/components/107-heatmap-chart.md | 4 +- docs/components/108-calendar-heatmap.md | 6 +- docs/components/109-histogram.md | 4 +- docs/components/110-box-plot.md | 6 +- docs/components/111-waterfall-chart.md | 8 +- docs/components/112-funnel-chart.md | 6 +- docs/components/113-bullet-chart.md | 4 +- docs/components/114-sparkline.md | 4 +- docs/components/115-gauge.md | 6 +- docs/components/116-treemap.md | 8 +- docs/components/117-sunburst.md | 8 +- docs/components/118-sankey-chart.md | 8 +- docs/components/119-network-graph.md | 10 +- docs/components/120-realtime-chart.md | 8 +- docs/components/121-chart-legend.md | 8 +- docs/components/122-chart-tooltip.md | 6 +- docs/components/123-chart-crosshair.md | 8 +- docs/components/124-chart-axis.md | 4 +- docs/components/125-chart-grid.md | 8 +- docs/components/126-chart-brush.md | 4 +- docs/components/127-chart-annotation.md | 10 +- docs/components/128-chart-export.md | 8 +- docs/components/129-violin-plot.md | 6 +- docs/components/130-parallel-coordinates.md | 4 +- docs/components/131-chord-diagram.md | 4 +- docs/components/132-spark-bar.md | 4 +- docs/components/133-trend-indicator.md | 4 +- docs/components/138-density-plot.md | 6 +- docs/components/140-chart-empty-state.md | 4 +- docs/components/141-candlestick-chart.md | 8 +- docs/components/142-ohlc-chart.md | 8 +- docs/components/143-heikin-ashi-chart.md | 8 +- docs/components/144-line-quote-chart.md | 6 +- docs/components/145-volume-chart.md | 8 +- docs/components/146-depth-chart.md | 8 +- docs/components/147-volume-profile.md | 6 +- docs/components/148-multi-chart-layout.md | 8 +- docs/components/149-chart-sync.md | 10 +- docs/components/150-time-range-selector.md | 6 +- docs/components/151-interval-selector.md | 8 +- docs/components/152-chart-type-switcher.md | 6 +- docs/components/153-indicator-overlay.md | 10 +- docs/components/154-indicator-pane.md | 10 +- docs/components/155-indicator-selector.md | 8 +- docs/components/156-drawing-tools.md | 10 +- docs/components/157-drawing-toolbar.md | 6 +- docs/components/158-compare-symbol.md | 12 +- docs/components/159-price-alert-line.md | 10 +- docs/components/160-candle-countdown.md | 4 +- docs/components/161-ticker-tape.md | 6 +- docs/components/162-quote-card.md | 6 +- docs/components/163-price-text.md | 6 +- docs/components/164-price-change-badge.md | 4 +- docs/components/165-order-book.md | 12 +- docs/components/166-level2-quotes.md | 10 +- docs/components/167-time-and-sales.md | 8 +- docs/components/168-depth-ladder.md | 8 +- docs/components/169-spread-indicator.md | 4 +- docs/components/170-bid-ask-bar.md | 4 +- docs/components/171-order-entry.md | 6 +- docs/components/172-quick-trade-buttons.md | 4 +- docs/components/173-position-table.md | 8 +- docs/components/174-order-table.md | 8 +- docs/components/175-trade-history-table.md | 8 +- docs/components/176-pnl-display.md | 4 +- docs/components/177-leverage-slider.md | 4 +- docs/components/178-margin-indicator.md | 4 +- docs/components/179-risk-meter.md | 4 +- docs/components/180-order-confirm-dialog.md | 6 +- docs/components/181-option-chain.md | 8 +- docs/components/182-greeks-table.md | 6 +- docs/components/183-payoff-diagram.md | 6 +- docs/components/184-watchlist.md | 8 +- docs/components/185-symbol-search.md | 8 +- docs/components/186-symbol-badge.md | 6 +- docs/components/187-market-heatmap.md | 6 +- docs/components/188-screener.md | 10 +- docs/components/189-market-overview.md | 6 +- docs/components/190-index-card.md | 6 +- docs/components/191-market-status.md | 4 +- docs/components/192-trading-session-clock.md | 6 +- docs/components/193-economic-calendar.md | 8 +- docs/components/194-earnings-calendar.md | 8 +- docs/components/195-news-feed.md | 8 +- docs/components/196-sentiment-gauge.md | 4 +- docs/components/197-portfolio-summary.md | 6 +- docs/components/198-asset-allocation-chart.md | 6 +- docs/components/199-performance-chart.md | 4 +- docs/components/200-currency-converter.md | 8 +- docs/components/201-chat-container.md | 4 +- docs/components/202-message-list.md | 2 +- docs/components/203-message-bubble.md | 2 +- docs/components/204-message-header.md | 2 +- docs/components/205-streaming-text.md | 2 +- docs/components/206-markdown-view.md | 2 +- docs/components/207-code-block.md | 2 +- docs/components/208-table-block.md | 2 +- docs/components/209-image-grid.md | 2 +- docs/components/210-file-message.md | 2 +- docs/components/211-audio-message.md | 2 +- docs/components/212-link-preview-card.md | 4 +- docs/components/213-quote-reply.md | 2 +- docs/components/214-message-actions.md | 2 +- docs/components/215-message-editor.md | 2 +- docs/components/216-branch-navigator.md | 2 +- docs/components/217-error-message.md | 4 +- docs/components/218-thinking-block.md | 2 +- .../components/219-scroll-to-bottom-button.md | 2 +- docs/components/220-typing-indicator.md | 2 +- docs/components/221-prompt-composer.md | 4 +- docs/components/222-attachment-chip.md | 4 +- docs/components/223-paste-image-preview.md | 6 +- docs/components/224-drag-drop-overlay.md | 4 +- docs/components/225-voice-input-button.md | 8 +- docs/components/226-voice-waveform.md | 4 +- docs/components/227-slash-command-menu.md | 8 +- docs/components/228-context-mention-menu.md | 10 +- docs/components/229-context-chips.md | 6 +- docs/components/230-model-selector.md | 6 +- docs/components/231-mode-selector.md | 6 +- docs/components/232-tool-toggle-menu.md | 6 +- docs/components/233-parameter-panel.md | 6 +- docs/components/234-token-counter.md | 4 +- docs/components/235-context-window-meter.md | 6 +- docs/components/236-send-button.md | 4 +- docs/components/237-citation-badge.md | 8 +- docs/components/238-source-card.md | 8 +- docs/components/239-sources-panel.md | 8 +- docs/components/240-search-progress.md | 6 +- docs/components/241-conversation-list.md | 8 +- docs/components/242-conversation-item.md | 6 +- docs/components/243-conversation-search.md | 6 +- docs/components/244-project-list.md | 12 +- .../components/245-project-knowledge-panel.md | 6 +- docs/components/246-welcome-screen.md | 6 +- docs/components/247-suggestion-chips.md | 4 +- docs/components/248-prompt-library.md | 8 +- docs/components/249-capability-cards.md | 6 +- docs/components/250-feedback-form.md | 6 +- docs/components/251-date-separator.md | 4 +- docs/components/252-thinking-indicator.md | 4 +- docs/components/253-stop-generating-button.md | 4 +- docs/components/254-system-prompt-editor.md | 4 +- docs/components/255-cost-estimator.md | 4 +- .../256-shared-conversation-view.md | 6 +- docs/components/257-conversation-export.md | 8 +- docs/components/258-regenerate-menu.md | 4 +- docs/components/259-artifact-card.md | 4 +- docs/components/260-chat-empty-state.md | 4 +- docs/components/261-tool-call-card.md | 8 +- docs/components/262-tool-call-group.md | 10 +- docs/components/263-tool-approval-dialog.md | 4 +- docs/components/264-permission-prompt.md | 6 +- docs/components/265-agent-step-list.md | 12 +- docs/components/266-agent-plan.md | 10 +- docs/components/267-agent-status.md | 4 +- docs/components/268-agent-progress.md | 4 +- docs/components/269-sub-agent-tree.md | 8 +- docs/components/270-file-change-card.md | 8 +- docs/components/271-multi-file-diff-review.md | 8 +- docs/components/272-command-execution-card.md | 6 +- docs/components/273-screenshot-stream.md | 8 +- docs/components/274-artifact-panel.md | 6 +- .../275-artifact-version-switcher.md | 6 +- docs/components/276-sandbox-status.md | 8 +- docs/components/277-mcp-server-list.md | 6 +- docs/components/278-memory-panel.md | 6 +- docs/components/279-checkpoint-list.md | 6 +- docs/components/280-human-input-request.md | 4 +- docs/components/281-interrupt-button.md | 4 +- docs/components/282-token-usage-chart.md | 6 +- docs/components/283-cost-breakdown.md | 8 +- docs/components/284-trace-viewer.md | 10 +- docs/components/285-eval-result-table.md | 8 +- docs/components/286-tool-registry-panel.md | 10 +- docs/components/287-dots-loader.md | 4 +- docs/components/288-bars-loader.md | 4 +- docs/components/289-pulse-loader.md | 4 +- docs/components/290-orbit-loader.md | 4 +- docs/components/291-wave-loader.md | 4 +- docs/components/292-shimmer.md | 4 +- docs/components/293-count-up.md | 4 +- docs/components/294-typewriter.md | 4 +- docs/components/295-shimmer-text.md | 4 +- docs/components/296-stagger.md | 4 +- docs/components/297-reorder-transition.md | 4 +- docs/components/298-layout-transition.md | 4 +- docs/components/299-presence.md | 4 +- docs/components/300-blink-highlight.md | 4 +- docs/components/301-diff-viewer.md | 6 +- docs/components/302-inline-diff.md | 4 +- docs/components/303-three-way-merge.md | 6 +- docs/components/304-conflict-resolver.md | 6 +- docs/components/305-changes-list.md | 12 +- docs/components/306-commit-input.md | 6 +- docs/components/307-commit-graph.md | 8 +- docs/components/308-commit-list.md | 8 +- docs/components/309-branch-selector.md | 6 +- docs/components/310-branch-list.md | 10 +- docs/components/311-tag-list.md | 8 +- docs/components/312-stash-list.md | 8 +- docs/components/313-blame-view.md | 8 +- docs/components/314-file-history.md | 8 +- docs/components/315-pull-request-card.md | 8 +- docs/components/316-review-comment.md | 8 +- docs/components/317-diff-stat.md | 4 +- docs/components/318-git-status-badge.md | 4 +- docs/components/319-command-block.md | 6 +- docs/components/320-ansi-text.md | 4 +- docs/components/321-terminal.md | 6 +- docs/components/322-terminal-tabs.md | 6 +- docs/components/323-terminal-search.md | 10 +- docs/components/324-command-history.md | 8 +- docs/components/325-log-viewer.md | 8 +- docs/components/326-process-list.md | 8 +- docs/components/327-code-viewer.md | 6 +- docs/components/328-minimap.md | 6 +- docs/components/329-find-widget.md | 6 +- docs/components/330-search-panel.md | 8 +- docs/components/331-problems-panel.md | 8 +- docs/components/332-output-panel.md | 8 +- docs/components/333-symbol-outline.md | 10 +- docs/components/334-completion-menu.md | 10 +- docs/components/335-debug-toolbar.md | 14 +- docs/components/336-breakpoint-list.md | 6 +- docs/components/337-call-stack.md | 8 +- docs/components/338-variables-panel.md | 8 +- docs/components/339-hex-viewer.md | 6 +- docs/components/340-flamegraph.md | 8 +- docs/components/341-image-cropper.md | 6 +- docs/components/342-image-annotator.md | 8 +- docs/components/343-image-thumbnail.md | 4 +- docs/components/344-lightbox.md | 8 +- docs/components/345-video-player.md | 6 +- docs/components/346-video-scrubber.md | 6 +- docs/components/347-video-thumbnail-strip.md | 6 +- docs/components/348-audio-player.md | 6 +- docs/components/349-audio-waveform.md | 6 +- docs/components/350-audio-spectrum.md | 4 +- docs/components/351-volume-control.md | 4 +- docs/components/352-playback-speed-control.md | 4 +- docs/components/353-media-controls.md | 8 +- docs/components/354-playlist.md | 6 +- docs/components/355-subtitle-editor.md | 8 +- .../356-screen-recorder-controls.md | 14 +- docs/components/357-camera-preview.md | 6 +- docs/components/358-mic-level-meter.md | 4 +- docs/components/359-device-selector.md | 12 +- docs/components/360-image-compare.md | 4 +- docs/components/361-kanban-board.md | 8 +- docs/components/362-task-list.md | 10 +- docs/components/363-task-item.md | 6 +- docs/components/364-task-detail-panel.md | 8 +- docs/components/365-subtask-list.md | 6 +- docs/components/368-sprint-board.md | 10 +- docs/components/369-burndown-chart.md | 6 +- docs/components/370-priority-indicator.md | 6 +- docs/components/371-status-select.md | 6 +- docs/components/372-assignee-picker.md | 6 +- docs/components/373-due-date-picker.md | 6 +- docs/components/374-label-manager.md | 6 +- docs/components/375-issue-card.md | 8 +- docs/components/376-issue-id-badge.md | 4 +- docs/components/377-milestone-progress.md | 6 +- docs/components/378-workload-view.md | 6 +- docs/components/379-time-tracker.md | 6 +- docs/components/380-pomodoro-timer.md | 6 +- docs/components/381-file-explorer.md | 12 +- docs/components/382-file-grid.md | 8 +- docs/components/383-file-icon.md | 4 +- docs/components/384-file-preview.md | 6 +- docs/components/385-path-bar.md | 4 +- docs/components/386-rename-inline.md | 4 +- .../components/387-file-operation-progress.md | 6 +- docs/components/388-transfer-queue.md | 8 +- docs/components/389-storage-usage.md | 6 +- docs/components/390-recent-files.md | 8 +- docs/components/391-archive-viewer.md | 8 +- docs/components/392-presence-avatars.md | 6 +- docs/components/394-reactions.md | 6 +- docs/components/395-share-dialog.md | 8 +- docs/components/396-permission-select.md | 6 +- docs/components/399-live-indicator.md | 4 +- docs/components/400-remote-cursor.md | 6 +- docs/components/401-channel-list.md | 6 +- docs/components/402-chat-message.md | 14 +- docs/components/403-unread-divider.md | 4 +- docs/components/404-read-receipt.md | 6 +- docs/components/405-member-list.md | 12 +- docs/components/406-user-profile-card.md | 8 +- docs/components/407-online-status.md | 4 +- docs/components/408-status-setter.md | 6 +- docs/components/409-call-controls.md | 6 +- docs/components/410-video-call-grid.md | 6 +- docs/components/411-incoming-call-dialog.md | 4 +- docs/components/412-emoji-picker.md | 4 +- docs/components/413-pinned-messages.md | 6 +- docs/components/414-thread-panel.md | 10 +- docs/components/415-mail-list.md | 8 +- docs/components/416-mail-reader.md | 8 +- docs/components/417-mail-composer.md | 10 +- docs/components/418-recipient-input.md | 6 +- docs/components/419-quoted-text.md | 4 +- docs/components/420-snooze-picker.md | 4 +- docs/components/421-calendar-month-view.md | 6 +- docs/components/422-calendar-week-view.md | 8 +- docs/components/423-calendar-day-view.md | 6 +- docs/components/424-agenda-view.md | 6 +- docs/components/425-year-view.md | 4 +- docs/components/426-event-chip.md | 4 +- docs/components/427-event-editor.md | 4 +- docs/components/428-event-popover.md | 6 +- docs/components/429-current-time-indicator.md | 4 +- docs/components/430-recurrence-editor.md | 8 +- docs/components/431-availability-picker.md | 6 +- docs/components/432-timezone-select.md | 4 +- docs/components/433-reminder-picker.md | 4 +- docs/components/434-attendee-list.md | 6 +- docs/components/435-week-picker.md | 6 +- docs/components/436-duration-picker.md | 6 +- docs/components/437-cron-editor.md | 6 +- docs/components/438-relative-time.md | 4 +- docs/components/439-countdown.md | 4 +- docs/components/440-stopwatch.md | 6 +- docs/components/442-stat-card.md | 6 +- docs/components/443-alert-list.md | 6 +- docs/components/444-incident-card.md | 6 +- docs/components/445-uptime-bar.md | 6 +- docs/components/446-service-status.md | 6 +- docs/components/447-log-stream.md | 8 +- docs/components/448-trace-waterfall.md | 6 +- docs/components/449-resource-gauge.md | 6 +- docs/components/450-system-monitor.md | 6 +- .../451-refresh-interval-selector.md | 4 +- docs/components/452-dashboard-filter-bar.md | 8 +- docs/components/453-json-viewer.md | 6 +- docs/components/454-json-editor.md | 4 +- docs/components/455-schema-tree.md | 8 +- docs/components/456-api-request-builder.md | 6 +- docs/components/457-response-viewer.md | 6 +- docs/components/458-key-value-input.md | 6 +- docs/components/461-settings-layout.md | 12 +- docs/components/462-settings-search.md | 6 +- docs/components/463-theme-selector.md | 6 +- docs/components/464-accent-color-picker.md | 4 +- docs/components/465-shortcut-recorder.md | 8 +- .../components/466-keyboard-shortcuts-list.md | 8 +- docs/components/467-login-form.md | 6 +- docs/components/468-oauth-buttons.md | 6 +- docs/components/469-two-factor-input.md | 4 +- docs/components/470-profile-editor.md | 6 +- docs/components/471-account-switcher.md | 6 +- docs/components/472-workspace-switcher.md | 6 +- docs/components/473-user-menu.md | 6 +- docs/components/474-session-list.md | 6 +- docs/components/475-api-key-manager.md | 8 +- docs/components/476-usage-quota.md | 6 +- docs/components/477-onboarding-wizard.md | 6 +- docs/components/478-coachmark.md | 6 +- docs/components/479-whats-new-dialog.md | 6 +- docs/components/480-feedback-widget.md | 6 +- docs/components/481-split-button.md | 6 +- docs/components/482-copy-button.md | 4 +- docs/components/483-hold-to-confirm.md | 4 +- docs/components/484-bulk-action-bar.md | 6 +- docs/components/485-scrub-input.md | 4 +- docs/components/486-currency-input.md | 4 +- docs/components/487-masked-input.md | 4 +- docs/components/488-mention-input.md | 12 +- docs/components/489-choice-chips.md | 6 +- docs/components/490-knob.md | 4 +- docs/components/491-vertical-slider.md | 4 +- docs/components/492-color-palette.md | 6 +- docs/components/493-signature-pad.md | 8 +- docs/components/494-banner.md | 6 +- docs/components/495-undo-toast.md | 4 +- docs/components/496-masonry.md | 4 +- docs/components/497-resizable-panel-group.md | 6 +- docs/components/498-dock-panel.md | 8 +- docs/components/499-qr-code.md | 4 +- docs/components/500-activity-feed.md | 6 +- docs/upstream.md | 20 +- example_account_test.go | 240 --------------- example_agent_test.go | 243 --------------- example_calendar_test.go | 262 ----------------- example_charts1_test.go | 221 -------------- example_charts2_test.go | 180 ------------ example_chatinput_test.go | 241 --------------- example_chatsession_test.go | 231 --------------- example_choice_test.go | 133 --------- example_code_test.go | 276 ----------------- example_datetime_test.go | 110 ------- example_devtools_test.go | 218 -------------- example_display_test.go | 136 --------- example_essentials_test.go | 248 ---------------- example_files_test.go | 177 ----------- example_form_test.go | 124 -------- example_git_test.go | 263 ----------------- example_input_test.go | 130 -------- example_markets_test.go | 255 ---------------- example_media_test.go | 246 ---------------- example_messaging_test.go | 242 --------------- example_motion_test.go | 221 -------------- example_navigation_test.go | 155 ---------- example_overlay_test.go | 167 ----------- example_projects_test.go | 242 --------------- example_trading_test.go | 221 -------------- examples/gallery/app.go | 48 +-- examples/gallery/catalog.go | 4 +- examples/gallery/composite_files.go | 96 +++--- examples/gallery/composite_form.go | 44 +-- examples/gallery/composite_settings.go | 82 +++--- examples/gallery/nav.go | 31 +- examples/gallery/overture.go | 46 +-- examples/gallery/page_view.go | 39 +-- examples/gallery/pages_account.go | 28 +- examples/gallery/pages_account_auth.go | 46 +-- examples/gallery/pages_account_code.go | 128 ++++---- examples/gallery/pages_account_more.go | 70 ++--- examples/gallery/pages_agent.go | 89 +++--- examples/gallery/pages_agent_code.go | 138 ++++----- examples/gallery/pages_agent_more.go | 96 +++--- examples/gallery/pages_calendar_code.go | 156 +++++----- examples/gallery/pages_calendar_parts.go | 84 +++--- examples/gallery/pages_calendar_time.go | 52 ++-- examples/gallery/pages_calendar_views.go | 90 +++--- examples/gallery/pages_charts1.go | 18 +- examples/gallery/pages_charts1_code.go | 128 ++++---- examples/gallery/pages_charts1_flow.go | 43 +-- examples/gallery/pages_charts1_series.go | 38 +-- examples/gallery/pages_charts1_stats.go | 40 +-- examples/gallery/pages_charts1_test.go | 4 +- examples/gallery/pages_charts2.go | 22 +- examples/gallery/pages_charts2_code.go | 88 +++--- .../gallery/pages_charts2_distribution.go | 17 +- examples/gallery/pages_charts2_parts.go | 47 +-- examples/gallery/pages_charts2_stats.go | 48 +-- examples/gallery/pages_charts2_test.go | 4 +- examples/gallery/pages_chatinput.go | 40 +-- examples/gallery/pages_chatinput_compose.go | 47 +-- examples/gallery/pages_chatinput_context.go | 43 +-- examples/gallery/pages_chatinput_sources.go | 55 ++-- examples/gallery/pages_chatinput_test.go | 10 +- examples/gallery/pages_chatmsg.go | 87 +++--- examples/gallery/pages_chatmsg_actions.go | 94 +++--- examples/gallery/pages_chatmsg_blocks.go | 45 +-- examples/gallery/pages_chatmsg_test.go | 10 +- examples/gallery/pages_chatsession.go | 114 +++---- examples/gallery/pages_chatsession_demos.go | 71 ++--- examples/gallery/pages_chatsession_more.go | 81 ++--- examples/gallery/pages_choice.go | 167 +++++------ examples/gallery/pages_choice_test.go | 4 +- examples/gallery/pages_code.go | 58 ++-- examples/gallery/pages_code_debug.go | 117 ++++---- examples/gallery/pages_code_snippets.go | 158 +++++----- examples/gallery/pages_code_tools.go | 82 +++--- examples/gallery/pages_data.go | 23 +- examples/gallery/pages_data_code.go | 70 ++--- examples/gallery/pages_data_collections.go | 52 ++-- examples/gallery/pages_data_content.go | 60 ++-- examples/gallery/pages_data_test.go | 4 +- examples/gallery/pages_datetime.go | 169 +++++------ examples/gallery/pages_devtools_code.go | 104 +++---- examples/gallery/pages_devtools_editors.go | 41 +-- examples/gallery/pages_devtools_status.go | 62 ++-- examples/gallery/pages_devtools_streams.go | 42 +-- examples/gallery/pages_devtools_test.go | 10 +- examples/gallery/pages_display.go | 116 ++++---- examples/gallery/pages_display_media.go | 37 +-- examples/gallery/pages_display_parts.go | 4 +- examples/gallery/pages_essentials.go | 110 +++---- examples/gallery/pages_essentials_demos.go | 49 +-- examples/gallery/pages_essentials_more.go | 74 ++--- examples/gallery/pages_feedback.go | 40 +-- examples/gallery/pages_feedback_demos.go | 94 +++--- examples/gallery/pages_files_browser.go | 132 +++++---- examples/gallery/pages_files_code.go | 110 +++---- examples/gallery/pages_files_collaboration.go | 63 ++-- examples/gallery/pages_files_test.go | 13 +- examples/gallery/pages_form.go | 120 ++++---- examples/gallery/pages_git.go | 138 ++++----- examples/gallery/pages_git_cards.go | 37 +-- examples/gallery/pages_git_demos.go | 121 ++++---- examples/gallery/pages_input.go | 121 ++++---- examples/gallery/pages_layout.go | 134 ++++----- examples/gallery/pages_layout_test.go | 4 +- examples/gallery/pages_markets.go | 149 +++++----- examples/gallery/pages_markets_1.go | 83 +++--- examples/gallery/pages_markets_2.go | 71 ++--- examples/gallery/pages_markets_3.go | 65 ++-- examples/gallery/pages_markets_4.go | 71 ++--- examples/gallery/pages_markets_test.go | 8 +- examples/gallery/pages_media.go | 9 +- examples/gallery/pages_media_av.go | 38 +-- examples/gallery/pages_media_code.go | 130 ++++---- examples/gallery/pages_media_devices.go | 71 ++--- examples/gallery/pages_media_image.go | 39 +-- examples/gallery/pages_messaging.go | 136 ++++----- examples/gallery/pages_messaging_calls.go | 20 +- examples/gallery/pages_messaging_chat.go | 62 ++-- examples/gallery/pages_messaging_mail.go | 57 ++-- examples/gallery/pages_messaging_people.go | 70 ++--- examples/gallery/pages_messaging_test.go | 11 +- examples/gallery/pages_mktchart.go | 4 +- examples/gallery/pages_mktchart_code.go | 160 +++++----- examples/gallery/pages_mktchart_demo.go | 96 +++--- examples/gallery/pages_mktchart_state.go | 84 +++--- examples/gallery/pages_mktchart_test.go | 5 +- examples/gallery/pages_motion.go | 116 ++++---- examples/gallery/pages_motion_fx.go | 65 ++-- examples/gallery/pages_motion_ops.go | 82 +++--- examples/gallery/pages_motion_test.go | 6 +- examples/gallery/pages_navigation.go | 172 +++++------ examples/gallery/pages_overlay.go | 164 ++++++----- examples/gallery/pages_projects.go | 48 +-- examples/gallery/pages_projects_demos.go | 74 ++--- examples/gallery/pages_projects_fields.go | 36 +-- .../gallery/pages_projects_fields_demos.go | 52 ++-- examples/gallery/pages_projects_test.go | 5 +- examples/gallery/pages_projects_time.go | 36 +-- examples/gallery/pages_projects_time_demos.go | 36 ++- examples/gallery/pages_trading.go | 90 +++--- examples/gallery/pages_trading_code.go | 124 ++++---- examples/gallery/pages_trading_orders.go | 86 +++--- examples/gallery/web.go | 2 +- examples/gallery/web_input.go | 10 +- examples/gallery/web_keys.go | 4 +- examples/gallery/web_test.go | 67 ++--- examples/gallery/zz_account_test.go | 11 +- examples/minimal/main.go | 9 +- activity_feed.go => feedback/activity_feed.go | 42 +-- .../activity_feed_test.go | 22 +- alert.go => feedback/alert.go | 80 +---- alert_test.go => feedback/alert_test.go | 28 +- bars_loader.go => feedback/bars_loader.go | 4 +- .../bars_loader_test.go | 6 +- .../blink_highlight.go | 15 +- .../blink_highlight_test.go | 6 +- .../cost_breakdown.go | 40 +-- .../cost_breakdown_test.go | 14 +- count_up.go => feedback/count_up.go | 7 +- count_up_test.go => feedback/count_up_test.go | 12 +- feedback/doc.go | 2 + dots_loader.go => feedback/dots_loader.go | 13 +- .../dots_loader_test.go | 6 +- empty_state.go => feedback/empty_state.go | 26 +- .../empty_state_test.go | 11 +- .../eval_result_table.go | 36 +-- .../eval_result_table_test.go | 29 +- feedback/example_essentials_test.go | 34 +++ .../example_feedback_test.go | 72 +++-- feedback/example_motion_test.go | 223 ++++++++++++++ .../feedback_review_test.go | 19 +- feedback/helpers_test.go | 72 +++++ .../interrupt_button.go | 27 +- .../interrupt_button_test.go | 10 +- .../layout_transition.go | 7 +- .../layout_transition_test.go | 8 +- loaders_test.go => feedback/loaders_test.go | 10 +- .../loading_overlay.go | 18 +- .../loading_overlay_test.go | 15 +- .../notification_center.go | 40 +-- .../notification_center_test.go | 8 +- orbit_loader.go => feedback/orbit_loader.go | 4 +- .../orbit_loader_test.go | 6 +- presence.go => feedback/presence.go | 7 +- presence_test.go => feedback/presence_test.go | 6 +- progress.go => feedback/progress.go | 54 ++-- progress_test.go => feedback/progress_test.go | 23 +- pulse_loader.go => feedback/pulse_loader.go | 4 +- .../pulse_loader_test.go | 6 +- .../reorder_transition.go | 7 +- .../reorder_transition_test.go | 10 +- result.go => feedback/result.go | 24 +- result_test.go => feedback/result_test.go | 12 +- shimmer.go => feedback/shimmer.go | 13 +- shimmer_test.go => feedback/shimmer_test.go | 6 +- shimmer_text.go => feedback/shimmer_text.go | 8 +- .../shimmer_text_test.go | 10 +- skeleton.go => feedback/skeleton.go | 11 +- skeleton_test.go => feedback/skeleton_test.go | 11 +- stagger.go => feedback/stagger.go | 7 +- stagger_test.go => feedback/stagger_test.go | 8 +- .../status_indicator.go | 17 +- .../status_indicator_test.go | 12 +- toast.go => feedback/toast.go | 24 +- toast_test.go => feedback/toast_test.go | 21 +- .../token_usage_chart.go | 25 +- .../token_usage_chart_test.go | 21 +- .../tool_registry_panel.go | 39 +-- .../tool_registry_panel_test.go | 25 +- trace_viewer.go => feedback/trace_viewer.go | 54 ++-- .../trace_viewer_test.go | 12 +- typewriter.go => feedback/typewriter.go | 15 +- .../typewriter_test.go | 14 +- undo_toast.go => feedback/undo_toast.go | 18 +- .../undo_toast_test.go | 11 +- wave_loader.go => feedback/wave_loader.go | 4 +- .../wave_loader_test.go | 6 +- archive_viewer.go => files/archive_viewer.go | 40 +-- .../archive_viewer_test.go | 15 +- files/doc.go | 2 + files/example_files_test.go | 181 ++++++++++++ file_explorer.go => files/file_explorer.go | 35 ++- .../file_explorer_test.go | 10 +- file_grid.go => files/file_grid.go | 20 +- file_grid_test.go => files/file_grid_test.go | 12 +- file_icon.go => files/file_icon.go | 17 +- file_icon_test.go => files/file_icon_test.go | 9 +- .../file_operation_progress.go | 38 +-- .../file_operation_progress_test.go | 6 +- file_preview.go => files/file_preview.go | 50 ++-- .../file_preview_test.go | 17 +- files/helpers_test.go | 24 ++ live_indicator.go => files/live_indicator.go | 36 +-- .../live_indicator_test.go | 8 +- path_bar.go => files/path_bar.go | 30 +- path_bar_test.go => files/path_bar_test.go | 6 +- .../permission_select.go | 17 +- .../permission_select_test.go | 8 +- .../presence_avatars.go | 31 +- .../presence_avatars_test.go | 13 +- reactions.go => files/reactions.go | 34 ++- reactions_test.go => files/reactions_test.go | 6 +- recent_files.go => files/recent_files.go | 35 ++- .../recent_files_test.go | 8 +- remote_cursor.go => files/remote_cursor.go | 16 +- .../remote_cursor_test.go | 14 +- rename_inline.go => files/rename_inline.go | 14 +- .../rename_inline_test.go | 9 +- share_dialog.go => files/share_dialog.go | 47 +-- .../share_dialog_test.go | 14 +- storage_usage.go => files/storage_usage.go | 30 +- .../storage_usage_test.go | 6 +- transfer_queue.go => files/transfer_queue.go | 42 +-- .../transfer_queue_test.go | 8 +- .../asset_allocation_chart.go | 51 ++-- .../asset_allocation_chart_test.go | 21 +- bid_ask_bar.go => finance/bid_ask_bar.go | 22 +- .../bid_ask_bar_test.go | 8 +- .../currency_converter.go | 49 +-- .../currency_converter_test.go | 5 +- depth_ladder.go => finance/depth_ladder.go | 41 +-- .../depth_ladder_test.go | 14 +- finance/doc.go | 2 + .../earnings_calendar.go | 50 ++-- .../earnings_calendar_test.go | 18 +- .../economic_calendar.go | 74 ++--- .../economic_calendar_test.go | 5 +- finance/example_markets_test.go | 247 ++++++++++++++++ finance/example_trading_test.go | 223 ++++++++++++++ greeks_table.go => finance/greeks_table.go | 26 +- .../greeks_table_test.go | 6 +- finance/helpers_test.go | 31 ++ index_card.go => finance/index_card.go | 20 +- .../index_card_test.go | 5 +- level2_quotes.go => finance/level2_quotes.go | 38 +-- .../level2_quotes_test.go | 8 +- .../leverage_slider.go | 41 +-- .../leverage_slider_test.go | 7 +- .../margin_indicator.go | 32 +- .../margin_indicator_test.go | 8 +- .../market_overview.go | 27 +- .../market_overview_test.go | 5 +- market_status.go => finance/market_status.go | 27 +- .../market_status_test.go | 5 +- .../markets_fixture_test.go | 20 +- .../markets_regression_test.go | 65 ++-- news_feed.go => finance/news_feed.go | 33 ++- .../news_feed_test.go | 11 +- option_chain.go => finance/option_chain.go | 61 ++-- .../option_chain_test.go | 8 +- order_book.go => finance/order_book.go | 45 +-- .../order_book_test.go | 16 +- .../order_confirm_dialog.go | 31 +- .../order_confirm_dialog_test.go | 17 +- order_entry.go => finance/order_entry.go | 72 ++--- .../order_entry_test.go | 6 +- order_table.go => finance/order_table.go | 66 +++-- .../order_table_test.go | 6 +- .../payoff_diagram.go | 42 +-- .../payoff_diagram_test.go | 21 +- .../performance_chart.go | 77 ++--- .../performance_chart_test.go | 27 +- pnl_display.go => finance/pnl_display.go | 48 +-- .../pnl_display_test.go | 6 +- .../portfolio_summary.go | 49 +-- .../portfolio_summary_test.go | 5 +- .../position_table.go | 42 +-- .../position_table_test.go | 19 +- .../price_change_badge.go | 26 +- .../price_change_badge_test.go | 6 +- finance/price_text.go | 84 ++++++ .../price_text_test.go | 27 +- .../quick_trade_buttons.go | 42 +-- .../quick_trade_buttons_test.go | 6 +- quote_card.go => finance/quote_card.go | 15 +- .../quote_card_test.go | 10 +- risk_meter.go => finance/risk_meter.go | 21 +- .../risk_meter_test.go | 8 +- screener.go => finance/screener.go | 60 ++-- screener_test.go => finance/screener_test.go | 5 +- .../sentiment_gauge.go | 41 +-- .../sentiment_gauge_test.go | 15 +- .../spread_indicator.go | 24 +- .../spread_indicator_test.go | 8 +- symbol_badge.go => finance/symbol_badge.go | 19 +- .../symbol_badge_test.go | 5 +- symbol_search.go => finance/symbol_search.go | 41 +-- .../symbol_search_test.go | 5 +- ticker_tape.go => finance/ticker_tape.go | 38 +-- .../ticker_tape_test.go | 10 +- .../time_and_sales.go | 57 ++-- .../time_and_sales_test.go | 10 +- .../trade_history_table.go | 48 +-- .../trade_history_table_test.go | 13 +- .../trading_session_clock.go | 15 +- .../trading_session_clock_test.go | 5 +- watchlist.go => finance/watchlist.go | 38 +-- .../watchlist_test.go | 6 +- ansi_text.go => git/ansi_text.go | 9 +- ansi_text_test.go => git/ansi_text_test.go | 9 +- blame_view.go => git/blame_view.go | 19 +- blame_view_test.go => git/blame_view_test.go | 10 +- branch_list.go => git/branch_list.go | 30 +- .../branch_list_test.go | 15 +- branch_selector.go => git/branch_selector.go | 39 +-- .../branch_selector_test.go | 8 +- changes_list.go => git/changes_list.go | 28 +- .../changes_list_test.go | 10 +- command_block.go => git/command_block.go | 40 +-- .../command_block_test.go | 23 +- commit_graph.go => git/commit_graph.go | 22 +- .../commit_graph_test.go | 8 +- commit_input.go => git/commit_input.go | 26 +- .../commit_input_test.go | 12 +- commit_list.go => git/commit_list.go | 24 +- .../commit_list_test.go | 19 +- .../conflict_resolver.go | 43 +-- .../conflict_resolver_test.go | 10 +- diff_stat.go => git/diff_stat.go | 13 +- diff_stat_test.go => git/diff_stat_test.go | 8 +- diff_viewer.go => git/diff_viewer.go | 31 +- .../diff_viewer_test.go | 9 +- git/doc.go | 2 + git/example_git_test.go | 266 +++++++++++++++++ file_history.go => git/file_history.go | 24 +- .../file_history_test.go | 19 +- .../git_status_badge.go | 15 +- .../git_status_badge_test.go | 10 +- git/helpers_test.go | 30 ++ inline_diff.go => git/inline_diff.go | 15 +- .../inline_diff_test.go | 8 +- .../pull_request_card.go | 44 +-- .../pull_request_card_test.go | 10 +- review_comment.go => git/review_comment.go | 27 +- .../review_comment_test.go | 10 +- stash_list.go => git/stash_list.go | 26 +- stash_list_test.go => git/stash_list_test.go | 19 +- tag_list.go => git/tag_list.go | 22 +- tag_list_test.go => git/tag_list_test.go | 15 +- three_way_merge.go => git/three_way_merge.go | 49 +-- .../three_way_merge_test.go | 6 +- go.mod | 2 +- banner.go => input/banner.go | 29 +- banner_test.go => input/banner_test.go | 20 +- .../baseline_states_test.go | 2 +- .../bulk_action_bar.go | 25 +- .../bulk_action_bar_test.go | 8 +- button.go => input/button.go | 17 +- button_group.go => input/button_group.go | 13 +- .../button_group_test.go | 11 +- button_test.go => input/button_test.go | 25 +- cascader.go => input/cascader.go | 39 +-- cascader_test.go => input/cascader_test.go | 8 +- checkbox.go => input/checkbox.go | 25 +- checkbox_test.go => input/checkbox_test.go | 12 +- .../choice_blocked_test.go | 6 +- choice_chips.go => input/choice_chips.go | 19 +- .../choice_chips_test.go | 8 +- .../choice_frame_test.go | 12 +- .../choice_review_test.go | 13 +- color_palette.go => input/color_palette.go | 19 +- .../color_palette_test.go | 8 +- color_picker.go => input/color_picker.go | 55 ++-- .../color_picker_test.go | 11 +- combobox.go => input/combobox.go | 33 ++- combobox_test.go => input/combobox_test.go | 8 +- copy_button.go => input/copy_button.go | 21 +- .../copy_button_test.go | 8 +- currency_input.go => input/currency_input.go | 12 +- .../currency_input_test.go | 10 +- input/doc.go | 2 + dock_panel.go => input/dock_panel.go | 20 +- .../dock_panel_test.go | 8 +- input/example_choice_test.go | 134 +++++++++ input/example_essentials_test.go | 209 +++++++++++++ input/example_form_test.go | 125 ++++++++ input/example_input_test.go | 131 +++++++++ file_drop_zone.go => input/file_drop_zone.go | 49 +-- .../file_drop_zone_test.go | 6 +- file_picker.go => input/file_picker.go | 23 +- .../file_picker_test.go | 6 +- form.go => input/form.go | 88 +++--- form_field.go => input/form_field.go | 17 +- .../form_field_test.go | 12 +- form_test.go => input/form_test.go | 10 +- input/helpers_test.go | 36 +++ .../hold_to_confirm.go | 25 +- .../hold_to_confirm_test.go | 8 +- icon_button.go => input/icon_button.go | 22 +- .../icon_button_test.go | 11 +- input_group.go => input/input_group.go | 11 +- .../input_group_test.go | 19 +- .../input_review2_test.go | 15 +- .../input_review_test.go | 23 +- knob.go => input/knob.go | 31 +- knob_test.go => input/knob_test.go | 8 +- masked_input.go => input/masked_input.go | 9 +- .../masked_input_test.go | 8 +- masonry.go => input/masonry.go | 6 +- masonry_test.go => input/masonry_test.go | 8 +- mention_input.go => input/mention_input.go | 23 +- .../mention_input_model.go | 2 +- .../mention_input_test.go | 8 +- menubar_test.go => input/menubar_test.go | 63 ++-- multi_select.go => input/multi_select.go | 81 ++--- .../multi_select_test.go | 6 +- number_input.go => input/number_input.go | 131 +++++---- .../number_input_test.go | 10 +- password_input.go => input/password_input.go | 13 +- .../password_input_test.go | 10 +- pin_input.go => input/pin_input.go | 19 +- pin_input_test.go => input/pin_input_test.go | 8 +- qr_code.go => input/qr_code.go | 13 +- .../qr_code_encoder.go | 2 +- .../qr_code_encoder_test.go | 2 +- qr_code_grid.go => input/qr_code_grid.go | 2 +- qr_code_test.go => input/qr_code_test.go | 11 +- radio_group.go => input/radio_group.go | 19 +- .../radio_group_test.go | 8 +- range_slider.go => input/range_slider.go | 37 +-- .../range_slider_test.go | 6 +- rating.go => input/rating.go | 23 +- rating_test.go => input/rating_test.go | 11 +- .../resizable_panel_group.go | 15 +- .../resizable_panel_group_test.go | 10 +- scrub_input.go => input/scrub_input.go | 15 +- .../scrub_input_test.go | 6 +- search_input.go => input/search_input.go | 27 +- .../search_input_test.go | 14 +- select.go => input/select.go | 73 ++--- select_test.go => input/select_test.go | 6 +- signature_pad.go => input/signature_pad.go | 29 +- .../signature_pad_test.go | 6 +- slider.go => input/slider.go | 105 +++---- .../slider_direction_test.go | 2 +- .../slider_grid_test.go | 6 +- .../slider_keys_test.go | 14 +- slider_test.go => input/slider_test.go | 8 +- spinner_test.go => input/spinner_test.go | 20 +- .../stable_keys_test.go | 8 +- switch.go => input/switch.go | 15 +- switch_test.go => input/switch_test.go | 10 +- tags_input.go => input/tags_input.go | 26 +- .../tags_input_test.go | 10 +- {testdata => input/testdata}/qr/hello-l.txt | 0 {testdata => input/testdata}/qr/long-h.txt | 0 {testdata => input/testdata}/qr/text-q.txt | 0 {testdata => input/testdata}/qr/url-m.txt | 0 {testdata => input/testdata}/qr/v8-l.txt | 0 {testdata => input/testdata}/qr/zh-m.txt | 0 text_area.go => input/text_area.go | 23 +- text_area_test.go => input/text_area_test.go | 12 +- text_input.go => input/text_input.go | 32 +- .../text_input_test.go | 17 +- toggle.go => input/toggle.go | 11 +- toggle_group.go => input/toggle_group.go | 24 +- .../toggle_group_test.go | 8 +- toggle_test.go => input/toggle_test.go | 11 +- transfer.go => input/transfer.go | 43 +-- transfer_test.go => input/transfer_test.go | 11 +- tree_select.go => input/tree_select.go | 41 +-- .../tree_select_test.go | 6 +- .../vertical_slider.go | 41 +-- .../vertical_slider_test.go | 8 +- internal/chart.go | 2 +- internal/chart_test.go | 2 +- internal/choice.go | 4 +- internal/git.go | 2 +- internal/git_test.go | 2 +- internal/paint.go | 2 +- .../snaptest/snaptest.go | 59 ++-- app_shell.go => layout/app_shell.go | 21 +- app_shell_test.go => layout/app_shell_test.go | 8 +- aspect_ratio.go => layout/aspect_ratio.go | 4 +- .../aspect_ratio_test.go | 12 +- container.go => layout/container.go | 7 +- container_test.go => layout/container_test.go | 19 +- divider.go => layout/divider.go | 15 +- divider_test.go => layout/divider_test.go | 21 +- layout/doc.go | 2 + .../example_layout_test.go | 68 ++--- grid.go => layout/grid.go | 10 +- grid_test.go => layout/grid_test.go | 14 +- layout/helpers_test.go | 24 ++ scroll_area.go => layout/scroll_area.go | 7 +- .../scroll_area_test.go | 14 +- split_pane.go => layout/split_pane.go | 17 +- .../split_pane_test.go | 14 +- stack.go => layout/stack.go | 10 +- stack_test.go => layout/stack_test.go | 16 +- status_bar.go => layout/status_bar.go | 39 +-- .../status_bar_test.go | 29 +- title_bar.go => layout/title_bar.go | 11 +- title_bar_test.go => layout/title_bar_test.go | 23 +- audio_player.go => media/audio_player.go | 20 +- .../audio_player_test.go | 6 +- audio_spectrum.go => media/audio_spectrum.go | 36 +-- .../audio_spectrum_test.go | 19 +- audio_waveform.go => media/audio_waveform.go | 22 +- .../audio_waveform_test.go | 21 +- camera_preview.go => media/camera_preview.go | 25 +- .../camera_preview_test.go | 6 +- media/device_selector.go | 80 +++++ .../device_selector_test.go | 6 +- media/doc.go | 2 + media/example_helpers_test.go | 8 + media/example_media_test.go | 245 +++++++++++++++ media/helpers_test.go | 61 ++++ .../image_annotator.go | 60 ++-- .../image_annotator_test.go | 6 +- image_compare.go => media/image_compare.go | 29 +- .../image_compare_test.go | 6 +- image_cropper.go => media/image_cropper.go | 29 +- .../image_cropper_geom.go | 2 +- .../image_cropper_test.go | 6 +- .../image_thumbnail.go | 21 +- .../image_thumbnail_test.go | 10 +- lightbox.go => media/lightbox.go | 30 +- lightbox_test.go => media/lightbox_test.go | 14 +- media_controls.go => media/media_controls.go | 20 +- .../media_controls_test.go | 6 +- .../media_palette_test.go | 4 +- .../mic_level_meter.go | 15 +- .../mic_level_meter_test.go | 6 +- .../playback_speed_control.go | 13 +- .../playback_speed_control_test.go | 6 +- playlist.go => media/playlist.go | 30 +- playlist_test.go => media/playlist_test.go | 6 +- .../screen_recorder_controls.go | 32 +- .../screen_recorder_controls_test.go | 8 +- .../subtitle_editor.go | 42 +-- .../subtitle_editor_test.go | 6 +- video_player.go => media/video_player.go | 39 +-- .../video_player_test.go | 6 +- video_scrubber.go => media/video_scrubber.go | 46 +-- .../video_scrubber_test.go | 8 +- .../video_thumbnail_strip.go | 29 +- .../video_thumbnail_strip_test.go | 8 +- volume_control.go => media/volume_control.go | 23 +- .../volume_control_test.go | 6 +- media_icons.go | 53 ---- .../call_controls.go | 30 +- .../call_controls_test.go | 11 +- channel_list.go => messaging/channel_list.go | 44 +-- .../channel_list_test.go | 2 +- chat_message.go => messaging/chat_message.go | 25 +- .../chat_message_test.go | 14 +- messaging/doc.go | 2 + emoji_data.go => messaging/emoji_data.go | 2 +- emoji_picker.go => messaging/emoji_picker.go | 40 +-- .../emoji_picker_test.go | 16 +- messaging/example_messaging_test.go | 247 ++++++++++++++++ messaging/helpers_test.go | 61 ++++ .../incoming_call_dialog.go | 42 +-- .../incoming_call_dialog_test.go | 8 +- .../mail_composer.go | 45 +-- .../mail_composer_test.go | 19 +- mail_list.go => messaging/mail_list.go | 31 +- .../mail_list_test.go | 19 +- mail_reader.go => messaging/mail_reader.go | 29 +- .../mail_reader_test.go | 8 +- member_list.go => messaging/member_list.go | 27 +- .../member_list_test.go | 10 +- .../messaging_calls_test.go | 9 +- messaging/messaging_icons.go | 19 ++ .../messaging_mail_test.go | 6 +- .../messaging_people_test.go | 6 +- .../messaging_regression_test.go | 6 +- .../messaging_small_test.go | 9 +- .../online_status.go | 15 +- .../online_status_test.go | 6 +- .../pinned_messages.go | 24 +- .../pinned_messages_test.go | 6 +- quoted_text.go => messaging/quoted_text.go | 20 +- .../quoted_text_test.go | 8 +- read_receipt.go => messaging/read_receipt.go | 24 +- .../read_receipt_test.go | 11 +- .../recipient_input.go | 39 +-- .../recipient_input_test.go | 16 +- .../snooze_picker.go | 33 ++- .../snooze_picker_test.go | 8 +- .../status_setter.go | 41 +-- .../status_setter_test.go | 8 +- thread_panel.go => messaging/thread_panel.go | 29 +- .../thread_panel_test.go | 8 +- .../unread_divider.go | 15 +- .../unread_divider_test.go | 6 +- .../user_profile_card.go | 20 +- .../user_profile_card_test.go | 13 +- .../video_call_grid.go | 29 +- .../video_call_grid_test.go | 6 +- messaging_icons.go | 15 - breadcrumb.go => navigation/breadcrumb.go | 35 +-- .../breadcrumb_test.go | 11 +- .../command_palette.go | 35 +-- .../command_palette_test.go | 11 +- navigation/doc.go | 2 + navigation/example_navigation_test.go | 156 ++++++++++ navigation/helpers_test.go | 59 ++++ menubar.go => navigation/menubar.go | 19 +- .../menubar_panel.go | 31 +- .../navigation_menu.go | 45 +-- .../navigation_menu_test.go | 11 +- .../navigation_review_test.go | 14 +- pagination.go => navigation/pagination.go | 75 ++--- .../pagination_test.go | 16 +- .../segmented_control.go | 13 +- .../segmented_control_test.go | 13 +- sidebar.go => navigation/sidebar.go | 39 +-- sidebar_test.go => navigation/sidebar_test.go | 17 +- steps.go => navigation/steps.go | 35 +-- steps_test.go => navigation/steps_test.go | 10 +- tabs.go => navigation/tabs.go | 27 +- tabs_test.go => navigation/tabs_test.go | 29 +- toolbar.go => navigation/toolbar.go | 23 +- toolbar_test.go => navigation/toolbar_test.go | 11 +- alert_dialog.go => overlay/alert_dialog.go | 46 +-- .../alert_dialog_test.go | 9 +- context_menu.go => overlay/context_menu.go | 4 +- .../context_menu_test.go | 13 +- dialog.go => overlay/dialog.go | 47 +-- dialog_test.go => overlay/dialog_test.go | 44 +-- overlay/doc.go | 2 + drawer.go => overlay/drawer.go | 27 +- drawer_test.go => overlay/drawer_test.go | 19 +- dropdown_menu.go => overlay/dropdown_menu.go | 4 +- .../dropdown_menu_test.go | 15 +- overlay/example_essentials_test.go | 23 ++ overlay/example_overlay_test.go | 169 +++++++++++ overlay/helpers_test.go | 30 ++ hover_card.go => overlay/hover_card.go | 12 +- .../hover_card_test.go | 19 +- overlay.go => overlay/overlay.go | 79 ++--- .../overlay_fit_test.go | 17 +- .../overlay_snapshot_test.go | 2 +- overlay_test.go => overlay/overlay_test.go | 35 ++- popconfirm.go => overlay/popconfirm.go | 30 +- .../popconfirm_test.go | 19 +- popover.go => overlay/popover.go | 27 +- popover_test.go => overlay/popover_test.go | 25 +- popup_menu.go => overlay/popup_menu.go | 50 ++-- split_button.go => overlay/split_button.go | 36 +-- .../split_button_test.go | 13 +- tooltip.go => overlay/tooltip.go | 37 +-- tooltip_test.go => overlay/tooltip_test.go | 11 +- price_text.go | 171 ----------- .../assignee_picker.go | 58 ++-- .../assignee_picker_test.go | 17 +- .../burndown_chart.go | 52 ++-- .../burndown_chart_test.go | 22 +- project/doc.go | 2 + .../due_date_picker.go | 45 +-- .../due_date_picker_test.go | 10 +- project/example_projects_test.go | 247 ++++++++++++++++ project/helpers_test.go | 55 ++++ issue_card.go => project/issue_card.go | 38 +-- .../issue_card_test.go | 8 +- .../issue_id_badge.go | 29 +- .../issue_id_badge_test.go | 10 +- kanban_board.go => project/kanban_board.go | 27 +- .../kanban_board_test.go | 8 +- label_manager.go => project/label_manager.go | 53 ++-- .../label_manager_test.go | 9 +- .../milestone_progress.go | 27 +- .../milestone_progress_test.go | 6 +- .../pomodoro_timer.go | 39 +-- .../pomodoro_timer_test.go | 8 +- .../priority_indicator.go | 13 +- .../priority_indicator_test.go | 11 +- projects.go => project/projects.go | 25 +- projects_test.go => project/projects_test.go | 13 +- sprint_board.go => project/sprint_board.go | 47 +-- .../sprint_board_test.go | 12 +- status_select.go => project/status_select.go | 28 +- .../status_select_test.go | 8 +- subtask_list.go => project/subtask_list.go | 28 +- .../subtask_list_test.go | 6 +- .../task_detail_panel.go | 48 +-- .../task_detail_panel_test.go | 8 +- task_item.go => project/task_item.go | 29 +- .../task_item_test.go | 6 +- task_list.go => project/task_list.go | 35 +-- .../task_list_test.go | 10 +- time_tracker.go => project/time_tracker.go | 39 +-- .../time_tracker_test.go | 15 +- workload_view.go => project/workload_view.go | 24 +- .../workload_view_test.go | 7 +- system_monitor.go | 81 ----- week_picker.go | 96 ------ 1822 files changed, 24568 insertions(+), 21314 deletions(-) rename accent_color_picker.go => account/accent_color_picker.go (76%) rename accent_color_picker_test.go => account/accent_color_picker_test.go (97%) rename account_switcher.go => account/account_switcher.go (74%) rename account_switcher_test.go => account/account_switcher_test.go (99%) rename api_key_manager.go => account/api_key_manager.go (69%) rename api_key_manager_test.go => account/api_key_manager_test.go (99%) rename coachmark.go => account/coachmark.go (67%) rename coachmark_test.go => account/coachmark_test.go (87%) create mode 100644 account/doc.go create mode 100644 account/example_account_test.go rename feedback_widget.go => account/feedback_widget.go (66%) rename feedback_widget_test.go => account/feedback_widget_test.go (92%) create mode 100644 account/helpers_test.go rename keyboard_shortcuts_list.go => account/keyboard_shortcuts_list.go (86%) rename keyboard_shortcuts_list_test.go => account/keyboard_shortcuts_list_test.go (99%) rename login_form.go => account/login_form.go (68%) rename login_form_test.go => account/login_form_test.go (99%) rename oauth_buttons.go => account/oauth_buttons.go (82%) rename oauth_buttons_test.go => account/oauth_buttons_test.go (99%) rename onboarding_wizard.go => account/onboarding_wizard.go (65%) rename onboarding_wizard_test.go => account/onboarding_wizard_test.go (93%) rename profile_editor.go => account/profile_editor.go (63%) rename profile_editor_test.go => account/profile_editor_test.go (99%) rename session_list.go => account/session_list.go (71%) rename session_list_test.go => account/session_list_test.go (99%) rename settings_layout.go => account/settings_layout.go (79%) rename settings_layout_test.go => account/settings_layout_test.go (92%) rename settings_search.go => account/settings_search.go (90%) rename settings_search_test.go => account/settings_search_test.go (97%) rename shortcut_recorder.go => account/shortcut_recorder.go (85%) rename shortcut_recorder_test.go => account/shortcut_recorder_test.go (96%) rename theme_selector.go => account/theme_selector.go (76%) rename theme_selector_test.go => account/theme_selector_test.go (77%) rename two_factor_input.go => account/two_factor_input.go (71%) rename two_factor_input_test.go => account/two_factor_input_test.go (99%) rename usage_quota.go => account/usage_quota.go (65%) rename usage_quota_test.go => account/usage_quota_test.go (99%) rename user_menu.go => account/user_menu.go (75%) rename user_menu_test.go => account/user_menu_test.go (99%) rename whats_new_dialog.go => account/whats_new_dialog.go (58%) rename whats_new_dialog_test.go => account/whats_new_dialog_test.go (99%) rename workspace_switcher.go => account/workspace_switcher.go (77%) rename workspace_switcher_test.go => account/workspace_switcher_test.go (99%) rename agent_diff.go => agent/agent_diff.go (75%) rename agent_parts.go => agent/agent_parts.go (79%) rename agent_plan.go => agent/agent_plan.go (66%) rename agent_plan_test.go => agent/agent_plan_test.go (96%) rename agent_progress.go => agent/agent_progress.go (70%) rename agent_progress_test.go => agent/agent_progress_test.go (90%) rename agent_status.go => agent/agent_status.go (76%) rename agent_status_test.go => agent/agent_status_test.go (91%) rename agent_step_list.go => agent/agent_step_list.go (88%) rename agent_step_list_test.go => agent/agent_step_list_test.go (96%) rename artifact_panel.go => agent/artifact_panel.go (78%) rename artifact_panel_test.go => agent/artifact_panel_test.go (95%) rename artifact_version_switcher.go => agent/artifact_version_switcher.go (72%) rename artifact_version_switcher_test.go => agent/artifact_version_switcher_test.go (96%) rename checkpoint_list.go => agent/checkpoint_list.go (73%) rename checkpoint_list_test.go => agent/checkpoint_list_test.go (96%) rename command_execution_card.go => agent/command_execution_card.go (69%) rename command_execution_card_test.go => agent/command_execution_card_test.go (97%) create mode 100644 agent/doc.go create mode 100644 agent/example_agent_test.go rename file_change_card.go => agent/file_change_card.go (65%) rename file_change_card_test.go => agent/file_change_card_test.go (96%) create mode 100644 agent/helpers_test.go rename human_input_request.go => agent/human_input_request.go (70%) rename human_input_request_test.go => agent/human_input_request_test.go (96%) rename mcp_server_list.go => agent/mcp_server_list.go (73%) rename mcp_server_list_test.go => agent/mcp_server_list_test.go (96%) rename memory_panel.go => agent/memory_panel.go (68%) rename memory_panel_test.go => agent/memory_panel_test.go (97%) rename multi_file_diff_review.go => agent/multi_file_diff_review.go (68%) rename multi_file_diff_review_test.go => agent/multi_file_diff_review_test.go (97%) rename permission_prompt.go => agent/permission_prompt.go (70%) rename permission_prompt_test.go => agent/permission_prompt_test.go (89%) rename sandbox_status.go => agent/sandbox_status.go (74%) rename sandbox_status_test.go => agent/sandbox_status_test.go (93%) rename screenshot_stream.go => agent/screenshot_stream.go (59%) rename screenshot_stream_test.go => agent/screenshot_stream_test.go (97%) rename sub_agent_tree.go => agent/sub_agent_tree.go (80%) rename sub_agent_tree_test.go => agent/sub_agent_tree_test.go (95%) rename tool_approval_dialog.go => agent/tool_approval_dialog.go (63%) rename tool_approval_dialog_test.go => agent/tool_approval_dialog_test.go (91%) rename tool_call_card.go => agent/tool_call_card.go (62%) rename tool_call_card_test.go => agent/tool_call_card_test.go (93%) rename tool_call_group.go => agent/tool_call_group.go (65%) rename tool_call_group_test.go => agent/tool_call_group_test.go (93%) rename area_chart.go => chart/area_chart.go (83%) rename area_chart_test.go => chart/area_chart_test.go (99%) rename bar_chart.go => chart/bar_chart.go (84%) rename bar_chart_test.go => chart/bar_chart_test.go (96%) rename box_plot.go => chart/box_plot.go (85%) rename box_plot_test.go => chart/box_plot_test.go (99%) rename bullet_chart.go => chart/bullet_chart.go (85%) rename bullet_chart_test.go => chart/bullet_chart_test.go (96%) rename calendar_heatmap.go => chart/calendar_heatmap.go (66%) rename calendar_heatmap_test.go => chart/calendar_heatmap_test.go (85%) rename candle_countdown.go => chart/candle_countdown.go (77%) rename candle_countdown_test.go => chart/candle_countdown_test.go (83%) rename candlestick_chart.go => chart/candlestick_chart.go (82%) rename candlestick_chart_test.go => chart/candlestick_chart_test.go (93%) rename candlestick_paint.go => chart/candlestick_paint.go (93%) rename chart_annotation.go => chart/chart_annotation.go (86%) rename chart_annotation_test.go => chart/chart_annotation_test.go (94%) rename chart_axis.go => chart/chart_axis.go (96%) rename chart_axis_test.go => chart/chart_axis_test.go (93%) rename chart_brush.go => chart/chart_brush.go (78%) rename chart_brush_test.go => chart/chart_brush_test.go (96%) rename chart_crosshair.go => chart/chart_crosshair.go (88%) rename chart_crosshair_test.go => chart/chart_crosshair_test.go (89%) rename chart_empty_state.go => chart/chart_empty_state.go (61%) rename chart_empty_state_test.go => chart/chart_empty_state_test.go (78%) rename chart_export.go => chart/chart_export.go (74%) rename chart_export_test.go => chart/chart_export_test.go (87%) rename chart_frame.go => chart/chart_frame.go (76%) rename chart_frame_test.go => chart/chart_frame_test.go (99%) rename chart_grid.go => chart/chart_grid.go (93%) rename chart_grid_test.go => chart/chart_grid_test.go (89%) rename chart_legend.go => chart/chart_legend.go (83%) rename chart_legend_test.go => chart/chart_legend_test.go (90%) rename chart_series.go => chart/chart_series.go (67%) rename chart_sync.go => chart/chart_sync.go (83%) rename chart_sync_test.go => chart/chart_sync_test.go (93%) rename chart_tooltip.go => chart/chart_tooltip.go (85%) rename chart_tooltip_test.go => chart/chart_tooltip_test.go (87%) rename chart_tree.go => chart/chart_tree.go (91%) rename chart_type_switcher.go => chart/chart_type_switcher.go (62%) rename chart_type_switcher_test.go => chart/chart_type_switcher_test.go (91%) rename chord_diagram.go => chart/chord_diagram.go (87%) rename chord_diagram_test.go => chart/chord_diagram_test.go (97%) rename compare_symbol_test.go => chart/compare_symbol_test.go (94%) rename density_plot.go => chart/density_plot.go (91%) rename density_plot_test.go => chart/density_plot_test.go (97%) rename depth_chart.go => chart/depth_chart.go (77%) rename depth_chart_test.go => chart/depth_chart_test.go (95%) create mode 100644 chart/doc.go rename drawing_toolbar.go => chart/drawing_toolbar.go (77%) rename drawing_toolbar_test.go => chart/drawing_toolbar_test.go (91%) rename drawing_tools.go => chart/drawing_tools.go (97%) rename drawing_tools_paint.go => chart/drawing_tools_paint.go (82%) rename drawing_tools_test.go => chart/drawing_tools_test.go (90%) create mode 100644 chart/example_charts1_test.go create mode 100644 chart/example_charts2_test.go create mode 100644 chart/example_markets_test.go rename example_mktchart_test.go => chart/example_mktchart_test.go (50%) rename funnel_chart.go => chart/funnel_chart.go (79%) rename funnel_chart_test.go => chart/funnel_chart_test.go (94%) rename gauge.go => chart/gauge.go (86%) rename gauge_test.go => chart/gauge_test.go (92%) rename heatmap_chart.go => chart/heatmap_chart.go (82%) rename heatmap_chart_test.go => chart/heatmap_chart_test.go (95%) rename heikin_ashi_chart_test.go => chart/heikin_ashi_chart_test.go (77%) create mode 100644 chart/helpers_test.go rename histogram.go => chart/histogram.go (91%) rename histogram_test.go => chart/histogram_test.go (92%) rename indicator_overlay.go => chart/indicator_overlay.go (88%) rename indicator_overlay_test.go => chart/indicator_overlay_test.go (92%) rename indicator_pane.go => chart/indicator_pane.go (80%) rename indicator_pane_test.go => chart/indicator_pane_test.go (88%) rename indicator_selector.go => chart/indicator_selector.go (62%) rename indicator_selector_test.go => chart/indicator_selector_test.go (94%) rename indicators.go => chart/indicators.go (99%) rename indicators_test.go => chart/indicators_test.go (99%) rename interval_selector.go => chart/interval_selector.go (67%) rename interval_selector_test.go => chart/interval_selector_test.go (94%) rename line_chart.go => chart/line_chart.go (75%) rename line_chart_test.go => chart/line_chart_test.go (92%) rename line_quote_chart.go => chart/line_quote_chart.go (80%) rename line_quote_chart_test.go => chart/line_quote_chart_test.go (84%) rename market_boundary_test.go => chart/market_boundary_test.go (94%) rename market_data.go => chart/market_data.go (99%) rename market_heatmap.go => chart/market_heatmap.go (63%) rename market_heatmap_test.go => chart/market_heatmap_test.go (98%) rename market_input.go => chart/market_input.go (88%) rename market_keyboard_test.go => chart/market_keyboard_test.go (92%) rename market_large_test.go => chart/market_large_test.go (91%) rename market_layers.go => chart/market_layers.go (78%) rename market_regression_test.go => chart/market_regression_test.go (93%) rename market_review_regression_test.go => chart/market_review_regression_test.go (90%) rename market_states_test.go => chart/market_states_test.go (89%) rename multi_chart_layout.go => chart/multi_chart_layout.go (75%) rename multi_chart_layout_test.go => chart/multi_chart_layout_test.go (95%) rename network_graph.go => chart/network_graph.go (93%) rename network_graph_test.go => chart/network_graph_test.go (97%) rename ohlc_chart_test.go => chart/ohlc_chart_test.go (92%) rename parallel_coordinates.go => chart/parallel_coordinates.go (82%) rename parallel_coordinates_test.go => chart/parallel_coordinates_test.go (98%) rename pie_chart.go => chart/pie_chart.go (79%) rename pie_chart_test.go => chart/pie_chart_test.go (92%) rename price_alert_line_test.go => chart/price_alert_line_test.go (95%) rename radar_chart.go => chart/radar_chart.go (83%) rename radar_chart_test.go => chart/radar_chart_test.go (92%) rename realtime_chart.go => chart/realtime_chart.go (84%) rename realtime_chart_test.go => chart/realtime_chart_test.go (99%) rename sankey_chart.go => chart/sankey_chart.go (90%) rename sankey_chart_test.go => chart/sankey_chart_test.go (96%) rename scatter_chart.go => chart/scatter_chart.go (83%) rename scatter_chart_test.go => chart/scatter_chart_test.go (96%) rename spark_bar.go => chart/spark_bar.go (88%) rename spark_bar_test.go => chart/spark_bar_test.go (90%) rename sparkline.go => chart/sparkline.go (84%) rename sparkline_test.go => chart/sparkline_test.go (86%) rename sunburst.go => chart/sunburst.go (84%) rename sunburst_test.go => chart/sunburst_test.go (95%) rename time_range_selector.go => chart/time_range_selector.go (69%) rename time_range_selector_test.go => chart/time_range_selector_test.go (79%) rename treemap.go => chart/treemap.go (79%) rename treemap_test.go => chart/treemap_test.go (96%) rename trend_indicator.go => chart/trend_indicator.go (75%) rename trend_indicator_test.go => chart/trend_indicator_test.go (86%) rename violin_plot.go => chart/violin_plot.go (85%) rename violin_plot_test.go => chart/violin_plot_test.go (96%) rename volume_chart.go => chart/volume_chart.go (80%) rename volume_chart_test.go => chart/volume_chart_test.go (86%) rename volume_profile.go => chart/volume_profile.go (82%) rename volume_profile_test.go => chart/volume_profile_test.go (94%) rename waterfall_chart.go => chart/waterfall_chart.go (87%) rename waterfall_chart_test.go => chart/waterfall_chart_test.go (94%) rename artifact_card.go => chat/artifact_card.go (66%) rename artifact_card_test.go => chat/artifact_card_test.go (61%) rename attachment_chip.go => chat/attachment_chip.go (71%) rename attachment_chip_test.go => chat/attachment_chip_test.go (93%) rename audio_message.go => chat/audio_message.go (83%) rename audio_message_test.go => chat/audio_message_test.go (93%) rename branch_navigator.go => chat/branch_navigator.go (56%) rename branch_navigator_test.go => chat/branch_navigator_test.go (91%) rename capability_cards.go => chat/capability_cards.go (88%) rename capability_cards_test.go => chat/capability_cards_test.go (95%) rename chat_container.go => chat/chat_container.go (90%) rename chat_container_test.go => chat/chat_container_test.go (87%) create mode 100644 chat/chat_empty_state.go rename chat_empty_state_test.go => chat/chat_empty_state_test.go (94%) rename chatsession_fixture_test.go => chat/chatsession_fixture_test.go (96%) rename citation_badge.go => chat/citation_badge.go (78%) rename citation_badge_test.go => chat/citation_badge_test.go (95%) rename code_block.go => chat/code_block.go (79%) rename code_block_test.go => chat/code_block_test.go (96%) rename context_chips.go => chat/context_chips.go (65%) rename context_chips_test.go => chat/context_chips_test.go (93%) rename context_mention_menu.go => chat/context_mention_menu.go (85%) rename context_mention_menu_test.go => chat/context_mention_menu_test.go (97%) rename context_window_meter.go => chat/context_window_meter.go (77%) rename context_window_meter_test.go => chat/context_window_meter_test.go (89%) rename conversation_export.go => chat/conversation_export.go (75%) rename conversation_export_test.go => chat/conversation_export_test.go (97%) rename conversation_item.go => chat/conversation_item.go (73%) rename conversation_item_test.go => chat/conversation_item_test.go (94%) rename conversation_list.go => chat/conversation_list.go (86%) rename conversation_list_test.go => chat/conversation_list_test.go (97%) rename conversation_search.go => chat/conversation_search.go (79%) rename conversation_search_test.go => chat/conversation_search_test.go (99%) rename cost_estimator.go => chat/cost_estimator.go (75%) rename cost_estimator_test.go => chat/cost_estimator_test.go (94%) create mode 100644 chat/date_separator.go create mode 100644 chat/date_separator_test.go create mode 100644 chat/doc.go rename drag_drop_overlay.go => chat/drag_drop_overlay.go (78%) rename drag_drop_overlay_test.go => chat/drag_drop_overlay_test.go (88%) rename error_message.go => chat/error_message.go (69%) rename error_message_test.go => chat/error_message_test.go (91%) create mode 100644 chat/example_chatinput_test.go rename example_chatmsg_test.go => chat/example_chatmsg_test.go (51%) create mode 100644 chat/example_chatsession_test.go rename feedback_form.go => chat/feedback_form.go (74%) rename feedback_form_test.go => chat/feedback_form_test.go (99%) rename file_message.go => chat/file_message.go (84%) rename file_message_test.go => chat/file_message_test.go (93%) create mode 100644 chat/helpers_test.go rename image_grid.go => chat/image_grid.go (79%) rename image_grid_test.go => chat/image_grid_test.go (94%) rename link_preview_card.go => chat/link_preview_card.go (75%) rename link_preview_card_test.go => chat/link_preview_card_test.go (91%) rename markdown_view.go => chat/markdown_view.go (90%) rename markdown_view_test.go => chat/markdown_view_test.go (92%) rename message_actions.go => chat/message_actions.go (86%) rename message_actions_test.go => chat/message_actions_test.go (94%) rename message_bubble.go => chat/message_bubble.go (85%) rename message_bubble_test.go => chat/message_bubble_test.go (86%) rename message_editor.go => chat/message_editor.go (73%) rename message_editor_test.go => chat/message_editor_test.go (94%) rename message_header.go => chat/message_header.go (76%) rename message_header_test.go => chat/message_header_test.go (89%) rename message_list.go => chat/message_list.go (85%) rename message_list_test.go => chat/message_list_test.go (96%) rename mode_selector.go => chat/mode_selector.go (80%) rename mode_selector_test.go => chat/mode_selector_test.go (84%) rename model_selector.go => chat/model_selector.go (84%) rename model_selector_test.go => chat/model_selector_test.go (92%) rename parameter_panel.go => chat/parameter_panel.go (60%) rename parameter_panel_test.go => chat/parameter_panel_test.go (92%) rename paste_image_preview.go => chat/paste_image_preview.go (81%) rename paste_image_preview_test.go => chat/paste_image_preview_test.go (92%) rename project_knowledge_panel.go => chat/project_knowledge_panel.go (67%) rename project_knowledge_panel_test.go => chat/project_knowledge_panel_test.go (97%) rename project_list.go => chat/project_list.go (83%) rename project_list_test.go => chat/project_list_test.go (99%) rename prompt_composer.go => chat/prompt_composer.go (80%) rename prompt_composer_test.go => chat/prompt_composer_test.go (91%) rename prompt_library.go => chat/prompt_library.go (84%) rename prompt_library_test.go => chat/prompt_library_test.go (99%) rename quote_reply.go => chat/quote_reply.go (57%) rename quote_reply_test.go => chat/quote_reply_test.go (90%) rename regenerate_menu.go => chat/regenerate_menu.go (72%) rename regenerate_menu_test.go => chat/regenerate_menu_test.go (99%) rename scroll_to_bottom_button.go => chat/scroll_to_bottom_button.go (73%) rename scroll_to_bottom_button_test.go => chat/scroll_to_bottom_button_test.go (90%) rename search_progress.go => chat/search_progress.go (68%) rename search_progress_test.go => chat/search_progress_test.go (94%) rename send_button.go => chat/send_button.go (80%) rename send_button_test.go => chat/send_button_test.go (91%) rename shared_conversation_view.go => chat/shared_conversation_view.go (66%) rename shared_conversation_view_test.go => chat/shared_conversation_view_test.go (95%) rename slash_command_menu.go => chat/slash_command_menu.go (87%) rename slash_command_menu_test.go => chat/slash_command_menu_test.go (97%) rename source_card.go => chat/source_card.go (78%) rename source_card_test.go => chat/source_card_test.go (92%) rename sources_panel.go => chat/sources_panel.go (71%) rename sources_panel_test.go => chat/sources_panel_test.go (94%) rename stop_generating_button.go => chat/stop_generating_button.go (67%) rename stop_generating_button_test.go => chat/stop_generating_button_test.go (99%) rename streaming_text.go => chat/streaming_text.go (93%) rename streaming_text_test.go => chat/streaming_text_test.go (96%) rename suggestion_chips.go => chat/suggestion_chips.go (88%) rename suggestion_chips_test.go => chat/suggestion_chips_test.go (95%) rename system_prompt_editor.go => chat/system_prompt_editor.go (66%) rename system_prompt_editor_test.go => chat/system_prompt_editor_test.go (99%) rename table_block.go => chat/table_block.go (82%) rename table_block_test.go => chat/table_block_test.go (92%) rename thinking_block.go => chat/thinking_block.go (64%) rename thinking_block_test.go => chat/thinking_block_test.go (92%) rename thinking_indicator.go => chat/thinking_indicator.go (78%) rename thinking_indicator_test.go => chat/thinking_indicator_test.go (99%) rename token_counter.go => chat/token_counter.go (68%) rename token_counter_test.go => chat/token_counter_test.go (91%) rename tool_toggle_menu.go => chat/tool_toggle_menu.go (70%) rename tool_toggle_menu_test.go => chat/tool_toggle_menu_test.go (96%) rename typing_indicator.go => chat/typing_indicator.go (75%) rename typing_indicator_test.go => chat/typing_indicator_test.go (90%) rename voice_input_button.go => chat/voice_input_button.go (73%) rename voice_input_button_test.go => chat/voice_input_button_test.go (92%) rename voice_waveform.go => chat/voice_waveform.go (84%) rename voice_waveform_test.go => chat/voice_waveform_test.go (92%) rename welcome_screen.go => chat/welcome_screen.go (81%) rename welcome_screen_test.go => chat/welcome_screen_test.go (95%) delete mode 100644 chat_empty_state.go rename breakpoint_list.go => code/breakpoint_list.go (70%) rename breakpoint_list_test.go => code/breakpoint_list_test.go (97%) rename call_stack.go => code/call_stack.go (80%) rename call_stack_test.go => code/call_stack_test.go (97%) rename code_shared.go => code/code_shared.go (61%) rename code_viewer.go => code/code_viewer.go (83%) rename code_viewer_test.go => code/code_viewer_test.go (93%) rename command_history.go => code/command_history.go (71%) rename command_history_test.go => code/command_history_test.go (96%) rename completion_menu.go => code/completion_menu.go (77%) rename completion_menu_test.go => code/completion_menu_test.go (92%) rename debug_toolbar.go => code/debug_toolbar.go (91%) rename debug_toolbar_test.go => code/debug_toolbar_test.go (96%) create mode 100644 code/doc.go create mode 100644 code/example_code_test.go rename find_widget.go => code/find_widget.go (74%) rename find_widget_test.go => code/find_widget_test.go (97%) rename flamegraph.go => code/flamegraph.go (82%) rename flamegraph_test.go => code/flamegraph_test.go (92%) create mode 100644 code/helpers_test.go rename hex_viewer.go => code/hex_viewer.go (85%) rename hex_viewer_test.go => code/hex_viewer_test.go (97%) rename log_viewer.go => code/log_viewer.go (66%) rename log_viewer_test.go => code/log_viewer_test.go (95%) rename minimap.go => code/minimap.go (87%) rename minimap_test.go => code/minimap_test.go (96%) rename output_panel.go => code/output_panel.go (65%) rename output_panel_test.go => code/output_panel_test.go (92%) rename problems_panel.go => code/problems_panel.go (83%) rename problems_panel_test.go => code/problems_panel_test.go (97%) rename process_list.go => code/process_list.go (68%) rename process_list_test.go => code/process_list_test.go (97%) rename search_panel.go => code/search_panel.go (81%) rename search_panel_test.go => code/search_panel_test.go (93%) rename symbol_outline.go => code/symbol_outline.go (86%) rename symbol_outline_test.go => code/symbol_outline_test.go (97%) rename terminal.go => code/terminal.go (97%) rename terminal_search.go => code/terminal_search.go (78%) rename terminal_search_test.go => code/terminal_search_test.go (97%) rename terminal_state.go => code/terminal_state.go (99%) rename terminal_tabs.go => code/terminal_tabs.go (80%) rename terminal_tabs_test.go => code/terminal_tabs_test.go (96%) rename terminal_test.go => code/terminal_test.go (98%) rename variables_panel.go => code/variables_panel.go (82%) rename variables_panel_test.go => code/variables_panel_test.go (97%) rename base_disabled_test.go => core/base_disabled_test.go (99%) create mode 100644 core/choice_result.go rename datetime.go => core/datetime.go (55%) rename docs_test.go => core/docs_test.go (87%) create mode 100644 core/example_feedback_test.go create mode 100644 core/geometry.go rename i18n.go => core/i18n.go (84%) create mode 100644 core/icon_action.go rename media_helpers_test.go => core/media_helpers_test.go (98%) create mode 100644 core/media_icons.go rename messages_account.go => core/messages_account.go (99%) rename messages_agent.go => core/messages_agent.go (99%) rename messages_calendar.go => core/messages_calendar.go (98%) rename messages_charts1.go => core/messages_charts1.go (99%) rename messages_charts2.go => core/messages_charts2.go (98%) rename messages_chatinput.go => core/messages_chatinput.go (99%) rename messages_chatmsg.go => core/messages_chatmsg.go (99%) rename messages_chatsession.go => core/messages_chatsession.go (99%) rename messages_choice.go => core/messages_choice.go (99%) rename messages_code.go => core/messages_code.go (99%) rename messages_data.go => core/messages_data.go (99%) rename messages_datetime.go => core/messages_datetime.go (99%) rename messages_devtools.go => core/messages_devtools.go (99%) rename messages_display.go => core/messages_display.go (97%) rename messages_essentials.go => core/messages_essentials.go (99%) rename messages_feedback.go => core/messages_feedback.go (99%) rename messages_files.go => core/messages_files.go (99%) rename messages_form.go => core/messages_form.go (99%) rename messages_git.go => core/messages_git.go (99%) rename messages_input.go => core/messages_input.go (98%) rename messages_layout.go => core/messages_layout.go (96%) rename messages_markets.go => core/messages_markets.go (99%) rename messages_media.go => core/messages_media.go (99%) rename messages_messaging.go => core/messages_messaging.go (98%) rename messages_mktchart.go => core/messages_mktchart.go (99%) rename messages_motion.go => core/messages_motion.go (99%) rename messages_navigation.go => core/messages_navigation.go (99%) rename messages_overlay.go => core/messages_overlay.go (96%) rename messages_projects.go => core/messages_projects.go (99%) rename messages_trading.go => core/messages_trading.go (99%) rename motion_delivery_test.go => core/motion_delivery_test.go (86%) rename overlay_layers.go => core/overlay_layers.go (80%) create mode 100644 core/overlay_motion.go create mode 100644 core/severity.go create mode 100644 core/snapshot_test.go rename spinner.go => core/spinner.go (95%) rename theme.go => core/theme.go (83%) rename theme_test.go => core/theme_test.go (95%) create mode 100644 core/trend.go rename accordion.go => data/accordion.go (89%) rename accordion_test.go => data/accordion_test.go (96%) rename card.go => data/card.go (82%) rename card_test.go => data/card_test.go (81%) rename carousel.go => data/carousel.go (80%) rename carousel_test.go => data/carousel_test.go (95%) rename collapsible.go => data/collapsible.go (78%) rename collapsible_test.go => data/collapsible_test.go (86%) rename data_table.go => data/data_table.go (93%) rename data_table_columns.go => data/data_table_columns.go (88%) rename data_table_test.go => data/data_table_test.go (98%) rename description_list.go => data/description_list.go (79%) rename description_list_test.go => data/description_list_test.go (93%) create mode 100644 data/doc.go rename example_data_test.go => data/example_data_test.go (68%) create mode 100644 data/helpers_test.go rename image_viewer.go => data/image_viewer.go (84%) rename image_viewer_test.go => data/image_viewer_test.go (97%) rename list.go => data/list.go (75%) rename list_test.go => data/list_test.go (97%) rename statistic.go => data/statistic.go (67%) rename statistic_test.go => data/statistic_test.go (91%) rename timeline.go => data/timeline.go (80%) rename timeline_test.go => data/timeline_test.go (94%) rename tree.go => data/tree.go (74%) rename tree_table.go => data/tree_table.go (86%) rename tree_table_test.go => data/tree_table_test.go (90%) rename tree_test.go => data/tree_test.go (90%) delete mode 100644 date_separator.go delete mode 100644 date_separator_test.go rename agenda_view.go => datetime/agenda_view.go (76%) rename agenda_view_test.go => datetime/agenda_view_test.go (97%) rename attendee_list.go => datetime/attendee_list.go (71%) rename attendee_list_test.go => datetime/attendee_list_test.go (96%) rename availability_picker.go => datetime/availability_picker.go (78%) rename availability_picker_test.go => datetime/availability_picker_test.go (95%) rename calendar.go => datetime/calendar.go (65%) rename calendar_day_view.go => datetime/calendar_day_view.go (61%) rename calendar_day_view_test.go => datetime/calendar_day_view_test.go (91%) rename calendar_events.go => datetime/calendar_events.go (88%) rename calendar_month_view.go => datetime/calendar_month_view.go (74%) rename calendar_month_view_test.go => datetime/calendar_month_view_test.go (95%) rename calendar_test.go => datetime/calendar_test.go (66%) rename calendar_time_grid.go => datetime/calendar_time_grid.go (85%) rename calendar_week_view.go => datetime/calendar_week_view.go (50%) rename calendar_week_view_test.go => datetime/calendar_week_view_test.go (98%) rename countdown.go => datetime/countdown.go (81%) rename countdown_test.go => datetime/countdown_test.go (92%) rename cron_editor.go => datetime/cron_editor.go (83%) rename cron_editor_test.go => datetime/cron_editor_test.go (96%) rename current_time_indicator.go => datetime/current_time_indicator.go (78%) rename current_time_indicator_test.go => datetime/current_time_indicator_test.go (86%) rename date_picker.go => datetime/date_picker.go (60%) rename date_picker_test.go => datetime/date_picker_test.go (67%) rename date_range_picker.go => datetime/date_range_picker.go (63%) rename date_range_picker_test.go => datetime/date_range_picker_test.go (91%) rename date_time_picker.go => datetime/date_time_picker.go (70%) rename date_time_picker_test.go => datetime/date_time_picker_test.go (89%) rename datetime_grid.go => datetime/datetime_grid.go (84%) create mode 100644 datetime/datetime_result.go rename datetime_review2_test.go => datetime/datetime_review2_test.go (88%) rename datetime_review_test.go => datetime/datetime_review_test.go (92%) create mode 100644 datetime/doc.go rename duration_picker.go => datetime/duration_picker.go (80%) rename duration_picker_test.go => datetime/duration_picker_test.go (96%) rename event_chip.go => datetime/event_chip.go (82%) rename event_chip_test.go => datetime/event_chip_test.go (92%) rename event_editor.go => datetime/event_editor.go (62%) rename event_editor_test.go => datetime/event_editor_test.go (96%) rename event_popover.go => datetime/event_popover.go (70%) rename event_popover_test.go => datetime/event_popover_test.go (89%) create mode 100644 datetime/example_calendar_test.go create mode 100644 datetime/example_datetime_test.go create mode 100644 datetime/example_helpers_test.go create mode 100644 datetime/helpers_test.go rename month_picker.go => datetime/month_picker.go (82%) rename month_picker_test.go => datetime/month_picker_test.go (96%) rename recurrence_editor.go => datetime/recurrence_editor.go (59%) rename recurrence_editor_test.go => datetime/recurrence_editor_test.go (93%) rename relative_time.go => datetime/relative_time.go (79%) rename relative_time_test.go => datetime/relative_time_test.go (87%) rename reminder_picker.go => datetime/reminder_picker.go (66%) rename reminder_picker_test.go => datetime/reminder_picker_test.go (94%) rename stopwatch.go => datetime/stopwatch.go (72%) rename stopwatch_test.go => datetime/stopwatch_test.go (95%) rename time_picker.go => datetime/time_picker.go (89%) rename time_picker_popup.go => datetime/time_picker_popup.go (89%) rename time_picker_test.go => datetime/time_picker_test.go (97%) rename time_range_picker.go => datetime/time_range_picker.go (86%) rename time_range_picker_test.go => datetime/time_range_picker_test.go (95%) rename timezone_select.go => datetime/timezone_select.go (86%) rename timezone_select_test.go => datetime/timezone_select_test.go (96%) create mode 100644 datetime/week_picker.go rename week_picker_test.go => datetime/week_picker_test.go (90%) rename year_picker.go => datetime/year_picker.go (86%) rename year_picker_test.go => datetime/year_picker_test.go (95%) rename year_view.go => datetime/year_view.go (70%) rename year_view_test.go => datetime/year_view_test.go (79%) delete mode 100644 device_selector.go rename alert_list.go => devtools/alert_list.go (73%) rename alert_list_test.go => devtools/alert_list_test.go (96%) rename api_request_builder.go => devtools/api_request_builder.go (60%) rename api_request_builder_test.go => devtools/api_request_builder_test.go (95%) rename dashboard_filter_bar.go => devtools/dashboard_filter_bar.go (51%) rename dashboard_filter_bar_test.go => devtools/dashboard_filter_bar_test.go (95%) create mode 100644 devtools/devtools_parts.go create mode 100644 devtools/doc.go create mode 100644 devtools/example_devtools_test.go create mode 100644 devtools/helpers_test.go rename incident_card.go => devtools/incident_card.go (66%) rename incident_card_test.go => devtools/incident_card_test.go (88%) rename json_editor.go => devtools/json_editor.go (76%) rename json_editor_test.go => devtools/json_editor_test.go (96%) rename json_tree.go => devtools/json_tree.go (99%) rename json_viewer.go => devtools/json_viewer.go (77%) rename json_viewer_test.go => devtools/json_viewer_test.go (96%) rename key_value_input.go => devtools/key_value_input.go (57%) rename key_value_input_test.go => devtools/key_value_input_test.go (95%) rename log_stream.go => devtools/log_stream.go (52%) rename log_stream_test.go => devtools/log_stream_test.go (68%) rename refresh_interval_selector.go => devtools/refresh_interval_selector.go (67%) rename refresh_interval_selector_test.go => devtools/refresh_interval_selector_test.go (96%) rename resource_gauge.go => devtools/resource_gauge.go (58%) rename resource_gauge_test.go => devtools/resource_gauge_test.go (84%) rename response_viewer.go => devtools/response_viewer.go (59%) rename response_viewer_test.go => devtools/response_viewer_test.go (89%) rename schema_tree.go => devtools/schema_tree.go (80%) rename schema_tree_test.go => devtools/schema_tree_test.go (96%) rename service_status.go => devtools/service_status.go (68%) rename service_status_test.go => devtools/service_status_test.go (91%) rename stat_card.go => devtools/stat_card.go (56%) rename stat_card_test.go => devtools/stat_card_test.go (82%) create mode 100644 devtools/system_monitor.go rename system_monitor_test.go => devtools/system_monitor_test.go (85%) rename trace_waterfall.go => devtools/trace_waterfall.go (77%) rename trace_waterfall_test.go => devtools/trace_waterfall_test.go (96%) rename uptime_bar.go => devtools/uptime_bar.go (83%) rename uptime_bar_test.go => devtools/uptime_bar_test.go (77%) delete mode 100644 devtools_parts.go rename avatar.go => display/avatar.go (91%) rename avatar_group.go => display/avatar_group.go (91%) rename avatar_group_test.go => display/avatar_group_test.go (92%) rename avatar_test.go => display/avatar_test.go (94%) rename badge.go => display/badge.go (89%) rename badge_test.go => display/badge_test.go (94%) create mode 100644 display/doc.go create mode 100644 display/example_display_test.go rename heading.go => display/heading.go (70%) rename heading_test.go => display/heading_test.go (93%) create mode 100644 display/helpers_test.go rename icon.go => display/icon.go (90%) rename icon_test.go => display/icon_test.go (93%) rename image.go => display/image.go (82%) rename image_state_test.go => display/image_state_test.go (94%) rename image_test.go => display/image_test.go (94%) rename kbd.go => display/kbd.go (73%) rename kbd_test.go => display/kbd_test.go (91%) rename link.go => display/link.go (82%) rename link_test.go => display/link_test.go (92%) rename tag.go => display/tag.go (85%) rename tag_test.go => display/tag_test.go (95%) rename text.go => display/text.go (87%) rename text_test.go => display/text_test.go (90%) delete mode 100644 example_account_test.go delete mode 100644 example_agent_test.go delete mode 100644 example_calendar_test.go delete mode 100644 example_charts1_test.go delete mode 100644 example_charts2_test.go delete mode 100644 example_chatinput_test.go delete mode 100644 example_chatsession_test.go delete mode 100644 example_choice_test.go delete mode 100644 example_code_test.go delete mode 100644 example_datetime_test.go delete mode 100644 example_devtools_test.go delete mode 100644 example_display_test.go delete mode 100644 example_essentials_test.go delete mode 100644 example_files_test.go delete mode 100644 example_form_test.go delete mode 100644 example_git_test.go delete mode 100644 example_input_test.go delete mode 100644 example_markets_test.go delete mode 100644 example_media_test.go delete mode 100644 example_messaging_test.go delete mode 100644 example_motion_test.go delete mode 100644 example_navigation_test.go delete mode 100644 example_overlay_test.go delete mode 100644 example_projects_test.go delete mode 100644 example_trading_test.go rename activity_feed.go => feedback/activity_feed.go (75%) rename activity_feed_test.go => feedback/activity_feed_test.go (84%) rename alert.go => feedback/alert.go (51%) rename alert_test.go => feedback/alert_test.go (79%) rename bars_loader.go => feedback/bars_loader.go (89%) rename bars_loader_test.go => feedback/bars_loader_test.go (81%) rename blink_highlight.go => feedback/blink_highlight.go (86%) rename blink_highlight_test.go => feedback/blink_highlight_test.go (96%) rename cost_breakdown.go => feedback/cost_breakdown.go (67%) rename cost_breakdown_test.go => feedback/cost_breakdown_test.go (95%) rename count_up.go => feedback/count_up.go (92%) rename count_up_test.go => feedback/count_up_test.go (89%) create mode 100644 feedback/doc.go rename dots_loader.go => feedback/dots_loader.go (84%) rename dots_loader_test.go => feedback/dots_loader_test.go (81%) rename empty_state.go => feedback/empty_state.go (71%) rename empty_state_test.go => feedback/empty_state_test.go (87%) rename eval_result_table.go => feedback/eval_result_table.go (67%) rename eval_result_table_test.go => feedback/eval_result_table_test.go (85%) create mode 100644 feedback/example_essentials_test.go rename example_feedback_test.go => feedback/example_feedback_test.go (50%) create mode 100644 feedback/example_motion_test.go rename feedback_review_test.go => feedback/feedback_review_test.go (92%) create mode 100644 feedback/helpers_test.go rename interrupt_button.go => feedback/interrupt_button.go (56%) rename interrupt_button_test.go => feedback/interrupt_button_test.go (94%) rename layout_transition.go => feedback/layout_transition.go (80%) rename layout_transition_test.go => feedback/layout_transition_test.go (85%) rename loaders_test.go => feedback/loaders_test.go (93%) rename loading_overlay.go => feedback/loading_overlay.go (83%) rename loading_overlay_test.go => feedback/loading_overlay_test.go (87%) rename notification_center.go => feedback/notification_center.go (76%) rename notification_center_test.go => feedback/notification_center_test.go (96%) rename orbit_loader.go => feedback/orbit_loader.go (92%) rename orbit_loader_test.go => feedback/orbit_loader_test.go (82%) rename presence.go => feedback/presence.go (90%) rename presence_test.go => feedback/presence_test.go (92%) rename progress.go => feedback/progress.go (85%) rename progress_test.go => feedback/progress_test.go (91%) rename pulse_loader.go => feedback/pulse_loader.go (88%) rename pulse_loader_test.go => feedback/pulse_loader_test.go (82%) rename reorder_transition.go => feedback/reorder_transition.go (88%) rename reorder_transition_test.go => feedback/reorder_transition_test.go (91%) rename result.go => feedback/result.go (79%) rename result_test.go => feedback/result_test.go (85%) rename shimmer.go => feedback/shimmer.go (86%) rename shimmer_test.go => feedback/shimmer_test.go (94%) rename shimmer_text.go => feedback/shimmer_text.go (90%) rename shimmer_text_test.go => feedback/shimmer_text_test.go (92%) rename skeleton.go => feedback/skeleton.go (92%) rename skeleton_test.go => feedback/skeleton_test.go (90%) rename stagger.go => feedback/stagger.go (88%) rename stagger_test.go => feedback/stagger_test.go (89%) rename status_indicator.go => feedback/status_indicator.go (80%) rename status_indicator_test.go => feedback/status_indicator_test.go (85%) rename toast.go => feedback/toast.go (91%) rename toast_test.go => feedback/toast_test.go (91%) rename token_usage_chart.go => feedback/token_usage_chart.go (69%) rename token_usage_chart_test.go => feedback/token_usage_chart_test.go (84%) rename tool_registry_panel.go => feedback/tool_registry_panel.go (74%) rename tool_registry_panel_test.go => feedback/tool_registry_panel_test.go (88%) rename trace_viewer.go => feedback/trace_viewer.go (72%) rename trace_viewer_test.go => feedback/trace_viewer_test.go (96%) rename typewriter.go => feedback/typewriter.go (82%) rename typewriter_test.go => feedback/typewriter_test.go (91%) rename undo_toast.go => feedback/undo_toast.go (87%) rename undo_toast_test.go => feedback/undo_toast_test.go (89%) rename wave_loader.go => feedback/wave_loader.go (91%) rename wave_loader_test.go => feedback/wave_loader_test.go (81%) rename archive_viewer.go => files/archive_viewer.go (78%) rename archive_viewer_test.go => files/archive_viewer_test.go (94%) create mode 100644 files/doc.go create mode 100644 files/example_files_test.go rename file_explorer.go => files/file_explorer.go (85%) rename file_explorer_test.go => files/file_explorer_test.go (97%) rename file_grid.go => files/file_grid.go (86%) rename file_grid_test.go => files/file_grid_test.go (95%) rename file_icon.go => files/file_icon.go (90%) rename file_icon_test.go => files/file_icon_test.go (92%) rename file_operation_progress.go => files/file_operation_progress.go (63%) rename file_operation_progress_test.go => files/file_operation_progress_test.go (97%) rename file_preview.go => files/file_preview.go (54%) rename file_preview_test.go => files/file_preview_test.go (91%) create mode 100644 files/helpers_test.go rename live_indicator.go => files/live_indicator.go (63%) rename live_indicator_test.go => files/live_indicator_test.go (94%) rename path_bar.go => files/path_bar.go (82%) rename path_bar_test.go => files/path_bar_test.go (96%) rename permission_select.go => files/permission_select.go (64%) rename permission_select_test.go => files/permission_select_test.go (95%) rename presence_avatars.go => files/presence_avatars.go (80%) rename presence_avatars_test.go => files/presence_avatars_test.go (92%) rename reactions.go => files/reactions.go (74%) rename reactions_test.go => files/reactions_test.go (96%) rename recent_files.go => files/recent_files.go (77%) rename recent_files_test.go => files/recent_files_test.go (97%) rename remote_cursor.go => files/remote_cursor.go (86%) rename remote_cursor_test.go => files/remote_cursor_test.go (86%) rename rename_inline.go => files/rename_inline.go (92%) rename rename_inline_test.go => files/rename_inline_test.go (95%) rename share_dialog.go => files/share_dialog.go (66%) rename share_dialog_test.go => files/share_dialog_test.go (96%) rename storage_usage.go => files/storage_usage.go (80%) rename storage_usage_test.go => files/storage_usage_test.go (96%) rename transfer_queue.go => files/transfer_queue.go (81%) rename transfer_queue_test.go => files/transfer_queue_test.go (96%) rename asset_allocation_chart.go => finance/asset_allocation_chart.go (70%) rename asset_allocation_chart_test.go => finance/asset_allocation_chart_test.go (81%) rename bid_ask_bar.go => finance/bid_ask_bar.go (62%) rename bid_ask_bar_test.go => finance/bid_ask_bar_test.go (75%) rename currency_converter.go => finance/currency_converter.go (62%) rename currency_converter_test.go => finance/currency_converter_test.go (98%) rename depth_ladder.go => finance/depth_ladder.go (75%) rename depth_ladder_test.go => finance/depth_ladder_test.go (86%) create mode 100644 finance/doc.go rename earnings_calendar.go => finance/earnings_calendar.go (59%) rename earnings_calendar_test.go => finance/earnings_calendar_test.go (56%) rename economic_calendar.go => finance/economic_calendar.go (51%) rename economic_calendar_test.go => finance/economic_calendar_test.go (99%) create mode 100644 finance/example_markets_test.go create mode 100644 finance/example_trading_test.go rename greeks_table.go => finance/greeks_table.go (77%) rename greeks_table_test.go => finance/greeks_table_test.go (94%) create mode 100644 finance/helpers_test.go rename index_card.go => finance/index_card.go (76%) rename index_card_test.go => finance/index_card_test.go (98%) rename level2_quotes.go => finance/level2_quotes.go (70%) rename level2_quotes_test.go => finance/level2_quotes_test.go (90%) rename leverage_slider.go => finance/leverage_slider.go (78%) rename leverage_slider_test.go => finance/leverage_slider_test.go (94%) rename margin_indicator.go => finance/margin_indicator.go (59%) rename margin_indicator_test.go => finance/margin_indicator_test.go (77%) rename market_overview.go => finance/market_overview.go (72%) rename market_overview_test.go => finance/market_overview_test.go (98%) rename market_status.go => finance/market_status.go (69%) rename market_status_test.go => finance/market_status_test.go (98%) rename markets_fixture_test.go => finance/markets_fixture_test.go (70%) rename markets_regression_test.go => finance/markets_regression_test.go (79%) rename news_feed.go => finance/news_feed.go (78%) rename news_feed_test.go => finance/news_feed_test.go (87%) rename option_chain.go => finance/option_chain.go (76%) rename option_chain_test.go => finance/option_chain_test.go (96%) rename order_book.go => finance/order_book.go (81%) rename order_book_test.go => finance/order_book_test.go (85%) rename order_confirm_dialog.go => finance/order_confirm_dialog.go (67%) rename order_confirm_dialog_test.go => finance/order_confirm_dialog_test.go (84%) rename order_entry.go => finance/order_entry.go (59%) rename order_entry_test.go => finance/order_entry_test.go (96%) rename order_table.go => finance/order_table.go (62%) rename order_table_test.go => finance/order_table_test.go (96%) rename payoff_diagram.go => finance/payoff_diagram.go (82%) rename payoff_diagram_test.go => finance/payoff_diagram_test.go (80%) rename performance_chart.go => finance/performance_chart.go (61%) rename performance_chart_test.go => finance/performance_chart_test.go (71%) rename pnl_display.go => finance/pnl_display.go (53%) rename pnl_display_test.go => finance/pnl_display_test.go (89%) rename portfolio_summary.go => finance/portfolio_summary.go (58%) rename portfolio_summary_test.go => finance/portfolio_summary_test.go (98%) rename position_table.go => finance/position_table.go (65%) rename position_table_test.go => finance/position_table_test.go (80%) rename price_change_badge.go => finance/price_change_badge.go (51%) rename price_change_badge_test.go => finance/price_change_badge_test.go (88%) create mode 100644 finance/price_text.go rename price_text_test.go => finance/price_text_test.go (73%) rename quick_trade_buttons.go => finance/quick_trade_buttons.go (68%) rename quick_trade_buttons_test.go => finance/quick_trade_buttons_test.go (94%) rename quote_card.go => finance/quote_card.go (75%) rename quote_card_test.go => finance/quote_card_test.go (81%) rename risk_meter.go => finance/risk_meter.go (77%) rename risk_meter_test.go => finance/risk_meter_test.go (83%) rename screener.go => finance/screener.go (64%) rename screener_test.go => finance/screener_test.go (99%) rename sentiment_gauge.go => finance/sentiment_gauge.go (69%) rename sentiment_gauge_test.go => finance/sentiment_gauge_test.go (91%) rename spread_indicator.go => finance/spread_indicator.go (57%) rename spread_indicator_test.go => finance/spread_indicator_test.go (78%) rename symbol_badge.go => finance/symbol_badge.go (81%) rename symbol_badge_test.go => finance/symbol_badge_test.go (98%) rename symbol_search.go => finance/symbol_search.go (71%) rename symbol_search_test.go => finance/symbol_search_test.go (99%) rename ticker_tape.go => finance/ticker_tape.go (75%) rename ticker_tape_test.go => finance/ticker_tape_test.go (90%) rename time_and_sales.go => finance/time_and_sales.go (63%) rename time_and_sales_test.go => finance/time_and_sales_test.go (89%) rename trade_history_table.go => finance/trade_history_table.go (59%) rename trade_history_table_test.go => finance/trade_history_table_test.go (84%) rename trading_session_clock.go => finance/trading_session_clock.go (91%) rename trading_session_clock_test.go => finance/trading_session_clock_test.go (99%) rename watchlist.go => finance/watchlist.go (77%) rename watchlist_test.go => finance/watchlist_test.go (97%) rename ansi_text.go => git/ansi_text.go (89%) rename ansi_text_test.go => git/ansi_text_test.go (91%) rename blame_view.go => git/blame_view.go (86%) rename blame_view_test.go => git/blame_view_test.go (94%) rename branch_list.go => git/branch_list.go (75%) rename branch_list_test.go => git/branch_list_test.go (87%) rename branch_selector.go => git/branch_selector.go (81%) rename branch_selector_test.go => git/branch_selector_test.go (95%) rename changes_list.go => git/changes_list.go (81%) rename changes_list_test.go => git/changes_list_test.go (94%) rename command_block.go => git/command_block.go (73%) rename command_block_test.go => git/command_block_test.go (71%) rename commit_graph.go => git/commit_graph.go (90%) rename commit_graph_test.go => git/commit_graph_test.go (95%) rename commit_input.go => git/commit_input.go (67%) rename commit_input_test.go => git/commit_input_test.go (92%) rename commit_list.go => git/commit_list.go (79%) rename commit_list_test.go => git/commit_list_test.go (85%) rename conflict_resolver.go => git/conflict_resolver.go (77%) rename conflict_resolver_test.go => git/conflict_resolver_test.go (95%) rename diff_stat.go => git/diff_stat.go (84%) rename diff_stat_test.go => git/diff_stat_test.go (92%) rename diff_viewer.go => git/diff_viewer.go (89%) rename diff_viewer_test.go => git/diff_viewer_test.go (94%) create mode 100644 git/doc.go create mode 100644 git/example_git_test.go rename file_history.go => git/file_history.go (58%) rename file_history_test.go => git/file_history_test.go (79%) rename git_status_badge.go => git/git_status_badge.go (75%) rename git_status_badge_test.go => git/git_status_badge_test.go (90%) create mode 100644 git/helpers_test.go rename inline_diff.go => git/inline_diff.go (83%) rename inline_diff_test.go => git/inline_diff_test.go (91%) rename pull_request_card.go => git/pull_request_card.go (83%) rename pull_request_card_test.go => git/pull_request_card_test.go (94%) rename review_comment.go => git/review_comment.go (76%) rename review_comment_test.go => git/review_comment_test.go (94%) rename stash_list.go => git/stash_list.go (61%) rename stash_list_test.go => git/stash_list_test.go (81%) rename tag_list.go => git/tag_list.go (63%) rename tag_list_test.go => git/tag_list_test.go (80%) rename three_way_merge.go => git/three_way_merge.go (85%) rename three_way_merge_test.go => git/three_way_merge_test.go (96%) rename banner.go => input/banner.go (62%) rename banner_test.go => input/banner_test.go (78%) rename baseline_states_test.go => input/baseline_states_test.go (99%) rename bulk_action_bar.go => input/bulk_action_bar.go (65%) rename bulk_action_bar_test.go => input/bulk_action_bar_test.go (88%) rename button.go => input/button.go (85%) rename button_group.go => input/button_group.go (92%) rename button_group_test.go => input/button_group_test.go (91%) rename button_test.go => input/button_test.go (86%) rename cascader.go => input/cascader.go (86%) rename cascader_test.go => input/cascader_test.go (97%) rename checkbox.go => input/checkbox.go (89%) rename checkbox_test.go => input/checkbox_test.go (94%) rename choice_blocked_test.go => input/choice_blocked_test.go (97%) rename choice_chips.go => input/choice_chips.go (85%) rename choice_chips_test.go => input/choice_chips_test.go (90%) rename choice_frame_test.go => input/choice_frame_test.go (94%) rename choice_review_test.go => input/choice_review_test.go (96%) rename color_palette.go => input/color_palette.go (85%) rename color_palette_test.go => input/color_palette_test.go (92%) rename color_picker.go => input/color_picker.go (88%) rename color_picker_test.go => input/color_picker_test.go (95%) rename combobox.go => input/combobox.go (88%) rename combobox_test.go => input/combobox_test.go (97%) rename copy_button.go => input/copy_button.go (72%) rename copy_button_test.go => input/copy_button_test.go (92%) rename currency_input.go => input/currency_input.go (93%) rename currency_input_test.go => input/currency_input_test.go (94%) create mode 100644 input/doc.go rename dock_panel.go => input/dock_panel.go (92%) rename dock_panel_test.go => input/dock_panel_test.go (92%) create mode 100644 input/example_choice_test.go create mode 100644 input/example_essentials_test.go create mode 100644 input/example_form_test.go create mode 100644 input/example_input_test.go rename file_drop_zone.go => input/file_drop_zone.go (85%) rename file_drop_zone_test.go => input/file_drop_zone_test.go (97%) rename file_picker.go => input/file_picker.go (83%) rename file_picker_test.go => input/file_picker_test.go (97%) rename form.go => input/form.go (84%) rename form_field.go => input/form_field.go (87%) rename form_field_test.go => input/form_field_test.go (94%) rename form_test.go => input/form_test.go (98%) create mode 100644 input/helpers_test.go rename hold_to_confirm.go => input/hold_to_confirm.go (82%) rename hold_to_confirm_test.go => input/hold_to_confirm_test.go (96%) rename icon_button.go => input/icon_button.go (57%) rename icon_button_test.go => input/icon_button_test.go (91%) rename input_group.go => input/input_group.go (96%) rename input_group_test.go => input/input_group_test.go (93%) rename input_review2_test.go => input/input_review2_test.go (95%) rename input_review_test.go => input/input_review_test.go (92%) rename knob.go => input/knob.go (75%) rename knob_test.go => input/knob_test.go (91%) rename masked_input.go => input/masked_input.go (97%) rename masked_input_test.go => input/masked_input_test.go (93%) rename masonry.go => input/masonry.go (98%) rename masonry_test.go => input/masonry_test.go (95%) rename mention_input.go => input/mention_input.go (93%) rename mention_input_model.go => input/mention_input_model.go (99%) rename mention_input_test.go => input/mention_input_test.go (97%) rename menubar_test.go => input/menubar_test.go (74%) rename multi_select.go => input/multi_select.go (72%) rename multi_select_test.go => input/multi_select_test.go (97%) rename number_input.go => input/number_input.go (70%) rename number_input_test.go => input/number_input_test.go (97%) rename password_input.go => input/password_input.go (78%) rename password_input_test.go => input/password_input_test.go (94%) rename pin_input.go => input/pin_input.go (89%) rename pin_input_test.go => input/pin_input_test.go (96%) rename qr_code.go => input/qr_code.go (82%) rename qr_code_encoder.go => input/qr_code_encoder.go (99%) rename qr_code_encoder_test.go => input/qr_code_encoder_test.go (99%) rename qr_code_grid.go => input/qr_code_grid.go (99%) rename qr_code_test.go => input/qr_code_test.go (87%) rename radio_group.go => input/radio_group.go (89%) rename radio_group_test.go => input/radio_group_test.go (95%) rename range_slider.go => input/range_slider.go (74%) rename range_slider_test.go => input/range_slider_test.go (96%) rename rating.go => input/rating.go (84%) rename rating_test.go => input/rating_test.go (93%) rename resizable_panel_group.go => input/resizable_panel_group.go (95%) rename resizable_panel_group_test.go => input/resizable_panel_group_test.go (96%) rename scrub_input.go => input/scrub_input.go (85%) rename scrub_input_test.go => input/scrub_input_test.go (95%) rename search_input.go => input/search_input.go (77%) rename search_input_test.go => input/search_input_test.go (90%) rename select.go => input/select.go (79%) rename select_test.go => input/select_test.go (97%) rename signature_pad.go => input/signature_pad.go (82%) rename signature_pad_test.go => input/signature_pad_test.go (95%) rename slider.go => input/slider.go (64%) rename slider_direction_test.go => input/slider_direction_test.go (98%) rename slider_grid_test.go => input/slider_grid_test.go (95%) rename slider_keys_test.go => input/slider_keys_test.go (87%) rename slider_test.go => input/slider_test.go (95%) rename spinner_test.go => input/spinner_test.go (76%) rename stable_keys_test.go => input/stable_keys_test.go (92%) rename switch.go => input/switch.go (86%) rename switch_test.go => input/switch_test.go (92%) rename tags_input.go => input/tags_input.go (87%) rename tags_input_test.go => input/tags_input_test.go (96%) rename {testdata => input/testdata}/qr/hello-l.txt (100%) rename {testdata => input/testdata}/qr/long-h.txt (100%) rename {testdata => input/testdata}/qr/text-q.txt (100%) rename {testdata => input/testdata}/qr/url-m.txt (100%) rename {testdata => input/testdata}/qr/v8-l.txt (100%) rename {testdata => input/testdata}/qr/zh-m.txt (100%) rename text_area.go => input/text_area.go (82%) rename text_area_test.go => input/text_area_test.go (93%) rename text_input.go => input/text_input.go (83%) rename text_input_test.go => input/text_input_test.go (91%) rename toggle.go => input/toggle.go (90%) rename toggle_group.go => input/toggle_group.go (87%) rename toggle_group_test.go => input/toggle_group_test.go (96%) rename toggle_test.go => input/toggle_test.go (88%) rename transfer.go => input/transfer.go (84%) rename transfer_test.go => input/transfer_test.go (94%) rename tree_select.go => input/tree_select.go (87%) rename tree_select_test.go => input/tree_select_test.go (97%) rename vertical_slider.go => input/vertical_slider.go (66%) rename vertical_slider_test.go => input/vertical_slider_test.go (90%) rename snapshot_test.go => internal/snaptest/snaptest.go (53%) rename app_shell.go => layout/app_shell.go (84%) rename app_shell_test.go => layout/app_shell_test.go (97%) rename aspect_ratio.go => layout/aspect_ratio.go (95%) rename aspect_ratio_test.go => layout/aspect_ratio_test.go (94%) rename container.go => layout/container.go (90%) rename container_test.go => layout/container_test.go (88%) rename divider.go => layout/divider.go (81%) rename divider_test.go => layout/divider_test.go (88%) create mode 100644 layout/doc.go rename example_layout_test.go => layout/example_layout_test.go (50%) rename grid.go => layout/grid.go (94%) rename grid_test.go => layout/grid_test.go (93%) create mode 100644 layout/helpers_test.go rename scroll_area.go => layout/scroll_area.go (92%) rename scroll_area_test.go => layout/scroll_area_test.go (93%) rename split_pane.go => layout/split_pane.go (95%) rename split_pane_test.go => layout/split_pane_test.go (97%) rename stack.go => layout/stack.go (88%) rename stack_test.go => layout/stack_test.go (92%) rename status_bar.go => layout/status_bar.go (89%) rename status_bar_test.go => layout/status_bar_test.go (94%) rename title_bar.go => layout/title_bar.go (79%) rename title_bar_test.go => layout/title_bar_test.go (78%) rename audio_player.go => media/audio_player.go (75%) rename audio_player_test.go => media/audio_player_test.go (96%) rename audio_spectrum.go => media/audio_spectrum.go (79%) rename audio_spectrum_test.go => media/audio_spectrum_test.go (78%) rename audio_waveform.go => media/audio_waveform.go (91%) rename audio_waveform_test.go => media/audio_waveform_test.go (89%) rename camera_preview.go => media/camera_preview.go (76%) rename camera_preview_test.go => media/camera_preview_test.go (96%) create mode 100644 media/device_selector.go rename device_selector_test.go => media/device_selector_test.go (96%) create mode 100644 media/doc.go create mode 100644 media/example_helpers_test.go create mode 100644 media/example_media_test.go create mode 100644 media/helpers_test.go rename image_annotator.go => media/image_annotator.go (79%) rename image_annotator_test.go => media/image_annotator_test.go (97%) rename image_compare.go => media/image_compare.go (74%) rename image_compare_test.go => media/image_compare_test.go (95%) rename image_cropper.go => media/image_cropper.go (90%) rename image_cropper_geom.go => media/image_cropper_geom.go (99%) rename image_cropper_test.go => media/image_cropper_test.go (97%) rename image_thumbnail.go => media/image_thumbnail.go (78%) rename image_thumbnail_test.go => media/image_thumbnail_test.go (93%) rename lightbox.go => media/lightbox.go (71%) rename lightbox_test.go => media/lightbox_test.go (81%) rename media_controls.go => media/media_controls.go (71%) rename media_controls_test.go => media/media_controls_test.go (96%) rename media_palette_test.go => media/media_palette_test.go (91%) rename mic_level_meter.go => media/mic_level_meter.go (84%) rename mic_level_meter_test.go => media/mic_level_meter_test.go (94%) rename playback_speed_control.go => media/playback_speed_control.go (72%) rename playback_speed_control_test.go => media/playback_speed_control_test.go (95%) rename playlist.go => media/playlist.go (77%) rename playlist_test.go => media/playlist_test.go (97%) rename screen_recorder_controls.go => media/screen_recorder_controls.go (60%) rename screen_recorder_controls_test.go => media/screen_recorder_controls_test.go (94%) rename subtitle_editor.go => media/subtitle_editor.go (79%) rename subtitle_editor_test.go => media/subtitle_editor_test.go (97%) rename video_player.go => media/video_player.go (79%) rename video_player_test.go => media/video_player_test.go (97%) rename video_scrubber.go => media/video_scrubber.go (64%) rename video_scrubber_test.go => media/video_scrubber_test.go (95%) rename video_thumbnail_strip.go => media/video_thumbnail_strip.go (74%) rename video_thumbnail_strip_test.go => media/video_thumbnail_strip_test.go (94%) rename volume_control.go => media/volume_control.go (58%) rename volume_control_test.go => media/volume_control_test.go (95%) delete mode 100644 media_icons.go rename call_controls.go => messaging/call_controls.go (70%) rename call_controls_test.go => messaging/call_controls_test.go (88%) rename channel_list.go => messaging/channel_list.go (82%) rename channel_list_test.go => messaging/channel_list_test.go (98%) rename chat_message.go => messaging/chat_message.go (71%) rename chat_message_test.go => messaging/chat_message_test.go (82%) create mode 100644 messaging/doc.go rename emoji_data.go => messaging/emoji_data.go (99%) rename emoji_picker.go => messaging/emoji_picker.go (77%) rename emoji_picker_test.go => messaging/emoji_picker_test.go (92%) create mode 100644 messaging/example_messaging_test.go create mode 100644 messaging/helpers_test.go rename incoming_call_dialog.go => messaging/incoming_call_dialog.go (56%) rename incoming_call_dialog_test.go => messaging/incoming_call_dialog_test.go (95%) rename mail_composer.go => messaging/mail_composer.go (69%) rename mail_composer_test.go => messaging/mail_composer_test.go (93%) rename mail_list.go => messaging/mail_list.go (78%) rename mail_list_test.go => messaging/mail_list_test.go (85%) rename mail_reader.go => messaging/mail_reader.go (75%) rename mail_reader_test.go => messaging/mail_reader_test.go (94%) rename member_list.go => messaging/member_list.go (85%) rename member_list_test.go => messaging/member_list_test.go (94%) rename messaging_calls_test.go => messaging/messaging_calls_test.go (75%) create mode 100644 messaging/messaging_icons.go rename messaging_mail_test.go => messaging/messaging_mail_test.go (90%) rename messaging_people_test.go => messaging/messaging_people_test.go (91%) rename messaging_regression_test.go => messaging/messaging_regression_test.go (92%) rename messaging_small_test.go => messaging/messaging_small_test.go (88%) rename online_status.go => messaging/online_status.go (82%) rename online_status_test.go => messaging/online_status_test.go (92%) rename pinned_messages.go => messaging/pinned_messages.go (65%) rename pinned_messages_test.go => messaging/pinned_messages_test.go (93%) rename quoted_text.go => messaging/quoted_text.go (57%) rename quoted_text_test.go => messaging/quoted_text_test.go (89%) rename read_receipt.go => messaging/read_receipt.go (73%) rename read_receipt_test.go => messaging/read_receipt_test.go (71%) rename recipient_input.go => messaging/recipient_input.go (78%) rename recipient_input_test.go => messaging/recipient_input_test.go (95%) rename snooze_picker.go => messaging/snooze_picker.go (76%) rename snooze_picker_test.go => messaging/snooze_picker_test.go (96%) rename status_setter.go => messaging/status_setter.go (61%) rename status_setter_test.go => messaging/status_setter_test.go (95%) rename thread_panel.go => messaging/thread_panel.go (68%) rename thread_panel_test.go => messaging/thread_panel_test.go (94%) rename unread_divider.go => messaging/unread_divider.go (61%) rename unread_divider_test.go => messaging/unread_divider_test.go (86%) rename user_profile_card.go => messaging/user_profile_card.go (72%) rename user_profile_card_test.go => messaging/user_profile_card_test.go (77%) rename video_call_grid.go => messaging/video_call_grid.go (76%) rename video_call_grid_test.go => messaging/video_call_grid_test.go (94%) delete mode 100644 messaging_icons.go rename breadcrumb.go => navigation/breadcrumb.go (82%) rename breadcrumb_test.go => navigation/breadcrumb_test.go (93%) rename command_palette.go => navigation/command_palette.go (86%) rename command_palette_test.go => navigation/command_palette_test.go (95%) create mode 100644 navigation/doc.go create mode 100644 navigation/example_navigation_test.go create mode 100644 navigation/helpers_test.go rename menubar.go => navigation/menubar.go (95%) rename menubar_panel.go => navigation/menubar_panel.go (87%) rename navigation_menu.go => navigation/navigation_menu.go (83%) rename navigation_menu_test.go => navigation/navigation_menu_test.go (94%) rename navigation_review_test.go => navigation/navigation_review_test.go (93%) rename pagination.go => navigation/pagination.go (71%) rename pagination_test.go => navigation/pagination_test.go (93%) rename segmented_control.go => navigation/segmented_control.go (90%) rename segmented_control_test.go => navigation/segmented_control_test.go (93%) rename sidebar.go => navigation/sidebar.go (84%) rename sidebar_test.go => navigation/sidebar_test.go (93%) rename steps.go => navigation/steps.go (81%) rename steps_test.go => navigation/steps_test.go (93%) rename tabs.go => navigation/tabs.go (88%) rename tabs_test.go => navigation/tabs_test.go (91%) rename toolbar.go => navigation/toolbar.go (75%) rename toolbar_test.go => navigation/toolbar_test.go (91%) rename alert_dialog.go => overlay/alert_dialog.go (70%) rename alert_dialog_test.go => overlay/alert_dialog_test.go (94%) rename context_menu.go => overlay/context_menu.go (96%) rename context_menu_test.go => overlay/context_menu_test.go (90%) rename dialog.go => overlay/dialog.go (74%) rename dialog_test.go => overlay/dialog_test.go (82%) create mode 100644 overlay/doc.go rename drawer.go => overlay/drawer.go (89%) rename drawer_test.go => overlay/drawer_test.go (85%) rename dropdown_menu.go => overlay/dropdown_menu.go (95%) rename dropdown_menu_test.go => overlay/dropdown_menu_test.go (95%) create mode 100644 overlay/example_essentials_test.go create mode 100644 overlay/example_overlay_test.go create mode 100644 overlay/helpers_test.go rename hover_card.go => overlay/hover_card.go (89%) rename hover_card_test.go => overlay/hover_card_test.go (89%) rename overlay.go => overlay/overlay.go (60%) rename overlay_fit_test.go => overlay/overlay_fit_test.go (83%) rename overlay_snapshot_test.go => overlay/overlay_snapshot_test.go (95%) rename overlay_test.go => overlay/overlay_test.go (89%) rename popconfirm.go => overlay/popconfirm.go (76%) rename popconfirm_test.go => overlay/popconfirm_test.go (85%) rename popover.go => overlay/popover.go (59%) rename popover_test.go => overlay/popover_test.go (82%) rename popup_menu.go => overlay/popup_menu.go (86%) rename split_button.go => overlay/split_button.go (73%) rename split_button_test.go => overlay/split_button_test.go (88%) rename tooltip.go => overlay/tooltip.go (69%) rename tooltip_test.go => overlay/tooltip_test.go (91%) delete mode 100644 price_text.go rename assignee_picker.go => project/assignee_picker.go (75%) rename assignee_picker_test.go => project/assignee_picker_test.go (91%) rename burndown_chart.go => project/burndown_chart.go (80%) rename burndown_chart_test.go => project/burndown_chart_test.go (79%) create mode 100644 project/doc.go rename due_date_picker.go => project/due_date_picker.go (56%) rename due_date_picker_test.go => project/due_date_picker_test.go (90%) create mode 100644 project/example_projects_test.go create mode 100644 project/helpers_test.go rename issue_card.go => project/issue_card.go (67%) rename issue_card_test.go => project/issue_card_test.go (94%) rename issue_id_badge.go => project/issue_id_badge.go (74%) rename issue_id_badge_test.go => project/issue_id_badge_test.go (89%) rename kanban_board.go => project/kanban_board.go (88%) rename kanban_board_test.go => project/kanban_board_test.go (97%) rename label_manager.go => project/label_manager.go (76%) rename label_manager_test.go => project/label_manager_test.go (91%) rename milestone_progress.go => project/milestone_progress.go (74%) rename milestone_progress_test.go => project/milestone_progress_test.go (95%) rename pomodoro_timer.go => project/pomodoro_timer.go (76%) rename pomodoro_timer_test.go => project/pomodoro_timer_test.go (96%) rename priority_indicator.go => project/priority_indicator.go (86%) rename priority_indicator_test.go => project/priority_indicator_test.go (84%) rename projects.go => project/projects.go (83%) rename projects_test.go => project/projects_test.go (85%) rename sprint_board.go => project/sprint_board.go (63%) rename sprint_board_test.go => project/sprint_board_test.go (88%) rename status_select.go => project/status_select.go (82%) rename status_select_test.go => project/status_select_test.go (94%) rename subtask_list.go => project/subtask_list.go (81%) rename subtask_list_test.go => project/subtask_list_test.go (96%) rename task_detail_panel.go => project/task_detail_panel.go (63%) rename task_detail_panel_test.go => project/task_detail_panel_test.go (97%) rename task_item.go => project/task_item.go (72%) rename task_item_test.go => project/task_item_test.go (96%) rename task_list.go => project/task_list.go (88%) rename task_list_test.go => project/task_list_test.go (95%) rename time_tracker.go => project/time_tracker.go (66%) rename time_tracker_test.go => project/time_tracker_test.go (86%) rename workload_view.go => project/workload_view.go (84%) rename workload_view_test.go => project/workload_view_test.go (94%) delete mode 100644 system_monitor.go delete mode 100644 week_picker.go diff --git a/AGENTS.md b/AGENTS.md index 5ee7e4e..ad4a13a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -MujicaUI: court-gothic components for MyGo native UI. Go library, package `mujica`, module `mujicaui`. +MujicaUI: court-gothic components for MyGo native UI. Go library, module `github.com/ZacharyZhang-NY/MujicaUI`, one package per component category. Progress truth: `TASKS.md`. Requirements: `PRD.md` and `docs/prd/ext-*.md` (101–500). Planned: once 001–500 land, the root package splits into category sub-packages (TASKS T22). Until then everything stays in the root package. @@ -18,17 +18,22 @@ Read MyGo source before using an API: `go env GOMODCACHE`/github.com/egoist/mygo ## Layout ```text -*.go one component per file, root package mujica -*_test.go tests beside the component -messages_.go built-in copy (en-US, zh-CN) via addMessages +core/ Use, Tokens, Density, Motion, FontSize, Msg; shared result types and helpers; + messages_.go built-in copy (en-US, zh-CN) via addMessages; docs tests +/ one component per file, package = folder name: + layout display input datetime navigation data overlay feedback chart + finance chat agent git code media project files messaging devtools account +/*_test.go tests beside the component; example__test.go compiled doc examples theme/ tokens, density, type, motion, contrast icons/ embedded Lucide icons: icons.Must("check") -internal/ focus ring, ornaments +internal/ focus ring, ornaments; internal/snaptest runs snapshot goldens examples/gallery/ native Gallery; pages_.go per PRD 4.x group docs/components/ NNN-name.md, one page per component -testdata/snapshots/ /-.png +testdata/snapshots/ /-.png, shared by every package ``` +Gallery categories map to packages: Inputs, Choices, Forms, Essentials -> input; Date & Time, Calendar -> datetime; Feedback, Agent Ops & Motion -> feedback; Charts I, Charts II, Market Charts -> chart; Trading, Markets -> finance; Chat Messages, Chat Input, Chat Sessions -> chat; Terminal & Code -> code; Projects -> project; Files & Collaboration -> files; Messaging & Mail -> messaging; Dashboards & Dev Tools -> devtools; Settings & Account -> account. Imports point down only: core imports no category; a piece two categories need moves to core or internal. + ## Component rules - Signature: `func Name(c *ui.Context, value *T, opts NameOptions) *ui.Element`. Composites may return a named struct. @@ -36,15 +41,15 @@ testdata/snapshots/ /-.png - Shared helpers: `iconAction` (borderless icon button), `overlayStyle(c, panel, overlayEnter(c, panel))` (popup surface and entry fade), `internal.RuleDiamond` / `internal.Frame` (ornaments), `colorFade` (state color transition). - Zero `Options` gives the default look. No `map[string]any` config. - Explicit font sizes: `FontSize(c, theme.CaptionSize)`; it applies the desktop text scale. -- Colors: `k := mujica.Tokens(c)`. Metrics: `theme.*` consts and `Density(c).Unit()` / `.ControlHeight()`. +- Colors: `k := core.Tokens(c)`. Metrics: `theme.*` consts and `Density(c).Unit()` / `.ControlHeight()`. - Build on MyGo Bases (`ui.ButtonBase`, `ui.CheckboxBase`, `ui.SelectBase`, ...) or MyGo widgets restyled through the theme `Use` sets. Never reach into MyGo internals. - Focus: `internal.Ring(e, k.Focus, radius)` for the 2 DIP ring with 2 DIP gap. - State marks (checked, selected, progress) use `OnAccent` on `Accent`, or `AccentText`, never `Accent` alone on dark surfaces (2.44:1). - Ornament color only for dividers, corners, panel titles. No gradients, glow, glass or texture. - Animated paths: pass coordinates through `internal.Snap(p, v)`. MyGo caches path masks by quarter-pixel keys (ui/path.go); unsnapped motion reuses near-miss masks and breaks snapshots. -- Durations: `mujica.Motion(c, theme.StateDuration)`; popups use `theme.PopupDuration`. +- Durations: `core.Motion(c, theme.StateDuration)`; popups use `theme.PopupDuration`. - Disabled blocks input before handling it (`Disabled(true)`); MyGo dims disabled elements, do not add opacity. -- Copy shown by a component comes from `msg(c, key)`; business copy comes from the caller. +- Copy shown by a component comes from `core.Msg(c, key)`; business copy comes from the caller. - No cross-frame `*ui.Element` caching, no goroutines touching elements. - Fail fast: panic on programmer errors (bad options); never swallow errors. - Log state transitions in composites with `slog.Debug("mujica: ", ...)`. Never log passwords. @@ -53,7 +58,7 @@ testdata/snapshots/ /-.png ## Tests - `ui.NewTester` drives click, keys, typing. Assert values, not existence. -- Every component: one `snapshot(t, name, w, h, view)` call (light and dark). Refresh with `go test -run -update .` +- Every component: one `snapshot(t, name, w, h, view)` call (light and dark; each package wraps `internal/snaptest`). Refresh with `go test -run -update ./` - Run `go vet ./... && go test ./...` before reporting done. - Each snapshot test renders in a fresh child process (snapshot_test.go), because MyGo's path-mask cache is process-wide. Order and neighbours never change pixels; `-run -update` is safe. diff --git a/README.md b/README.md index 4b07234..5ed073b 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,8 @@ ## 安装 -模块路径暂为 `mujicaui`,尚未发布到代码托管地址。本地使用: - ```sh -go mod edit -require=mujicaui@v0.0.0 -replace=mujicaui=/path/to/MujicaUI +go get github.com/ZacharyZhang-NY/MujicaUI ``` 要求 Go 1.27.1 与 MyGo v0.2.10(见 [兼容表](docs/compat.md))。 @@ -27,16 +25,17 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type app struct{ name string } func (a *app) view(c *ui.Context) { - mujica.Use(c, mujica.Settings{Locale: mujica.ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) ui.Column(c).Fill().Center().Gap(12).Children(func() { - mujica.TextInput(c, &a.name, mujica.TextInputOptions{Placeholder: "姓名"}) - if mujica.Button(c, "觐见", mujica.ButtonOptions{}).Clicked() { + input.TextInput(c, &a.name, input.TextInputOptions{Placeholder: "姓名"}) + if input.Button(c, "觐见", input.ButtonOptions{}).Clicked() { log.Println("hello", a.name) } }) @@ -55,15 +54,15 @@ func main() { ## 主题 -每帧开头调用 `mujica.Use(c, settings)`。`Settings` 决定模式(`System`、`Light`、`Dark`)、密度(`theme.Compact`、`theme.Default`、`theme.Comfortable`)、语言与文案覆盖。每个窗口各自设置,切换不重置业务状态。 +每帧开头调用 `core.Use(c, settings)`。`Settings` 决定模式(`System`、`Light`、`Dark`)、密度(`theme.Compact`、`theme.Default`、`theme.Comfortable`)、语言与文案覆盖。每个窗口各自设置,切换不重置业务状态。 ```go -mujica.Use(c, mujica.Settings{ - Mode: mujica.Dark, +core.Use(c, core.Settings{ + Mode: core.Dark, Density: theme.Compact, - Locale: mujica.ZhCN, + Locale: core.ZhCN, }) -k := mujica.Tokens(c) // 当前窗口的颜色 +k := core.Tokens(c) // 当前窗口的颜色 ``` 覆盖顺序:组件参数,窗口 `Settings`,库默认值。系统高对比度、文本缩放、减少动画始终生效。 diff --git a/accent_color_picker.go b/account/accent_color_picker.go similarity index 76% rename from accent_color_picker.go rename to account/accent_color_picker.go index 7fed104..3a55f67 100644 --- a/accent_color_picker.go +++ b/account/accent_color_picker.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AccentColorPickerOptions configure an AccentColorPicker. @@ -31,7 +33,7 @@ func accentPickerReadable(k theme.Tokens, accent ui.Color) bool { } // AccentColorPicker selects an accent and marks insufficient contrast. -func AccentColorPicker(c *ui.Context, color *ui.Color, opts AccentColorPickerOptions) ChoiceResult { +func AccentColorPicker(c *ui.Context, color *ui.Color, opts AccentColorPickerOptions) core.ChoiceResult { cands := opts.Candidates if cands == nil { cands = accentPickerColors @@ -39,13 +41,13 @@ func AccentColorPicker(c *ui.Context, color *ui.Color, opts AccentColorPickerOpt if len(cands) == 0 { panic("mujica: AccentColorPicker needs candidates") } - k := Tokens(c) - d := Density(c) - var r ChoiceResult + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult r.Element = ui.Column(c).Gap(d.Unit() * 2) r.Element.Children(func() { low := false - ui.Row(c).Gap(d.Unit() * 2).Wrap().Role(ui.RoleRadioGroup).Label(msg(c, "accent_color_picker.label")). + ui.Row(c).Gap(d.Unit() * 2).Wrap().Role(ui.RoleRadioGroup).Label(core.Msg(c, "accent_color_picker.label")). FocusGroup(ui.Horizontal).Disabled(opts.Disabled).Children(func() { selected := color if r.Element.IsDisabled() || opts.Disabled { @@ -55,13 +57,13 @@ func AccentColorPicker(c *ui.Context, color *ui.Color, opts AccentColorPickerOpt for _, col := range cands { ok := accentPickerReadable(k, col) low = low || !ok - hex := strings.ToUpper(colorPickerHex(col)) + hex := strings.ToUpper(input.ColorPickerHex(col)) sw := ui.RadioBase(c, selected, col).Size(36, 36).Center().Radius(theme.CardRadius).Label(hex) if !ok { - sw.Description(msg(c, "accent_color_picker.low")) + sw.Description(core.Msg(c, "accent_color_picker.low")) } if sw.Changed() && !sw.IsDisabled() { - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: accent_color_picker chose", "color", hex) } internal.Ring(sw, k.Focus, theme.CardRadius) @@ -86,12 +88,12 @@ func AccentColorPicker(c *ui.Context, color *ui.Color, opts AccentColorPickerOpt if low { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(k.TextMuted).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(12, 12) - ui.Text(c, msg(c, "accent_color_picker.low_note")).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, "accent_color_picker.low_note")).FontSize(core.FontSize(c, theme.CaptionSize)) }) } if !accentPickerReadable(k, *color) { - ui.Text(c, fmt.Sprintf(msg(c, "accent_color_picker.chosen_low"), strings.ToUpper(colorPickerHex(*color)))). - TextColor(k.Warning).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "accent_color_picker.chosen_low"), strings.ToUpper(input.ColorPickerHex(*color)))). + TextColor(k.Warning).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) } }) return r diff --git a/accent_color_picker_test.go b/account/accent_color_picker_test.go similarity index 97% rename from accent_color_picker_test.go rename to account/accent_color_picker_test.go index e59e3f0..acc473b 100644 --- a/accent_color_picker_test.go +++ b/account/accent_color_picker_test.go @@ -1,11 +1,11 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestAccentColorPickerMarksLowContrast(t *testing.T) { diff --git a/account_switcher.go b/account/account_switcher.go similarity index 74% rename from account_switcher.go rename to account/account_switcher.go index 786c654..dcfc44c 100644 --- a/account_switcher.go +++ b/account/account_switcher.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,9 +6,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Account is a signed-in account a user can switch to. @@ -62,8 +67,8 @@ func AccountSwitcher(c *ui.Context, current *string, accounts []Account, opts Ac accountSwitcherLarge(c, b, open, current, accounts, &r) return r } - popupMenuOpen(c, b, open, func(m *PopupMenu) { - m.Group(msg(c, "account_switcher.accounts"), func() { + overlay.PopupMenuOpen(c, b, open, func(m *overlay.PopupMenu) { + m.Group(core.Msg(c, "account_switcher.accounts"), func() { for _, a := range accounts { on := a.ID == *current label := a.Name @@ -71,7 +76,7 @@ func AccountSwitcher(c *ui.Context, current *string, accounts []Account, opts Ac label += " · " + a.Email } ui.Column(c).Key(a.ID).Shrink(0).Children(func() { - if m.Check(label, &on, PopupMenuItemOptions{}) && a.ID != *current && !b.IsDisabled() { + if m.Check(label, &on, overlay.PopupMenuItemOptions{}) && a.ID != *current && !b.IsDisabled() { *current, r.changed = a.ID, true slog.Debug("mujica: account_switcher switched") } @@ -79,7 +84,7 @@ func AccountSwitcher(c *ui.Context, current *string, accounts []Account, opts Ac } }) m.Separator() - if m.Item(msg(c, "account_switcher.add"), PopupMenuItemOptions{}) && !b.IsDisabled() { + if m.Item(core.Msg(c, "account_switcher.add"), overlay.PopupMenuItemOptions{}) && !b.IsDisabled() { r.add = true slog.Debug("mujica: account_switcher add") } @@ -88,13 +93,13 @@ func AccountSwitcher(c *ui.Context, current *string, accounts []Account, opts Ac } func accountSwitcherLarge(c *ui.Context, b *ui.Element, open *bool, current *string, accounts []Account, r *AccountSwitcherResult) { - Popover(c, b, open, PopoverOptions{Label: msg(c, "account_switcher.accounts"), Width: 320}, func() { + overlay.Popover(c, b, open, overlay.PopoverOptions{Label: core.Msg(c, "account_switcher.accounts"), Width: 320}, func() { cursor := ui.Local(b, "cursor", func() int { return -1 }) rows := ui.Local(b, "list", func() ui.ListState { return ui.ListState{} }) rows.Key = func(i int) any { return accounts[i].ID } rows.Label = func(i int) string { return accounts[i].Name } rows.Selected = cursor - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, rows, len(accounts), func(i int) { a := accounts[i] row := workspaceRow(c, Workspace{ID: a.ID, Name: a.Name, Detail: a.Email}, a.ID == *current, i == *cursor) @@ -106,7 +111,7 @@ func accountSwitcherLarge(c *ui.Context, b *ui.Element, open *bool, current *str } }).Height(280).MinHeight(0).AutoFocus() restore() - internal.Ring(list, Tokens(c).Focus, theme.ControlRadius) + internal.Ring(list, core.Tokens(c).Focus, theme.ControlRadius) if list.Changed() && !b.IsDisabled() { list.Focus() } @@ -114,7 +119,7 @@ func accountSwitcherLarge(c *ui.Context, b *ui.Element, open *bool, current *str id := accounts[*cursor].ID r.changed, *current, *open = id != *current, id, false } - if Button(c, msg(c, "account_switcher.add"), ButtonOptions{Variant: Ghost}).Clicked() && !b.IsDisabled() { + if input.Button(c, core.Msg(c, "account_switcher.add"), input.ButtonOptions{Variant: input.Ghost}).Clicked() && !b.IsDisabled() { r.add, *open = true, false } }) @@ -125,10 +130,10 @@ func accountSwitcherLarge(c *ui.Context, b *ui.Element, open *bool, current *str // accountTrigger displays an avatar, name and detail. func accountTrigger(c *ui.Context, name, detail string, avatar *ui.Bitmap, disabled bool) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).Role(ui.RoleMenuButton).MinHeight(d.ControlHeight()+d.Unit()*2).Padding(d.Unit(), d.Unit()*3, d.Unit(), d.Unit()*2). - Gap(d.Unit() * 2).Radius(theme.ControlRadius).Justify(ui.Start).Disabled(disabled).Label(name).Transition(colorFade) + Gap(d.Unit() * 2).Radius(theme.ControlRadius).Justify(ui.Start).Disabled(disabled).Label(name).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) switch { case b.Pressed(): @@ -137,11 +142,11 @@ func accountTrigger(c *ui.Context, name, detail string, avatar *ui.Bitmap, disab b.Background(k.SurfaceHover) } b.Children(func() { - Avatar(c, name, AvatarOptions{Image: avatar, Size: 28}) + display.Avatar(c, name, display.AvatarOptions{Image: avatar, Size: 28}) ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, name).FontWeight(600).SingleLine().Ellipsis("…") if detail != "" { - ui.Text(c, detail).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine().Ellipsis("…") + ui.Text(c, detail).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine().Ellipsis("…") } }) ui.Icon(c, icons.Must("chevron-down")).Size(14, 14).TextColor(k.TextMuted) diff --git a/account_switcher_test.go b/account/account_switcher_test.go similarity index 99% rename from account_switcher_test.go rename to account/account_switcher_test.go index 047032a..abd0bc7 100644 --- a/account_switcher_test.go +++ b/account/account_switcher_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/api_key_manager.go b/account/api_key_manager.go similarity index 69% rename from api_key_manager.go rename to account/api_key_manager.go index 01c35e1..7a824b7 100644 --- a/api_key_manager.go +++ b/account/api_key_manager.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -9,8 +9,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // APIKey contains public metadata and the visible prefix. @@ -65,8 +69,8 @@ func (r ApiKeyManagerResult) Revoked() (string, bool) { return r.revoke, r.revok // ApiKeyManager masks stored keys and confirms revocation. func ApiKeyManager(c *ui.Context, s *ApiKeyManagerState, keys []APIKey, opts ApiKeyManagerOptions) ApiKeyManagerResult { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r ApiKeyManagerResult r.Element = ui.Column(c).Gap(d.Unit() * 3).MinWidth(460).Disabled(opts.Disabled) disabled := r.Element.IsDisabled() @@ -89,12 +93,12 @@ func ApiKeyManager(c *ui.Context, s *ApiKeyManagerState, keys []APIKey, opts Api byID := s.index r.Element.Children(func() { ui.Row(c).Children(func() { - if Button(c, msg(c, "api_key_manager.create"), ButtonOptions{Icon: icons.Must("plus"), Disabled: disabled}).Clicked() && !disabled { + if input.Button(c, core.Msg(c, "api_key_manager.create"), input.ButtonOptions{Icon: icons.Must("plus"), Disabled: disabled}).Clicked() && !disabled { s.creating, s.name, s.err = true, "", "" } }) if len(keys) == 0 { - EmptyState(c, EmptyStateOptions{Title: msg(c, "api_key_manager.empty")}) + feedback.EmptyState(c, feedback.EmptyStateOptions{Title: core.Msg(c, "api_key_manager.empty")}) } accountRows(c, len(keys), func(i int) any { return keys[i].ID }, func(i int) { key := keys[i] @@ -103,10 +107,10 @@ func ApiKeyManager(c *ui.Context, s *ApiKeyManagerState, keys []APIKey, opts Api ui.Column(c).Grow(1).MinWidth(0).Gap(2).Children(func() { ui.Text(c, key.Name).FontWeight(600).SingleLine() ui.Text(c, apiKeyMasked(key.Prefix)).Font(theme.MonoFont).TextColor(k.TextMuted).SingleLine() - ui.Text(c, apiKeyDates(c, key)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, apiKeyDates(c, key)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) }) - b := Button(c, msg(c, "api_key_manager.revoke"), ButtonOptions{Variant: Secondary, Disabled: disabled}). - Label(fmt.Sprintf(msg(c, "api_key_manager.revoke_named"), key.Name)) + b := input.Button(c, core.Msg(c, "api_key_manager.revoke"), input.ButtonOptions{Variant: input.Secondary, Disabled: disabled}). + Label(fmt.Sprintf(core.Msg(c, "api_key_manager.revoke_named"), key.Name)) if b.Clicked() && !disabled { s.revoking = key.ID } @@ -119,8 +123,8 @@ func ApiKeyManager(c *ui.Context, s *ApiKeyManagerState, keys []APIKey, opts Api } victim, exists := byID[s.revoking] open := s.revoking != "" && exists - res := AlertDialog(c, &open, AlertDialogOptions{Title: fmt.Sprintf(msg(c, "api_key_manager.revoke_title"), victim.Name), - Description: msg(c, "api_key_manager.revoke_text"), ConfirmLabel: msg(c, "api_key_manager.revoke")}) + res := overlay.AlertDialog(c, &open, overlay.AlertDialogOptions{Title: fmt.Sprintf(core.Msg(c, "api_key_manager.revoke_title"), victim.Name), + Description: core.Msg(c, "api_key_manager.revoke_text"), ConfirmLabel: core.Msg(c, "api_key_manager.revoke")}) if res.Confirmed && !disabled { r.revoke = victim.ID slog.Debug("mujica: api_key_manager revoke") @@ -136,36 +140,36 @@ func apiKeyMasked(prefix string) string { return prefix + strings.Repeat("•", // apiKeyDates says when a key was made and last used. func apiKeyDates(c *ui.Context, key APIKey) string { - used := msg(c, "api_key_manager.never") + used := core.Msg(c, "api_key_manager.never") if !key.LastUsed.IsZero() { used = key.LastUsed.Format("2006-01-02") } - return fmt.Sprintf(msg(c, "api_key_manager.dates"), key.Created.Format("2006-01-02"), used) + return fmt.Sprintf(core.Msg(c, "api_key_manager.dates"), key.Created.Format("2006-01-02"), used) } // apiKeyCreateDialog collects a name and reveals the generated secret. func apiKeyCreateDialog(c *ui.Context, s *ApiKeyManagerState, disabled bool) (string, bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) open := !disabled && (s.creating || s.pending || s.secret != "") was := open asked := false - title := msg(c, "api_key_manager.create_title") + title := core.Msg(c, "api_key_manager.create_title") if s.secret != "" { - title = msg(c, "api_key_manager.secret_title") + title = core.Msg(c, "api_key_manager.secret_title") } - Dialog(c, &open, DialogOptions{Title: title, KeepOnBackdrop: true, KeepOnEscape: s.pending, HideClose: s.pending, Actions: func() { + overlay.Dialog(c, &open, overlay.DialogOptions{Title: title, KeepOnBackdrop: true, KeepOnEscape: s.pending, HideClose: s.pending, Actions: func() { switch { case s.secret != "": - if Button(c, msg(c, "api_key_manager.done"), ButtonOptions{}).Clicked() { + if input.Button(c, core.Msg(c, "api_key_manager.done"), input.ButtonOptions{}).Clicked() { open = false } default: - if Button(c, msg(c, "api_key_manager.cancel"), ButtonOptions{Variant: Secondary, Disabled: s.pending}).Clicked() { + if input.Button(c, core.Msg(c, "api_key_manager.cancel"), input.ButtonOptions{Variant: input.Secondary, Disabled: s.pending}).Clicked() { open = false } ok := strings.TrimSpace(s.name) != "" - if Button(c, msg(c, "api_key_manager.create"), ButtonOptions{Loading: s.pending, Disabled: !ok}).Clicked() && ok && !s.pending { + if input.Button(c, core.Msg(c, "api_key_manager.create"), input.ButtonOptions{Loading: s.pending, Disabled: !ok}).Clicked() && ok && !s.pending { asked = true } } @@ -173,19 +177,19 @@ func apiKeyCreateDialog(c *ui.Context, s *ApiKeyManagerState, disabled bool) (st if s.secret != "" { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Start).Padding(d.Unit() * 3).Radius(theme.ControlRadius).Background(k.Warning.Alpha(0.12)).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(16, 16).TextColor(k.Warning).Shrink(0) - ui.Text(c, msg(c, "api_key_manager.once")).Grow(1) + ui.Text(c, core.Msg(c, "api_key_manager.once")).Grow(1) }) ui.Row(c).Gap(d.Unit()*2).AlignItems(ui.Center).Padding(d.Unit()*2, d.Unit()*3).Radius(theme.ControlRadius). Background(k.Background).Border(theme.BorderWidth, k.Border).Children(func() { - ui.Text(c, s.secret).Font(theme.MonoFont).Selectable().Label(msg(c, "api_key_manager.secret_title")).Grow(1).MinWidth(0) - if iconAction(c, icons.Must("copy"), msg(c, "api_key_manager.copy")).Clicked() { + ui.Text(c, s.secret).Font(theme.MonoFont).Selectable().Label(core.Msg(c, "api_key_manager.secret_title")).Grow(1).MinWidth(0) + if core.IconAction(c, icons.Must("copy"), core.Msg(c, "api_key_manager.copy")).Clicked() { c.WriteClipboard(s.secret) - c.Announce(msg(c, "api_key_manager.copied")) + c.Announce(core.Msg(c, "api_key_manager.copied")) } }) return } - res := TextInput(c, &s.name, TextInputOptions{Label: msg(c, "api_key_manager.name"), Placeholder: msg(c, "api_key_manager.name"), + res := input.TextInput(c, &s.name, input.TextInputOptions{Label: core.Msg(c, "api_key_manager.name"), Placeholder: core.Msg(c, "api_key_manager.name"), Disabled: s.pending, Error: s.err}) res.Input.AutoFocus() if res.Submitted() && strings.TrimSpace(s.name) != "" && !s.pending { diff --git a/api_key_manager_test.go b/account/api_key_manager_test.go similarity index 99% rename from api_key_manager_test.go rename to account/api_key_manager_test.go index 3b58882..b366cc7 100644 --- a/api_key_manager_test.go +++ b/account/api_key_manager_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/coachmark.go b/account/coachmark.go similarity index 67% rename from coachmark.go rename to account/coachmark.go index f436519..d65e2a0 100644 --- a/coachmark.go +++ b/account/coachmark.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,7 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CoachmarkStep uses an anchor built in the current frame. @@ -44,8 +47,8 @@ func Coachmark(c *ui.Context, open *bool, step *int, steps []CoachmarkStep) Coac *open = false return r } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) if b := cur.Anchor.Bounds(); b.W > 0 { const pad = 4 ui.Overlay(c, func() { @@ -54,26 +57,26 @@ func Coachmark(c *ui.Context, open *bool, step *int, steps []CoachmarkStep) Coac }) } from := *step - r.Element = Popover(c, cur.Anchor, open, PopoverOptions{Title: cur.Title, Width: 300}, func() { + r.Element = overlay.Popover(c, cur.Anchor, open, overlay.PopoverOptions{Title: cur.Title, Width: 300}, func() { ui.Column(c).Gap(d.Unit() * 3).Children(func() { if cur.Body != "" { ui.Text(c, cur.Body) } ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "coachmark.count"), *step+1, len(steps))).TextColor(k.TextMuted). - FontSize(FontSize(c, theme.CaptionSize)).Grow(1) - if *step < len(steps)-1 && Button(c, msg(c, "coachmark.skip"), ButtonOptions{Variant: Ghost}).Clicked() { + ui.Text(c, fmt.Sprintf(core.Msg(c, "coachmark.count"), *step+1, len(steps))).TextColor(k.TextMuted). + FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1) + if *step < len(steps)-1 && input.Button(c, core.Msg(c, "coachmark.skip"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { *open, r.dismissed = false, true } - if *step > 0 && Button(c, msg(c, "coachmark.back"), ButtonOptions{Variant: Secondary}).Clicked() { + if *step > 0 && input.Button(c, core.Msg(c, "coachmark.back"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { *step-- } last := *step == len(steps)-1 - next := msg(c, "coachmark.next") + next := core.Msg(c, "coachmark.next") if last { - next = msg(c, "coachmark.done") + next = core.Msg(c, "coachmark.done") } - if Button(c, next, ButtonOptions{}).AutoFocus().Clicked() { + if input.Button(c, next, input.ButtonOptions{}).AutoFocus().Clicked() { if last { *open, r.finished = false, true } else { diff --git a/coachmark_test.go b/account/coachmark_test.go similarity index 87% rename from coachmark_test.go rename to account/coachmark_test.go index 648bd15..a54abb3 100644 --- a/coachmark_test.go +++ b/account/coachmark_test.go @@ -1,16 +1,18 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func coachmarkView(c *ui.Context, open *bool, step *int, r *CoachmarkResult) { var a, b *ui.Element ui.Row(c).Gap(40).Padding(20).Children(func() { - a = Button(c, "Compose", ButtonOptions{}) - b = Button(c, "Archive", ButtonOptions{Variant: Secondary}) + a = input.Button(c, "Compose", input.ButtonOptions{}) + b = input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) }) *r = Coachmark(c, open, step, []CoachmarkStep{ {Anchor: a, Title: "Write here", Body: "Start a new letter."}, @@ -63,7 +65,7 @@ func TestCoachmarkSnapshot(t *testing.T) { func TestCoachmarkDisabledAnchorCloses(t *testing.T) { open, step, disabled := true, 0, false tt := accountTester(600, 400, func(c *ui.Context) { - a := Button(c, "Compose", ButtonOptions{Disabled: disabled}) + a := input.Button(c, "Compose", input.ButtonOptions{Disabled: disabled}) Coachmark(c, &open, &step, []CoachmarkStep{{Anchor: a, Title: "Write here"}}) }) disabled = true diff --git a/account/doc.go b/account/doc.go new file mode 100644 index 0000000..6d58503 --- /dev/null +++ b/account/doc.go @@ -0,0 +1,2 @@ +// Package account holds MujicaUI settings and account management. +package account diff --git a/account/example_account_test.go b/account/example_account_test.go new file mode 100644 index 0000000..1417074 --- /dev/null +++ b/account/example_account_test.go @@ -0,0 +1,242 @@ +package account_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/account" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleSettingsLayout() { + page := "" + sound := true + pages := []account.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.SettingsLayout(c, &page, pages, account.SettingsLayoutOptions{}, func(id string) { + account.SettingsSection(c, "Notifications", func() { + account.SettingsRow(c, "Sound", account.SettingsRowOptions{ID: "sound"}, func() *ui.Element { + return input.Switch(c, &sound, "", input.SwitchOptions{}) + }) + }) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSettingsSearch() { + query, page := "", "general" + entries := []account.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if e, ok := account.SettingsSearch(c, &query, entries, account.SettingsSearchOptions{}).Chosen(); ok { + page = e.Page + } + fmt.Println("page", page) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleThemeSelector() { + mode := core.System + view := func(c *ui.Context) { + core.Use(c, core.Settings{Mode: mode}) + account.ThemeSelector(c, &mode, account.ThemeSelectorOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAccentColorPicker() { + accent := ui.Hex("#7D2034") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.AccentColorPicker(c, &accent, account.AccentColorPickerOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleShortcutRecorder() { + palette := account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.ShortcutRecorder(c, &palette, account.ShortcutRecorderOptions{ + Label: "Command palette", + Taken: []account.ShortcutBinding{{Action: "Save", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleKeyboardShortcutsList() { + query := "" + groups := []account.ShortcutGroup{{Title: "General", Bindings: []account.ShortcutBinding{ + {Action: "Command palette", Shortcut: account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, + }}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.KeyboardShortcutsList(c, &query, groups, account.KeyboardShortcutsListOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLoginForm() { + var state account.LoginFormState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.LoginForm(c, &state, account.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { + // Submit credentials through the caller-provided auth service. + state.EndSubmit("") + }}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOAuthButtons() { + providers := []account.OAuthProvider{{ID: "sso", Name: "Company SSO"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := account.OAuthButtons(c, providers, account.OAuthButtonsOptions{}).Chosen(); ok { + fmt.Println("start sign-in with", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTwoFactorInput() { + code := "" + resendAt := time.Now().Add(30 * time.Second) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := account.TwoFactorInput(c, &code, account.TwoFactorInputOptions{ResendAt: resendAt}) + if r.Completed() { + fmt.Println("verify the code") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProfileEditor() { + saved := account.Profile{Name: "Sakiko", Bio: "Keyboard."} + profile := saved + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if account.ProfileEditor(c, &profile, account.ProfileEditorOptions{Saved: saved}).Save() { + saved = profile + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAccountSwitcher() { + current := "a" + accounts := []account.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if account.AccountSwitcher(c, ¤t, accounts, account.AccountSwitcherOptions{}).Add() { + fmt.Println("sign in another account") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWorkspaceSwitcher() { + current := "ave" + spaces := []account.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.WorkspaceSwitcher(c, ¤t, spaces, account.WorkspaceSwitcherOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleUserMenu() { + status := account.UserStatusOnline + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if account.UserMenu(c, &status, account.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { + fmt.Println("sign out") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSessionList() { + sessions := []account.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := account.SessionList(c, sessions, account.SessionListOptions{}).Revoked(); ok { + fmt.Println("sign out", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleApiKeyManager() { + var state account.ApiKeyManagerState + keys := []account.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if name, ok := account.ApiKeyManager(c, &state, keys, account.ApiKeyManagerOptions{}).Create(); ok { + fmt.Println("create", name) + state.Reveal("secret from your key service") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleUsageQuota() { + items := []account.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.UsageQuota(c, items, account.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOnboardingWizard() { + step := 0 + steps := []account.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if account.OnboardingWizard(c, &step, steps, account.OnboardingWizardOptions{}).Finished() { + fmt.Println("onboarded") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCoachmark() { + open, step := true, 0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + compose := input.Button(c, "Compose", input.ButtonOptions{}) + account.Coachmark(c, &open, &step, []account.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWhatsNewDialog() { + open, hide := true, false + releases := []account.Release{{Version: "1.1", Changes: []account.ReleaseChange{{Kind: account.ChangeNew, Text: "500 components."}}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + account.WhatsNewDialog(c, &open, &hide, releases, account.WhatsNewDialogOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFeedbackWidget() { + var state account.FeedbackWidgetState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if f, ok := account.FeedbackWidget(c, &state, account.FeedbackWidgetOptions{}).Submitted(); ok { + fmt.Println("send", f.Kind) + state.EndSubmit("") + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/feedback_widget.go b/account/feedback_widget.go similarity index 66% rename from feedback_widget.go rename to account/feedback_widget.go index 8002399..d617508 100644 --- a/feedback_widget.go +++ b/account/feedback_widget.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,6 +6,10 @@ import ( "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) // FeedbackKind is what the feedback is about. @@ -27,7 +31,7 @@ type Feedback struct { // FeedbackWidgetState holds the draft and its form state. type FeedbackWidgetState struct { Draft Feedback - Form FormState + Form input.FormState } // EndSubmit clears successful drafts and preserves failed ones. @@ -69,41 +73,41 @@ func FeedbackWidget(c *ui.Context, s *FeedbackWidgetState, opts FeedbackWidgetOp if minLen == 0 { minLen = 10 } - d := Density(c) + d := core.Density(c) var r FeedbackWidgetResult r.Element = ui.Column(c).Gap(d.Unit() * 4).Width(420).Disabled(opts.Disabled) disabled := r.Element.IsDisabled() if disabled { frozen := *s - frozen.Form.order = append([]string(nil), s.Form.order...) - frozen.Form.fields = make(map[string]*formFieldState, len(s.Form.fields)) - for name, field := range s.Form.fields { + frozen.Form.Order = append([]string(nil), s.Form.Order...) + frozen.Form.Fields = make(map[string]*input.FormFieldState, len(s.Form.Fields)) + for name, field := range s.Form.Fields { copy := *field - frozen.Form.fields[name] = © + frozen.Form.Fields[name] = © } - frozen.Form.requested, frozen.Form.awaiting = false, false + frozen.Form.Requested, frozen.Form.Awaiting = false, false s = &frozen } locked := disabled || s.Form.Submitting() r.Element.Children(func() { kind := int(s.Draft.Kind) - var segs []Segment + var segs []navigation.Segment for f := FeedbackBug; f <= FeedbackOther; f++ { - segs = append(segs, Segment{Label: msg(c, fmt.Sprintf("feedback_widget.kind%d", f))}) + segs = append(segs, navigation.Segment{Label: core.Msg(c, fmt.Sprintf("feedback_widget.kind%d", f))}) } - SegmentedControl(c, &kind, segs, SegmentedControlOptions{Equal: true, Label: msg(c, "feedback_widget.kind"), Disabled: locked}) + navigation.SegmentedControl(c, &kind, segs, navigation.SegmentedControlOptions{Equal: true, Label: core.Msg(c, "feedback_widget.kind"), Disabled: locked}) if !locked { s.Draft.Kind = FeedbackKind(kind) } - res := Form(c, &s.Form, FormOptions{SubmitLabel: msg(c, "feedback_widget.send"), ResetLabel: msg(c, "feedback_widget.clear")}, func(f *FormScope) { - FormItem(c, f, "text", &s.Draft.Text, FormItemOptions[string]{Label: msg(c, "feedback_widget.message"), Required: true, + res := input.Form(c, &s.Form, input.FormOptions{SubmitLabel: core.Msg(c, "feedback_widget.send"), ResetLabel: core.Msg(c, "feedback_widget.clear")}, func(f *input.FormScope) { + input.FormItem(c, f, "text", &s.Draft.Text, input.FormItemOptions[string]{Label: core.Msg(c, "feedback_widget.message"), Required: true, Validate: func(v string) string { if n := len([]rune(strings.TrimSpace(v))); n < minLen { - return fmt.Sprintf(msg(c, "feedback_widget.short"), minLen) + return fmt.Sprintf(core.Msg(c, "feedback_widget.short"), minLen) } return "" }}, func() *ui.Element { - return TextArea(c, &s.Draft.Text, TextAreaOptions{MinHeight: 96, MaxLength: 2000, Disabled: locked}).Input + return input.TextArea(c, &s.Draft.Text, input.TextAreaOptions{MinHeight: 96, MaxLength: 2000, Disabled: locked}).Input }) if opts.Attachments != nil { ui.Column(c).Gap(d.Unit()).Disabled(locked).Children(opts.Attachments) diff --git a/feedback_widget_test.go b/account/feedback_widget_test.go similarity index 92% rename from feedback_widget_test.go rename to account/feedback_widget_test.go index f40e405..bde1edc 100644 --- a/feedback_widget_test.go +++ b/account/feedback_widget_test.go @@ -1,9 +1,11 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestFeedbackWidgetValidatesSubmitsOnce(t *testing.T) { @@ -63,7 +65,7 @@ func TestFeedbackWidgetSnapshot(t *testing.T) { }) ui.Column(c).Padding(16).Children(func() { FeedbackWidget(c, s, FeedbackWidgetOptions{Attachments: func() { - Button(c, "Attach screenshot", ButtonOptions{Variant: Secondary}) + input.Button(c, "Attach screenshot", input.ButtonOptions{Variant: input.Secondary}) }}) }) }) @@ -74,7 +76,7 @@ func TestFeedbackWidgetPendingFreezesDraftAndAttachments(t *testing.T) { attachments := 0 tt := accountTester(500, 500, func(c *ui.Context) { FeedbackWidget(c, &s, FeedbackWidgetOptions{Attachments: func() { - if Button(c, "Attach", ButtonOptions{}).Clicked() { + if input.Button(c, "Attach", input.ButtonOptions{}).Clicked() { attachments++ } }}) @@ -115,7 +117,7 @@ func TestFeedbackWidgetDisabledKeepsFormState(t *testing.T) { s := FeedbackWidgetState{Draft: Feedback{Text: "A long enough message."}} s.Form.Submit() accountTester(600, 500, func(c *ui.Context) { FeedbackWidget(c, &s, FeedbackWidgetOptions{Disabled: true}) }) - if s.Form.Submitting() || !s.Form.requested || s.Form.fields != nil { + if s.Form.Submitting() || !s.Form.Requested || s.Form.Fields != nil { t.Fatal("disabled frame edits form state") } } diff --git a/account/helpers_test.go b/account/helpers_test.go new file mode 100644 index 0000000..5a02d7b --- /dev/null +++ b/account/helpers_test.go @@ -0,0 +1,24 @@ +package account + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/keyboard_shortcuts_list.go b/account/keyboard_shortcuts_list.go similarity index 86% rename from keyboard_shortcuts_list.go rename to account/keyboard_shortcuts_list.go index 4b7fa70..d05c269 100644 --- a/keyboard_shortcuts_list.go +++ b/account/keyboard_shortcuts_list.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "reflect" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ShortcutGroup is a titled group of bindings in a KeyboardShortcutsList. @@ -33,19 +36,19 @@ type shortcutsListState struct { // KeyboardShortcutsList displays searchable, grouped shortcuts using visible rows. func KeyboardShortcutsList(c *ui.Context, query *string, groups []ShortcutGroup, opts KeyboardShortcutsListOptions) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "keyboard_shortcuts_list.search") + label = core.Msg(c, "keyboard_shortcuts_list.search") } e := ui.Column(c).Gap(d.Unit() * 4).MinWidth(320) st := ui.Local(e, "shortcuts", func() shortcutsListState { return shortcutsListState{} }) e.Children(func() { - SearchInput(c, query, SearchInputOptions{Placeholder: label, Label: label}) + input.SearchInput(c, query, input.SearchInputOptions{Placeholder: label, Label: label}) shortcutsListPlan(st, groups, strings.ToLower(strings.TrimSpace(*query))) if len(st.rows) == 0 { - EmptyState(c, EmptyStateOptions{Kind: EmptyNoResults, Query: *query}) + feedback.EmptyState(c, feedback.EmptyStateOptions{Kind: feedback.EmptyNoResults, Query: *query}) return } accountRows(c, len(st.rows), func(i int) any { return st.rows[i] }, func(i int) { diff --git a/keyboard_shortcuts_list_test.go b/account/keyboard_shortcuts_list_test.go similarity index 99% rename from keyboard_shortcuts_list_test.go rename to account/keyboard_shortcuts_list_test.go index 22b1611..958a133 100644 --- a/keyboard_shortcuts_list_test.go +++ b/account/keyboard_shortcuts_list_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/login_form.go b/account/login_form.go similarity index 68% rename from login_form.go rename to account/login_form.go index ddd3470..38934d0 100644 --- a/login_form.go +++ b/account/login_form.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // LoginFormState holds the form; only OnSubmit sees the password. @@ -42,42 +44,42 @@ func LoginForm(c *ui.Context, s *LoginFormState, opts LoginFormOptions) *ui.Elem if opts.OnSubmit == nil { panic("mujica: LoginForm needs OnSubmit") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) accountLabel := opts.AccountLabel if accountLabel == "" { - accountLabel = msg(c, "login_form.account") + accountLabel = core.Msg(c, "login_form.account") } e := ui.Column(c).Gap(d.Unit() * 4).Width(340).Disabled(opts.Disabled) disabled := e.IsDisabled() missing := func(v string) string { if s.tried && strings.TrimSpace(v) == "" { - return msg(c, "form.required") + return core.Msg(c, "form.required") } return "" } submit := false - var acc, pw InputResult + var acc, pw input.InputResult e.Children(func() { - FormField(c, accountLabel, FormFieldOptions{Required: true}, func() *ui.Element { - acc = TextInput(c, &s.Account, TextInputOptions{Disabled: disabled || s.submitting, Error: missing(s.Account)}) + input.FormField(c, accountLabel, input.FormFieldOptions{Required: true}, func() *ui.Element { + acc = input.TextInput(c, &s.Account, input.TextInputOptions{Disabled: disabled || s.submitting, Error: missing(s.Account)}) return acc.Input }) - FormField(c, msg(c, "login_form.password"), FormFieldOptions{Required: true}, func() *ui.Element { - pw = PasswordInput(c, &s.password, PasswordInputOptions{Disabled: disabled || s.submitting, Error: missing(s.password)}) + input.FormField(c, core.Msg(c, "login_form.password"), input.FormFieldOptions{Required: true}, func() *ui.Element { + pw = input.PasswordInput(c, &s.password, input.PasswordInputOptions{Disabled: disabled || s.submitting, Error: missing(s.password)}) return pw.Input }) if acc.Changed() || pw.Changed() { s.err = "" } - Checkbox(c, &s.Remember, msg(c, "login_form.remember"), CheckboxOptions{Disabled: disabled || s.submitting}) + input.Checkbox(c, &s.Remember, core.Msg(c, "login_form.remember"), input.CheckboxOptions{Disabled: disabled || s.submitting}) if s.err != "" { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Start).TextColor(k.Danger).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16).Shrink(0) ui.Text(c, s.err).Grow(1) }) } - b := Button(c, msg(c, "login_form.submit"), ButtonOptions{Loading: s.submitting, Disabled: disabled}) + b := input.Button(c, core.Msg(c, "login_form.submit"), input.ButtonOptions{Loading: s.submitting, Disabled: disabled}) b.FillWidth() submit = b.Clicked() || acc.Submitted() || pw.Submitted() }) diff --git a/login_form_test.go b/account/login_form_test.go similarity index 99% rename from login_form_test.go rename to account/login_form_test.go index a014e16..912eace 100644 --- a/login_form_test.go +++ b/account/login_form_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "bytes" diff --git a/oauth_buttons.go b/account/oauth_buttons.go similarity index 82% rename from oauth_buttons.go rename to account/oauth_buttons.go index b13b08e..1464b31 100644 --- a/oauth_buttons.go +++ b/account/oauth_buttons.go @@ -1,10 +1,13 @@ -package mujica +package account import ( "fmt" "log/slog" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // OAuthProvider is a sign-in provider; icons are plain, not trademarks. @@ -34,7 +37,7 @@ func OAuthButtons(c *ui.Context, providers []OAuthProvider, opts OAuthButtonsOpt if len(providers) == 0 { panic("mujica: OAuthButtons needs providers") } - d := Density(c) + d := core.Density(c) var r OAuthButtonsResult r.Element = ui.Column(c).Gap(d.Unit() * 2).Width(340).Disabled(opts.Disabled) disabled := r.Element.IsDisabled() @@ -43,9 +46,9 @@ func OAuthButtons(c *ui.Context, providers []OAuthProvider, opts OAuthButtonsOpt if p.ID == "" || p.Name == "" { panic("mujica: OAuthProvider needs an ID and a Name") } - label := fmt.Sprintf(msg(c, "oauth_buttons.continue"), p.Name) + label := fmt.Sprintf(core.Msg(c, "oauth_buttons.continue"), p.Name) ui.Column(c).Key(p.ID).AlignItems(ui.Stretch).Children(func() { - b := Button(c, label, ButtonOptions{Variant: Secondary, Icon: p.Icon, Loading: opts.Pending == p.ID, + b := input.Button(c, label, input.ButtonOptions{Variant: input.Secondary, Icon: p.Icon, Loading: opts.Pending == p.ID, Disabled: opts.Pending != "" && opts.Pending != p.ID}) if b.Clicked() && !disabled && opts.Pending == "" { r.chosen = p.ID diff --git a/oauth_buttons_test.go b/account/oauth_buttons_test.go similarity index 99% rename from oauth_buttons_test.go rename to account/oauth_buttons_test.go index 5d7f0e5..4e61935 100644 --- a/oauth_buttons_test.go +++ b/account/oauth_buttons_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "testing" diff --git a/onboarding_wizard.go b/account/onboarding_wizard.go similarity index 65% rename from onboarding_wizard.go rename to account/onboarding_wizard.go index 98ee3e7..6985090 100644 --- a/onboarding_wizard.go +++ b/account/onboarding_wizard.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,7 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WizardStep is one page of an OnboardingWizard. @@ -40,26 +43,26 @@ func OnboardingWizard(c *ui.Context, step *int, steps []WizardStep, opts Onboard if len(steps) == 0 || *step < 0 || *step >= len(steps) { panic(fmt.Sprintf("mujica: OnboardingWizard step %d outside %d steps", *step, len(steps))) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r OnboardingWizardResult from := *step r.Element = ui.Column(c).Gap(d.Unit()*5).Width(520).Padding(d.Unit()*6).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border) r.Element.Children(func() { - head := make([]Step, len(steps)) + head := make([]navigation.Step, len(steps)) for i, s := range steps { if s.Title == "" { panic("mujica: WizardStep needs a Title") } - head[i] = Step{Title: s.Title} + head[i] = navigation.Step{Title: s.Title} } at := *step - Steps(c, &at, head, StepsOptions{Label: msg(c, "onboarding_wizard.progress")}) + navigation.Steps(c, &at, head, navigation.StepsOptions{Label: core.Msg(c, "onboarding_wizard.progress")}) cur := steps[*step] ui.Column(c).Key(*step).Gap(d.Unit() * 3).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "onboarding_wizard.count"), *step+1, len(steps))).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) - ui.Text(c, cur.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).FontWeight(600) + ui.Text(c, fmt.Sprintf(core.Msg(c, "onboarding_wizard.count"), *step+1, len(steps))).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) + ui.Text(c, cur.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).FontWeight(600) if cur.Description != "" { ui.Text(c, cur.Description).TextColor(k.TextMuted) } @@ -70,18 +73,18 @@ func OnboardingWizard(c *ui.Context, step *int, steps []WizardStep, opts Onboard last := *step == len(steps)-1 ready := cur.Ready == nil || cur.Ready() ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { - if !opts.NoSkip && !last && Button(c, msg(c, "onboarding_wizard.skip"), ButtonOptions{Variant: Ghost}).Clicked() { + if !opts.NoSkip && !last && input.Button(c, core.Msg(c, "onboarding_wizard.skip"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { r.skipped = true } ui.Box(c).Grow(1) - if *step > 0 && Button(c, msg(c, "onboarding_wizard.back"), ButtonOptions{Variant: Secondary}).Clicked() { + if *step > 0 && input.Button(c, core.Msg(c, "onboarding_wizard.back"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { *step-- } - next := msg(c, "onboarding_wizard.next") + next := core.Msg(c, "onboarding_wizard.next") if last { - next = msg(c, "onboarding_wizard.finish") + next = core.Msg(c, "onboarding_wizard.finish") } - if Button(c, next, ButtonOptions{Disabled: !ready}).AutoFocus().Clicked() && ready { + if input.Button(c, next, input.ButtonOptions{Disabled: !ready}).AutoFocus().Clicked() && ready { if last { r.finished = true } else { diff --git a/onboarding_wizard_test.go b/account/onboarding_wizard_test.go similarity index 93% rename from onboarding_wizard_test.go rename to account/onboarding_wizard_test.go index 84ca0da..1c9f9a2 100644 --- a/onboarding_wizard_test.go +++ b/account/onboarding_wizard_test.go @@ -1,9 +1,11 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestOnboardingWizardStepsSkipFinish(t *testing.T) { @@ -15,7 +17,7 @@ func TestOnboardingWizardStepsSkipFinish(t *testing.T) { {Title: "Done"}, } tt := accountTester(600, 500, func(c *ui.Context) { - steps[1].Content = func() { TextInput(c, &name, TextInputOptions{Label: "Name"}) } + steps[1].Content = func() { input.TextInput(c, &name, input.TextInputOptions{Label: "Name"}) } r := OnboardingWizard(c, &step, steps, OnboardingWizardOptions{}) if r.Finished() { finished++ diff --git a/profile_editor.go b/account/profile_editor.go similarity index 63% rename from profile_editor.go rename to account/profile_editor.go index 52a0caf..d43a32f 100644 --- a/profile_editor.go +++ b/account/profile_editor.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "log/slog" @@ -7,8 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Profile is what a ProfileEditor edits. @@ -53,28 +56,28 @@ func ProfileEditor(c *ui.Context, p *Profile, opts ProfileEditorOptions) Profile if limit == 0 { limit = 160 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r ProfileEditorResult r.Element = ui.Column(c).Gap(d.Unit() * 4).Width(420).Disabled(opts.Disabled) locked := r.Element.IsDisabled() || opts.Saving dirty := *p != opts.Saved nameErr := "" if strings.TrimSpace(p.Name) == "" { - nameErr = msg(c, "form.required") + nameErr = core.Msg(c, "form.required") } r.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 4).AlignItems(ui.Center).Children(func() { - Avatar(c, p.Name, AvatarOptions{Image: opts.Avatar, Size: 64}) - if Button(c, msg(c, "profile_editor.avatar"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() && !locked { + display.Avatar(c, p.Name, display.AvatarOptions{Image: opts.Avatar, Size: 64}) + if input.Button(c, core.Msg(c, "profile_editor.avatar"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() && !locked { r.avatar = true } }) - FormField(c, msg(c, "profile_editor.name"), FormFieldOptions{Required: true}, func() *ui.Element { - return TextInput(c, &p.Name, TextInputOptions{Disabled: locked, Error: nameErr}).Input + input.FormField(c, core.Msg(c, "profile_editor.name"), input.FormFieldOptions{Required: true}, func() *ui.Element { + return input.TextInput(c, &p.Name, input.TextInputOptions{Disabled: locked, Error: nameErr}).Input }) - FormField(c, msg(c, "profile_editor.bio"), FormFieldOptions{}, func() *ui.Element { - return TextArea(c, &p.Bio, TextAreaOptions{MaxLength: limit, Disabled: locked}).Input + input.FormField(c, core.Msg(c, "profile_editor.bio"), input.FormFieldOptions{}, func() *ui.Element { + return input.TextArea(c, &p.Bio, input.TextAreaOptions{MaxLength: limit, Disabled: locked}).Input }) ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { if dirty { @@ -82,16 +85,16 @@ func ProfileEditor(c *ui.Context, p *Profile, opts ProfileEditorOptions) Profile ui.Box(c).Size(10, 10).Draw(func(pt *ui.Painter, rect ui.Rect) { pt.FillPath(internal.Diamond(internal.Snap(pt, rect.X+5), internal.Snap(pt, rect.Y+5), 10), k.Warning) }) - ui.Text(c, msg(c, "profile_editor.unsaved")).TextColor(k.Warning).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600) + ui.Text(c, core.Msg(c, "profile_editor.unsaved")).TextColor(k.Warning).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600) }) } else { ui.Box(c).Grow(1) } - if Button(c, msg(c, "profile_editor.discard"), ButtonOptions{Variant: Ghost, Disabled: locked || !dirty}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "profile_editor.discard"), input.ButtonOptions{Variant: input.Ghost, Disabled: locked || !dirty}).Clicked() && !locked { *p, r.reset = opts.Saved, true } valid := strings.TrimSpace(p.Name) != "" && utf8.RuneCountInString(p.Bio) <= limit - if Button(c, msg(c, "profile_editor.save"), ButtonOptions{Loading: opts.Saving, Disabled: r.Element.IsDisabled() || !dirty || !valid}).Clicked() && !locked && valid { + if input.Button(c, core.Msg(c, "profile_editor.save"), input.ButtonOptions{Loading: opts.Saving, Disabled: r.Element.IsDisabled() || !dirty || !valid}).Clicked() && !locked && valid { r.save = true } }) diff --git a/profile_editor_test.go b/account/profile_editor_test.go similarity index 99% rename from profile_editor_test.go rename to account/profile_editor_test.go index 5188900..60c598d 100644 --- a/profile_editor_test.go +++ b/account/profile_editor_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "testing" diff --git a/session_list.go b/account/session_list.go similarity index 71% rename from session_list.go rename to account/session_list.go index 0dbedbe..0fbf146 100644 --- a/session_list.go +++ b/account/session_list.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -8,7 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Session describes a device without carrying its authentication token. @@ -46,8 +50,8 @@ type sessionListState struct { // SessionList displays devices and requests sign-out through events. func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) SessionListResult { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r SessionListResult r.Element = ui.Column(c).Gap(d.Unit() * 3).MinWidth(420).Disabled(opts.Disabled) st := ui.Local(r.Element, "sessions", func() sessionListState { return sessionListState{} }) @@ -67,7 +71,7 @@ func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) Ses } others := st.others r.Element.Children(func() { - ui.Column(c).Role(ui.RoleList).Label(msg(c, "session_list.label")).Children(func() { + ui.Column(c).Role(ui.RoleList).Label(core.Msg(c, "session_list.label")).Children(func() { accountRows(c, len(sessions), func(i int) any { return sessions[i].ID }, func(i int) { s := sessions[i] ui.Row(c).Key(s.ID).MinHeight(d.ControlHeight()+d.Unit()*4).Gap(d.Unit()*3).AlignItems(ui.Center). @@ -76,14 +80,14 @@ func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) Ses ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, s.Device).FontWeight(600).SingleLine() if s.Current { - Badge(c, BadgeOptions{Text: msg(c, "session_list.current")}, nil) + display.Badge(c, display.BadgeOptions{Text: core.Msg(c, "session_list.current")}, nil) } }) - ui.Text(c, sessionListDetail(c, s)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, sessionListDetail(c, s)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) if !s.Current { - b := Button(c, msg(c, "session_list.sign_out"), ButtonOptions{Variant: Secondary, Disabled: disabled}). - Label(fmt.Sprintf(msg(c, "session_list.sign_out_of"), s.Device)) + b := input.Button(c, core.Msg(c, "session_list.sign_out"), input.ButtonOptions{Variant: input.Secondary, Disabled: disabled}). + Label(fmt.Sprintf(core.Msg(c, "session_list.sign_out_of"), s.Device)) if b.Clicked() && !disabled { r.revoke = s.ID } @@ -92,7 +96,7 @@ func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) Ses }) }) ui.Row(c).Children(func() { - if Button(c, msg(c, "session_list.sign_out_others"), ButtonOptions{Variant: Danger, Disabled: disabled || others == 0}).Clicked() && !disabled { + if input.Button(c, core.Msg(c, "session_list.sign_out_others"), input.ButtonOptions{Variant: input.Danger, Disabled: disabled || others == 0}).Clicked() && !disabled { st.confirm = true } }) @@ -100,8 +104,8 @@ func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) Ses if disabled || others == 0 { st.confirm = false } - res := AlertDialog(c, &st.confirm, AlertDialogOptions{Title: msg(c, "session_list.confirm_title"), - Description: fmt.Sprintf(msg(c, "session_list.confirm_text"), others), ConfirmLabel: msg(c, "session_list.sign_out_others")}) + res := overlay.AlertDialog(c, &st.confirm, overlay.AlertDialogOptions{Title: core.Msg(c, "session_list.confirm_title"), + Description: fmt.Sprintf(core.Msg(c, "session_list.confirm_text"), others), ConfirmLabel: core.Msg(c, "session_list.sign_out_others")}) r.others = res.Confirmed && !disabled if r.revoke != "" || r.others { slog.Debug("mujica: session_list sign out", "others", r.others) @@ -111,9 +115,9 @@ func SessionList(c *ui.Context, sessions []Session, opts SessionListOptions) Ses // sessionListDetail formats the location and last active time. func sessionListDetail(c *ui.Context, s Session) string { - when := msg(c, "session_list.now") + when := core.Msg(c, "session_list.now") if ago := c.Now().Sub(s.LastActive); s.LastActive.IsZero() { - when = msg(c, "session_list.unknown") + when = core.Msg(c, "session_list.unknown") } else if ago >= time.Minute { when = s.LastActive.Format("2006-01-02 15:04") } diff --git a/session_list_test.go b/account/session_list_test.go similarity index 99% rename from session_list_test.go rename to account/session_list_test.go index 569ab41..92114cd 100644 --- a/session_list_test.go +++ b/account/session_list_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/settings_layout.go b/account/settings_layout.go similarity index 79% rename from settings_layout.go rename to account/settings_layout.go index a525c60..958f5e4 100644 --- a/settings_layout.go +++ b/account/settings_layout.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SettingsPage is one page of a SettingsLayout's navigation. @@ -22,18 +24,18 @@ type SettingsLayoutOptions struct { } // SettingsLayout builds section navigation and independently scrolling content. -func SettingsLayout(c *ui.Context, page *string, pages []SettingsPage, opts SettingsLayoutOptions, content func(id string)) ChoiceResult { +func SettingsLayout(c *ui.Context, page *string, pages []SettingsPage, opts SettingsLayoutOptions, content func(id string)) core.ChoiceResult { if len(pages) == 0 || content == nil { panic("mujica: SettingsLayout needs pages and content") } found := false - items := make([]SidebarItem, len(pages)) + items := make([]navigation.SidebarItem, len(pages)) for i, p := range pages { if p.ID == "" || p.Label == "" { panic("mujica: SettingsLayout page needs an ID and a Label") } found = found || p.ID == *page - items[i] = SidebarItem{ID: p.ID, Label: p.Label, Icon: p.Icon} + items[i] = navigation.SidebarItem{ID: p.ID, Label: p.Label, Icon: p.Icon} } if *page == "" { *page, found = pages[0].ID, true @@ -43,16 +45,16 @@ func SettingsLayout(c *ui.Context, page *string, pages []SettingsPage, opts Sett } title := opts.Title if title == "" { - title = msg(c, "settings_layout.title") + title = core.Msg(c, "settings_layout.title") } - k := Tokens(c) - d := Density(c) - var r ChoiceResult + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult r.Element = ui.Row(c).Fill().AlignItems(ui.Stretch).Background(k.Background) r.Element.Children(func() { - nav := Sidebar(c, page, []SidebarSection{{Title: title, Items: items}}, SidebarOptions{Width: 220}) + nav := navigation.Sidebar(c, page, []navigation.SidebarSection{{Title: title, Items: items}}, navigation.SidebarOptions{Width: 220}) if nav.Changed() { - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: settings_layout page", "page", *page) } ui.Scroll(c).Grow(1).MinWidth(0).Padding(d.Unit()*6, d.Unit()*8).Children(func() { @@ -64,11 +66,11 @@ func SettingsLayout(c *ui.Context, page *string, pages []SettingsPage, opts Sett // SettingsSection groups rows under a serif heading. func SettingsSection(c *ui.Context, title string, rows func()) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) e := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(title) e.Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Role(ui.RoleNone) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Role(ui.RoleNone) ui.Column(c).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Padding(0, d.Unit()*4). Dividers(theme.BorderWidth, k.Border).Children(rows) }) @@ -96,8 +98,8 @@ func SettingsRow(c *ui.Context, label string, opts SettingsRowOptions, control f if label == "" || control == nil { panic("mujica: SettingsRow needs a label and a control") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) row := ui.Row(c).MinHeight(d.ControlHeight()+d.Unit()*4).Gap(d.Unit()*4).AlignItems(ui.Center).Padding(d.Unit()*3, 0) if opts.ID != "" { row.Key(opts.ID) @@ -110,7 +112,7 @@ func SettingsRow(c *ui.Context, label string, opts SettingsRowOptions, control f text.Children(func() { ui.Text(c, label).FontWeight(500).Role(ui.RoleNone) if opts.Description != "" { - ui.Text(c, opts.Description).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) + ui.Text(c, opts.Description).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) } }) ui.Row(c).Shrink(0).Children(func() { ctrl = control() }) @@ -129,7 +131,7 @@ func SettingsRow(c *ui.Context, label string, opts SettingsRowOptions, control f slog.Debug("mujica: settings_layout reveal", "row", opts.ID) } // A revealed row's face fades from the selection color. - if dur := Motion(c, theme.StateDuration); !st.at.IsZero() && dur > 0 { + if dur := core.Motion(c, theme.StateDuration); !st.at.IsZero() && dur > 0 { if t := c.Now().Sub(st.at); t < dur { row.Background(k.Selection.Alpha(1 - float32(t)/float32(dur))) c.AnimationFrame() diff --git a/settings_layout_test.go b/account/settings_layout_test.go similarity index 92% rename from settings_layout_test.go rename to account/settings_layout_test.go index 44699ef..769ac61 100644 --- a/settings_layout_test.go +++ b/account/settings_layout_test.go @@ -1,15 +1,18 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // accountTester runs a themed view inside a padded column. func accountTester(w, h int, view func(c *ui.Context)) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Fill().Children(func() { view(c) }) }, w, h) } @@ -22,13 +25,13 @@ func settingsView(c *ui.Context, page *string, sound, track *bool, changes *int) case "general": SettingsSection(c, "Notifications", func() { SettingsRow(c, "Sound", SettingsRowOptions{ID: "sound", Description: "Chime on new messages."}, func() *ui.Element { - return Switch(c, sound, "", SwitchOptions{}) + return input.Switch(c, sound, "", input.SwitchOptions{}) }) }) case "privacy": SettingsSection(c, "Data", func() { SettingsRow(c, "Usage reports", SettingsRowOptions{ID: "track"}, func() *ui.Element { - return Switch(c, track, "", SwitchOptions{}) + return input.Switch(c, track, "", input.SwitchOptions{}) }) }) } diff --git a/settings_search.go b/account/settings_search.go similarity index 90% rename from settings_search.go rename to account/settings_search.go index 5e0cf97..0209c2b 100644 --- a/settings_search.go +++ b/account/settings_search.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "log/slog" @@ -9,8 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SettingEntry identifies a searchable setting and its destination. @@ -60,12 +62,12 @@ func SettingsSearch(c *ui.Context, query *string, entries []SettingEntry, opts S if limit == 0 { limit = 8 } - d := Density(c) + d := core.Density(c) var r SettingsSearchResult r.Element = ui.Column(c).Gap(d.Unit() * 2).MinWidth(280) st := ui.Local(r.Element, "search", func() settingsSearchState { return settingsSearchState{} }) r.Element.Children(func() { - res := SearchInput(c, query, SearchInputOptions{Placeholder: msg(c, "settings_search.placeholder"), Label: msg(c, "settings_search.placeholder")}) + res := input.SearchInput(c, query, input.SearchInputOptions{Placeholder: core.Msg(c, "settings_search.placeholder"), Label: core.Msg(c, "settings_search.placeholder")}) in := res.Input q := strings.ToLower(strings.TrimSpace(*query)) settingsSearchPlan(st, entries, q, limit) @@ -87,7 +89,7 @@ func SettingsSearch(c *ui.Context, query *string, entries []SettingEntry, opts S r.chosen = &entries[hits[st.cursor]] } if q != "" && len(hits) == 0 { - ui.Text(c, msg(c, "settings_search.none")).TextColor(Tokens(c).TextMuted).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "settings_search.none")).TextColor(core.Tokens(c).TextMuted).Role(ui.RoleStatus) } list := accountRows(c, len(hits), func(n int) any { return entries[hits[n]].ID }, func(n int) { i := hits[n] @@ -131,8 +133,8 @@ func settingsSearchPlan(st *settingsSearchState, entries []SettingEntry, q strin // settingsSearchHit builds a marked, clickable search result. func settingsSearchHit(c *ui.Context, e SettingEntry, q string, current bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).Key(e.ID).MinHeight(d.ControlHeight()).Padding(d.Unit(), d.Unit()*3).Justify(ui.Start). Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(e.Label).Description(e.Description).Selected(current).Gap(d.Unit() * 2) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -160,8 +162,8 @@ func settingsSearchHit(c *ui.Context, e SettingEntry, q string, current bool) bo // settingsSearchSpans highlights matches while preserving original Unicode text. func settingsSearchSpans(c *ui.Context, s, q string, size float32) []ui.Span { - k := Tokens(c) - size = FontSize(c, size) + k := core.Tokens(c) + size = core.FontSize(c, size) plain := func(t string) ui.Span { return ui.Span{Text: t, Size: size, Color: k.Text} } if q == "" { return []ui.Span{plain(s)} diff --git a/settings_search_test.go b/account/settings_search_test.go similarity index 97% rename from settings_search_test.go rename to account/settings_search_test.go index 81ef93b..5c08623 100644 --- a/settings_search_test.go +++ b/account/settings_search_test.go @@ -1,9 +1,11 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var settingEntries = []SettingEntry{ @@ -74,7 +76,7 @@ func TestSettingsSearchHighlightsUnicodeCaseMapping(t *testing.T) { func TestSettingsSearchPlainSpansHaveReadableColor(t *testing.T) { accountTester(400, 220, func(c *ui.Context) { for _, span := range settingsSearchSpans(c, "Usage reports", "us", 14) { - if span.Color != Tokens(c).Text { + if span.Color != core.Tokens(c).Text { t.Error("search text does not use the readable Text token") } } diff --git a/shortcut_recorder.go b/account/shortcut_recorder.go similarity index 85% rename from shortcut_recorder.go rename to account/shortcut_recorder.go index 65c4ad2..33c09a5 100644 --- a/shortcut_recorder.go +++ b/account/shortcut_recorder.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Shortcut combines modifiers and a key; zero means unassigned. @@ -81,10 +83,10 @@ var shortcutNames = map[ui.Key]string{ // shortcutKbd displays keycaps or a muted unassigned mark. func shortcutKbd(c *ui.Context, s Shortcut) { if s.IsZero() { - ui.Text(c, "—").TextColor(Tokens(c).TextMuted) + ui.Text(c, "—").TextColor(core.Tokens(c).TextMuted) return } - Kbd(c, s.keys()...) + display.Kbd(c, s.keys()...) } // ShortcutBinding names an action that already owns a shortcut. @@ -109,12 +111,12 @@ type shortcutRecorderState struct { } // ShortcutRecorder records key combinations; Escape cancels. -func ShortcutRecorder(c *ui.Context, value *Shortcut, opts ShortcutRecorderOptions) ChoiceResult { +func ShortcutRecorder(c *ui.Context, value *Shortcut, opts ShortcutRecorderOptions) core.ChoiceResult { if opts.Label == "" { panic("mujica: ShortcutRecorder needs a Label") } - d := Density(c) - var r ChoiceResult + d := core.Density(c) + var r core.ChoiceResult r.Element = ui.Column(c).Gap(d.Unit()).AlignItems(ui.Start).Disabled(opts.Disabled) r.Element.Children(func() { b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).MinWidth(160).Padding(0, d.Unit()*3).Gap(d.Unit() * 2). @@ -129,11 +131,11 @@ func ShortcutRecorder(c *ui.Context, value *Shortcut, opts ShortcutRecorderOptio st.caught, st.recording, st.conflict = nil, false, "" for _, t := range opts.Taken { if t.Shortcut == s && s != *value { - st.conflict = fmt.Sprintf(msg(c, "shortcut_recorder.conflict"), s.String(), t.Action) + st.conflict = fmt.Sprintf(core.Msg(c, "shortcut_recorder.conflict"), s.String(), t.Action) } } if st.conflict == "" && s != *value { - *value, r.changed = s, true + *value, r = s, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: shortcut_recorder set", "shortcut", s.String()) } } @@ -177,11 +179,11 @@ func shortcutRecorderKey(st *shortcutRecorderState, ev ui.InputEvent) bool { // shortcutRecorderLook displays the shortcut or recording prompt. func shortcutRecorderLook(c *ui.Context, b *ui.Element, st *shortcutRecorderState, value Shortcut, name string) { - k := Tokens(c) + k := core.Tokens(c) internal.Ring(b, k.Focus, theme.ControlRadius) label := name + ": " + value.String() if value.IsZero() { - label = name + ": " + msg(c, "shortcut_recorder.none") + label = name + ": " + core.Msg(c, "shortcut_recorder.none") } b.Label(label) bg, border := k.Surface, k.ControlBorder @@ -193,7 +195,7 @@ func shortcutRecorderLook(c *ui.Context, b *ui.Element, st *shortcutRecorderStat b.Background(bg).Border(theme.BorderWidth, border) b.Children(func() { if st.recording { - ui.Text(c, msg(c, "shortcut_recorder.press")).TextColor(k.AccentText).SingleLine() + ui.Text(c, core.Msg(c, "shortcut_recorder.press")).TextColor(k.AccentText).SingleLine() } else { shortcutKbd(c, value) } @@ -205,13 +207,13 @@ func shortcutRecorderLook(c *ui.Context, b *ui.Element, st *shortcutRecorderStat func shortcutRecorderNote(c *ui.Context, st *shortcutRecorderState) { text := st.conflict if st.needMod && st.recording { - text = msg(c, "shortcut_recorder.needs_modifier") + text = core.Msg(c, "shortcut_recorder.needs_modifier") } if text == "" { return } - ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(Tokens(c).Warning).Role(ui.RoleStatus).Children(func() { + ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(core.Tokens(c).Warning).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14) - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)) }) } diff --git a/shortcut_recorder_test.go b/account/shortcut_recorder_test.go similarity index 96% rename from shortcut_recorder_test.go rename to account/shortcut_recorder_test.go index 1d99aad..f7efbdd 100644 --- a/shortcut_recorder_test.go +++ b/account/shortcut_recorder_test.go @@ -1,10 +1,12 @@ -package mujica +package account import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func shortcutRecorderTester(v *Shortcut, opts *ShortcutRecorderOptions, n *int) *ui.Tester { @@ -81,7 +83,7 @@ func TestShortcutRecorderFunctionKeyAndTabCancellation(t *testing.T) { if ShortcutRecorder(c, &v, opts).Changed() { n++ } - Button(c, "Next control", ButtonOptions{}) + input.Button(c, "Next control", input.ButtonOptions{}) }) if err := tt.Click("Refresh: none"); err != nil { t.Fatal(err) diff --git a/theme_selector.go b/account/theme_selector.go similarity index 76% rename from theme_selector.go rename to account/theme_selector.go index 62d537d..5c68f82 100644 --- a/theme_selector.go +++ b/account/theme_selector.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ThemeSelectorOptions configure a ThemeSelector. @@ -17,14 +18,14 @@ type ThemeSelectorOptions struct { } // ThemeSelector selects an appearance using preview cards. -func ThemeSelector(c *ui.Context, mode *Mode, opts ThemeSelectorOptions) ChoiceResult { - if *mode != System && *mode != Light && *mode != Dark { +func ThemeSelector(c *ui.Context, mode *core.Mode, opts ThemeSelectorOptions) core.ChoiceResult { + if *mode != core.System && *mode != core.Light && *mode != core.Dark { panic(fmt.Sprintf("mujica: ThemeSelector mode %d is not System, Light or Dark", *mode)) } - k := Tokens(c) - d := Density(c) - var r ChoiceResult - r.Element = ui.Row(c).Gap(d.Unit() * 3).Wrap().Role(ui.RoleRadioGroup).Label(msg(c, "theme_selector.label")). + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult + r.Element = ui.Row(c).Gap(d.Unit() * 3).Wrap().Role(ui.RoleRadioGroup).Label(core.Msg(c, "theme_selector.label")). FocusGroup(ui.Horizontal).Disabled(opts.Disabled) selected := mode if r.Element.IsDisabled() { @@ -32,10 +33,10 @@ func ThemeSelector(c *ui.Context, mode *Mode, opts ThemeSelectorOptions) ChoiceR selected = © } r.Element.Children(func() { - for _, m := range []Mode{System, Light, Dark} { - card := ui.RadioBase(c, selected, m).Column().Gap(d.Unit() * 2).Padding(d.Unit() * 2).Radius(theme.CardRadius).AlignItems(ui.Stretch).Transition(colorFade) + for _, m := range []core.Mode{core.System, core.Light, core.Dark} { + card := ui.RadioBase(c, selected, m).Column().Gap(d.Unit() * 2).Padding(d.Unit() * 2).Radius(theme.CardRadius).AlignItems(ui.Stretch).Transition(core.ColorFade) if card.Changed() && !card.IsDisabled() { - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: theme_selector chose", "mode", m.String()) } on := *mode == m @@ -52,9 +53,9 @@ func ThemeSelector(c *ui.Context, mode *Mode, opts ThemeSelectorOptions) ChoiceR card.Children(func() { ui.Box(c).Size(132, 84).Role(ui.RoleNone).Draw(func(p *ui.Painter, rect ui.Rect) { switch m { - case Light: + case core.Light: themePreview(p, rect, theme.Light()) - case Dark: + case core.Dark: themePreview(p, rect, theme.Dark()) default: half := ui.Rect{X: rect.X, Y: rect.Y, W: rect.W / 2, H: rect.H} @@ -66,7 +67,7 @@ func ThemeSelector(c *ui.Context, mode *Mode, opts ThemeSelectorOptions) ChoiceR if on { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) } - ui.Text(c, msg(c, "theme_selector."+m.String())).SingleLine() + ui.Text(c, core.Msg(c, "theme_selector."+m.String())).SingleLine() }) }) } diff --git a/theme_selector_test.go b/account/theme_selector_test.go similarity index 77% rename from theme_selector_test.go rename to account/theme_selector_test.go index 5906fad..d7fb2bb 100644 --- a/theme_selector_test.go +++ b/account/theme_selector_test.go @@ -1,13 +1,15 @@ -package mujica +package account import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestThemeSelectorPicksByPointerAndKeys(t *testing.T) { - mode, n := System, 0 + mode, n := core.System, 0 disabled := false tt := accountTester(600, 220, func(c *ui.Context) { if ThemeSelector(c, &mode, ThemeSelectorOptions{Disabled: disabled}).Changed() { @@ -17,18 +19,18 @@ func TestThemeSelectorPicksByPointerAndKeys(t *testing.T) { if err := tt.Click("Dark"); err != nil { t.Fatal(err) } - if mode != Dark || n != 1 { + if mode != core.Dark || n != 1 { t.Fatalf("click: %v %d", mode, n) } tt.Key(0, ui.KeyLeft) tt.Key(0, ui.KeySpace) - if mode != Light || n != 2 { + if mode != core.Light || n != 2 { t.Fatalf("keys: %v %d", mode, n) } disabled = true tt.Frame() _ = tt.Click("System") - if mode != Light { + if mode != core.Light { t.Fatal("disabled selector changed") } defer func() { @@ -36,13 +38,13 @@ func TestThemeSelectorPicksByPointerAndKeys(t *testing.T) { t.Fatal("unknown mode accepted") } }() - bad := Mode(9) + bad := core.Mode(9) accountTester(100, 100, func(c *ui.Context) { ThemeSelector(c, &bad, ThemeSelectorOptions{}) }) } func TestThemeSelectorSnapshot(t *testing.T) { snapshot(t, "theme_selector", 520, 170, func(c *ui.Context) { - m := ui.Local(c.Root(), "m", func() Mode { return Dark }) + m := ui.Local(c.Root(), "m", func() core.Mode { return core.Dark }) ui.Column(c).Padding(16).Children(func() { ThemeSelector(c, m, ThemeSelectorOptions{}) }) }) } diff --git a/two_factor_input.go b/account/two_factor_input.go similarity index 71% rename from two_factor_input.go rename to account/two_factor_input.go index 3e3282d..9fa8899 100644 --- a/two_factor_input.go +++ b/account/two_factor_input.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TwoFactorInputOptions configure a TwoFactorInput. @@ -38,30 +40,30 @@ func TwoFactorInput(c *ui.Context, code *string, opts TwoFactorInputOptions) Two if code == nil || len(*code) > 6 || strings.Trim(*code, "0123456789") != "" { panic("mujica: TwoFactorInput needs up to six digits") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r TwoFactorResult r.Element = ui.Column(c).Gap(d.Unit() * 3).Disabled(opts.Disabled) disabled := r.Element.IsDisabled() r.Element.Children(func() { - pin := PinInput(c, code, PinInputOptions{Length: 6, Label: msg(c, "two_factor_input.label"), Disabled: disabled, Invalid: opts.Error != ""}) + pin := input.PinInput(c, code, input.PinInputOptions{Length: 6, Label: core.Msg(c, "two_factor_input.label"), Disabled: disabled, Invalid: opts.Error != ""}) r.completed = pin.Completed() && !disabled if opts.Error != "" { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(k.Danger).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(14, 14) - ui.Text(c, opts.Error).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, opts.Error).FontSize(core.FontSize(c, theme.CaptionSize)) }) } wait := opts.ResendAt.Sub(c.Now()) - label := msg(c, "two_factor_input.resend") + label := core.Msg(c, "two_factor_input.resend") if wait > 0 { secs := int((wait + time.Second - 1) / time.Second) - label = fmt.Sprintf(msg(c, "two_factor_input.resend_in"), secs/60, secs%60) + label = fmt.Sprintf(core.Msg(c, "two_factor_input.resend_in"), secs/60, secs%60) // Wake when the shown second changes. c.After(wait - time.Duration(secs-1)*time.Second) } ui.Row(c).Children(func() { - b := Button(c, label, ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw"), Disabled: disabled || wait > 0}) + b := input.Button(c, label, input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("refresh-cw"), Disabled: disabled || wait > 0}) if b.Clicked() && !disabled && wait <= 0 { r.resend = true slog.Debug("mujica: two_factor_input resend") diff --git a/two_factor_input_test.go b/account/two_factor_input_test.go similarity index 99% rename from two_factor_input_test.go rename to account/two_factor_input_test.go index 5bf5aeb..d2f9dd3 100644 --- a/two_factor_input_test.go +++ b/account/two_factor_input_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/usage_quota.go b/account/usage_quota.go similarity index 65% rename from usage_quota.go rename to account/usage_quota.go index 64d33ae..5001314 100644 --- a/usage_quota.go +++ b/account/usage_quota.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QuotaItem is one metered resource: Used of Limit, in Unit. @@ -35,8 +37,8 @@ func UsageQuota(c *ui.Context, items []QuotaItem, opts UsageQuotaOptions) *ui.El if warn < 0 || warn > 1 || math.IsNaN(warn) { panic("mujica: UsageQuota Warn must be between 0 and 1") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) e := ui.Column(c).Gap(d.Unit() * 4).MinWidth(320) e.Children(func() { for i, it := range items { @@ -47,22 +49,22 @@ func UsageQuota(c *ui.Context, items []QuotaItem, opts UsageQuotaOptions) *ui.El col, icon, note := k.AccentText, "", "" switch { case share >= 1: - col, icon, note = k.Danger, "circle-x", msg(c, "usage_quota.over") + col, icon, note = k.Danger, "circle-x", core.Msg(c, "usage_quota.over") case share >= warn: - col, icon, note = k.Warning, "triangle-alert", msg(c, "usage_quota.near") + col, icon, note = k.Warning, "triangle-alert", core.Msg(c, "usage_quota.near") } - amount := fmt.Sprintf(msg(c, "usage_quota.amount"), statisticFormat(it.Used, 0), statisticFormat(it.Limit, 0), it.Unit) + amount := fmt.Sprintf(core.Msg(c, "usage_quota.amount"), data.StatisticFormat(it.Used, 0), data.StatisticFormat(it.Limit, 0), it.Unit) ui.Column(c).Key(i).Gap(d.Unit()).Role(ui.RoleMeter).Label(it.Label). - Description(fmt.Sprintf("%s, %s%% %s", amount, statisticFormat(share*100, 0), note)).Children(func() { + Description(fmt.Sprintf("%s, %s%% %s", amount, data.StatisticFormat(share*100, 0), note)).Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, it.Label).FontWeight(500).Grow(1) if icon != "" { ui.Row(c).Gap(d.Unit() / 2).AlignItems(ui.Center).TextColor(col).Children(func() { ui.Icon(c, icons.Must(icon)).Size(12, 12) - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600) + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600) }) } - ui.Text(c, amount).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + ui.Text(c, amount).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") }) ui.Box(c).Height(6).FillWidth().Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.SurfaceHover, theme.ControlRadius) @@ -73,7 +75,7 @@ func UsageQuota(c *ui.Context, items []QuotaItem, opts UsageQuotaOptions) *ui.El if !opts.ResetAt.IsZero() { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(k.TextMuted).Children(func() { ui.Icon(c, icons.Must("clock")).Size(12, 12) - ui.Text(c, usageQuotaReset(c, opts.ResetAt)).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, usageQuotaReset(c, opts.ResetAt)).FontSize(core.FontSize(c, theme.CaptionSize)) }) } }) @@ -85,9 +87,9 @@ func usageQuotaReset(c *ui.Context, at time.Time) string { left := at.Sub(c.Now()) switch { case left <= 0: - return msg(c, "usage_quota.reset_due") + return core.Msg(c, "usage_quota.reset_due") case left < 24*time.Hour: - return fmt.Sprintf(msg(c, "usage_quota.reset_hours"), int(math.Ceil(left.Hours()))) + return fmt.Sprintf(core.Msg(c, "usage_quota.reset_hours"), int(math.Ceil(left.Hours()))) } - return fmt.Sprintf(msg(c, "usage_quota.reset_on"), at.Format("2006-01-02")) + return fmt.Sprintf(core.Msg(c, "usage_quota.reset_on"), at.Format("2006-01-02")) } diff --git a/usage_quota_test.go b/account/usage_quota_test.go similarity index 99% rename from usage_quota_test.go rename to account/usage_quota_test.go index 503d2ff..717a7d3 100644 --- a/usage_quota_test.go +++ b/account/usage_quota_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/user_menu.go b/account/user_menu.go similarity index 75% rename from user_menu.go rename to account/user_menu.go index f0d363c..00e13fa 100644 --- a/user_menu.go +++ b/account/user_menu.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // UserStatus is the presence a user shows to others. @@ -50,11 +53,11 @@ func UserMenu(c *ui.Context, status *UserStatus, opts UserMenuOptions) UserMenuR if *status < UserStatusOnline || *status > UserStatusInvisible { panic(fmt.Sprintf("mujica: UserMenu status %d is unknown", *status)) } - k := Tokens(c) + k := core.Tokens(c) var r UserMenuResult - word := msg(c, fmt.Sprintf("user_menu.status%d", *status)) + word := core.Msg(c, fmt.Sprintf("user_menu.status%d", *status)) b := ui.ButtonBase(c).Role(ui.RoleMenuButton).Size(36, 36).Radius(18).Disabled(opts.Disabled). - Label(fmt.Sprintf(msg(c, "user_menu.label"), opts.Name, word)) + Label(fmt.Sprintf(core.Msg(c, "user_menu.label"), opts.Name, word)) r.Element = b internal.Ring(b, k.Focus, 18) open := ui.Local(b, "open", func() bool { return false }) @@ -65,9 +68,9 @@ func UserMenu(c *ui.Context, status *UserStatus, opts UserMenuOptions) UserMenuR *open = true } st := *status - b.Children(func() { Avatar(c, opts.Name, AvatarOptions{Image: opts.Avatar, Size: 32}) }) + b.Children(func() { display.Avatar(c, opts.Name, display.AvatarOptions{Image: opts.Avatar, Size: 32}) }) b.DrawOver(func(p *ui.Painter, rect ui.Rect) { userMenuDot(p, k, rect.X+rect.W-6, rect.Y+rect.H-6, st) }) - popupMenuOpen(c, b, open, func(m *PopupMenu) { + overlay.PopupMenuOpen(c, b, open, func(m *overlay.PopupMenu) { head := opts.Name if opts.Email != "" { head += " · " + opts.Email @@ -75,15 +78,15 @@ func UserMenu(c *ui.Context, status *UserStatus, opts UserMenuOptions) UserMenuR m.Group(head, func() { for s := UserStatusOnline; s <= UserStatusInvisible; s++ { on := s == *status - if m.Check(msg(c, fmt.Sprintf("user_menu.status%d", s)), &on, PopupMenuItemOptions{}) && s != *status && !disabled { + if m.Check(core.Msg(c, fmt.Sprintf("user_menu.status%d", s)), &on, overlay.PopupMenuItemOptions{}) && s != *status && !disabled { *status, r.changed = s, true slog.Debug("mujica: user_menu status", "status", s) } } }) m.Separator() - r.settings = m.Item(msg(c, "user_menu.settings"), PopupMenuItemOptions{}) && !disabled - r.leave = m.Item(msg(c, "user_menu.sign_out"), PopupMenuItemOptions{}) && !disabled + r.settings = m.Item(core.Msg(c, "user_menu.settings"), overlay.PopupMenuItemOptions{}) && !disabled + r.leave = m.Item(core.Msg(c, "user_menu.sign_out"), overlay.PopupMenuItemOptions{}) && !disabled }) if r.settings || r.leave { slog.Debug("mujica: user_menu chose", "settings", r.settings, "sign_out", r.leave) diff --git a/user_menu_test.go b/account/user_menu_test.go similarity index 99% rename from user_menu_test.go rename to account/user_menu_test.go index 11ccd25..3cdce90 100644 --- a/user_menu_test.go +++ b/account/user_menu_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "testing" diff --git a/whats_new_dialog.go b/account/whats_new_dialog.go similarity index 58% rename from whats_new_dialog.go rename to account/whats_new_dialog.go index e83e96d..b0022aa 100644 --- a/whats_new_dialog.go +++ b/account/whats_new_dialog.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -6,7 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChangeKind tags a release note. @@ -37,7 +41,7 @@ type WhatsNewDialogOptions struct { } // WhatsNewDialog displays release notes and a persistent hide choice. -func WhatsNewDialog(c *ui.Context, open *bool, hide *bool, releases []Release, opts WhatsNewDialogOptions) ChoiceResult { +func WhatsNewDialog(c *ui.Context, open *bool, hide *bool, releases []Release, opts WhatsNewDialogOptions) core.ChoiceResult { if len(releases) == 0 { panic("mujica: WhatsNewDialog needs releases") } @@ -54,19 +58,19 @@ func WhatsNewDialog(c *ui.Context, open *bool, hide *bool, releases []Release, o if ui.Box(c).IsDisabled() { *open = false } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) title := opts.Title if title == "" { - title = msg(c, "whats_new_dialog.title") + title = core.Msg(c, "whats_new_dialog.title") } - var r ChoiceResult + var r core.ChoiceResult was := *open before := *hide - r.Element = Dialog(c, open, DialogOptions{Title: title, Width: 480, Decorated: true, Actions: func() { - Checkbox(c, hide, msg(c, "whats_new_dialog.hide"), CheckboxOptions{}) + r.Element = overlay.Dialog(c, open, overlay.DialogOptions{Title: title, Width: 480, Decorated: true, Actions: func() { + input.Checkbox(c, hide, core.Msg(c, "whats_new_dialog.hide"), input.CheckboxOptions{}) ui.Box(c).Grow(1) - if Button(c, msg(c, "whats_new_dialog.close"), ButtonOptions{}).AutoFocus().Clicked() { + if input.Button(c, core.Msg(c, "whats_new_dialog.close"), input.ButtonOptions{}).AutoFocus().Clicked() { *open = false } }}, func() { @@ -76,22 +80,22 @@ func WhatsNewDialog(c *ui.Context, open *bool, hide *bool, releases []Release, o } ui.Column(c).Key(i).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(rel.Version).Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.End).Children(func() { - ui.Text(c, rel.Version).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600) + ui.Text(c, rel.Version).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600) if rel.Date != "" { - ui.Text(c, rel.Date).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, rel.Date).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) } }) for _, ch := range rel.Changes { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Start).Children(func() { - tone := map[ChangeKind]BadgeTone{ChangeNew: BadgeAccent, ChangeImproved: BadgeInfo, ChangeFixed: BadgeSuccess}[ch.Kind] - Badge(c, BadgeOptions{Text: msg(c, fmt.Sprintf("whats_new_dialog.kind%d", ch.Kind)), Tone: tone}, nil).Shrink(0) + tone := map[ChangeKind]display.BadgeTone{ChangeNew: display.BadgeAccent, ChangeImproved: display.BadgeInfo, ChangeFixed: display.BadgeSuccess}[ch.Kind] + display.Badge(c, display.BadgeOptions{Text: core.Msg(c, fmt.Sprintf("whats_new_dialog.kind%d", ch.Kind)), Tone: tone}, nil).Shrink(0) ui.Text(c, ch.Text).Grow(1).MinWidth(0) }) } }) } }) - r.changed = *hide != before + r = core.NewChoiceResult(r.Element, *hide != before) if was && !*open { slog.Debug("mujica: whats_new_dialog close", "hide", *hide) } diff --git a/whats_new_dialog_test.go b/account/whats_new_dialog_test.go similarity index 99% rename from whats_new_dialog_test.go rename to account/whats_new_dialog_test.go index dc716a6..8ab1be3 100644 --- a/whats_new_dialog_test.go +++ b/account/whats_new_dialog_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "testing" diff --git a/workspace_switcher.go b/account/workspace_switcher.go similarity index 77% rename from workspace_switcher.go rename to account/workspace_switcher.go index 6fcd0e2..4478cb5 100644 --- a/workspace_switcher.go +++ b/account/workspace_switcher.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Workspace is a place a user can switch to. @@ -36,7 +39,7 @@ type workspaceSwitcherState struct { } // WorkspaceSwitcher filters workspaces and changes the current selection. -func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts WorkspaceSwitcherOptions) ChoiceResult { +func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts WorkspaceSwitcherOptions) core.ChoiceResult { var cur *Workspace for i := range spaces { if spaces[i].ID == "" || spaces[i].Name == "" { @@ -49,7 +52,7 @@ func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts if cur == nil { panic(fmt.Sprintf("mujica: WorkspaceSwitcher current %q is not a workspace", *current)) } - var r ChoiceResult + var r core.ChoiceResult b := accountTrigger(c, cur.Name, cur.Detail, nil, opts.Disabled) r.Element = b st := ui.Local(b, "ws", func() workspaceSwitcherState { return workspaceSwitcherState{} }) @@ -61,13 +64,13 @@ func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts } choose := func(i int) { if !disabled && spaces[i].ID != *current { - *current, r.changed = spaces[i].ID, true + *current, r = spaces[i].ID, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: workspace_switcher switched") } st.open = false } - Popover(c, b, &st.open, PopoverOptions{Width: 300, Label: msg(c, "workspace_switcher.label")}, func() { - res := SearchInput(c, &st.query, SearchInputOptions{Placeholder: msg(c, "workspace_switcher.search"), Label: msg(c, "workspace_switcher.search")}) + overlay.Popover(c, b, &st.open, overlay.PopoverOptions{Width: 300, Label: core.Msg(c, "workspace_switcher.label")}, func() { + res := input.SearchInput(c, &st.query, input.SearchInputOptions{Placeholder: core.Msg(c, "workspace_switcher.search"), Label: core.Msg(c, "workspace_switcher.search")}) in := res.Input in.AutoFocus() q := strings.ToLower(strings.TrimSpace(st.query)) @@ -94,7 +97,7 @@ func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts choose(hits[st.cursor]) } if len(hits) == 0 { - ui.Text(c, msg(c, "workspace_switcher.none")).TextColor(Tokens(c).TextMuted).Padding(Density(c).Unit() * 2).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "workspace_switcher.none")).TextColor(core.Tokens(c).TextMuted).Padding(core.Density(c).Unit() * 2).Role(ui.RoleStatus) } list := accountRows(c, len(hits), func(n int) any { return spaces[hits[n]].ID }, func(n int) { i := hits[n] @@ -113,8 +116,8 @@ func WorkspaceSwitcher(c *ui.Context, current *string, spaces []Workspace, opts // workspaceRow displays a clickable workspace with a selection mark. func workspaceRow(c *ui.Context, w Workspace, current, cursor bool) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).Key(w.ID).MinHeight(d.ControlHeight()).Padding(d.Unit(), d.Unit()*2).Gap(d.Unit() * 2). Justify(ui.Start).Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(w.Name).Selected(current) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -133,7 +136,7 @@ func workspaceRow(c *ui.Context, w Workspace, current, cursor bool) *ui.Element ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, w.Name).SingleLine().Ellipsis("…") if w.Detail != "" { - ui.Text(c, w.Detail).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, w.Detail).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() } }) }) diff --git a/workspace_switcher_test.go b/account/workspace_switcher_test.go similarity index 99% rename from workspace_switcher_test.go rename to account/workspace_switcher_test.go index aa8e8b6..709de94 100644 --- a/workspace_switcher_test.go +++ b/account/workspace_switcher_test.go @@ -1,4 +1,4 @@ -package mujica +package account import ( "fmt" diff --git a/agent_diff.go b/agent/agent_diff.go similarity index 75% rename from agent_diff.go rename to agent/agent_diff.go index c686101..5cf13c8 100644 --- a/agent_diff.go +++ b/agent/agent_diff.go @@ -1,13 +1,14 @@ -package mujica +package agent import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileChange is a proposed edit; empty Old adds a file. @@ -59,9 +60,9 @@ func agentDiffOf(holder *ui.Element, ch FileChange) *agentDiffCache { // agentDiffStats shows signed added and removed counts. func agentDiffStats(c *ui.Context, add, del int) { - k := Tokens(c) - size := FontSize(c, theme.CaptionSize+1) - ui.Row(c).Gap(Density(c).Unit() * 1.5).Shrink(0).Label(fmt.Sprintf(msg(c, "file_change_card.stats"), add, del)).Children(func() { + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize+1) + ui.Row(c).Gap(core.Density(c).Unit() * 1.5).Shrink(0).Label(fmt.Sprintf(core.Msg(c, "file_change_card.stats"), add, del)).Children(func() { ui.Text(c, fmt.Sprintf("+%d", add)).TextColor(k.Success).FontSize(size).Font(theme.MonoFont) ui.Text(c, fmt.Sprintf("−%d", del)).TextColor(k.Danger).FontSize(size).Font(theme.MonoFont) }) @@ -69,8 +70,8 @@ func agentDiffStats(c *ui.Context, add, del int) { // agentDiffRow draws one numbered, signed, tinted diff line. func agentDiffRow(c *ui.Context, l internal.DiffLine) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() sign, bg, fg := " ", ui.Transparent, k.TextMuted switch l.Op { case internal.DiffInsert: @@ -98,26 +99,26 @@ func agentDiffRow(c *ui.Context, l internal.DiffLine) { // agentDiffView shows lines[from:to] in a capped well, virtualized when long. func agentDiffView(c *ui.Context, lines []internal.DiffLine, maxHeight float32) *ui.Element { - w := agentWellRows(c, len(lines), func(i int) { agentDiffRow(c, lines[i]) }, maxHeight) - return w.Padding(Density(c).Unit(), 0) + w := AgentWellRows(c, len(lines), func(i int) { agentDiffRow(c, lines[i]) }, maxHeight) + return w.Padding(core.Density(c).Unit(), 0) } // agentDecisionMark shows a decision by icon and word. func agentDecisionMark(c *ui.Context, d FileDecision) { - ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { - agentCaption(c, agentDecisionIcon(c, d)) + ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { + AgentCaption(c, agentDecisionIcon(c, d)) }) } // agentDecisionIcon draws a named decision icon, returning its word. func agentDecisionIcon(c *ui.Context, d FileDecision) string { - k := Tokens(c) - tone, icon, word := k.TextMuted, "circle", msg(c, "file_change_card.pending") + k := core.Tokens(c) + tone, icon, word := k.TextMuted, "circle", core.Msg(c, "file_change_card.pending") switch d { case FileAccepted: - tone, icon, word = k.Success, "circle-check", msg(c, "file_change_card.accepted") + tone, icon, word = k.Success, "circle-check", core.Msg(c, "file_change_card.accepted") case FileRejected: - tone, icon, word = k.Danger, "circle-x", msg(c, "file_change_card.rejected") + tone, icon, word = k.Danger, "circle-x", core.Msg(c, "file_change_card.rejected") case FilePending: default: panic(fmt.Sprintf("mujica: unknown FileDecision %d", int(d))) diff --git a/agent_parts.go b/agent/agent_parts.go similarity index 79% rename from agent_parts.go rename to agent/agent_parts.go index d2758a0..d9df73d 100644 --- a/agent_parts.go +++ b/agent/agent_parts.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "bytes" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AgentState is an agent or step state; zero is pending. @@ -67,18 +68,18 @@ func agentTone(k theme.Tokens, s AgentState) (ui.Color, *ui.SVG) { // agentMark draws s; running spins unless still. func agentMark(c *ui.Context, s AgentState, size float32, still bool) *ui.Element { - tone, icon := agentTone(Tokens(c), s) + tone, icon := agentTone(core.Tokens(c), s) if s == AgentRunning && !still { - return Spinner(c, SpinnerOptions{Size: size, Color: tone, Label: msg(c, "agent_status.running")}) + return core.Spinner(c, core.SpinnerOptions{Size: size, Color: tone, Label: core.Msg(c, "agent_status.running")}) } return ui.Icon(c, icon).Size(size, size).TextColor(tone).Shrink(0) } // agentWord is the built-in word for s. -func agentWord(c *ui.Context, s AgentState) string { return msg(c, "agent_status."+s.String()) } +func agentWord(c *ui.Context, s AgentState) string { return core.Msg(c, "agent_status."+s.String()) } -// agentDuration formats d like "320 ms" or "4.2 s". -func agentDuration(d time.Duration) string { +// AgentDuration formats d like "320 ms" or "4.2 s". +func AgentDuration(d time.Duration) string { if d < 0 { panic(fmt.Sprintf("mujica: negative duration %v", d)) } @@ -93,35 +94,35 @@ func agentDuration(d time.Duration) string { return fmt.Sprintf("%d h %02d m", int(d.Hours()), int(d.Minutes())%60) } -// agentCard is the bordered surface every agent card sits on. -func agentCard(c *ui.Context) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() +// AgentCard is the bordered surface every agent card sits on. +func AgentCard(c *ui.Context) *ui.Element { + k := core.Tokens(c) + u := core.Density(c).Unit() return ui.Column(c).Shrink(0).Padding(u*3).Gap(u*2).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border) } -// agentCaption is small muted text. -func agentCaption(c *ui.Context, s string) *ui.Element { - return ui.Text(c, s).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted) +// AgentCaption is small muted text. +func AgentCaption(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted) } // agentWell styles a sunken monospace well. func agentWell(c *ui.Context, l *ui.Element, maxHeight float32) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() return l.Shrink(1).MaxHeight(maxHeight).Padding(u*2, u*3).Radius(theme.ControlRadius). Background(k.Background).Border(theme.BorderWidth, k.Border).Font(theme.MonoFont). - FontSize(FontSize(c, theme.CaptionSize+1)) + FontSize(core.FontSize(c, theme.CaptionSize+1)) } -// agentWellRows builds n rows in a well. -func agentWellRows(c *ui.Context, n int, row func(i int), maxHeight float32) *ui.Element { - return agentWell(c, agentRows(c, n, row), maxHeight) +// AgentWellRows builds n rows in a well. +func AgentWellRows(c *ui.Context, n int, row func(i int), maxHeight float32) *ui.Element { + return agentWell(c, AgentRows(c, n, row), maxHeight) } -// agentRows builds n scrolling rows, virtualized when many. -func agentRows(c *ui.Context, n int, row func(i int)) *ui.Element { +// AgentRows builds n scrolling rows, virtualized when many. +func AgentRows(c *ui.Context, n int, row func(i int)) *ui.Element { if n > agentVirtualRows { return ui.List(c, nil, n, row) } @@ -182,7 +183,7 @@ func agentTokenColor(k theme.Tokens, kind internal.TokenKind) ui.Color { // agentCodeLine builds one highlighted line of language. func agentCodeLine(c *ui.Context, language, line string, inBlock bool) { - k := Tokens(c) + k := core.Tokens(c) toks, _ := internal.Highlight(language, line, inBlock) spans := make([]ui.Span, 0, len(toks)) for _, t := range toks { @@ -194,15 +195,15 @@ func agentCodeLine(c *ui.Context, language, line string, inBlock bool) { ui.RichText(c, spans...) } -// agentCode shows highlighted text in a capped well. -func agentCode(c *ui.Context, language, text string, maxHeight float32) *ui.Element { +// AgentCode shows highlighted text in a capped well. +func AgentCode(c *ui.Context, language, text string, maxHeight float32) *ui.Element { holder := ui.Column(c).Shrink(1) cc := ui.Local(holder, "mujica.agent.code", func() *agentCodeCache { return &agentCodeCache{} }) (*cc).update(language, text) lines, open := (*cc).lines, (*cc).open row := func(i int) { agentCodeLine(c, language, lines[i], open[i]) } holder.Children(func() { - agentWellRows(c, len(lines), row, maxHeight) + AgentWellRows(c, len(lines), row, maxHeight) }) return holder } @@ -263,7 +264,7 @@ func agentBar(c *ui.Context, frac float64, label string) *ui.Element { if frac < 0 || frac > 1 || frac != frac { panic(fmt.Sprintf("mujica: progress %v outside 0..1", frac)) } - k := Tokens(c) + k := core.Tokens(c) return ui.Box(c).FillWidth().Height(4).Shrink(0).Role(ui.RoleProgress).Label(label). Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.Border, 2) @@ -273,10 +274,10 @@ func agentBar(c *ui.Context, frac float64, label string) *ui.Element { }) } -// agentToggle is a small text button reporting a click. -func agentToggle(c *ui.Context, label string, icon string, disabled bool) bool { - k := Tokens(c) - u := Density(c).Unit() +// AgentToggle is a small text button reporting a click. +func AgentToggle(c *ui.Context, label string, icon string, disabled bool) bool { + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*2).Gap(u). Radius(theme.ControlRadius).Disabled(disabled).TextColor(k.AccentText) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -287,7 +288,7 @@ func agentToggle(c *ui.Context, label string, icon string, disabled bool) bool { if icon != "" { ui.Icon(c, icons.Must(icon)).Size(14, 14) } - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine() }) return !b.IsDisabled() && b.Clicked() } diff --git a/agent_plan.go b/agent/agent_plan.go similarity index 66% rename from agent_plan.go rename to agent/agent_plan.go index 2be4683..7713c76 100644 --- a/agent_plan.go +++ b/agent/agent_plan.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PlanItem is a plan task: pending, running, done or skipped. @@ -28,7 +30,7 @@ type AgentPlanOptions struct { type agentPlanState struct{ draft string } // AgentPlan is a live checklist with skip, restore and add. -func AgentPlan(c *ui.Context, items *[]PlanItem, opts AgentPlanOptions) ChoiceResult { +func AgentPlan(c *ui.Context, items *[]PlanItem, opts AgentPlanOptions) core.ChoiceResult { if items == nil { panic("mujica: AgentPlan needs items") } @@ -49,40 +51,40 @@ func AgentPlan(c *ui.Context, items *[]PlanItem, opts AgentPlanOptions) ChoiceRe } done += agentB2I(it.State == AgentDone) } - u := Density(c).Unit() + u := core.Density(c).Unit() title := opts.Title if title == "" { - title = msg(c, "agent_plan.title") + title = core.Msg(c, "agent_plan.title") } - var r ChoiceResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(title) + var r core.ChoiceResult + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(title) st := ui.Local(r.Element, "mujica.plan", func() agentPlanState { return agentPlanState{} }) locked := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) - agentCaption(c, fmt.Sprintf(msg(c, "agent_plan.progress"), done, counted)) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + AgentCaption(c, fmt.Sprintf(core.Msg(c, "agent_plan.progress"), done, counted)) }) frac := 0.0 if counted > 0 { frac = float64(done) / float64(counted) } - agentBar(c, frac, fmt.Sprintf(msg(c, "agent_plan.progress"), done, counted)) + agentBar(c, frac, fmt.Sprintf(core.Msg(c, "agent_plan.progress"), done, counted)) list := *items for i := range list { - ui.Row(c).Key(list[i].ID).Gap(u * 2).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Row(c).Key(list[i].ID).Gap(u * 2).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Children(func() { if agentPlanRow(c, &list[i], locked, opts.Still) { - r.changed = true + r = core.NewChoiceResult(r.Element, true) } }) } if !locked && agentPlanAdd(c, &st.draft) { *items = append(*items, PlanItem{ID: agentPlanID(seen), Text: strings.TrimSpace(st.draft)}) st.draft = "" - r.changed = true + r = core.NewChoiceResult(r.Element, true) } }) - if r.changed { + if r.Changed() { slog.Debug("mujica: agent_plan change", "items", len(*items)) } return r @@ -90,9 +92,9 @@ func AgentPlan(c *ui.Context, items *[]PlanItem, opts AgentPlanOptions) ChoiceRe // agentPlanRow builds one item, reporting a user change. func agentPlanRow(c *ui.Context, it *PlanItem, locked, still bool) bool { - k := Tokens(c) + k := core.Tokens(c) checked := it.State == AgentDone - box := Checkbox(c, &checked, "", CheckboxOptions{Disabled: locked || it.State == AgentSkipped}).Label(it.Text) + box := input.Checkbox(c, &checked, "", input.CheckboxOptions{Disabled: locked || it.State == AgentSkipped}).Label(it.Text) changed := !locked && box.Changed() if changed { it.State = AgentPending @@ -106,16 +108,16 @@ func agentPlanRow(c *ui.Context, it *PlanItem, locked, still bool) bool { t.Strikethrough().TextColor(k.TextMuted) case AgentRunning: agentMark(c, AgentRunning, 14, still) - agentCaption(c, msg(c, "agent_plan.doing")) + AgentCaption(c, core.Msg(c, "agent_plan.doing")) } if it.State == AgentDone || locked { return changed } - label, icon, next := msg(c, "agent_plan.skip"), "ban", AgentSkipped + label, icon, next := core.Msg(c, "agent_plan.skip"), "ban", AgentSkipped if it.State == AgentSkipped { - label, icon, next = msg(c, "agent_plan.restore"), "refresh-cw", AgentPending + label, icon, next = core.Msg(c, "agent_plan.restore"), "refresh-cw", AgentPending } - if agentToggle(c, label, icon, false) { + if AgentToggle(c, label, icon, false) { it.State = next changed = true } @@ -124,13 +126,13 @@ func agentPlanRow(c *ui.Context, it *PlanItem, locked, still bool) bool { // agentPlanAdd builds the new-item field and reports a non-empty submit. func agentPlanAdd(c *ui.Context, draft *string) bool { - u := Density(c).Unit() + u := core.Density(c).Unit() add := false ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - in := TextInput(c, draft, TextInputOptions{Placeholder: msg(c, "agent_plan.placeholder"), Label: msg(c, "agent_plan.placeholder")}) + in := input.TextInput(c, draft, input.TextInputOptions{Placeholder: core.Msg(c, "agent_plan.placeholder"), Label: core.Msg(c, "agent_plan.placeholder")}) in.Element.Grow(1) blank := strings.TrimSpace(*draft) == "" - add = Button(c, msg(c, "agent_plan.add"), ButtonOptions{Variant: Secondary, Disabled: blank}).Clicked() + add = input.Button(c, core.Msg(c, "agent_plan.add"), input.ButtonOptions{Variant: input.Secondary, Disabled: blank}).Clicked() add = (add || in.Submitted()) && !blank }) return add diff --git a/agent_plan_test.go b/agent/agent_plan_test.go similarity index 96% rename from agent_plan_test.go rename to agent/agent_plan_test.go index 1353cc3..929c9be 100644 --- a/agent_plan_test.go +++ b/agent/agent_plan_test.go @@ -1,9 +1,11 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func agentPlanSample() []PlanItem { @@ -17,7 +19,7 @@ func agentPlanSample() []PlanItem { func planTester(items *[]PlanItem, opts AgentPlanOptions, changes *int) *ui.Tester { opts.Still = true return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if AgentPlan(c, items, opts).Changed() { *changes++ } diff --git a/agent_progress.go b/agent/agent_progress.go similarity index 70% rename from agent_progress.go rename to agent/agent_progress.go index 3fb6459..b286e0b 100644 --- a/agent_progress.go +++ b/agent/agent_progress.go @@ -1,7 +1,10 @@ -package mujica +package agent import ( "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" ) // AgentProgressOptions configure an AgentProgress. @@ -19,16 +22,16 @@ type AgentProgressOptions struct { func AgentProgress(c *ui.Context, value float64, opts AgentProgressOptions) *ui.Element { label := opts.Label if label == "" { - label = msg(c, "agent_progress.label") + label = core.Msg(c, "agent_progress.label") } - u := Density(c).Unit() + u := core.Density(c).Unit() col := ui.Column(c).FillWidth().Shrink(0).Gap(u * 1.5) col.Children(func() { - Progress(c, value, ProgressOptions{Label: label, ShowValue: !opts.Indeterminate, Indeterminate: opts.Indeterminate}) + feedback.Progress(c, value, feedback.ProgressOptions{Label: label, ShowValue: !opts.Indeterminate, Indeterminate: opts.Indeterminate}) if opts.Action != "" { ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Role(ui.RoleStatus).Label(opts.Action).Children(func() { agentMark(c, AgentRunning, 14, opts.Still) - agentCaption(c, opts.Action) + AgentCaption(c, opts.Action) }) } }) diff --git a/agent_progress_test.go b/agent/agent_progress_test.go similarity index 90% rename from agent_progress_test.go rename to agent/agent_progress_test.go index 5103ede..e1875ce 100644 --- a/agent_progress_test.go +++ b/agent/agent_progress_test.go @@ -1,15 +1,17 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestAgentProgressValueAndAction(t *testing.T) { value, action := 0.25, "Reading ledger.md" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AgentProgress(c, value, AgentProgressOptions{Action: action, Still: true}) }, 400, 200) if !tt.HasText("25%") || !tt.HasText("Reading ledger.md") || !tt.HasText("Progress") { @@ -24,7 +26,7 @@ func TestAgentProgressValueAndAction(t *testing.T) { func TestAgentProgressIndeterminate(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AgentProgress(c, 0, AgentProgressOptions{Label: "Research", Indeterminate: true, Still: true}) }, 400, 200) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -40,7 +42,7 @@ func TestAgentProgressRejectsOutOfRange(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AgentProgress(c, 1.5, AgentProgressOptions{}) }, 200, 100) } diff --git a/agent_status.go b/agent/agent_status.go similarity index 76% rename from agent_status.go rename to agent/agent_status.go index 4384112..3ab05d9 100644 --- a/agent_status.go +++ b/agent/agent_status.go @@ -1,9 +1,10 @@ -package mujica +package agent import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AgentStatusOptions configure an AgentStatus. @@ -20,15 +21,15 @@ func AgentStatus(c *ui.Context, state AgentState, opts AgentStatusOptions) *ui.E if label == "" { label = agentWord(c, state) // panics on an unknown state } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() tone, _ := agentTone(k, state) pill := ui.Row(c).Shrink(0).AlignSelf(ui.Start).AlignItems(ui.Center).Gap(u*1.5).Padding(u, u*2.5). Radius(theme.CardRadius).Border(theme.BorderWidth, tone).Background(k.Surface). Role(ui.RoleStatus).Label(label) pill.Children(func() { agentMark(c, state, 14, opts.Still) - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize+1)).TextColor(k.Text).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize+1)).TextColor(k.Text).SingleLine() }) return pill } diff --git a/agent_status_test.go b/agent/agent_status_test.go similarity index 91% rename from agent_status_test.go rename to agent/agent_status_test.go index a642c4c..2119128 100644 --- a/agent_status_test.go +++ b/agent/agent_status_test.go @@ -1,16 +1,18 @@ -package mujica +package agent import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestAgentStatusWords(t *testing.T) { states := []AgentState{AgentPlanning, AgentRunning, AgentWaiting, AgentDone, AgentFailed} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) for _, s := range states { AgentStatus(c, s, AgentStatusOptions{Still: true}) } @@ -26,7 +28,7 @@ func TestAgentStatusWords(t *testing.T) { func TestAgentStatusAnimationStopsWhenStill(t *testing.T) { still := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AgentStatus(c, AgentRunning, AgentStatusOptions{Still: still}) }, 300, 100) tt.Frame() @@ -53,7 +55,7 @@ func TestAgentStatusRejectsUnknownState(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AgentStatus(c, AgentState(42), AgentStatusOptions{}) }, 200, 100) } diff --git a/agent_step_list.go b/agent/agent_step_list.go similarity index 88% rename from agent_step_list.go rename to agent/agent_step_list.go index 710dc56..1ea0d3e 100644 --- a/agent_step_list.go +++ b/agent/agent_step_list.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AgentStep is one step of an agent run. @@ -78,8 +79,8 @@ func AgentStepList(c *ui.Context, steps []AgentStep, opts AgentStepListOptions) // agentStepRow builds one step, reporting a header click. func agentStepRow(c *ui.Context, s AgentStep, i, n int, current, open, still bool, r *AgentStepListResult) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() clicked := false top := (d.ControlHeight() - 18) / 2 // centers the mark on the step header @@ -109,11 +110,11 @@ func agentStepRow(c *ui.Context, s AgentStep, i, n int, current, open, still boo head.Children(func() { ui.Text(c, s.Title).FontWeight(600).Grow(1).Shrink(1) if current { - agentCaption(c, msg(c, "agent_step_list.current")) + AgentCaption(c, core.Msg(c, "agent_step_list.current")) } - agentCaption(c, agentWord(c, s.State)) + AgentCaption(c, agentWord(c, s.State)) if s.State == AgentDone || s.State == AgentFailed { - agentCaption(c, agentDuration(s.Duration)) + AgentCaption(c, AgentDuration(s.Duration)) } if s.Detail != "" { chev := "chevron-right" @@ -129,7 +130,7 @@ func agentStepRow(c *ui.Context, s AgentStep, i, n int, current, open, still boo } if s.State == AgentFailed { ui.Row(c).Children(func() { - if agentToggle(c, msg(c, "agent_step_list.retry"), "refresh-cw", head.IsDisabled()) { + if AgentToggle(c, core.Msg(c, "agent_step_list.retry"), "refresh-cw", head.IsDisabled()) { r.Retry = s.ID } }) diff --git a/agent_step_list_test.go b/agent/agent_step_list_test.go similarity index 96% rename from agent_step_list_test.go rename to agent/agent_step_list_test.go index fa9d0b4..ff86908 100644 --- a/agent_step_list_test.go +++ b/agent/agent_step_list_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var agentStepsSample = []AgentStep{ @@ -16,7 +18,7 @@ var agentStepsSample = []AgentStep{ func stepTester(steps []AgentStep, opts AgentStepListOptions, got *[]AgentStepListResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := AgentStepList(c, steps, opts) if r.Retry != "" || r.Toggled != "" { *got = append(*got, r) diff --git a/artifact_panel.go b/agent/artifact_panel.go similarity index 78% rename from artifact_panel.go rename to agent/artifact_panel.go index 189333b..305a538 100644 --- a/artifact_panel.go +++ b/agent/artifact_panel.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ArtifactKind is what an artifact holds; zero is code. @@ -71,35 +72,35 @@ func ArtifactPanel(c *ui.Context, art Artifact, opts ArtifactPanelOptions) Artif if h == 0 { h = 320 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r ArtifactPanelResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(art.Title) + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(art.Title) st := ui.Local(r.Element, "mujica.artifact", func() artifactPanelState { return artifactPanelState{source: art.Kind == ArtifactCode} }) locked := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, art.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).SingleLine().Grow(1).Shrink(1) + ui.Text(c, art.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).SingleLine().Grow(1).Shrink(1) if opts.Preview != nil { artifactViewSwitch(c, &st.source, locked) } copied := !st.copiedAt.IsZero() && c.Now().Sub(st.copiedAt) < 2*time.Second - label, icon := msg(c, "artifact_panel.copy"), "copy" + label, icon := core.Msg(c, "artifact_panel.copy"), "copy" if copied { - label, icon = msg(c, "artifact_panel.copied"), "check" + label, icon = core.Msg(c, "artifact_panel.copied"), "check" c.After(2*time.Second - c.Now().Sub(st.copiedAt)) } - if agentToggle(c, label, icon, locked) { + if AgentToggle(c, label, icon, locked) { c.WriteClipboard(art.Content) st.copiedAt, r.Copied = c.Now(), true } - if agentToggle(c, msg(c, "artifact_panel.export"), "upload", locked) { + if AgentToggle(c, core.Msg(c, "artifact_panel.export"), "upload", locked) { r.Exported = true } }) ui.Column(c).Height(h).Shrink(0).Children(func() { if st.source || opts.Preview == nil { - agentCode(c, art.Language, art.Content, h).Grow(1) + AgentCode(c, art.Language, art.Content, h).Grow(1) return } ui.Scroll(c).Grow(1).Padding(u*3).Radius(theme.ControlRadius).Background(k.Background). @@ -114,14 +115,14 @@ func ArtifactPanel(c *ui.Context, art Artifact, opts ArtifactPanelOptions) Artif // artifactViewSwitch is the Preview / Source segmented switch. func artifactViewSwitch(c *ui.Context, source *bool, locked bool) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ui.Row(c).Shrink(0).FocusGroup(ui.Horizontal).Border(theme.BorderWidth, k.ControlBorder).Radius(theme.ControlRadius). Role(ui.RoleRadioGroup).Children(func() { for _, src := range []bool{false, true} { - label, icon := msg(c, "artifact_panel.preview"), "eye" + label, icon := core.Msg(c, "artifact_panel.preview"), "eye" if src { - label, icon = msg(c, "artifact_panel.source"), "file-text" + label, icon = core.Msg(c, "artifact_panel.source"), "file-text" } b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Padding(0, u*2).Gap(u).Role(ui.RoleRadio).Label(label). Disabled(locked) @@ -137,7 +138,7 @@ func artifactViewSwitch(c *ui.Context, source *bool, locked bool) { ui.Icon(c, icons.Must("check")).Size(12, 12).TextColor(k.AccentText) } ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize+1)) + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize+1)) }) if !b.IsDisabled() && b.Clicked() && !on { *source = src diff --git a/artifact_panel_test.go b/agent/artifact_panel_test.go similarity index 95% rename from artifact_panel_test.go rename to agent/artifact_panel_test.go index a37f441..82921aa 100644 --- a/artifact_panel_test.go +++ b/agent/artifact_panel_test.go @@ -1,16 +1,18 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var artifactCodeSample = Artifact{Title: "seating.go", Language: "go", Content: "package court\n\n// Seats maps guests to tables.\nfunc Seats() map[string]int {\n\treturn map[string]int{\"Sakiko\": 1}\n}\n"} func artifactTester(art Artifact, opts ArtifactPanelOptions, got *[]ArtifactPanelResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ArtifactPanel(c, art, opts) if r.Copied || r.Exported { *got = append(*got, r) @@ -37,7 +39,7 @@ func TestArtifactPanelCodeCopyExport(t *testing.T) { func TestArtifactPanelPreviewSourceSwitch(t *testing.T) { doc := Artifact{Title: "Invitation", Kind: ArtifactDocument, Content: "# You are invited\n"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ArtifactPanel(c, doc, ArtifactPanelOptions{Preview: func() { ui.Text(c, "Rendered invitation") }}) }, 600, 460) if !tt.HasText("Rendered invitation") || tt.HasText("# You are invited") { diff --git a/artifact_version_switcher.go b/agent/artifact_version_switcher.go similarity index 72% rename from artifact_version_switcher.go rename to agent/artifact_version_switcher.go index 2913f14..23b5a37 100644 --- a/artifact_version_switcher.go +++ b/agent/artifact_version_switcher.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // ArtifactVersion is one saved version of an artifact. @@ -39,11 +40,11 @@ func ArtifactVersionSwitcher(c *ui.Context, current *int, versions []ArtifactVer panic(fmt.Sprintf("mujica: ArtifactVersionSwitcher version %d needs a Label", i)) } } - u := Density(c).Unit() + u := core.Density(c).Unit() n := len(versions) var r ArtifactVersionResult r.Element = ui.Row(c).Shrink(0).Gap(u * 2).AlignItems(ui.Center).Disabled(opts.Disabled). - Label(msg(c, "artifact_version_switcher.label")) + Label(core.Msg(c, "artifact_version_switcher.label")) locked := r.Element.IsDisabled() move := func(d int) { if to := *current + d; !locked && to >= 0 && to < n { @@ -51,25 +52,25 @@ func ArtifactVersionSwitcher(c *ui.Context, current *int, versions []ArtifactVer } } r.Element.Children(func() { - if iconAction(c, icons.Must("chevron-left"), msg(c, "artifact_version_switcher.older")).Disabled(locked || *current == 0).Clicked() { + if core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "artifact_version_switcher.older")).Disabled(locked || *current == 0).Clicked() { move(-1) } v := versions[*current] ui.Column(c).Shrink(1).Role(ui.RoleStatus).Label(v.Label).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, v.Label).FontWeight(600).SingleLine() - agentCaption(c, fmt.Sprintf(msg(c, "artifact_version_switcher.count"), *current+1, n)) + AgentCaption(c, fmt.Sprintf(core.Msg(c, "artifact_version_switcher.count"), *current+1, n)) }) if v.Note != "" { - agentCaption(c, v.Note).SingleLine() + AgentCaption(c, v.Note).SingleLine() } }) - if iconAction(c, icons.Must("chevron-right"), msg(c, "artifact_version_switcher.newer")).Disabled(locked || *current == n-1).Clicked() { + if core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "artifact_version_switcher.newer")).Disabled(locked || *current == n-1).Clicked() { move(1) } if *current > 0 { - label := fmt.Sprintf(msg(c, "artifact_version_switcher.compare"), versions[*current-1].Label) - if agentToggle(c, label, "arrow-up-down", locked) { + label := fmt.Sprintf(core.Msg(c, "artifact_version_switcher.compare"), versions[*current-1].Label) + if AgentToggle(c, label, "arrow-up-down", locked) { r.Compare = true } } diff --git a/artifact_version_switcher_test.go b/agent/artifact_version_switcher_test.go similarity index 96% rename from artifact_version_switcher_test.go rename to agent/artifact_version_switcher_test.go index f910887..e513979 100644 --- a/artifact_version_switcher_test.go +++ b/agent/artifact_version_switcher_test.go @@ -1,9 +1,11 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var artifactVersionsSample = []ArtifactVersion{ @@ -14,7 +16,7 @@ var artifactVersionsSample = []ArtifactVersion{ func versionTester(cur *int, opts ArtifactVersionSwitcherOptions, got *[]ArtifactVersionResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ArtifactVersionSwitcher(c, cur, artifactVersionsSample, opts) if r.Changed || r.Compare { *got = append(*got, r) diff --git a/checkpoint_list.go b/agent/checkpoint_list.go similarity index 73% rename from checkpoint_list.go rename to agent/checkpoint_list.go index ff721e8..1b1253a 100644 --- a/checkpoint_list.go +++ b/agent/checkpoint_list.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Checkpoint is a saved point to roll back to. @@ -30,7 +32,7 @@ type checkpointListState struct { } // CheckpointList marks the current point and confirms rollbacks. -func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts CheckpointListOptions) ChoiceResult { +func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts CheckpointListOptions) core.ChoiceResult { if current == nil { panic("mujica: CheckpointList needs current") } @@ -48,9 +50,9 @@ func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts Ch if maxH == 0 { maxH = 360 } - k := Tokens(c) - var r ChoiceResult - r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled).Label(msg(c, "checkpoint_list.label")) + k := core.Tokens(c) + var r core.ChoiceResult + r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled).Label(core.Msg(c, "checkpoint_list.label")) st := ui.Local(r.Element, "mujica.checkpoints", func() checkpointListState { return checkpointListState{} }) locked := r.Element.IsDisabled() if locked { @@ -58,10 +60,10 @@ func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts Ch } r.Element.Children(func() { if len(points) == 0 { - agentCaption(c, msg(c, "checkpoint_list.empty")) + AgentCaption(c, core.Msg(c, "checkpoint_list.empty")) return } - agentRows(c, len(points), func(i int) { + AgentRows(c, len(points), func(i int) { if checkpointRow(c, points[i], points[i].ID == *current, locked) { st.asking, st.target = true, points[i].ID } @@ -73,10 +75,10 @@ func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts Ch label = p.Label } } - res := AlertDialog(c, &st.asking, AlertDialogOptions{Title: fmt.Sprintf(msg(c, "checkpoint_list.confirm"), label), - Description: msg(c, "checkpoint_list.warning"), ConfirmLabel: msg(c, "checkpoint_list.rollback")}) + res := overlay.AlertDialog(c, &st.asking, overlay.AlertDialogOptions{Title: fmt.Sprintf(core.Msg(c, "checkpoint_list.confirm"), label), + Description: core.Msg(c, "checkpoint_list.warning"), ConfirmLabel: core.Msg(c, "checkpoint_list.rollback")}) if res.Confirmed && !locked && seen[st.target] { - *current, r.changed = st.target, true + *current, r = st.target, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: checkpoint_list rollback", "to", st.target) } }) @@ -85,8 +87,8 @@ func CheckpointList(c *ui.Context, current *string, points []Checkpoint, opts Ch // checkpointRow shows one checkpoint and reports a rollback request. func checkpointRow(c *ui.Context, p Checkpoint, current, locked bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() asked := false row := ui.Row(c).Key(p.ID).Gap(u*3).AlignItems(ui.Center).MinHeight(d.ControlHeight()+u*2).Padding(u*2, u*3) @@ -105,14 +107,14 @@ func checkpointRow(c *ui.Context, p Checkpoint, current, locked bool) bool { ui.Column(c).Grow(1).Shrink(1).Gap(u / 2).Children(func() { ui.Text(c, p.Label).FontWeight(600).SingleLine() if p.Note != "" { - agentCaption(c, p.Note) + AgentCaption(c, p.Note) } }) if current { - agentCaption(c, msg(c, "checkpoint_list.current")) + AgentCaption(c, core.Msg(c, "checkpoint_list.current")) return } - asked = agentToggle(c, msg(c, "checkpoint_list.rollback"), "refresh-cw", locked) + asked = AgentToggle(c, core.Msg(c, "checkpoint_list.rollback"), "refresh-cw", locked) }) row.Label(p.Label) return asked diff --git a/checkpoint_list_test.go b/agent/checkpoint_list_test.go similarity index 96% rename from checkpoint_list_test.go rename to agent/checkpoint_list_test.go index f37f863..9ac66c2 100644 --- a/checkpoint_list_test.go +++ b/agent/checkpoint_list_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var checkpointsSample = []Checkpoint{ @@ -15,7 +17,7 @@ var checkpointsSample = []Checkpoint{ func checkpointTester(cur *string, points []Checkpoint, opts CheckpointListOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if CheckpointList(c, cur, points, opts).Changed() { *changes++ } diff --git a/command_execution_card.go b/agent/command_execution_card.go similarity index 69% rename from command_execution_card.go rename to agent/command_execution_card.go index 347f707..db439a3 100644 --- a/command_execution_card.go +++ b/agent/command_execution_card.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CommandRun is a command run; the caller appends Output. @@ -57,7 +59,7 @@ func (o *commandOutput) update(out string) { } // CommandExecutionCard shows a command run; Changed requests an abort. -func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCardOptions) ChoiceResult { +func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCardOptions) core.ChoiceResult { if run.Command == "" { panic("mujica: CommandExecutionCard needs a Command") } @@ -68,10 +70,10 @@ func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCa if h == 0 { h = 240 } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(run.Command) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(run.Command) out := ui.Local(r.Element, "mujica.cmd.output", func() *commandOutput { return &commandOutput{} }) o := *out o.list.FollowEnd = true @@ -82,32 +84,32 @@ func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCa ui.Text(c, run.Command).Font(theme.MonoFont).FontWeight(600).Grow(1).Shrink(1).Selectable() if run.Running { agentMark(c, AgentRunning, 16, opts.Still) - agentCaption(c, msg(c, "agent_status.running")) + AgentCaption(c, core.Msg(c, "agent_status.running")) locked := r.Element.IsDisabled() - if Button(c, msg(c, "command_execution_card.abort"), ButtonOptions{Variant: Outline, Disabled: locked}).Clicked() && !locked { - r.changed = true + if input.Button(c, core.Msg(c, "command_execution_card.abort"), input.ButtonOptions{Variant: input.Outline, Disabled: locked}).Clicked() && !locked { + r = core.NewChoiceResult(r.Element, true) } return } commandExit(c, run.ExitCode) - agentCaption(c, agentDuration(run.Duration)) + AgentCaption(c, AgentDuration(run.Duration)) }) if run.Dir != "" { ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Children(func() { ui.Icon(c, icons.Must("folder")).Size(14, 14).TextColor(k.TextMuted) - agentCaption(c, run.Dir).Font(theme.MonoFont) + AgentCaption(c, run.Dir).Font(theme.MonoFont) }) } well := ui.List(c, &o.list, len(o.lines), func(i int) { ui.RichText(c, agentAnsiSpans(k, o.lines[i])...) }) - agentWell(c, well, h).Height(h).Label(msg(c, "command_execution_card.output")).Children(func() { + agentWell(c, well, h).Height(h).Label(core.Msg(c, "command_execution_card.output")).Children(func() { if len(o.lines) == 0 { - agentCaption(c, msg(c, "command_execution_card.no_output")) + AgentCaption(c, core.Msg(c, "command_execution_card.no_output")) } }) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: command_execution_card abort", "command", run.Command) } return r @@ -115,14 +117,14 @@ func CommandExecutionCard(c *ui.Context, run CommandRun, opts CommandExecutionCa // commandExit shows the exit code by icon and word. func commandExit(c *ui.Context, code int) { - k := Tokens(c) + k := core.Tokens(c) tone, icon := k.Success, "circle-check" if code != 0 { tone, icon = k.Danger, "circle-x" } - ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { + ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(tone) - ui.Text(c, fmt.Sprintf(msg(c, "command_execution_card.exit"), code)).TextColor(tone). - FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "command_execution_card.exit"), code)).TextColor(tone). + FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine() }) } diff --git a/command_execution_card_test.go b/agent/command_execution_card_test.go similarity index 97% rename from command_execution_card_test.go rename to agent/command_execution_card_test.go index 82b0da5..2c4f7ad 100644 --- a/command_execution_card_test.go +++ b/agent/command_execution_card_test.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,12 +7,14 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func cmdTester(run *CommandRun, opts CommandExecutionCardOptions, aborts *int) *ui.Tester { opts.Still = true return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if CommandExecutionCard(c, *run, opts).Changed() { *aborts++ } diff --git a/agent/doc.go b/agent/doc.go new file mode 100644 index 0000000..f7ce33d --- /dev/null +++ b/agent/doc.go @@ -0,0 +1,2 @@ +// Package agent holds MujicaUI agent plans, tool calls and approvals. +package agent diff --git a/agent/example_agent_test.go b/agent/example_agent_test.go new file mode 100644 index 0000000..8d59ef0 --- /dev/null +++ b/agent/example_agent_test.go @@ -0,0 +1,244 @@ +package agent_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func ExampleToolCallCard() { + call := agent.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", + Result: "{\"lines\":40}", State: agent.AgentDone, Duration: 420 * time.Millisecond} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.ToolCallCard(c, call, agent.ToolCallCardOptions{OpenArgs: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToolCallGroup() { + open := false + calls := []agent.ToolCall{ + {ID: "a", Name: "list_dir", State: agent.AgentDone, Duration: 300 * time.Millisecond}, + {ID: "b", Name: "read_file", State: agent.AgentFailed, Error: "not found"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.ToolCallGroup(c, &open, calls, agent.ToolCallGroupOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToolApprovalDialog() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := agent.ToolApprovalDialog(c, &open, agent.ToolApprovalOptions{ + Tool: "delete_files", Args: "{\"glob\":\"*.tmp\"}", Reason: "Clean the build folder.", + }) + if r.Allowed { + fmt.Println("run the tool") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePermissionPrompt() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := agent.PermissionPrompt(c, &open, agent.PermissionPromptOptions{ + Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger", + }) + if r.Choice == agent.PermissionAllowAlways { + fmt.Println("remember the grant") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAgentStepList() { + steps := []agent.AgentStep{ + {ID: "read", Title: "Read the guest list", State: agent.AgentDone, Duration: time.Second}, + {ID: "seat", Title: "Assign the seats", State: agent.AgentFailed, Detail: "Table 4 is full."}, + {ID: "send", Title: "Send invitations", State: agent.AgentPending}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := agent.AgentStepList(c, steps, agent.AgentStepListOptions{}); r.Retry != "" { + fmt.Println("retry", r.Retry) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAgentPlan() { + items := []agent.PlanItem{ + {ID: "a", Text: "Read the guest list", State: agent.AgentDone}, + {ID: "b", Text: "Assign the seats", State: agent.AgentRunning}, + {ID: "c", Text: "Order the flowers"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.AgentPlan(c, &items, agent.AgentPlanOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAgentStatus() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.AgentStatus(c, agent.AgentWaiting, agent.AgentStatusOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAgentProgress() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.AgentProgress(c, 0.4, agent.AgentProgressOptions{Action: "Reading ledger.md"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSubAgentTree() { + agents := []agent.SubAgent{{ID: "lead", Name: "Steward", State: agent.AgentRunning, Children: []agent.SubAgent{ + {ID: "cook", Name: "Cook", Task: "Plan the menu", State: agent.AgentWaiting}, + }}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.SubAgentTree(c, agents, agent.SubAgentTreeOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFileChangeCard() { + decision := agent.FilePending + change := agent.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if agent.FileChangeCard(c, &decision, change, agent.FileChangeCardOptions{}).OpenDiff { + fmt.Println("open the full diff") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMultiFileDiffReview() { + files := []agent.FileChange{ + {Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"}, + {Path: "menu.md", Old: "soup\n", New: "soup\ntart\n"}, + } + decisions := make([]agent.FileDecision, len(files)) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.MultiFileDiffReview(c, &decisions, files, agent.MultiFileDiffReviewOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommandExecutionCard() { + run := agent.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if agent.CommandExecutionCard(c, run, agent.CommandExecutionCardOptions{}).Changed() { + fmt.Println("abort the process") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleScreenshotStream() { + var st agent.ScreenshotStreamState + var frames []agent.ScreenshotFrame // appended as the agent captures them + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.ScreenshotStream(c, &st, frames, agent.ScreenshotStreamOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleArtifactPanel() { + art := agent.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if agent.ArtifactPanel(c, art, agent.ArtifactPanelOptions{}).Exported { + fmt.Println("save the file") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleArtifactVersionSwitcher() { + current := 1 + versions := []agent.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if agent.ArtifactVersionSwitcher(c, ¤t, versions, agent.ArtifactVersionSwitcherOptions{}).Compare { + fmt.Println("diff v1 and v2") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSandboxStatus() { + info := agent.SandboxInfo{Name: "sandbox-7", State: agent.SandboxRunning, + Resources: []agent.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + agent.SandboxStatus(c, info, agent.SandboxStatusOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMCPServerList() { + servers := []agent.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: agent.MCPConnected, Tools: 12}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := agent.MCPServerList(c, servers, agent.MCPServerListOptions{}); r.Action == agent.MCPStop { + fmt.Println("stop", r.Server) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMemoryPanel() { + entries := []agent.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := agent.MemoryPanel(c, &entries, agent.MemoryPanelOptions{}); r.Deleted != "" { + fmt.Println("forget", r.Deleted) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCheckpointList() { + current := "c2" + points := []agent.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if agent.CheckpointList(c, ¤t, points, agent.CheckpointListOptions{}).Changed() { + fmt.Println("restore", current) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHumanInputRequest() { + answer := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := agent.HumanInputRequest(c, &answer, agent.HumanInputRequestOptions{ + Question: "Which hall?", Options: []string{"East wing", "Great hall"}, AllowFree: true, + }) + if r.Submitted { + fmt.Println("reply", answer) + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/file_change_card.go b/agent/file_change_card.go similarity index 65% rename from file_change_card.go rename to agent/file_change_card.go index 76785dc..0334b67 100644 --- a/file_change_card.go +++ b/agent/file_change_card.go @@ -1,12 +1,14 @@ -package mujica +package agent import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileChangeCardOptions configure a FileChangeCard. @@ -37,20 +39,20 @@ func FileChangeCard(c *ui.Context, decision *FileDecision, change FileChange, op if preview == 0 { preview = 8 } - u := Density(c).Unit() + u := core.Density(c).Unit() var r FileChangeResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(change.Path) + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(change.Path) dc := agentDiffOf(r.Element, change) locked := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Children(func() { - ui.Icon(c, icons.Must("file-text")).Size(16, 16).TextColor(Tokens(c).TextMuted).Shrink(0) + ui.Icon(c, icons.Must("file-text")).Size(16, 16).TextColor(core.Tokens(c).TextMuted).Shrink(0) ui.Text(c, change.Path).Font(theme.MonoFont).FontWeight(600).SingleLine().Grow(1).Shrink(1) switch { case change.Old == "" && change.New != "": - agentCaption(c, msg(c, "file_change_card.new")) + AgentCaption(c, core.Msg(c, "file_change_card.new")) case change.New == "" && change.Old != "": - agentCaption(c, msg(c, "file_change_card.deleted")) + AgentCaption(c, core.Msg(c, "file_change_card.deleted")) } agentDiffStats(c, dc.add, dc.del) agentDecisionMark(c, *decision) @@ -61,19 +63,19 @@ func FileChangeCard(c *ui.Context, decision *FileDecision, change FileChange, op agentDiffView(c, shown, 320).Font(theme.MonoFont) } ui.Row(c).Gap(u * 2).Justify(ui.End).Wrap().Children(func() { - if Button(c, msg(c, "file_change_card.open"), ButtonOptions{Variant: Ghost, Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.open"), input.ButtonOptions{Variant: input.Ghost, Disabled: locked}).Clicked() && !locked { r.OpenDiff = true } if *decision != FilePending { - if Button(c, msg(c, "file_change_card.undo"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.undo"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() && !locked { *decision, r.Decided = FilePending, true } return } - if Button(c, msg(c, "file_change_card.reject"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.reject"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() && !locked { *decision, r.Decided = FileRejected, true } - if Button(c, msg(c, "file_change_card.accept"), ButtonOptions{Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.accept"), input.ButtonOptions{Disabled: locked}).Clicked() && !locked { *decision, r.Decided = FileAccepted, true } }) diff --git a/file_change_card_test.go b/agent/file_change_card_test.go similarity index 96% rename from file_change_card_test.go rename to agent/file_change_card_test.go index 0c93ff4..61ae7ca 100644 --- a/file_change_card_test.go +++ b/agent/file_change_card_test.go @@ -1,9 +1,11 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var fileChangeSample = FileChange{Path: "court/ledger.md", @@ -12,7 +14,7 @@ var fileChangeSample = FileChange{Path: "court/ledger.md", func fileChangeTester(d *FileDecision, ch FileChange, opts FileChangeCardOptions, got *[]FileChangeResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FileChangeCard(c, d, ch, opts) if r.Decided || r.OpenDiff { *got = append(*got, r) diff --git a/agent/helpers_test.go b/agent/helpers_test.go new file mode 100644 index 0000000..876f114 --- /dev/null +++ b/agent/helpers_test.go @@ -0,0 +1,30 @@ +package agent + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/human_input_request.go b/agent/human_input_request.go similarity index 70% rename from human_input_request.go rename to agent/human_input_request.go index 1d4a6cf..2967239 100644 --- a/human_input_request.go +++ b/agent/human_input_request.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -9,8 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // HumanInputRequestOptions configure a HumanInputRequest. Question is required. @@ -45,10 +47,10 @@ func HumanInputRequest(c *ui.Context, answer *string, opts HumanInputRequestOpti if len(opts.Options) == 0 && !opts.AllowFree { panic("mujica: HumanInputRequest needs Options or AllowFree") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r HumanInputResult - r.Element = agentCard(c).Border(theme.BorderWidth, k.Info).Role(ui.RoleGroup).Label(opts.Question) + r.Element = AgentCard(c).Border(theme.BorderWidth, k.Info).Role(ui.RoleGroup).Label(opts.Question) st := ui.Local(r.Element, "mujica.humaninput", func() humanInputState { return humanInputState{} }) left := time.Duration(0) if !opts.Deadline.IsZero() { @@ -65,19 +67,19 @@ func HumanInputRequest(c *ui.Context, answer *string, opts HumanInputRequestOpti ui.Text(c, opts.Question).FontWeight(600).Grow(1).Shrink(1) }) if len(opts.Options) > 0 { - items := make([]RadioItem[string], len(opts.Options)) + items := make([]input.RadioItem[string], len(opts.Options)) for i, o := range opts.Options { - items[i] = RadioItem[string]{Value: o, Label: o} + items[i] = input.RadioItem[string]{Value: o, Label: o} } chosen := *answer - if RadioGroup(c, &chosen, items, RadioGroupOptions{Label: opts.Question, Disabled: locked}).Changed() && !locked { + if input.RadioGroup(c, &chosen, items, input.RadioGroupOptions{Label: opts.Question, Disabled: locked}).Changed() && !locked { *answer, st.draft = chosen, "" } } submit := false if opts.AllowFree { - in := TextInput(c, &st.draft, TextInputOptions{Placeholder: msg(c, "human_input_request.free"), - Label: msg(c, "human_input_request.free"), Disabled: locked}) + in := input.TextInput(c, &st.draft, input.TextInputOptions{Placeholder: core.Msg(c, "human_input_request.free"), + Label: core.Msg(c, "human_input_request.free"), Disabled: locked}) if in.Changed() && !locked { *answer = st.draft } @@ -86,7 +88,7 @@ func HumanInputRequest(c *ui.Context, answer *string, opts HumanInputRequestOpti ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { humanInputClock(c, opts.Deadline, left, st.expired) blank := strings.TrimSpace(*answer) == "" - if Button(c, msg(c, "human_input_request.send"), ButtonOptions{Disabled: locked || blank}).Clicked() { + if input.Button(c, core.Msg(c, "human_input_request.send"), input.ButtonOptions{Disabled: locked || blank}).Clicked() { submit = true } r.Submitted = submit && !locked && !blank @@ -103,24 +105,24 @@ func HumanInputRequest(c *ui.Context, answer *string, opts HumanInputRequestOpti // humanInputClock shows the time left, or that it ran out. func humanInputClock(c *ui.Context, deadline time.Time, left time.Duration, expired bool) { - k := Tokens(c) + k := core.Tokens(c) text, tone := "", k.TextMuted switch { case expired: - text, tone = msg(c, "human_input_request.timeout"), k.Warning + text, tone = core.Msg(c, "human_input_request.timeout"), k.Warning case !deadline.IsZero(): - text = fmt.Sprintf(msg(c, "human_input_request.left"), agentDuration(left.Truncate(time.Second))) + text = fmt.Sprintf(core.Msg(c, "human_input_request.left"), AgentDuration(left.Truncate(time.Second))) wait := left % time.Second if wait == 0 { wait = time.Second } c.After(wait) } - ui.Row(c).Grow(1).Gap(Density(c).Unit()).AlignItems(ui.Center).Role(ui.RoleStatus).Label(text).Children(func() { + ui.Row(c).Grow(1).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Role(ui.RoleStatus).Label(text).Children(func() { if text == "" { return } ui.Icon(c, icons.Must("clock")).Size(14, 14).TextColor(tone) - ui.Text(c, text).TextColor(tone).FontSize(FontSize(c, theme.CaptionSize+1)) + ui.Text(c, text).TextColor(tone).FontSize(core.FontSize(c, theme.CaptionSize+1)) }) } diff --git a/human_input_request_test.go b/agent/human_input_request_test.go similarity index 96% rename from human_input_request_test.go rename to agent/human_input_request_test.go index cb82671..744f97f 100644 --- a/human_input_request_test.go +++ b/agent/human_input_request_test.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "strings" @@ -6,11 +6,13 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func humanTester(answer *string, opts *HumanInputRequestOptions, got *[]HumanInputResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := HumanInputRequest(c, answer, *opts) if r.Submitted || r.TimedOut { *got = append(*got, r) diff --git a/mcp_server_list.go b/agent/mcp_server_list.go similarity index 73% rename from mcp_server_list.go rename to agent/mcp_server_list.go index 649fdaa..c8f9b18 100644 --- a/mcp_server_list.go +++ b/agent/mcp_server_list.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MCPState is a server connection; zero is stopped. @@ -74,15 +76,15 @@ func MCPServerList(c *ui.Context, servers []MCPServer, opts MCPServerListOptions maxH = 360 } var r MCPServerListResult - r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled).Label(msg(c, "mcp_server_list.label")) + r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled).Label(core.Msg(c, "mcp_server_list.label")) locked := r.Element.IsDisabled() r.Element.Children(func() { if len(servers) == 0 { - agentCaption(c, msg(c, "mcp_server_list.empty")) + AgentCaption(c, core.Msg(c, "mcp_server_list.empty")) return } - k := Tokens(c) - agentRows(c, len(servers), func(i int) { + k := core.Tokens(c) + AgentRows(c, len(servers), func(i int) { if a := mcpServerRow(c, servers[i], locked, opts.Still); a != MCPNone && !locked { r.Action, r.Server = a, servers[i].ID } @@ -97,11 +99,11 @@ func MCPServerList(c *ui.Context, servers []MCPServer, opts MCPServerListOptions // mcpServerRow builds one server, returning its pressed action. func mcpServerRow(c *ui.Context, s MCPServer, locked, still bool) MCPAction { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() words := [...]string{"stopped", "connecting", "connected", "error"} - word := msg(c, "mcp_server_list."+words[s.State]) + word := core.Msg(c, "mcp_server_list."+words[s.State]) act := MCPNone ui.Row(c).Key(s.ID).Gap(u*3).AlignItems(ui.Center).MinHeight(d.ControlHeight()+u*2).Padding(u*2, u*3). Label(s.Name + ", " + word).Children(func() { @@ -118,25 +120,25 @@ func mcpServerRow(c *ui.Context, s MCPServer, locked, still bool) MCPAction { ui.Column(c).Grow(1).Shrink(1).Gap(u / 2).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, s.Name).FontWeight(600).SingleLine() - agentCaption(c, s.Transport).Font(theme.MonoFont) + AgentCaption(c, s.Transport).Font(theme.MonoFont) }) line := word if s.State == MCPConnected { - line += " · " + fmt.Sprintf(msg(c, "mcp_server_list.tools"), s.Tools) + line += " · " + fmt.Sprintf(core.Msg(c, "mcp_server_list.tools"), s.Tools) } - agentCaption(c, line) + AgentCaption(c, line) if s.State == MCPError && s.Error != "" { - ui.Text(c, s.Error).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Selectable() + ui.Text(c, s.Error).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Selectable() } }) - if s.State == MCPError && Button(c, msg(c, "mcp_server_list.reconnect"), ButtonOptions{Variant: Outline, Disabled: locked}).Clicked() { + if s.State == MCPError && input.Button(c, core.Msg(c, "mcp_server_list.reconnect"), input.ButtonOptions{Variant: input.Outline, Disabled: locked}).Clicked() { act = MCPReconnect } if s.State == MCPStopped { - if Button(c, msg(c, "mcp_server_list.start"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() { + if input.Button(c, core.Msg(c, "mcp_server_list.start"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() { act = MCPStart } - } else if Button(c, msg(c, "mcp_server_list.stop"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() { + } else if input.Button(c, core.Msg(c, "mcp_server_list.stop"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() { act = MCPStop } }) diff --git a/mcp_server_list_test.go b/agent/mcp_server_list_test.go similarity index 96% rename from mcp_server_list_test.go rename to agent/mcp_server_list_test.go index a3954ee..708908c 100644 --- a/mcp_server_list_test.go +++ b/agent/mcp_server_list_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var mcpServersSample = []MCPServer{ @@ -17,7 +19,7 @@ var mcpServersSample = []MCPServer{ func mcpTester(servers []MCPServer, opts MCPServerListOptions, got *[]MCPServerListResult) *ui.Tester { opts.Still = true return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MCPServerList(c, servers, opts) if r.Action != MCPNone { *got = append(*got, r) diff --git a/memory_panel.go b/agent/memory_panel.go similarity index 68% rename from memory_panel.go rename to agent/memory_panel.go index fd275f5..85c5c62 100644 --- a/memory_panel.go +++ b/agent/memory_panel.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MemoryEntry is one memory; the caller persists it. @@ -53,26 +56,26 @@ func MemoryPanel(c *ui.Context, entries *[]MemoryEntry, opts MemoryPanelOptions) if maxH == 0 { maxH = 360 } - u := Density(c).Unit() + u := core.Density(c).Unit() var r MemoryPanelResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(msg(c, "memory_panel.title")) + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(core.Msg(c, "memory_panel.title")) st := ui.Local(r.Element, "mujica.memory", func() memoryPanelState { return memoryPanelState{} }) locked := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "memory_panel.title")).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) - agentCaption(c, fmt.Sprintf(msg(c, "memory_panel.count"), len(*entries))) + ui.Text(c, core.Msg(c, "memory_panel.title")).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + AgentCaption(c, fmt.Sprintf(core.Msg(c, "memory_panel.count"), len(*entries))) }) - SearchInput(c, &st.query, SearchInputOptions{Placeholder: msg(c, "memory_panel.search"), Disabled: locked}) + input.SearchInput(c, &st.query, input.SearchInputOptions{Placeholder: core.Msg(c, "memory_panel.search"), Disabled: locked}) memoryFilter(st, *entries) if len(st.shown) == 0 { - agentCaption(c, msg(c, "memory_panel.empty")) + AgentCaption(c, core.Msg(c, "memory_panel.empty")) return } - agentRows(c, len(st.shown), func(i int) { + AgentRows(c, len(st.shown), func(i int) { idx := st.shown[i] ui.Column(c).Key((*entries)[idx].ID).Children(func() { memoryRow(c, entries, idx, st, locked, &r) }) - }).Shrink(1).MaxHeight(maxH).Dividers(theme.BorderWidth, Tokens(c).Border) + }).Shrink(1).MaxHeight(maxH).Dividers(theme.BorderWidth, core.Tokens(c).Border) }) if r.Deleted != "" { for i, e := range *entries { @@ -117,16 +120,16 @@ func memoryRow(c *ui.Context, entries *[]MemoryEntry, idx int, st *memoryPanelSt if e.ID == "" { panic(fmt.Sprintf("mujica: MemoryPanel entry %d needs an ID", idx)) } - u := Density(c).Unit() + u := core.Density(c).Unit() ui.Column(c).Gap(u).Padding(u*2, 0).Children(func() { if st.editing == e.ID && !locked { - TextArea(c, &st.draft, TextAreaOptions{Label: msg(c, "memory_panel.edit"), MinHeight: 60}).Input.AutoFocus() + input.TextArea(c, &st.draft, input.TextAreaOptions{Label: core.Msg(c, "memory_panel.edit"), MinHeight: 60}).Input.AutoFocus() ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { - if Button(c, msg(c, "memory_panel.cancel"), ButtonOptions{Variant: Secondary}).Clicked() { + if input.Button(c, core.Msg(c, "memory_panel.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { st.editing = "" } blank := strings.TrimSpace(st.draft) == "" - if Button(c, msg(c, "memory_panel.save"), ButtonOptions{Disabled: blank}).Clicked() && !blank { + if input.Button(c, core.Msg(c, "memory_panel.save"), input.ButtonOptions{Disabled: blank}).Clicked() && !blank { e.Text, st.editing, st.key = strings.TrimSpace(st.draft), "", "" r.Edited = e.ID } @@ -135,19 +138,19 @@ func memoryRow(c *ui.Context, entries *[]MemoryEntry, idx int, st *memoryPanelSt } ui.Text(c, e.Text).Selectable() ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - agentCaption(c, e.Source).Grow(1) - if agentToggle(c, msg(c, "memory_panel.edit"), "", locked) { + AgentCaption(c, e.Source).Grow(1) + if AgentToggle(c, core.Msg(c, "memory_panel.edit"), "", locked) { st.editing, st.draft = e.ID, e.Text } del := ui.Row(c) del.Children(func() { - if agentToggle(c, msg(c, "memory_panel.delete"), "trash-2", locked) { + if AgentToggle(c, core.Msg(c, "memory_panel.delete"), "trash-2", locked) { st.confirm = e.ID } }) open := st.confirm == e.ID && !locked - res := Popconfirm(c, del, &open, PopconfirmOptions{Title: msg(c, "memory_panel.confirm"), Description: e.Text, - ConfirmLabel: msg(c, "memory_panel.delete"), Danger: true}) + res := overlay.Popconfirm(c, del, &open, overlay.PopconfirmOptions{Title: core.Msg(c, "memory_panel.confirm"), Description: e.Text, + ConfirmLabel: core.Msg(c, "memory_panel.delete"), Danger: true}) if st.confirm == e.ID && !open { st.confirm = "" } diff --git a/memory_panel_test.go b/agent/memory_panel_test.go similarity index 97% rename from memory_panel_test.go rename to agent/memory_panel_test.go index 3d2db38..f69ee97 100644 --- a/memory_panel_test.go +++ b/agent/memory_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func memorySample() []MemoryEntry { @@ -17,7 +19,7 @@ func memorySample() []MemoryEntry { func memoryTester(entries *[]MemoryEntry, opts MemoryPanelOptions, got *[]MemoryPanelResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MemoryPanel(c, entries, opts) if r.Edited != "" || r.Deleted != "" { *got = append(*got, r) diff --git a/multi_file_diff_review.go b/agent/multi_file_diff_review.go similarity index 68% rename from multi_file_diff_review.go rename to agent/multi_file_diff_review.go index 8d1eaf4..8804094 100644 --- a/multi_file_diff_review.go +++ b/agent/multi_file_diff_review.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MultiFileDiffReviewOptions configure a MultiFileDiffReview. @@ -20,7 +22,7 @@ type MultiFileDiffReviewOptions struct { type multiFileDiffState struct{ selected int } // MultiFileDiffReview reviews many file changes, one diff at a time. -func MultiFileDiffReview(c *ui.Context, decisions *[]FileDecision, files []FileChange, opts MultiFileDiffReviewOptions) ChoiceResult { +func MultiFileDiffReview(c *ui.Context, decisions *[]FileDecision, files []FileChange, opts MultiFileDiffReviewOptions) core.ChoiceResult { if decisions == nil || len(*decisions) != len(files) { panic("mujica: MultiFileDiffReview needs one decision per file") } @@ -40,34 +42,34 @@ func MultiFileDiffReview(c *ui.Context, decisions *[]FileDecision, files []FileC if h == 0 { h = 420 } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = agentCard(c).Height(h).Disabled(opts.Disabled).Label(msg(c, "multi_file_diff_review.title")) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = AgentCard(c).Height(h).Disabled(opts.Disabled).Label(core.Msg(c, "multi_file_diff_review.title")) st := ui.Local(r.Element, "mujica.mfdr", func() multiFileDiffState { return multiFileDiffState{} }) st.selected = max(0, min(st.selected, len(files)-1)) locked := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "multi_file_diff_review.title")).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600) - agentCaption(c, fmt.Sprintf(msg(c, "multi_file_diff_review.progress"), reviewed, len(files))).Grow(1) + ui.Text(c, core.Msg(c, "multi_file_diff_review.title")).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600) + AgentCaption(c, fmt.Sprintf(core.Msg(c, "multi_file_diff_review.progress"), reviewed, len(files))).Grow(1) all := reviewed < len(files) - if Button(c, msg(c, "multi_file_diff_review.accept_all"), ButtonOptions{Variant: Secondary, Disabled: locked || !all}).Clicked() && !locked && all { + if input.Button(c, core.Msg(c, "multi_file_diff_review.accept_all"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked || !all}).Clicked() && !locked && all { for i := range *decisions { if (*decisions)[i] == FilePending { (*decisions)[i] = FileAccepted } } - r.changed = true + r = core.NewChoiceResult(r.Element, true) } }) frac := 0.0 if len(files) > 0 { frac = float64(reviewed) / float64(len(files)) } - agentBar(c, frac, fmt.Sprintf(msg(c, "multi_file_diff_review.progress"), reviewed, len(files))) + agentBar(c, frac, fmt.Sprintf(core.Msg(c, "multi_file_diff_review.progress"), reviewed, len(files))) if len(files) == 0 { - agentCaption(c, msg(c, "multi_file_diff_review.empty")) + AgentCaption(c, core.Msg(c, "multi_file_diff_review.empty")) return } ui.Row(c).Grow(1).Shrink(1).Gap(u * 3).AlignItems(ui.Stretch).Children(func() { @@ -92,12 +94,12 @@ func MultiFileDiffReview(c *ui.Context, decisions *[]FileDecision, files []FileC ui.Column(c).Grow(1).Shrink(1).Gap(u * 2).Children(func() { i := st.selected if fileChangeDecide(c, &(*decisions)[i], files[i], locked) { - r.changed = true + r = core.NewChoiceResult(r.Element, true) } }) }) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: multi_file_diff_review decide", "reviewed", reviewed) } return r @@ -105,9 +107,9 @@ func MultiFileDiffReview(c *ui.Context, decisions *[]FileDecision, files []FileC // multiFileRow is one listed file; it reports a choice. func multiFileRow(c *ui.Context, f FileChange, d FileDecision, selected bool) bool { - k := Tokens(c) - u := Density(c).Unit() - row := ui.ButtonBase(c).Key(f.Path).Justify(ui.Start).Gap(u*2).MinHeight(Density(c).ControlHeight()). + k := core.Tokens(c) + u := core.Density(c).Unit() + row := ui.ButtonBase(c).Key(f.Path).Justify(ui.Start).Gap(u*2).MinHeight(core.Density(c).ControlHeight()). Padding(0, u*2).Radius(theme.ControlRadius).Label(f.Path) internal.Ring(row, k.Focus, theme.ControlRadius) dc := agentDiffOf(row, f) @@ -121,7 +123,7 @@ func multiFileRow(c *ui.Context, f FileChange, d FileDecision, selected bool) bo } row.Children(func() { agentDecisionIcon(c, d) - ui.Text(c, f.Path).Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine().Grow(1).Shrink(1) + ui.Text(c, f.Path).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine().Grow(1).Shrink(1) agentDiffStats(c, dc.add, dc.del) }) return row.Clicked() @@ -129,7 +131,7 @@ func multiFileRow(c *ui.Context, f FileChange, d FileDecision, selected bool) bo // fileChangeDecide shows a full diff and its decision buttons. func fileChangeDecide(c *ui.Context, d *FileDecision, f FileChange, locked bool) bool { - u := Density(c).Unit() + u := core.Density(c).Unit() changed := false holder := ui.Column(c).Key(f.Path).Grow(1).Shrink(1).Gap(u * 2) dc := agentDiffOf(holder, f) @@ -141,15 +143,15 @@ func fileChangeDecide(c *ui.Context, d *FileDecision, f FileChange, locked bool) agentDiffView(c, dc.lines, 1e5).Grow(1).Font(theme.MonoFont) ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { if *d != FilePending { - if Button(c, msg(c, "file_change_card.undo"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.undo"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() && !locked { *d, changed = FilePending, true } return } - if Button(c, msg(c, "file_change_card.reject"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.reject"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() && !locked { *d, changed = FileRejected, true } - if Button(c, msg(c, "file_change_card.accept"), ButtonOptions{Disabled: locked}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "file_change_card.accept"), input.ButtonOptions{Disabled: locked}).Clicked() && !locked { *d, changed = FileAccepted, true } }) diff --git a/multi_file_diff_review_test.go b/agent/multi_file_diff_review_test.go similarity index 97% rename from multi_file_diff_review_test.go rename to agent/multi_file_diff_review_test.go index a8d5455..bacfe3d 100644 --- a/multi_file_diff_review_test.go +++ b/agent/multi_file_diff_review_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func reviewFiles() []FileChange { @@ -17,7 +19,7 @@ func reviewFiles() []FileChange { func reviewTester(d *[]FileDecision, files []FileChange, opts MultiFileDiffReviewOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(8).Children(func() { if MultiFileDiffReview(c, d, files, opts).Changed() { *changes++ diff --git a/permission_prompt.go b/agent/permission_prompt.go similarity index 70% rename from permission_prompt.go rename to agent/permission_prompt.go index ac13a5c..d83b3db 100644 --- a/permission_prompt.go +++ b/agent/permission_prompt.go @@ -1,12 +1,15 @@ -package mujica +package agent import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PermissionChoice answers a PermissionPrompt; zero is no answer. @@ -40,18 +43,18 @@ func PermissionPrompt(c *ui.Context, open *bool, opts PermissionPromptOptions) P } var r PermissionResult was := *open - k := Tokens(c) - u := Density(c).Unit() - r.Element = Dialog(c, open, DialogOptions{ + k := core.Tokens(c) + u := core.Density(c).Unit() + r.Element = overlay.Dialog(c, open, overlay.DialogOptions{ Title: opts.Title, Description: opts.Description, KeepOnBackdrop: true, HideClose: true, Actions: func() { - if Button(c, msg(c, "permission_prompt.deny"), ButtonOptions{Variant: Secondary}).AutoFocus().Clicked() { + if input.Button(c, core.Msg(c, "permission_prompt.deny"), input.ButtonOptions{Variant: input.Secondary}).AutoFocus().Clicked() { r.Choice = PermissionDeny } - if Button(c, msg(c, "permission_prompt.always"), ButtonOptions{Variant: Outline}).Clicked() { + if input.Button(c, core.Msg(c, "permission_prompt.always"), input.ButtonOptions{Variant: input.Outline}).Clicked() { r.Choice = PermissionAllowAlways } - if Button(c, msg(c, "permission_prompt.once"), ButtonOptions{}).Clicked() { + if input.Button(c, core.Msg(c, "permission_prompt.once"), input.ButtonOptions{}).Clicked() { r.Choice = PermissionAllowOnce } }, @@ -60,7 +63,7 @@ func PermissionPrompt(c *ui.Context, open *bool, opts PermissionPromptOptions) P Border(1, k.Border).Children(func() { ui.Icon(c, icons.Must("lock")).Size(16, 16).TextColor(k.Info).Shrink(0).Margin(2, 0, 0, 0) ui.Column(c).Gap(u / 2).Shrink(1).Children(func() { - agentCaption(c, msg(c, "permission_prompt.scope")) + AgentCaption(c, core.Msg(c, "permission_prompt.scope")) ui.Text(c, opts.Scope) }) }) diff --git a/permission_prompt_test.go b/agent/permission_prompt_test.go similarity index 89% rename from permission_prompt_test.go rename to agent/permission_prompt_test.go index bba25c7..03b6105 100644 --- a/permission_prompt_test.go +++ b/agent/permission_prompt_test.go @@ -1,15 +1,18 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func permissionTester(open *bool, got *[]PermissionChoice) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Ask", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Ask", input.ButtonOptions{}).Clicked() { *open = true } r := PermissionPrompt(c, open, PermissionPromptOptions{Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger"}) @@ -64,7 +67,7 @@ func TestPermissionPromptNeedsScope(t *testing.T) { }() open := true ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PermissionPrompt(c, &open, PermissionPromptOptions{Title: "x"}) }, 200, 200) } diff --git a/sandbox_status.go b/agent/sandbox_status.go similarity index 74% rename from sandbox_status.go rename to agent/sandbox_status.go index e9390bd..e38f769 100644 --- a/sandbox_status.go +++ b/agent/sandbox_status.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SandboxState is a sandbox's lifecycle. The zero value is SandboxStarting. @@ -55,10 +56,10 @@ func SandboxStatus(c *ui.Context, info SandboxInfo, opts SandboxStatusOptions) * panic(fmt.Sprintf("mujica: SandboxStatus resource %q needs a name and 0 <= Used <= Limit", r.Name)) } } - k := Tokens(c) - u := Density(c).Unit() - word := msg(c, "sandbox_status."+[...]string{"starting", "running", "stopped", "failed"}[info.State]) - card := agentCard(c).Label(info.Name + ", " + word) + k := core.Tokens(c) + u := core.Density(c).Unit() + word := core.Msg(c, "sandbox_status."+[...]string{"starting", "running", "stopped", "failed"}[info.State]) + card := AgentCard(c).Label(info.Name + ", " + word) card.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, info.Name).FontWeight(600).SingleLine().Grow(1).Shrink(1) @@ -68,21 +69,21 @@ func SandboxStatus(c *ui.Context, info SandboxInfo, opts SandboxStatusOptions) * frac := r.Used / r.Limit ui.Column(c).Gap(u).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - agentCaption(c, r.Name).Grow(1) + AgentCaption(c, r.Name).Grow(1) if frac >= 0.9 { - ui.Icon(c, icons.Must("triangle-alert")).Size(12, 12).TextColor(k.Warning).Label(msg(c, "sandbox_status.high")) + ui.Icon(c, icons.Must("triangle-alert")).Size(12, 12).TextColor(k.Warning).Label(core.Msg(c, "sandbox_status.high")) } - ui.Text(c, fmt.Sprintf("%s / %s %s", sandboxNum(r.Used), sandboxNum(r.Limit), r.Unit)). - FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(k.Text) + ui.Text(c, fmt.Sprintf("%s / %s %s", SandboxNum(r.Used), SandboxNum(r.Limit), r.Unit)). + FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(k.Text) }) agentBar(c, frac, fmt.Sprintf("%s %.0f%%", r.Name, frac*100)) }) } if !info.Expires.IsZero() && info.State != SandboxStopped && info.State != SandboxFailed { left := info.Expires.Sub(c.Now()) - text := msg(c, "sandbox_status.expired") + text := core.Msg(c, "sandbox_status.expired") if left > 0 { - text = fmt.Sprintf(msg(c, "sandbox_status.left"), agentDuration(left.Truncate(time.Second))) + text = fmt.Sprintf(core.Msg(c, "sandbox_status.left"), AgentDuration(left.Truncate(time.Second))) wait := left % time.Second if wait == 0 { wait = time.Second @@ -91,15 +92,15 @@ func SandboxStatus(c *ui.Context, info SandboxInfo, opts SandboxStatusOptions) * } ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Role(ui.RoleStatus).Label(text).Children(func() { ui.Icon(c, icons.Must("clock")).Size(14, 14).TextColor(k.TextMuted) - agentCaption(c, text) + AgentCaption(c, text) }) } }) return card } -// sandboxNum prints a resource amount with at most one decimal. -func sandboxNum(v float64) string { +// SandboxNum prints a resource amount with at most one decimal. +func SandboxNum(v float64) string { if v == math.Trunc(v) { return fmt.Sprintf("%.0f", v) } diff --git a/sandbox_status_test.go b/agent/sandbox_status_test.go similarity index 93% rename from sandbox_status_test.go rename to agent/sandbox_status_test.go index 2a8815b..5aec961 100644 --- a/sandbox_status_test.go +++ b/agent/sandbox_status_test.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "strings" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func sandboxSample(expires time.Time) SandboxInfo { @@ -18,7 +20,7 @@ func sandboxSample(expires time.Time) SandboxInfo { func TestSandboxStatusResourcesAndCountdown(t *testing.T) { info := sandboxSample(time.Now().Add(90 * time.Second)) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SandboxStatus(c, info, SandboxStatusOptions{Still: true}) }, 480, 300) for _, s := range []string{"sandbox-7", "Running", "1.5 / 2 cores", "3.8 / 4 GB", "Near the limit"} { @@ -54,7 +56,7 @@ func TestSandboxStatusRejectsBadResource(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SandboxStatus(c, SandboxInfo{Name: "x", Resources: []SandboxResource{{Name: "CPU", Used: 3, Limit: 2}}}, SandboxStatusOptions{}) }, 200, 200) } diff --git a/screenshot_stream.go b/agent/screenshot_stream.go similarity index 59% rename from screenshot_stream.go rename to agent/screenshot_stream.go index 86df6d9..c98a6bb 100644 --- a/screenshot_stream.go +++ b/agent/screenshot_stream.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ScreenshotFrame is one caller-supplied frame of the agent's screen. @@ -34,7 +36,7 @@ type ScreenshotStreamOptions struct { } // ScreenshotStream shows screen frames live, paused or stepped. -func ScreenshotStream(c *ui.Context, st *ScreenshotStreamState, frames []ScreenshotFrame, opts ScreenshotStreamOptions) ChoiceResult { +func ScreenshotStream(c *ui.Context, st *ScreenshotStreamState, frames []ScreenshotFrame, opts ScreenshotStreamOptions) core.ChoiceResult { if st == nil { panic("mujica: ScreenshotStream needs a state") } @@ -50,8 +52,8 @@ func ScreenshotStream(c *ui.Context, st *ScreenshotStreamState, frames []Screens if h == 0 { h = 280 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() n := len(frames) if st.Index < 0 { panic(fmt.Sprintf("mujica: ScreenshotStream Index %d is negative", st.Index)) @@ -60,24 +62,24 @@ func ScreenshotStream(c *ui.Context, st *ScreenshotStreamState, frames []Screens st.Index = max(0, n-1) } - var r ChoiceResult - r.Element = agentCard(c).Disabled(opts.Disabled).Label(msg(c, "screenshot_stream.label")) + var r core.ChoiceResult + r.Element = AgentCard(c).Disabled(opts.Disabled).Label(core.Msg(c, "screenshot_stream.label")) locked := r.Element.IsDisabled() step := func(d int) { if n == 0 || locked { return } st.Index = max(0, min(n-1, st.Index+d)) - st.Paused, r.changed = true, true + st.Paused, r = true, core.NewChoiceResult(r.Element, true) } r.Element.Children(func() { view := ui.Box(c).FillWidth().Height(h).Center().Clip().Radius(theme.ControlRadius). Background(k.Background).Border(theme.BorderWidth, k.Border).Focusable(). - Label(msg(c, "screenshot_stream.label")) + Label(core.Msg(c, "screenshot_stream.label")) internal.Ring(view, k.Focus, theme.ControlRadius) view.Children(func() { if n == 0 { - agentCaption(c, msg(c, "screenshot_stream.waiting")) + AgentCaption(c, core.Msg(c, "screenshot_stream.waiting")) return } ui.Image(c, frames[st.Index].Image).Fill().Fit(ui.Contain).Label(frames[st.Index].Caption) @@ -89,36 +91,36 @@ func ScreenshotStream(c *ui.Context, st *ScreenshotStreamState, frames []Screens case view.Shortcut(0, ui.KeyRight): step(1) case view.Shortcut(0, ui.KeySpace): - st.Paused, r.changed = !st.Paused, true + st.Paused, r = !st.Paused, core.NewChoiceResult(r.Element, true) } } ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - if iconAction(c, icons.Must("chevron-left"), msg(c, "screenshot_stream.prev")).Disabled(locked || st.Index == 0).Clicked() { + if core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "screenshot_stream.prev")).Disabled(locked || st.Index == 0).Clicked() { step(-1) } - pause := msg(c, "screenshot_stream.pause") + pause := core.Msg(c, "screenshot_stream.pause") if st.Paused { - pause = msg(c, "screenshot_stream.live") + pause = core.Msg(c, "screenshot_stream.live") } - if Button(c, pause, ButtonOptions{Variant: Secondary, Disabled: locked || n == 0}).Clicked() && !locked { - st.Paused, r.changed = !st.Paused, true + if input.Button(c, pause, input.ButtonOptions{Variant: input.Secondary, Disabled: locked || n == 0}).Clicked() && !locked { + st.Paused, r = !st.Paused, core.NewChoiceResult(r.Element, true) } - if iconAction(c, icons.Must("chevron-right"), msg(c, "screenshot_stream.next")).Disabled(locked || st.Index >= n-1).Clicked() { + if core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "screenshot_stream.next")).Disabled(locked || st.Index >= n-1).Clicked() { step(1) } - counter := fmt.Sprintf(msg(c, "screenshot_stream.counter"), min(st.Index+1, n), n) - agentCaption(c, counter).Role(ui.RoleStatus) + counter := fmt.Sprintf(core.Msg(c, "screenshot_stream.counter"), min(st.Index+1, n), n) + AgentCaption(c, counter).Role(ui.RoleStatus) if n > 0 { f := frames[st.Index] ui.Text(c, f.Caption).SingleLine().Grow(1).Shrink(1) - agentCaption(c, agentDuration(f.At)) + AgentCaption(c, AgentDuration(f.At)) } if !st.Paused && n > 0 { - AgentStatus(c, AgentRunning, AgentStatusOptions{Label: msg(c, "screenshot_stream.following"), Still: true}) + AgentStatus(c, AgentRunning, AgentStatusOptions{Label: core.Msg(c, "screenshot_stream.following"), Still: true}) } }) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: screenshot_stream move", "index", st.Index, "paused", st.Paused) } return r diff --git a/screenshot_stream_test.go b/agent/screenshot_stream_test.go similarity index 97% rename from screenshot_stream_test.go rename to agent/screenshot_stream_test.go index bd52c20..75d3efb 100644 --- a/screenshot_stream_test.go +++ b/agent/screenshot_stream_test.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -8,6 +8,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // screenshotFrames makes n solid frames of shifting colors. @@ -26,7 +28,7 @@ func screenshotFrames(n int) []ScreenshotFrame { func screenshotTester(st *ScreenshotStreamState, frames *[]ScreenshotFrame, opts ScreenshotStreamOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ScreenshotStream(c, st, *frames, opts).Changed() { *changes++ } diff --git a/sub_agent_tree.go b/agent/sub_agent_tree.go similarity index 80% rename from sub_agent_tree.go rename to agent/sub_agent_tree.go index c142006..585dc86 100644 --- a/sub_agent_tree.go +++ b/agent/sub_agent_tree.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SubAgent is one agent in a hierarchy. @@ -27,7 +28,7 @@ type SubAgentTreeOptions struct { } // SubAgentTree shows nested agents and their states. -func SubAgentTree(c *ui.Context, roots []SubAgent, opts SubAgentTreeOptions) ChoiceResult { +func SubAgentTree(c *ui.Context, roots []SubAgent, opts SubAgentTreeOptions) core.ChoiceResult { seen := map[string]bool{} var check func([]SubAgent) check = func(list []SubAgent) { @@ -40,7 +41,7 @@ func SubAgentTree(c *ui.Context, roots []SubAgent, opts SubAgentTreeOptions) Cho } } check(roots) - var r ChoiceResult + var r core.ChoiceResult r.Element = ui.Column(c).Shrink(0).FocusGroup(ui.Vertical).Disabled(opts.Disabled) open := ui.Local(r.Element, "mujica.subagents.open", func() map[string]bool { return map[string]bool{} }) r.Element.Children(func() { @@ -49,7 +50,7 @@ func SubAgentTree(c *ui.Context, roots []SubAgent, opts SubAgentTreeOptions) Cho for _, a := range list { if subAgentRow(c, a, depth, (*open)[a.ID], opts.Still) { (*open)[a.ID] = !(*open)[a.ID] - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: sub_agent_tree toggle", "id", a.ID, "open", (*open)[a.ID]) } if (*open)[a.ID] { @@ -69,8 +70,8 @@ func SubAgentTree(c *ui.Context, roots []SubAgent, opts SubAgentTreeOptions) Cho // subAgentRow builds one row, reporting an expand or collapse. func subAgentRow(c *ui.Context, a SubAgent, depth int, open, still bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() parent := len(a.Children) > 0 row := ui.ButtonBase(c).Key(a.ID).Justify(ui.Start).Gap(u*2).MinHeight(d.ControlHeight()). @@ -91,10 +92,10 @@ func subAgentRow(c *ui.Context, a SubAgent, depth int, open, still bool) bool { } agentMark(c, a.State, 16, still) ui.Text(c, a.Name).FontWeight(600).SingleLine().Shrink(0) - ui.Text(c, a.Task).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine().Grow(1).Shrink(1) - agentCaption(c, agentWord(c, a.State)) + ui.Text(c, a.Task).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine().Grow(1).Shrink(1) + AgentCaption(c, agentWord(c, a.State)) if parent { - agentCaption(c, fmt.Sprintf(msg(c, "sub_agent_tree.children"), len(a.Children))) + AgentCaption(c, fmt.Sprintf(core.Msg(c, "sub_agent_tree.children"), len(a.Children))) } }) if !parent || row.IsDisabled() { diff --git a/sub_agent_tree_test.go b/agent/sub_agent_tree_test.go similarity index 95% rename from sub_agent_tree_test.go rename to agent/sub_agent_tree_test.go index 8961923..f10b269 100644 --- a/sub_agent_tree_test.go +++ b/agent/sub_agent_tree_test.go @@ -1,9 +1,11 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var subAgentSample = []SubAgent{ @@ -18,7 +20,7 @@ var subAgentSample = []SubAgent{ func subAgentTester(opts SubAgentTreeOptions, changes *int) *ui.Tester { opts.Still = true return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if SubAgentTree(c, subAgentSample, opts).Changed() { *changes++ } @@ -66,7 +68,7 @@ func TestSubAgentTreeRejectsDuplicateIDs(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SubAgentTree(c, []SubAgent{{ID: "a", Name: "x", Children: []SubAgent{{ID: "a", Name: "y"}}}}, SubAgentTreeOptions{}) }, 200, 200) } diff --git a/tool_approval_dialog.go b/agent/tool_approval_dialog.go similarity index 63% rename from tool_approval_dialog.go rename to agent/tool_approval_dialog.go index 7509c91..759b012 100644 --- a/tool_approval_dialog.go +++ b/agent/tool_approval_dialog.go @@ -1,11 +1,14 @@ -package mujica +package agent import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolApprovalOptions configure a ToolApprovalDialog. Tool is required. @@ -31,26 +34,26 @@ func ToolApprovalDialog(c *ui.Context, open *bool, opts ToolApprovalOptions) Too } var r ToolApprovalResult was := *open - u := Density(c).Unit() - r.Element = Dialog(c, open, DialogOptions{ - Title: msg(c, "tool_approval_dialog.title"), Description: opts.Reason, KeepOnBackdrop: true, HideClose: true, + u := core.Density(c).Unit() + r.Element = overlay.Dialog(c, open, overlay.DialogOptions{ + Title: core.Msg(c, "tool_approval_dialog.title"), Description: opts.Reason, KeepOnBackdrop: true, HideClose: true, Actions: func() { // Deny first: a reflexive Enter never runs the tool. - if Button(c, msg(c, "tool_approval_dialog.deny"), ButtonOptions{Variant: Secondary}).AutoFocus().Clicked() { + if input.Button(c, core.Msg(c, "tool_approval_dialog.deny"), input.ButtonOptions{Variant: input.Secondary}).AutoFocus().Clicked() { r.Denied = true } - if Button(c, msg(c, "tool_approval_dialog.allow"), ButtonOptions{}).Clicked() { + if input.Button(c, core.Msg(c, "tool_approval_dialog.allow"), input.ButtonOptions{}).Clicked() { r.Allowed = true } }, }, func() { ui.Column(c).Gap(u * 2).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - agentCaption(c, msg(c, "tool_approval_dialog.tool")) + AgentCaption(c, core.Msg(c, "tool_approval_dialog.tool")) ui.Text(c, opts.Tool).Font(theme.MonoFont).FontWeight(600) }) if opts.Args != "" { - agentCode(c, "json", agentPrettyJSON(opts.Args), 260) + AgentCode(c, "json", agentPrettyJSON(opts.Args), 260) } }) }) diff --git a/tool_approval_dialog_test.go b/agent/tool_approval_dialog_test.go similarity index 91% rename from tool_approval_dialog_test.go rename to agent/tool_approval_dialog_test.go index 331255e..95c16e6 100644 --- a/tool_approval_dialog_test.go +++ b/agent/tool_approval_dialog_test.go @@ -1,9 +1,12 @@ -package mujica +package agent import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type approvalApp struct { @@ -12,8 +15,8 @@ type approvalApp struct { } func (a *approvalApp) view(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Run", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Run", input.ButtonOptions{}).Clicked() { a.open = true } r := ToolApprovalDialog(c, &a.open, ToolApprovalOptions{Tool: "delete_files", Args: `{"glob":"*.tmp"}`, Reason: "Clean the build folder."}) @@ -83,7 +86,7 @@ func TestToolApprovalDialogNeedsTool(t *testing.T) { }() open := true ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolApprovalDialog(c, &open, ToolApprovalOptions{}) }, 200, 200) } diff --git a/tool_call_card.go b/agent/tool_call_card.go similarity index 62% rename from tool_call_card.go rename to agent/tool_call_card.go index 164e6ec..769fe49 100644 --- a/tool_call_card.go +++ b/agent/tool_call_card.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolCall is one tool invocation; the caller owns it. @@ -33,14 +35,14 @@ type ToolCallCardOptions struct { type toolCallCardState struct{ args, result bool } // ToolCallCard shows a tool call with foldable arguments and result. -func ToolCallCard(c *ui.Context, call ToolCall, opts ToolCallCardOptions) ChoiceResult { +func ToolCallCard(c *ui.Context, call ToolCall, opts ToolCallCardOptions) core.ChoiceResult { if call.Name == "" { panic("mujica: ToolCallCard needs a Name") } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = agentCard(c).Role(ui.RoleGroup).Label(call.Name + ", " + agentWord(c, call.State)) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = AgentCard(c).Role(ui.RoleGroup).Label(call.Name + ", " + agentWord(c, call.State)) st := ui.Local(r.Element, "mujica.toolcall", func() toolCallCardState { return toolCallCardState{opts.OpenArgs, opts.OpenResult} }) @@ -48,31 +50,31 @@ func ToolCallCard(c *ui.Context, call ToolCall, opts ToolCallCardOptions) Choice ui.Row(c).Gap(u * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Children(func() { agentMark(c, call.State, 16, opts.Still) ui.Text(c, call.Name).Font(theme.MonoFont).FontWeight(600).SingleLine().Grow(1).Shrink(1) - agentCaption(c, agentWord(c, call.State)) + AgentCaption(c, agentWord(c, call.State)) if call.State == AgentDone || call.State == AgentFailed { - agentCaption(c, agentDuration(call.Duration)) + AgentCaption(c, AgentDuration(call.Duration)) } }) if call.Args != "" { - r.changed = Collapsible(c, &st.args, CollapsibleOptions{Title: msg(c, "tool_call_card.args")}, func() { - agentCode(c, "json", agentPrettyJSON(call.Args), 240) - }).Changed() || r.changed + r = core.NewChoiceResult(r.Element, data.Collapsible(c, &st.args, data.CollapsibleOptions{Title: core.Msg(c, "tool_call_card.args")}, func() { + AgentCode(c, "json", agentPrettyJSON(call.Args), 240) + }).Changed() || r.Changed()) } - body, title := call.Result, msg(c, "tool_call_card.result") + body, title := call.Result, core.Msg(c, "tool_call_card.result") if call.Error != "" { - body, title = call.Error, msg(c, "tool_call_card.error") + body, title = call.Error, core.Msg(c, "tool_call_card.error") } if body != "" { - r.changed = Collapsible(c, &st.result, CollapsibleOptions{Title: title}, func() { + r = core.NewChoiceResult(r.Element, data.Collapsible(c, &st.result, data.CollapsibleOptions{Title: title}, func() { if call.Error != "" { ui.Text(c, call.Error).TextColor(k.Danger).Selectable() return } - agentCode(c, "json", agentPrettyJSON(body), 320) - }).Changed() || r.changed + AgentCode(c, "json", agentPrettyJSON(body), 320) + }).Changed() || r.Changed()) } }) - if r.changed { + if r.Changed() { slog.Debug("mujica: tool_call_card toggle", "tool", call.Name, "args", st.args, "result", st.result) } return r diff --git a/tool_call_card_test.go b/agent/tool_call_card_test.go similarity index 93% rename from tool_call_card_test.go rename to agent/tool_call_card_test.go index 770a4c1..d2862ca 100644 --- a/tool_call_card_test.go +++ b/agent/tool_call_card_test.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var toolCallSample = ToolCall{ID: "1", Name: "read_file", Args: `{"path":"court/ledger.md","limit":40}`, @@ -16,7 +18,7 @@ func TestToolCallCardShowsAndFolds(t *testing.T) { call := toolCallSample changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ToolCallCard(c, call, ToolCallCardOptions{}).Changed() { changes++ } @@ -43,7 +45,7 @@ func TestToolCallCardShowsAndFolds(t *testing.T) { func TestToolCallCardErrorAndPlainText(t *testing.T) { call := ToolCall{Name: "shell", Args: "not json at all", Error: "permission denied", State: AgentFailed, Duration: 3 * time.Second} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallCard(c, call, ToolCallCardOptions{OpenArgs: true, OpenResult: true}) }, 500, 400) for _, s := range []string{"Failed", "3.0 s", "Error", "permission denied", "not json at all"} { @@ -56,7 +58,7 @@ func TestToolCallCardErrorAndPlainText(t *testing.T) { func TestToolCallCardRunningHidesDuration(t *testing.T) { call := ToolCall{Name: "search", State: AgentRunning, Duration: time.Second} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallCard(c, call, ToolCallCardOptions{Still: true}) }, 400, 200) if !tt.HasText("Running") || tt.HasText("1.0 s") { @@ -71,7 +73,7 @@ func TestToolCallCardLargeResultIsVirtualized(t *testing.T) { } call := ToolCall{Name: "dump", Result: b.String(), State: AgentDone} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallCard(c, call, ToolCallCardOptions{OpenResult: true}) }, 500, 500) if n := len(tt.Texts()); n > 200 { @@ -89,7 +91,7 @@ func TestToolCallCardNeedsName(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallCard(c, ToolCall{}, ToolCallCardOptions{}) }, 200, 200) } diff --git a/tool_call_group.go b/agent/tool_call_group.go similarity index 65% rename from tool_call_group.go rename to agent/tool_call_group.go index dbe5a61..74afd5e 100644 --- a/tool_call_group.go +++ b/agent/tool_call_group.go @@ -1,4 +1,4 @@ -package mujica +package agent import ( "fmt" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolCallGroupOptions configure a ToolCallGroup. @@ -17,7 +19,7 @@ type ToolCallGroupOptions struct { } // ToolCallGroup folds tool calls under a count, time, failures summary. -func ToolCallGroup(c *ui.Context, open *bool, calls []ToolCall, opts ToolCallGroupOptions) ChoiceResult { +func ToolCallGroup(c *ui.Context, open *bool, calls []ToolCall, opts ToolCallGroupOptions) core.ChoiceResult { if open == nil { panic("mujica: ToolCallGroup needs open") } @@ -33,11 +35,11 @@ func ToolCallGroup(c *ui.Context, open *bool, calls []ToolCall, opts ToolCallGro failed += agentB2I(call.State == AgentFailed) running = running || call.State == AgentRunning } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() title := opts.Title if title == "" { - title = fmt.Sprintf(msg(c, "tool_call_group.count"), len(calls)) + title = fmt.Sprintf(core.Msg(c, "tool_call_group.count"), len(calls)) } state := AgentDone switch { @@ -46,18 +48,18 @@ func ToolCallGroup(c *ui.Context, open *bool, calls []ToolCall, opts ToolCallGro case failed > 0: state = AgentFailed } - summary := title + " · " + agentDuration(total) + summary := title + " · " + AgentDuration(total) if failed > 0 { - summary += " · " + fmt.Sprintf(msg(c, "tool_call_group.failed"), failed) + summary += " · " + fmt.Sprintf(core.Msg(c, "tool_call_group.failed"), failed) } - return Collapsible(c, open, CollapsibleOptions{Title: summary, Trigger: func() { + return data.Collapsible(c, open, data.CollapsibleOptions{Title: summary, Trigger: func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { agentMark(c, state, 16, opts.Still) ui.Text(c, title).FontWeight(600).SingleLine() - agentCaption(c, agentDuration(total)) + AgentCaption(c, AgentDuration(total)) if failed > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "tool_call_group.failed"), failed)).TextColor(k.Danger). - FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "tool_call_group.failed"), failed)).TextColor(k.Danger). + FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine() } }) }}, func() { diff --git a/tool_call_group_test.go b/agent/tool_call_group_test.go similarity index 93% rename from tool_call_group_test.go rename to agent/tool_call_group_test.go index 2e96b31..7f2c737 100644 --- a/tool_call_group_test.go +++ b/agent/tool_call_group_test.go @@ -1,10 +1,12 @@ -package mujica +package agent import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var toolCallGroupSample = []ToolCall{ @@ -16,7 +18,7 @@ var toolCallGroupSample = []ToolCall{ func TestToolCallGroupSummaryAndFold(t *testing.T) { open, changes := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ToolCallGroup(c, &open, toolCallGroupSample, ToolCallGroupOptions{}).Changed() { changes++ } @@ -40,7 +42,7 @@ func TestToolCallGroupRunningAndEmpty(t *testing.T) { open := false calls := []ToolCall{{ID: "x", Name: "fetch", State: AgentRunning}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallGroup(c, &open, calls, ToolCallGroupOptions{Title: "Research", Still: true}) ToolCallGroup(c, &open, nil, ToolCallGroupOptions{}) }, 500, 300) @@ -57,7 +59,7 @@ func TestToolCallGroupRejectsDuplicateIDs(t *testing.T) { }() open := false ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolCallGroup(c, &open, []ToolCall{{ID: "a", Name: "x"}, {ID: "a", Name: "y"}}, ToolCallGroupOptions{}) }, 200, 200) } diff --git a/area_chart.go b/chart/area_chart.go similarity index 83% rename from area_chart.go rename to chart/area_chart.go index 739085d..bc54289 100644 --- a/area_chart.go +++ b/chart/area_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -24,15 +24,15 @@ type AreaChartOptions struct { // AreaChart fills series to the baseline, or stacked. func AreaChart(c *ui.Context, series []ChartSeries, opts AreaChartOptions) ChartResult { n := chartCheckSeries(series) - ch := newChart(c, opts.Frame) - ax := chartMemo(ch, "x", [3]string{chartID(opts.X), chartID(opts.Times), chartID(series)}, func() chartX { return newChartX(n, opts.X, opts.Times) }) + ch := NewChart(c, opts.Frame) + ax := ChartMemo(ch, "x", [3]string{ChartID(opts.X), ChartID(opts.Times), ChartID(series)}, func() ChartX { return NewChartX(n, opts.X, opts.Times) }) deps := struct { id string stacked bool base float64 }{chartSeriesID(ch, series), opts.Stacked, opts.Baseline} - bands := chartMemo(ch, "bands", deps, func() [2][][]float64 { return areaBands(series, ch.hidden, opts.Stacked, opts.Baseline) }) - lo, hi, ok := chartYRange(ch, series, false, bands[0]) + bands := ChartMemo(ch, "bands", deps, func() [2][][]float64 { return areaBands(series, ch.Hidden, opts.Stacked, opts.Baseline) }) + lo, hi, ok := ChartYRange(ch, series, false, bands[0]) if !ok { n = 0 } diff --git a/area_chart_test.go b/chart/area_chart_test.go similarity index 99% rename from area_chart_test.go rename to chart/area_chart_test.go index 04db044..39cdaa5 100644 --- a/area_chart_test.go +++ b/chart/area_chart_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" diff --git a/bar_chart.go b/chart/bar_chart.go similarity index 84% rename from bar_chart.go rename to chart/bar_chart.go index e8aaabd..fe438d0 100644 --- a/bar_chart.go +++ b/chart/bar_chart.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BarChartOptions configure a BarChart. @@ -28,15 +29,15 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar if n := chartCheckSeries(series); n != len(categories) && len(series) > 0 { panic("mujica: BarChart series length differs from categories") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) keys := make([]ChartLegendItem, len(series)) var vis []int for i, s := range series { keys[i] = ChartLegendItem{Name: s.Name, Color: pal[i%len(pal)], Shape: i} - if !ch.hidden(s.Name) { + if !ch.Hidden(s.Name) { vis = append(vis, i) } } @@ -46,7 +47,7 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar from []float64 lo, hi float64 } - lay := chartMemo(ch, "bars", [2]any{chartSeriesID(ch, series), opts.Stacked}, func() layout { + lay := ChartMemo(ch, "bars", [2]any{chartSeriesID(ch, series), opts.Stacked}, func() layout { var l layout for ci := range categories { pos, neg := 0.0, 0.0 @@ -81,11 +82,11 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar } geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear) { if opts.Horizontal { - plot := chartInset(r, 96, 26) + plot := ChartInset(r, 96, 26) return plot, internal.Band{N: len(categories), R0: plot.Y, R1: plot.Y + plot.H, Pad: 0.3}, internal.Linear{D0: lo, D1: hi, R0: float64(plot.X), R1: float64(plot.X + plot.W)} } - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) return plot, internal.Band{N: len(categories), R0: plot.X, R1: plot.X + plot.W, Pad: 0.3}, internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -111,10 +112,10 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar return ui.Rect{X: at, Y: a, W: w, H: max(b-a, 1)} } value := func(i int) float64 { m := lay.marks[i]; return series[m.series].Values[m.cat] } - spec := ChartSpec{Marks: len(lay.marks), Legend: keys, Hidden: ch.toggles(len(series) > 1)} + spec := ChartSpec{Marks: len(lay.marks), Legend: keys, Hidden: ch.Toggles(len(series) > 1)} spec.Readout = func(i int) string { m := lay.marks[i] - return categories[m.cat] + " · " + series[m.series].Name + " " + chartValue(c, value(i), opts.Format) + return categories[m.cat] + " · " + series[m.series].Name + " " + ChartValue(c, value(i), opts.Format) } spec.Pick = func(r ui.Rect, x, y float32) int { _, band, vs := geom(r) @@ -139,10 +140,10 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar for _, t := range ticks { x := internal.Snap(p, vs.Map(t)) p.Line(x, plot.Y, x, plot.Y+plot.H, 1, k.Border) - chartText(p, label(t), x, plot.Y+plot.H+6, size, k.TextMuted, 0.5, 0) + ChartText(p, label(t), x, plot.Y+plot.H+6, size, k.TextMuted, 0.5, 0) } for i, y := range pos { - chartText(p, categories[i], plot.X-8, y, size, k.TextMuted, 1, 0.5) + ChartText(p, categories[i], plot.X-8, y, size, k.TextMuted, 1, 0.5) } p.Line(internal.Snap(p, zero), plot.Y, internal.Snap(p, zero), plot.Y+plot.H, 1, k.ControlBorder) } else { @@ -162,11 +163,11 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar internal.Marker(p, m.series, b.X+b.W/2, b.Y+b.H/2, min(b.W, b.H, 14)*0.6, k.Surface) } if opts.Labels && !opts.Stacked { - barLabel(p, chartValue(c, value(i), opts.Format), b, value(i) < 0, opts.Horizontal, size, k.Text) + barLabel(p, ChartValue(c, value(i), opts.Format), b, value(i) < 0, opts.Horizontal, size, k.Text) } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } @@ -174,12 +175,12 @@ func BarChart(c *ui.Context, categories []string, series []ChartSeries, opts Bar func barLabel(p *ui.Painter, s string, b ui.Rect, neg, horizontal bool, size float32, col ui.Color) { switch { case horizontal && neg: - chartText(p, s, b.X-4, b.Y+b.H/2, size, col, 1, 0.5) + ChartText(p, s, b.X-4, b.Y+b.H/2, size, col, 1, 0.5) case horizontal: - chartText(p, s, b.X+b.W+4, b.Y+b.H/2, size, col, 0, 0.5) + ChartText(p, s, b.X+b.W+4, b.Y+b.H/2, size, col, 0, 0.5) case neg: - chartText(p, s, b.X+b.W/2, b.Y+b.H+2, size, col, 0.5, 0) + ChartText(p, s, b.X+b.W/2, b.Y+b.H+2, size, col, 0.5, 0) default: - chartText(p, s, b.X+b.W/2, b.Y-2, size, col, 0.5, 1) + ChartText(p, s, b.X+b.W/2, b.Y-2, size, col, 0.5, 1) } } diff --git a/bar_chart_test.go b/chart/bar_chart_test.go similarity index 96% rename from bar_chart_test.go rename to chart/bar_chart_test.go index dfde4b0..e78de24 100644 --- a/bar_chart_test.go +++ b/chart/bar_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var barCats = []string{"Nave", "Choir", "Crypt"} @@ -71,7 +73,7 @@ func TestBarChartBadAndEmpty(t *testing.T) { t.Fatalf("empty: %q", tt.Texts()) } ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BarChart(c, barCats[:2], barSeries(), BarChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/box_plot.go b/chart/box_plot.go similarity index 85% rename from box_plot.go rename to chart/box_plot.go index 931bc50..c4e30cc 100644 --- a/box_plot.go +++ b/chart/box_plot.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BoxGroup is one named sample of a BoxPlot. @@ -65,18 +66,18 @@ func boxSummary(values []float64) boxStats { // BoxPlot compares groups by quartile box, whiskers and outlier points. func BoxPlot(c *ui.Context, groups []BoxGroup, opts BoxPlotOptions) ChartResult { - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) ids := "" for _, g := range groups { if g.Name == "" { panic("mujica: BoxPlot groups need names") } - ids += chartID(g.Values) + ";" + ids += ChartID(g.Values) + ";" } - stats := chartMemo(ch, "stats", ids, func() []boxStats { + stats := ChartMemo(ch, "stats", ids, func() []boxStats { out := make([]boxStats, len(groups)) for i, g := range groups { out[i] = boxSummary(g.Values) @@ -96,9 +97,9 @@ func BoxPlot(c *ui.Context, groups []BoxGroup, opts BoxPlotOptions) ChartResult } else { groups = nil } - val := func(v float64) string { return chartValue(c, v, opts.Format) } + val := func(v float64) string { return ChartValue(c, v, opts.Format) } geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear) { - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) return plot, internal.Band{N: len(groups), R0: plot.X, R1: plot.X + plot.W, Pad: 0.45}, internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -106,9 +107,9 @@ func BoxPlot(c *ui.Context, groups []BoxGroup, opts BoxPlotOptions) ChartResult spec.Readout = func(i int) string { s := stats[i] if s.n == 0 { - return groups[i].Name + " · " + msg(c, "chart.missing") + return groups[i].Name + " · " + core.Msg(c, "chart.missing") } - return fmt.Sprintf(msg(c, "box_plot.readout"), groups[i].Name, val(s.median), val(s.q1), val(s.q3), val(s.lo), val(s.hi), len(s.outliers)) + return fmt.Sprintf(core.Msg(c, "box_plot.readout"), groups[i].Name, val(s.median), val(s.q1), val(s.q3), val(s.lo), val(s.hi), len(s.outliers)) } spec.Pick = func(r ui.Rect, x, y float32) int { plot, band, _ := geom(r) @@ -155,6 +156,6 @@ func BoxPlot(c *ui.Context, groups []BoxGroup, opts BoxPlotOptions) ChartResult } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/box_plot_test.go b/chart/box_plot_test.go similarity index 99% rename from box_plot_test.go rename to chart/box_plot_test.go index 34b3bfd..052d93b 100644 --- a/box_plot_test.go +++ b/chart/box_plot_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" diff --git a/bullet_chart.go b/chart/bullet_chart.go similarity index 85% rename from bullet_chart.go rename to chart/bullet_chart.go index be84ad5..e226680 100644 --- a/bullet_chart.go +++ b/chart/bullet_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BulletChartOptions configure a BulletChart; Frame.Height 0 means 72 DIPs. @@ -32,9 +33,9 @@ func BulletChart(c *ui.Context, value float64, opts BulletChartOptions) ChartRes if opts.Frame.Height == 0 { opts.Frame.Height = 72 } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) top := opts.Max if top == 0 { top = value @@ -47,17 +48,17 @@ func BulletChart(c *ui.Context, value float64, opts BulletChartOptions) ChartRes _, top, _ = internal.Nice(0, max(top, 1e-9), 4) } _, _, step := internal.Nice(0, top, 4) - val := func(v float64) string { return chartValue(c, v, opts.Format) } - marks := []string{msg(c, "bullet_chart.actual") + " " + val(value)} + val := func(v float64) string { return ChartValue(c, v, opts.Format) } + marks := []string{core.Msg(c, "bullet_chart.actual") + " " + val(value)} if opts.Target != nil { - marks = append(marks, msg(c, "bullet_chart.target")+" "+val(*opts.Target)) + marks = append(marks, core.Msg(c, "bullet_chart.target")+" "+val(*opts.Target)) } for i, b := range opts.Bands { lo := 0.0 if i > 0 { lo = opts.Bands[i-1] } - marks = append(marks, fmt.Sprintf(msg(c, "bullet_chart.band"), i+1, val(lo), val(b))) + marks = append(marks, fmt.Sprintf(core.Msg(c, "bullet_chart.band"), i+1, val(lo), val(b))) } // geom returns the plot and its value scale. geom := func(r ui.Rect) (ui.Rect, internal.Linear) { @@ -130,13 +131,13 @@ func BulletChart(c *ui.Context, value float64, opts BulletChartOptions) ChartRes } for _, tk := range internal.Ticks(0, top, step) { if opts.Vertical { - chartText(p, internal.FormatTick(tk, step), plot.X+plot.W+6, s.Map(tk), size, k.TextMuted, 0, 0.5) + ChartText(p, internal.FormatTick(tk, step), plot.X+plot.W+6, s.Map(tk), size, k.TextMuted, 0, 0.5) } else { - chartText(p, internal.FormatTick(tk, step), s.Map(tk), plot.Y+plot.H+4, size, k.TextMuted, 0.5, 0) + ChartText(p, internal.FormatTick(tk, step), s.Map(tk), plot.Y+plot.H+4, size, k.TextMuted, 0.5, 0) } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/bullet_chart_test.go b/chart/bullet_chart_test.go similarity index 96% rename from bullet_chart_test.go rename to chart/bullet_chart_test.go index 1d8ba54..dd2b2f5 100644 --- a/bullet_chart_test.go +++ b/chart/bullet_chart_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBulletChartReadout(t *testing.T) { @@ -49,7 +51,7 @@ func TestBulletChartVerticalAndBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BulletChart(c, 1, BulletChartOptions{Frame: ChartFrame{Label: "x"}, Bands: []float64{5, 2}}) }, 300, 300) } diff --git a/calendar_heatmap.go b/chart/calendar_heatmap.go similarity index 66% rename from calendar_heatmap.go rename to chart/calendar_heatmap.go index 6415748..f9982c3 100644 --- a/calendar_heatmap.go +++ b/chart/calendar_heatmap.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "log/slog" @@ -7,31 +7,33 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CalendarHeatmapOptions configure a CalendarHeatmap. type CalendarHeatmapOptions struct { Frame ChartFrame // WeekStart picks each column's first weekday; zero follows the locale. - WeekStart WeekStart + WeekStart datetime.WeekStart Format func(v float64) string } // CalendarHeatmap colors each day of *year, a column per week. -func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts CalendarHeatmapOptions) ChartResult { +func CalendarHeatmap(c *ui.Context, year *int, values map[core.Date]float64, opts CalendarHeatmapOptions) ChartResult { if *year < 1 || *year > 9999 { panic("mujica: CalendarHeatmap year out of range") } - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) - first := Date{Year: *year, Month: time.January, Day: 1} - days := first.AddMonths(12).civil().Sub(first.civil()).Hours() / 24 + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) + first := core.Date{Year: *year, Month: time.January, Day: 1} + days := first.AddMonths(12).Civil().Sub(first.Civil()).Hours() / 24 n := int(days) - lead := (int(first.Weekday()) - int(calendarFirstWeekday(c, opts.WeekStart)) + 7) % 7 + lead := (int(first.Weekday()) - int(datetime.CalendarFirstWeekday(c, opts.WeekStart)) + 7) % 7 weeks := (lead + n + 6) / 7 top := 0.0 for i := range n { @@ -51,9 +53,9 @@ func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts Cal wrap := ui.Column(c).Gap(u * 2).MinWidth(0) wrap.Children(func() { ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { - prev := iconAction(c, icons.Must("chevron-left"), msg(c, "calendar_heatmap.prev")).Disabled(*year <= 1) - ui.Text(c, strconv.Itoa(*year)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).Width(64).TextAlign(ui.Center) - next := iconAction(c, icons.Must("chevron-right"), msg(c, "calendar_heatmap.next")).Disabled(*year >= 9999) + prev := core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "calendar_heatmap.prev")).Disabled(*year <= 1) + ui.Text(c, strconv.Itoa(*year)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).Width(64).TextAlign(ui.Center) + next := core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "calendar_heatmap.next")).Disabled(*year >= 9999) if prev.Clicked() && !prev.IsDisabled() { *year-- res.value = true @@ -63,7 +65,7 @@ func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts Cal res.value = true } }) - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) geom := func(r ui.Rect) (x, y, step float32) { step = min((r.W-36)/float32(weeks), (r.H-48)/7) return r.X + 32, r.Y + 18, step @@ -79,9 +81,9 @@ func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts Cal v, ok := values[d] s := d.String() + " · " if !ok { - return s + msg(c, "chart.missing") + return s + core.Msg(c, "chart.missing") } - return s + chartValue(c, v, opts.Format) + return s + ChartValue(c, v, opts.Format) } spec.Move = func(i int, key ui.Key) int { step := map[ui.Key]int{ui.KeyLeft: -7, ui.KeyRight: 7, ui.KeyUp: -1, ui.KeyDown: 1}[key] @@ -106,18 +108,18 @@ func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts Cal p.Fill(b, internal.Ramp(k, float64(lv)/4), 2) } if d := first.AddDays(i); d.Day == 1 { - chartText(p, d.Format("Jan"), b.X, y0-4, size, k.TextMuted, 0, 1) + ChartText(p, d.Format("Jan"), b.X, y0-4, size, k.TextMuted, 0, 1) } if i == active { p.Stroke(ui.Rect{X: b.X - 2, Y: b.Y - 2, W: b.W + 4, H: b.H + 4}, k.Text, 3, 2) } } for row := 0; row < 7; row += 2 { - wd := time.Weekday((int(calendarFirstWeekday(c, opts.WeekStart)) + row) % 7) - chartText(p, wd.String()[:3], x0-4, y0+float32(row)*step+step/2, size, k.TextMuted, 1, 0.5) + wd := time.Weekday((int(datetime.CalendarFirstWeekday(c, opts.WeekStart)) + row) % 7) + ChartText(p, wd.String()[:3], x0-4, y0+float32(row)*step+step/2, size, k.TextMuted, 1, 0.5) } ly := y0 + 7*step + 8 - w, _ := chartText(p, msg(c, "calendar_heatmap.less"), x0, ly, size, k.TextMuted, 0, 0) + w, _ := ChartText(p, core.Msg(c, "calendar_heatmap.less"), x0, ly, size, k.TextMuted, 0, 0) for lv := range 5 { sw := ui.Rect{X: x0 + w + 6 + float32(lv)*(step+2), Y: ly + 2, W: step - 2, H: step - 2} if lv == 0 { @@ -126,9 +128,9 @@ func CalendarHeatmap(c *ui.Context, year *int, values map[Date]float64, opts Cal p.Fill(sw, internal.Ramp(k, float64(lv)/4), 2) } } - chartText(p, msg(c, "calendar_heatmap.more"), x0+w+12+5*(step+2), ly, size, k.TextMuted, 0, 0) + ChartText(p, core.Msg(c, "calendar_heatmap.more"), x0+w+12+5*(step+2), ly, size, k.TextMuted, 0, 0) } - res2, _ := ch.build(spec) + res2, _ := ch.Build(spec) res.Point, res.changed, res.retry = res2.Point, res2.changed, res2.retry }) if res.value { diff --git a/calendar_heatmap_test.go b/chart/calendar_heatmap_test.go similarity index 85% rename from calendar_heatmap_test.go rename to chart/calendar_heatmap_test.go index 6c417e2..043962b 100644 --- a/calendar_heatmap_test.go +++ b/chart/calendar_heatmap_test.go @@ -1,15 +1,18 @@ -package mujica +package chart import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) -func calendarValues() map[Date]float64 { - m := map[Date]float64{} - d := Date{Year: 2026, Month: time.January, Day: 1} +func calendarValues() map[core.Date]float64 { + m := map[core.Date]float64{} + d := core.Date{Year: 2026, Month: time.January, Day: 1} for i := range 365 { if i%3 == 0 { m[d.AddDays(i)] = float64(i % 11) @@ -21,7 +24,7 @@ func calendarValues() map[Date]float64 { func TestCalendarHeatmapKeysAndYear(t *testing.T) { year := 2026 values := 0 - start := WeekStartMonday + start := datetime.WeekStartMonday h := &chartHarness{build: func(c *ui.Context) ChartResult { r := CalendarHeatmap(c, &year, calendarValues(), CalendarHeatmapOptions{Frame: ChartFrame{Label: "Prayers"}, WeekStart: start}) if r.ValueChanged() { @@ -55,9 +58,9 @@ func TestCalendarHeatmapKeysAndYear(t *testing.T) { func TestCalendarHeatmapWeekStartHover(t *testing.T) { year := 2026 // 1 January 2026 is a Thursday for _, tc := range []struct { - start WeekStart + start datetime.WeekStart row int - }{{WeekStartSunday, 4}, {WeekStartMonday, 3}} { + }{{datetime.WeekStartSunday, 4}, {datetime.WeekStartMonday, 3}} { h := &chartHarness{build: func(c *ui.Context) ChartResult { return CalendarHeatmap(c, &year, calendarValues(), CalendarHeatmapOptions{Frame: ChartFrame{Label: "Prayers"}, WeekStart: tc.start}) }} @@ -80,7 +83,7 @@ func TestCalendarHeatmapBadYear(t *testing.T) { }() year := 0 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CalendarHeatmap(c, &year, nil, CalendarHeatmapOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } @@ -89,7 +92,7 @@ func TestCalendarHeatmapSnapshot(t *testing.T) { snapshot(t, "calendar_heatmap", 600, 260, func(c *ui.Context) { year := 2026 ui.Column(c).Padding(16).Children(func() { - CalendarHeatmap(c, &year, calendarValues(), CalendarHeatmapOptions{Frame: ChartFrame{Label: "Prayers", Height: 150}, WeekStart: WeekStartMonday}) + CalendarHeatmap(c, &year, calendarValues(), CalendarHeatmapOptions{Frame: ChartFrame{Label: "Prayers", Height: 150}, WeekStart: datetime.WeekStartMonday}) }) }) } diff --git a/candle_countdown.go b/chart/candle_countdown.go similarity index 77% rename from candle_countdown.go rename to chart/candle_countdown.go index e4561c5..3c088cb 100644 --- a/candle_countdown.go +++ b/chart/candle_countdown.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CandleCountdownOptions configure a CandleCountdown. @@ -28,11 +29,11 @@ func CandleCountdown(c *ui.Context, interval Interval, opts CandleCountdownOptio now = c.Now() } left := interval.Close(now, loc).Sub(now) - k := Tokens(c) + k := core.Tokens(c) text := candleCountdownText(left) - e := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Role(ui.RoleText).Label(fmt.Sprintf(msg(c, "candle_countdown.label"), string(interval), text)) + e := ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Role(ui.RoleText).Label(fmt.Sprintf(core.Msg(c, "candle_countdown.label"), string(interval), text)) e.Children(func() { - ui.Text(c, string(interval)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, string(interval)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) ui.Text(c, text).FontFeatures("tnum").FontWeight(500).TextColor(k.Text).SingleLine() }) // The next whole second, so the text never skips one. diff --git a/candle_countdown_test.go b/chart/candle_countdown_test.go similarity index 83% rename from candle_countdown_test.go rename to chart/candle_countdown_test.go index 2b42c7b..88cb213 100644 --- a/candle_countdown_test.go +++ b/chart/candle_countdown_test.go @@ -1,15 +1,21 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCandleCountdownIntervalsAndClock(t *testing.T) { now := time.Date(2026, 10, 5, 12, 34, 5, 0, time.UTC) iv := Interval("1m") - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); CandleCountdown(c, iv, CandleCountdownOptions{Now: now}) }, 300, 100) + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + CandleCountdown(c, iv, CandleCountdownOptions{Now: now}) + }, 300, 100) if !tt.HasText("00:55") { t.Fatalf("countdown %q", tt.Texts()) } diff --git a/candlestick_chart.go b/chart/candlestick_chart.go similarity index 82% rename from candlestick_chart.go rename to chart/candlestick_chart.go index 8dcdf08..b18c2e5 100644 --- a/candlestick_chart.go +++ b/chart/candlestick_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartType selects the price chart shape. @@ -91,17 +94,17 @@ func CandlestickChart(c *ui.Context, candles []Candle, view *MarketView, opts Ca if opts.Sync != nil && opts.SyncID == "" { panic("mujica: CandlestickChart Sync needs a SyncID") } - ch := newChart(c, opts.Frame) - shown := chartMemo(ch, "candles", [2]any{chartID(candles), opts.Type}, func() []Candle { + ch := NewChart(c, opts.Frame) + shown := ChartMemo(ch, "candles", [2]any{ChartID(candles), opts.Type}, func() []Candle { marketCheckCandles(candles) if opts.Type == ChartHeikinAshi { return HeikinAshi(candles) } return candles }) - m := &mktCore{c: c, k: Tokens(c), ch: ch, candles: shown, view: view, opts: opts, dec: priceDecimals("CandlestickChart", opts.Decimals)} - m.st = *ui.Local(ch.res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1, alertFocus: -1} }) - if ch.res.Element.IsDisabled() { + m := &mktCore{c: c, k: core.Tokens(c), ch: ch, candles: shown, view: view, opts: opts, dec: core.PriceDecimals("CandlestickChart", opts.Decimals)} + m.st = *ui.Local(ch.Res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1, alertFocus: -1} }) + if ch.Res.Element.IsDisabled() { copy := *view view = © m.view = view @@ -116,7 +119,7 @@ func CandlestickChart(c *ui.Context, candles []Candle, view *MarketView, opts Ca m.compareLines() m.yDomain() m.alertsCheck() - res, plot := ch.build(m.spec()) + res, plot := ch.Build(m.spec()) if plot != nil { m.input(plot) } @@ -142,12 +145,12 @@ func HeikinAshiChart(c *ui.Context, candles []Candle, view *MarketView, raw *boo opts.Type = ChartCandles } var res ChartResult - col := ui.Column(c).Gap(Density(c).Unit() * 2).MinWidth(0) + col := ui.Column(c).Gap(core.Density(c).Unit() * 2).MinWidth(0) col.Children(func() { ui.Row(c).Children(func() { v := []bool{*raw} - if ToggleGroup(c, &v, []ToggleItem[bool]{{Value: false, Label: msg(c, "heikin_ashi_chart.smoothed")}, {Value: true, Label: msg(c, "heikin_ashi_chart.raw")}}, - ToggleGroupOptions{Label: msg(c, "heikin_ashi_chart.switch")}).Changed() { + if input.ToggleGroup(c, &v, []input.ToggleItem[bool]{{Value: false, Label: core.Msg(c, "heikin_ashi_chart.smoothed")}, {Value: true, Label: core.Msg(c, "heikin_ashi_chart.raw")}}, + input.ToggleGroupOptions{Label: core.Msg(c, "heikin_ashi_chart.switch")}).Changed() { *raw = v[0] slog.Debug("mujica: heikin_ashi switch", "raw", *raw) } @@ -165,7 +168,7 @@ func (m *mktCore) window() { // room is how many candles fit, three DIPs each. func (m *mktCore) room() int { - w := m.ch.res.Element.Bounds().W - 64 + w := m.ch.Res.Element.Bounds().W - 64 if w <= 0 { return 200 } @@ -181,11 +184,11 @@ func (m *mktCore) yDomain() { if m.opts.Alerts != nil { alerts = *m.opts.Alerts } - ids := fmt.Sprintf("%s|%d|%d|%v|%d|%v", chartID(m.candles), m.from, m.to, m.base, len(m.overlays), alerts) + ids := fmt.Sprintf("%s|%d|%d|%v|%d|%v", ChartID(m.candles), m.from, m.to, m.base, len(m.overlays), alerts) for _, l := range m.overlays { - ids += chartID(l.values) + ids += ChartID(l.values) } - d := chartMemo(m.ch, "ydomain", ids, func() [2]float64 { + d := ChartMemo(m.ch, "ydomain", ids, func() [2]float64 { lo, hi := math.Inf(1), math.Inf(-1) for i := m.from; i < m.to; i++ { lo, hi = min(lo, m.val(m.candles[i].Low)), max(hi, m.val(m.candles[i].High)) @@ -223,7 +226,7 @@ type mktGeom struct { } func (m *mktCore) geom(r ui.Rect) mktGeom { - plot := chartInset(r, 60, 26) + plot := ChartInset(r, 60, 26) lo, hi, step := internal.Nice(m.lo, m.hi, 4) return mktGeom{ plot: plot, @@ -287,24 +290,24 @@ func (m *mktCore) spec() ChartSpec { // readout describes time, OHLC, volume, direction, and indicators. func (m *mktCore) readout(i int) string { c := m.candles[i] - f := func(v float64) string { return priceUngrouped(v, m.dec) } + f := func(v float64) string { return core.PriceUngrouped(v, m.dec) } prev := c.Open if i > 0 { prev = m.candles[i-1].Close } - sign := trendSign(c.Close - prev) + sign := core.TrendSign(c.Close - prev) pct := 0.0 if prev != 0 { pct = (c.Close/prev - 1) * 100 } parts := []string{ c.Time.Format("2006-01-02 15:04"), - fmt.Sprintf(msg(m.c, "candlestick_chart.ohlc"), f(c.Open), f(c.High), f(c.Low), f(c.Close)), - fmt.Sprintf("%+.2f%% %s", pct, trendWord(m.c, sign)), - msg(m.c, "candlestick_chart.volume") + " " + statisticFormat(c.Volume, 0), + fmt.Sprintf(core.Msg(m.c, "candlestick_chart.ohlc"), f(c.Open), f(c.High), f(c.Low), f(c.Close)), + fmt.Sprintf("%+.2f%% %s", pct, core.TrendWord(m.c, sign)), + core.Msg(m.c, "candlestick_chart.volume") + " " + data.StatisticFormat(c.Volume, 0), } for _, l := range m.overlays { - parts = append(parts, l.name+" "+chartValue(m.c, l.values[i], f)) + parts = append(parts, l.name+" "+ChartValue(m.c, l.values[i], f)) } return strings.Join(parts, " · ") } diff --git a/candlestick_chart_test.go b/chart/candlestick_chart_test.go similarity index 93% rename from candlestick_chart_test.go rename to chart/candlestick_chart_test.go index 6b4c554..ac44488 100644 --- a/candlestick_chart_test.go +++ b/chart/candlestick_chart_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // sampleCandles is a deterministic random walk, hourly from 2026-01-01. @@ -33,7 +35,7 @@ func TestCandlestickChartDrawsOnlyVisible(t *testing.T) { var view MarketView var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}}) }, 900, 400) tt.Frame() @@ -55,7 +57,7 @@ func TestCandlestickChartKeyboardPanZoomReadout(t *testing.T) { view := MarketView{Count: 50} var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}}) }, 900, 400) tt.Key(0, ui.KeyTab) @@ -89,7 +91,7 @@ func TestCandlestickChartDragPansAndRejects(t *testing.T) { candles := sampleCandles(300) view := MarketView{Count: 100} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}}).Element.Width(860) }, 900, 400) tt.Frame() @@ -118,7 +120,7 @@ func TestCandlestickChartDragPansAndRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 200) }() } } @@ -128,8 +130,8 @@ func TestCandlestickChartTrendColors(t *testing.T) { view := MarketView{Count: 30} var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) - UseTrend(c, RedUp) + core.Use(c, core.Settings{Locale: core.ZhCN}) + core.UseTrend(c, core.RedUp) res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "沪深300"}}) }, 600, 300) tt.Key(0, ui.KeyTab) diff --git a/candlestick_paint.go b/chart/candlestick_paint.go similarity index 93% rename from candlestick_paint.go rename to chart/candlestick_paint.go index 436f8f7..4c2862b 100644 --- a/candlestick_paint.go +++ b/chart/candlestick_paint.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // paint draws axes, visible candles, layers, and the crosshair. @@ -15,7 +16,7 @@ func (m *mktCore) paint(p *ui.Painter, r ui.Rect, active int) { g := m.geom(r) m.st.plot, m.st.ys = g.plot, g.ys k := m.k - size := FontSize(m.c, theme.CaptionSize) + size := core.FontSize(m.c, theme.CaptionSize) label := func(v float64) string { if m.base != 0 { return internal.FormatTick(v, g.step) + "%" @@ -50,7 +51,7 @@ func (m *mktCore) paint(p *ui.Painter, r ui.Rect, active int) { // paintTimeAxis labels candles about 90 DIPs apart. func (m *mktCore) paintTimeAxis(p *ui.Painter, g mktGeom, size float32) { every := max(1, int(math.Ceil(float64(90/g.band.Step())))) - layout := msg(m.c, "candlestick_chart.daily_time") + layout := core.Msg(m.c, "candlestick_chart.daily_time") if m.to-m.from > 1 && m.candles[m.from+1].Time.Sub(m.candles[m.from].Time) < 24*time.Hour { layout = "15:04" } @@ -89,9 +90,9 @@ func (m *mktCore) paintCandles(p *ui.Painter, g mktGeom) int { for i := m.from; i < m.to; i++ { c := m.candles[i] up := c.Close >= c.Open - col := trendColor(m.c, 1) + col := core.TrendColor(m.c, 1) if !up { - col = trendColor(m.c, -1) + col = core.TrendColor(m.c, -1) } x := internal.Snap(p, g.x(m, i)) yo, yc := g.ys.Map(m.val(c.Open)), g.ys.Map(m.val(c.Close)) diff --git a/chart_annotation.go b/chart/chart_annotation.go similarity index 86% rename from chart_annotation.go rename to chart/chart_annotation.go index 92e7bf8..1e55e34 100644 --- a/chart_annotation.go +++ b/chart/chart_annotation.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartNoteKind is what a ChartNote draws. @@ -57,7 +58,7 @@ func ChartAnnotation(c *ui.Context, notes []ChartNote, opts ChartAnnotationOptio panic("mujica: ChartAnnotation note needs a known kind, a band End above Value, and text") } } - return ChartAnnotationPart{c: c, notes: notes, opts: opts, k: Tokens(c), size: FontSize(c, theme.CaptionSize)} + return ChartAnnotationPart{c: c, notes: notes, opts: opts, k: core.Tokens(c), size: core.FontSize(c, theme.CaptionSize)} } // Draw paints bands first, then lines and text, inside plot. @@ -67,7 +68,7 @@ func (a ChartAnnotationPart) Draw(p *ui.Painter, plot ui.Rect) { if n.Kind == NoteBand { x0, x1 := x.Map(plot, n.Value), x.Map(plot, n.End) p.Fill(ui.Rect{X: x0, Y: plot.Y, W: x1 - x0, H: plot.H}, k.Info.Alpha(0.12), 0) - chartText(p, n.Text, x0+4, plot.Y+4, a.size, k.Text, 0, 0) + ChartText(p, n.Text, x0+4, plot.Y+4, a.size, k.Text, 0, 0) } } for _, n := range a.notes { @@ -75,21 +76,21 @@ func (a ChartAnnotationPart) Draw(p *ui.Painter, plot ui.Rect) { case NoteHLine: at := internal.Snap(p, y.Map(plot, n.Value)) chartDash(p, plot.X, at, plot.X+plot.W, at, k.Text) - chartText(p, chartNoteLabel(n, y), plot.X+plot.W-4, at-3, a.size, k.Text, 1, 1) + ChartText(p, chartNoteLabel(n, y), plot.X+plot.W-4, at-3, a.size, k.Text, 1, 1) if n.Draggable { p.FillPath(internal.Diamond(internal.Snap(p, plot.X+8), at, 11), k.AccentText) } case NoteVLine: at := internal.Snap(p, x.Map(plot, n.Value)) chartDash(p, at, plot.Y, at, plot.Y+plot.H, k.Text) - chartText(p, chartNoteLabel(n, x), at+4, plot.Y+4, a.size, k.Text, 0, 0) + ChartText(p, chartNoteLabel(n, x), at+4, plot.Y+4, a.size, k.Text, 0, 0) if n.Draggable { p.FillPath(internal.Diamond(at, internal.Snap(p, plot.Y+plot.H-8), 11), k.AccentText) } case NoteText: px, py := x.Map(plot, n.Value), y.Map(plot, n.End) p.FillPath(internal.Diamond(internal.Snap(p, px), internal.Snap(p, py), 8), k.Text) - chartText(p, n.Text, px+8, py, a.size, k.Text, 0, 0.5) + ChartText(p, n.Text, px+8, py, a.size, k.Text, 0, 0.5) } } } @@ -146,15 +147,15 @@ func (a ChartAnnotationPart) Drag(plotElement *ui.Element, plot ui.Rect) bool { var d float32 = 1e9 switch n.Kind { case NoteHLine: - d = abs32(y.Map(plot, n.Value) - (py - dy)) + d = core.Abs32(y.Map(plot, n.Value) - (py - dy)) case NoteVLine: - d = abs32(x.Map(plot, n.Value) - (px - dx)) + d = core.Abs32(x.Map(plot, n.Value) - (px - dx)) case NoteBand: if x0, x1 := x.Map(plot, n.Value), x.Map(plot, n.End); px-dx >= x0 && px-dx <= x1 { d = 0 } case NoteText: - d = max(abs32(x.Map(plot, n.Value)-(px-dx)), abs32(y.Map(plot, n.End)-(py-dy))) + d = max(core.Abs32(x.Map(plot, n.Value)-(px-dx)), core.Abs32(y.Map(plot, n.End)-(py-dy))) } if d <= 6 { st.note = i @@ -239,9 +240,9 @@ func (a ChartAnnotationPart) keyboard(e *ui.Element) bool { case NoteBand: value += "–" + a.opts.X.Label(n.End) case NoteText: - value += " · " + fmt.Sprintf(msg(a.c, "chart_annotation.y"), a.opts.Y.Label(n.End)) + value += " · " + fmt.Sprintf(core.Msg(a.c, "chart_annotation.y"), a.opts.Y.Label(n.End)) } - text := fmt.Sprintf(msg(a.c, "chart_annotation.readout"), editable[*index]+1, fmt.Sprintf(msg(a.c, key), value)) + text := fmt.Sprintf(core.Msg(a.c, "chart_annotation.readout"), editable[*index]+1, fmt.Sprintf(core.Msg(a.c, key), value)) e.Description(text) a.c.Announce(text) slog.Debug("mujica: chart_annotation keyboard", "note", editable[*index], "value", n.Value) diff --git a/chart_annotation_test.go b/chart/chart_annotation_test.go similarity index 94% rename from chart_annotation_test.go rename to chart/chart_annotation_test.go index 8eecf18..2ea051e 100644 --- a/chart_annotation_test.go +++ b/chart/chart_annotation_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestChartAnnotationDrag(t *testing.T) { @@ -18,7 +20,7 @@ func TestChartAnnotationDrag(t *testing.T) { h := &chartHarness{build: func(c *ui.Context) ChartResult { return partsChart(c, notes, false) }} tt := ui.NewTester(h.view, 520, 420) b := h.res.Element.Bounds() - plot := chartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) + plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) yAt := func(v float32) float32 { return plot.Y + plot.H*(1-v/100) } xAt := func(v float32) float32 { return plot.X + plot.W*v/10 } // Drag Goal from 50 to 70. @@ -50,7 +52,7 @@ func TestChartAnnotationDisabledAndBad(t *testing.T) { h := &chartHarness{disabled: true, build: func(c *ui.Context) ChartResult { return partsChart(c, notes, false) }} tt := ui.NewTester(h.view, 520, 420) b := h.res.Element.Bounds() - plot := chartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) + plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) tt.Press(plot.X+100, plot.Y+plot.H/2) tt.Move(plot.X+100, plot.Y+20) tt.Release(plot.X+100, plot.Y+20) @@ -63,7 +65,7 @@ func TestChartAnnotationDisabledAndBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) partsChart(c, []ChartNote{{Kind: NoteBand, Value: 2, End: 2}}, false) }, 300, 300) } diff --git a/chart_axis.go b/chart/chart_axis.go similarity index 96% rename from chart_axis.go rename to chart/chart_axis.go index dd27c04..9984922 100644 --- a/chart_axis.go +++ b/chart/chart_axis.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartAxisKind is how an axis maps values. @@ -62,7 +63,7 @@ type ChartAxisPart struct { // ChartAxis resolves an axis: its domain, ticks and labels. func ChartAxis(c *ui.Context, opts ChartAxisOptions) ChartAxisPart { - a := ChartAxisPart{opts: opts, k: Tokens(c), size: FontSize(c, theme.CaptionSize)} + a := ChartAxisPart{opts: opts, k: core.Tokens(c), size: core.FontSize(c, theme.CaptionSize)} switch opts.Kind { case AxisLinear, AxisLog: a.lo, a.hi = opts.Min, opts.Max diff --git a/chart_axis_test.go b/chart/chart_axis_test.go similarity index 93% rename from chart_axis_test.go rename to chart/chart_axis_test.go index 447f1cb..d943827 100644 --- a/chart_axis_test.go +++ b/chart/chart_axis_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,11 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) func axisIn(opts ChartAxisOptions) (a ChartAxisPart) { - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); a = ChartAxis(c, opts) }, 100, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); a = ChartAxis(c, opts) }, 100, 100) return a } @@ -82,8 +83,8 @@ func partsChart(c *ui.Context, notes []ChartNote, crosshair bool) ChartResult { grid := ChartGrid(c, ChartGridOptions{X: &x, Y: &y, Minor: 2}) hair := ChartCrosshair(c, ChartCrosshairOptions{X: &x, Y: &y}) notesPart := ChartAnnotation(c, notes, ChartAnnotationOptions{X: &x, Y: &y}) - plotOf := func(r ui.Rect) ui.Rect { return chartInset(r, 52, 26) } - ink := Tokens(c).AccentText + plotOf := func(r ui.Rect) ui.Rect { return ChartInset(r, 52, 26) } + ink := core.Tokens(c).AccentText at := func(r ui.Rect, i int) (float32, float32) { plot := plotOf(r) return x.Map(plot, pts[i][0]), y.Map(plot, pts[i][1]) @@ -153,5 +154,5 @@ func charts2MustPanic(t *testing.T, view func(*ui.Context)) { t.Error("invalid options accepted") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); view(c) }, 320, 240) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); view(c) }, 320, 240) } diff --git a/chart_brush.go b/chart/chart_brush.go similarity index 78% rename from chart_brush.go rename to chart/chart_brush.go index d4a0da3..9317229 100644 --- a/chart_brush.go +++ b/chart/chart_brush.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartBrushOptions configure a ChartBrush. @@ -34,7 +36,7 @@ type brushDrag struct { } // ChartBrush selects a range with dragging and keyboard controls. -func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) ChoiceResult { +func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) core.ChoiceResult { if opts.Label == "" || !(opts.Max > opts.Min) || math.IsInf(opts.Min, 0) || math.IsInf(opts.Max, 0) || math.IsNaN(opts.Step) || math.IsInf(opts.Step, 0) || opts.Step < 0 || math.IsNaN(float64(opts.Height)) || math.IsInf(float64(opts.Height), 0) || opts.Height != 0 && opts.Height < theme.MinHitSize { panic("mujica: ChartBrush needs a Label, finite domain and step, and Height ≥ 28") } @@ -42,7 +44,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice if step == 0 { step = (opts.Max - opts.Min) / 100 } - spec := numberInputResolve(NumberInputOptions{Min: opts.Min, Max: opts.Max, Step: step}) + spec := input.NumberInputResolve(input.NumberInputOptions{Min: opts.Min, Max: opts.Max, Step: step}) if value[0] < opts.Min || value[1] > opts.Max || !(value[1] > value[0]) { panic("mujica: ChartBrush value must be an ascending range inside Min..Max") } @@ -50,14 +52,14 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice if h == 0 { h = 48 } - k := Tokens(c) - u := Density(c).Unit() - val := func(v float64) string { return chartValue(c, v, opts.Format) } + k := core.Tokens(c) + u := core.Density(c).Unit() + val := func(v float64) string { return ChartValue(c, v, opts.Format) } snap := func(v float64) float64 { - return max(spec.lo, min(spec.hi, spec.round(spec.lo+math.Round((v-spec.lo)/spec.step)*spec.step))) + return max(spec.Lo, min(spec.Hi, spec.Round(spec.Lo+math.Round((v-spec.Lo)/spec.Step)*spec.Step))) } before := *value - var res ChoiceResult + var res core.ChoiceResult res.Element = ui.Column(c).Gap(u).MinWidth(0).Disabled(opts.Disabled) res.Element.Children(func() { track := ui.Box(c).Height(h).FillWidth().MinWidth(0).Radius(theme.ControlRadius).Background(k.SurfaceHover) @@ -68,17 +70,17 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice drag := ui.Local(track, "mujica.brush.drag", func() brushDrag { return brushDrag{part: -1} }) set := func(lo, hi float64) { lo, hi = snap(lo), snap(hi) - if hi-lo >= spec.step-1e-9 { + if hi-lo >= spec.Step-1e-9 { *value = [2]float64{lo, hi} } } move := func(lo, hi float64) { w := hi - lo - lo = max(spec.lo, min(lo, spec.hi-w)) + lo = max(spec.Lo, min(lo, spec.Hi-w)) set(lo, lo+w) } preview := ui.Local(track, "mujica.brush.preview", func() brushPreview { return brushPreview{} }) - if id := chartID(opts.Values); preview.id != id { + if id := ChartID(opts.Values); preview.id != id { *preview = brushPreview{id: id, lo: math.Inf(1), hi: math.Inf(-1)} for _, v := range opts.Values { if math.IsInf(v, 0) { @@ -94,7 +96,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice x0, x1 := at(value[0]), at(value[1]) end := func(i int, x float32) *ui.Element { e := ui.Box(c).Absolute().Left(x-theme.MinHitSize/2).Top(0).Size(theme.MinHitSize, h).Focusable(). - Role(ui.RoleSlider).Label(fmt.Sprintf(msg(c, []string{"chart_brush.start", "chart_brush.end"}[i]), val(value[i]))) + Role(ui.RoleSlider).Label(fmt.Sprintf(core.Msg(c, []string{"chart_brush.start", "chart_brush.end"}[i]), val(value[i]))) internal.Ring(e, k.Focus, theme.ControlRadius) return e.Draw(func(p *ui.Painter, r ui.Rect) { cx := internal.Snap(p, r.X+r.W/2) @@ -104,7 +106,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice // Tab order runs start, window, end. ends := [2]*ui.Element{end(0, x0)} window := ui.Box(c).Absolute().Left(x0 + theme.MinHitSize/2).Top(0).Width(max(x1-x0-theme.MinHitSize, theme.MinHitSize)).Height(h).Focusable(). - Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1]))) + Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1]))) internal.Ring(window, k.Focus, theme.ControlRadius) ends[1] = end(1, x1) if res.Element.IsDisabled() { @@ -116,54 +118,54 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) Choice switch { case !ok: case i == 0: - set(min(v, value[1]-spec.step), value[1]) + set(min(v, value[1]-spec.Step), value[1]) default: - set(value[0], max(v, value[0]+spec.step)) + set(value[0], max(v, value[0]+spec.Step)) } } w := value[1] - value[0] switch { case window.Shortcut(0, ui.KeyLeft): - lo := max(spec.lo, spec.stepBy(value[0], -1)) + lo := max(spec.Lo, spec.StepBy(value[0], -1)) set(lo, lo+w) case window.Shortcut(0, ui.KeyRight): - hi := min(spec.hi, spec.stepBy(value[1], 1)) + hi := min(spec.Hi, spec.StepBy(value[1], 1)) set(hi-w, hi) case window.Shortcut(0, ui.KeyHome): - set(spec.lo, spec.lo+w) + set(spec.Lo, spec.Lo+w) case window.Shortcut(0, ui.KeyEnd): - set(spec.hi-w, spec.hi) + set(spec.Hi-w, spec.Hi) } brushPointer(track, window, ends, drag, value, from, set, move) }) - ui.Text(c, val(value[0])+" – "+val(value[1])).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, val(value[0])+" – "+val(value[1])).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) }) - res.changed = *value != before - if res.changed { - c.Announce(fmt.Sprintf(msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1]))) + res = core.NewChoiceResult(res.Element, *value != before) + if res.Changed() { + c.Announce(fmt.Sprintf(core.Msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1]))) slog.Debug("mujica: chart_brush range", "from", value[0], "to", value[1]) } return res } // brushKey handles arrow, Page, Home and End keys. -func brushKey(e *ui.Element, v float64, spec numberInputSpec) (float64, bool) { +func brushKey(e *ui.Element, v float64, spec input.NumberInputSpec) (float64, bool) { if e.IsDisabled() { return v, false } switch { case e.Shortcut(0, ui.KeyLeft), e.Shortcut(0, ui.KeyDown): - return spec.stepBy(v, -1), true + return spec.StepBy(v, -1), true case e.Shortcut(0, ui.KeyRight), e.Shortcut(0, ui.KeyUp): - return spec.stepBy(v, 1), true + return spec.StepBy(v, 1), true case e.Shortcut(0, ui.KeyPageDown): - return spec.stepBy(v, -10), true + return spec.StepBy(v, -10), true case e.Shortcut(0, ui.KeyPageUp): - return spec.stepBy(v, 10), true + return spec.StepBy(v, 10), true case e.Shortcut(0, ui.KeyHome): - return spec.lo, true + return spec.Lo, true case e.Shortcut(0, ui.KeyEnd): - return spec.hi, true + return spec.Hi, true } return v, false } diff --git a/chart_brush_test.go b/chart/chart_brush_test.go similarity index 96% rename from chart_brush_test.go rename to chart/chart_brush_test.go index 86d83b8..da52492 100644 --- a/chart_brush_test.go +++ b/chart/chart_brush_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type brushHarness struct { @@ -16,7 +18,7 @@ type brushHarness struct { } func (h *brushHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(10).Width(420).Children(func() { r := ChartBrush(c, &h.value, ChartBrushOptions{Label: "Months", Min: 0, Max: 100, Step: 1, Disabled: h.disabled}) h.el = r.Element @@ -103,7 +105,7 @@ func TestChartBrushDisabledAndBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ChartBrush(c, &v, ChartBrushOptions{Label: "x", Max: 100}) }, 300, 100) }() diff --git a/chart_crosshair.go b/chart/chart_crosshair.go similarity index 88% rename from chart_crosshair.go rename to chart/chart_crosshair.go index d25ba55..2f7c360 100644 --- a/chart_crosshair.go +++ b/chart/chart_crosshair.go @@ -1,11 +1,13 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "math" - "mujicaui/internal" - "mujicaui/theme" + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartCrosshairOptions configure a ChartCrosshair. @@ -32,7 +34,7 @@ func ChartCrosshair(c *ui.Context, opts ChartCrosshairOptions) ChartCrosshairPar if opts.Reach == 0 { opts.Reach = 24 } - return ChartCrosshairPart{opts: opts, k: Tokens(c), size: FontSize(c, theme.CaptionSize), pad: Density(c).Unit()} + return ChartCrosshairPart{opts: opts, k: core.Tokens(c), size: core.FontSize(c, theme.CaptionSize), pad: core.Density(c).Unit()} } // Snap returns the nearest point within Reach, or -1. diff --git a/chart_crosshair_test.go b/chart/chart_crosshair_test.go similarity index 89% rename from chart_crosshair_test.go rename to chart/chart_crosshair_test.go index 45cfc20..9f22886 100644 --- a/chart_crosshair_test.go +++ b/chart/chart_crosshair_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "image/color" @@ -7,14 +7,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestChartCrosshairSnapsAndFollowsKeys(t *testing.T) { h := &chartHarness{build: func(c *ui.Context) ChartResult { return partsChart(c, nil, true) }} tt := ui.NewTester(h.view, 520, 420) b := h.res.Element.Bounds() - plot := chartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) + plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26) // Point 1 is ten DIPs from the pointer. px, py := plot.X+plot.W/2, plot.Y+plot.H*0.2 tt.Move(px+10, py-6) @@ -52,7 +53,7 @@ func TestChartCrosshairBad(t *testing.T) { t.Fatal("no panic without axes") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ChartCrosshair(c, ChartCrosshairOptions{}) }, 100, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); ChartCrosshair(c, ChartCrosshairOptions{}) }, 100, 100) } func TestChartCrosshairSnapshot(t *testing.T) { diff --git a/chart_empty_state.go b/chart/chart_empty_state.go similarity index 61% rename from chart_empty_state.go rename to chart/chart_empty_state.go index 3b83d4f..dcde065 100644 --- a/chart_empty_state.go +++ b/chart/chart_empty_state.go @@ -1,7 +1,10 @@ -package mujica +package chart import ( "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) // ChartEmptyStateOptions configure a ChartEmptyState. @@ -15,7 +18,7 @@ type ChartEmptyStateOptions struct { } // ChartEmptyState preserves plot size and reports retry clicks. -func ChartEmptyState(c *ui.Context, status DataStatus, opts ChartEmptyStateOptions) ChoiceResult { +func ChartEmptyState(c *ui.Context, status data.DataStatus, opts ChartEmptyStateOptions) core.ChoiceResult { if opts.Height < 0 { panic("mujica: ChartEmptyState Height must be ≥ 0") } @@ -23,10 +26,10 @@ func ChartEmptyState(c *ui.Context, status DataStatus, opts ChartEmptyStateOptio if h == 0 { h = 240 } - var res ChoiceResult + var res core.ChoiceResult res.Element = ui.Column(c).Height(h).FillWidth().MinWidth(0) res.Element.Children(func() { - res.changed = dataPlaceholder(c, status, opts.Error, opts.Empty, msg(c, "chart_empty_state.empty")) && !res.Element.IsDisabled() + res = core.NewChoiceResult(res.Element, data.DataPlaceholder(c, status, opts.Error, opts.Empty, core.Msg(c, "chart_empty_state.empty")) && !res.Element.IsDisabled()) }) return res } diff --git a/chart_empty_state_test.go b/chart/chart_empty_state_test.go similarity index 78% rename from chart_empty_state_test.go rename to chart/chart_empty_state_test.go index 2a94b95..1201cb8 100644 --- a/chart_empty_state_test.go +++ b/chart/chart_empty_state_test.go @@ -1,18 +1,21 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestChartEmptyStateKeepsSizeAndRetries(t *testing.T) { - status := DataReady + status := data.DataReady retries, disabled := 0, false var heights []float32 var el *ui.Element tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(400).Disabled(disabled).Children(func() { r := ChartEmptyState(c, status, ChartEmptyStateOptions{Height: 180, Empty: "No hymns this week", Error: "The choir did not answer"}) el = r.Element @@ -25,13 +28,13 @@ func TestChartEmptyStateKeepsSizeAndRetries(t *testing.T) { if !tt.HasText("No hymns this week") { t.Fatalf("empty: %q", tt.Texts()) } - status = DataLoading + status = data.DataLoading tt.Frame() heights = append(heights, el.Bounds().H) if !tt.HasText("Loading") { t.Fatalf("loading: %q", tt.Texts()) } - status = DataFailed + status = data.DataFailed tt.Frame() heights = append(heights, el.Bounds().H) if err := tt.Click("Retry"); err != nil || retries != 1 || !tt.HasText("The choir did not answer") { @@ -51,8 +54,8 @@ func TestChartEmptyStateKeepsSizeAndRetries(t *testing.T) { func TestChartEmptyStateSnapshot(t *testing.T) { snapshot(t, "chart_empty_state", 640, 220, func(c *ui.Context) { ui.Row(c).Padding(16).Gap(16).Children(func() { - for _, st := range []DataStatus{DataReady, DataLoading, DataFailed} { - ui.Column(c).Width(192).Border(1, Tokens(c).Border).Children(func() { + for _, st := range []data.DataStatus{data.DataReady, data.DataLoading, data.DataFailed} { + ui.Column(c).Width(192).Border(1, core.Tokens(c).Border).Children(func() { ChartEmptyState(c, st, ChartEmptyStateOptions{Height: 180}) }) } diff --git a/chart_export.go b/chart/chart_export.go similarity index 74% rename from chart_export.go rename to chart/chart_export.go index 901b888..03fa217 100644 --- a/chart_export.go +++ b/chart/chart_export.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "bytes" @@ -7,6 +7,8 @@ import ( "math" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // ChartExportOptions configure a ChartExport. @@ -26,24 +28,14 @@ func ChartExport(c *ui.Context, chart func(c *ui.Context), opts ChartExportOptio if scale == 0 { scale = 2 } - w := current(c) - settings := Settings{Mode: Light, Density: w.density, Locale: w.locale, Messages: w.messages} - if w.tokens.Dark { - settings.Mode = Dark - } - tokens := w.tokens + settings, tokens := core.CurrentSettings(c), core.Tokens(c) ready := false tester := ui.NewTester(func(c *ui.Context) { if !ready { return } - if settings.Mode == Dark { - settings.Dark = &tokens - } else { - settings.Light = &tokens - } - Use(c, settings) - ui.Column(c).Fill().Background(tokens.Surface).Padding(Density(c).Unit() * 4).Children(func() { chart(c) }) + core.Use(c, settings) + ui.Column(c).Fill().Background(tokens.Surface).Padding(core.Density(c).Unit() * 4).Children(func() { chart(c) }) }, opts.Width, opts.Height) tester.SetScale(scale) tester.SetPreferences(c.Preferences()) diff --git a/chart_export_test.go b/chart/chart_export_test.go similarity index 87% rename from chart_export_test.go rename to chart/chart_export_test.go index b608e9c..21ce99a 100644 --- a/chart_export_test.go +++ b/chart/chart_export_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "bytes" @@ -8,14 +8,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestChartExportPNGInTheme(t *testing.T) { - for _, mode := range []Mode{Light, Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { var out []byte ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) if out != nil { return } @@ -35,7 +36,7 @@ func TestChartExportPNGInTheme(t *testing.T) { t.Fatalf("%v: size %v", mode, b) } k := theme.Light() - if mode == Dark { + if mode == core.Dark { k = theme.Dark() } if got := color.RGBAModel.Convert(img.At(3, 3)).(color.RGBA); got != toRGBA(k.Surface) { @@ -51,7 +52,7 @@ func TestChartExportBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) _, _ = ChartExport(c, func(*ui.Context) {}, ChartExportOptions{}) }, 100, 100) } @@ -76,7 +77,7 @@ func TestChartExportPreservesPreferences(t *testing.T) { var data []byte var got ui.Preferences tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Dark, Locale: ZhCN}) + core.Use(c, core.Settings{Mode: core.Dark, Locale: core.ZhCN}) if data != nil { return } diff --git a/chart_frame.go b/chart/chart_frame.go similarity index 76% rename from chart_frame.go rename to chart/chart_frame.go index e69bae0..216de6d 100644 --- a/chart_frame.go +++ b/chart/chart_frame.go @@ -1,12 +1,14 @@ -package mujica +package chart import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartFrame is what every chart shares. @@ -16,7 +18,7 @@ type ChartFrame struct { // Height is the plot height in DIPs; 0 means 240. Height float32 // Status shows loading or failure in the plot's place. - Status DataStatus + Status data.DataStatus Error string // Empty replaces the built-in "No data" text. Empty string @@ -75,7 +77,7 @@ func Chart(c *ui.Context, frame ChartFrame, spec ChartSpec) ChartResult { if spec.Marks < 0 || spec.Draw == nil || spec.Pick == nil || spec.Readout == nil { panic("mujica: Chart needs Marks ≥ 0, Draw, Pick and Readout") } - res, _ := newChart(c, frame).build(spec) + res, _ := NewChart(c, frame).Build(spec) return res } @@ -89,11 +91,11 @@ type chart struct { c *ui.Context frame ChartFrame st *chartState - res ChartResult + Res ChartResult } -// newChart starts a chart; ask it which series are hidden. -func newChart(c *ui.Context, frame ChartFrame) *chart { +// NewChart starts a chart; ask it which series are hidden. +func NewChart(c *ui.Context, frame ChartFrame) *chart { if frame.Label == "" || frame.Height < 0 { panic("mujica: chart needs a Label and a non-negative Height") } @@ -101,37 +103,37 @@ func newChart(c *ui.Context, frame ChartFrame) *chart { frame.Height = 240 } ch := &chart{c: c, frame: frame} - ch.res.Element = ui.Column(c).Gap(Density(c).Unit() * 2).MinWidth(0) - ch.st = ui.Local(ch.res.Element, "mujica.chart", func() chartState { return chartState{focus: -1, last: -1, hidden: map[string]bool{}} }) + ch.Res.Element = ui.Column(c).Gap(core.Density(c).Unit() * 2).MinWidth(0) + ch.st = ui.Local(ch.Res.Element, "mujica.chart", func() chartState { return chartState{focus: -1, last: -1, hidden: map[string]bool{}} }) return ch } -// toggles returns the legend's hidden set when series can be switched off. -func (ch *chart) toggles(on bool) *map[string]bool { +// Toggles returns the legend's hidden set when series can be switched off. +func (ch *chart) Toggles(on bool) *map[string]bool { if on { return &ch.st.hidden } return nil } -// hidden reports whether the legend switched series name off. -func (ch *chart) hidden(name string) bool { return ch.st.hidden[name] } +// Hidden reports whether the legend switched series name off. +func (ch *chart) Hidden(name string) bool { return ch.st.hidden[name] } -// build lays out legend, plot and readout; returns the plot. -func (ch *chart) build(spec ChartSpec) (ChartResult, *ui.Element) { - c, k, st := ch.c, Tokens(ch.c), ch.st - u := Density(c).Unit() +// Build lays out legend, plot and readout; returns the plot. +func (ch *chart) Build(spec ChartSpec) (ChartResult, *ui.Element) { + c, k, st := ch.c, core.Tokens(ch.c), ch.st + u := core.Density(c).Unit() var plot *ui.Element - ch.res.Element.Children(func() { + ch.Res.Element.Children(func() { if len(spec.Legend) > 0 { if name, ok := ChartLegend(c, spec.Hidden, spec.Legend, ChartLegendOptions{}).Toggled(); ok { - ch.res.toggled = name + ch.Res.toggled = name } } - if ch.frame.Status == DataLoading || ch.frame.Status == DataFailed || spec.Marks == 0 { + if ch.frame.Status == data.DataLoading || ch.frame.Status == data.DataFailed || spec.Marks == 0 { st.focus = -1 - ch.res.retry = ChartEmptyState(c, ch.frame.Status, ChartEmptyStateOptions{Height: ch.frame.Height, Error: ch.frame.Error, Empty: ch.frame.Empty}).Changed() - ch.res.Point = -1 + ch.Res.retry = ChartEmptyState(c, ch.frame.Status, ChartEmptyStateOptions{Height: ch.frame.Height, Error: ch.frame.Error, Empty: ch.frame.Empty}).Changed() + ch.Res.Point = -1 ui.Box(c).Height(u * 5) return } @@ -158,7 +160,7 @@ func (ch *chart) build(spec ChartSpec) (ChartResult, *ui.Element) { x, y = spec.Anchor(local, active) } } - ch.res.Point = active + ch.Res.Point = active if active >= 0 && b.W > 0 { lines := chartTipLines(spec.Readout(active)) if spec.Tooltip != nil { @@ -167,15 +169,15 @@ func (ch *chart) build(spec ChartSpec) (ChartResult, *ui.Element) { ChartTooltip(c, b.X+x, b.Y+y, lines) } plot.Draw(func(p *ui.Painter, r ui.Rect) { spec.Draw(p, r, active) }) - text, col := msg(c, "chart.hint"), k.TextMuted + text, col := core.Msg(c, "chart.hint"), k.TextMuted if active >= 0 { text, col = spec.Readout(active), k.Text } - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(col).Role(ui.RoleStatus).SingleLine().Height(u * 5) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(col).Role(ui.RoleStatus).SingleLine().Height(u * 5) }) - ch.res.changed = ch.res.Point != st.last - st.last = ch.res.Point - return ch.res, plot + ch.Res.changed = ch.Res.Point != st.last + st.last = ch.Res.Point + return ch.Res, plot } // input moves the keyboard mark, and takes clicks on marks. @@ -230,20 +232,20 @@ func (ch *chart) input(plot *ui.Element, spec ChartSpec) { } } -// chartText draws s anchored at (x, y) by ax, ay. -func chartText(p *ui.Painter, s string, x, y, size float32, col ui.Color, ax, ay float32) (w, h float32) { +// ChartText draws s anchored at (x, y) by ax, ay. +func ChartText(p *ui.Painter, s string, x, y, size float32, col ui.Color, ax, ay float32) (w, h float32) { w, h = p.MeasureText(0, ui.Span{Text: s, Size: size}) p.Text(x-w*ax, y-h*ay, s, size, col) return w, h } -// chartInset is r less room for axis labels. -func chartInset(r ui.Rect, left, bottom float32) ui.Rect { +// ChartInset is r less room for axis labels. +func ChartInset(r ui.Rect, left, bottom float32) ui.Rect { return ui.Rect{X: r.X + left, Y: r.Y + 8, W: max(r.W-left-12, 1), H: max(r.H-bottom-8, 1)} } -// chartInk is Text or Surface, whichever reads better on bg. -func chartInk(k theme.Tokens, bg ui.Color) ui.Color { +// ChartInk is Text or Surface, whichever reads better on bg. +func ChartInk(k theme.Tokens, bg ui.Color) ui.Color { if theme.Contrast(k.Text, bg) >= theme.Contrast(k.Surface, bg) { return k.Text } diff --git a/chart_frame_test.go b/chart/chart_frame_test.go similarity index 99% rename from chart_frame_test.go rename to chart/chart_frame_test.go index a678038..9483cab 100644 --- a/chart_frame_test.go +++ b/chart/chart_frame_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "testing" diff --git a/chart_grid.go b/chart/chart_grid.go similarity index 93% rename from chart_grid.go rename to chart/chart_grid.go index e64556d..b064843 100644 --- a/chart_grid.go +++ b/chart/chart_grid.go @@ -1,11 +1,12 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // ChartGridOptions configure a ChartGrid. @@ -32,7 +33,7 @@ func ChartGrid(c *ui.Context, opts ChartGridOptions) ChartGridPart { panic("mujica: ChartGrid has no minor lines on a category axis") } } - k := Tokens(c) + k := core.Tokens(c) return ChartGridPart{opts: opts, major: k.Border, minor: k.Border.Mix(k.Surface, 0.55)} } diff --git a/chart_grid_test.go b/chart/chart_grid_test.go similarity index 89% rename from chart_grid_test.go rename to chart/chart_grid_test.go index b81c0e3..84a6394 100644 --- a/chart_grid_test.go +++ b/chart/chart_grid_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "image/color" @@ -6,13 +6,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestChartGridPixelAlignedWithMinor(t *testing.T) { for _, scale := range []float32{1, 2} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) y := ChartAxis(c, ChartAxisOptions{Side: SideLeft, Min: 0, Max: 100, Ticks: []float64{0, 50, 100}}) grid := ChartGrid(c, ChartGridOptions{Y: &y, Minor: 2}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { grid.Draw(p, ui.Rect{X: 0, Y: 10.3, W: 200, H: 100}) }) @@ -46,7 +47,7 @@ func TestChartGridBad(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); build(c) }, 100, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); build(c) }, 100, 100) }() } } diff --git a/chart_legend.go b/chart/chart_legend.go similarity index 83% rename from chart_legend.go rename to chart/chart_legend.go index b269165..9dcbf08 100644 --- a/chart_legend.go +++ b/chart/chart_legend.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartLegendItem is a series key: name, color and marker shape. @@ -51,8 +52,8 @@ func ChartLegend(c *ui.Context, hidden *map[string]bool, items []ChartLegendItem } seen[it.Name] = true } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res ChartLegendResult res.Element = ui.Row(c).Gap(u * 3).Wrap() orient := ui.Horizontal @@ -74,16 +75,16 @@ func ChartLegend(c *ui.Context, hidden *map[string]bool, items []ChartLegendItem }) } if opts.MaxItems > 0 && len(items) > opts.MaxItems { - label := fmt.Sprintf(msg(c, "chart_legend.more"), len(items)-opts.MaxItems) + label := fmt.Sprintf(core.Msg(c, "chart_legend.more"), len(items)-opts.MaxItems) if *open { - label = msg(c, "chart_legend.fewer") + label = core.Msg(c, "chart_legend.fewer") } b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Padding(0, u*2).Radius(theme.ControlRadius).Disabled(res.Element.IsDisabled()).TextColor(k.AccentText) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() }) + b.Children(func() { ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) if b.Clicked() && !b.IsDisabled() { *open = !*open res.expanded, res.folded = *open, true @@ -96,8 +97,8 @@ func ChartLegend(c *ui.Context, hidden *map[string]bool, items []ChartLegendItem // chartLegendEntry builds a series visibility control. func chartLegendEntry(c *ui.Context, hidden *map[string]bool, it ChartLegendItem, disabled bool) (string, bool) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() on := hidden == nil || !(*hidden)[it.Name] var e *ui.Element switched := false @@ -119,7 +120,7 @@ func chartLegendEntry(c *ui.Context, hidden *map[string]bool, it ChartLegendItem } else { e = ui.Row(c) } - e.MinWidth(theme.MinHitSize).MinHeight(Density(c).ControlHeight()).Gap(u*2).AlignItems(ui.Center).Padding(0, u).Radius(theme.ControlRadius) + e.MinWidth(theme.MinHitSize).MinHeight(core.Density(c).ControlHeight()).Gap(u*2).AlignItems(ui.Center).Padding(0, u).Radius(theme.ControlRadius) col := it.Color if !on { col = k.TextMuted @@ -132,7 +133,7 @@ func chartLegendEntry(c *ui.Context, hidden *map[string]bool, it ChartLegendItem p.Stroke(r, col, 2, 1) } }) - t := ui.Text(c, it.Name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text) + t := ui.Text(c, it.Name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text) if !on { t.Strikethrough().TextColor(k.TextMuted) } diff --git a/chart_legend_test.go b/chart/chart_legend_test.go similarity index 90% rename from chart_legend_test.go rename to chart/chart_legend_test.go index 7fa3572..38a147b 100644 --- a/chart_legend_test.go +++ b/chart/chart_legend_test.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func legendItems() []ChartLegendItem { @@ -22,7 +23,7 @@ func TestChartLegendToggleKeyboard(t *testing.T) { hidden := map[string]bool{} var toggled []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if s, ok := ChartLegend(c, &hidden, legendItems(), ChartLegendOptions{}).Toggled(); ok { toggled = append(toggled, s) } @@ -49,7 +50,7 @@ func TestChartLegendToggleKeyboard(t *testing.T) { func TestChartLegendFoldAndStatic(t *testing.T) { vertical := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ChartLegend(c, nil, legendItems(), ChartLegendOptions{Vertical: vertical, MaxItems: 2}) }, 500, 400) if tt.HasText("Prime") || !tt.HasText("+3 more") { @@ -75,7 +76,7 @@ func TestChartLegendBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ChartLegend(c, nil, []ChartLegendItem{{Name: "a"}, {Name: "a"}}, ChartLegendOptions{}) }, 100, 100) } @@ -85,7 +86,7 @@ func TestChartLegendSnapshot(t *testing.T) { hidden := map[string]bool{"Lauds": true} items := legendItems() for i := range items { - items[i].Color = internal.Palette(Tokens(c))[i] + items[i].Color = internal.Palette(core.Tokens(c))[i] } ui.Column(c).Padding(16).Gap(12).Children(func() { ChartLegend(c, &hidden, items, ChartLegendOptions{}) @@ -99,7 +100,7 @@ func TestChartLegendDisabledAndExpansionEvents(t *testing.T) { disabled := false events := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := ChartLegend(c, &hidden, legendItems(), ChartLegendOptions{MaxItems: 2}) if _, ok := r.Expanded(); ok { diff --git a/chart_series.go b/chart/chart_series.go similarity index 67% rename from chart_series.go rename to chart/chart_series.go index 0856a04..f5fff27 100644 --- a/chart_series.go +++ b/chart/chart_series.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -10,7 +10,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // ChartSeries is named values; NaN marks a missing one. @@ -19,22 +20,22 @@ type ChartSeries struct { Values []float64 } -type chartMemoEntry struct { +type ChartMemoEntry struct { deps any - val any + Val any } -// chartMemo recomputes f only when the comparable deps change. -func chartMemo[T any](ch *chart, name string, deps any, f func() T) T { - m := ui.Local(ch.res.Element, "mujica.memo."+name, func() chartMemoEntry { return chartMemoEntry{} }) - if m.val == nil || m.deps != deps { - m.deps, m.val = deps, f() +// ChartMemo recomputes f only when the comparable deps change. +func ChartMemo[T any](ch *chart, name string, deps any, f func() T) T { + m := ui.Local(ch.Res.Element, "mujica.memo."+name, func() ChartMemoEntry { return ChartMemoEntry{} }) + if m.Val == nil || m.deps != deps { + m.deps, m.Val = deps, f() } - return m.val.(T) + return m.Val.(T) } -// chartID identifies a slice by first element and length. -func chartID[T any](s []T) string { +// ChartID identifies a slice by first element and length. +func ChartID[T any](s []T) string { if len(s) == 0 { return "0" } @@ -45,7 +46,7 @@ func chartID[T any](s []T) string { func chartSeriesID(ch *chart, series []ChartSeries) string { var b strings.Builder for _, s := range series { - fmt.Fprintf(&b, "%s|%s|%v;", s.Name, chartID(s.Values), ch.hidden(s.Name)) + fmt.Fprintf(&b, "%s|%s|%v;", s.Name, ChartID(s.Values), ch.Hidden(s.Name)) } return b.String() } @@ -66,58 +67,58 @@ func chartCheckSeries(series []ChartSeries) int { return n } -// chartX is an ascending axis of numbers, times or indices. -type chartX struct { - vals []float64 +// ChartX is an ascending axis of numbers, times or indices. +type ChartX struct { + Vals []float64 times bool // d0 and d1, when d1 > d0, fix the domain. d0, d1 float64 } -func newChartX(n int, x []float64, times []time.Time) chartX { +func NewChartX(n int, x []float64, times []time.Time) ChartX { switch { case x != nil && times != nil: panic("mujica: chart takes X or Times, not both") case x != nil && len(x) != n, times != nil && len(times) != n: panic("mujica: chart X or Times length differs from the series") } - a := chartX{vals: x, times: times != nil} + a := ChartX{Vals: x, times: times != nil} if times != nil { - a.vals = make([]float64, n) + a.Vals = make([]float64, n) for i, t := range times { - a.vals[i] = float64(t.UnixNano()) / 1e9 + a.Vals[i] = float64(t.UnixNano()) / 1e9 } } - if a.vals == nil { - a.vals = make([]float64, n) - for i := range a.vals { - a.vals[i] = float64(i) + if a.Vals == nil { + a.Vals = make([]float64, n) + for i := range a.Vals { + a.Vals[i] = float64(i) } } - if !sort.Float64sAreSorted(a.vals) { + if !sort.Float64sAreSorted(a.Vals) { panic("mujica: chart X or Times must ascend") } return a } -func (a chartX) scale(plot ui.Rect) internal.Linear { - d0, d1 := a.vals[0], a.vals[len(a.vals)-1] +func (a ChartX) Scale(plot ui.Rect) internal.Linear { + d0, d1 := a.Vals[0], a.Vals[len(a.Vals)-1] if a.d1 > a.d0 { d0, d1 = a.d0, a.d1 } return internal.Linear{D0: d0, D1: d1, R0: float64(plot.X), R1: float64(plot.X + plot.W)} } -func (a chartX) label(i int) string { +func (a ChartX) label(i int) string { if a.times { - return chartTime(a.vals[i]).Format("2006-01-02 15:04:05") + return chartTime(a.Vals[i]).Format("2006-01-02 15:04:05") } - return strconv.FormatFloat(a.vals[i], 'g', 6, 64) + return strconv.FormatFloat(a.Vals[i], 'g', 6, 64) } -// ticks returns tick positions and labels along plot. -func (a chartX) ticks(plot ui.Rect) ([]float32, []string) { - s := a.scale(plot) +// Ticks returns tick positions and labels along plot. +func (a ChartX) Ticks(plot ui.Rect) ([]float32, []string) { + s := a.Scale(plot) n := max(2, int(plot.W/90)) var xs []float32 var labels []string @@ -139,10 +140,10 @@ func (a chartX) ticks(plot ui.Rect) ([]float32, []string) { return xs, labels } -// nearest returns the index whose x is closest to v. -func (a chartX) nearest(v float64) int { - i := sort.SearchFloat64s(a.vals, v) - if i == len(a.vals) || i > 0 && v-a.vals[i-1] < a.vals[i]-v { +// Nearest returns the index whose x is closest to v. +func (a ChartX) Nearest(v float64) int { + i := sort.SearchFloat64s(a.Vals, v) + if i == len(a.Vals) || i > 0 && v-a.Vals[i-1] < a.Vals[i]-v { i-- } return i @@ -174,10 +175,10 @@ func chartYDomain(series []ChartSeries, hidden func(string) bool, withZero bool) return lo, hi, true } -// chartValue formats v, or names a missing value. -func chartValue(c *ui.Context, v float64, format func(float64) string) string { +// ChartValue formats v, or names a missing value. +func ChartValue(c *ui.Context, v float64, format func(float64) string) string { if math.IsNaN(v) { - return msg(c, "chart.missing") + return core.Msg(c, "chart.missing") } if format != nil { return format(v) diff --git a/chart_sync.go b/chart/chart_sync.go similarity index 83% rename from chart_sync.go rename to chart/chart_sync.go index 82e43ab..a9c661c 100644 --- a/chart_sync.go +++ b/chart/chart_sync.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "log/slog" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartSync shares time windows and crosshairs across linked charts. @@ -97,15 +98,15 @@ func (s *ChartSync) cursorIndex(id string, candles []Candle) int { } // Link toggles synchronization for a stable chart ID. -func (s *ChartSync) Link(c *ui.Context, id string) ChoiceResult { +func (s *ChartSync) Link(c *ui.Context, id string) core.ChoiceResult { if id == "" { panic("mujica: ChartSync.Link needs an id") } - k := Tokens(c) + k := core.Tokens(c) on := s.linked(id) pressed := on - b := ui.ToggleBase(c, &pressed).MinHeight(theme.MinHitSize).Padding(0, Density(c).Unit()*2).Gap(Density(c).Unit()). - Radius(theme.ControlRadius).Label(msg(c, "chart_sync.link")) + b := ui.ToggleBase(c, &pressed).MinHeight(theme.MinHitSize).Padding(0, core.Density(c).Unit()*2).Gap(core.Density(c).Unit()). + Radius(theme.ControlRadius).Label(core.Msg(c, "chart_sync.link")) internal.Ring(b, k.Focus, theme.ControlRadius) changed := b.Changed() && !b.IsDisabled() if changed { @@ -119,21 +120,21 @@ func (s *ChartSync) Link(c *ui.Context, id string) ChoiceResult { } slog.Debug("mujica: chart_sync link", "chart", id, "linked", on) } - fg, bg, word := k.TextMuted, ui.Transparent, msg(c, "chart_sync.unlinked") + fg, bg, word := k.TextMuted, ui.Transparent, core.Msg(c, "chart_sync.unlinked") if on { - fg, bg, word = k.AccentText, k.Selection, msg(c, "chart_sync.linked") + fg, bg, word = k.AccentText, k.Selection, core.Msg(c, "chart_sync.linked") } if b.Hovered() && !on { bg = k.SurfaceHover } - b.Background(bg).TextColor(fg).Border(theme.BorderWidth, fg).Transition(colorFade) + b.Background(bg).TextColor(fg).Border(theme.BorderWidth, fg).Transition(core.ColorFade) b.Children(func() { mark := "circle" if on { mark = "check" } ui.Box(c).Size(14, 14).Draw(func(p *ui.Painter, r ui.Rect) { internal.Mark(p, r, mark, fg) }) - ui.Text(c, word).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, word).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) - return ChoiceResult{b, changed} + return core.NewChoiceResult(b, changed) } diff --git a/chart_sync_test.go b/chart/chart_sync_test.go similarity index 93% rename from chart_sync_test.go rename to chart/chart_sync_test.go index b1547f4..bc17cc8 100644 --- a/chart_sync_test.go +++ b/chart/chart_sync_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestChartSyncRangeCursorAndUnlink(t *testing.T) { @@ -17,7 +20,7 @@ func TestChartSyncRangeCursorAndUnlink(t *testing.T) { } disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Disabled(disabled).Children(func() { sync.Link(c, "b") }) }, 300, 90) tt.Key(0, ui.KeyTab) diff --git a/chart_tooltip.go b/chart/chart_tooltip.go similarity index 85% rename from chart_tooltip.go rename to chart/chart_tooltip.go index d760884..a4b378d 100644 --- a/chart_tooltip.go +++ b/chart/chart_tooltip.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "strings" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartTooltipLine pairs readout text with an optional marker. @@ -21,9 +22,9 @@ func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) *ui.Ele if len(lines) == 0 { panic("mujica: ChartTooltip needs a line") } - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) pad, gap := u*2, float32(12) w, h := float32(0), pad*2 lh := make([]float32, len(lines)) @@ -38,7 +39,7 @@ func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) *ui.Ele w += pad * 2 ww, wh := c.Size() w, h = min(w, max(1, ww-8)), min(h, max(1, wh-8)) - left, top := overlayPlace(c, ui.Rect{X: x, Y: y}, w, h, gap) + left, top := core.PlaceOverlay(c, ui.Rect{X: x, Y: y}, w, h, gap) if x+gap+w <= ww-4 { left = max(4, x+gap) } else { @@ -47,7 +48,7 @@ func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) *ui.Ele var tip *ui.Element ui.Overlay(c, func() { tip = ui.Column(c).Absolute().Left(left).Top(top).Size(w, h).Role(ui.RoleNone) - overlayStyle(c, tip, overlayEnter(c, tip)) + core.OverlayStyle(c, tip, core.OverlayEnter(c, tip)) tip.Children(func() { inner := max(1, w-2*pad) content := float32(0) diff --git a/chart_tooltip_test.go b/chart/chart_tooltip_test.go similarity index 87% rename from chart_tooltip_test.go rename to chart/chart_tooltip_test.go index 08f47d0..4fde944 100644 --- a/chart_tooltip_test.go +++ b/chart/chart_tooltip_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "image/color" @@ -6,15 +6,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestChartTooltipFlipsAtEdges(t *testing.T) { var at [2]float32 var tip ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) tip = ChartTooltip(c, at[0], at[1], []ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: theme.Light().Info, Shape: 1}}).Bounds() }, 400, 300) at = [2]float32{50, 50} @@ -34,7 +35,7 @@ func TestChartTooltipFlipsAtEdges(t *testing.T) { t.Fatal("no panic without lines") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ChartTooltip(c, 0, 0, nil) }, 100, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); ChartTooltip(c, 0, 0, nil) }, 100, 100) } func TestChartTooltipFollowsFocusAndPointer(t *testing.T) { @@ -81,7 +82,7 @@ func tooltipShown(tt *ui.Tester) bool { func TestChartTooltipSnapshot(t *testing.T) { snapshot(t, "chart_tooltip", 320, 140, func(c *ui.Context) { - pal := internal.Palette(Tokens(c)) + pal := internal.Palette(core.Tokens(c)) ChartTooltip(c, 20, 20, []ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: pal[0]}, {Text: "Candles 2", Color: pal[1], Shape: 1}}) }) } @@ -89,7 +90,7 @@ func TestChartTooltipSnapshot(t *testing.T) { func TestChartTooltipFitsLongContent(t *testing.T) { var b ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) lines := make([]ChartTooltipLine, 40) for i := range lines { lines[i].Text = "A very long series name that wraps across the tooltip panel" diff --git a/chart_tree.go b/chart/chart_tree.go similarity index 91% rename from chart_tree.go rename to chart/chart_tree.go index 4f51d5a..5b6470a 100644 --- a/chart_tree.go +++ b/chart/chart_tree.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "log/slog" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartNode is a hierarchy node; branches sum their leaves. @@ -62,11 +63,11 @@ func chartDescend(root ChartNode, path []string) ChartNode { // chartPath shows Back and the breadcrumb; reports a move up. func chartPath(c *ui.Context, root string, path *[]string, disabled bool) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() moved := false ui.Row(c).Gap(u).AlignItems(ui.Center).Wrap().Children(func() { - back := iconAction(c, icons.Must("chevron-left"), msg(c, "chart.back")).Disabled(disabled || len(*path) == 0) + back := core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "chart.back")).Disabled(disabled || len(*path) == 0) if back.Clicked() && !back.IsDisabled() { *path, moved = (*path)[:len(*path)-1], true } diff --git a/chart_type_switcher.go b/chart/chart_type_switcher.go similarity index 62% rename from chart_type_switcher.go rename to chart/chart_type_switcher.go index ee105af..dc53b89 100644 --- a/chart_type_switcher.go +++ b/chart/chart_type_switcher.go @@ -1,10 +1,13 @@ -package mujica +package chart import ( "fmt" "log/slog" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // ChartTypeSwitcherOptions configure a ChartTypeSwitcher. @@ -13,21 +16,21 @@ type ChartTypeSwitcherOptions struct { } // ChartTypeSwitcher chooses candles, OHLC, line, area, or Heikin-Ashi. -func ChartTypeSwitcher(c *ui.Context, value *ChartType, opts ChartTypeSwitcherOptions) ChoiceResult { +func ChartTypeSwitcher(c *ui.Context, value *ChartType, opts ChartTypeSwitcherOptions) core.ChoiceResult { if value == nil { panic("mujica: ChartTypeSwitcher needs a value") } if *value < ChartCandles || *value > ChartHeikinAshi { panic(fmt.Sprintf("mujica: unknown ChartType %d", *value)) } - var items []ToggleItem[ChartType] + var items []input.ToggleItem[ChartType] for t := ChartCandles; t <= ChartHeikinAshi; t++ { - items = append(items, ToggleItem[ChartType]{Value: t, Label: msg(c, fmt.Sprintf("chart_type_switcher.type%d", t))}) + items = append(items, input.ToggleItem[ChartType]{Value: t, Label: core.Msg(c, fmt.Sprintf("chart_type_switcher.type%d", t))}) } sel := []ChartType{*value} - var r ChoiceResult + var r core.ChoiceResult ui.Row(c).Disabled(opts.Disabled).Children(func() { - r = ToggleGroup(c, &sel, items, ToggleGroupOptions{Label: msg(c, "chart_type_switcher.label")}) + r = input.ToggleGroup(c, &sel, items, input.ToggleGroupOptions{Label: core.Msg(c, "chart_type_switcher.label")}) }) if r.Changed() { *value = sel[0] diff --git a/chart_type_switcher_test.go b/chart/chart_type_switcher_test.go similarity index 91% rename from chart_type_switcher_test.go rename to chart/chart_type_switcher_test.go index f810fa2..0553fe2 100644 --- a/chart_type_switcher_test.go +++ b/chart/chart_type_switcher_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestChartTypeSwitcherKeyboardAndDisabled(t *testing.T) { @@ -10,7 +13,7 @@ func TestChartTypeSwitcherKeyboardAndDisabled(t *testing.T) { disabled := false changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ChartTypeSwitcher(c, &value, ChartTypeSwitcherOptions{Disabled: disabled}).Changed() { changes++ } diff --git a/chord_diagram.go b/chart/chord_diagram.go similarity index 87% rename from chord_diagram.go rename to chart/chord_diagram.go index 078e4c9..07caf94 100644 --- a/chord_diagram.go +++ b/chart/chord_diagram.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChordDiagramOptions configure a ChordDiagram. @@ -85,12 +86,12 @@ func ChordDiagram(c *ui.Context, names []string, matrix [][]float64, opts ChordD } seen[name] = true } - ch := newChart(c, opts.Frame) - l := chartMemo(ch, "chords", chartID(matrix), func() chordLayout { return chordPlace(matrix) }) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + l := ChartMemo(ch, "chords", ChartID(matrix), func() chordLayout { return chordPlace(matrix) }) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) - val := func(v float64) string { return chartValue(c, v, opts.Format) } + size := core.FontSize(c, theme.CaptionSize) + val := func(v float64) string { return ChartValue(c, v, opts.Format) } n := len(names) + len(l.pairs) if l.total == 0 { n = 0 @@ -98,7 +99,7 @@ func ChordDiagram(c *ui.Context, names []string, matrix [][]float64, opts ChordD geom := func(r ui.Rect) (cx, cy, rad float32) { return r.X + r.W/2, r.Y + r.H/2, min(r.W, r.H)/2 - 30 } - visible := ui.Local(ch.res.Element, "mujica.chord.visible", func() []int { return nil }) + visible := ui.Local(ch.Res.Element, "mujica.chord.visible", func() []int { return nil }) spec := ChartSpec{Marks: n} spec.Readout = func(i int) string { if i < len(names) { @@ -107,7 +108,7 @@ func ChordDiagram(c *ui.Context, names []string, matrix [][]float64, opts ChordD out += matrix[i][j] in += matrix[j][i] } - return names[i] + " · " + msg(c, "chord_diagram.out") + " " + val(out) + " · " + msg(c, "chord_diagram.in") + " " + val(in) + return names[i] + " · " + core.Msg(c, "chord_diagram.out") + " " + val(out) + " · " + core.Msg(c, "chord_diagram.in") + " " + val(in) } p := l.pairs[i-len(names)] return names[p[0]] + " → " + names[p[1]] + " " + val(matrix[p[0]][p[1]]) + " · " + names[p[1]] + " → " + names[p[0]] + " " + val(matrix[p[1]][p[0]]) @@ -172,10 +173,10 @@ func ChordDiagram(c *ui.Context, names []string, matrix [][]float64, opts ChordD if arc[1]-arc[0] > 0.15 { lx, ly := internal.Polar(cx, cy, rad+20, (arc[0]+arc[1])/2) ax := 0.5 - 0.5*float32(math.Sin((arc[0]+arc[1])/2)) - chartText(p, names[i], lx, ly, size, k.Text, ax, 0.5+0.5*float32(math.Cos((arc[0]+arc[1])/2))) + ChartText(p, names[i], lx, ly, size, k.Text, ax, 0.5+0.5*float32(math.Cos((arc[0]+arc[1])/2))) } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/chord_diagram_test.go b/chart/chord_diagram_test.go similarity index 97% rename from chord_diagram_test.go rename to chart/chord_diagram_test.go index 156f6d4..4da25c7 100644 --- a/chord_diagram_test.go +++ b/chart/chord_diagram_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var chordNames = []string{"Nave", "Choir", "Crypt", "Cloister"} @@ -57,7 +59,7 @@ func TestChordDiagramBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ChordDiagram(c, []string{"a", "b"}, m, ChordDiagramOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) }() diff --git a/compare_symbol_test.go b/chart/compare_symbol_test.go similarity index 94% rename from compare_symbol_test.go rename to chart/compare_symbol_test.go index 138c83f..eb61db7 100644 --- a/compare_symbol_test.go +++ b/chart/compare_symbol_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCompareSymbolPercentAndRemove(t *testing.T) { @@ -17,7 +19,7 @@ func TestCompareSymbolPercentAndRemove(t *testing.T) { cmp := []CompareSymbol{{Name: "ETH", Closes: eth}} removed := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if CompareLegend(c, &cmp).Changed() { removed++ } @@ -48,7 +50,7 @@ func TestCompareSymbolPercentAndRemove(t *testing.T) { }() bad := []CompareSymbol{{Name: "X", Closes: []float64{1}}} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "x"}, Compare: &bad}) }, 300, 200) } diff --git a/density_plot.go b/chart/density_plot.go similarity index 91% rename from density_plot.go rename to chart/density_plot.go index 5ae92ec..3fff306 100644 --- a/density_plot.go +++ b/chart/density_plot.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -109,7 +109,7 @@ func DensityPlot(c *ui.Context, groups []BoxGroup, opts DensityPlotOptions) Char if points == 0 { points = 128 } - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) ids := "" seen := map[string]bool{} for _, g := range groups { @@ -117,13 +117,13 @@ func DensityPlot(c *ui.Context, groups []BoxGroup, opts DensityPlotOptions) Char panic("mujica: DensityPlot groups need unique names") } seen[g.Name] = true - ids += g.Name + chartID(g.Values) + ";" + ids += g.Name + ChartID(g.Values) + ";" } type curves struct { xs []float64 series []ChartSeries } - cv := chartMemo(ch, "kde", fmt.Sprint(ids, opts.Bandwidth, points), func() curves { + cv := ChartMemo(ch, "kde", fmt.Sprint(ids, opts.Bandwidth, points), func() curves { xs, s := densityCurves(groups, opts.Bandwidth, points) return curves{xs, s} }) @@ -136,11 +136,11 @@ func DensityPlot(c *ui.Context, groups []BoxGroup, opts DensityPlotOptions) Char for i, s := range series { tops[i], bases[i] = s.Values, make([]float64, n) } - var ax chartX + var ax ChartX if n > 0 { - ax = chartMemo(ch, "x", ids+fmt.Sprint(opts.Bandwidth, points), func() chartX { return newChartX(n, cv.xs, nil) }) + ax = ChartMemo(ch, "x", ids+fmt.Sprint(opts.Bandwidth, points), func() ChartX { return NewChartX(n, cv.xs, nil) }) } - lo, hi, ok := chartYRange(ch, series, true, nil) + lo, hi, ok := ChartYRange(ch, series, true, nil) if !ok { n = 0 } diff --git a/density_plot_test.go b/chart/density_plot_test.go similarity index 97% rename from density_plot_test.go rename to chart/density_plot_test.go index 4bd901d..47da73c 100644 --- a/density_plot_test.go +++ b/chart/density_plot_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func densityGroups() []BoxGroup { @@ -75,7 +77,7 @@ func TestDensityPlotBadAndEmpty(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DensityPlot(c, densityGroups(), DensityPlotOptions{Frame: ChartFrame{Label: "x"}, Points: 1}) }, 300, 300) } diff --git a/depth_chart.go b/chart/depth_chart.go similarity index 77% rename from depth_chart.go rename to chart/depth_chart.go index e455673..de6eacc 100644 --- a/depth_chart.go +++ b/chart/depth_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DepthLevel is one price level of an order book. @@ -32,16 +34,16 @@ type depthData struct { // DepthChart paints cumulative depth and supports keyboard level readouts. func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) ChartResult { - dec := priceDecimals("DepthChart", opts.Decimals) - ch := newChart(c, opts.Frame) - k := Tokens(c) - d := chartMemo(ch, "depth", [2]string{chartID(bids), chartID(asks)}, func() depthData { return depthBuild(bids, asks) }) - bidCol, askCol := trendColor(c, 1), trendColor(c, -1) - keys := []ChartLegendItem{{Name: msg(c, "depth_chart.bids"), Color: bidCol, Shape: 1}, {Name: msg(c, "depth_chart.asks"), Color: askCol, Shape: 3}} - drawn := ui.Local(ch.res.Element, "mujica.depth.drawn", func() int { return 0 }) - size := FontSize(c, theme.CaptionSize) + dec := core.PriceDecimals("DepthChart", opts.Decimals) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + d := ChartMemo(ch, "depth", [2]string{ChartID(bids), ChartID(asks)}, func() depthData { return depthBuild(bids, asks) }) + bidCol, askCol := core.TrendColor(c, 1), core.TrendColor(c, -1) + keys := []ChartLegendItem{{Name: core.Msg(c, "depth_chart.bids"), Color: bidCol, Shape: 1}, {Name: core.Msg(c, "depth_chart.asks"), Color: askCol, Shape: 3}} + drawn := ui.Local(ch.Res.Element, "mujica.depth.drawn", func() int { return 0 }) + size := core.FontSize(c, theme.CaptionSize) geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 56, 26) + plot := ChartInset(r, 56, 26) lo, hi := d.price[d.nBids-1], d.price[len(d.price)-1] return plot, internal.Linear{D0: lo, D1: hi, R0: float64(plot.X), R1: float64(plot.X + plot.W)}, internal.Linear{D0: 0, D1: d.maxSize * 1.05, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} @@ -79,12 +81,12 @@ func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) return order(pos) } spec.Readout = func(i int) string { - side := msg(c, "depth_chart.bid") + side := core.Msg(c, "depth_chart.bid") if i >= d.nBids { - side = msg(c, "depth_chart.ask") + side = core.Msg(c, "depth_chart.ask") } - return strings.Join([]string{side + " " + priceUngrouped(d.price[i], dec), fmt.Sprintf(msg(c, "depth_chart.total"), statisticFormat(d.total[i], 0)), - msg(c, "depth_chart.mid") + " " + priceUngrouped(d.mid, dec)}, " · ") + return strings.Join([]string{side + " " + core.PriceUngrouped(d.price[i], dec), fmt.Sprintf(core.Msg(c, "depth_chart.total"), data.StatisticFormat(d.total[i], 0)), + core.Msg(c, "depth_chart.mid") + " " + core.PriceUngrouped(d.mid, dec)}, " · ") } spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { plot, xs, ys := geom(r) @@ -106,7 +108,7 @@ func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) area.MoveTo(mx, base) edge.MoveTo(mx, base) prevY := base - indices := chartMemo(ch, fmt.Sprint("depth.indices.", side), [3]any{chartID(d.price), plot.X, plot.W}, func() []int { + indices := ChartMemo(ch, fmt.Sprint("depth.indices.", side), [3]any{ChartID(d.price), plot.X, plot.W}, func() []int { return internal.Columns(idx[1]-idx[0], func(i int) float32 { return xs.Map(d.price[idx[0]+i]) }, func(i int) float64 { return d.total[idx[0]+i] }) }) *drawn += len(indices) @@ -125,14 +127,14 @@ func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) edge.LineTo(end, prevY) p.FillPath(&area, col.Alpha(0.2)) p.StrokePath(&edge, 2, col) - label := []string{msg(c, "depth_chart.bids"), msg(c, "depth_chart.asks")}[side] - chartText(p, label, []float32{plot.X + 6, plot.X + plot.W - 6}[side], plot.Y+4, size, col, []float32{0, 1}[side], 0) + label := []string{core.Msg(c, "depth_chart.bids"), core.Msg(c, "depth_chart.asks")}[side] + ChartText(p, label, []float32{plot.X + 6, plot.X + plot.W - 6}[side], plot.Y+4, size, col, []float32{0, 1}[side], 0) } mx := internal.Snap(p, xs.Map(d.mid)) for y := plot.Y; y < base; y += 6 { p.Line(mx, y, mx, min(y+3, base), 1, k.TextMuted) } - chartText(p, priceUngrouped(d.mid, dec), mx, plot.Y+4, size, k.Text, 0.5, 0) + ChartText(p, core.PriceUngrouped(d.mid, dec), mx, plot.Y+4, size, k.Text, 0.5, 0) if active >= 0 { x, y := xs.Map(d.price[active]), ys.Map(d.total[active]) p.Line(internal.Snap(p, x), plot.Y, internal.Snap(p, x), base, 1, k.TextMuted) @@ -143,7 +145,7 @@ func DepthChart(c *ui.Context, bids, asks []DepthLevel, opts DepthChartOptions) internal.Marker(p, shape, x, y, 10, keys[shape/3].Color) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/depth_chart_test.go b/chart/depth_chart_test.go similarity index 95% rename from depth_chart_test.go rename to chart/depth_chart_test.go index 89763bf..2f7030c 100644 --- a/depth_chart_test.go +++ b/chart/depth_chart_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func sampleBook() ([]DepthLevel, []DepthLevel) { @@ -23,7 +25,7 @@ func TestDepthChartCumulativeAndKeyboard(t *testing.T) { } var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = DepthChart(c, bids, asks, DepthChartOptions{Frame: ChartFrame{Label: "Depth"}}) }, 700, 360) tt.Key(0, ui.KeyTab) diff --git a/chart/doc.go b/chart/doc.go new file mode 100644 index 0000000..3b58729 --- /dev/null +++ b/chart/doc.go @@ -0,0 +1,2 @@ +// Package chart holds MujicaUI general and market charts with their shared frame. +package chart diff --git a/drawing_toolbar.go b/chart/drawing_toolbar.go similarity index 77% rename from drawing_toolbar.go rename to chart/drawing_toolbar.go index 6dbaaf0..3449402 100644 --- a/drawing_toolbar.go +++ b/chart/drawing_toolbar.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DrawingToolbarOptions configure a DrawingToolbar. @@ -18,18 +19,18 @@ type DrawingToolbarOptions struct { } // DrawingToolbar selects tools and manages lock, clear, and undo. -func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions) ChoiceResult { +func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions) core.ChoiceResult { if d == nil { panic("mujica: DrawingToolbar needs drawings") } if d.Tool < ToolSelect || d.Tool > ToolText { panic(fmt.Sprintf("mujica: unknown DrawingTool %d", d.Tool)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() changed := false bar := ui.Column(c).Gap(u).Padding(u).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border). - Role(ui.RoleToolbar).Label(msg(c, "drawing_toolbar.label")) + Role(ui.RoleToolbar).Label(core.Msg(c, "drawing_toolbar.label")) if opts.Horizontal { bar.Row().FocusGroup(ui.Horizontal) } else { @@ -50,7 +51,7 @@ func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions) } bar.Children(func() { for t := ToolSelect; t <= ToolText; t++ { - name := fmt.Sprintf("%s (Alt+%d)", msg(c, fmt.Sprintf("drawing_toolbar.tool%d", t)), t+1) + name := fmt.Sprintf("%s (Alt+%d)", core.Msg(c, fmt.Sprintf("drawing_toolbar.tool%d", t)), t+1) on := d.Tool == t b := ui.ButtonBase(c).Key(t).Size(theme.MinHitSize+4, theme.MinHitSize+4).Radius(theme.ControlRadius).Label(name).Tooltip(name).Disabled(d.Locked) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -73,11 +74,11 @@ func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions) } else { div.Size(u*6, theme.BorderWidth) } - lockName := msg(c, "drawing_toolbar.lock") + lockName := core.Msg(c, "drawing_toolbar.lock") if d.Locked { - lockName = msg(c, "drawing_toolbar.unlock") + lockName = core.Msg(c, "drawing_toolbar.unlock") } - lock := iconAction(c, icons.Must("lock"), lockName).Tooltip(lockName) + lock := core.IconAction(c, icons.Must("lock"), lockName).Tooltip(lockName) if d.Locked { lock.Background(k.Selection).TextColor(k.AccentText) } @@ -86,16 +87,16 @@ func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions) d.Tool = ToolSelect slog.Debug("mujica: drawing_toolbar lock", "locked", d.Locked) } - undo := iconAction(c, icons.Must("rotate-ccw"), msg(c, "drawing_toolbar.undo")).Tooltip(msg(c, "drawing_toolbar.undo")).Disabled(d.Locked || !d.CanUndo()) + undo := core.IconAction(c, icons.Must("rotate-ccw"), core.Msg(c, "drawing_toolbar.undo")).Tooltip(core.Msg(c, "drawing_toolbar.undo")).Disabled(d.Locked || !d.CanUndo()) if undo.Clicked() && !bar.IsDisabled() && d.Undo() { changed = true } - clear := iconAction(c, icons.Must("trash-2"), msg(c, "drawing_toolbar.clear")).Tooltip(msg(c, "drawing_toolbar.clear")).Disabled(d.Locked || len(d.Items) == 0) + clear := core.IconAction(c, icons.Must("trash-2"), core.Msg(c, "drawing_toolbar.clear")).Tooltip(core.Msg(c, "drawing_toolbar.clear")).Disabled(d.Locked || len(d.Items) == 0) if clear.Clicked() && !bar.IsDisabled() && d.Clear() { changed = true } }) - return ChoiceResult{bar, changed} + return core.NewChoiceResult(bar, changed) } // drawingToolGlyph draws a small picture of tool t in r. diff --git a/drawing_toolbar_test.go b/chart/drawing_toolbar_test.go similarity index 91% rename from drawing_toolbar_test.go rename to chart/drawing_toolbar_test.go index 142cd0c..9f89de6 100644 --- a/drawing_toolbar_test.go +++ b/chart/drawing_toolbar_test.go @@ -1,15 +1,18 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestDrawingToolbarShortcutsLockAndDisabled(t *testing.T) { d := DrawingTools{} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Disabled(disabled).Children(func() { DrawingToolbar(c, &d, DrawingToolbarOptions{Horizontal: true}) }) }, 550, 90) tt.Key(ui.Alt, ui.Key4) diff --git a/drawing_tools.go b/chart/drawing_tools.go similarity index 97% rename from drawing_tools.go rename to chart/drawing_tools.go index 1a13889..f6a757e 100644 --- a/drawing_tools.go +++ b/chart/drawing_tools.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,6 +8,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // DrawingTool selects annotation creation or selection behavior. @@ -134,7 +136,7 @@ func (d *DrawingTools) input(m *mktCore, plot *ui.Element, g mktGeom, x, y float if plot.Shortcut(ui.Alt, ui.KeyEnter) && !d.Locked && len(d.Items) > 0 { d.sel = d.sel%len(d.Items) + 1 m.st.alertFocus = -1 - m.c.Announce(fmt.Sprintf(msg(m.c, "drawing_tools.selected"), d.sel)) + m.c.Announce(fmt.Sprintf(core.Msg(m.c, "drawing_tools.selected"), d.sel)) } if plot.Shortcut(ui.Cmd, ui.KeyZ) { d.Undo() @@ -251,7 +253,7 @@ func (d *DrawingTools) place(m *mktCore, pt DrawingPoint) { item.A = d.pending.A } if d.Tool == ToolText && item.Text == "" { - item.Text = msg(m.c, "drawing_tools.note") + item.Text = core.Msg(m.c, "drawing_tools.note") } d.save() d.Items = append(slices.Clone(d.Items), item) diff --git a/drawing_tools_paint.go b/chart/drawing_tools_paint.go similarity index 82% rename from drawing_tools_paint.go rename to chart/drawing_tools_paint.go index fc894b5..b0e4169 100644 --- a/drawing_tools_paint.go +++ b/chart/drawing_tools_paint.go @@ -1,11 +1,12 @@ -package mujica +package chart import ( "strconv" "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // paint draws annotations, previews, and selected handles. @@ -30,7 +31,7 @@ func (d *DrawingTools) paint(p *ui.Painter, g mktGeom, m *mktCore, size float32) drawingStroke(p, ax, ay, bx, by, width, col) case ToolHorizontal: p.Line(g.plot.X, ay, g.plot.X+g.plot.W, ay, width, col) - chartText(p, priceUngrouped(it.A.Price, m.dec), g.plot.X+4, ay-2, size, col, 0, 1) + ChartText(p, core.PriceUngrouped(it.A.Price, m.dec), g.plot.X+4, ay-2, size, col, 0, 1) case ToolRectangle: r := ui.Rect{X: min(ax, bx), Y: min(ay, by), W: max(bx, ax) - min(ax, bx), H: max(by, ay) - min(ay, by)} p.Fill(r, col.Alpha(0.1), 0) @@ -40,11 +41,11 @@ func (d *DrawingTools) paint(p *ui.Painter, g mktGeom, m *mktCore, size float32) price := it.B.Price + (it.A.Price-it.B.Price)*lv y := internal.Snap(p, g.ys.Map(m.val(price))) p.Line(min(ax, bx), y, max(ax, bx), y, 1, col) - chartText(p, strconv.FormatFloat(lv, 'f', -1, 64)+" · "+priceUngrouped(price, m.dec), max(ax, bx)+4, y, size, col, 0, 0.5) + ChartText(p, strconv.FormatFloat(lv, 'f', -1, 64)+" · "+core.PriceUngrouped(price, m.dec), max(ax, bx)+4, y, size, col, 0, 0.5) } drawingStroke(p, ax, ay, bx, by, 1, col.Alpha(0.5)) case ToolText: - w, h := chartText(p, it.Text, ax, ay, size, k.Text, 0.5, 0.5) + w, h := ChartText(p, it.Text, ax, ay, size, k.Text, 0.5, 0.5) p.Stroke(ui.Rect{X: ax - w/2 - 4, Y: ay - h/2 - 2, W: w + 8, H: h + 4}, col, 2, width/1.5) } if selected { diff --git a/drawing_tools_test.go b/chart/drawing_tools_test.go similarity index 90% rename from drawing_tools_test.go rename to chart/drawing_tools_test.go index 13e9902..6321260 100644 --- a/drawing_tools_test.go +++ b/chart/drawing_tools_test.go @@ -1,10 +1,13 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestDrawingToolsPlaceMoveDeleteUndo(t *testing.T) { @@ -13,7 +16,7 @@ func TestDrawingToolsPlaceMoveDeleteUndo(t *testing.T) { d := DrawingTools{Tool: ToolHorizontal} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "Drawing"}, Drawings: &d}) }) @@ -57,8 +60,8 @@ func TestDrawingToolsSnapshot(t *testing.T) { func TestDrawingToolsTrendLinePaint(t *testing.T) { cs := sampleCandles(2) img := ui.Render(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - m := &mktCore{c: c, k: Tokens(c), candles: cs, from: 0, to: 2} + core.Use(c, core.Settings{Mode: core.Light}) + m := &mktCore{c: c, k: core.Tokens(c), candles: cs, from: 0, to: 2} g := mktGeom{plot: ui.Rect{W: 200, H: 200}, band: internal.Band{N: 2, R0: 0, R1: 200}, ys: internal.Linear{D0: 0, D1: 10, R0: 180, R1: 20}} d := DrawingTools{Items: []Drawing{{Tool: ToolTrendLine, A: DrawingPoint{cs[0].Time, 0}, B: DrawingPoint{cs[1].Time, 10}}}} ui.Box(c).Size(200, 200).Draw(func(p *ui.Painter, _ ui.Rect) { d.paint(p, g, m, 12) }) @@ -77,7 +80,7 @@ func TestDrawingToolsAllTools(t *testing.T) { v := MarketView{Count: 30} d := DrawingTools{Tool: tool, Text: "Note"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "Drawing"}, Drawings: &d}) }, 640, 340) tt.ClickAt(200, 120) diff --git a/chart/example_charts1_test.go b/chart/example_charts1_test.go new file mode 100644 index 0000000..5d78dd6 --- /dev/null +++ b/chart/example_charts1_test.go @@ -0,0 +1,222 @@ +package chart_test + +import ( + "math" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func ExampleLineChart() { + series := []chart.ChartSeries{ + {Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}}, + {Name: "Candles", Values: []float64{5, 2, 6, 7, 1}}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.LineChart(c, series, chart.LineChartOptions{Frame: chart.ChartFrame{Label: "Chapel"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAreaChart() { + series := []chart.ChartSeries{ + {Name: "Matins", Values: []float64{3, 4, 6, 5}}, + {Name: "Vespers", Values: []float64{2, 3, 2, 4}}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.AreaChart(c, series, chart.AreaChartOptions{Frame: chart.ChartFrame{Label: "Attendance"}, Stacked: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBarChart() { + categories := []string{"Nave", "Choir", "Crypt"} + series := []chart.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.BarChart(c, categories, series, chart.BarChartOptions{Frame: chart.ChartFrame{Label: "Tithes"}, Labels: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePieChart() { + slices := []chart.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.PieChart(c, slices, chart.PieChartOptions{Frame: chart.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleScatterChart() { + var selected []chart.ScatterKey + groups := []chart.ScatterGroup{{Name: "Monks", Points: []chart.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ScatterChart(c, &selected, groups, chart.ScatterChartOptions{Frame: chart.ChartFrame{Label: "Choir"}, Bubbles: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRadarChart() { + axes := []string{"Voice", "Latin", "Chant", "Organ"} + series := []chart.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.RadarChart(c, axes, series, chart.RadarChartOptions{Frame: chart.ChartFrame{Label: "Skills"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHeatmapChart() { + rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"} + values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.HeatmapChart(c, rows, cols, values, chart.HeatmapChartOptions{Frame: chart.ChartFrame{Label: "Hours"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCalendarHeatmap() { + year := 2026 + values := map[core.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.CalendarHeatmap(c, &year, values, chart.CalendarHeatmapOptions{Frame: chart.ChartFrame{Label: "Prayers"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHistogram() { + values := []float64{1, 2, 2, 3, 4, 5, 9, 10} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.Histogram(c, values, chart.HistogramOptions{Frame: chart.ChartFrame{Label: "Ages"}, BinWidth: 5}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBoxPlot() { + groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.BoxPlot(c, groups, chart.BoxPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWaterfallChart() { + steps := []chart.WaterfallStep{ + {Label: "Tithes", Value: 12}, + {Label: "Repairs", Value: -5}, + {Label: "Year", Kind: chart.WaterfallTotal}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.WaterfallChart(c, steps, chart.WaterfallChartOptions{Frame: chart.ChartFrame{Label: "Ledger"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFunnelChart() { + stages := []chart.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.FunnelChart(c, stages, chart.FunnelChartOptions{Frame: chart.ChartFrame{Label: "Order"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBulletChart() { + target := 250.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.BulletChart(c, 270, chart.BulletChartOptions{Frame: chart.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSparkline() { + values := []float64{3, 5, 4, 7, 6, 9} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.Sparkline(c, values, chart.SparklineOptions{Frame: chart.ChartFrame{Label: "Bells"}, Kind: chart.SparklineArea}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleGauge() { + segments := []chart.GaugeSegment{{To: 60, Label: "Calm", Tone: chart.GaugeGood}, {To: 100, Label: "Crowded", Tone: chart.GaugeDanger}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.Gauge(c, 72, chart.GaugeOptions{Frame: chart.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTreemap() { + var path []string + root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, + {Name: "Kitchen", Value: 25}, + }} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.Treemap(c, &path, root, chart.TreemapOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSunburst() { + var path []string + root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, + {Name: "Kitchen", Value: 25}, + }} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.Sunburst(c, &path, root, chart.SunburstOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSankeyChart() { + nodes := []chart.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} + links := []chart.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.SankeyChart(c, nodes, links, chart.SankeyChartOptions{Frame: chart.ChartFrame{Label: "Money"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleNetworkGraph() { + var state chart.NetworkGraphState + nodes := []chart.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} + edges := []chart.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.NetworkGraph(c, &state, nodes, edges, chart.NetworkGraphOptions{Frame: chart.ChartFrame{Label: "House"}, Force: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRealtimeChart() { + buf := chart.NewRealtimeBuffer(600, "Bell") + start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + for i := range 60 { + buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7)) + } + var state chart.RealtimeChartState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.RealtimeChart(c, &state, buf, chart.RealtimeChartOptions{Frame: chart.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/chart/example_charts2_test.go b/chart/example_charts2_test.go new file mode 100644 index 0000000..ea06561 --- /dev/null +++ b/chart/example_charts2_test.go @@ -0,0 +1,183 @@ +package chart_test + +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" +) + +func ExampleChartLegend() { + hidden := map[string]bool{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + k := core.Tokens(c) + items := []chart.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} + chart.ChartLegend(c, &hidden, items, chart.ChartLegendOptions{MaxItems: 1}) + } + ui.Render(view, 400, 200, 1) +} + +func ExampleChartTooltip() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + k := core.Tokens(c) + chart.ChartTooltip(c, 40, 40, []chart.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) + } + ui.Render(view, 400, 200, 1) +} + +func ExampleChartCrosshair() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + hair := chart.ChartCrosshair(c, chart.ChartCrosshairOptions{X: &x, Y: &y}) + ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { + plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} + x.Draw(p, plot) + y.Draw(p, plot) + hair.Draw(p, plot, x.Map(plot, 5), y.Map(plot, 70)) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChartAxis() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + axis := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Kind: chart.AxisLog, Min: 1, Max: 1000}) + ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { + axis.Draw(p, ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChartGrid() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + grid := chart.ChartGrid(c, chart.ChartGridOptions{X: &x, Y: &y, Minor: 2}) + ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { + plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} + grid.Draw(p, plot) + x.Draw(p, plot) + y.Draw(p, plot) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChartBrush() { + value := [2]float64{20, 60} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ChartBrush(c, &value, chart.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) + } + ui.Render(view, 400, 160, 1) +} + +func ExampleChartAnnotation() { + notes := []chart.ChartNote{{Kind: chart.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: chart.NoteBand, Value: 6, End: 8, Text: "Lent"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + annotation := chart.ChartAnnotation(c, notes, chart.ChartAnnotationOptions{X: &x, Y: &y}) + box := ui.Box(c).Fill().Focusable().Label("Annotations") + b := box.Bounds() + annotation.Drag(box, ui.Rect{X: 60, Y: 20, W: b.W - 80, H: b.H - 60}) + box.Draw(func(p *ui.Painter, r ui.Rect) { + plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} + x.Draw(p, plot) + y.Draw(p, plot) + annotation.Draw(p, plot) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChartExport() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + data, err := chart.ChartExport(c, func(c *ui.Context) { + chart.SparkBar(c, []float64{3, -2, 5}, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}}) + }, chart.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) + if err != nil { + panic(err) + } + bitmap, err := ui.DecodeBitmap(data) + if err != nil { + panic(err) + } + ui.Image(c, bitmap).Size(300, 100) + } + ui.Render(view, 400, 160, 1) +} + +func ExampleSparkBar() { + values := []float64{3, -2, 4, 1, -3} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.SparkBar(c, values, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}, BarWidth: 10}) + } + ui.Render(view, 400, 100, 1) +} + +func ExampleTrendIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.TrendIndicator(c, 12.5, chart.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) + } + ui.Render(view, 400, 100, 1) +} + +func ExampleChartEmptyState() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ChartEmptyState(c, data.DataFailed, chart.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) + } + ui.Render(view, 400, 240, 1) +} + +func ExampleViolinPlot() { + groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ViolinPlot(c, groups, chart.ViolinPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleParallelCoordinates() { + filters := map[string][2]float64{} + axes := []string{"Age", "Hymns", "Vigils"} + rows := [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ParallelCoordinates(c, &filters, axes, rows, chart.ParallelCoordinatesOptions{Frame: chart.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChordDiagram() { + names := []string{"Nave", "Choir", "Crypt"} + matrix := [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.ChordDiagram(c, names, matrix, chart.ChordDiagramOptions{Frame: chart.ChartFrame{Label: "Paths"}}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDensityPlot() { + groups := []chart.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chart.DensityPlot(c, groups, chart.DensityPlotOptions{Frame: chart.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/chart/example_markets_test.go b/chart/example_markets_test.go new file mode 100644 index 0000000..24629e7 --- /dev/null +++ b/chart/example_markets_test.go @@ -0,0 +1,23 @@ +package chart_test + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func ExampleMarketHeatmap() { + var path []string + root := chart.HeatNode{Name: "Market", Children: []chart.HeatNode{{Name: "Technology", Children: []chart.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := chart.MarketHeatmap(c, &path, root, chart.MarketHeatmapOptions{Frame: chart.ChartFrame{Label: "Market"}}) + if r.Changed() || r.ValueChanged() || r.Retried() { + fmt.Println(path, r.Point) + } + } + ui.Render(view, 960, 440, 1) +} diff --git a/example_mktchart_test.go b/chart/example_mktchart_test.go similarity index 50% rename from example_mktchart_test.go rename to chart/example_mktchart_test.go index 4ef3be8..c0f6790 100644 --- a/example_mktchart_test.go +++ b/chart/example_mktchart_test.go @@ -1,107 +1,111 @@ -package mujica_test +package chart_test import ( - "github.com/egoist/mygo/ui" - mujica "mujicaui" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func ExampleCandlestickChart() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) } func ExampleOHLCChart() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OHLCChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.OHLCChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) } func ExampleHeikinAshiChart() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView raw := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeikinAshiChart(c, candles, &window, &raw, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.HeikinAshiChart(c, candles, &window, &raw, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) } func ExampleLineQuoteChart() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - points := []mujica.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} + points := []chart.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineQuoteChart(c, points, mujica.LineQuoteChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) + core.Use(c, core.Settings{}) + chart.LineQuoteChart(c, points, chart.LineQuoteChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) } ui.Render(view, 640, 400, 1) } func ExampleVolumeChart() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeChart(c, candles, &window, mujica.VolumeChartOptions{Frame: mujica.ChartFrame{Label: "Volume"}}) + core.Use(c, core.Settings{}) + chart.VolumeChart(c, candles, &window, chart.VolumeChartOptions{Frame: chart.ChartFrame{Label: "Volume"}}) } ui.Render(view, 640, 400, 1) } func ExampleDepthChart() { - bids := []mujica.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} - asks := []mujica.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} + bids := []chart.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} + asks := []chart.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.DepthChart(c, bids, asks, chart.DepthChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) } func ExampleVolumeProfile() { - bins := []mujica.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} + bins := []chart.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.VolumeProfile(c, bins, chart.VolumeProfileOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) } func ExampleMultiChartLayout() { - state := mujica.MultiChartState{Count: 2} + state := chart.MultiChartState{Count: 2} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiChartLayout(c, &state, mujica.MultiChartLayoutOptions{Height: 200}, func(i int, cell *mujica.ChartCell) { - mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: "Symbol"}) + core.Use(c, core.Settings{}) + chart.MultiChartLayout(c, &state, chart.MultiChartLayoutOptions{Height: 200}, func(i int, cell *chart.ChartCell) { + input.TextInput(c, &cell.Symbol, input.TextInputOptions{Label: "Symbol"}) }) } ui.Render(view, 640, 400, 1) @@ -109,146 +113,146 @@ func ExampleMultiChartLayout() { func ExampleChartSync() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - var sync mujica.ChartSync + var window chart.MarketView + var sync chart.ChartSync view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) sync.Link(c, "price") - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) } ui.Render(view, 640, 400, 1) } func ExampleTimeRangeSelector() { - value := mujica.MarketRange{Preset: mujica.Range1M} + value := chart.MarketRange{Preset: chart.Range1M} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeRangeSelector(c, &value, mujica.TimeRangeSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.TimeRangeSelector(c, &value, chart.TimeRangeSelectorOptions{}) } ui.Render(view, 640, 400, 1) } func ExampleIntervalSelector() { - value := mujica.Interval("1h") - favorites := []mujica.Interval{"1m", "1h", "1D"} + value := chart.Interval("1h") + favorites := []chart.Interval{"1m", "1h", "1D"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IntervalSelector(c, &value, &favorites, mujica.IntervalSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IntervalSelector(c, &value, &favorites, chart.IntervalSelectorOptions{}) } ui.Render(view, 640, 400, 1) } func ExampleChartTypeSwitcher() { - value := mujica.ChartCandles + value := chart.ChartCandles view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartTypeSwitcher(c, &value, mujica.ChartTypeSwitcherOptions{}) + core.Use(c, core.Settings{}) + chart.ChartTypeSwitcher(c, &value, chart.ChartTypeSwitcherOptions{}) } ui.Render(view, 640, 400, 1) } func ExampleIndicatorOverlay() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - overlays := []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{2}}} + var window chart.MarketView + overlays := []chart.IndicatorOverlay{{Kind: chart.IndicatorMA, Params: []float64{2}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) } ui.Render(view, 640, 400, 1) } func ExampleIndicatorPane() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - panes := mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorRSI, Params: []float64{2}}}} + var window chart.MarketView + panes := chart.IndicatorPanes{Panes: []chart.IndicatorPaneSpec{{Kind: chart.IndicatorRSI, Params: []float64{2}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorPane(c, candles, &window, &panes, mujica.IndicatorPaneOptions{Height: 200, Label: "BTC"}) + core.Use(c, core.Settings{}) + chart.IndicatorPane(c, candles, &window, &panes, chart.IndicatorPaneOptions{Height: 200, Label: "BTC"}) } ui.Render(view, 640, 400, 1) } func ExampleIndicatorSelector() { - var overlays []mujica.IndicatorOverlay - var panes mujica.IndicatorPanes + var overlays []chart.IndicatorOverlay + var panes chart.IndicatorPanes view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorSelector(c, &overlays, &panes, mujica.IndicatorSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IndicatorSelector(c, &overlays, &panes, chart.IndicatorSelectorOptions{}) } ui.Render(view, 640, 400, 1) } func ExampleDrawingTools() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - drawings := mujica.DrawingTools{Tool: mujica.ToolTrendLine} + var window chart.MarketView + drawings := chart.DrawingTools{Tool: chart.ToolTrendLine} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) } ui.Render(view, 640, 400, 1) } func ExampleDrawingToolbar() { - var drawings mujica.DrawingTools + var drawings chart.DrawingTools view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DrawingToolbar(c, &drawings, mujica.DrawingToolbarOptions{Horizontal: true}) + core.Use(c, core.Settings{}) + chart.DrawingToolbar(c, &drawings, chart.DrawingToolbarOptions{Horizontal: true}) } ui.Render(view, 640, 400, 1) } func ExampleCompareSymbol() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - symbols := []mujica.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} + var window chart.MarketView + symbols := []chart.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CompareLegend(c, &symbols) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) + core.Use(c, core.Settings{}) + chart.CompareLegend(c, &symbols) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) } ui.Render(view, 640, 400, 1) } func ExamplePriceAlertLine() { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - alerts := []mujica.PriceAlertLine{{ID: "take-profit", Price: 105}} + var window chart.MarketView + alerts := []chart.PriceAlertLine{{ID: "take-profit", Price: 105}} triggered := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert mujica.PriceAlertLine, last mujica.Candle) { + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert chart.PriceAlertLine, last chart.Candle) { triggered = alert.ID }}) ui.Text(c, triggered) @@ -258,8 +262,8 @@ func ExamplePriceAlertLine() { func ExampleCandleCountdown() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandleCountdown(c, "1h", mujica.CandleCountdownOptions{Location: time.UTC}) + core.Use(c, core.Settings{}) + chart.CandleCountdown(c, "1h", chart.CandleCountdownOptions{Location: time.UTC}) } ui.Render(view, 640, 400, 1) } diff --git a/funnel_chart.go b/chart/funnel_chart.go similarity index 79% rename from funnel_chart.go rename to chart/funnel_chart.go index dd7bd18..39a51f6 100644 --- a/funnel_chart.go +++ b/chart/funnel_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FunnelChartOptions configure a FunnelChart. @@ -25,10 +26,10 @@ func FunnelChart(c *ui.Context, stages []ChartSlice, opts FunnelChartOptions) Ch } top = max(top, s.Value) } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) col := internal.Palette(k)[0] - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) n := len(stages) if top == 0 { n = 0 @@ -52,7 +53,7 @@ func FunnelChart(c *ui.Context, stages []ChartSlice, opts FunnelChartOptions) Ch spec := ChartSpec{Marks: n} spec.Readout = func(i int) string { s := stages[i] - return fmt.Sprintf(msg(c, "funnel_chart.readout"), s.Label, chartValue(c, s.Value, opts.Format), rate(i), pct(s.Value, stages[0].Value)) + return fmt.Sprintf(core.Msg(c, "funnel_chart.readout"), s.Label, ChartValue(c, s.Value, opts.Format), rate(i), pct(s.Value, stages[0].Value)) } spec.Pick = func(r ui.Rect, x, y float32) int { plot, band := geom(r) @@ -76,10 +77,10 @@ func FunnelChart(c *ui.Context, stages []ChartSlice, opts FunnelChartOptions) Ch } ly := (y0 + y1) / 2 lx := plot.X + plot.W + 16 - chartText(p, s.Label+" · "+chartValue(c, s.Value, opts.Format), lx, ly, size, k.Text, 0, 1) - chartText(p, rate(i), lx, ly+2, size, k.TextMuted, 0, 0) + ChartText(p, s.Label+" · "+ChartValue(c, s.Value, opts.Format), lx, ly, size, k.Text, 0, 1) + ChartText(p, rate(i), lx, ly+2, size, k.TextMuted, 0, 0) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/funnel_chart_test.go b/chart/funnel_chart_test.go similarity index 94% rename from funnel_chart_test.go rename to chart/funnel_chart_test.go index d3dbfa6..1aa947a 100644 --- a/funnel_chart_test.go +++ b/chart/funnel_chart_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var funnelStages = []ChartSlice{{"Visitors", 1000}, {"Novices", 400}, {"Vows", 100}, {"Abbots", 0}} @@ -40,7 +42,7 @@ func TestFunnelChartBadAndEmpty(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FunnelChart(c, []ChartSlice{{"a", -1}}, FunnelChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/gauge.go b/chart/gauge.go similarity index 86% rename from gauge.go rename to chart/gauge.go index b2f3745..225f0a2 100644 --- a/gauge.go +++ b/chart/gauge.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // GaugeTone colors a Gauge segment; its label still names it. @@ -54,13 +55,13 @@ func Gauge(c *ui.Context, value float64, opts GaugeOptions) ChartResult { if opts.Frame.Height == 0 { opts.Frame.Height = 200 } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) tone := func(t GaugeTone) ui.Color { return map[GaugeTone]ui.Color{GaugeNeutral: internal.Palette(k)[5], GaugeGood: k.Success, GaugeWarning: k.Warning, GaugeDanger: k.Danger}[t] } - val := func(v float64) string { return chartValue(c, v, opts.Format) } + val := func(v float64) string { return ChartValue(c, v, opts.Format) } seg := -1 for i, s := range opts.Segments { if value <= s.To && seg < 0 { @@ -130,7 +131,7 @@ func Gauge(c *ui.Context, value float64, opts GaugeOptions) ChartResult { p.StrokePath(internal.Sector(cx, cy, rad*0.78-grow, rad+grow, from, to), 1.5, k.Surface) lx, ly := internal.Polar(cx, cy, rad+10, to) if i < len(opts.Segments)-1 { - chartText(p, val(s.To), lx, ly, size, k.TextMuted, 0.5, 1) + ChartText(p, val(s.To), lx, ly, size, k.TextMuted, 0.5, 1) } from = to } @@ -144,11 +145,11 @@ func Gauge(c *ui.Context, value float64, opts GaugeOptions) ChartResult { p.StrokePath(&needle, w, k.Text) p.FillPath(internal.Diamond(cx, cy, 12), k.Text) ty := cy + 34 - chartText(p, val(value)+" "+opts.Unit, cx, ty, FontSize(c, theme.H3Size), k.Text, 0.5, 1) + ChartText(p, val(value)+" "+opts.Unit, cx, ty, core.FontSize(c, theme.H3Size), k.Text, 0.5, 1) if seg >= 0 { - chartText(p, opts.Segments[seg].Label, cx, ty+2, size, k.TextMuted, 0.5, 0) + ChartText(p, opts.Segments[seg].Label, cx, ty+2, size, k.TextMuted, 0.5, 0) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/gauge_test.go b/chart/gauge_test.go similarity index 92% rename from gauge_test.go rename to chart/gauge_test.go index 5f87b6a..215af5b 100644 --- a/gauge_test.go +++ b/chart/gauge_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var gaugeSegments = []GaugeSegment{{60, "Calm", GaugeGood}, {85, "Busy", GaugeWarning}, {100, "Crowded", GaugeDanger}} @@ -56,7 +58,7 @@ func TestGaugeBad(t *testing.T) { } }() o.Frame.Label = "x" - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Gauge(c, 0, o) }, 300, 300) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); Gauge(c, 0, o) }, 300, 300) }() } } diff --git a/heatmap_chart.go b/chart/heatmap_chart.go similarity index 82% rename from heatmap_chart.go rename to chart/heatmap_chart.go index 04880b3..167d338 100644 --- a/heatmap_chart.go +++ b/chart/heatmap_chart.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // HeatmapChartOptions configure a HeatmapChart. @@ -34,9 +35,9 @@ func HeatmapChart(c *ui.Context, rows, cols []string, values [][]float64, opts H } } } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) n := len(rows) * len(cols) if math.IsInf(lo, 1) { n = 0 @@ -57,7 +58,7 @@ func HeatmapChart(c *ui.Context, rows, cols []string, values [][]float64, opts H } spec := ChartSpec{Marks: n} spec.Readout = func(i int) string { - return rows[i/len(cols)] + " · " + cols[i%len(cols)] + " · " + chartValue(c, values[i/len(cols)][i%len(cols)], opts.Format) + return rows[i/len(cols)] + " · " + cols[i%len(cols)] + " · " + ChartValue(c, values[i/len(cols)][i%len(cols)], opts.Format) } spec.Move = func(i int, key ui.Key) int { r, col := i/len(cols), i%len(cols) @@ -93,7 +94,7 @@ func HeatmapChart(c *ui.Context, rows, cols []string, values [][]float64, opts H bg := internal.Ramp(k, (float64(level(v))+0.5)/heatSteps) p.Fill(b, bg, 2) if b.W >= 30 && b.H >= 18 { - chartText(p, chartValue(c, v, opts.Format), b.X+b.W/2, b.Y+b.H/2, size, chartInk(k, bg), 0.5, 0.5) + ChartText(p, ChartValue(c, v, opts.Format), b.X+b.W/2, b.Y+b.H/2, size, ChartInk(k, bg), 0.5, 0.5) } } if i == active { @@ -101,14 +102,14 @@ func HeatmapChart(c *ui.Context, rows, cols []string, values [][]float64, opts H } } for i, name := range rows { - chartText(p, name, plot.X-8, yb.Center(i), size, k.TextMuted, 1, 0.5) + ChartText(p, name, plot.X-8, yb.Center(i), size, k.TextMuted, 1, 0.5) } for i, name := range cols { - chartText(p, name, xb.Center(i), plot.Y+plot.H+4, size, k.TextMuted, 0.5, 0) + ChartText(p, name, xb.Center(i), plot.Y+plot.H+4, size, k.TextMuted, 0.5, 0) } heatLegend(p, ui.Rect{X: plot.X + plot.W + 16, Y: plot.Y, W: 14, H: plot.H}, lo, hi, size, k, opts.Format, c) } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } @@ -118,6 +119,6 @@ func heatLegend(p *ui.Painter, r ui.Rect, lo, hi float64, size float32, k theme. for i := range heatSteps { p.Fill(ui.Rect{X: r.X, Y: r.Y + float32(heatSteps-1-i)*h, W: r.W, H: h - 1}, internal.Ramp(k, (float64(i)+0.5)/heatSteps), 1) } - chartText(p, chartValue(c, hi, format), r.X+r.W+4, r.Y, size, k.TextMuted, 0, 0) - chartText(p, chartValue(c, lo, format), r.X+r.W+4, r.Y+h*heatSteps, size, k.TextMuted, 0, 1) + ChartText(p, ChartValue(c, hi, format), r.X+r.W+4, r.Y, size, k.TextMuted, 0, 0) + ChartText(p, ChartValue(c, lo, format), r.X+r.W+4, r.Y+h*heatSteps, size, k.TextMuted, 0, 1) } diff --git a/heatmap_chart_test.go b/chart/heatmap_chart_test.go similarity index 95% rename from heatmap_chart_test.go rename to chart/heatmap_chart_test.go index 4b6b10d..8560f3b 100644 --- a/heatmap_chart_test.go +++ b/chart/heatmap_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var heatRows = []string{"Mon", "Tue", "Wed"} @@ -54,7 +56,7 @@ func TestHeatmapChartBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) HeatmapChart(c, heatRows[:1], heatCols, [][]float64{{1}}, HeatmapChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/heikin_ashi_chart_test.go b/chart/heikin_ashi_chart_test.go similarity index 77% rename from heikin_ashi_chart_test.go rename to chart/heikin_ashi_chart_test.go index 40c9f2f..f28d3fe 100644 --- a/heikin_ashi_chart_test.go +++ b/chart/heikin_ashi_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestHeikinAshiChartSwitchesToRaw(t *testing.T) { @@ -12,7 +14,7 @@ func TestHeikinAshiChartSwitchesToRaw(t *testing.T) { view := MarketView{Count: 30} raw := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) HeikinAshiChart(c, candles, &view, &raw, CandlestickOptions{Frame: ChartFrame{Label: "HA"}}) }, 600, 360) ha := HeikinAshi(candles) @@ -27,7 +29,7 @@ func TestHeikinAshiChartSwitchesToRaw(t *testing.T) { tt.Key(0, ui.KeyTab) // the switch tt.Key(0, ui.KeyTab) // the plot tt.Key(0, ui.KeyHome) - if want := "O " + priceUngrouped(ha[0].Open, 2); !strings.Contains(readout(), want) { + if want := "O " + core.PriceUngrouped(ha[0].Open, 2); !strings.Contains(readout(), want) { t.Fatalf("Heikin-Ashi readout %q, want %q", readout(), want) } if err := tt.Click("Candles"); err != nil || !raw { @@ -35,7 +37,7 @@ func TestHeikinAshiChartSwitchesToRaw(t *testing.T) { } tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyHome) - if want := "O " + priceUngrouped(candles[0].Open, 2); !strings.Contains(readout(), want) { + if want := "O " + core.PriceUngrouped(candles[0].Open, 2); !strings.Contains(readout(), want) { t.Fatalf("raw readout %q, want %q", readout(), want) } defer func() { @@ -43,7 +45,10 @@ func TestHeikinAshiChartSwitchesToRaw(t *testing.T) { t.Fatal("nil raw accepted") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); HeikinAshiChart(c, candles, &view, nil, CandlestickOptions{}) }, 200, 100) + ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + HeikinAshiChart(c, candles, &view, nil, CandlestickOptions{}) + }, 200, 100) } func TestHeikinAshiChartSnapshot(t *testing.T) { diff --git a/chart/helpers_test.go b/chart/helpers_test.go new file mode 100644 index 0000000..f569206 --- /dev/null +++ b/chart/helpers_test.go @@ -0,0 +1,70 @@ +package chart + +import ( + "image" + "image/color" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// tabTo presses Tab until the element labeled label has the focus. +func tabTo(t *testing.T, tt *ui.Tester, label string) { + t.Helper() + for range 60 { + if tt.Focused(label) { + return + } + tt.Key(0, ui.KeyTab) + } + t.Fatalf("Tab never reached %q", label) +} + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func toRGBA(c ui.Color) color.RGBA { return color.RGBA{c.R, c.G, c.B, c.A} } + +func encode(t *testing.T, img image.Image) []byte { + t.Helper() + return snaptest.Encode(t, img) +} + +func mktHeat() HeatNode { + return HeatNode{Name: "Market", Children: []HeatNode{ + {Name: "Technology", Children: []HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}, {Name: "NVDA", Cap: 2200, Change: 3.4}}}, + {Name: "Energy", Children: []HeatNode{{Name: "XOM", Cap: 450, Change: -2.1}, {Name: "CVX", Cap: 300, Change: -1.4}}}, + {Name: "Health", Children: []HeatNode{{Name: "LLY", Cap: 700, Change: 0.4}, {Name: "JNJ", Cap: 380, Change: 0}}}, + }} +} + +func chatTester(w, h int, view func(c *ui.Context)) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Column(c).Padding(10).Fill().Children(func() { view(c) }) + }, w, h) +} + +func marketsPanic(t *testing.T, view func(*ui.Context)) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatal("expected panic") + } + }() + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); view(c) }, 960, 500, 1) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/histogram.go b/chart/histogram.go similarity index 91% rename from histogram.go rename to chart/histogram.go index a864b72..001bbc1 100644 --- a/histogram.go +++ b/chart/histogram.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // HistogramOptions configure a Histogram. @@ -82,11 +83,11 @@ func Histogram(c *ui.Context, values []float64, opts HistogramOptions) ChartResu if opts.Bins < 0 || opts.BinWidth < 0 || math.IsNaN(opts.BinWidth) { panic("mujica: Histogram Bins and BinWidth must be ≥ 0") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) col := internal.Palette(k)[0] - size := FontSize(c, theme.CaptionSize) - b := chartMemo(ch, "bins", [4]any{chartID(values), opts.Bins, opts.BinWidth, opts.Cumulative}, func() histBins { + size := core.FontSize(c, theme.CaptionSize) + b := ChartMemo(ch, "bins", [4]any{ChartID(values), opts.Bins, opts.BinWidth, opts.Cumulative}, func() histBins { return histogramBin(values, opts.Bins, opts.BinWidth, opts.Cumulative) }) top := 0 @@ -112,7 +113,7 @@ func Histogram(c *ui.Context, values []float64, opts HistogramOptions) ChartResu return s } geom := func(r ui.Rect) (ui.Rect, internal.Band) { - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) ys = internal.Linear{D0: 0, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} return plot, internal.Band{N: len(b.counts), R0: plot.X, R1: plot.X + plot.W, Pad: 0.06} } @@ -152,6 +153,6 @@ func Histogram(c *ui.Context, values []float64, opts HistogramOptions) ChartResu } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/histogram_test.go b/chart/histogram_test.go similarity index 92% rename from histogram_test.go rename to chart/histogram_test.go index 514bbdd..ba3a380 100644 --- a/histogram_test.go +++ b/chart/histogram_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestHistogramBin(t *testing.T) { @@ -58,7 +60,7 @@ func TestHistogramBad(t *testing.T) { } }() o.Frame.Label = "x" - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Histogram(c, []float64{0, 1}, o) }, 300, 300) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); Histogram(c, []float64{0, 1}, o) }, 300, 300) }() } } diff --git a/indicator_overlay.go b/chart/indicator_overlay.go similarity index 88% rename from indicator_overlay.go rename to chart/indicator_overlay.go index 8531155..36e7f09 100644 --- a/indicator_overlay.go +++ b/chart/indicator_overlay.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // IndicatorKind names a technical indicator. @@ -137,8 +139,8 @@ func mktOverlayLines(ch *chart, m *mktCore, candles []Candle) []mktLine { out = append(out, mktLine{name: indicatorName(o.Kind, indicatorParams(o.Kind, o.Params)), values: o.Values, color: col, shape: len(out)}) continue } - key := fmt.Sprintf("%s|%d|%v", chartID(candles), o.Kind, o.Params) - series := chartMemo(ch, fmt.Sprint("overlay.", i), key, func() []ChartSeries { return indicatorSeries(o.Kind, o.Params, candles) }) + key := fmt.Sprintf("%s|%d|%v", ChartID(candles), o.Kind, o.Params) + series := ChartMemo(ch, fmt.Sprint("overlay.", i), key, func() []ChartSeries { return indicatorSeries(o.Kind, o.Params, candles) }) for _, s := range series { out = append(out, mktLine{name: s.Name, values: s.Values, color: col, shape: len(out)}) } @@ -147,11 +149,11 @@ func mktOverlayLines(ch *chart, m *mktCore, candles []Candle) []mktLine { } // IndicatorParamsField edits validated indicator parameters in place. -func IndicatorParamsField(c *ui.Context, kind IndicatorKind, params *[]float64) ChoiceResult { +func IndicatorParamsField(c *ui.Context, kind IndicatorKind, params *[]float64) core.ChoiceResult { if params == nil { panic("mujica: IndicatorParamsField needs params") } - row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Role(ui.RoleGroup).Label(kind.String()) + row := ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Role(ui.RoleGroup).Label(kind.String()) if row.IsDisabled() { copy := append([]float64(nil), (*params)...) params = © @@ -168,7 +170,7 @@ func IndicatorParamsField(c *ui.Context, kind IndicatorKind, params *[]float64) if kind == IndicatorBOLL && i == 1 { step, prec = 0.1, 1 } - if NumberInput(c, &v, NumberInputOptions{Min: step, Max: 500, Step: step, Precision: prec, Label: fmt.Sprintf("%v %d", kind, i+1)}).Element.Width(110).Key(i).Changed() || v != (*params)[i] { + if input.NumberInput(c, &v, input.NumberInputOptions{Min: step, Max: 500, Step: step, Precision: prec, Label: fmt.Sprintf("%v %d", kind, i+1)}).Element.Width(110).Key(i).Changed() || v != (*params)[i] { if kind == IndicatorMACD && (i == 0 && v >= (*params)[1] || i == 1 && v <= (*params)[0]) { continue } @@ -179,5 +181,5 @@ func IndicatorParamsField(c *ui.Context, kind IndicatorKind, params *[]float64) if changed { slog.Debug("mujica: indicator_overlay params", "kind", kind) } - return ChoiceResult{row, changed} + return core.NewChoiceResult(row, changed) } diff --git a/indicator_overlay_test.go b/chart/indicator_overlay_test.go similarity index 92% rename from indicator_overlay_test.go rename to chart/indicator_overlay_test.go index 06d7c32..00c2a53 100644 --- a/indicator_overlay_test.go +++ b/chart/indicator_overlay_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIndicatorOverlayValuesInReadoutAndMemo(t *testing.T) { @@ -12,7 +14,7 @@ func TestIndicatorOverlayValuesInReadoutAndMemo(t *testing.T) { view := MarketView{Count: 60} overlays := []IndicatorOverlay{{Kind: IndicatorMA, Params: []float64{5}}, {Kind: IndicatorBOLL}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}, Overlays: overlays}) }, 800, 400) if !tt.HasText("MA(5)") || !tt.HasText("BOLL(20,2) ↑") { @@ -21,7 +23,7 @@ func TestIndicatorOverlayValuesInReadoutAndMemo(t *testing.T) { tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnd) ma := SMA(closesOf(candles), 5) - want := "MA(5) " + priceUngrouped(ma[199], 2) + want := "MA(5) " + core.PriceUngrouped(ma[199], 2) found := false for _, s := range tt.Texts() { found = found || strings.Contains(s, want) @@ -52,7 +54,7 @@ func TestIndicatorOverlayValuesInReadoutAndMemo(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "x"}, Overlays: []IndicatorOverlay{o}}) }, 300, 200) }() @@ -70,7 +72,7 @@ func closesOf(cs []Candle) []float64 { func TestIndicatorParamsField(t *testing.T) { var params []float64 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IndicatorParamsField(c, IndicatorMACD, ¶ms) }, 500, 100) if len(params) != 3 || params[0] != 12 { diff --git a/indicator_pane.go b/chart/indicator_pane.go similarity index 80% rename from indicator_pane.go rename to chart/indicator_pane.go index 285bffb..0e9daaf 100644 --- a/indicator_pane.go +++ b/chart/indicator_pane.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IndicatorPaneSpec is one lower pane: MACD, RSI, KDJ or OBV. @@ -34,7 +37,7 @@ type IndicatorPaneOptions struct { // Label prefixes each pane's accessible name, as the symbol. Label string Disabled bool - Status DataStatus + Status data.DataStatus Error string Empty string } @@ -81,8 +84,8 @@ func IndicatorPane(c *ui.Context, candles []Candle, view *MarketView, panes *Ind if len(panes.Sizes) != n { panes.Sizes = nil } - u := Density(c).Unit() - lineH := FontSize(c, theme.CaptionSize) * theme.LineHeight + u := core.Density(c).Unit() + lineH := core.FontSize(c, theme.CaptionSize) * theme.LineHeight chrome := 2*max(theme.MinHitSize, lineH) + max(u*5, lineH) + 4*u minimum := chrome + 40 h := opts.Height @@ -114,11 +117,11 @@ func IndicatorPane(c *ui.Context, candles []Candle, view *MarketView, panes *Ind build(0)() return } - list := make([]ResizablePanel, n) + list := make([]input.ResizablePanel, n) for i, p := range panes.Panes { - list[i] = ResizablePanel{Label: indicatorName(p.Kind, indicatorParams(p.Kind, p.Params)), Min: minimum, Build: build(i)} + list[i] = input.ResizablePanel{Label: indicatorName(p.Kind, indicatorParams(p.Kind, p.Params)), Min: minimum, Build: build(i)} } - r := ResizablePanelGroup(c, &panes.Sizes, list, ResizablePanelGroupOptions{Vertical: true}) + r := input.ResizablePanelGroup(c, &panes.Sizes, list, input.ResizablePanelGroupOptions{Vertical: true}) r.Element.Fill() res.Resized = r.Committed }) @@ -134,17 +137,17 @@ func IndicatorPane(c *ui.Context, candles []Candle, view *MarketView, panes *Ind // indicatorPaneOne draws one indicator and reports its events. func indicatorPaneOne(c *ui.Context, candles []Candle, view *MarketView, spec IndicatorPaneSpec, opts IndicatorPaneOptions, height float32) (bool, ChartResult) { - k := Tokens(c) + k := core.Tokens(c) params := indicatorParams(spec.Kind, spec.Params) name := indicatorName(spec.Kind, params) closed := false - ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit()).Children(func() { - ui.Text(c, name).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.Text).Grow(1) - closed = iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "indicator_pane.remove"), name)).Clicked() + ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit()).Children(func() { + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.Text).Grow(1) + closed = core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "indicator_pane.remove"), name)).Clicked() }) label := strings.TrimSpace(opts.Label + " " + name) - ch := newChart(c, ChartFrame{Label: label, Height: height, Status: opts.Status, Error: opts.Error, Empty: opts.Empty}) - series := chartMemo(ch, "series", fmt.Sprintf("%s|%v|%v", chartID(candles), spec.Kind, params), func() []ChartSeries { + ch := NewChart(c, ChartFrame{Label: label, Height: height, Status: opts.Status, Error: opts.Error, Empty: opts.Empty}) + series := ChartMemo(ch, "series", fmt.Sprintf("%s|%v|%v", ChartID(candles), spec.Kind, params), func() []ChartSeries { marketCheckCandles(candles) return indicatorSeries(spec.Kind, params, candles) }) @@ -157,7 +160,7 @@ func indicatorPaneOne(c *ui.Context, candles []Candle, view *MarketView, spec In lines[i] = mktLine{name: s.Name, values: s.Values, color: pal[(i+1)%len(pal)], shape: i} keys[i] = ChartLegendItem{Name: s.Name, Color: lines[i].color, Shape: i} } - yr := chartMemo(ch, "y", [5]any{chartID(candles), m.from, m.to, spec.Kind, fmt.Sprint(params)}, func() [2]float64 { + yr := ChartMemo(ch, "y", [5]any{ChartID(candles), m.from, m.to, spec.Kind, fmt.Sprint(params)}, func() [2]float64 { lo, hi := math.Inf(1), math.Inf(-1) for _, l := range lines { for i := m.from; i < m.to; i++ { @@ -187,13 +190,13 @@ func indicatorPaneOne(c *ui.Context, candles []Candle, view *MarketView, spec In spec2.Readout = func(i int) string { parts := []string{candles[i].Time.Format("2006-01-02 15:04")} for _, l := range lines { - parts = append(parts, l.name+" "+chartValue(c, l.values[i], func(v float64) string { return fmt.Sprintf("%.2f", v) })) + parts = append(parts, l.name+" "+ChartValue(c, l.values[i], func(v float64) string { return fmt.Sprintf("%.2f", v) })) } return strings.Join(parts, " · ") } spec2.Draw = func(p *ui.Painter, r ui.Rect, active int) { g := m.paneGeom(r) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) internal.AxisY(p, g.plot, g.ys, g.ticks, func(v float64) string { return volumeShort(v, g.step) }, size, k) p.Clip(g.plot, 0, func() { for i, l := range lines { @@ -209,21 +212,21 @@ func indicatorPaneOne(c *ui.Context, candles []Candle, view *MarketView, spec In p.Line(x, g.plot.Y, x, g.plot.Y+g.plot.H, 1, k.TextMuted) } } - m.st = *ui.Local(ch.res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1, alertFocus: -1} }) - if ch.res.Element.IsDisabled() { + m.st = *ui.Local(ch.Res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1, alertFocus: -1} }) + if ch.Res.Element.IsDisabled() { copy := *view m.view = © } - result, plot := ch.build(spec2) + result, plot := ch.Build(spec2) if plot != nil { m.input(plot) } - return closed && !ch.res.Element.IsDisabled(), result + return closed && !ch.Res.Element.IsDisabled(), result } // paneGeom is geom without the time axis, for lower panes. func (m *mktCore) paneGeom(r ui.Rect) mktGeom { - plot := chartInset(r, 60, 4) + plot := ChartInset(r, 60, 4) lo, hi, step := internal.Nice(m.lo, m.hi, 2) return mktGeom{plot: plot, band: internal.Band{N: m.to - m.from, R0: plot.X, R1: plot.X + plot.W, Pad: 0.3}, ys: internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)}, ticks: internal.Ticks(lo, hi, step), step: step} @@ -231,7 +234,7 @@ func (m *mktCore) paneGeom(r ui.Rect) mktGeom { // indicatorPaneBars draws MACD's histogram: hollow bars above zero, filled below. func indicatorPaneBars(p *ui.Painter, g mktGeom, m *mktCore, vals []float64, c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) zero := g.ys.Map(0) w := max(1, g.band.Width()) for i := m.from; i < m.to; i++ { @@ -244,9 +247,9 @@ func indicatorPaneBars(p *ui.Painter, g mktGeom, m *mktCore, vals []float64, c * bar := ui.Rect{X: x - w/2, Y: min(y, zero), W: w, H: max(1, float32(math.Abs(float64(y-zero))))} if v >= 0 && w >= 3 { p.Fill(bar, k.Surface, 0) - p.Stroke(bar, trendColor(c, 1), 0, 1) + p.Stroke(bar, core.TrendColor(c, 1), 0, 1) } else { - p.Fill(bar, trendColor(c, trendSign(v)), 0) + p.Fill(bar, core.TrendColor(c, core.TrendSign(v)), 0) } } } diff --git a/indicator_pane_test.go b/chart/indicator_pane_test.go similarity index 88% rename from indicator_pane_test.go rename to chart/indicator_pane_test.go index 332e2ac..e80cd6f 100644 --- a/indicator_pane_test.go +++ b/chart/indicator_pane_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIndicatorPaneStackReadAndRemove(t *testing.T) { @@ -13,7 +15,7 @@ func TestIndicatorPaneStackReadAndRemove(t *testing.T) { panes := IndicatorPanes{Panes: []IndicatorPaneSpec{{Kind: IndicatorMACD}, {Kind: IndicatorRSI, Params: []float64{6}}}} removed := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := IndicatorPane(c, candles, &view, &panes, IndicatorPaneOptions{Label: "BTC"}); r.Removed >= 0 { removed = r.Removed } @@ -48,7 +50,10 @@ func TestIndicatorPaneStackReadAndRemove(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); IndicatorPane(c, candles, &view, &p, IndicatorPaneOptions{}) }, 300, 200) + ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + IndicatorPane(c, candles, &view, &p, IndicatorPaneOptions{}) + }, 300, 200) }() } } diff --git a/indicator_selector.go b/chart/indicator_selector.go similarity index 62% rename from indicator_selector.go rename to chart/indicator_selector.go index 2e687ba..c3c0243 100644 --- a/indicator_selector.go +++ b/chart/indicator_selector.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IndicatorSelectorOptions configure an IndicatorSelector. @@ -18,12 +21,12 @@ type IndicatorSelectorOptions struct { } // IndicatorSelector searches, adds, removes, and configures chart indicators. -func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *IndicatorPanes, opts IndicatorSelectorOptions) ChoiceResult { +func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *IndicatorPanes, opts IndicatorSelectorOptions) core.ChoiceResult { if overlays == nil || panes == nil { panic("mujica: IndicatorSelector needs overlays and panes") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() changed := false box := ui.Row(c).Disabled(opts.Disabled) box.Children(func() { @@ -32,14 +35,14 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic if box.IsDisabled() { *open = false } - label := fmt.Sprintf(msg(c, "indicator_selector.button"), len(*overlays)+len(panes.Panes)) - trigger := Button(c, label, ButtonOptions{Variant: Secondary, Icon: icons.Must("plus")}) + label := fmt.Sprintf(core.Msg(c, "indicator_selector.button"), len(*overlays)+len(panes.Panes)) + trigger := input.Button(c, label, input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("plus")}) if trigger.Clicked() { *open = !*open } - Popover(c, trigger, open, PopoverOptions{Title: msg(c, "indicator_selector.title"), Width: 340}, func() { - TextInput(c, query, TextInputOptions{Placeholder: msg(c, "indicator_selector.search"), Label: msg(c, "indicator_selector.search"), Clearable: true}) - ui.Text(c, msg(c, "indicator_selector.available")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u, 0, 0, 0) + overlay.Popover(c, trigger, open, overlay.PopoverOptions{Title: core.Msg(c, "indicator_selector.title"), Width: 340}, func() { + input.TextInput(c, query, input.TextInputOptions{Placeholder: core.Msg(c, "indicator_selector.search"), Label: core.Msg(c, "indicator_selector.search"), Clearable: true}) + ui.Text(c, core.Msg(c, "indicator_selector.available")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u, 0, 0, 0) found := 0 for kind := IndicatorMA; kind <= IndicatorOBV; kind++ { name := kind.String() @@ -47,14 +50,14 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic continue } found++ - ui.Row(c).Key(name).AlignItems(ui.Center).Gap(u * 2).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Row(c).Key(name).AlignItems(ui.Center).Gap(u * 2).MinHeight(core.Density(c).ControlHeight()).Children(func() { ui.Text(c, name).FontWeight(600).Width(56) where := "indicator_selector.pane" if indicatorInfo[kind].overlay { where = "indicator_selector.overlay" } - ui.Text(c, msg(c, where)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) - if Button(c, msg(c, "indicator_selector.add"), ButtonOptions{Variant: Ghost, Label: fmt.Sprintf(msg(c, "indicator_selector.add_named"), name)}).Clicked() { + ui.Text(c, core.Msg(c, where)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) + if input.Button(c, core.Msg(c, "indicator_selector.add"), input.ButtonOptions{Variant: input.Ghost, Label: fmt.Sprintf(core.Msg(c, "indicator_selector.add_named"), name)}).Clicked() { if indicatorInfo[kind].overlay { *overlays = append(slices.Clone(*overlays), IndicatorOverlay{Kind: kind, Params: kind.DefaultParams()}) } else { @@ -66,12 +69,12 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic }) } if found == 0 { - ui.Text(c, msg(c, "indicator_selector.none")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "indicator_selector.none")).TextColor(k.TextMuted) } if len(*overlays)+len(panes.Panes) == 0 { return } - ui.Text(c, msg(c, "indicator_selector.active")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u*2, 0, 0, 0) + ui.Text(c, core.Msg(c, "indicator_selector.active")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u*2, 0, 0, 0) remove := -1 for i := range *overlays { if indicatorSelectorActive(c, u, fmt.Sprint("o", i), (*overlays)[i].Kind, &(*overlays)[i].Params, &changed) { @@ -94,7 +97,7 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic } }) }) - return ChoiceResult{box, changed} + return core.NewChoiceResult(box, changed) } // indicatorSelectorActive edits parameters and reports removal of an indicator. @@ -106,7 +109,7 @@ func indicatorSelectorActive(c *ui.Context, u float32, key string, kind Indicato *changed = true } ui.Spacer(c) - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "indicator_pane.remove"), indicatorName(kind, *params))).Clicked() { + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "indicator_pane.remove"), indicatorName(kind, *params))).Clicked() { removed = true slog.Debug("mujica: indicator_selector remove", "kind", kind) } diff --git a/indicator_selector_test.go b/chart/indicator_selector_test.go similarity index 94% rename from indicator_selector_test.go rename to chart/indicator_selector_test.go index b826a25..06bf3c1 100644 --- a/indicator_selector_test.go +++ b/chart/indicator_selector_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIndicatorSelectorSearchAddRemoveAndDisabled(t *testing.T) { @@ -10,7 +13,7 @@ func TestIndicatorSelectorSearchAddRemoveAndDisabled(t *testing.T) { var panes IndicatorPanes disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IndicatorSelector(c, &overlays, &panes, IndicatorSelectorOptions{Disabled: disabled}) }, 640, 740) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -54,7 +57,7 @@ func TestIndicatorSelectorOverlayParams(t *testing.T) { var overlays []IndicatorOverlay var panes IndicatorPanes tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IndicatorSelector(c, &overlays, &panes, IndicatorSelectorOptions{}) }, 640, 740) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/indicators.go b/chart/indicators.go similarity index 99% rename from indicators.go rename to chart/indicators.go index f4321df..457fcc2 100644 --- a/indicators.go +++ b/chart/indicators.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" diff --git a/indicators_test.go b/chart/indicators_test.go similarity index 99% rename from indicators_test.go rename to chart/indicators_test.go index c73b94a..8a23f06 100644 --- a/indicators_test.go +++ b/chart/indicators_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" diff --git a/interval_selector.go b/chart/interval_selector.go similarity index 67% rename from interval_selector.go rename to chart/interval_selector.go index b7e430c..0d55652 100644 --- a/interval_selector.go +++ b/chart/interval_selector.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,9 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // intervalGroups sorts Intervals under their headings. @@ -28,7 +31,7 @@ type IntervalSelectorOptions struct { } // IntervalSelector groups candle periods and preserves caller-owned favorites. -func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opts IntervalSelectorOptions) ChoiceResult { +func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opts IntervalSelectorOptions) core.ChoiceResult { if value == nil || favorites == nil { panic("mujica: IntervalSelector needs a value and favorites") } @@ -37,8 +40,8 @@ func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opt panic(fmt.Sprintf("mujica: unknown Interval %q", string(iv))) } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() changed := false row := ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Disabled(opts.Disabled) disabled := row.IsDisabled() @@ -48,15 +51,15 @@ func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opt quick = append(quick, *value) } slices.SortFunc(quick, func(a, b Interval) int { return slices.Index(Intervals, a) - slices.Index(Intervals, b) }) - items := make([]ToggleItem[Interval], len(quick)) + items := make([]input.ToggleItem[Interval], len(quick)) for i, iv := range quick { - items[i] = ToggleItem[Interval]{Value: iv, Label: string(iv)} + items[i] = input.ToggleItem[Interval]{Value: iv, Label: string(iv)} } sel := []Interval{*value} - if ToggleGroup(c, &sel, items, ToggleGroupOptions{Label: msg(c, "interval_selector.label")}).Changed() && !disabled { + if input.ToggleGroup(c, &sel, items, input.ToggleGroupOptions{Label: core.Msg(c, "interval_selector.label")}).Changed() && !disabled { *value, changed = sel[0], true } - trigger := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-down"), Label: msg(c, "interval_selector.all")}) + trigger := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("chevron-down"), Label: core.Msg(c, "interval_selector.all")}) open := ui.Local(row, "interval_selector.open", func() bool { return false }) if disabled { *open = false @@ -64,9 +67,9 @@ func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opt if trigger.Clicked() && !disabled { *open = !*open } - Popover(c, trigger, open, PopoverOptions{Label: msg(c, "interval_selector.all"), Width: 220}, func() { + overlay.Popover(c, trigger, open, overlay.PopoverOptions{Label: core.Msg(c, "interval_selector.all"), Width: 220}, func() { for _, g := range intervalGroups { - ui.Text(c, msg(c, g.key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u, 0, 0, u) + ui.Text(c, core.Msg(c, g.key)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(u, 0, 0, u) for _, iv := range g.items { if intervalSelectorRow(c, k, u, iv, iv == *value, slices.Contains(*favorites, iv), favorites) { *value, changed, *open = iv, true, false @@ -78,14 +81,14 @@ func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opt if changed { slog.Debug("mujica: interval_selector changed", "interval", *value) } - return ChoiceResult{row, changed} + return core.NewChoiceResult(row, changed) } // intervalSelectorRow shows a selectable period and its favorite toggle. func intervalSelectorRow(c *ui.Context, k theme.Tokens, u float32, iv Interval, on, fav bool, favorites *[]Interval) bool { chosen := false ui.Row(c).Key(string(iv)).AlignItems(ui.Center).Gap(u).Children(func() { - b := ui.ButtonBase(c).Grow(1).MinHeight(Density(c).ControlHeight()).Padding(0, u*2).Justify(ui.Start).Radius(theme.ControlRadius).Gap(u * 2) + b := ui.ButtonBase(c).Grow(1).MinHeight(core.Density(c).ControlHeight()).Padding(0, u*2).Justify(ui.Start).Radius(theme.ControlRadius).Gap(u * 2) internal.Ring(b, k.Focus, theme.ControlRadius) if on { b.Background(k.Selection).TextColor(k.AccentText) @@ -101,11 +104,11 @@ func intervalSelectorRow(c *ui.Context, k theme.Tokens, u float32, iv Interval, ui.Text(c, string(iv)).SingleLine() }) chosen = b.Clicked() - name := fmt.Sprintf(msg(c, "interval_selector.favorite"), iv) + name := fmt.Sprintf(core.Msg(c, "interval_selector.favorite"), iv) if fav { - name = fmt.Sprintf(msg(c, "interval_selector.unfavorite"), iv) + name = fmt.Sprintf(core.Msg(c, "interval_selector.unfavorite"), iv) } - star := iconAction(c, icons.Must("star"), name) + star := core.IconAction(c, icons.Must("star"), name) if fav { star.TextColor(k.Warning) } diff --git a/interval_selector_test.go b/chart/interval_selector_test.go similarity index 94% rename from interval_selector_test.go rename to chart/interval_selector_test.go index 910c87a..bd6d838 100644 --- a/interval_selector_test.go +++ b/chart/interval_selector_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIntervalSelectorGroupsFavoritesAndDisabled(t *testing.T) { @@ -10,7 +13,7 @@ func TestIntervalSelectorGroupsFavoritesAndDisabled(t *testing.T) { favorites := []Interval{"1m", "1h"} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IntervalSelector(c, &value, &favorites, IntervalSelectorOptions{Disabled: disabled}) }, 500, 700) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/line_chart.go b/chart/line_chart.go similarity index 75% rename from line_chart.go rename to chart/line_chart.go index a51e83e..e762569 100644 --- a/line_chart.go +++ b/chart/line_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LineChartOptions configure a LineChart. @@ -25,9 +26,9 @@ type LineChartOptions struct { // LineChart draws series as lines; NaN breaks them. func LineChart(c *ui.Context, series []ChartSeries, opts LineChartOptions) ChartResult { n := chartCheckSeries(series) - ch := newChart(c, opts.Frame) - ax := chartMemo(ch, "x", [3]string{chartID(opts.X), chartID(opts.Times), chartID(series)}, func() chartX { return newChartX(n, opts.X, opts.Times) }) - lo, hi, ok := chartYRange(ch, series, false, nil) + ch := NewChart(c, opts.Frame) + ax := ChartMemo(ch, "x", [3]string{ChartID(opts.X), ChartID(opts.Times), ChartID(series)}, func() ChartX { return NewChartX(n, opts.X, opts.Times) }) + lo, hi, ok := ChartYRange(ch, series, false, nil) if !ok { n = 0 } @@ -38,9 +39,9 @@ func LineChart(c *ui.Context, series []ChartSeries, opts LineChartOptions) Chart return chartLines(ch, chartLineData{series: series, tops: tops, ax: ax, lo: lo, hi: hi, n: n, format: opts.Format}) } -// chartYRange is the memoized y domain of visible series. -func chartYRange(ch *chart, series []ChartSeries, zero bool, tops [][]float64) (lo, hi float64, ok bool) { - d := chartMemo(ch, "y", chartSeriesID(ch, series)+chartID(tops), func() [3]float64 { +// ChartYRange is the memoized y domain of visible series. +func ChartYRange(ch *chart, series []ChartSeries, zero bool, tops [][]float64) (lo, hi float64, ok bool) { + d := ChartMemo(ch, "y", chartSeriesID(ch, series)+ChartID(tops), func() [3]float64 { src := series if tops != nil { src = make([]ChartSeries, len(series)) @@ -48,7 +49,7 @@ func chartYRange(ch *chart, series []ChartSeries, zero bool, tops [][]float64) ( src[i] = ChartSeries{Name: s.Name, Values: tops[i]} } } - lo, hi, ok := chartYDomain(src, ch.hidden, zero) + lo, hi, ok := chartYDomain(src, ch.Hidden, zero) if !ok { return [3]float64{} } @@ -61,16 +62,16 @@ func chartYRange(ch *chart, series []ChartSeries, zero bool, tops [][]float64) ( type chartLineData struct { series []ChartSeries tops, bases [][]float64 - ax chartX + ax ChartX lo, hi float64 n int format func(float64) string } func chartLines(ch *chart, d chartLineData) ChartResult { - c, k := ch.c, Tokens(ch.c) + c, k := ch.c, core.Tokens(ch.c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) keys := make([]ChartLegendItem, len(d.series)) for i, s := range d.series { keys[i] = ChartLegendItem{Name: s.Name, Color: pal[i%len(pal)], Shape: i} @@ -83,22 +84,22 @@ func chartLines(ch *chart, d chartLineData) ChartResult { ticks = internal.Ticks(lo, hi, step) } geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 52, 26) - return plot, d.ax.scale(plot), internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} + plot := ChartInset(r, 52, 26) + return plot, d.ax.Scale(plot), internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } - spec := ChartSpec{Marks: d.n, Legend: keys, Hidden: ch.toggles(true)} + spec := ChartSpec{Marks: d.n, Legend: keys, Hidden: ch.Toggles(true)} spec.Pick = func(r ui.Rect, x, y float32) int { plot, xs, _ := geom(r) if !(ui.Rect{X: plot.X - 6, Y: plot.Y, W: plot.W + 12, H: plot.H}).Contains(x, y) { return -1 } - return d.ax.nearest(xs.Invert(x)) + return d.ax.Nearest(xs.Invert(x)) } spec.Readout = func(i int) string { parts := []string{d.ax.label(i)} for _, s := range d.series { - if !ch.hidden(s.Name) { - parts = append(parts, s.Name+" "+chartValue(c, s.Values[i], d.format)) + if !ch.Hidden(s.Name) { + parts = append(parts, s.Name+" "+ChartValue(c, s.Values[i], d.format)) } } return strings.Join(parts, " · ") @@ -106,25 +107,25 @@ func chartLines(ch *chart, d chartLineData) ChartResult { spec.Tooltip = func(i int) []ChartTooltipLine { lines := []ChartTooltipLine{{Text: d.ax.label(i)}} for si, s := range d.series { - if !ch.hidden(s.Name) { - lines = append(lines, ChartTooltipLine{Text: s.Name + " " + chartValue(c, s.Values[i], d.format), Color: keys[si].Color, Shape: si}) + if !ch.Hidden(s.Name) { + lines = append(lines, ChartTooltipLine{Text: s.Name + " " + ChartValue(c, s.Values[i], d.format), Color: keys[si].Color, Shape: si}) } } return lines } spec.Anchor = func(r ui.Rect, i int) (float32, float32) { plot, xs, _ := geom(r) - return xs.Map(d.ax.vals[i]), plot.Y + return xs.Map(d.ax.Vals[i]), plot.Y } spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { plot, xs, ys := geom(r) internal.AxisY(p, plot, ys, ticks, func(v float64) string { return internal.FormatTick(v, step) }, size, k) - tx, tl := d.ax.ticks(plot) + tx, tl := d.ax.Ticks(plot) internal.AxisX(p, plot, tx, tl, size, k) - x := func(i int) float32 { return xs.Map(d.ax.vals[i]) } + x := func(i int) float32 { return xs.Map(d.ax.Vals[i]) } pt := func(i int, v float64) (float32, float32) { return internal.Snap(p, x(i)), internal.Snap(p, ys.Map(v)) } for si, s := range d.series { - if ch.hidden(s.Name) { + if ch.Hidden(s.Name) { continue } col, top := keys[si].Color, d.tops[si] @@ -161,12 +162,12 @@ func chartLines(ch *chart, d chartLineData) ChartResult { ax0 := internal.Snap(p, x(active)) p.Line(ax0, plot.Y, ax0, plot.Y+plot.H, 1, k.TextMuted) for si, s := range d.series { - if v := d.tops[si][active]; !ch.hidden(s.Name) && !math.IsNaN(v) { + if v := d.tops[si][active]; !ch.Hidden(s.Name) && !math.IsNaN(v) { internal.Marker(p, si, ax0, ys.Map(v), 13, k.Surface) internal.Marker(p, si, ax0, ys.Map(v), 9, keys[si].Color) } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/line_chart_test.go b/chart/line_chart_test.go similarity index 92% rename from line_chart_test.go rename to chart/line_chart_test.go index a933b4d..6b83d91 100644 --- a/line_chart_test.go +++ b/chart/line_chart_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) type chartHarness struct { @@ -17,7 +20,7 @@ type chartHarness struct { } func (h *chartHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Width(480).Disabled(h.disabled).Children(func() { h.res = h.build(c) if h.res.Changed() { @@ -100,7 +103,7 @@ func TestLineChartHoverAndToggle(t *testing.T) { func TestLineChartTimeAxisAndStates(t *testing.T) { t0 := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) times := []time.Time{t0, t0.Add(time.Hour), t0.Add(2 * time.Hour), t0.Add(3 * time.Hour), t0.Add(4 * time.Hour)} - status := DataReady + status := data.DataReady var series []ChartSeries var heights []float32 h := &chartHarness{build: func(c *ui.Context) ChartResult { @@ -116,7 +119,7 @@ func TestLineChartTimeAxisAndStates(t *testing.T) { t.Fatalf("time readout %q", got) } heights = append(heights, h.res.Element.Bounds().H) - for _, st := range []DataStatus{DataLoading, DataFailed} { + for _, st := range []data.DataStatus{data.DataLoading, data.DataFailed} { status = st tt.Frame() heights = append(heights, h.res.Element.Bounds().H) @@ -124,7 +127,7 @@ func TestLineChartTimeAxisAndStates(t *testing.T) { if !tt.HasText("Retry") || heights[0] != heights[1] || heights[1] != heights[2] { t.Fatalf("states changed size: %v", heights) } - status, series = DataReady, []ChartSeries{{Name: "A", Values: []float64{math.NaN()}}} + status, series = data.DataReady, []ChartSeries{{Name: "A", Values: []float64{math.NaN()}}} tt.Frame() if !tt.HasText("No data") { t.Fatalf("all-missing chart: %q", tt.Texts()) @@ -164,7 +167,7 @@ func TestLineChartBadOptions(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); build(c) }, 300, 300) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); build(c) }, 300, 300) }() } } diff --git a/line_quote_chart.go b/chart/line_quote_chart.go similarity index 80% rename from line_quote_chart.go rename to chart/line_quote_chart.go index bd7f274..2505d4c 100644 --- a/line_quote_chart.go +++ b/chart/line_quote_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QuotePoint holds time, price, and incremental volume. @@ -50,10 +51,10 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio if opts.RegularOpen.IsZero() != opts.RegularClose.IsZero() || opts.RegularClose.Before(opts.RegularOpen) { panic("mujica: LineQuoteChart needs both session bounds, in order") } - dec := priceDecimals("LineQuoteChart", opts.Decimals) - ch := newChart(c, opts.Frame) - k := Tokens(c) - avg := chartMemo(ch, "avg", chartID(points), func() []float64 { + dec := core.PriceDecimals("LineQuoteChart", opts.Decimals) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + avg := ChartMemo(ch, "avg", ChartID(points), func() []float64 { out := make([]float64, len(points)) var pv, v float64 for i, p := range points { @@ -69,7 +70,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio } return out }) - dom := chartMemo(ch, "y", [2]any{chartID(points), opts.PrevClose}, func() [2]float64 { + dom := ChartMemo(ch, "y", [2]any{ChartID(points), opts.PrevClose}, func() [2]float64 { lo, hi := opts.PrevClose, opts.PrevClose for i, p := range points { lo, hi = min(lo, p.Price, avg[i]), max(hi, p.Price, avg[i]) @@ -78,9 +79,9 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio d := max(hi-opts.PrevClose, opts.PrevClose-lo) return [2]float64{opts.PrevClose - d, opts.PrevClose + d} }) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 60, 26) + plot := ChartInset(r, 60, 26) plot.W -= 44 // percent axis on the right var t0, t1 float64 if len(points) > 0 { @@ -91,8 +92,8 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio } x := func(xs internal.Linear, i int) float32 { return xs.Map(float64(points[i].Time.Unix())) } spec := ChartSpec{Marks: len(points), Legend: []ChartLegendItem{ - {Name: msg(c, "line_quote_chart.price"), Color: k.AccentText, Shape: 0}, - {Name: msg(c, "line_quote_chart.average"), Color: k.Warning, Shape: 3}, + {Name: core.Msg(c, "line_quote_chart.price"), Color: k.AccentText, Shape: 0}, + {Name: core.Msg(c, "line_quote_chart.average"), Color: k.Warning, Shape: 3}, }} spec.Pick = func(r ui.Rect, px, py float32) int { plot, xs, _ := geom(r) @@ -104,10 +105,10 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio spec.Readout = func(i int) string { p := points[i] chg := (p.Price/opts.PrevClose - 1) * 100 - parts := []string{p.Time.Format("15:04"), priceUngrouped(p.Price, dec), fmt.Sprintf("%+.2f%% %s", chg, trendWord(c, trendSign(chg))), - msg(c, "line_quote_chart.average") + " " + priceUngrouped(avg[i], dec)} + parts := []string{p.Time.Format("15:04"), core.PriceUngrouped(p.Price, dec), fmt.Sprintf("%+.2f%% %s", chg, core.TrendWord(c, core.TrendSign(chg))), + core.Msg(c, "line_quote_chart.average") + " " + core.PriceUngrouped(avg[i], dec)} if s := lineQuoteSession(p.Time, opts); s != 0 { - parts = append(parts, msg(c, map[int]string{-1: "line_quote_chart.pre", 1: "line_quote_chart.after"}[s])) + parts = append(parts, core.Msg(c, map[int]string{-1: "line_quote_chart.pre", 1: "line_quote_chart.after"}[s])) } return strings.Join(parts, " · ") } @@ -117,7 +118,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio ticks := internal.Ticks(dom[0], dom[1], step) internal.AxisY(p, plot, ys, ticks, func(v float64) string { return internal.FormatTick(v, step) }, size, k) for _, t := range ticks { - chartText(p, fmt.Sprintf("%+.2f%%", (t/opts.PrevClose-1)*100), plot.X+plot.W+6, ys.Map(t), size, trendColor(c, trendSign(t-opts.PrevClose)), 0, 0.5) + ChartText(p, fmt.Sprintf("%+.2f%%", (t/opts.PrevClose-1)*100), plot.X+plot.W+6, ys.Map(t), size, core.TrendColor(c, core.TrendSign(t-opts.PrevClose)), 0, 0.5) } lineQuoteSessions(p, plot, xs, opts, size, c) var tx []float32 @@ -126,7 +127,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio ts, layout := internal.TimeTicks(points[0].Time, points[len(points)-1].Time, max(2, int(plot.W/90))) for _, t := range ts { if layout == "Jan 2" { - layout = msg(c, "candlestick_chart.daily_time") + layout = core.Msg(c, "candlestick_chart.daily_time") } tx, tl = append(tx, xs.Map(float64(t.Unix()))), append(tl, t.Format(layout)) } @@ -136,7 +137,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio for xx := plot.X; xx < plot.X+plot.W; xx += 6 { p.Line(xx, by, min(xx+3, plot.X+plot.W), by, 1, k.TextMuted) } - price := chartMemo(ch, "prices", chartID(points), func() []float64 { + price := ChartMemo(ch, "prices", ChartID(points), func() []float64 { out := make([]float64, len(points)) for i, pt := range points { out[i] = pt.Price @@ -146,7 +147,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio at := func(i int) float32 { return x(xs, i) } for li, vals := range [][]float64{price, avg} { col := []ui.Color{k.AccentText, k.Warning}[li] - runs := chartMemo(ch, fmt.Sprint("quote.runs.", li), [3]any{chartID(points), plot.X, plot.W}, func() [][]int { + runs := ChartMemo(ch, fmt.Sprint("quote.runs.", li), [3]any{ChartID(points), plot.X, plot.W}, func() [][]int { var out [][]int chartRuns(vals, at, func(idx []int) { out = append(out, idx) }) return out @@ -175,7 +176,7 @@ func LineQuoteChart(c *ui.Context, points []QuotePoint, opts LineQuoteChartOptio internal.Marker(p, 3, ax, ys.Map(avg[active]), 9, k.Warning) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } @@ -184,7 +185,7 @@ func lineQuoteSessions(p *ui.Painter, plot ui.Rect, xs internal.Linear, opts Lin if opts.RegularOpen.IsZero() { return } - k := Tokens(c) + k := core.Tokens(c) x0, x1 := max(plot.X, xs.Map(float64(opts.RegularOpen.Unix()))), min(plot.X+plot.W, xs.Map(float64(opts.RegularClose.Unix()))) for _, span := range []struct { a, b float32 @@ -194,6 +195,6 @@ func lineQuoteSessions(p *ui.Painter, plot ui.Rect, xs internal.Linear, opts Lin continue } p.Fill(ui.Rect{X: span.a, Y: plot.Y, W: span.b - span.a, H: plot.H}, k.SurfaceHover.Alpha(0.6), 0) - chartText(p, msg(c, span.key), span.a+4, plot.Y+2, size, k.TextMuted, 0, 0) + ChartText(p, core.Msg(c, span.key), span.a+4, plot.Y+2, size, k.TextMuted, 0, 0) } } diff --git a/line_quote_chart_test.go b/chart/line_quote_chart_test.go similarity index 84% rename from line_quote_chart_test.go rename to chart/line_quote_chart_test.go index 404d7a4..ef67622 100644 --- a/line_quote_chart_test.go +++ b/chart/line_quote_chart_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func samplePoints() ([]QuotePoint, time.Time, time.Time) { @@ -23,21 +25,21 @@ func TestLineQuoteChartReadoutAverageSessions(t *testing.T) { pts, open, closeAt := samplePoints() var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = LineQuoteChart(c, pts, LineQuoteChartOptions{Frame: ChartFrame{Label: "AAPL"}, PrevClose: 100, RegularOpen: open, RegularClose: closeAt}) }, 800, 400) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyRight) var pv, v float64 pv, v = pts[0].Price*pts[0].Volume, pts[0].Volume - want := "08:00 · " + priceUngrouped(pts[0].Price, 2) + want := "08:00 · " + core.PriceUngrouped(pts[0].Price, 2) found := "" for _, s := range tt.Texts() { if strings.HasPrefix(s, want) { found = s } } - if found == "" || !strings.Contains(found, "Average "+priceUngrouped(pv/v, 2)) || !strings.HasSuffix(found, "Pre-market") { + if found == "" || !strings.Contains(found, "Average "+core.PriceUngrouped(pv/v, 2)) || !strings.HasSuffix(found, "Pre-market") { t.Fatalf("first readout %q in %q", found, tt.Texts()) } tt.Key(0, ui.KeyEnd) @@ -60,7 +62,7 @@ func TestLineQuoteChartReadoutAverageSessions(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); LineQuoteChart(c, pts, o) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); LineQuoteChart(c, pts, o) }, 300, 200) }() } } diff --git a/market_boundary_test.go b/chart/market_boundary_test.go similarity index 94% rename from market_boundary_test.go rename to chart/market_boundary_test.go index 91c9a79..21c37f2 100644 --- a/market_boundary_test.go +++ b/chart/market_boundary_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "bytes" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMarketIndicatorDomainChangesWithParams(t *testing.T) { @@ -13,7 +15,7 @@ func TestMarketIndicatorDomainChangesWithParams(t *testing.T) { v := MarketView{Count: 80} panes := IndicatorPanes{Panes: []IndicatorPaneSpec{{Kind: IndicatorRSI}}} build := func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IndicatorPane(c, cs, &v, &panes, IndicatorPaneOptions{Label: "BTC"}) } tt := ui.NewTester(build, 640, 400) @@ -37,7 +39,7 @@ func TestMarketRejectsInvalidLayers(t *testing.T) { cs := sampleCandles(2) v := MarketView{} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := CandlestickOptions{Frame: ChartFrame{Label: "Price"}} switch kind { case "compare zero": diff --git a/market_data.go b/chart/market_data.go similarity index 99% rename from market_data.go rename to chart/market_data.go index 87293a9..236def0 100644 --- a/market_data.go +++ b/chart/market_data.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" diff --git a/market_heatmap.go b/chart/market_heatmap.go similarity index 63% rename from market_heatmap.go rename to chart/market_heatmap.go index 0112bcf..b9a31f8 100644 --- a/market_heatmap.go +++ b/chart/market_heatmap.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "cmp" @@ -10,7 +10,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type HeatNode struct { @@ -27,19 +29,19 @@ type MarketHeatmapOptions struct { Scale float64 } -type marketHeatMemo struct { +type MarketHeatMemo struct { id string - tiles []marketHeatTile - caps []float64 + Tiles []MarketHeatTile + Caps []float64 width, height float32 - rects []ui.Rect - visible []int - small []marketHeatCell + Rects []ui.Rect + Visible []int + Small []marketHeatCell } -type marketHeatTile struct { - name string - cap, change float64 +type MarketHeatTile struct { + Name string + Cap, Change float64 branch bool } @@ -48,47 +50,47 @@ func MarketHeatmap(c *ui.Context, path *[]string, root HeatNode, opts MarketHeat panic("mujica: MarketHeatmap Scale is negative") } scale := cmp.Or(opts.Scale, 3) - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) var res ChartResult wrap := ui.Column(c).Gap(u * 2).MinWidth(0) wrap.Children(func() { res.value = chartPath(c, root.Name, path, wrap.IsDisabled()) node := marketHeatDescend(root, *path) - memo := ui.Local(wrap, "mujica.heat", func() marketHeatMemo { return marketHeatMemo{} }) - if id := strings.Join(*path, "/") + "|" + chartID(node.Children) + fmt.Sprint(opts.Version); id != memo.id { + memo := ui.Local(wrap, "mujica.heat", func() MarketHeatMemo { return MarketHeatMemo{} }) + if id := strings.Join(*path, "/") + "|" + ChartID(node.Children) + fmt.Sprint(opts.Version); id != memo.id { marketHeatCheck(root) - memo.id, memo.tiles, memo.caps = id, nil, nil - memo.rects = nil + memo.id, memo.Tiles, memo.Caps = id, nil, nil + memo.Rects = nil for _, ch := range node.Children { - t := marketHeatTile{ch.Name, ch.total(), ch.change(), len(ch.Children) > 0} - if t.cap > 0 { - memo.tiles = append(memo.tiles, t) + t := MarketHeatTile{ch.Name, ch.total(), ch.change(), len(ch.Children) > 0} + if t.Cap > 0 { + memo.Tiles = append(memo.Tiles, t) } } - slices.SortStableFunc(memo.tiles, func(a, b marketHeatTile) int { return cmp.Compare(b.cap, a.cap) }) - for _, t := range memo.tiles { - memo.caps = append(memo.caps, t.cap) + slices.SortStableFunc(memo.Tiles, func(a, b MarketHeatTile) int { return cmp.Compare(b.Cap, a.Cap) }) + for _, t := range memo.Tiles { + memo.Caps = append(memo.Caps, t.Cap) } } - tiles, caps := memo.tiles, memo.caps + tiles, caps := memo.Tiles, memo.Caps color := func(ch float64) ui.Color { - return k.Surface.Mix(trendColor(c, trendSign(ch)), float32(0.15+0.85*min(math.Abs(ch)/scale, 1))) + return k.Surface.Mix(core.TrendColor(c, core.TrendSign(ch)), float32(0.15+0.85*min(math.Abs(ch)/scale, 1))) } - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) spec := ChartSpec{Marks: len(tiles)} spec.Readout = func(i int) string { t := tiles[i] - s := fmt.Sprintf("%s · %s %s%% · %s", t.name, trendWord(c, trendSign(t.change)), statisticSigned(t.change, 2), statisticFormat(t.cap, 0)) + s := fmt.Sprintf("%s · %s %s%% · %s", t.Name, core.TrendWord(c, core.TrendSign(t.Change)), data.StatisticSigned(t.Change, 2), data.StatisticFormat(t.Cap, 0)) if t.branch { - s += " · " + msg(c, "chart.opens") + s += " · " + core.Msg(c, "chart.opens") } return s } spec.Pick = func(r ui.Rect, x, y float32) int { - memo.layout(caps, r) - for i, t := range memo.rects { + memo.Layout(caps, r) + for i, t := range memo.Rects { if t.Contains(x, y) { return i } @@ -97,34 +99,34 @@ func MarketHeatmap(c *ui.Context, path *[]string, root HeatNode, opts MarketHeat } spec.Activate = func(i int) { if tiles[i].branch { - *path = append(slices.Clone(*path), tiles[i].name) + *path = append(slices.Clone(*path), tiles[i].Name) res.value = true slog.Debug("mujica: market_heatmap drill", "path", strings.Join(*path, "/")) } } spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { - memo.layout(caps, r) - for _, cell := range memo.small { + memo.Layout(caps, r) + for _, cell := range memo.Small { p.Fill(ui.Rect{X: r.X + cell.x, Y: r.Y + cell.y, W: 4, H: 4}, color(cell.change), 0) } - for _, i := range memo.visible { - t := memo.rects[i] + for _, i := range memo.Visible { + t := memo.Rects[i] t.X, t.Y = t.X+r.X, t.Y+r.Y - bg := color(tiles[i].change) + bg := color(tiles[i].Change) p.Fill(t, bg, 2) p.Stroke(t, k.Surface, 2, 2) - ink := chartInk(k, bg) + ink := ChartInk(k, bg) if i == active { p.Stroke(ui.Rect{X: t.X + 3, Y: t.Y + 3, W: t.W - 6, H: t.H - 6}, ink, 1, 2) } // Signed percents say the direction without color. if t.W >= 40 && t.H >= 34 { - chartText(p, chartFit(p, tiles[i].name, t.W-12, size), t.X+t.W/2, t.Y+t.H/2-1, size, ink, 0.5, 1) - chartText(p, statisticSigned(tiles[i].change, 2)+"%", t.X+t.W/2, t.Y+t.H/2+1, size, ink, 0.5, 0) + ChartText(p, chartFit(p, tiles[i].Name, t.W-12, size), t.X+t.W/2, t.Y+t.H/2-1, size, ink, 0.5, 1) + ChartText(p, data.StatisticSigned(tiles[i].Change, 2)+"%", t.X+t.W/2, t.Y+t.H/2+1, size, ink, 0.5, 0) } } } - r, plot := ch.build(spec) + r, plot := ch.Build(spec) if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 { *path, res.value = (*path)[:len(*path)-1], true } @@ -136,14 +138,14 @@ func MarketHeatmap(c *ui.Context, path *[]string, root HeatNode, opts MarketHeat } func marketHeatLegend(c *ui.Context, scale float64, color func(float64) ui.Color) { - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) steps := []float64{-scale, -scale / 2, 0, scale / 2, scale} ui.Row(c).Gap(2).Children(func() { for _, s := range steps { ui.Column(c).Gap(2).AlignItems(ui.Center).Children(func() { ui.Box(c).Size(44, 10).Background(color(s)).Radius(1) - ui.Text(c, statisticSigned(s, 1)+"%").FontSize(size).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, data.StatisticSigned(s, 1)+"%").FontSize(size).TextColor(k.TextMuted).FontFeatures("tnum") }) } }) @@ -207,30 +209,30 @@ type marketHeatCell struct { change float64 } -func (m *marketHeatMemo) layout(caps []float64, r ui.Rect) { - if m.rects != nil && m.width == r.W && m.height == r.H { +func (m *MarketHeatMemo) Layout(caps []float64, r ui.Rect) { + if m.Rects != nil && m.width == r.W && m.height == r.H { return } m.width, m.height = r.W, r.H - m.rects = squarify(caps, ui.Rect{W: r.W, H: r.H}) - m.visible, m.small = nil, nil + m.Rects = squarify(caps, ui.Rect{W: r.W, H: r.H}) + m.Visible, m.Small = nil, nil type aggregate struct{ weight, change float64 } bins := map[[2]int]aggregate{} - for i, t := range m.rects { + for i, t := range m.Rects { if t.W >= 4 && t.H >= 4 { - m.visible = append(m.visible, i) + m.Visible = append(m.Visible, i) continue } key := [2]int{int((t.X + t.W/2) / 4), int((t.Y + t.H/2) / 4)} a := bins[key] a.weight += caps[i] - a.change += caps[i] * m.tiles[i].change + a.change += caps[i] * m.Tiles[i].Change bins[key] = a } for key, a := range bins { - m.small = append(m.small, marketHeatCell{float32(key[0]) * 4, float32(key[1]) * 4, a.change / a.weight}) + m.Small = append(m.Small, marketHeatCell{float32(key[0]) * 4, float32(key[1]) * 4, a.change / a.weight}) } - slices.SortFunc(m.small, func(a, b marketHeatCell) int { + slices.SortFunc(m.Small, func(a, b marketHeatCell) int { if a.y != b.y { return cmp.Compare(a.y, b.y) } diff --git a/market_heatmap_test.go b/chart/market_heatmap_test.go similarity index 98% rename from market_heatmap_test.go rename to chart/market_heatmap_test.go index 6bf5ad1..0ca2cb5 100644 --- a/market_heatmap_test.go +++ b/chart/market_heatmap_test.go @@ -1,8 +1,9 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestMarketHeatmapAreaDrillAndKeys(t *testing.T) { diff --git a/market_input.go b/chart/market_input.go similarity index 88% rename from market_input.go rename to chart/market_input.go index 8fb7428..f5a442a 100644 --- a/market_input.go +++ b/chart/market_input.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,6 +6,9 @@ import ( "math" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // input handles pan, zoom, alerts, drawings, and keyboard following. @@ -111,12 +114,12 @@ func (m *mktCore) alertsKeyboard(plot *ui.Element) { } } if i := m.st.alertFocus; i >= 0 && (selected || delta != 0) { - spec := numberInputResolve(NumberInputOptions{Step: math.Pow10(-m.dec), Precision: m.dec}) + spec := input.NumberInputResolve(input.NumberInputOptions{Step: math.Pow10(-m.dec), Precision: m.dec}) if delta != 0 { - a[i].Price = spec.stepBy(a[i].Price, delta) + a[i].Price = spec.StepBy(a[i].Price, delta) slog.Debug("mujica: price_alert_line moved", "id", a[i].ID) } - m.c.Announce(fmt.Sprintf(msg(m.c, "price_alert_line.tag"), priceUngrouped(a[i].Price, m.dec))) + m.c.Announce(fmt.Sprintf(core.Msg(m.c, "price_alert_line.tag"), core.PriceUngrouped(a[i].Price, m.dec))) m.c.Invalidate() } } diff --git a/market_keyboard_test.go b/chart/market_keyboard_test.go similarity index 92% rename from market_keyboard_test.go rename to chart/market_keyboard_test.go index a4f99b3..6be362b 100644 --- a/market_keyboard_test.go +++ b/chart/market_keyboard_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestDrawingToolsKeyboardCreateAndSelect(t *testing.T) { @@ -10,7 +13,7 @@ func TestDrawingToolsKeyboardCreateAndSelect(t *testing.T) { v := MarketView{Count: 30} d := DrawingTools{Tool: ToolTrendLine} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "Drawing"}, Drawings: &d}) }, 640, 340) tt.Key(0, ui.KeyTab) @@ -37,7 +40,7 @@ func TestPriceAlertLineKeyboard(t *testing.T) { v := MarketView{Count: 30} alerts := []PriceAlertLine{{ID: "a", Price: 100}, {ID: "b", Price: 105}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "Alerts"}, Alerts: &alerts}) }, 640, 340) tt.Key(0, ui.KeyTab) diff --git a/market_large_test.go b/chart/market_large_test.go similarity index 91% rename from market_large_test.go rename to chart/market_large_test.go index 826edd1..a5c8d1d 100644 --- a/market_large_test.go +++ b/chart/market_large_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestDepthChartLargeDataBounded(t *testing.T) { @@ -13,7 +16,7 @@ func TestDepthChartLargeDataBounded(t *testing.T) { } var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = DepthChart(c, bids, asks, DepthChartOptions{Frame: ChartFrame{Label: "Depth"}}) }, 640, 340) drawn := *ui.Local(res.Element, "mujica.depth.drawn", func() int { return 0 }) @@ -34,7 +37,7 @@ func TestVolumeProfileLargeDataBounded(t *testing.T) { } var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = VolumeProfile(c, bins, VolumeProfileOptions{Frame: ChartFrame{Label: "Profile"}}) }, 640, 340) drawn := *ui.Local(res.Element, "mujica.profile.drawn", func() int { return 0 }) diff --git a/market_layers.go b/chart/market_layers.go similarity index 78% rename from market_layers.go rename to chart/market_layers.go index c98aefb..d8de602 100644 --- a/market_layers.go +++ b/chart/market_layers.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // PriceAlertLine holds a draggable, caller-owned price threshold. @@ -39,7 +41,7 @@ func (m *mktCore) compareLines() { line := &m.overlays[i] values := line.values base := m.base - line.values = chartMemo(m.ch, fmt.Sprint("compare.overlay.", i), [2]any{chartID(values), base}, func() []float64 { + line.values = ChartMemo(m.ch, fmt.Sprint("compare.overlay.", i), [2]any{ChartID(values), base}, func() []float64 { out := make([]float64, len(values)) for j, v := range values { out[j] = (v/base - 1) * 100 @@ -51,7 +53,7 @@ func (m *mktCore) compareLines() { if s.Name == "" || len(s.Closes) != len(m.candles) { panic(fmt.Sprintf("mujica: CompareSymbol %q needs a Name and one close per candle", s.Name)) } - chartMemo(m.ch, "compare.check."+s.Name, chartID(s.Closes), func() bool { + ChartMemo(m.ch, "compare.check."+s.Name, ChartID(s.Closes), func() bool { for _, v := range s.Closes { if math.IsInf(v, 0) || (!math.IsNaN(v) && v <= 0) { panic("mujica: compared closes must be positive or NaN") @@ -64,7 +66,7 @@ func (m *mktCore) compareLines() { base = s.Closes[j] } closes := s.Closes - vals := chartMemo(m.ch, "compare."+s.Name, [2]any{chartID(closes), math.Float64bits(base)}, func() []float64 { + vals := ChartMemo(m.ch, "compare."+s.Name, [2]any{ChartID(closes), math.Float64bits(base)}, func() []float64 { out := make([]float64, len(closes)) for j, v := range closes { out[j] = (v/base - 1) * 100 @@ -76,22 +78,22 @@ func (m *mktCore) compareLines() { } // CompareLegend shows series markers and removal actions. -func CompareLegend(c *ui.Context, symbols *[]CompareSymbol) ChoiceResult { +func CompareLegend(c *ui.Context, symbols *[]CompareSymbol) core.ChoiceResult { if symbols == nil { panic("mujica: CompareLegend needs symbols") } - k := Tokens(c) + k := core.Tokens(c) pal := internal.Palette(k) - u := Density(c).Unit() + u := core.Density(c).Unit() removed := -1 - row := ui.Row(c).Wrap().Gap(u * 2).Role(ui.RoleList).Label(msg(c, "compare_symbol.label")) + row := ui.Row(c).Wrap().Gap(u * 2).Role(ui.RoleList).Label(core.Msg(c, "compare_symbol.label")) row.Children(func() { for i, s := range *symbols { ui.Row(c).Key(s.Name).AlignItems(ui.Center).Gap(u).Padding(0, 0, 0, u*2).Radius(6).Border(1, k.Border).Role(ui.RoleListItem).Label(s.Name).Children(func() { col := pal[(i+1)%len(pal)] ui.Box(c).Size(10, 10).Draw(func(p *ui.Painter, r ui.Rect) { internal.Marker(p, i+1, r.X+5, r.Y+5, 9, col) }) ui.Text(c, s.Name).SingleLine() - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "compare_symbol.remove"), s.Name)).Clicked() && !row.IsDisabled() { + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "compare_symbol.remove"), s.Name)).Clicked() && !row.IsDisabled() { removed = i } }) @@ -101,7 +103,7 @@ func CompareLegend(c *ui.Context, symbols *[]CompareSymbol) ChoiceResult { slog.Debug("mujica: compare_symbol removed", "name", (*symbols)[removed].Name) *symbols = append((*symbols)[:removed:removed], (*symbols)[removed+1:]...) } - return ChoiceResult{row, removed >= 0} + return core.NewChoiceResult(row, removed >= 0) } // alertsCheck fires callbacks when the latest close crosses thresholds. @@ -119,7 +121,7 @@ func (m *mktCore) alertsCheck() { return } last := m.candles[len(m.candles)-1] - if !m.ch.res.Element.IsDisabled() && m.st.alertSeen && last.Close != m.st.lastClose && m.opts.OnAlert != nil { + if !m.ch.Res.Element.IsDisabled() && m.st.alertSeen && last.Close != m.st.lastClose && m.opts.OnAlert != nil { for _, a := range *m.opts.Alerts { if (m.st.lastClose-a.Price)*(last.Close-a.Price) <= 0 && m.st.lastClose != a.Price { slog.Debug("mujica: price_alert_line crossed", "id", a.ID) @@ -150,8 +152,8 @@ func (m *mktCore) alertsInput(g mktGeom, x, y float32, held bool, dy float32) bo if m.st.grab < 0 || m.st.grab >= len(alerts) { return false } - spec := numberInputResolve(NumberInputOptions{Step: math.Pow10(-m.dec), Precision: m.dec}) - alerts[m.st.grab].Price = spec.round(g.ys.Invert(y)) + spec := input.NumberInputResolve(input.NumberInputOptions{Step: math.Pow10(-m.dec), Precision: m.dec}) + alerts[m.st.grab].Price = spec.Round(g.ys.Invert(y)) slog.Debug("mujica: price_alert_line moved", "id", alerts[m.st.grab].ID) return true } @@ -169,10 +171,10 @@ func (m *mktCore) paintAlerts(p *ui.Painter, g mktGeom, size float32) { for x := g.plot.X; x < g.plot.X+g.plot.W; x += 8 { p.Line(x, y, min(x+4, g.plot.X+g.plot.W), y, 1, m.k.Warning) } - text := priceUngrouped(a.Price, m.dec) + text := core.PriceUngrouped(a.Price, m.dec) if a.Label != "" { text = a.Label + " " + text } - mktTag(p, g.plot.X+g.plot.W, y, fmt.Sprintf(msg(m.c, "price_alert_line.tag"), text), size, m.k.Warning, m.k.Surface) + mktTag(p, g.plot.X+g.plot.W, y, fmt.Sprintf(core.Msg(m.c, "price_alert_line.tag"), text), size, m.k.Warning, m.k.Surface) } } diff --git a/market_regression_test.go b/chart/market_regression_test.go similarity index 93% rename from market_regression_test.go rename to chart/market_regression_test.go index db4267d..ab6cc84 100644 --- a/market_regression_test.go +++ b/chart/market_regression_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMarketEmptyWindow(t *testing.T) { @@ -20,7 +22,7 @@ func TestMarketDisabledAlert(t *testing.T) { alerts := []PriceAlertLine{{ID: "a", Price: cs[1].Close + 1}} disabled, fired := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "Price"}, Alerts: &alerts, OnAlert: func(PriceAlertLine, Candle) { fired++ }}) @@ -41,7 +43,7 @@ func TestMarketDisabledAlert(t *testing.T) { func TestMarketDisabledLayoutCopy(t *testing.T) { state := MultiChartState{Count: 2} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { MultiChartLayout(c, &state, MultiChartLayoutOptions{}, func(_ int, cell *ChartCell) { cell.Symbol = "changed" @@ -56,7 +58,7 @@ func TestMarketDisabledLayoutCopy(t *testing.T) { func TestMarketDisabledParamsCopy(t *testing.T) { var params []float64 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { IndicatorParamsField(c, IndicatorMA, ¶ms) }) }, 500, 100) if params != nil { diff --git a/market_review_regression_test.go b/chart/market_review_regression_test.go similarity index 90% rename from market_review_regression_test.go rename to chart/market_review_regression_test.go index 04ddf07..95913d4 100644 --- a/market_review_regression_test.go +++ b/chart/market_review_regression_test.go @@ -1,11 +1,14 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "image" "math" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMarketSyncRange(t *testing.T) { @@ -14,7 +17,7 @@ func TestMarketSyncRange(t *testing.T) { s := ChartSync{Count: 40, End: cs[249].Time} var a, b MarketView ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { CandlestickChart(c, cs, &a, CandlestickOptions{Frame: ChartFrame{Label: "A", Height: 100}, Sync: &s, SyncID: "a"}) CandlestickChart(c, short, &b, CandlestickOptions{Frame: ChartFrame{Label: "B", Height: 100}, Sync: &s, SyncID: "b"}) @@ -31,7 +34,7 @@ func TestMarketPaneKeyboardGeometry(t *testing.T) { panes := IndicatorPanes{Panes: []IndicatorPaneSpec{{Kind: IndicatorOBV}}} var r IndicatorPaneResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r = IndicatorPane(c, cs, &v, &panes, IndicatorPaneOptions{Label: "BTC", Height: 220}) }, 640, 300) tt.Key(0, ui.KeyTab) @@ -50,7 +53,7 @@ func TestMarketAlertsWithDrawingTools(t *testing.T) { a := []PriceAlertLine{{ID: "a", Price: 100}} d := DrawingTools{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "A"}, Alerts: &a, Drawings: &d}) }, 640, 300) tt.Key(0, ui.KeyTab) @@ -66,7 +69,7 @@ func TestMarketDrawingSwitchCancelsPending(t *testing.T) { v := MarketView{} d := DrawingTools{Tool: ToolTrendLine} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { DrawingToolbar(c, &d, DrawingToolbarOptions{}) CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "A"}, Drawings: &d}).Element.Grow(1) @@ -104,7 +107,7 @@ func TestMarketPaneResizeGeometry(t *testing.T) { v := MarketView{Count: 40} p := IndicatorPanes{Panes: []IndicatorPaneSpec{{Kind: IndicatorMACD}, {Kind: IndicatorRSI}}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IndicatorPane(c, cs, &v, &p, IndicatorPaneOptions{Label: "BTC", Height: 320}) }, 640, 400) for i := 0; i < 12; i++ { @@ -132,14 +135,14 @@ func TestMarketMissingComparisonCache(t *testing.T) { } var r ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r = CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "A"}, Compare: &s}) }, 640, 320) - memo := ui.Local(r.Element, "mujica.memo.compare.missing", func() chartMemoEntry { return chartMemoEntry{} }) - before := chartID(memo.val.([]float64)) + memo := ui.Local(r.Element, "mujica.memo.compare.missing", func() ChartMemoEntry { return ChartMemoEntry{} }) + before := ChartID(memo.Val.([]float64)) tt.Frame() - memo = ui.Local(r.Element, "mujica.memo.compare.missing", func() chartMemoEntry { return chartMemoEntry{} }) - after := chartID(memo.val.([]float64)) + memo = ui.Local(r.Element, "mujica.memo.compare.missing", func() ChartMemoEntry { return ChartMemoEntry{} }) + after := ChartID(memo.Val.([]float64)) t.Logf("before=%s after=%s", before, after) if before != after { t.Error("unchanged missing comparison allocated and recomputed 100000 values") @@ -151,14 +154,14 @@ func TestMarketDailyAxisLocale(t *testing.T) { for i := range candles { candles[i].Time = candles[0].Time.AddDate(0, 0, i) } - render := func(locale Locale) *image.RGBA { + render := func(locale core.Locale) *image.RGBA { view := MarketView{} return ui.Render(func(c *ui.Context) { - Use(c, Settings{Locale: locale}) + core.Use(c, core.Settings{Locale: locale}) CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC", Height: 240}}) }, 640, 320, 1) } - en, zh := render(EnUS), render(ZhCN) + en, zh := render(core.EnUS), render(core.ZhCN) for y := 214; y < 240; y++ { for x := 60; x < 620; x++ { if en.RGBAAt(x, y) != zh.RGBAAt(x, y) { @@ -178,7 +181,7 @@ func TestMarketSyncDifferentIntervals(t *testing.T) { s := ChartSync{Count: 40, End: cs[199].Time} var a, b MarketView tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { CandlestickChart(c, cs, &a, CandlestickOptions{Frame: ChartFrame{Label: "A", Height: 100}, Sync: &s, SyncID: "a"}) CandlestickChart(c, slower, &b, CandlestickOptions{Frame: ChartFrame{Label: "B", Height: 100}, Sync: &s, SyncID: "b"}) diff --git a/market_states_test.go b/chart/market_states_test.go similarity index 89% rename from market_states_test.go rename to chart/market_states_test.go index 90f3d53..b072b22 100644 --- a/market_states_test.go +++ b/chart/market_states_test.go @@ -1,9 +1,13 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" - "mujicaui/theme" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) { @@ -18,11 +22,11 @@ func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) { symbols := []CompareSymbol{{Name: "Other", Closes: closesOf(cs)}} alerts := []PriceAlertLine{{ID: "a", Price: 100}} drawings := DrawingTools{} - status := DataReady + status := data.DataReady disabled, empty := false, false var r ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) ui.Column(c).Disabled(disabled).Width(600).Children(func() { candles := cs if empty { @@ -102,9 +106,9 @@ func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) { disabled = false tt.Frame() height := r.Element.Bounds().H - for _, st := range []DataStatus{DataLoading, DataFailed, DataReady} { + for _, st := range []data.DataStatus{data.DataLoading, data.DataFailed, data.DataReady} { status = st - empty = st == DataReady + empty = st == data.DataReady tt.Frame() if r.Point != -1 || r.Element.Bounds().H != height { t.Fatalf("state %v: point %d height %v want %v", st, r.Point, r.Element.Bounds().H, height) diff --git a/multi_chart_layout.go b/chart/multi_chart_layout.go similarity index 75% rename from multi_chart_layout.go rename to chart/multi_chart_layout.go index 977b422..50780da 100644 --- a/multi_chart_layout.go +++ b/chart/multi_chart_layout.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartCell holds one chart's symbol, interval, and window. @@ -33,11 +35,11 @@ type MultiChartLayoutOptions struct { } // MultiChartLayout builds 1, 2, 4, or 6 independent charts. -func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayoutOptions, cell func(i int, cell *ChartCell)) ChoiceResult { +func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayoutOptions, cell func(i int, cell *ChartCell)) core.ChoiceResult { if state == nil || cell == nil { panic("mujica: MultiChartLayout needs a state and a cell builder") } - root := ui.Column(c).Gap(Density(c).Unit() * 2).FillWidth() + root := ui.Column(c).Gap(core.Density(c).Unit() * 2).FillWidth() if root.IsDisabled() { copy := *state state = © @@ -55,8 +57,8 @@ func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayo if state.Active < 0 || state.Active >= state.Count { panic(fmt.Sprintf("mujica: MultiChartLayout Active %d outside %d cells", state.Active, state.Count)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() h := opts.Height if h == 0 { h = 480 @@ -64,12 +66,12 @@ func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayo changed := false root.Children(func() { sel := []int{state.Count} - var items []ToggleItem[int] + var items []input.ToggleItem[int] for _, n := range []int{1, 2, 4, 6} { - items = append(items, ToggleItem[int]{Value: n, Label: fmt.Sprint(n), Name: fmt.Sprintf(msg(c, "multi_chart_layout.count"), n)}) + items = append(items, input.ToggleItem[int]{Value: n, Label: fmt.Sprint(n), Name: fmt.Sprintf(core.Msg(c, "multi_chart_layout.count"), n)}) } ui.Row(c).Children(func() { - if ToggleGroup(c, &sel, items, ToggleGroupOptions{Label: msg(c, "multi_chart_layout.label")}).Changed() { + if input.ToggleGroup(c, &sel, items, input.ToggleGroupOptions{Label: core.Msg(c, "multi_chart_layout.label")}).Changed() { state.Count, changed = sel[0], true state.Active = min(state.Active, state.Count-1) slog.Debug("mujica: multi_chart_layout count", "count", state.Count) @@ -82,7 +84,7 @@ func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayo for i := range state.Count { box := ui.Column(c).Key(i).MinWidth(0).MinHeight(0).Height((h - u*2*float32(rows-1)) / float32(rows)).Padding(u * 2). Background(k.Surface).Radius(theme.CardRadius).Clip().Role(ui.RoleGroup). - Label(fmt.Sprintf(msg(c, "multi_chart_layout.cell"), i+1, state.Cells[i].Symbol)) + Label(fmt.Sprintf(core.Msg(c, "multi_chart_layout.cell"), i+1, state.Cells[i].Symbol)) if i == state.Active && state.Count > 1 { box.Border(2, k.AccentText) box.DrawOver(func(p *ui.Painter, r ui.Rect) { internal.Frame(p, r, 3, 5, k.Ornament) }) @@ -98,5 +100,5 @@ func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayo } }) }) - return ChoiceResult{root, changed} + return core.NewChoiceResult(root, changed) } diff --git a/multi_chart_layout_test.go b/chart/multi_chart_layout_test.go similarity index 95% rename from multi_chart_layout_test.go rename to chart/multi_chart_layout_test.go index 9a85df6..5d091d9 100644 --- a/multi_chart_layout_test.go +++ b/chart/multi_chart_layout_test.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMultiChartLayoutKeepsCellsAndDisabled(t *testing.T) { @@ -13,7 +16,7 @@ func TestMultiChartLayoutKeepsCellsAndDisabled(t *testing.T) { disabled := false built := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) built = 0 ui.Column(c).Disabled(disabled).Children(func() { MultiChartLayout(c, &state, MultiChartLayoutOptions{Height: 200}, func(_ int, cell *ChartCell) { built++; ui.Text(c, cell.Symbol) }) diff --git a/network_graph.go b/chart/network_graph.go similarity index 93% rename from network_graph.go rename to chart/network_graph.go index 5c5e66a..b81ca7f 100644 --- a/network_graph.go +++ b/chart/network_graph.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // GraphNode is a node placed at X, Y unless forced. @@ -79,7 +80,7 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges if s.zoom == 0 { s.zoom = 1 } - if key := chartID(nodes) + chartID(edges) + fmt.Sprint(opts.Force); s.layout != key { + if key := ChartID(nodes) + ChartID(edges) + fmt.Sprint(opts.Force); s.layout != key { s.layout, s.pos = key, networkLayout(nodes, links, opts.Force) // The view fits the layout; dragging does not refit. s.lo, s.hi = [2]float64{math.Inf(1), math.Inf(1)}, [2]float64{math.Inf(-1), math.Inf(-1)} @@ -88,10 +89,10 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges } slog.Debug("mujica: network_graph layout", "nodes", len(nodes)) } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) lo, hi := s.lo, s.hi // screen maps a layout point into r, zoomed and panned. screen := func(r ui.Rect, p [2]float64) (float32, float32) { @@ -117,7 +118,7 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges near = append(near, nodes[l[0]].Label) } } - return fmt.Sprintf(msg(c, "network_graph.readout"), nodes[i].Label, len(near), strings.Join(near, ", ")) + return fmt.Sprintf(core.Msg(c, "network_graph.readout"), nodes[i].Label, len(near), strings.Join(near, ", ")) } spec.Pick = func(r ui.Rect, x, y float32) int { return internal.Nearest(len(nodes), func(i int) (float32, float32) { return pt(r, i) }, x, y, 12, false) @@ -144,11 +145,11 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges ring.Circle(internal.Snap(p, x), internal.Snap(p, y), 10) p.StrokePath(&ring, 2, k.Text) } - chartText(p, n.Label, x, y+9, size, k.Text, 0.5, 0) + ChartText(p, n.Label, x, y+9, size, k.Text, 0.5, 0) } }) } - res, plot := ch.build(spec) + res, plot := ch.Build(spec) if plot == nil || plot.IsDisabled() { return res } diff --git a/network_graph_test.go b/chart/network_graph_test.go similarity index 97% rename from network_graph_test.go rename to chart/network_graph_test.go index 7b45133..ae18dfb 100644 --- a/network_graph_test.go +++ b/chart/network_graph_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var graphNodes = []GraphNode{{"a", "Abbot", 0, 0}, {"b", "Prior", 10, 0}, {"c", "Cellarer", 10, 10}, {"d", "Novice", 0, 10}} @@ -86,7 +88,7 @@ func TestNetworkGraphBad(t *testing.T) { }() var s NetworkGraphState ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NetworkGraph(c, &s, graphNodes, []GraphEdge{{"a", "z"}}, NetworkGraphOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/ohlc_chart_test.go b/chart/ohlc_chart_test.go similarity index 92% rename from ohlc_chart_test.go rename to chart/ohlc_chart_test.go index 8bd948f..9dda9ac 100644 --- a/ohlc_chart_test.go +++ b/chart/ohlc_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "bytes" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestOHLCChartSharesDataAndDiffersInLook(t *testing.T) { @@ -13,7 +15,7 @@ func TestOHLCChartSharesDataAndDiffersInLook(t *testing.T) { ohlc := true var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ohlc { res = OHLCChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "Bars"}}) } else { diff --git a/parallel_coordinates.go b/chart/parallel_coordinates.go similarity index 82% rename from parallel_coordinates.go rename to chart/parallel_coordinates.go index e2fd991..630806c 100644 --- a/parallel_coordinates.go +++ b/chart/parallel_coordinates.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ParallelCoordinatesOptions configure a ParallelCoordinates chart. @@ -44,12 +45,12 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s panic("mujica: ParallelCoordinates filters need known axes and finite ascending ranges") } } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) ink := internal.Palette(k)[0] type scales struct{ lo, hi []float64 } - sc := chartMemo(ch, "scales", [2]string{chartID(rows), chartID(axes)}, func() scales { + sc := ChartMemo(ch, "scales", [2]string{ChartID(rows), ChartID(axes)}, func() scales { for _, r := range rows { if len(r) != len(axes) { panic("mujica: ParallelCoordinates rows need one value per axis") @@ -83,14 +84,14 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s } return true } - passing := chartMemo(ch, "pc_filter", [2]string{chartID(rows), fmt.Sprint(*filters)}, func() []bool { + passing := ChartMemo(ch, "pc_filter", [2]string{ChartID(rows), fmt.Sprint(*filters)}, func() []bool { out := make([]bool, len(rows)) for i, row := range rows { out[i] = keep(row) } return out }) - visible := ui.Local(ch.res.Element, "mujica.pc.visible", func() []int { return nil }) + visible := ui.Local(ch.Res.Element, "mujica.pc.visible", func() []int { return nil }) geom := func(r ui.Rect) (ui.Rect, func(a int) float32, func(a int, v float64) float32) { plot := ui.Rect{X: r.X + 36, Y: r.Y + 24, W: r.W - 72, H: r.H - 32} ax := func(a int) float32 { return plot.X + plot.W*float32(a)/float32(len(axes)-1) } @@ -103,16 +104,16 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s if opts.Names != nil { return opts.Names[i] } - return "#" + chartValue(c, float64(i+1), nil) + return "#" + ChartValue(c, float64(i+1), nil) } spec := ChartSpec{Marks: len(rows)} spec.Readout = func(i int) string { parts := []string{name(i)} for a, n := range axes { - parts = append(parts, n+" "+chartValue(c, rows[i][a], opts.Format)) + parts = append(parts, n+" "+ChartValue(c, rows[i][a], opts.Format)) } if !passing[i] { - parts = append(parts, msg(c, "parallel_coordinates.filtered")) + parts = append(parts, core.Msg(c, "parallel_coordinates.filtered")) } return strings.Join(parts, " · ") } @@ -127,15 +128,15 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s continue } t := (x - x0) / (x1 - x0) - if d := abs32(ys(a, row[a]) + t*(ys(a+1, row[a+1])-ys(a, row[a])) - y); d < dist { + if d := core.Abs32(ys(a, row[a]) + t*(ys(a+1, row[a+1])-ys(a, row[a])) - y); d < dist { best, dist = i, d } } } return best } - drag := ui.Local(ch.res.Element, "mujica.pc.drag", func() pcDrag { return pcDrag{axis: -1} }) - keyAxis := ui.Local(ch.res.Element, "mujica.pc.axis", func() int { return 0 }) + drag := ui.Local(ch.Res.Element, "mujica.pc.drag", func() pcDrag { return pcDrag{axis: -1} }) + keyAxis := ui.Local(ch.Res.Element, "mujica.pc.axis", func() int { return 0 }) spec.Input = func(plot *ui.Element, r ui.Rect) { if plot.IsDisabled() { *drag = pcDrag{axis: -1} @@ -178,9 +179,9 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s } else { m[axes[a]] = f } - *filters, ch.res.value = m, true + *filters, ch.Res.value = m, true c.Invalidate() - c.Announce(fmt.Sprintf(msg(c, "parallel_coordinates.brush"), axes[a], chartValue(c, f[0], opts.Format), chartValue(c, f[1], opts.Format))) + c.Announce(fmt.Sprintf(core.Msg(c, "parallel_coordinates.brush"), axes[a], ChartValue(c, f[0], opts.Format), ChartValue(c, f[1], opts.Format))) slog.Debug("mujica: parallel_coordinates keyboard", "axis", axes[a]) } box, ax, _ := geom(r) @@ -189,7 +190,7 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s switch { case held && drag.axis < 0: for a := range axes { - if abs32(px-ax(a)) <= 12 { + if core.Abs32(px-ax(a)) <= 12 { *drag = pcDrag{axis: a, y0: py, y1: py} } } @@ -201,7 +202,7 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s if m == nil { m = map[string][2]float64{} } - if abs32(drag.y1-drag.y0) < 4 { + if core.Abs32(drag.y1-drag.y0) < 4 { delete(m, axes[a]) } else { v := func(y float32) float64 { @@ -209,7 +210,7 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s } m[axes[a]] = [2]float64{min(v(drag.y0), v(drag.y1)), max(v(drag.y0), v(drag.y1))} } - *filters, ch.res.value = m, true + *filters, ch.Res.value = m, true c.Invalidate() *drag = pcDrag{axis: -1} slog.Debug("mujica: parallel_coordinates brush", "axis", axes[a], "filters", len(m)) @@ -258,16 +259,16 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s p.Fill(ui.Rect{X: x - 6, Y: y0, W: 12, H: max(y1-y0, 1)}, k.Selection, 2) p.Stroke(ui.Rect{X: x - 6, Y: y0, W: 12, H: max(y1-y0, 1)}, k.AccentText, 2, 1.5) } - chartText(p, n, x, plot.Y-8, size, k.Text, 0.5, 1) - chartText(p, internal.FormatTick(sc.hi[a], (sc.hi[a]-sc.lo[a])/4), x+10, plot.Y, size, k.TextMuted, 0, 0) - chartText(p, internal.FormatTick(sc.lo[a], (sc.hi[a]-sc.lo[a])/4), x+10, plot.Y+plot.H, size, k.TextMuted, 0, 1) + ChartText(p, n, x, plot.Y-8, size, k.Text, 0.5, 1) + ChartText(p, internal.FormatTick(sc.hi[a], (sc.hi[a]-sc.lo[a])/4), x+10, plot.Y, size, k.TextMuted, 0, 0) + ChartText(p, internal.FormatTick(sc.lo[a], (sc.hi[a]-sc.lo[a])/4), x+10, plot.Y+plot.H, size, k.TextMuted, 0, 1) } if shownDrag.axis >= 0 { x := ax(shownDrag.axis) - p.Stroke(ui.Rect{X: x - 6, Y: min(shownDrag.y0, shownDrag.y1), W: 12, H: abs32(shownDrag.y1 - shownDrag.y0)}, k.AccentText, 2, 1.5) + p.Stroke(ui.Rect{X: x - 6, Y: min(shownDrag.y0, shownDrag.y1), W: 12, H: core.Abs32(shownDrag.y1 - shownDrag.y0)}, k.AccentText, 2, 1.5) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) if res.Element.IsDisabled() { *drag = pcDrag{axis: -1} } diff --git a/parallel_coordinates_test.go b/chart/parallel_coordinates_test.go similarity index 98% rename from parallel_coordinates_test.go rename to chart/parallel_coordinates_test.go index 1cb158d..53c5fdf 100644 --- a/parallel_coordinates_test.go +++ b/chart/parallel_coordinates_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var pcAxes = []string{"Age", "Hymns", "Vigils"} @@ -69,7 +71,7 @@ func TestParallelCoordinatesHoverAndBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ParallelCoordinates(c, &filters, pcAxes, [][]float64{{1}}, ParallelCoordinatesOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/pie_chart.go b/chart/pie_chart.go similarity index 79% rename from pie_chart.go rename to chart/pie_chart.go index ae5b479..ee0ac73 100644 --- a/pie_chart.go +++ b/chart/pie_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChartSlice is one share of a PieChart or FunnelChart. @@ -31,12 +32,12 @@ func PieChart(c *ui.Context, slices []ChartSlice, opts PieChartOptions) ChartRes if opts.MinAngle < 0 || opts.MinAngle >= 360 { panic("mujica: PieChart MinAngle must be in [0, 360)") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) - parts := chartMemo(ch, "parts", [2]any{chartID(slices), opts.MinAngle}, func() []ChartSlice { - return pieMerge(slices, opts.MinAngle, msg(c, "pie_chart.other")) + size := core.FontSize(c, theme.CaptionSize) + parts := ChartMemo(ch, "parts", [2]any{ChartID(slices), opts.MinAngle}, func() []ChartSlice { + return PieMerge(slices, opts.MinAngle, core.Msg(c, "pie_chart.other")) }) total := 0.0 for _, s := range parts { @@ -68,7 +69,7 @@ func PieChart(c *ui.Context, slices []ChartSlice, opts PieChartOptions) ChartRes share := func(i int) string { return fmt.Sprintf("%.1f%%", 100*parts[i].Value/total) } spec := ChartSpec{Marks: n, Legend: keys} spec.Readout = func(i int) string { - return parts[i].Label + " · " + chartValue(c, parts[i].Value, opts.Format) + " · " + share(i) + return parts[i].Label + " · " + ChartValue(c, parts[i].Value, opts.Format) + " · " + share(i) } spec.Pick = func(r ui.Rect, x, y float32) int { cx, cy, r0, r1 := geom(r) @@ -103,25 +104,25 @@ func PieChart(c *ui.Context, slices []ChartSlice, opts PieChartOptions) ChartRes // Wide slices carry their shape and percent. if a1-a0 > 0.38 { mx, my := internal.Polar(cx, cy, (r0+r1)/2+float32(chartBool(r0 == 0))*r1*0.12, (a0+a1)/2) - ink := chartInk(k, keys[i].Color) + ink := ChartInk(k, keys[i].Color) internal.Marker(p, i, mx, my-8, 9, ink) // The percent shows where the ring is wide enough to hold it. if w, _ := p.MeasureText(0, ui.Span{Text: share(i), Size: size}); r1-r0 >= w+6 { - chartText(p, share(i), mx, my+2, size, ink, 0.5, 0) + ChartText(p, share(i), mx, my+2, size, ink, 0.5, 0) } } } if opts.Donut { - chartText(p, chartValue(c, total, opts.Format), cx, cy, FontSize(c, theme.H3Size), k.Text, 0.5, 0.75) - chartText(p, msg(c, "pie_chart.total"), cx, cy+4, size, k.TextMuted, 0.5, 0) + ChartText(p, ChartValue(c, total, opts.Format), cx, cy, core.FontSize(c, theme.H3Size), k.Text, 0.5, 0.75) + ChartText(p, core.Msg(c, "pie_chart.total"), cx, cy+4, size, k.TextMuted, 0.5, 0) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } -// pieMerge folds two or more narrow slices into other. -func pieMerge(slices []ChartSlice, minAngle float64, other string) []ChartSlice { +// PieMerge folds two or more narrow slices into other. +func PieMerge(slices []ChartSlice, minAngle float64, other string) []ChartSlice { total := 0.0 for _, s := range slices { if s.Value < 0 || math.IsNaN(s.Value) || math.IsInf(s.Value, 0) || s.Label == "" { diff --git a/pie_chart_test.go b/chart/pie_chart_test.go similarity index 92% rename from pie_chart_test.go rename to chart/pie_chart_test.go index 19328dd..125c214 100644 --- a/pie_chart_test.go +++ b/chart/pie_chart_test.go @@ -1,20 +1,22 @@ -package mujica +package chart import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var pieSlices = []ChartSlice{{"Wax", 50}, {"Oil", 30}, {"Myrrh", 12}, {"Salt", 4}, {"Ash", 4}} func TestPieMerge(t *testing.T) { - got := pieMerge(pieSlices, 20, "Other") + got := PieMerge(pieSlices, 20, "Other") if fmt.Sprint(got) != "[{Wax 50} {Oil 30} {Myrrh 12} {Other 8}]" { t.Fatalf("merge 20°: %v", got) } - if got := pieMerge(pieSlices[:4], 20, "Other"); len(got) != 4 { + if got := PieMerge(pieSlices[:4], 20, "Other"); len(got) != 4 { t.Fatalf("a single narrow slice merged: %v", got) } } @@ -55,7 +57,7 @@ func TestPieChartBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PieChart(c, s, PieChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) }() diff --git a/price_alert_line_test.go b/chart/price_alert_line_test.go similarity index 95% rename from price_alert_line_test.go rename to chart/price_alert_line_test.go index d138d26..06a8d60 100644 --- a/price_alert_line_test.go +++ b/chart/price_alert_line_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPriceAlertLineDragAndTrigger(t *testing.T) { @@ -14,7 +16,7 @@ func TestPriceAlertLineDragAndTrigger(t *testing.T) { var res ChartResult _ = res tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}, Alerts: &alerts, OnAlert: func(a PriceAlertLine, last Candle) { fired = append(fired, a.ID) }}) }, 800, 400) diff --git a/radar_chart.go b/chart/radar_chart.go similarity index 83% rename from radar_chart.go rename to chart/radar_chart.go index d554784..43a3a29 100644 --- a/radar_chart.go +++ b/chart/radar_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RadarChartOptions configure a RadarChart. @@ -23,10 +24,10 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh if n := chartCheckSeries(series); len(axes) < 3 || n != len(axes) && len(series) > 0 || opts.Max < 0 { panic("mujica: RadarChart needs three or more axes, one value per axis and Max ≥ 0") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) keys := make([]ChartLegendItem, len(series)) top := opts.Max for i, s := range series { @@ -35,7 +36,7 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh if v < 0 { panic("mujica: RadarChart values must be ≥ 0") } - if !ch.hidden(s.Name) && opts.Max == 0 && !math.IsNaN(v) { + if !ch.Hidden(s.Name) && opts.Max == 0 && !math.IsNaN(v) { top = max(top, v) } } @@ -54,12 +55,12 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh return r.X + r.W/2, r.Y + r.H/2 + 4, min(r.W/2-70, r.H/2-24) } angle := func(i int) float64 { return 2 * math.Pi * float64(i) / float64(len(axes)) } - spec := ChartSpec{Marks: n, Legend: keys, Hidden: ch.toggles(len(series) > 1)} + spec := ChartSpec{Marks: n, Legend: keys, Hidden: ch.Toggles(len(series) > 1)} spec.Readout = func(i int) string { parts := []string{axes[i]} for _, s := range series { - if !ch.hidden(s.Name) { - parts = append(parts, s.Name+" "+chartValue(c, s.Values[i], opts.Format)) + if !ch.Hidden(s.Name) { + parts = append(parts, s.Name+" "+ChartValue(c, s.Values[i], opts.Format)) } } return strings.Join(parts, " · ") @@ -85,7 +86,7 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh } ring.Close() p.StrokePath(&ring, 1, k.Border) - chartText(p, internal.FormatTick(t, step), cx-4, cy-rad*float32(t/top), size, k.TextMuted, 1, 0.5) + ChartText(p, internal.FormatTick(t, step), cx-4, cy-rad*float32(t/top), size, k.TextMuted, 1, 0.5) } for i, a := range axes { x, y := internal.Polar(cx, cy, rad, angle(i)) @@ -102,10 +103,10 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh if i == active { col = k.Text } - chartText(p, a, lx, ly, size, col, 1-ax, 0.5+0.5*float32(math.Cos(angle(i)))) + ChartText(p, a, lx, ly, size, col, 1-ax, 0.5+0.5*float32(math.Cos(angle(i)))) } for si, s := range series { - if ch.hidden(s.Name) { + if ch.Hidden(s.Name) { continue } var poly ui.Path @@ -136,6 +137,6 @@ func RadarChart(c *ui.Context, axes []string, series []ChartSeries, opts RadarCh } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/radar_chart_test.go b/chart/radar_chart_test.go similarity index 92% rename from radar_chart_test.go rename to chart/radar_chart_test.go index fde7651..ec7668e 100644 --- a/radar_chart_test.go +++ b/chart/radar_chart_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var radarAxes = []string{"Voice", "Latin", "Chant", "Organ", "Bells"} @@ -57,7 +59,7 @@ func TestRadarChartBad(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); build(c) }, 300, 300) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); build(c) }, 300, 300) }() } } diff --git a/realtime_chart.go b/chart/realtime_chart.go similarity index 84% rename from realtime_chart.go rename to chart/realtime_chart.go index 559f7be..d06a25f 100644 --- a/realtime_chart.go +++ b/chart/realtime_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "log/slog" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // RealtimeBuffer keeps the latest samples of named series. @@ -66,15 +69,15 @@ func RealtimeChart(c *ui.Context, s *RealtimeChartState, buf *RealtimeBuffer, op if opts.Window <= 0 { panic("mujica: RealtimeChart needs a positive Window") } - u := Density(c).Unit() + u := core.Density(c).Unit() var res ChartResult wrap := ui.Column(c).Gap(u * 2).MinWidth(0) wrap.Children(func() { - label := msg(c, "realtime_chart.pause") + label := core.Msg(c, "realtime_chart.pause") if s.Paused { - label = msg(c, "realtime_chart.resume") + label = core.Msg(c, "realtime_chart.resume") } - if b := Button(c, label, ButtonOptions{Variant: Secondary, Disabled: wrap.IsDisabled()}).AlignSelf(ui.Start); b.Clicked() && !b.IsDisabled() { + if b := input.Button(c, label, input.ButtonOptions{Variant: input.Secondary, Disabled: wrap.IsDisabled()}).AlignSelf(ui.Start); b.Clicked() && !b.IsDisabled() { s.Paused, res.value = !s.Paused, true slog.Debug("mujica: realtime_chart pause", "paused", s.Paused) } @@ -90,7 +93,7 @@ func RealtimeChart(c *ui.Context, s *RealtimeChartState, buf *RealtimeBuffer, op i0 := sort.Search(len(times), func(i int) bool { return !times[i].Before(from) }) i1 := sort.Search(len(times), func(i int) bool { return times[i].After(s.end) }) off := len(buf.times) - len(times) - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) series := make([]ChartSeries, len(buf.names)) tops := make([][]float64, len(buf.names)) for i, name := range buf.names { @@ -98,10 +101,10 @@ func RealtimeChart(c *ui.Context, s *RealtimeChartState, buf *RealtimeBuffer, op tops[i] = series[i].Values } n := i1 - i0 - var ax chartX + var ax ChartX if n > 0 { - ax = chartMemo(ch, "x", [2]any{chartID(times[i0:i1]), s.end}, func() chartX { - a := newChartX(n, nil, times[i0:i1]) + ax = ChartMemo(ch, "x", [2]any{ChartID(times[i0:i1]), s.end}, func() ChartX { + a := NewChartX(n, nil, times[i0:i1]) a.d0, a.d1 = float64(from.UnixNano())/1e9, float64(s.end.UnixNano())/1e9 if a.d1 == a.d0 { a.d0 -= 1 @@ -109,7 +112,7 @@ func RealtimeChart(c *ui.Context, s *RealtimeChartState, buf *RealtimeBuffer, op return a }) } - lo, hi, ok := chartYRange(ch, series, false, nil) + lo, hi, ok := ChartYRange(ch, series, false, nil) if !ok || math.IsNaN(lo) { n = 0 } diff --git a/realtime_chart_test.go b/chart/realtime_chart_test.go similarity index 99% rename from realtime_chart_test.go rename to chart/realtime_chart_test.go index ab71e08..84aed17 100644 --- a/realtime_chart_test.go +++ b/chart/realtime_chart_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" diff --git a/sankey_chart.go b/chart/sankey_chart.go similarity index 90% rename from sankey_chart.go rename to chart/sankey_chart.go index f66c4b2..f9bd22f 100644 --- a/sankey_chart.go +++ b/chart/sankey_chart.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SankeyNode is a stage that flow passes through. @@ -77,12 +78,12 @@ func sankeyPlace(nodes []SankeyNode, links []SankeyLink) sankeyLayout { // SankeyChart draws flows; reading a mark highlights its paths. func SankeyChart(c *ui.Context, nodes []SankeyNode, links []SankeyLink, opts SankeyChartOptions) ChartResult { - ch := newChart(c, opts.Frame) - l := chartMemo(ch, "layout", [2]string{chartID(nodes), chartID(links)}, func() sankeyLayout { return sankeyPlace(nodes, links) }) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + l := ChartMemo(ch, "layout", [2]string{ChartID(nodes), ChartID(links)}, func() sankeyLayout { return sankeyPlace(nodes, links) }) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) - val := func(v float64) string { return chartValue(c, v, opts.Format) } + size := core.FontSize(c, theme.CaptionSize) + val := func(v float64) string { return ChartValue(c, v, opts.Format) } size1 := func(i int) float64 { return max(l.in[i], l.out[i]) } // geom returns node boxes and link ends. geom := func(r ui.Rect) (boxes []ui.Rect, y0, y1 []float32, scale float32) { @@ -130,7 +131,7 @@ func SankeyChart(c *ui.Context, nodes []SankeyNode, links []SankeyLink, opts San spec := ChartSpec{Marks: n} spec.Readout = func(i int) string { if i < len(nodes) { - return nodes[i].Label + " · " + msg(c, "sankey_chart.in") + " " + val(l.in[i]) + " · " + msg(c, "sankey_chart.out") + " " + val(l.out[i]) + return nodes[i].Label + " · " + core.Msg(c, "sankey_chart.in") + " " + val(l.in[i]) + " · " + core.Msg(c, "sankey_chart.out") + " " + val(l.out[i]) } e := links[i-len(nodes)] return nodes[l.from[i-len(nodes)]].Label + " → " + nodes[l.to[i-len(nodes)]].Label + " · " + val(e.Value) @@ -192,13 +193,13 @@ func SankeyChart(c *ui.Context, nodes []SankeyNode, links []SankeyLink, opts San p.Stroke(ui.Rect{X: b.X - 2, Y: b.Y - 2, W: b.W + 4, H: b.H + 4}, k.Text, 2, 1.5) } if l.col[i] == l.cols-1 { - chartText(p, nodes[i].Label, b.X-6, b.Y+b.H/2, size, k.Text, 1, 0.5) + ChartText(p, nodes[i].Label, b.X-6, b.Y+b.H/2, size, k.Text, 1, 0.5) } else { - chartText(p, nodes[i].Label, b.X+b.W+6, b.Y+b.H/2, size, k.Text, 0, 0.5) + ChartText(p, nodes[i].Label, b.X+b.W+6, b.Y+b.H/2, size, k.Text, 0, 0.5) } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/sankey_chart_test.go b/chart/sankey_chart_test.go similarity index 96% rename from sankey_chart_test.go rename to chart/sankey_chart_test.go index 056c1b5..f553919 100644 --- a/sankey_chart_test.go +++ b/chart/sankey_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var sankeyNodes = []SankeyNode{{"tithe", "Tithes"}, {"alms", "Alms"}, {"fund", "Fund"}, {"roof", "Roof"}, {"bread", "Bread"}, {"bells", "Bells"}} @@ -65,7 +67,7 @@ func TestSankeyChartBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SankeyChart(c, sankeyNodes, links, SankeyChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) }() diff --git a/scatter_chart.go b/chart/scatter_chart.go similarity index 83% rename from scatter_chart.go rename to chart/scatter_chart.go index 20152a4..7ea3261 100644 --- a/scatter_chart.go +++ b/chart/scatter_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ScatterPoint is one dot; Size sets a bubble's area. @@ -39,16 +40,16 @@ type scatterDrag struct { // ScatterChart plots groups; box drag, Enter or Space selects. func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, opts ScatterChartOptions) ChartResult { - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) keys := make([]ChartLegendItem, len(groups)) var marks []ScatterKey b := [6]float64{math.Inf(1), math.Inf(-1), math.Inf(1), math.Inf(-1), math.Inf(1), math.Inf(-1)} for gi, g := range groups { keys[gi] = ChartLegendItem{Name: g.Name, Color: pal[gi%len(pal)], Shape: gi} - if g.Name == "" || ch.hidden(g.Name) { + if g.Name == "" || ch.Hidden(g.Name) { continue } for pi, pt := range g.Points { @@ -68,7 +69,7 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, } pt := func(m ScatterKey) ScatterPoint { return groups[m.Group].Points[m.Point] } geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) return plot, internal.Linear{D0: xs[0], D1: xs[1], R0: float64(plot.X), R1: float64(plot.X + plot.W)}, internal.Linear{D0: ys[0], D1: ys[1], R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -84,18 +85,18 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, } else { *selected = append(slices.Clone(*selected), m) } - ch.res.value = true + ch.Res.value = true slog.Debug("mujica: scatter_chart select", "count", len(*selected)) } - spec := ChartSpec{Marks: len(marks), Legend: keys, Hidden: ch.toggles(len(groups) > 1)} + spec := ChartSpec{Marks: len(marks), Legend: keys, Hidden: ch.Toggles(len(groups) > 1)} spec.Readout = func(i int) string { m, p := marks[i], pt(marks[i]) - s := fmt.Sprintf("%s · x %s · y %s", groups[m.Group].Name, chartValue(c, p.X, opts.Format), chartValue(c, p.Y, opts.Format)) + s := fmt.Sprintf("%s · x %s · y %s", groups[m.Group].Name, ChartValue(c, p.X, opts.Format), ChartValue(c, p.Y, opts.Format)) if opts.Bubbles { - s += " · " + msg(c, "scatter_chart.size") + " " + chartValue(c, p.Size, opts.Format) + s += " · " + core.Msg(c, "scatter_chart.size") + " " + ChartValue(c, p.Size, opts.Format) } if slices.Contains(*selected, m) { - s += " · " + msg(c, "scatter_chart.selected") + s += " · " + core.Msg(c, "scatter_chart.selected") } return s } @@ -104,7 +105,7 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, return internal.Nearest(len(marks), func(i int) (float32, float32) { return sx.Map(pt(marks[i]).X), sy.Map(pt(marks[i]).Y) }, x, y, 14, false) } spec.Activate = func(i int) { toggle(marks[i]) } - drag := ui.Local(ch.res.Element, "mujica.scatter.drag", func() scatterDrag { return scatterDrag{} }) + drag := ui.Local(ch.Res.Element, "mujica.scatter.drag", func() scatterDrag { return scatterDrag{} }) box := *drag spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { plot, sx, sy := geom(r) @@ -128,7 +129,7 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, p.StrokeDashed(scatterRect(box, r.X, r.Y), k.Text, 0, 1) } } - res, plot := ch.build(spec) + res, plot := ch.Build(spec) if plot != nil && !plot.IsDisabled() { x, y, _ := plot.PointerPosition() if _, _, held := plot.Dragged(); held { @@ -137,7 +138,7 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, } drag.x1, drag.y1 = x, y } else if drag.on { - if pb := plot.Bounds(); max(abs32(drag.x1-drag.x0), abs32(drag.y1-drag.y0)) > 4 { + if pb := plot.Bounds(); max(core.Abs32(drag.x1-drag.x0), core.Abs32(drag.y1-drag.y0)) > 4 { _, sx, sy := geom(ui.Rect{W: pb.W, H: pb.H}) in := scatterRect(*drag, 0, 0) var picked []ScatterKey @@ -156,5 +157,5 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup, } func scatterRect(d scatterDrag, ox, oy float32) ui.Rect { - return ui.Rect{X: ox + min(d.x0, d.x1), Y: oy + min(d.y0, d.y1), W: abs32(d.x1 - d.x0), H: abs32(d.y1 - d.y0)} + return ui.Rect{X: ox + min(d.x0, d.x1), Y: oy + min(d.y0, d.y1), W: core.Abs32(d.x1 - d.x0), H: core.Abs32(d.y1 - d.y0)} } diff --git a/scatter_chart_test.go b/chart/scatter_chart_test.go similarity index 96% rename from scatter_chart_test.go rename to chart/scatter_chart_test.go index 713b57c..11a3f44 100644 --- a/scatter_chart_test.go +++ b/chart/scatter_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var scatterGroups = []ScatterGroup{ @@ -67,7 +69,7 @@ func TestScatterChartBad(t *testing.T) { }() var sel []ScatterKey ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ScatterChart(c, &sel, []ScatterGroup{{Name: "a", Points: []ScatterPoint{{1, 1, -1}}}}, ScatterChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/spark_bar.go b/chart/spark_bar.go similarity index 88% rename from spark_bar.go rename to chart/spark_bar.go index 431e83f..834e304 100644 --- a/spark_bar.go +++ b/chart/spark_bar.go @@ -1,8 +1,11 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "math" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // SparkBarOptions configure a SparkBar; Frame.Height 0 means 28 DIPs. @@ -25,7 +28,7 @@ func SparkBar(c *ui.Context, values []float64, opts SparkBarOptions) ChartResult w, _ := c.Size() frame := opts.Frame if frame.Empty == "" { - frame.Empty = msg(c, "spark_bar.empty") + frame.Empty = core.Msg(c, "spark_bar.empty") } return Sparkline(c, values, SparklineOptions{Frame: frame, Kind: SparklineBar, Width: min(max(28, float32(len(values))*(bw+1)+6), max(28, w-48)), Format: opts.Format}) } diff --git a/spark_bar_test.go b/chart/spark_bar_test.go similarity index 90% rename from spark_bar_test.go rename to chart/spark_bar_test.go index 4e468c3..5ee1599 100644 --- a/spark_bar_test.go +++ b/chart/spark_bar_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "image/color" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) var sparkBarValues = []float64{3, -2, 4, 1, -3} @@ -46,7 +48,7 @@ func TestSparkBarWidthAndColors(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "x"}, BarWidth: -1}) }, 100, 100) } @@ -65,7 +67,7 @@ func TestSparkBarShrinkingAndDisabled(t *testing.T) { disabled := false var r ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r = SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "Bars"}}) }) @@ -92,7 +94,7 @@ func TestSparkBarLargeData(t *testing.T) { } var built int tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res := SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "Large bars"}}) memo := ui.Local(res.Element, "mujica.sparkline.data", func() sparkMemo { return sparkMemo{} }) built = len(memo.indices) @@ -116,27 +118,27 @@ func TestSparkBarFiniteWidthAndStates(t *testing.T) { charts2MustPanic(t, func(c *ui.Context) { SparkBar(c, []float64{1}, SparkBarOptions{Frame: ChartFrame{Label: "Bars"}, BarWidth: float32(math.Inf(1))}) }) - status := DataReady + status := data.DataReady retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SparkBar(c, []float64{2, -1, 3}, SparkBarOptions{Frame: ChartFrame{Label: "Bars", Status: status, Height: 40}}) if r.Retried() { retries++ } }, 320, 100) ready, _ := tt.Find("Bars") - status = DataFailed + status = data.DataFailed tt.Frame() if err := tt.Click("Retry"); err != nil || retries != 1 { t.Fatalf("retry %v count %d", err, retries) } - status = DataLoading + status = data.DataLoading tt.Frame() if ready.W != 28 || ready.H != 40 { t.Fatalf("ready plot %v", ready) } - status = DataReady + status = data.DataReady tt.Frame() tt.Move(319, 99) tabTo(t, tt, "Bars") @@ -150,7 +152,7 @@ func TestSparkBarMinimumHitWidth(t *testing.T) { for _, values := range [][]float64{nil, {1}} { var result ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) result = SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "Bars"}}) }, 200, 100) tt.Frame() diff --git a/sparkline.go b/chart/sparkline.go similarity index 84% rename from sparkline.go rename to chart/sparkline.go index 43c25a7..34ed1e9 100644 --- a/sparkline.go +++ b/chart/sparkline.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "log/slog" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SparklineKind is how a Sparkline draws its values. @@ -42,14 +44,14 @@ func Sparkline(c *ui.Context, values []float64, opts SparklineOptions) ChartResu if w == 0 { w = 96 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := internal.Palette(k)[0] - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) res := ChartResult{Point: -1} res.Element = ui.Row(c).Gap(u * 2).AlignItems(ui.Center).MinWidth(0) memo := ui.Local(res.Element, "mujica.sparkline.data", func() sparkMemo { return sparkMemo{} }) - if id := chartID(values); memo.id != id { + if id := ChartID(values); memo.id != id { *memo = sparkMemo{id: id, lo: math.Inf(1), hi: math.Inf(-1), last: -1} for i, v := range values { if math.IsInf(v, 0) { @@ -64,19 +66,19 @@ func Sparkline(c *ui.Context, values []float64, opts SparklineOptions) ChartResu st := ui.Local(res.Element, "mujica.sparkline", func() chartState { return chartState{focus: -1, last: -1} }) st.focus = min(st.focus, len(values)-1) res.Element.Children(func() { - if f.Status == DataLoading || f.Status == DataFailed || last < 0 { + if f.Status == data.DataLoading || f.Status == data.DataFailed || last < 0 { box := ui.Row(c).Size(w, f.Height).AlignItems(ui.Center).Gap(u) box.Children(func() { switch f.Status { - case DataLoading: - Spinner(c, SpinnerOptions{Size: 14, Label: msg(c, "list.loading")}) - case DataFailed: - ui.Text(c, dataError(c, f.Error)).FontSize(size).TextColor(k.Danger).SingleLine().Grow(1).MinWidth(0) - res.retry = iconAction(c, icons.Must("refresh-cw"), msg(c, "list.retry")).Clicked() + case data.DataLoading: + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: core.Msg(c, "list.loading")}) + case data.DataFailed: + ui.Text(c, data.DataError(c, f.Error)).FontSize(size).TextColor(k.Danger).SingleLine().Grow(1).MinWidth(0) + res.retry = core.IconAction(c, icons.Must("refresh-cw"), core.Msg(c, "list.retry")).Clicked() default: empty := f.Empty if empty == "" { - empty = msg(c, "chart.empty") + empty = core.Msg(c, "chart.empty") } ui.Text(c, empty).FontSize(size).TextColor(k.TextMuted).SingleLine() } @@ -114,7 +116,7 @@ func Sparkline(c *ui.Context, values []float64, opts SparklineOptions) ChartResu if to != st.focus { st.focus = to if to >= 0 { - c.Announce(chartValue(c, values[to], opts.Format)) + c.Announce(ChartValue(c, values[to], opts.Format)) } slog.Debug("mujica: sparkline focus", "label", f.Label, "mark", to) } @@ -165,7 +167,7 @@ func Sparkline(c *ui.Context, values []float64, opts SparklineOptions) ChartResu if active >= 0 { shown = active } - ui.Text(c, chartValue(c, values[shown], opts.Format)).FontSize(size).TextColor(k.Text).SingleLine().Role(ui.RoleStatus) + ui.Text(c, ChartValue(c, values[shown], opts.Format)).FontSize(size).TextColor(k.Text).SingleLine().Role(ui.RoleStatus) }) res.changed = res.Point != st.last st.last = res.Point @@ -199,6 +201,6 @@ func sparkBars(p *ui.Painter, r ui.Rect, values []float64, indices []int, x func } } b := s.Map(v) - p.Fill(ui.Rect{X: x(i) - bw/2, Y: min(zero, b), W: bw, H: max(abs32(zero-b), 1)}, col, 0) + p.Fill(ui.Rect{X: x(i) - bw/2, Y: min(zero, b), W: bw, H: max(core.Abs32(zero-b), 1)}, col, 0) } } diff --git a/sparkline_test.go b/chart/sparkline_test.go similarity index 86% rename from sparkline_test.go rename to chart/sparkline_test.go index b8bdbbd..a24fabb 100644 --- a/sparkline_test.go +++ b/chart/sparkline_test.go @@ -1,10 +1,13 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) var sparkValues = []float64{3, 5, 4, math.NaN(), 7, 6, 9} @@ -43,18 +46,18 @@ func TestSparklineEndValueAndKeys(t *testing.T) { } func TestSparklineStates(t *testing.T) { - status := DataLoading + status := data.DataLoading var vals []float64 h := &chartHarness{build: func(c *ui.Context) ChartResult { return Sparkline(c, vals, SparklineOptions{Frame: ChartFrame{Label: "Bells", Status: status}}) }} tt := ui.NewTester(h.view, 300, 200) - status = DataFailed + status = data.DataFailed tt.Frame() if err := tt.Click("Retry"); err != nil { t.Fatal(err) } - status = DataReady + status = data.DataReady tt.Frame() if !tt.HasText("No data") { t.Fatalf("empty %q", tt.Texts()) @@ -64,7 +67,7 @@ func TestSparklineStates(t *testing.T) { t.Fatal("no panic without a label") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Sparkline(c, vals, SparklineOptions{}) }, 100, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); Sparkline(c, vals, SparklineOptions{}) }, 100, 100) } func TestSparklineSnapshot(t *testing.T) { diff --git a/sunburst.go b/chart/sunburst.go similarity index 84% rename from sunburst.go rename to chart/sunburst.go index d6e17de..4b21972 100644 --- a/sunburst.go +++ b/chart/sunburst.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SunburstOptions configure a Sunburst. @@ -41,9 +42,9 @@ func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOption } chartCheckTree(root) chartDescend(root, *path) - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) pal := internal.Palette(k) var res ChartResult wrap := ui.Column(c).Gap(u * 2).MinWidth(0) @@ -81,13 +82,13 @@ func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOption hole = rad * 0.3 return r.X + r.W/2, r.Y + r.H/2, hole, (rad - hole) / float32(depth) } - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) spec := ChartSpec{Marks: len(arcs)} spec.Readout = func(i int) string { a := arcs[i] - s := fmt.Sprintf("%s · %s · %.1f%%", strings.Join(a.path, " › "), chartValue(c, a.node.total(), opts.Format), 100*a.node.total()/sum) + s := fmt.Sprintf("%s · %s · %.1f%%", strings.Join(a.path, " › "), ChartValue(c, a.node.total(), opts.Format), 100*a.node.total()/sum) if len(a.node.Children) > 0 { - s += " · " + msg(c, "chart.opens") + s += " · " + core.Msg(c, "chart.opens") } return s } @@ -129,7 +130,7 @@ func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOption p.FillPath(internal.Sector(cx, cy, r0, r0+ring, a.a0, a.a1), col) p.StrokePath(internal.Sector(cx, cy, r0, r0+ring, a.a0, a.a1), 1.5, k.Surface) if i == active { - p.StrokePath(internal.Sector(cx, cy, r0+2, r0+ring-2, a.a0, a.a1), 2, chartInk(k, col)) + p.StrokePath(internal.Sector(cx, cy, r0+2, r0+ring-2, a.a0, a.a1), 2, ChartInk(k, col)) } } // Labels go on top where arcs are long enough. @@ -138,13 +139,13 @@ func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOption col := color(a) if mid := float64(r0+ring/2) * (a.a1 - a.a0); mid > 44 { x, y := internal.Polar(cx, cy, r0+ring/2, (a.a0+a.a1)/2) - chartText(p, chartFit(p, a.node.Name, min(float32(mid)*0.8, ring*1.25), size), x, y, size, chartInk(k, col), 0.5, 0.5) + ChartText(p, chartFit(p, a.node.Name, min(float32(mid)*0.8, ring*1.25), size), x, y, size, ChartInk(k, col), 0.5, 0.5) } } - chartText(p, chartFit(p, node.Name, hole*2-8, size), cx, cy, size, k.TextMuted, 0.5, 1) - chartText(p, chartValue(c, sum, opts.Format), cx, cy+2, FontSize(c, theme.BodySize), k.Text, 0.5, 0) + ChartText(p, chartFit(p, node.Name, hole*2-8, size), cx, cy, size, k.TextMuted, 0.5, 1) + ChartText(p, ChartValue(c, sum, opts.Format), cx, cy+2, core.FontSize(c, theme.BodySize), k.Text, 0.5, 0) } - r, plot := ch.build(spec) + r, plot := ch.Build(spec) if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 { *path, res.value = (*path)[:len(*path)-1], true } diff --git a/sunburst_test.go b/chart/sunburst_test.go similarity index 95% rename from sunburst_test.go rename to chart/sunburst_test.go index e65d9c4..09ceee9 100644 --- a/sunburst_test.go +++ b/chart/sunburst_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSunburstReadoutAndDrill(t *testing.T) { @@ -49,7 +51,7 @@ func TestSunburstBad(t *testing.T) { }() var path []string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Sunburst(c, &path, ChartNode{Name: "r", Children: []ChartNode{{Name: "a", Value: 1}, {Name: "a", Value: 2}}}, SunburstOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/time_range_selector.go b/chart/time_range_selector.go similarity index 69% rename from time_range_selector.go rename to chart/time_range_selector.go index 127a6e0..9d97670 100644 --- a/time_range_selector.go +++ b/chart/time_range_selector.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,6 +6,10 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // RangePreset names a time range; zero selects Range1D. @@ -28,7 +32,7 @@ var rangePresetNames = []string{"1D", "5D", "1M", "3M", "YTD", "1Y", "5Y"} type MarketRange struct { Preset RangePreset // Custom holds the dates when Preset is RangeCustom. - Custom DateRange + Custom datetime.DateRange } // Bounds returns local boundaries, including the custom end date. @@ -63,11 +67,11 @@ func (r MarketRange) Bounds(now time.Time, loc *time.Location) (from, to time.Ti type TimeRangeSelectorOptions struct { Disabled bool // Today marks the calendar's today; zero uses the clock. - Today Date + Today core.Date } // TimeRangeSelector chooses presets and custom dates through keyboard-accessible controls. -func TimeRangeSelector(c *ui.Context, value *MarketRange, opts TimeRangeSelectorOptions) ChoiceResult { +func TimeRangeSelector(c *ui.Context, value *MarketRange, opts TimeRangeSelectorOptions) core.ChoiceResult { if value == nil { panic("mujica: TimeRangeSelector needs a value") } @@ -77,24 +81,24 @@ func TimeRangeSelector(c *ui.Context, value *MarketRange, opts TimeRangeSelector if value.Preset == RangeCustom && value.Custom.IsZero() { panic("mujica: custom MarketRange without dates") } - items := make([]ToggleItem[RangePreset], len(rangePresetNames)+1) + items := make([]input.ToggleItem[RangePreset], len(rangePresetNames)+1) for i, n := range rangePresetNames { - items[i] = ToggleItem[RangePreset]{Value: RangePreset(i), Label: n} + items[i] = input.ToggleItem[RangePreset]{Value: RangePreset(i), Label: n} } - items[len(items)-1] = ToggleItem[RangePreset]{Value: RangeCustom, Label: msg(c, "time_range_selector.custom")} + items[len(items)-1] = input.ToggleItem[RangePreset]{Value: RangeCustom, Label: core.Msg(c, "time_range_selector.custom")} changed := false - row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Wrap().Disabled(opts.Disabled) + row := ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Wrap().Disabled(opts.Disabled) row.Children(func() { sel := []RangePreset{value.Preset} - if ToggleGroup(c, &sel, items, ToggleGroupOptions{Label: msg(c, "time_range_selector.label")}).Changed() && !row.IsDisabled() { + if input.ToggleGroup(c, &sel, items, input.ToggleGroupOptions{Label: core.Msg(c, "time_range_selector.label")}).Changed() && !row.IsDisabled() { if sel[0] == RangeCustom && value.Custom.IsZero() { - today := datetimeToday(c, opts.Today) - value.Custom = DateRange{Start: today.AddMonths(-1), End: today} + today := core.DatetimeToday(c, opts.Today) + value.Custom = datetime.DateRange{Start: today.AddMonths(-1), End: today} } value.Preset, changed = sel[0], true } if value.Preset == RangeCustom { - if DateRangePicker(c, &value.Custom, DateRangePickerOptions{Today: opts.Today, Max: datetimeToday(c, opts.Today)}).Changed() { + if datetime.DateRangePicker(c, &value.Custom, datetime.DateRangePickerOptions{Today: opts.Today, Max: core.DatetimeToday(c, opts.Today)}).Changed() { changed = true } } @@ -102,5 +106,5 @@ func TimeRangeSelector(c *ui.Context, value *MarketRange, opts TimeRangeSelector if changed { slog.Debug("mujica: time_range_selector changed", "preset", value.Preset, "custom", value.Custom) } - return ChoiceResult{row, changed} + return core.NewChoiceResult(row, changed) } diff --git a/time_range_selector_test.go b/chart/time_range_selector_test.go similarity index 79% rename from time_range_selector_test.go rename to chart/time_range_selector_test.go index 714dd27..f839d32 100644 --- a/time_range_selector_test.go +++ b/chart/time_range_selector_test.go @@ -1,17 +1,20 @@ -package mujica +package chart import ( - "github.com/egoist/mygo/ui" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTimeRangeSelectorPresetsCustomAndDisabled(t *testing.T) { value := MarketRange{} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - TimeRangeSelector(c, &value, TimeRangeSelectorOptions{Disabled: disabled, Today: Date{2026, time.October, 5}}) + core.Use(c, core.Settings{}) + TimeRangeSelector(c, &value, TimeRangeSelectorOptions{Disabled: disabled, Today: core.Date{Year: 2026, Month: time.October, Day: 5}}) }, 850, 160) tt.Key(0, ui.KeyTab) for _, want := range []RangePreset{Range5D, Range1M, Range3M, RangeYTD, Range1Y, Range5Y, RangeCustom} { @@ -21,7 +24,7 @@ func TestTimeRangeSelectorPresetsCustomAndDisabled(t *testing.T) { t.Fatalf("preset %v", value.Preset) } } - if value.Custom.Start != (Date{2026, time.September, 5}) || value.Custom.End != (Date{2026, time.October, 5}) { + if value.Custom.Start != (core.Date{Year: 2026, Month: time.September, Day: 5}) || value.Custom.End != (core.Date{Year: 2026, Month: time.October, Day: 5}) { t.Fatalf("custom %+v", value.Custom) } now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) diff --git a/treemap.go b/chart/treemap.go similarity index 79% rename from treemap.go rename to chart/treemap.go index 8740523..60d0f3b 100644 --- a/treemap.go +++ b/chart/treemap.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "cmp" @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TreemapOptions configure a Treemap. @@ -23,9 +24,9 @@ type TreemapOptions struct { func Treemap(c *ui.Context, path *[]string, root ChartNode, opts TreemapOptions) ChartResult { chartCheckTree(root) node := chartDescend(root, *path) - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) pal := internal.Palette(k) var res ChartResult wrap := ui.Column(c).Gap(u * 2).MinWidth(0) @@ -44,13 +45,13 @@ func Treemap(c *ui.Context, path *[]string, root ChartNode, opts TreemapOptions) for i, ch := range kids { values[i] = ch.total() } - ch := newChart(c, opts.Frame) + ch := NewChart(c, opts.Frame) tiles := func(r ui.Rect) []ui.Rect { return squarify(values, r) } spec := ChartSpec{Marks: len(kids)} spec.Readout = func(i int) string { - s := fmt.Sprintf("%s · %s · %.1f%%", kids[i].Name, chartValue(c, values[i], opts.Format), 100*values[i]/sum) + s := fmt.Sprintf("%s · %s · %.1f%%", kids[i].Name, ChartValue(c, values[i], opts.Format), 100*values[i]/sum) if len(kids[i].Children) > 0 { - s += " · " + msg(c, "chart.opens") + s += " · " + core.Msg(c, "chart.opens") } return s } @@ -75,19 +76,19 @@ func Treemap(c *ui.Context, path *[]string, root ChartNode, opts TreemapOptions) p.Fill(t, bg, 2) p.Stroke(t, k.Surface, 2, 2) if i == active { - p.Stroke(ui.Rect{X: t.X + 3, Y: t.Y + 3, W: t.W - 6, H: t.H - 6}, chartInk(k, bg), 1, 2) + p.Stroke(ui.Rect{X: t.X + 3, Y: t.Y + 3, W: t.W - 6, H: t.H - 6}, ChartInk(k, bg), 1, 2) } // Labels show only where tiles have room. if t.W >= 36 && t.H >= 22 { - ink := chartInk(k, bg) - chartText(p, chartFit(p, kids[i].Name, t.W-12, size), t.X+6, t.Y+5, size, ink, 0, 0) + ink := ChartInk(k, bg) + ChartText(p, chartFit(p, kids[i].Name, t.W-12, size), t.X+6, t.Y+5, size, ink, 0, 0) if t.H >= 40 { - chartText(p, chartFit(p, chartValue(c, values[i], opts.Format), t.W-12, size), t.X+6, t.Y+22, size, ink, 0, 0) + ChartText(p, chartFit(p, ChartValue(c, values[i], opts.Format), t.W-12, size), t.X+6, t.Y+22, size, ink, 0, 0) } } } } - r, plot := ch.build(spec) + r, plot := ch.Build(spec) if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 { *path, res.value = (*path)[:len(*path)-1], true } diff --git a/treemap_test.go b/chart/treemap_test.go similarity index 96% rename from treemap_test.go rename to chart/treemap_test.go index 4abeacb..fa7f75e 100644 --- a/treemap_test.go +++ b/chart/treemap_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func chartTree() ChartNode { @@ -74,7 +76,7 @@ func TestTreemapBadPath(t *testing.T) { }() path := []string{"Nowhere"} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Treemap(c, &path, chartTree(), TreemapOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/trend_indicator.go b/chart/trend_indicator.go similarity index 75% rename from trend_indicator.go rename to chart/trend_indicator.go index 397143d..daa436d 100644 --- a/trend_indicator.go +++ b/chart/trend_indicator.go @@ -1,12 +1,14 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TrendIndicatorOptions configure a TrendIndicator. @@ -28,8 +30,8 @@ func TrendIndicator(c *ui.Context, change float64, opts TrendIndicatorOptions) * if opts.Precision < 0 || math.IsNaN(change) || math.IsInf(change, 0) { panic("mujica: TrendIndicator needs a number and Precision ≥ 0") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() word, text := trendWords(c, change, opts) col, icon := k.TextMuted, "minus" switch { @@ -48,7 +50,7 @@ func TrendIndicator(c *ui.Context, change float64, opts TrendIndicatorOptions) * ui.Text(c, text).FontWeight(600).SingleLine() }) if opts.Note != "" { - ui.Text(c, opts.Note).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, opts.Note).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) return e @@ -56,15 +58,15 @@ func TrendIndicator(c *ui.Context, change float64, opts TrendIndicatorOptions) * // trendWords formats direction and signed change. func trendWords(c *ui.Context, change float64, opts TrendIndicatorOptions) (word, text string) { - word = msg(c, "trend_indicator.flat") + word = core.Msg(c, "trend_indicator.flat") switch { case change > 0: - word = msg(c, "trend_indicator.up") + word = core.Msg(c, "trend_indicator.up") case change < 0: - word = msg(c, "trend_indicator.down") + word = core.Msg(c, "trend_indicator.down") } if opts.Format != nil { return word, opts.Format(change) } - return word, statisticSigned(change, opts.Precision) + opts.Unit + return word, data.StatisticSigned(change, opts.Precision) + opts.Unit } diff --git a/trend_indicator_test.go b/chart/trend_indicator_test.go similarity index 86% rename from trend_indicator_test.go rename to chart/trend_indicator_test.go index f2dce8a..0acff5b 100644 --- a/trend_indicator_test.go +++ b/chart/trend_indicator_test.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "image/color" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestTrendIndicatorWordsAndColors(t *testing.T) { @@ -24,7 +25,7 @@ func TestTrendIndicatorWordsAndColors(t *testing.T) { } { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { r = TrendIndicator(c, tc.change, TrendIndicatorOptions{Precision: 1, Unit: "%", Invert: tc.invert, Note: "vs Lent"}).Bounds() }) @@ -53,7 +54,7 @@ func TestTrendIndicatorWordsAndColors(t *testing.T) { func TestTrendIndicatorFormatAndBad(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TrendIndicator(c, 2, TrendIndicatorOptions{Format: func(v float64) string { return "two more" }}) }, 200, 100) if !tt.HasText("two more") { @@ -64,7 +65,10 @@ func TestTrendIndicatorFormatAndBad(t *testing.T) { t.Fatal("no panic for NaN") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); TrendIndicator(c, math.NaN(), TrendIndicatorOptions{}) }, 100, 100) + ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + TrendIndicator(c, math.NaN(), TrendIndicatorOptions{}) + }, 100, 100) } func TestTrendIndicatorSnapshot(t *testing.T) { @@ -81,7 +85,7 @@ func TestTrendIndicatorSnapshot(t *testing.T) { func TestTrendIndicatorFiniteAndChinese(t *testing.T) { charts2MustPanic(t, func(c *ui.Context) { TrendIndicator(c, math.Inf(1), TrendIndicatorOptions{}) }) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) TrendIndicator(c, -2, TrendIndicatorOptions{Unit: "%"}) }, 200, 100) if _, ok := tt.Find("下降 −2%"); !ok { diff --git a/violin_plot.go b/chart/violin_plot.go similarity index 85% rename from violin_plot.go rename to chart/violin_plot.go index 1f4884c..b192b0e 100644 --- a/violin_plot.go +++ b/chart/violin_plot.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ViolinPlotOptions configure a ViolinPlot. @@ -32,10 +33,10 @@ func ViolinPlot(c *ui.Context, groups []BoxGroup, opts ViolinPlotOptions) ChartR if opts.Bandwidth < 0 || math.IsNaN(opts.Bandwidth) || math.IsInf(opts.Bandwidth, 0) { panic("mujica: ViolinPlot Bandwidth must be ≥ 0") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) ids := "" seen := map[string]bool{} for _, g := range groups { @@ -43,9 +44,9 @@ func ViolinPlot(c *ui.Context, groups []BoxGroup, opts ViolinPlotOptions) ChartR panic("mujica: ViolinPlot groups need names") } seen[g.Name] = true - ids += chartID(g.Values) + ";" + ids += ChartID(g.Values) + ";" } - shapes := chartMemo(ch, "violins", fmt.Sprint(ids, opts.Bandwidth), func() []violinShape { + shapes := ChartMemo(ch, "violins", fmt.Sprint(ids, opts.Bandwidth), func() []violinShape { out := make([]violinShape, len(groups)) for i, g := range groups { v := densitySample(g.Values) @@ -85,20 +86,20 @@ func ViolinPlot(c *ui.Context, groups []BoxGroup, opts ViolinPlotOptions) ChartR } else { lo, hi, step = internal.Nice(lo, hi, 4) } - val := func(v float64) string { return chartValue(c, v, opts.Format) } + val := func(v float64) string { return ChartValue(c, v, opts.Format) } geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear) { - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) return plot, internal.Band{N: len(groups), R0: plot.X, R1: plot.X + plot.W, Pad: 0.15}, internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } - visible := ui.Local(ch.res.Element, "mujica.violin.visible", func() []int { return nil }) + visible := ui.Local(ch.Res.Element, "mujica.violin.visible", func() []int { return nil }) spec := ChartSpec{Marks: n} spec.Readout = func(i int) string { s := shapes[i].stats if s.n == 0 { - return groups[i].Name + " · " + msg(c, "chart.missing") + return groups[i].Name + " · " + core.Msg(c, "chart.missing") } - return fmt.Sprintf(msg(c, "violin_plot.readout"), groups[i].Name, val(s.median), val(s.q1), val(s.q3), s.n) + return fmt.Sprintf(core.Msg(c, "violin_plot.readout"), groups[i].Name, val(s.median), val(s.q1), val(s.q3), s.n) } spec.Pick = func(r ui.Rect, x, y float32) int { plot, band, _ := geom(r) @@ -154,6 +155,6 @@ func ViolinPlot(c *ui.Context, groups []BoxGroup, opts ViolinPlotOptions) ChartR p.FillPath(internal.Diamond(internal.Snap(p, cx), internal.Snap(p, ys.Map(s.stats.median)), 7), k.Text) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/violin_plot_test.go b/chart/violin_plot_test.go similarity index 96% rename from violin_plot_test.go rename to chart/violin_plot_test.go index a6c5d6c..a7b4fca 100644 --- a/violin_plot_test.go +++ b/chart/violin_plot_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestViolinPlotReadout(t *testing.T) { @@ -32,7 +34,7 @@ func TestViolinPlotBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ViolinPlot(c, []BoxGroup{{Values: []float64{1}}}, ViolinPlotOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) } diff --git a/volume_chart.go b/chart/volume_chart.go similarity index 80% rename from volume_chart.go rename to chart/volume_chart.go index bd3bb6a..b3ec5d2 100644 --- a/volume_chart.go +++ b/chart/volume_chart.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "math" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // VolumeChartOptions configure a VolumeChart. @@ -29,19 +31,19 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC if opts.Frame.Height == 0 { opts.Frame.Height = 120 } - ch := newChart(c, opts.Frame) - chartMemo(ch, "check", chartID(candles), func() bool { marketCheckCandles(candles); return true }) - if ch.res.Element.IsDisabled() { + ch := NewChart(c, opts.Frame) + ChartMemo(ch, "check", ChartID(candles), func() bool { marketCheckCandles(candles); return true }) + if ch.Res.Element.IsDisabled() { copy := *view view = © opts.Sync = nil } - m := &mktCore{c: c, k: Tokens(c), ch: ch, candles: candles, view: view} + m := &mktCore{c: c, k: core.Tokens(c), ch: ch, candles: candles, view: view} if s := opts.Sync; s != nil { s.load(opts.SyncID, candles, view) } m.window() - hi := chartMemo(ch, "vmax", [3]any{chartID(candles), m.from, m.to}, func() float64 { + hi := ChartMemo(ch, "vmax", [3]any{ChartID(candles), m.from, m.to}, func() float64 { v := 0.0 for i := m.from; i < m.to; i++ { v = max(v, candles[i].Volume) @@ -49,9 +51,9 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC return v }) k := m.k - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear, []float64, float64) { - plot := chartInset(r, 60, 4) + plot := ChartInset(r, 60, 4) lo, top, step := internal.Nice(0, max(hi, 1), 2) return plot, internal.Band{N: m.to - m.from, R0: plot.X, R1: plot.X + plot.W, Pad: 0.3}, internal.Linear{D0: lo, D1: top, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)}, internal.Ticks(lo, top, step), step @@ -69,8 +71,8 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC } spec.Readout = func(i int) string { cd := candles[i] - return strings.Join([]string{cd.Time.Format("2006-01-02 15:04"), msg(c, "candlestick_chart.volume") + " " + statisticFormat(cd.Volume, 0), - trendWord(c, trendSign(cd.Close-cd.Open))}, " · ") + return strings.Join([]string{cd.Time.Format("2006-01-02 15:04"), core.Msg(c, "candlestick_chart.volume") + " " + data.StatisticFormat(cd.Volume, 0), + core.TrendWord(c, core.TrendSign(cd.Close-cd.Open))}, " · ") } spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { plot, band, ys, ticks, step := geom(r) @@ -86,9 +88,9 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC y := ys.Map(cd.Volume) bar := ui.Rect{X: x - w/2, Y: y, W: w, H: plot.Y + plot.H - y} up := cd.Close >= cd.Open - col := trendColor(c, 1) + col := core.TrendColor(c, 1) if !up { - col = trendColor(c, -1) + col = core.TrendColor(c, -1) } if up && w >= 3 { p.Fill(bar, k.Surface, 0) @@ -103,7 +105,7 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC p.Line(x, plot.Y, x, plot.Y+plot.H, 1, k.TextMuted) } } - res, plot := ch.build(spec) + res, plot := ch.Build(spec) if plot != nil && !plot.IsDisabled() { room := m.room() switch { @@ -125,7 +127,7 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC // st0 is the chart's market state, made on first use. func (m *mktCore) st0() *mktState { if m.st == nil { - m.st = *ui.Local(m.ch.res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1} }) + m.st = *ui.Local(m.ch.Res.Element, "mujica.mkt", func() *mktState { return &mktState{grab: -1, focus: -1} }) } return m.st } diff --git a/volume_chart_test.go b/chart/volume_chart_test.go similarity index 86% rename from volume_chart_test.go rename to chart/volume_chart_test.go index ef563d6..10a11f5 100644 --- a/volume_chart_test.go +++ b/chart/volume_chart_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestVolumeChartSharesViewAndReads(t *testing.T) { @@ -12,7 +14,7 @@ func TestVolumeChartSharesViewAndReads(t *testing.T) { view := MarketView{Count: 60} var price, vol ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(800).Children(func() { price = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "Price"}}) vol = VolumeChart(c, candles, &view, VolumeChartOptions{Frame: ChartFrame{Label: "Volume"}}) @@ -42,7 +44,7 @@ func TestVolumeChartSharesViewAndReads(t *testing.T) { t.Fatal("nil view accepted") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); VolumeChart(c, candles, nil, VolumeChartOptions{}) }, 200, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); VolumeChart(c, candles, nil, VolumeChartOptions{}) }, 200, 100) } func TestVolumeChartSnapshot(t *testing.T) { diff --git a/volume_profile.go b/chart/volume_profile.go similarity index 82% rename from volume_profile.go rename to chart/volume_profile.go index 34f774a..366d01a 100644 --- a/volume_profile.go +++ b/chart/volume_profile.go @@ -1,4 +1,4 @@ -package mujica +package chart import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ProfileBin is the volume traded between two prices. @@ -114,10 +116,10 @@ func VolumeProfile(c *ui.Context, bins []ProfileBin, opts VolumeProfileOptions) if !(frac > 0 && frac <= 1) { panic("mujica: VolumeProfile ValueArea must be in (0, 1]") } - dec := priceDecimals("VolumeProfile", opts.Decimals) - ch := newChart(c, opts.Frame) - k := Tokens(c) - va := chartMemo(ch, "va", [2]any{chartID(bins), frac}, func() [4]float64 { + dec := core.PriceDecimals("VolumeProfile", opts.Decimals) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + va := ChartMemo(ch, "va", [2]any{ChartID(bins), frac}, func() [4]float64 { vmax := 0.0 for i, b := range bins { if math.IsNaN(b.Volume) || math.IsInf(b.Volume, 0) || math.IsInf(b.High, 0) || math.IsInf(b.Low, 0) || !(b.High > b.Low) || b.Volume < 0 || i > 0 && b.Low < bins[i-1].High-1e-9 { @@ -133,11 +135,11 @@ func VolumeProfile(c *ui.Context, bins []ProfileBin, opts VolumeProfileOptions) }) poc, vlo, vhi, vmax := int(va[0]), int(va[1]), int(va[2]), va[3] inArea, outArea := k.AccentText, k.TextMuted - keys := []ChartLegendItem{{Name: msg(c, "volume_profile.value_area"), Color: inArea, Shape: 1}, {Name: msg(c, "volume_profile.other"), Color: outArea, Shape: 4}} - drawn := ui.Local(ch.res.Element, "mujica.profile.drawn", func() int { return 0 }) - size := FontSize(c, theme.CaptionSize) + keys := []ChartLegendItem{{Name: core.Msg(c, "volume_profile.value_area"), Color: inArea, Shape: 1}, {Name: core.Msg(c, "volume_profile.other"), Color: outArea, Shape: 4}} + drawn := ui.Local(ch.Res.Element, "mujica.profile.drawn", func() int { return 0 }) + size := core.FontSize(c, theme.CaptionSize) geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 60, 26) + plot := ChartInset(r, 60, 26) return plot, internal.Linear{D0: 0, D1: max(vmax, 1), R0: float64(plot.X), R1: float64(plot.X + plot.W)}, internal.Linear{D0: bins[0].Low, D1: bins[len(bins)-1].High, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -162,12 +164,12 @@ func VolumeProfile(c *ui.Context, bins []ProfileBin, opts VolumeProfileOptions) } spec.Readout = func(i int) string { b := bins[i] - parts := []string{priceUngrouped(b.Low, dec) + "–" + priceUngrouped(b.High, dec), msg(c, "candlestick_chart.volume") + " " + statisticFormat(b.Volume, 0)} + parts := []string{core.PriceUngrouped(b.Low, dec) + "–" + core.PriceUngrouped(b.High, dec), core.Msg(c, "candlestick_chart.volume") + " " + data.StatisticFormat(b.Volume, 0)} if i == poc { - parts = append(parts, msg(c, "volume_profile.poc")) + parts = append(parts, core.Msg(c, "volume_profile.poc")) } if i >= vlo && i <= vhi { - parts = append(parts, msg(c, "volume_profile.value_area")) + parts = append(parts, core.Msg(c, "volume_profile.value_area")) } return strings.Join(parts, " · ") } @@ -182,7 +184,7 @@ func VolumeProfile(c *ui.Context, bins []ProfileBin, opts VolumeProfileOptions) tx, tl = append(tx, xs.Map(t)), append(tl, volumeShort(t, xstep)) } internal.AxisX(p, plot, tx, tl, size, k) - indices := chartMemo(ch, "profile.indices", [3]any{chartID(bins), plot.Y, plot.H}, func() []int { + indices := ChartMemo(ch, "profile.indices", [3]any{ChartID(bins), plot.Y, plot.H}, func() []int { return internal.Columns(len(bins), func(i int) float32 { return ys.Map(bins[i].Low) }, func(i int) float64 { return bins[i].Volume }) }) *drawn = len(indices) @@ -206,9 +208,9 @@ func VolumeProfile(c *ui.Context, bins []ProfileBin, opts VolumeProfileOptions) }{{(bins[poc].Low + bins[poc].High) / 2, "volume_profile.poc"}, {bins[vhi].High, "volume_profile.vah"}, {bins[vlo].Low, "volume_profile.val"}} { y := internal.Snap(p, ys.Map(line.v)) p.Line(plot.X, y, plot.X+plot.W, y, 1, k.Warning) - mktTag(p, plot.X+plot.W, y, msg(c, line.key)+" "+priceUngrouped(line.v, dec), size, k.Warning, k.Surface) + mktTag(p, plot.X+plot.W, y, core.Msg(c, line.key)+" "+core.PriceUngrouped(line.v, dec), size, k.Warning, k.Surface) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/volume_profile_test.go b/chart/volume_profile_test.go similarity index 94% rename from volume_profile_test.go rename to chart/volume_profile_test.go index 5444b82..2cc3bf5 100644 --- a/volume_profile_test.go +++ b/chart/volume_profile_test.go @@ -1,10 +1,12 @@ -package mujica +package chart import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestVolumeProfileOfAndValueArea(t *testing.T) { @@ -37,7 +39,7 @@ func TestVolumeProfileKeyboard(t *testing.T) { vb := []ProfileBin{{0, 1, 5}, {1, 2, 10}, {2, 3, 40}, {3, 4, 30}, {4, 5, 15}} var res ChartResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res = VolumeProfile(c, vb, VolumeProfileOptions{Frame: ChartFrame{Label: "Profile"}}) }, 500, 360) tt.Key(0, ui.KeyTab) @@ -57,7 +59,7 @@ func TestVolumeProfileKeyboard(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) VolumeProfile(c, []ProfileBin{{0, 2, 1}, {1, 3, 1}}, VolumeProfileOptions{Frame: ChartFrame{Label: "x"}}) }, 200, 100) } diff --git a/waterfall_chart.go b/chart/waterfall_chart.go similarity index 87% rename from waterfall_chart.go rename to chart/waterfall_chart.go index 9010ecb..fb0f136 100644 --- a/waterfall_chart.go +++ b/chart/waterfall_chart.go @@ -1,12 +1,13 @@ -package mujica +package chart import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WaterfallKind says how a step's bar is drawn. @@ -49,15 +50,15 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt } lo, hi = min(lo, from[i], to[i]), max(hi, from[i], to[i]) } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) var step float64 if len(steps) > 0 { pad := (hi - lo) * 0.12 lo, hi, step = internal.Nice(lo-pad*chartBool(lo < 0), hi+pad, 4) } - val := func(v float64) string { return chartValue(c, v, opts.Format) } + val := func(v float64) string { return ChartValue(c, v, opts.Format) } signed := func(v float64) string { if v >= 0 { return "+" + val(v) @@ -65,7 +66,7 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt return "−" + val(-v) } geom := func(r ui.Rect) (ui.Rect, internal.Band, internal.Linear) { - plot := chartInset(r, 52, 26) + plot := ChartInset(r, 52, 26) return plot, internal.Band{N: len(steps), R0: plot.X, R1: plot.X + plot.W, Pad: 0.3}, internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -74,7 +75,7 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt if steps[i].Kind != WaterfallDelta { return steps[i].Label + " · " + val(to[i]) } - return steps[i].Label + " · " + signed(steps[i].Value) + " · " + msg(c, "waterfall_chart.running") + " " + val(to[i]) + return steps[i].Label + " · " + signed(steps[i].Value) + " · " + core.Msg(c, "waterfall_chart.running") + " " + val(to[i]) } spec.Pick = func(r ui.Rect, x, y float32) int { plot, band, _ := geom(r) @@ -95,7 +96,7 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt internal.Rule(p, plot.X, plot.X+plot.W, ys.Map(0), k.ControlBorder) for i, s := range steps { a, b := ys.Map(from[i]), ys.Map(to[i]) - bar := ui.Rect{X: band.Start(i), Y: min(a, b), W: band.Width(), H: max(abs32(a-b), 1)} + bar := ui.Rect{X: band.Start(i), Y: min(a, b), W: band.Width(), H: max(core.Abs32(a-b), 1)} col, shape, text := internal.Palette(k)[5], 1, val(to[i]) switch { case s.Kind != WaterfallDelta: @@ -110,7 +111,7 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt if s.Kind == WaterfallDelta && s.Value < 0 { ty, ay = bar.Y+bar.H+22, 0 } - chartText(p, text, bar.X+bar.W/2, ty, size, k.Text, 0.5, ay) + ChartText(p, text, bar.X+bar.W/2, ty, size, k.Text, 0.5, ay) if i+1 < len(steps) { y := internal.Snap(p, ys.Map(to[i])) p.Line(bar.X+bar.W, y, band.Start(i+1), y, 1, k.TextMuted) @@ -120,6 +121,6 @@ func WaterfallChart(c *ui.Context, steps []WaterfallStep, opts WaterfallChartOpt } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/waterfall_chart_test.go b/chart/waterfall_chart_test.go similarity index 94% rename from waterfall_chart_test.go rename to chart/waterfall_chart_test.go index 70c9b7d..e553e5e 100644 --- a/waterfall_chart_test.go +++ b/chart/waterfall_chart_test.go @@ -1,9 +1,11 @@ -package mujica +package chart import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var waterfallSteps = []WaterfallStep{ @@ -38,7 +40,7 @@ func TestWaterfallChartBad(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) WaterfallChart(c, []WaterfallStep{s}, WaterfallChartOptions{Frame: ChartFrame{Label: "x"}}) }, 300, 300) }() diff --git a/artifact_card.go b/chat/artifact_card.go similarity index 66% rename from artifact_card.go rename to chat/artifact_card.go index 87d84fe..802b1aa 100644 --- a/artifact_card.go +++ b/chat/artifact_card.go @@ -1,18 +1,20 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ArtifactCardOptions configure an ArtifactCard. Title is required. type ArtifactCardOptions struct { - Kind ArtifactKind + Kind agent.ArtifactKind Title string // Detail is a short fact, such as "Go, 120 lines". Detail string @@ -26,27 +28,27 @@ var artifactKinds = [3]struct{ icon, name string }{ } // ArtifactCard stands for an artifact; activating asks to open it. -func ArtifactCard(c *ui.Context, opts ArtifactCardOptions) ChoiceResult { +func ArtifactCard(c *ui.Context, opts ArtifactCardOptions) core.ChoiceResult { if opts.Title == "" { panic("mujica: ArtifactCard needs a Title") } - if opts.Kind < ArtifactCode || opts.Kind > ArtifactWeb { + if opts.Kind < agent.ArtifactCode || opts.Kind > agent.ArtifactWeb { panic("mujica: ArtifactCard Kind out of range") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) kind := artifactKinds[opts.Kind] var b *ui.Element ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { b = ui.ButtonBase(c).MinWidth(260).MaxWidth(420).MinHeight(56).Padding(d.Unit()*2, d.Unit()*3).Gap(d.Unit()*3). Justify(ui.Start).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder). - Label(msg(c, kind.name) + ": " + opts.Title) + Label(core.Msg(c, kind.name) + ": " + opts.Title) }) internal.Ring(b, k.Focus, theme.CardRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { ui.Box(c).Size(36, 36).Shrink(0).Center().Radius(theme.ControlRadius).Background(k.Selection).Children(func() { ui.Icon(c, icons.Must(kind.icon)).Size(18, 18).TextColor(k.AccentText) @@ -54,22 +56,22 @@ func ArtifactCard(c *ui.Context, opts ArtifactCardOptions) ChoiceResult { ui.Column(c).Grow(1).MinWidth(0).Gap(2).Children(func() { ui.Text(c, opts.Title).SingleLine().Bold().TextColor(k.Text) ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Children(func() { - detail := msg(c, kind.name) + detail := core.Msg(c, kind.name) if opts.Detail != "" { detail += " · " + opts.Detail } - ui.Text(c, detail).SingleLine().FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, detail).SingleLine().FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) if opts.Streaming { - Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, "artifact_card.writing")}) + core.Spinner(c, core.SpinnerOptions{Size: 12, Label: core.Msg(c, "artifact_card.writing")}) } }) }) - ui.Text(c, msg(c, "artifact_card.open")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText).Shrink(0) + ui.Text(c, core.Msg(c, "artifact_card.open")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText).Shrink(0) ui.Icon(c, icons.Must("chevron-right")).Size(14, 14).TextColor(k.AccentText) }) open := b.Clicked() && !b.IsDisabled() if open { slog.Debug("mujica: artifact_card open", "kind", opts.Kind) } - return ChoiceResult{Element: b, changed: open} + return core.NewChoiceResult(b, open) } diff --git a/artifact_card_test.go b/chat/artifact_card_test.go similarity index 61% rename from artifact_card_test.go rename to chat/artifact_card_test.go index 409109c..06989a0 100644 --- a/artifact_card_test.go +++ b/chat/artifact_card_test.go @@ -1,15 +1,18 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestArtifactCardOpens(t *testing.T) { opens := 0 tt := chatTester(480, 160, func(c *ui.Context) { - if ArtifactCard(c, ArtifactCardOptions{Kind: ArtifactDocument, Title: "Decree", Detail: "2 pages"}).Changed() { + if ArtifactCard(c, ArtifactCardOptions{Kind: agent.ArtifactDocument, Title: "Decree", Detail: "2 pages"}).Changed() { opens++ } }) @@ -27,7 +30,7 @@ func TestArtifactCardOpens(t *testing.T) { func TestArtifactCardStreamingDisabledAndChecks(t *testing.T) { opens := 0 tt := chatTester(480, 160, func(c *ui.Context) { - if ArtifactCard(c, ArtifactCardOptions{Kind: ArtifactWeb, Title: "Menu", Streaming: true, Disabled: true}).Changed() { + if ArtifactCard(c, ArtifactCardOptions{Kind: agent.ArtifactWeb, Title: "Menu", Streaming: true, Disabled: true}).Changed() { opens++ } }) @@ -45,7 +48,7 @@ func TestArtifactCardStreamingDisabledAndChecks(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ArtifactCard(c, opts) }, 100, 100, 1) }() @@ -55,9 +58,9 @@ func TestArtifactCardStreamingDisabledAndChecks(t *testing.T) { func TestArtifactCardSnapshot(t *testing.T) { snapshot(t, "artifact_card", 460, 210, func(c *ui.Context) { ui.Column(c).Padding(16).Gap(8).Children(func() { - ArtifactCard(c, ArtifactCardOptions{Kind: ArtifactCode, Title: "seating.go", Detail: "Go · 120 lines"}) - ArtifactCard(c, ArtifactCardOptions{Kind: ArtifactDocument, Title: "Royal decree", Streaming: true}) - ArtifactCard(c, ArtifactCardOptions{Kind: ArtifactWeb, Title: "Ball invitation", Disabled: true}) + ArtifactCard(c, ArtifactCardOptions{Kind: agent.ArtifactCode, Title: "seating.go", Detail: "Go · 120 lines"}) + ArtifactCard(c, ArtifactCardOptions{Kind: agent.ArtifactDocument, Title: "Royal decree", Streaming: true}) + ArtifactCard(c, ArtifactCardOptions{Kind: agent.ArtifactWeb, Title: "Ball invitation", Disabled: true}) }) }) } diff --git a/attachment_chip.go b/chat/attachment_chip.go similarity index 71% rename from attachment_chip.go rename to chat/attachment_chip.go index 87cec24..6a56c6d 100644 --- a/attachment_chip.go +++ b/chat/attachment_chip.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Attachment describes a caller-uploaded file. @@ -45,8 +46,8 @@ func AttachmentChip(c *ui.Context, a Attachment) AttachmentChipResult { if a.Uploading && !(a.Progress >= 0 && a.Progress <= 1) { panic("mujica: Attachment Progress must be from 0 to 1") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r AttachmentChipResult border := k.Border if a.Err != nil { @@ -62,16 +63,16 @@ func AttachmentChip(c *ui.Context, a Attachment) AttachmentChipResult { } ui.Icon(c, icons.Must(icon)).Size(20, 20).Shrink(0).TextColor(color) ui.Column(c).Grow(1).MinWidth(0).Gap(u / 2).Children(func() { - ui.Text(c, a.Name).SingleLine().FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600) - small := FontSize(c, theme.CaptionSize) + ui.Text(c, a.Name).SingleLine().FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600) + small := core.FontSize(c, theme.CaptionSize) switch { case a.Err != nil: - ui.Text(c, msg(c, "attachment_chip.failed")).SingleLine().FontSize(small).TextColor(k.Danger) + ui.Text(c, core.Msg(c, "attachment_chip.failed")).SingleLine().FontSize(small).TextColor(k.Danger) case a.Uploading: pct := int(math.Round(a.Progress * 100)) - ui.Text(c, fmt.Sprintf(msg(c, "attachment_chip.uploading"), pct)).SingleLine().FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf(core.Msg(c, "attachment_chip.uploading"), pct)).SingleLine().FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum") frac := float32(a.Progress) - ui.Box(c).Height(3).Radius(1.5).Background(k.Border).Role(ui.RoleProgress).Label(fmt.Sprintf(msg(c, "attachment_chip.uploading"), pct)). + ui.Box(c).Height(3).Radius(1.5).Background(k.Border).Role(ui.RoleProgress).Label(fmt.Sprintf(core.Msg(c, "attachment_chip.uploading"), pct)). Draw(func(p *ui.Painter, b ui.Rect) { p.Fill(ui.Rect{X: b.X, Y: b.Y, W: b.W * frac, H: b.H}, k.AccentText, 1.5) }) @@ -79,11 +80,11 @@ func AttachmentChip(c *ui.Context, a Attachment) AttachmentChipResult { ui.Text(c, fileMessageSize(c, a.Size)).SingleLine().FontSize(small).TextColor(k.TextMuted) } }) - if a.Err != nil && iconAction(c, icons.Must("refresh-cw"), fmt.Sprintf(msg(c, "attachment_chip.retry"), a.Name)).Clicked() && !disabled { + if a.Err != nil && core.IconAction(c, icons.Must("refresh-cw"), fmt.Sprintf(core.Msg(c, "attachment_chip.retry"), a.Name)).Clicked() && !disabled { r.retried = true slog.Debug("mujica: attachment_chip retry") } - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "attachment_chip.remove"), a.Name)).Clicked() && !disabled { + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "attachment_chip.remove"), a.Name)).Clicked() && !disabled { r.removed = true slog.Debug("mujica: attachment_chip remove") } diff --git a/attachment_chip_test.go b/chat/attachment_chip_test.go similarity index 93% rename from attachment_chip_test.go rename to chat/attachment_chip_test.go index 9835d81..2f830d1 100644 --- a/attachment_chip_test.go +++ b/chat/attachment_chip_test.go @@ -1,17 +1,19 @@ -package mujica +package chat import ( "errors" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestAttachmentChipStatesAndActions(t *testing.T) { a := Attachment{Name: "guests.csv", Size: 4096, Uploading: true, Progress: 0.4} removed, retried := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := AttachmentChip(c, a) if r.Removed() { removed++ @@ -47,7 +49,7 @@ func TestAttachmentChipBadProgress(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AttachmentChip(c, Attachment{Name: "a", Uploading: true, Progress: 2}) }, 100, 100) } @@ -64,7 +66,7 @@ func TestAttachmentChipSnapshot(t *testing.T) { func TestAttachmentChipKeyboardAndDisabled(t *testing.T) { off, removed, retried := false, 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Disabled(off).Children(func() { r := AttachmentChip(c, Attachment{Name: "guests.csv", Size: 0, Err: errors.New("upload failed")}) if r.Removed() { diff --git a/audio_message.go b/chat/audio_message.go similarity index 83% rename from audio_message.go rename to chat/audio_message.go index a97bb08..a371196 100644 --- a/audio_message.go +++ b/chat/audio_message.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AudioMessageOptions describe a voice message the caller plays. @@ -50,24 +51,24 @@ func AudioMessage(c *ui.Context, opts AudioMessageOptions) AudioMessageResult { panic("mujica: AudioMessage Waveform samples must be from 0 to 1") } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r AudioMessageResult r.Element = ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(u, u*3, u, u).Width(300). Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border) disabled := r.Element.IsDisabled() r.Element.Children(func() { - icon, name := "play", msg(c, "audio_message.play") + icon, name := "play", core.Msg(c, "audio_message.play") if opts.Playing { - icon, name = "pause", msg(c, "audio_message.pause") + icon, name = "pause", core.Msg(c, "audio_message.pause") } - if iconAction(c, icons.Must(icon), name).Clicked() && !disabled { + if core.IconAction(c, icons.Must(icon), name).Clicked() && !disabled { r.toggled = true slog.Debug("mujica: audio_message toggle", "playing", !opts.Playing) } pos := opts.Position.Seconds() s := ui.SliderBase(c, &pos, 0, opts.Duration.Seconds()).Grow(1).Height(theme.MinHitSize). - Label(msg(c, "audio_message.seek")).Description(audioClock(opts.Position) + " / " + audioClock(opts.Duration)) + Label(core.Msg(c, "audio_message.seek")).Description(audioClock(opts.Position) + " / " + audioClock(opts.Duration)) internal.Ring(s, k.Focus, theme.ControlRadius) if s.Changed() && !disabled { r.seek, r.seeked = time.Duration(pos*float64(time.Second)), true @@ -76,7 +77,7 @@ func AudioMessage(c *ui.Context, opts AudioMessageOptions) AudioMessageResult { frac := float32(opts.Position.Seconds() / opts.Duration.Seconds()) s.Draw(func(p *ui.Painter, b ui.Rect) { audioWave(p, b, opts.Waveform, frac, k) }) ui.Text(c, audioClock(opts.Position)+" / "+audioClock(opts.Duration)).FontFeatures("tnum"). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Shrink(0) + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Shrink(0) }) return r } diff --git a/audio_message_test.go b/chat/audio_message_test.go similarity index 93% rename from audio_message_test.go rename to chat/audio_message_test.go index 682e821..618b021 100644 --- a/audio_message_test.go +++ b/chat/audio_message_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var audioMessageWave = []float32{0.2, 0.5, 0.9, 0.4, 0.7, 0.3, 0.8, 0.6, 0.2, 0.5} @@ -13,7 +15,7 @@ func TestAudioMessagePlaySeekAndKeys(t *testing.T) { playing, pos := false, 10*time.Second var seeks []time.Duration tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := AudioMessage(c, AudioMessageOptions{Duration: time.Minute, Position: pos, Playing: playing, Waveform: audioMessageWave}) if r.Toggled() { playing = !playing @@ -51,7 +53,7 @@ func TestAudioMessagePlaySeekAndKeys(t *testing.T) { func TestAudioMessageDisabled(t *testing.T) { events := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { r := AudioMessage(c, AudioMessageOptions{Duration: time.Minute}) if _, ok := r.Seek(); ok || r.Toggled() { @@ -76,7 +78,7 @@ func TestAudioMessageBadOptions(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AudioMessage(c, o) }, 100, 100) }() diff --git a/branch_navigator.go b/chat/branch_navigator.go similarity index 56% rename from branch_navigator.go rename to chat/branch_navigator.go index b08f1b3..14c772d 100644 --- a/branch_navigator.go +++ b/chat/branch_navigator.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,25 +6,27 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BranchNavigator switches among count branches of a message, showing "< 2 / 3 >"; Left and Right step. -func BranchNavigator(c *ui.Context, index *int, count int) ChoiceResult { +func BranchNavigator(c *ui.Context, index *int, count int) core.ChoiceResult { if count < 1 || *index < 0 || *index >= count { panic("mujica: BranchNavigator needs 0 <= index < count") } - k := Tokens(c) - label := fmt.Sprintf(msg(c, "branch_navigator.label"), *index+1, count) - var r ChoiceResult + k := core.Tokens(c) + label := fmt.Sprintf(core.Msg(c, "branch_navigator.label"), *index+1, count) + var r core.ChoiceResult r.Element = ui.Row(c).Focusable().AlignItems(ui.Center).Radius(theme.ControlRadius).Role(ui.RoleGroup).Label(label) internal.Ring(r.Element, k.Focus, theme.ControlRadius) disabled := r.Element.IsDisabled() step := func(d int) { if i := *index + d; i >= 0 && i < count && !disabled { - *index, r.changed = i, true - c.Announce(fmt.Sprintf(msg(c, "branch_navigator.label"), i+1, count)) + *index, r = i, core.NewChoiceResult(r.Element, true) + c.Announce(fmt.Sprintf(core.Msg(c, "branch_navigator.label"), i+1, count)) slog.Debug("mujica: branch_navigator change", "index", i) } } @@ -39,12 +41,12 @@ func BranchNavigator(c *ui.Context, index *int, count int) ChoiceResult { step(count - 1 - *index) } r.Element.Children(func() { - if numberInputButton(c, "chevron-left", msg(c, "branch_navigator.previous"), *index > 0) { + if input.NumberInputButton(c, "chevron-left", core.Msg(c, "branch_navigator.previous"), *index > 0) { step(-1) } - ui.Text(c, fmt.Sprintf("%d / %d", *index+1, count)).FontFeatures("tnum").FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, fmt.Sprintf("%d / %d", *index+1, count)).FontFeatures("tnum").FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).SingleLine() - if numberInputButton(c, "chevron-right", msg(c, "branch_navigator.next"), *index < count-1) { + if input.NumberInputButton(c, "chevron-right", core.Msg(c, "branch_navigator.next"), *index < count-1) { step(1) } }) diff --git a/branch_navigator_test.go b/chat/branch_navigator_test.go similarity index 91% rename from branch_navigator_test.go rename to chat/branch_navigator_test.go index eee74eb..fa70291 100644 --- a/branch_navigator_test.go +++ b/chat/branch_navigator_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBranchNavigatorButtonsAndKeys(t *testing.T) { i, changes, off := 1, 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(off).Children(func() { if BranchNavigator(c, &i, 3).Changed() { changes++ @@ -51,7 +53,7 @@ func TestBranchNavigatorBounds(t *testing.T) { }() i := 3 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BranchNavigator(c, &i, 3) }, 100, 100) } diff --git a/capability_cards.go b/chat/capability_cards.go similarity index 88% rename from capability_cards.go rename to chat/capability_cards.go index fb977cf..584d3f9 100644 --- a/capability_cards.go +++ b/chat/capability_cards.go @@ -1,12 +1,13 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CapabilityCard explains one assistant capability with example prompts. @@ -39,8 +40,8 @@ func CapabilityCards(c *ui.Context, cards []CapabilityCard, opts CapabilityCards if cols == 0 { cols = 3 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) g := ui.Grid(c).Columns(cols).Gap(d.Unit() * 3).Disabled(opts.Disabled) v := CapabilityCardsView{Element: g} g.Children(func() { @@ -55,7 +56,7 @@ func CapabilityCards(c *ui.Context, cards []CapabilityCard, opts CapabilityCards if card.Icon != nil { ui.Icon(c, card.Icon).Size(20, 20).TextColor(k.AccentText) } - ui.Text(c, card.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, card.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) }) if card.Description != "" { ui.Text(c, card.Description).TextColor(k.TextMuted) @@ -78,7 +79,7 @@ func CapabilityCards(c *ui.Context, cards []CapabilityCard, opts CapabilityCards // capabilityExample is a quoted example button reporting a click. func capabilityExample(c *ui.Context, k theme.Tokens, text string) bool { - d := Density(c) + d := core.Density(c) b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(d.Unit(), d.Unit()*3).Justify(ui.Start). Radius(theme.ControlRadius).Background(k.Background).Border(theme.BorderWidth, k.Border).Label(text) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -88,7 +89,7 @@ func capabilityExample(c *ui.Context, k theme.Tokens, text string) bool { case b.Hovered(): b.Background(k.SurfaceHover).Border(theme.BorderWidth, k.ControlBorder) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { ui.Text(c, "“"+text+"”").TextColor(k.Text) }) return b.Clicked() && !b.IsDisabled() } diff --git a/capability_cards_test.go b/chat/capability_cards_test.go similarity index 95% rename from capability_cards_test.go rename to chat/capability_cards_test.go index a80add7..ca4d73e 100644 --- a/capability_cards_test.go +++ b/chat/capability_cards_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCapabilityCardsExample(t *testing.T) { @@ -61,7 +63,7 @@ func TestCapabilityCardsNeedTitles(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CapabilityCards(c, []CapabilityCard{{}}, CapabilityCardsOptions{}) }, 100, 100, 1) } diff --git a/chat_container.go b/chat/chat_container.go similarity index 90% rename from chat_container.go rename to chat/chat_container.go index a3ad762..06dba59 100644 --- a/chat_container.go +++ b/chat/chat_container.go @@ -1,9 +1,10 @@ -package mujica +package chat import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChatContainerOptions configure a ChatContainer. @@ -27,8 +28,8 @@ func ChatContainer(c *ui.Context, s *MessageListState, n int, opts ChatContainer if w == 0 { w = 260 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r MessageListResult shell := ui.Row(c).Fill().AlignItems(ui.Stretch).Background(k.Background) shell.Children(func() { diff --git a/chat_container_test.go b/chat/chat_container_test.go similarity index 87% rename from chat_container_test.go rename to chat/chat_container_test.go index ac9f9ca..863f183 100644 --- a/chat_container_test.go +++ b/chat/chat_container_test.go @@ -1,9 +1,12 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // chatContainerApp is a chat with a sidebar and an input. @@ -14,11 +17,11 @@ type chatContainerApp struct { } func (a *chatContainerApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ChatContainer(c, &a.state, len(a.msgs), ChatContainerOptions{ List: MessageListOptions{ID: func(i int) string { return a.msgs[i] }}, Sidebar: func() { ui.Text(c, "Sessions").Padding(12) }, - Input: func() { TextInput(c, &a.draft, TextInputOptions{Label: "Prompt"}) }, + Input: func() { input.TextInput(c, &a.draft, input.TextInputOptions{Label: "Prompt"}) }, }, func(i int) { MessageBubble(c, MessageAssistant, MessageBubbleOptions{}, func() { ui.Text(c, a.msgs[i]) }) }) diff --git a/chat/chat_empty_state.go b/chat/chat_empty_state.go new file mode 100644 index 0000000..c5c8039 --- /dev/null +++ b/chat/chat_empty_state.go @@ -0,0 +1,60 @@ +package chat + +import ( + "cmp" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" +) + +// ChatEmptyKind is why a chat is empty; zero is ChatEmptyNew. +type ChatEmptyKind int + +const ( + // ChatEmptyNew: a new conversation with no messages. + ChatEmptyNew ChatEmptyKind = iota + // ChatEmptyNoResults: a conversation search matched nothing. + ChatEmptyNoResults + // ChatEmptyOffline: the connection is down. + ChatEmptyOffline +) + +// ChatEmptyStateOptions configure a ChatEmptyState; empty texts use built-in copy. +type ChatEmptyStateOptions struct { + Kind ChatEmptyKind + // Query, for ChatEmptyNoResults, is named in the description. + Query string + Title string + Description string + // Action replaces the kind's built-in button text. + Action string +} + +// ChatEmptyState explains an empty chat and offers the next step. +func ChatEmptyState(c *ui.Context, opts ChatEmptyStateOptions) feedback.EmptyStateResult { + switch opts.Kind { + case ChatEmptyNew: + return feedback.EmptyState(c, feedback.EmptyStateOptions{ + Title: cmp.Or(opts.Title, core.Msg(c, "chat_empty_state.new.title")), + Description: cmp.Or(opts.Description, core.Msg(c, "chat_empty_state.new.description")), + Icon: icons.Must("star"), + Action: cmp.Or(opts.Action, core.Msg(c, "chat_empty_state.new.action")), + }) + case ChatEmptyNoResults: + return feedback.EmptyState(c, feedback.EmptyStateOptions{ + Kind: feedback.EmptyNoResults, Query: opts.Query, Title: opts.Title, Description: opts.Description, + Action: cmp.Or(opts.Action, core.Msg(c, "chat_empty_state.no_results.action")), + }) + case ChatEmptyOffline: + return feedback.EmptyState(c, feedback.EmptyStateOptions{ + Title: cmp.Or(opts.Title, core.Msg(c, "chat_empty_state.offline.title")), + Description: cmp.Or(opts.Description, core.Msg(c, "chat_empty_state.offline.description")), + Icon: icons.Must("wifi-off"), + Action: cmp.Or(opts.Action, core.Msg(c, "chat_empty_state.offline.action")), + }) + } + panic("mujica: ChatEmptyState Kind out of range") +} diff --git a/chat_empty_state_test.go b/chat/chat_empty_state_test.go similarity index 94% rename from chat_empty_state_test.go rename to chat/chat_empty_state_test.go index c0eaa73..7e83f01 100644 --- a/chat_empty_state_test.go +++ b/chat/chat_empty_state_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestChatEmptyStateKinds(t *testing.T) { @@ -43,7 +45,7 @@ func TestChatEmptyStateRejectsBadKind(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ChatEmptyState(c, ChatEmptyStateOptions{Kind: 9}) }, 100, 100, 1) } diff --git a/chatsession_fixture_test.go b/chat/chatsession_fixture_test.go similarity index 96% rename from chatsession_fixture_test.go rename to chat/chatsession_fixture_test.go index 4f2b199..6ba680c 100644 --- a/chatsession_fixture_test.go +++ b/chat/chatsession_fixture_test.go @@ -1,9 +1,9 @@ -package mujica +package chat import ( "time" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // chatNow is the fixed clock of the chat session tests. diff --git a/citation_badge.go b/chat/citation_badge.go similarity index 78% rename from citation_badge.go rename to chat/citation_badge.go index cab90fb..25c6e31 100644 --- a/citation_badge.go +++ b/chat/citation_badge.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Source is a document an answer cites. @@ -56,14 +58,14 @@ func (s *CitationState) expire(c *ui.Context, frame time.Time) { } // CitationBadge previews and locates a numbered source. -func CitationBadge(c *ui.Context, n int, src Source, st *CitationState) ChoiceResult { +func CitationBadge(c *ui.Context, n int, src Source, st *CitationState) core.ChoiceResult { if n < 1 || st == nil || src.Title == "" || src.URL == "" { panic("mujica: CitationBadge needs n >= 1, a state and a Source with Title and URL") } - k := Tokens(c) - label := fmt.Sprintf(msg(c, "citation_badge.label"), n, src.Title) - size := FontSize(c, theme.CaptionSize-1) - b := ui.ButtonBase(c).MinWidth(theme.MinHitSize).MinHeight(theme.MinHitSize).Padding(0, 4).Radius(theme.ControlRadius).Label(label).Transition(colorFade) + k := core.Tokens(c) + label := fmt.Sprintf(core.Msg(c, "citation_badge.label"), n, src.Title) + size := core.FontSize(c, theme.CaptionSize-1) + b := ui.ButtonBase(c).MinWidth(theme.MinHitSize).MinHeight(theme.MinHitSize).Padding(0, 4).Radius(theme.ControlRadius).Label(label).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) st.track(c, b, n) if st.lit(n) { @@ -79,9 +81,9 @@ func CitationBadge(c *ui.Context, n int, src Source, st *CitationState) ChoiceRe } }) b.Children(func() { ui.Text(c, fmt.Sprint(n)).FontSize(size).FontFeatures("tnum").FontWeight(600).SingleLine() }) - r := ChoiceResult{Element: b} + r := core.ChoiceResult{Element: b} if b.Clicked() && !b.IsDisabled() { - st.Active, r.changed = n, true + st.Active, r = n, core.NewChoiceResult(r.Element, true) st.locate++ slog.Debug("mujica: citation_badge activate", "n", n) } @@ -95,33 +97,33 @@ const citationPreviewDelay = 500 * time.Millisecond // citationPreview displays a source after resting hover or focus. // Pass-through preserves badge clicks; Escape dismisses the preview. func citationPreview(c *ui.Context, b *ui.Element, n int, src Source) { - st := ui.Local(b, "mujica.citation", func() tooltipState { return tooltipState{} }) + st := ui.Local(b, "mujica.citation", func() overlay.TooltipState { return overlay.TooltipState{} }) size := ui.Local(b, "mujica.citation.size", func() ui.Rect { return ui.Rect{} }) if b.IsDisabled() || !b.Hovered() && !b.FocusVisible() { - *st = tooltipState{} + *st = overlay.TooltipState{} return } - if st.dismissed { + if st.Dismissed { return } - if st.since.IsZero() { - st.since = c.Now() + if st.Since.IsZero() { + st.Since = c.Now() } - if wait := citationPreviewDelay - c.Now().Sub(st.since); wait > 0 { + if wait := citationPreviewDelay - c.Now().Sub(st.Since); wait > 0 { c.After(wait) return } - if overlayEscape(c, st) { - st.dismissed = true + if core.OverlayEscape(c, st) { + st.Dismissed = true return } - u := Density(c).Unit() + u := core.Density(c).Unit() ww, wh := c.Size() w, h := min(float32(300), max(0, ww-8)), max(0, wh-8) - x, y := overlayPlace(c, b.Bounds(), w, min(size.H, h), 6) + x, y := core.PlaceOverlay(c, b.Bounds(), w, min(size.H, h), 6) ui.Overlay(c, func() { - panel := ui.Column(c).Absolute().Left(x).Top(y).Width(w).MaxHeight(h).Padding(u * 3).PassThrough().Role(ui.RoleTooltip).Label(msg(c, "citation_badge.preview")) - overlayStyle(c, panel, overlayAnchored(c, panel, *size)) + panel := ui.Column(c).Absolute().Left(x).Top(y).Width(w).MaxHeight(h).Padding(u * 3).PassThrough().Role(ui.RoleTooltip).Label(core.Msg(c, "citation_badge.preview")) + core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, *size)) *size = panel.Bounds() panel.Children(func() { ui.Scroll(c).Shrink(1).Children(func() { sourceSummary(c, n, src) }) }) }) @@ -129,9 +131,9 @@ func citationPreview(c *ui.Context, b *ui.Element, n int, src Source) { // sourceSummary shows a source's number, site, title, time and snippet. func sourceSummary(c *ui.Context, n int, src Source) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) ui.Column(c).Gap(u).MinWidth(0).Grow(1).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, fmt.Sprintf("[%d]", n)).FontSize(small).FontFeatures("tnum").TextColor(k.AccentText).SingleLine() diff --git a/citation_badge_test.go b/chat/citation_badge_test.go similarity index 95% rename from citation_badge_test.go rename to chat/citation_badge_test.go index 90d83be..6aeb7bb 100644 --- a/citation_badge_test.go +++ b/chat/citation_badge_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var sourcesSample = []Source{ @@ -21,7 +23,7 @@ type citationApp struct { } func (a *citationApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Fill().Padding(16).Gap(16).AlignItems(ui.Start).Children(func() { ui.Row(c).Gap(4).Children(func() { ui.Text(c, "The court moved in 1682") @@ -82,7 +84,7 @@ func TestCitationBadgeBadNumber(t *testing.T) { }() var st CitationState ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CitationBadge(c, 0, sourcesSample[0], &st) }, 100, 100) } @@ -102,7 +104,7 @@ func TestCitationBadgeHitAreaAndDisabledPreview(t *testing.T) { var st CitationState off := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(16).Disabled(off).Children(func() { CitationBadge(c, 1, sourcesSample[0], &st) }) @@ -126,7 +128,7 @@ func TestCitationBadgePreviewFitsNarrowWindow(t *testing.T) { var st CitationState var preview ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) src := sourcesSample[0] ui.Row(c).Padding(8).Children(func() { CitationBadge(c, 1, src, &st) }) }, 180, 120) @@ -143,7 +145,7 @@ func TestCitationBadgeRemovedOwnerClearsHover(t *testing.T) { show := true st := CitationState{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { ui.Row(c).Key("badge").Height(36).Children(func() { if show { diff --git a/code_block.go b/chat/code_block.go similarity index 79% rename from code_block.go rename to chat/code_block.go index 3143923..f6cd3a2 100644 --- a/code_block.go +++ b/chat/code_block.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CopyResult is a block's element and whether its copy action ran this frame. @@ -42,8 +43,8 @@ const codeBlockCopiedFor = 2 * time.Second // CodeBlock shows code with a language label, highlighting, copy, collapse and wrap toggles. func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r CopyResult r.Element = ui.Column(c).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface).Clip().MinWidth(0) st := ui.Local(r.Element, "code", func() codeBlockState { return codeBlockState{wrap: opts.Wrap, collapsed: opts.Collapsed} }) @@ -53,28 +54,28 @@ func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult { ui.Row(c).Padding(0, u, 0, u*3).Gap(u).AlignItems(ui.Center).Background(k.SurfaceHover).Children(func() { lang := opts.Language if lang == "" { - lang = msg(c, "code_block.plain") + lang = core.Msg(c, "code_block.plain") } - ui.Text(c, lang).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Grow(1) - if codeBlockToggle(c, "wrap-text", msg(c, "code_block.wrap"), &st.wrap) { + ui.Text(c, lang).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Grow(1) + if codeBlockToggle(c, "wrap-text", core.Msg(c, "code_block.wrap"), &st.wrap) { slog.Debug("mujica: code_block wrap", "on", st.wrap) } - icon, name := "copy", msg(c, "code_block.copy") + icon, name := "copy", core.Msg(c, "code_block.copy") if left := codeBlockCopiedFor - c.Now().Sub(st.copiedAt); left > 0 { - icon, name = "check", msg(c, "code_block.copied") + icon, name = "check", core.Msg(c, "code_block.copied") c.After(left) } - if iconAction(c, icons.Must(icon), name).Clicked() && !disabled { + if core.IconAction(c, icons.Must(icon), name).Clicked() && !disabled { c.WriteClipboard(code) st.copiedAt, r.copied = c.Now(), true - c.Announce(msg(c, "code_block.copied")) + c.Announce(core.Msg(c, "code_block.copied")) slog.Debug("mujica: code_block copy", "lines", len(lines)) } - fold, foldName := "chevron-up", msg(c, "code_block.collapse") + fold, foldName := "chevron-up", core.Msg(c, "code_block.collapse") if st.collapsed { - fold, foldName = "chevron-down", fmt.Sprintf(msg(c, "code_block.expand"), len(lines)) + fold, foldName = "chevron-down", fmt.Sprintf(core.Msg(c, "code_block.expand"), len(lines)) } - if iconAction(c, icons.Must(fold), foldName).Clicked() && !disabled { + if core.IconAction(c, icons.Must(fold), foldName).Clicked() && !disabled { st.collapsed = !st.collapsed slog.Debug("mujica: code_block collapse", "on", st.collapsed) } @@ -88,7 +89,7 @@ func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult { } else { body = ui.ScrollHorizontal(c).Padding(u*2, u*3).AlignItems(ui.Start) } - body.Font(theme.MonoFont).FontSize(FontSize(c, 13)).Children(func() { + body.Font(theme.MonoFont).FontSize(core.FontSize(c, 13)).Children(func() { ui.Column(c).MinWidth(0).Grow(1).Children(func() { codeBlockLines(c, lines, opts, st.wrap) }) }) }) @@ -97,8 +98,8 @@ func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult { // codeBlockLines builds numbered, highlighted lines. func codeBlockLines(c *ui.Context, lines []string, opts CodeBlockOptions, wrap bool) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() digits := len(fmt.Sprint(len(lines))) inBlock := false for i, line := range lines { @@ -144,7 +145,7 @@ func codeBlockColor(k theme.Tokens, kind internal.TokenKind) ui.Color { // codeBlockToggle is a pressed-state icon button; on shows a Selection face and frame. func codeBlockToggle(c *ui.Context, icon, label string, on *bool) bool { - k := Tokens(c) + k := core.Tokens(c) b := ui.ToggleBase(c, on).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted) internal.Ring(b, k.Focus, theme.ControlRadius) switch { diff --git a/code_block_test.go b/chat/code_block_test.go similarity index 96% rename from code_block_test.go rename to chat/code_block_test.go index 59d1995..205ecf3 100644 --- a/code_block_test.go +++ b/chat/code_block_test.go @@ -1,16 +1,18 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const codeBlockSample = "// greet the court\nfunc greet(name string) int {\n\treturn len(\"hi \" + name) + 42\n}" func codeBlockTester(opts CodeBlockOptions, copies *int, disabled *bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(360).Disabled(*disabled).Children(func() { if CodeBlock(c, codeBlockSample, opts).Copied() { *copies++ diff --git a/context_chips.go b/chat/context_chips.go similarity index 65% rename from context_chips.go rename to chat/context_chips.go index fc9d183..b08deac 100644 --- a/context_chips.go +++ b/chat/context_chips.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ContextChipsOptions configure ContextChips. @@ -35,10 +36,10 @@ func ContextChips(c *ui.Context, items []ContextItem, opts ContextChipsOptions) if limit == 0 { limit = 4 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r RemoveResult - r.Element = ui.Row(c).Gap(u * 2).Wrap().AlignItems(ui.Center).Role(ui.RoleList).Label(msg(c, "context_chips.label")) + r.Element = ui.Row(c).Gap(u * 2).Wrap().AlignItems(ui.Center).Role(ui.RoleList).Label(core.Msg(c, "context_chips.label")) expanded := ui.Local(r.Element, "expanded", func() bool { return false }) disabled := r.Element.IsDisabled() r.Element.Children(func() { @@ -47,12 +48,12 @@ func ContextChips(c *ui.Context, items []ContextItem, opts ContextChipsOptions) shown = min(shown, limit) } for _, it := range items[:shown] { - ui.Row(c).Key(it.ID).Gap(u).AlignItems(ui.Center).Padding(0, 0, 0, u*2).MinHeight(Density(c).ControlHeight()). + ui.Row(c).Key(it.ID).Gap(u).AlignItems(ui.Center).Padding(0, 0, 0, u*2).MinHeight(core.Density(c).ControlHeight()). Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleListItem). Children(func() { ui.Icon(c, icons.Must(it.Kind.icon())).Size(14, 14).Shrink(0).TextColor(k.AccentText) - ui.Text(c, it.Label).SingleLine().MaxWidth(180).FontSize(FontSize(c, theme.CaptionSize)) - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "context_chips.remove"), it.Label)).Clicked() && !disabled { + ui.Text(c, it.Label).SingleLine().MaxWidth(180).FontSize(core.FontSize(c, theme.CaptionSize)) + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "context_chips.remove"), it.Label)).Clicked() && !disabled { r.removed = it.ID slog.Debug("mujica: context_chips remove", "id", it.ID) } @@ -61,16 +62,16 @@ func ContextChips(c *ui.Context, items []ContextItem, opts ContextChipsOptions) if len(items) <= limit { return } - label := fmt.Sprintf(msg(c, "context_chips.more"), len(items)-limit) + label := fmt.Sprintf(core.Msg(c, "context_chips.more"), len(items)-limit) if *expanded { - label = msg(c, "context_chips.less") + label = core.Msg(c, "context_chips.less") } - b := ui.ButtonBase(c).MinHeight(Density(c).ControlHeight()).Padding(0, u*2).Radius(theme.ControlRadius).Label(label).TextColor(k.AccentText) + b := ui.ButtonBase(c).MinHeight(core.Density(c).ControlHeight()).Padding(0, u*2).Radius(theme.ControlRadius).Label(label).TextColor(k.AccentText) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() }) + b.Children(func() { ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) if b.Clicked() && !disabled { *expanded = !*expanded slog.Debug("mujica: context_chips fold", "expanded", *expanded) diff --git a/context_chips_test.go b/chat/context_chips_test.go similarity index 93% rename from context_chips_test.go rename to chat/context_chips_test.go index 4542f5d..49e49bc 100644 --- a/context_chips_test.go +++ b/chat/context_chips_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestContextChipsRemoveAndFold(t *testing.T) { @@ -11,7 +13,7 @@ func TestContextChipsRemoveAndFold(t *testing.T) { items = append(items, ContextItem{ID: "w2", Label: "Hall of Mirrors", Kind: ContextWeb}) off := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(off).Children(func() { if id, ok := ContextChips(c, items, ContextChipsOptions{Max: 3}).Removed(); ok { for i, it := range items { @@ -48,7 +50,7 @@ func TestContextChipsNegativeMax(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ContextChips(c, nil, ContextChipsOptions{Max: -1}) }, 100, 100) } @@ -62,7 +64,7 @@ func TestContextChipsSnapshot(t *testing.T) { func TestContextChipsKeyboardAndEmpty(t *testing.T) { items, removed := mentionSample, "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if id, ok := ContextChips(c, items, ContextChipsOptions{Max: 1}).Removed(); ok { removed = id } diff --git a/context_mention_menu.go b/chat/context_mention_menu.go similarity index 85% rename from context_mention_menu.go rename to chat/context_mention_menu.go index 74518a7..3e54936 100644 --- a/context_mention_menu.go +++ b/chat/context_mention_menu.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -6,6 +6,9 @@ import ( "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // ContextKind is what a piece of context is. @@ -66,12 +69,12 @@ func (r ContextMentionResult) Chosen() (ContextItem, bool) { return r.chosen, r. // ContextMentionMenu groups results for the active mention query. // The caller filters results synchronously or asynchronously. -func ContextMentionMenu(c *ui.Context, composer InputResult, value *string, items []ContextItem, opts ContextMentionOptions) ContextMentionResult { +func ContextMentionMenu(c *ui.Context, composer input.InputResult, value *string, items []ContextItem, opts ContextMentionOptions) ContextMentionResult { if composer.Element == nil || composer.Input == nil || value == nil { panic("mujica: ContextMentionMenu needs composer and value") } data := ui.Local(composer.Element, "mention-data", func() mentionMenuData { return mentionMenuData{} }) - locale := current(c).locale + locale := core.CurrentLocale(c) if !data.built || locale != data.locale || !slices.Equal(data.items, items) { data.built, data.locale = true, locale data.items = append(data.items[:0], items...) @@ -83,7 +86,7 @@ func ContextMentionMenu(c *ui.Context, composer InputResult, value *string, item panic("mujica: ContextItem needs ID and Label") } if it.Kind == kind { - data.rows = append(data.rows, slashMenuRow{label: it.Label, detail: it.Detail, icon: kind.icon(), group: msg(c, kind.group())}) + data.rows = append(data.rows, slashMenuRow{label: it.Label, detail: it.Detail, icon: kind.icon(), group: core.Msg(c, kind.group())}) data.order = append(data.order, i) } } @@ -101,11 +104,11 @@ func ContextMentionMenu(c *ui.Context, composer InputResult, value *string, item } // Opening or changing queries requests caller-owned results. r.queryChanged = at >= 0 && !composer.Element.IsDisabled() && composer.Input.Focused() && (r.query != st.query || !st.open && st.dismissed == nil) - empty := msg(c, "context_mention_menu.empty") + empty := core.Msg(c, "context_mention_menu.empty") if opts.Loading { - empty = msg(c, "context_mention_menu.loading") + empty = core.Msg(c, "context_mention_menu.loading") } - i, _, panel := slashMenuShow(c, composer, st, r.query, at >= 0, rows, slashMenuOptions{empty: empty, label: msg(c, "context_mention_menu.label"), loading: opts.Loading}) + i, _, panel := slashMenuShow(c, composer, st, r.query, at >= 0, rows, slashMenuOptions{empty: empty, label: core.Msg(c, "context_mention_menu.label"), loading: opts.Loading}) r.Element = panel if i >= 0 { r.chosen, r.ok = items[order[i]], true @@ -126,7 +129,7 @@ func contextMentionAt(s string) int { type mentionMenuData struct { built bool - locale Locale + locale core.Locale items []ContextItem rows []slashMenuRow order []int diff --git a/context_mention_menu_test.go b/chat/context_mention_menu_test.go similarity index 97% rename from context_mention_menu_test.go rename to chat/context_mention_menu_test.go index 703c658..5853485 100644 --- a/context_mention_menu_test.go +++ b/chat/context_mention_menu_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var mentionSample = []ContextItem{ @@ -24,7 +26,7 @@ type contextMentionTester struct { } func (m *contextMentionTester) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Justify(ui.End).Padding(16).Children(func() { p := PromptComposer(c, &m.text, PromptComposerOptions{Label: "Prompt"}) r := ContextMentionMenu(c, p, &m.text, m.items, ContextMentionOptions{Loading: m.loading}) @@ -112,7 +114,7 @@ func TestContextMentionMenuAsyncFirstChoiceAndDisabledQuery(t *testing.T) { var items []ContextItem picked, queries := "", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt", Disabled: off}) r := ContextMentionMenu(c, p, &text, items, ContextMentionOptions{Loading: items == nil}) if r.QueryChanged() { diff --git a/context_window_meter.go b/chat/context_window_meter.go similarity index 77% rename from context_window_meter.go rename to chat/context_window_meter.go index 3e56a28..d1fa17f 100644 --- a/context_window_meter.go +++ b/chat/context_window_meter.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ContextSegment describes one part of the context window. @@ -40,17 +41,17 @@ func ContextWindowMeter(c *ui.Context, segments []ContextSegment, limit int, opt if warn == 0 { warn = 0.8 } - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) pal := internal.Palette(k) share := float64(used) / float64(limit) pct := int(math.Round(share * 100)) - summary := fmt.Sprintf(msg(c, "context_window_meter.used"), tokenThousands(used), tokenThousands(limit), pct) - col := ui.Column(c).Gap(u * 2).MinWidth(200).Role(ui.RoleMeter).Label(msg(c, "context_window_meter.label")).Description(summary) + summary := fmt.Sprintf(core.Msg(c, "context_window_meter.used"), tokenThousands(used), tokenThousands(limit), pct) + col := ui.Column(c).Gap(u * 2).MinWidth(200).Role(ui.RoleMeter).Label(core.Msg(c, "context_window_meter.label")).Description(summary) col.Children(func() { ui.Row(c).Gap(u * 2).Children(func() { - ui.Text(c, msg(c, "context_window_meter.label")).FontWeight(600).Grow(1).SingleLine() + ui.Text(c, core.Msg(c, "context_window_meter.label")).FontWeight(600).Grow(1).SingleLine() ui.Text(c, summary).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted).SingleLine() }) ui.Box(c).Height(10).Radius(theme.ControlRadius).Background(k.Border).Clip().Draw(func(p *ui.Painter, r ui.Rect) { @@ -77,7 +78,7 @@ func ContextWindowMeter(c *ui.Context, segments []ContextSegment, limit int, opt ui.Text(c, s.Label+" "+tokenThousands(s.Tokens)).FontSize(small).FontFeatures("tnum").SingleLine() }) } - ui.Text(c, msg(c, "context_window_meter.free")+" "+tokenThousands(max(limit-used, 0))).FontSize(small). + ui.Text(c, core.Msg(c, "context_window_meter.free")+" "+tokenThousands(max(limit-used, 0))).FontSize(small). FontFeatures("tnum").TextColor(k.TextMuted).SingleLine() }) if share >= warn { @@ -87,7 +88,7 @@ func ContextWindowMeter(c *ui.Context, segments []ContextSegment, limit int, opt } ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(color).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14) - ui.Text(c, fmt.Sprintf(msg(c, "context_window_meter.warn"), pct)).FontSize(small) + ui.Text(c, fmt.Sprintf(core.Msg(c, "context_window_meter.warn"), pct)).FontSize(small) }) } }) diff --git a/context_window_meter_test.go b/chat/context_window_meter_test.go similarity index 89% rename from context_window_meter_test.go rename to chat/context_window_meter_test.go index f961478..28591e9 100644 --- a/context_window_meter_test.go +++ b/chat/context_window_meter_test.go @@ -1,12 +1,13 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) var contextSegmentsSample = []ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: 58000}, {Label: "Attachments", Tokens: 24000}} @@ -14,7 +15,7 @@ var contextSegmentsSample = []ContextSegment{{Label: "System", Tokens: 1200}, {L func TestContextWindowMeterLegendAndWarning(t *testing.T) { segs := contextSegmentsSample tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ContextWindowMeter(c, segs, 128000, ContextWindowMeterOptions{}) }, 600, 200) for _, s := range []string{"System 1,200", "History 58,000", "Attachments 24,000", "Free 44,800", "83,200 of 128,000 tokens (65%)"} { @@ -49,7 +50,7 @@ func TestContextWindowMeterBadOptions(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ContextWindowMeter(c, nil, 0, ContextWindowMeterOptions{}) }, 100, 100) } diff --git a/conversation_export.go b/chat/conversation_export.go similarity index 75% rename from conversation_export.go rename to chat/conversation_export.go index 3acf073..f720d52 100644 --- a/conversation_export.go +++ b/chat/conversation_export.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "encoding/json" @@ -9,8 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ExportFormat is an export's text form; zero is ExportMarkdown. @@ -164,38 +166,38 @@ func ConversationExport(c *ui.Context, s *ConversationExportState, messages []Se if s.Last == 0 { s.Last = 10 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Gap(d.Unit() * 3).Disabled(opts.Disabled) v := ConversationExportView{Element: col, Format: s.Format} content := conversationExportContent(s, messages, opts) col.Children(func() { - FormField(c, msg(c, "conversation_export.format"), FormFieldOptions{}, func() *ui.Element { - return RadioGroup(c, &s.Format, []RadioItem[ExportFormat]{ + input.FormField(c, core.Msg(c, "conversation_export.format"), input.FormFieldOptions{}, func() *ui.Element { + return input.RadioGroup(c, &s.Format, []input.RadioItem[ExportFormat]{ {Value: ExportMarkdown, Label: "Markdown"}, {Value: ExportJSON, Label: "JSON"}, - {Value: ExportText, Label: msg(c, "conversation_export.text")}, - }, RadioGroupOptions{Horizontal: true, Label: msg(c, "conversation_export.format"), Disabled: opts.Disabled}).Element + {Value: ExportText, Label: core.Msg(c, "conversation_export.text")}, + }, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "conversation_export.format"), Disabled: opts.Disabled}).Element }) - FormField(c, msg(c, "conversation_export.range"), FormFieldOptions{}, func() *ui.Element { + input.FormField(c, core.Msg(c, "conversation_export.range"), input.FormFieldOptions{}, func() *ui.Element { return ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { - RadioGroup(c, &s.Range, []RadioItem[ExportRange]{ - {Value: ExportAll, Label: fmt.Sprintf(msg(c, "conversation_export.all"), len(messages))}, - {Value: ExportLast, Label: msg(c, "conversation_export.last")}, - {Value: ExportSelected, Label: fmt.Sprintf(msg(c, "conversation_export.selected"), len(opts.Selected)), Disabled: len(opts.Selected) == 0}, - }, RadioGroupOptions{Horizontal: true, Label: msg(c, "conversation_export.range"), Disabled: opts.Disabled}) + input.RadioGroup(c, &s.Range, []input.RadioItem[ExportRange]{ + {Value: ExportAll, Label: fmt.Sprintf(core.Msg(c, "conversation_export.all"), len(messages))}, + {Value: ExportLast, Label: core.Msg(c, "conversation_export.last")}, + {Value: ExportSelected, Label: fmt.Sprintf(core.Msg(c, "conversation_export.selected"), len(opts.Selected)), Disabled: len(opts.Selected) == 0}, + }, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "conversation_export.range"), Disabled: opts.Disabled}) if s.Range == ExportLast { - NumberInput(c, &s.Last, NumberInputOptions{Min: 1, Max: float64(max(1, len(messages))), Step: 1, - Label: msg(c, "conversation_export.last"), Disabled: opts.Disabled}).Element.Width(120) + input.NumberInput(c, &s.Last, input.NumberInputOptions{Min: 1, Max: float64(max(1, len(messages))), Step: 1, + Label: core.Msg(c, "conversation_export.last"), Disabled: opts.Disabled}).Element.Width(120) } }) }) - ui.Text(c, msg(c, "conversation_export.preview")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "conversation_export.preview")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) ui.Scroll(c).MaxHeight(220).Padding(d.Unit()*3).Background(k.Background).Border(theme.BorderWidth, k.Border). Radius(theme.ControlRadius).Children(func() { - ui.Text(c, content).Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).Selectable() + ui.Text(c, content).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).Selectable() }) ui.Row(c).Justify(ui.End).Children(func() { - v.Exported = Button(c, msg(c, "conversation_export.export"), ButtonOptions{Icon: icons.Must("upload"), Disabled: opts.Disabled}).Clicked() + v.Exported = input.Button(c, core.Msg(c, "conversation_export.export"), input.ButtonOptions{Icon: icons.Must("upload"), Disabled: opts.Disabled}).Clicked() }) }) if v.Exported && !col.IsDisabled() { diff --git a/conversation_export_test.go b/chat/conversation_export_test.go similarity index 97% rename from conversation_export_test.go rename to chat/conversation_export_test.go index 68c63aa..2af71bc 100644 --- a/conversation_export_test.go +++ b/chat/conversation_export_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "encoding/json" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestExportConversationFormats(t *testing.T) { @@ -80,7 +82,7 @@ func TestConversationExportDisabledAndChecks(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ConversationExport(c, &s, chatMessages(), ConversationExportOptions{Title: "T", Selected: []int{9}}) }, 100, 100, 1) } diff --git a/conversation_item.go b/chat/conversation_item.go similarity index 73% rename from conversation_item.go rename to chat/conversation_item.go index 2846c97..b99f194 100644 --- a/conversation_item.go +++ b/chat/conversation_item.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "cmp" @@ -7,15 +7,17 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ConversationItemOptions configure a ConversationItem. type ConversationItemOptions struct { // Today decides the time stamp; zero uses the clock. - Today Date + Today core.Date Selected bool Disabled bool // Draggable lets the row be dropped on a ProjectList. @@ -46,11 +48,11 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt if conv == nil || conv.ID == "" { panic("mujica: ConversationItem needs a conversation with an ID") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) today := opts.Today if today.IsZero() { - today = DateOf(c.Now()) + today = core.DateOf(c.Now()) } row := ui.Row(c).Key(conv.ID).FillWidth().MinHeight(d.ControlHeight()).Gap(d.Unit()).AlignItems(ui.Center). Radius(theme.ControlRadius).Disabled(opts.Disabled) @@ -62,7 +64,7 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt if opts.Selected { row.Background(k.Selection) } - title := cmp.Or(conv.Title, msg(c, "conversation_list.untitled")) + title := cmp.Or(conv.Title, core.Msg(c, "conversation_list.untitled")) rename := false row.Children(func() { if st.editing { @@ -77,16 +79,16 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt if main.Hovered() && !opts.Selected { main.Background(k.SurfaceHover) } - main.Transition(colorFade) + main.Transition(core.ColorFade) main.Children(func() { if conv.Pinned { - ui.Icon(c, icons.Must("star")).Size(14, 14).Shrink(0).TextColor(k.AccentText).Label(msg(c, "conversation_item.pinned")) + ui.Icon(c, icons.Must("star")).Size(14, 14).Shrink(0).TextColor(k.AccentText).Label(core.Msg(c, "conversation_item.pinned")) } t := ui.Text(c, title).SingleLine().Grow(1).MinWidth(0) if opts.Selected { t.TextColor(k.AccentText) } - ui.Text(c, dateSeparatorWhen(c, conv.Updated, today)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) + ui.Text(c, DateSeparatorWhen(c, conv.Updated, today)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) }) if !main.IsDisabled() { v.Opened = main.Clicked() @@ -94,16 +96,16 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt v.Deleted = main.Shortcut(0, ui.KeyDelete) } } - conversationItemMenu(c, msg(c, "conversation_item.actions"), func(m *PopupMenu) { - rename = m.Item(msg(c, "conversation_item.rename"), PopupMenuItemOptions{}) || rename - pin := msg(c, "conversation_item.pin") + conversationItemMenu(c, core.Msg(c, "conversation_item.actions"), func(m *overlay.PopupMenu) { + rename = m.Item(core.Msg(c, "conversation_item.rename"), overlay.PopupMenuItemOptions{}) || rename + pin := core.Msg(c, "conversation_item.pin") if conv.Pinned { - pin = msg(c, "conversation_item.unpin") + pin = core.Msg(c, "conversation_item.unpin") } - v.PinToggled = m.Item(pin, PopupMenuItemOptions{}) - v.Exported = m.Item(msg(c, "conversation_item.export"), PopupMenuItemOptions{}) + v.PinToggled = m.Item(pin, overlay.PopupMenuItemOptions{}) + v.Exported = m.Item(core.Msg(c, "conversation_item.export"), overlay.PopupMenuItemOptions{}) m.Separator() - v.Deleted = m.Item(msg(c, "conversation_item.delete"), PopupMenuItemOptions{}) || v.Deleted + v.Deleted = m.Item(core.Msg(c, "conversation_item.delete"), overlay.PopupMenuItemOptions{}) || v.Deleted }) }) if rename && !row.IsDisabled() { @@ -120,10 +122,10 @@ func ConversationItem(c *ui.Context, conv *ChatConversation, opts ConversationIt // conversationItemEditor edits the title in place; Escape cancels. func conversationItemEditor(c *ui.Context, k theme.Tokens, conv *ChatConversation, st *conversationItemState, v *ConversationItemView) { - d := Density(c) + d := core.Density(c) in := ui.TextInputBase(c, &st.draft).Grow(1).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*2). Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.AccentText). - Label(msg(c, "conversation_item.rename")) + Label(core.Msg(c, "conversation_item.rename")) internal.Ring(in, k.Focus, theme.ControlRadius) switch { case st.fresh: @@ -143,14 +145,14 @@ func conversationItemEditor(c *ui.Context, k theme.Tokens, conv *ChatConversatio } // conversationItemMenu is a borderless ellipsis button opening a menu. -func conversationItemMenu(c *ui.Context, label string, build func(m *PopupMenu)) *ui.Element { - b := iconAction(c, icons.Must("ellipsis"), label).Role(ui.RoleMenuButton).Shrink(0) +func conversationItemMenu(c *ui.Context, label string, build func(m *overlay.PopupMenu)) *ui.Element { + b := core.IconAction(c, icons.Must("ellipsis"), label).Role(ui.RoleMenuButton).Shrink(0) open := ui.Local(b, conversationItemKey{}, func() bool { return false }) if b.IsDisabled() { *open = false } else if b.Clicked() || b.Shortcut(0, ui.KeyDown) { *open = true } - popupMenuOpen(c, b, open, build) + overlay.PopupMenuOpen(c, b, open, build) return b } diff --git a/conversation_item_test.go b/chat/conversation_item_test.go similarity index 94% rename from conversation_item_test.go rename to chat/conversation_item_test.go index 1d358fa..c585f26 100644 --- a/conversation_item_test.go +++ b/chat/conversation_item_test.go @@ -1,14 +1,16 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func conversationItemTester(conv *ChatConversation, opts ConversationItemOptions, got *ConversationItemView) *ui.Tester { return chatTester(420, 360, func(c *ui.Context) { - opts.Today = DateOf(chatNow) + opts.Today = core.DateOf(chatNow) v := ConversationItem(c, conv, opts) if v.Opened || v.Deleted || v.Exported || v.PinToggled || v.Renamed { *got = v @@ -110,9 +112,9 @@ func TestConversationItemSnapshot(t *testing.T) { snapshot(t, "conversation_item", 420, 140, func(c *ui.Context) { convs := chatConversations() ui.Column(c).Padding(16).Gap(4).Children(func() { - ConversationItem(c, &convs[0], ConversationItemOptions{Today: DateOf(chatNow), Selected: true}) - ConversationItem(c, &convs[4], ConversationItemOptions{Today: DateOf(chatNow)}) - ConversationItem(c, &convs[3], ConversationItemOptions{Today: DateOf(chatNow), Disabled: true}) + ConversationItem(c, &convs[0], ConversationItemOptions{Today: core.DateOf(chatNow), Selected: true}) + ConversationItem(c, &convs[4], ConversationItemOptions{Today: core.DateOf(chatNow)}) + ConversationItem(c, &convs[3], ConversationItemOptions{Today: core.DateOf(chatNow), Disabled: true}) }) }) } diff --git a/conversation_list.go b/chat/conversation_list.go similarity index 86% rename from conversation_list.go rename to chat/conversation_list.go index 9700a1b..27904c1 100644 --- a/conversation_list.go +++ b/chat/conversation_list.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "cmp" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChatConversation is one saved conversation, as the caller stores it. @@ -40,7 +42,7 @@ type ConversationListState struct { // conversationListPlan holds grouped rows, rebuilt on new data or day. type conversationListPlan struct { src []ChatConversation - day Date + day core.Date rows []conversationListRow built bool } @@ -72,12 +74,12 @@ func ConversationList(c *ui.Context, s *ConversationListState, items []ChatConve if now.IsZero() { now = c.Now() } - conversationListPlanRows(&s.plan, items, DateOf(now)) + conversationListPlanRows(&s.plan, items, core.DateOf(now)) rows := s.plan.rows s.cursor = slices.IndexFunc(rows, func(r conversationListRow) bool { return r.item >= 0 && items[r.item].ID == s.Selected }) before := s.cursor - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) cursor := &s.cursor if opts.Disabled { // A disabled list moves a copy of the cursor. @@ -96,11 +98,11 @@ func ConversationList(c *ui.Context, s *ConversationListState, items []ChatConve var list *ui.Element wrap := ui.Column(c).MinHeight(0).Disabled(opts.Disabled) wrap.Children(func() { - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list = ui.List(c, &s.rows, len(rows), func(i int) { r := rows[i] if r.item < 0 { - conversationListHeader(c, k, msg(c, conversationListGroups[r.group])) + conversationListHeader(c, k, core.Msg(c, conversationListGroups[r.group])) return } conv := items[r.item] @@ -134,20 +136,20 @@ func ConversationList(c *ui.Context, s *ConversationListState, items []ChatConve func conversationListTitle(c *ui.Context, items []ChatConversation, r conversationListRow) string { if r.item < 0 { - return msg(c, conversationListGroups[r.group]) + return core.Msg(c, conversationListGroups[r.group]) } - return cmp.Or(items[r.item].Title, msg(c, "conversation_list.untitled")) + return cmp.Or(items[r.item].Title, core.Msg(c, "conversation_list.untitled")) } func conversationListHeader(c *ui.Context, k theme.Tokens, title string) { - d := Density(c) + d := core.Density(c) ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).AlignItems(ui.Center).Background(k.Surface).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).SingleLine() + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).SingleLine() }) } // conversationListPlanRows regroups items when the slice or the day changed. -func conversationListPlanRows(p *conversationListPlan, items []ChatConversation, today Date) { +func conversationListPlanRows(p *conversationListPlan, items []ChatConversation, today core.Date) { same := p.built && p.day == today && len(p.src) == len(items) && (len(items) == 0 || &p.src[0] == &items[0]) if same { return @@ -182,8 +184,8 @@ var conversationListGroups = [5]string{ } // conversationListGroup returns the recency group index of conv. -func conversationListGroup(conv ChatConversation, today Date) int { - day := DateOf(conv.Updated) +func conversationListGroup(conv ChatConversation, today core.Date) int { + day := core.DateOf(conv.Updated) switch { case conv.Pinned: return 0 diff --git a/conversation_list_test.go b/chat/conversation_list_test.go similarity index 97% rename from conversation_list_test.go rename to chat/conversation_list_test.go index 3307cd6..27c2cd5 100644 --- a/conversation_list_test.go +++ b/chat/conversation_list_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,11 +7,13 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func chatTester(w, h int, view func(c *ui.Context)) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Fill().Children(func() { view(c) }) }, w, h) } diff --git a/conversation_search.go b/chat/conversation_search.go similarity index 79% rename from conversation_search.go rename to chat/conversation_search.go index f711f30..c0f7cfa 100644 --- a/conversation_search.go +++ b/chat/conversation_search.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -6,9 +6,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ConversationSearchHit is one conversation the caller found for the query. @@ -52,15 +55,15 @@ func ConversationSearch(c *ui.Context, query *string, hits []ConversationSearchH if query == nil { panic("mujica: ConversationSearch needs a query") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Gap(d.Unit() * 2).MinHeight(0).Disabled(opts.Disabled) v := ConversationSearchView{Element: col} st := ui.Local(col, conversationSearchKey{}, func() *conversationSearchState { return &conversationSearchState{cursor: -1} }) s := *st s.cursor = min(s.cursor, len(hits)-1) col.Children(func() { - in := SearchInput(c, query, SearchInputOptions{Placeholder: opts.Placeholder, Label: opts.Label, Disabled: opts.Disabled}) + in := input.SearchInput(c, query, input.SearchInputOptions{Placeholder: opts.Placeholder, Label: opts.Label, Disabled: opts.Disabled}) v.QueryChanged = in.Changed() if in.Input.Shortcut(0, ui.KeyDown) && !in.Input.IsDisabled() && len(hits) > 0 { s.enter, s.cursor = true, max(s.cursor, 0) @@ -70,12 +73,12 @@ func ConversationSearch(c *ui.Context, query *string, hits []ConversationSearchH ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) ui.Text(c, opts.Error).Role(ui.RoleStatus).Grow(1) - v.Retry = Button(c, msg(c, "conversation_search.retry"), ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw")}).Clicked() + v.Retry = input.Button(c, core.Msg(c, "conversation_search.retry"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("refresh-cw")}).Clicked() }) case opts.Loading && len(hits) == 0: - Spinner(c, SpinnerOptions{Label: msg(c, "conversation_search.searching"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "conversation_search.searching"), ShowLabel: true}) case len(hits) == 0 && *query != "": - ui.Text(c, msg(c, "conversation_search.no_results")).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "conversation_search.no_results")).TextColor(k.TextMuted).Role(ui.RoleStatus) case len(hits) > 0: v.Chosen = conversationSearchList(c, s, *query, hits, opts.Loading) } @@ -91,15 +94,15 @@ func ConversationSearch(c *ui.Context, query *string, hits []ConversationSearchH } func conversationSearchList(c *ui.Context, s *conversationSearchState, q string, hits []ConversationSearchHit, loading bool) string { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) chosen := "" s.rows.Key = func(i int) any { return hits[i].ID } s.rows.Label = func(i int) string { return hits[i].Title } s.rows.Selected = &s.cursor // Rows share one height, so the list sizes itself early. - rowH := d.ControlHeight() + FontSize(c, theme.CaptionSize)*theme.LineHeight + 2 - restore := dataSelectTheme(c, 0) + rowH := d.ControlHeight() + core.FontSize(c, theme.CaptionSize)*theme.LineHeight + 2 + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(hits), func(i int) { h := hits[i] row := ui.Column(c).Height(rowH).Padding(d.Unit(), d.Unit()*2).Gap(2).Justify(ui.Center).ClipY() @@ -111,14 +114,14 @@ func conversationSearchList(c *ui.Context, s *conversationSearchState, q string, row.Children(func() { ui.RichText(c, conversationSearchSpans(h.Title, q, k)...).SingleLine() if h.Snippet != "" { - ui.RichText(c, conversationSearchSpans(h.Snippet, q, k)...).SingleLine().TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.RichText(c, conversationSearchSpans(h.Snippet, q, k)...).SingleLine().TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) } }) if !row.IsDisabled() && row.Clicked() { chosen = h.ID } }).Height(min(360, float32(len(hits))*rowH+2*d.Unit()+2)).Padding(d.Unit()).Background(k.Surface).Border(theme.BorderWidth, k.Border). - Radius(theme.ControlRadius).Label(msg(c, "conversation_search.results")) + Radius(theme.ControlRadius).Label(core.Msg(c, "conversation_search.results")) restore() internal.Ring(list, k.Focus, theme.ControlRadius) if s.enter { @@ -126,7 +129,7 @@ func conversationSearchList(c *ui.Context, s *conversationSearchState, q string, list.Focus() } if loading { - Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, "conversation_search.searching")}) + core.Spinner(c, core.SpinnerOptions{Size: 12, Label: core.Msg(c, "conversation_search.searching")}) } if list.Submitted() && !list.IsDisabled() && s.cursor >= 0 && s.cursor < len(hits) { chosen = hits[s.cursor].ID diff --git a/conversation_search_test.go b/chat/conversation_search_test.go similarity index 99% rename from conversation_search_test.go rename to chat/conversation_search_test.go index 2afb0f9..7f02361 100644 --- a/conversation_search_test.go +++ b/chat/conversation_search_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "slices" diff --git a/cost_estimator.go b/chat/cost_estimator.go similarity index 75% rename from cost_estimator.go rename to chat/cost_estimator.go index b70e411..c271b44 100644 --- a/cost_estimator.go +++ b/chat/cost_estimator.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CostEstimatorOptions configure a CostEstimator; prices are per million tokens. @@ -53,9 +54,9 @@ func CostEstimator(c *ui.Context, opts CostEstimatorOptions) *ui.Element { } money := func(v float64) string { return cur + strconv.FormatFloat(v, 'f', digits, 64) } est := EstimateCost(opts.InputTokens, opts.OutputTokens, opts.InputPrice, opts.OutputPrice) - k := Tokens(c) - d := Density(c) - total := fmt.Sprintf(msg(c, "cost_estimator.total_label"), money(est.Total)) + k := core.Tokens(c) + d := core.Density(c) + total := fmt.Sprintf(core.Msg(c, "cost_estimator.total_label"), money(est.Total)) g := ui.Grid(c).ColumnTracks(ui.Fr(1), ui.FitContent(), ui.FitContent()).GapX(d.Unit()*4).GapY(d.Unit()). Padding(d.Unit()*3).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). Role(ui.RoleGroup).Label(total) @@ -66,22 +67,22 @@ func CostEstimator(c *ui.Context, opts CostEstimatorOptions) *ui.Element { } } g.Children(func() { - cell(msg(c, "cost_estimator.kind"), k.TextMuted, false) - cell(msg(c, "cost_estimator.tokens"), k.TextMuted, true) - cell(msg(c, "cost_estimator.cost"), k.TextMuted, true) - cell(msg(c, "cost_estimator.input"), k.Text, false) + cell(core.Msg(c, "cost_estimator.kind"), k.TextMuted, false) + cell(core.Msg(c, "cost_estimator.tokens"), k.TextMuted, true) + cell(core.Msg(c, "cost_estimator.cost"), k.TextMuted, true) + cell(core.Msg(c, "cost_estimator.input"), k.Text, false) cell(strconv.Itoa(opts.InputTokens), k.Text, true) cell(money(est.Input), k.Text, true) - cell(msg(c, "cost_estimator.output"), k.Text, false) + cell(core.Msg(c, "cost_estimator.output"), k.Text, false) cell(strconv.Itoa(opts.OutputTokens), k.Text, true) cell(money(est.Output), k.Text, true) ui.Box(c).ColumnSpan(3).Height(theme.BorderWidth).MarginY(d.Unit()).Background(k.Ornament) - ui.Text(c, msg(c, "cost_estimator.total")).Font(theme.SerifFont).TextColor(k.Text) + ui.Text(c, core.Msg(c, "cost_estimator.total")).Font(theme.SerifFont).TextColor(k.Text) cell(strconv.Itoa(opts.InputTokens+opts.OutputTokens), k.Text, true) ui.Text(c, money(est.Total)).Bold().TextColor(k.AccentText).JustifySelf(ui.End).FontFeatures("tnum") - ui.Text(c, fmt.Sprintf(msg(c, "cost_estimator.rates"), cur+strconv.FormatFloat(opts.InputPrice, 'f', -1, 64), + ui.Text(c, fmt.Sprintf(core.Msg(c, "cost_estimator.rates"), cur+strconv.FormatFloat(opts.InputPrice, 'f', -1, 64), cur+strconv.FormatFloat(opts.OutputPrice, 'f', -1, 64))). - ColumnSpan(3).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ColumnSpan(3).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) return g } diff --git a/cost_estimator_test.go b/chat/cost_estimator_test.go similarity index 94% rename from cost_estimator_test.go rename to chat/cost_estimator_test.go index f1dfb2a..0b22f79 100644 --- a/cost_estimator_test.go +++ b/chat/cost_estimator_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCostEstimatorTable(t *testing.T) { @@ -43,7 +45,7 @@ func TestCostEstimatorRejectsBadInput(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CostEstimator(c, opts) }, 100, 100, 1) }() diff --git a/chat/date_separator.go b/chat/date_separator.go new file mode 100644 index 0000000..7f0b0fe --- /dev/null +++ b/chat/date_separator.go @@ -0,0 +1,71 @@ +package chat + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// DateSeparatorOptions configure a DateSeparator. +type DateSeparatorOptions struct { + // Today decides "Today" and "Yesterday"; zero uses the window clock. + Today core.Date +} + +// DateSeparator marks where a day's messages begin, localized. +func DateSeparator(c *ui.Context, day core.Date, opts DateSeparatorOptions) *ui.Element { + if !day.Valid() { + panic("mujica: DateSeparator needs a valid date") + } + today := opts.Today + if today.IsZero() { + today = core.DateOf(c.Now()) + } + k := core.Tokens(c) + u := core.Density(c).Unit() + label := dateSeparatorLabel(c, day, today) + row := ui.Row(c).FillWidth().Gap(u*3).AlignItems(ui.Center).Padding(u*2, 0).Role(ui.RoleText).Label(label) + line := func() { + ui.Box(c).Grow(1).Height(theme.BorderWidth).Background(k.Border) + } + row.Children(func() { + line() + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Shrink(0).Children(func() { + ui.Box(c).Size(6, 6).Draw(func(p *ui.Painter, r ui.Rect) { + p.FillPath(internal.Diamond(r.X+3, r.Y+3, 6), k.Ornament) + }) + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + }) + line() + }) + return row +} + +// dateSeparatorLabel names day relative to today, in the window's language. +func dateSeparatorLabel(c *ui.Context, day, today core.Date) string { + switch day { + case today: + return core.Msg(c, "date_separator.today") + case today.AddDays(-1): + return core.Msg(c, "date_separator.yesterday") + } + return fmt.Sprintf(core.Msg(c, "calendar.day_label"), core.Msg(c, fmt.Sprintf("calendar.weekday_long_%d", day.Weekday())), + core.Msg(c, fmt.Sprintf("calendar.month_%d", day.Month)), day.Day, day.Year) +} + +// DateSeparatorWhen is a short stamp: time, day or date. +func DateSeparatorWhen(c *ui.Context, t time.Time, today core.Date) string { + day := core.DateOf(t) + if day == today { + return t.Format("15:04") + } + if day == today.AddDays(-1) { + return core.Msg(c, "date_separator.yesterday") + } + return fmt.Sprintf(core.Msg(c, "date_separator.short"), core.Msg(c, fmt.Sprintf("calendar.month_%d", day.Month)), day.Day) +} diff --git a/chat/date_separator_test.go b/chat/date_separator_test.go new file mode 100644 index 0000000..60a01b9 --- /dev/null +++ b/chat/date_separator_test.go @@ -0,0 +1,67 @@ +package chat + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func TestDateSeparatorLabels(t *testing.T) { + today := core.DateOf(chatNow) + for _, tc := range []struct { + locale core.Locale + day core.Date + want string + }{ + {core.EnUS, today, "Today"}, + {core.EnUS, today.AddDays(-1), "Yesterday"}, + {core.EnUS, core.Date{Year: 2026, Month: 9, Day: 1}, "Tuesday, September 1, 2026"}, + {core.ZhCN, today, "今天"}, + {core.ZhCN, today.AddDays(-1), "昨天"}, + {core.ZhCN, core.Date{Year: 2026, Month: 9, Day: 1}, "2026年9月1日 星期二"}, + } { + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{Locale: tc.locale}) + DateSeparator(c, tc.day, DateSeparatorOptions{Today: today}) + }, 500, 80) + if !tt.HasText(tc.want) { + t.Errorf("%s %v: texts %q", tc.locale, tc.day, tt.Texts()) + } + } +} + +func TestDateSeparatorShortStamp(t *testing.T) { + var got []string + ui.Render(func(c *ui.Context) { + core.Use(c, core.Settings{}) + today := core.DateOf(chatNow) + got = []string{DateSeparatorWhen(c, chatNow, today), DateSeparatorWhen(c, chatNow.AddDate(0, 0, -1), today), + DateSeparatorWhen(c, chatNow.AddDate(0, -2, 0), today)} + }, 100, 100, 1) + if got[0] != "15:30" || got[1] != "Yesterday" || got[2] != "August 5" { + t.Fatalf("stamps %q", got) + } +} + +func TestDateSeparatorRejectsInvalid(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.Render(func(c *ui.Context) { + core.Use(c, core.Settings{}) + DateSeparator(c, core.Date{Year: 2026, Month: 2, Day: 30}, DateSeparatorOptions{}) + }, 100, 100, 1) +} + +func TestDateSeparatorSnapshot(t *testing.T) { + snapshot(t, "date_separator", 520, 110, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { + DateSeparator(c, core.DateOf(chatNow), DateSeparatorOptions{Today: core.DateOf(chatNow)}) + DateSeparator(c, core.Date{Year: 2026, Month: 9, Day: 1}, DateSeparatorOptions{Today: core.DateOf(chatNow)}) + }) + }) +} diff --git a/chat/doc.go b/chat/doc.go new file mode 100644 index 0000000..8d41ec1 --- /dev/null +++ b/chat/doc.go @@ -0,0 +1,2 @@ +// Package chat holds MujicaUI chat messages, the chat input and chat sessions. +package chat diff --git a/drag_drop_overlay.go b/chat/drag_drop_overlay.go similarity index 78% rename from drag_drop_overlay.go rename to chat/drag_drop_overlay.go index 1c00391..2900cc0 100644 --- a/drag_drop_overlay.go +++ b/chat/drag_drop_overlay.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DragDropOverlayOptions configure a DragDropOverlay. @@ -37,8 +38,8 @@ func DragDropOverlay(c *ui.Context, opts DragDropOverlayOptions, content func()) if content == nil { panic("mujica: DragDropOverlay needs content") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r DropResult r.Element = ui.Box(c).MinHeight(0).MinWidth(0) disabled := r.Element.IsDisabled() @@ -54,7 +55,7 @@ func DragDropOverlay(c *ui.Context, opts DragDropOverlayOptions, content func()) } hint := opts.Hint if hint == "" { - hint = msg(c, "drag_drop_overlay.hint") + hint = core.Msg(c, "drag_drop_overlay.hint") } r.Element.Children(func() { content() @@ -63,8 +64,8 @@ func DragDropOverlay(c *ui.Context, opts DragDropOverlayOptions, content func()) for i, p := range *rejected { names[i] = filepath.Base(p) } - ui.Text(c, fmt.Sprintf(msg(c, "drag_drop_overlay.rejected"), strings.Join(names, ", "))). - TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus).Padding(u, u*2) + ui.Text(c, fmt.Sprintf(core.Msg(c, "drag_drop_overlay.rejected"), strings.Join(names, ", "))). + TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus).Padding(u, u*2) } if over { dragDropMask(c, hint) @@ -75,15 +76,15 @@ func DragDropOverlay(c *ui.Context, opts DragDropOverlayOptions, content func()) // dragDropMask covers its parent with the drop hint. func dragDropMask(c *ui.Context, hint string) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() mask := ui.Column(c).Absolute().Left(0).Top(0).Right(0).Bottom(0).Center().Gap(u * 2). Background(k.Background.Alpha(0.92)).Radius(theme.CardRadius).PassThrough().Role(ui.RoleStatus).Label(hint) - mask.Opacity(overlayEnter(c, mask)) + mask.Opacity(core.OverlayEnter(c, mask)) mask.Draw(func(p *ui.Painter, b ui.Rect) { internal.Frame(p, b, u*2, 5, k.Ornament) }) mask.Children(func() { ui.Icon(c, icons.Must("upload")).Size(24, 24).TextColor(k.AccentText) - ui.Text(c, hint).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, hint).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) }) } diff --git a/drag_drop_overlay_test.go b/chat/drag_drop_overlay_test.go similarity index 88% rename from drag_drop_overlay_test.go rename to chat/drag_drop_overlay_test.go index 19fb1e7..334bb38 100644 --- a/drag_drop_overlay_test.go +++ b/chat/drag_drop_overlay_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "slices" @@ -6,11 +6,13 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestDragDropOverlayIdleShowsContentOnly(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DragDropOverlay(c, DragDropOverlayOptions{}, func() { ui.Text(c, "Chat") }) }, 300, 200) if !tt.HasText("Chat") || tt.HasText("Drop files to attach") { @@ -37,7 +39,7 @@ func TestDragDropOverlayNeedsContent(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DragDropOverlay(c, DragDropOverlayOptions{}, nil) }, 100, 100) } @@ -47,7 +49,7 @@ func TestDragDropOverlaySnapshot(t *testing.T) { snapshot(t, "drag_drop_overlay", 420, 220, func(c *ui.Context) { ui.Box(c).Fill().Padding(12).Children(func() { ui.Text(c, "A conversation sits under the mask.") - dragDropMask(c, msg(c, "drag_drop_overlay.hint")) + dragDropMask(c, core.Msg(c, "drag_drop_overlay.hint")) }) }) } diff --git a/error_message.go b/chat/error_message.go similarity index 69% rename from error_message.go rename to chat/error_message.go index 64847d3..7a8ff05 100644 --- a/error_message.go +++ b/chat/error_message.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ErrorMessageOptions configure an ErrorMessage. @@ -34,11 +36,11 @@ func ErrorMessage(c *ui.Context, text string, opts ErrorMessageOptions) ErrorMes if text == "" { panic("mujica: ErrorMessage needs the error text") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() title := opts.Title if title == "" { - title = msg(c, "error_message.title") + title = core.Msg(c, "error_message.title") } wait := opts.RetryAt.Sub(c.Now()) var r ErrorMessageResult @@ -50,21 +52,21 @@ func ErrorMessage(c *ui.Context, text string, opts ErrorMessageOptions) ErrorMes ui.Icon(c, icons.Must("circle-alert")).Size(16, 16).Shrink(0).TextColor(k.Danger) ui.Text(c, title).FontWeight(600).TextColor(k.Danger) }) - ui.Text(c, text).Selectable().Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text) + ui.Text(c, text).Selectable().Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text) ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { - label := msg(c, "error_message.retry") + label := core.Msg(c, "error_message.retry") if wait > 0 { secs := int(math.Ceil(wait.Seconds())) - label = fmt.Sprintf(msg(c, "error_message.wait"), secs) + label = fmt.Sprintf(core.Msg(c, "error_message.wait"), secs) // Wake when the shown second changes. c.After(wait - time.Duration(secs-1)*time.Second) } - if Button(c, label, ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw"), Disabled: wait > 0}).Clicked() && !disabled { + if input.Button(c, label, input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("refresh-cw"), Disabled: wait > 0}).Clicked() && !disabled { r.retried = true slog.Debug("mujica: error_message retry") } if wait > 0 { - ui.Text(c, msg(c, "error_message.limited")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "error_message.limited")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) }) diff --git a/error_message_test.go b/chat/error_message_test.go similarity index 91% rename from error_message_test.go rename to chat/error_message_test.go index 08afeac..76b8b47 100644 --- a/error_message_test.go +++ b/chat/error_message_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const errorMessageText = `429 Too Many Requests: {"error":"rate_limit","retry_after":30}` @@ -12,7 +14,7 @@ const errorMessageText = `429 Too Many Requests: {"error":"rate_limit","retry_af func TestErrorMessageKeepsTextAndRetries(t *testing.T) { retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ErrorMessage(c, errorMessageText, ErrorMessageOptions{}).Retried() { retries++ } @@ -30,7 +32,7 @@ func TestErrorMessageRateLimitCountdown(t *testing.T) { retries := 0 until := time.Now().Add(2500 * time.Millisecond) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ErrorMessage(c, errorMessageText, ErrorMessageOptions{Title: "Rate limited by the API", RetryAt: until}).Retried() { retries++ } @@ -57,7 +59,7 @@ func TestErrorMessageNeedsText(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ErrorMessage(c, "", ErrorMessageOptions{}) }, 100, 100) } diff --git a/chat/example_chatinput_test.go b/chat/example_chatinput_test.go new file mode 100644 index 0000000..1049fdd --- /dev/null +++ b/chat/example_chatinput_test.go @@ -0,0 +1,242 @@ +package chat_test + +import ( + "fmt" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func ExamplePromptComposer() { + var prompt string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{Placeholder: "Ask the herald"}) + if r.Submitted() { + fmt.Println("send", prompt) + prompt = "" + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAttachmentChip() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := chat.AttachmentChip(c, chat.Attachment{Name: "guests.csv", Size: 4096, Uploading: true, Progress: 0.4}) + if r.Removed() { + fmt.Println("cancel the upload") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePasteImagePreview() { + pic := ui.MustParseSVG([]byte(``)) + images := []chat.PastedImage{{ID: "p1", Source: pic, Name: "screenshot"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := chat.PasteImagePreview(c, images).Removed(); ok { + fmt.Println("remove", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDragDropOverlay() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + images := func(path string) bool { return strings.HasSuffix(path, ".png") } + r := chat.DragDropOverlay(c, chat.DragDropOverlayOptions{Accept: images}, func() { + ui.Text(c, "Drag images into the chat") + }) + for _, path := range r.Accepted() { + fmt.Println("attach", path) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVoiceInputButton() { + var rec chat.VoiceInputOptions + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.VoiceInputButton(c, rec).Changed() { + rec = chat.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()} + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVoiceWaveform() { + levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.VoiceWaveform(c, levels, chat.VoiceWaveformOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSlashCommandMenu() { + var prompt string + commands := []chat.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + composer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{}) + if cmd, ok := chat.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok { + fmt.Println("run", cmd.Name) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleContextMentionMenu() { + var prompt string + var found []chat.ContextItem + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + composer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{}) + r := chat.ContextMentionMenu(c, composer, &prompt, found, chat.ContextMentionOptions{}) + if r.QueryChanged() { + found = []chat.ContextItem{{ID: "f1", Label: r.Query() + ".md", Kind: chat.ContextFile}} + } + if item, ok := r.Chosen(); ok { + fmt.Println("attach", item.ID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleContextChips() { + items := []chat.ContextItem{{ID: "f1", Label: "guests.csv", Kind: chat.ContextFile}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := chat.ContextChips(c, items, chat.ContextChipsOptions{}).Removed(); ok { + fmt.Println("detach", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleModelSelector() { + model := "herald" + models := []chat.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision"}, ContextTokens: 200000}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.ModelSelector(c, &model, models, chat.ModelSelectorOptions{}).Changed() { + fmt.Println("model", model) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleModeSelector() { + mode := chat.ModeChat + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.ModeSelector(c, &mode, chat.ModeSelectorOptions{}).Changed() { + fmt.Println("mode", mode) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToolToggleMenu() { + web := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + tools := []chat.ToolItem{{ID: "web", Label: "Web search", On: &web}} + if id, ok := chat.ToolToggleMenu(c, tools).Changed(); ok { + fmt.Println("toggled", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleParameterPanel() { + temperature := 1.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + params := []chat.ModelParam{{Label: "Temperature", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}} + chat.ParameterPanel(c, params) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTokenCounter() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.TokenCounter(c, 1234, 8000) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleContextWindowMeter() { + segments := []chat.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: 58000}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.ContextWindowMeter(c, segments, 128000, chat.ContextWindowMeterOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSendButton() { + streaming := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := chat.SendButton(c, chat.SendButtonOptions{Streaming: streaming, Shortcut: "Enter"}) + if r.Sent() || r.Stopped() { + streaming = r.Sent() + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCitationBadge() { + var cites chat.CitationState + src := chat.Source{Title: "The Palace of Versailles", URL: "https://example.com/versailles"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.CitationBadge(c, 1, src, &cites).Changed() { + fmt.Println("show source", cites.Active) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSourceCard() { + var cites chat.CitationState + src := chat.Source{Title: "Hall of Mirrors", URL: "https://example.org/hall", Snippet: "Seventeen arches."} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.SourceCard(c, 1, src, &cites).Clicked() { + fmt.Println("open", src.URL) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSourcesPanel() { + var cites chat.CitationState + sources := []chat.Source{{Title: "Hall of Mirrors", URL: "https://example.org/hall"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if n, ok := chat.SourcesPanel(c, sources, &cites).Opened(); ok { + fmt.Println("open", sources[n-1].URL) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSearchProgress() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + stages := []chat.SearchStage{{Label: "Searching the web", Done: true}, {Label: "Reading sources"}} + chat.SearchProgress(c, chat.SearchProgressOptions{Query: "Versailles", Read: 3, Total: 8, Stages: stages}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/example_chatmsg_test.go b/chat/example_chatmsg_test.go similarity index 51% rename from example_chatmsg_test.go rename to chat/example_chatmsg_test.go index 63b8ed2..fe7979e 100644 --- a/example_chatmsg_test.go +++ b/chat/example_chatmsg_test.go @@ -1,4 +1,4 @@ -package mujica_test +package chat_test import ( "fmt" @@ -6,16 +6,17 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func ExampleChatContainer() { - var chat mujica.MessageListState + var thread chat.MessageListState msgs := []string{"Hello", "Bonjour"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ - List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, + core.Use(c, core.Settings{}) + chat.ChatContainer(c, &thread, len(msgs), chat.ChatContainerOptions{ + List: chat.MessageListOptions{ID: func(i int) string { return msgs[i] }}, Input: func() { ui.Text(c, "Composer goes here") }, }, func(i int) { ui.Text(c, msgs[i]) }) } @@ -23,11 +24,11 @@ func ExampleChatContainer() { } func ExampleMessageList() { - var list mujica.MessageListState + var list chat.MessageListState ids := []string{"m1", "m2", "m3"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ + core.Use(c, core.Settings{}) + r := chat.MessageList(c, &list, len(ids), chat.MessageListOptions{ ID: func(i int) string { return ids[i] }, }, func(i int) { ui.Text(c, "Message "+ids[i]) }) fmt.Println("unread", r.Unread()) @@ -37,8 +38,8 @@ func ExampleMessageList() { func ExampleMessageBubble() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { + core.Use(c, core.Settings{}) + chat.MessageBubble(c, chat.MessageAssistant, chat.MessageBubbleOptions{Name: "Herald"}, func() { ui.Text(c, "The ball begins at nine.") }) } @@ -47,8 +48,8 @@ func ExampleMessageBubble() { func ExampleMessageHeader() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent}) + core.Use(c, core.Settings{}) + chat.MessageHeader(c, chat.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: chat.MessageSent}) } ui.Render(view, 400, 300, 1) } @@ -56,16 +57,16 @@ func ExampleMessageHeader() { func ExampleStreamingText() { reply := "The decree reads" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true}) + core.Use(c, core.Settings{}) + chat.StreamingText(c, reply, chat.StreamingTextOptions{Streaming: true, Smooth: true}) } ui.Render(view, 400, 300, 1) } func ExampleMarkdownView() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { + core.Use(c, core.Settings{}) + if url, ok := chat.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { fmt.Println("open", url) } } @@ -74,16 +75,16 @@ func ExampleMarkdownView() { func ExampleCodeBlock() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true}) + core.Use(c, core.Settings{}) + chat.CodeBlock(c, "func bow() {}", chat.CodeBlockOptions{Language: "go", LineNumbers: true}) } ui.Render(view, 400, 300, 1) } func ExampleTableBlock() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { + core.Use(c, core.Settings{}) + if chat.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, chat.TableBlockOptions{}).Copied() { fmt.Println("copied as TSV") } } @@ -93,8 +94,8 @@ func ExampleTableBlock() { func ExampleImageGrid() { pic := ui.MustParseSVG([]byte(``)) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { + core.Use(c, core.Settings{}) + if i, ok := chat.ImageGrid(c, []chat.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { fmt.Println("reload", i) } } @@ -103,8 +104,8 @@ func ExampleImageGrid() { func ExampleFileMessage() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) + core.Use(c, core.Settings{}) + r := chat.FileMessage(c, chat.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) if r.Downloaded() { fmt.Println("save treaty.pdf") } @@ -115,8 +116,8 @@ func ExampleFileMessage() { func ExampleAudioMessage() { pos := 5 * time.Second view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) + core.Use(c, core.Settings{}) + r := chat.AudioMessage(c, chat.AudioMessageOptions{Duration: time.Minute, Position: pos}) if d, ok := r.Seek(); ok { pos = d } @@ -126,9 +127,9 @@ func ExampleAudioMessage() { func ExampleLinkPreviewCard() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} - if mujica.LinkPreviewCard(c, p).Clicked() { + core.Use(c, core.Settings{}) + p := chat.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} + if chat.LinkPreviewCard(c, p).Clicked() { fmt.Println("open", p.URL) } } @@ -137,8 +138,8 @@ func ExampleLinkPreviewCard() { func ExampleQuoteReply() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { + core.Use(c, core.Settings{}) + if chat.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { fmt.Println("jump to the quoted message") } } @@ -146,10 +147,10 @@ func ExampleQuoteReply() { } func ExampleMessageActions() { - rating := mujica.FeedbackNone + rating := chat.FeedbackNone view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { + core.Use(c, core.Settings{}) + if a, ok := chat.MessageActions(c, chat.MessageAssistant, chat.MessageActionsOptions{Feedback: &rating}).Action(); ok { fmt.Println("action", a) } } @@ -159,8 +160,8 @@ func ExampleMessageActions() { func ExampleMessageEditor() { draft := "Hello" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { + core.Use(c, core.Settings{}) + if chat.MessageEditor(c, &draft, chat.MessageEditorOptions{Original: "Hello"}).Saved() { fmt.Println("new branch from", draft) } } @@ -170,8 +171,8 @@ func ExampleMessageEditor() { func ExampleBranchNavigator() { branch := 1 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.BranchNavigator(c, &branch, 3).Changed() { + core.Use(c, core.Settings{}) + if chat.BranchNavigator(c, &branch, 3).Changed() { fmt.Println("show branch", branch) } } @@ -180,9 +181,9 @@ func ExampleBranchNavigator() { func ExampleErrorMessage() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} - if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { + core.Use(c, core.Settings{}) + opts := chat.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} + if chat.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { fmt.Println("send again") } } @@ -192,8 +193,8 @@ func ExampleErrorMessage() { func ExampleThinkingBlock() { open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { + core.Use(c, core.Settings{}) + chat.ThinkingBlock(c, &open, chat.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { ui.Text(c, "Weigh the guest list.") }) } @@ -201,11 +202,11 @@ func ExampleThinkingBlock() { } func ExampleScrollToBottomButton() { - var list mujica.MessageListState + var list chat.MessageListState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Fill().Children(func() { - if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + if chat.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { list.ScrollToEnd() } }) @@ -215,8 +216,8 @@ func ExampleScrollToBottomButton() { func ExampleTypingIndicator() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TypingIndicator(c, "Herald") + core.Use(c, core.Settings{}) + chat.TypingIndicator(c, "Herald") } ui.Render(view, 400, 300, 1) } diff --git a/chat/example_chatsession_test.go b/chat/example_chatsession_test.go new file mode 100644 index 0000000..37e3131 --- /dev/null +++ b/chat/example_chatsession_test.go @@ -0,0 +1,232 @@ +package chat_test + +import ( + "log" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" +) + +func ExampleConversationList() { + var state chat.ConversationListState + convs := []chat.ChatConversation{ + {ID: "1", Title: "Court etiquette", Updated: time.Now()}, + {ID: "2", Title: "Royal letters", Updated: time.Now().AddDate(0, 0, -30), Pinned: true}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.ConversationList(c, &state, convs, chat.ConversationListOptions{Label: "History"}, nil).Changed() { + log.Println("open", state.Selected) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleConversationItem() { + conv := chat.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + v := chat.ConversationItem(c, &conv, chat.ConversationItemOptions{}) + if v.Deleted { + log.Println("delete", conv.ID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleConversationSearch() { + query := "seat" + hits := []chat.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + v := chat.ConversationSearch(c, &query, hits, chat.ConversationSearchOptions{}) + if v.QueryChanged { + log.Println("search", query) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProjectList() { + var state chat.ProjectListState + projects := []chat.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} + convs := func(id string) []chat.ChatConversation { + return []chat.ChatConversation{{ID: "1", Title: "Menu"}} + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.ProjectList(c, &state, projects, convs, chat.ProjectListOptions{}); v.Moved { + log.Println("move", v.MoveID, "to", v.MoveTo) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProjectKnowledgePanel() { + files := []chat.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + v := chat.ProjectKnowledgePanel(c, files, chat.ProjectKnowledgePanelOptions{Capacity: 200000}) + if v.Add { + log.Println("pick files") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWelcomeScreen() { + cards := []chat.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.WelcomeScreen(c, chat.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { + log.Println("ask", v.Example) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSuggestionChips() { + input := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, chat.SuggestionChipsOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePromptLibrary() { + var state chat.PromptLibraryState + templates := []chat.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.PromptLibrary(c, &state, templates, chat.PromptLibraryOptions{}); v.Inserted != "" { + log.Println("insert", v.Inserted) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCapabilityCards() { + cards := []chat.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.CapabilityCards(c, cards, chat.CapabilityCardsOptions{}); v.Example != "" { + log.Println("ask", v.Example) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFeedbackForm() { + var value chat.FeedbackValue + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.FeedbackForm(c, &value, chat.FeedbackFormOptions{Rating: chat.FeedbackDown}).Submitted { + log.Println("reasons", value.Reasons) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDateSeparator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.DateSeparator(c, core.DateOf(time.Now()), chat.DateSeparatorOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleThinkingIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.ThinkingIndicator(c, chat.ThinkingIndicatorOptions{Phase: "Searching the archives"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStopGeneratingButton() { + generating := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.StopGeneratingButton(c, chat.StopGeneratingButtonOptions{Generating: generating}).Changed() { + generating = false + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSystemPromptEditor() { + prompt, saved := "You are helpful.", "You are helpful." + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.SystemPromptEditor(c, &prompt, chat.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { + saved = prompt + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCostEstimator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chat.CostEstimator(c, chat.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSharedConversationView() { + msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}, {Role: chat.SessionAssistant, Text: "Good day."}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.SharedConversationView(c, msgs, chat.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { + c.WriteClipboard("https://example.com/share/1") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleConversationExport() { + var state chat.ConversationExportState + msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.ConversationExport(c, &state, msgs, chat.ConversationExportOptions{Title: "Greeting"}); v.Exported { + log.Println(len(v.Content), "bytes to save") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRegenerateMenu() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if v := chat.RegenerateMenu(c, chat.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { + log.Println("retry", v.Choice.Kind, v.Choice.Model, v.Choice.Variant) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleArtifactCard() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.ArtifactCard(c, chat.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { + log.Println("open artifact panel") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChatEmptyState() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if chat.ChatEmptyState(c, chat.ChatEmptyStateOptions{Kind: chat.ChatEmptyOffline}).Action { + log.Println("reconnect") + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/feedback_form.go b/chat/feedback_form.go similarity index 74% rename from feedback_form.go rename to chat/feedback_form.go index 0b7e0b5..6172378 100644 --- a/feedback_form.go +++ b/chat/feedback_form.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FeedbackRating is the thumb given; zero is FeedbackUp. @@ -56,26 +58,26 @@ func FeedbackForm(c *ui.Context, value *FeedbackValue, opts FeedbackFormOptions) reasons := opts.Reasons if reasons == nil { for _, key := range feedbackReasons[opts.Rating] { - reasons = append(reasons, msg(c, key)) + reasons = append(reasons, core.Msg(c, key)) } } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) blocked := opts.Disabled || opts.Submitting col := ui.Column(c).Gap(d.Unit()*3).Padding(d.Unit()*4).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Role(ui.RoleGroup).Disabled(blocked) v := FeedbackFormView{Element: col} title := map[FeedbackRating]string{FeedbackUp: "feedback_form.up.title", FeedbackDown: "feedback_form.down.title"}[opts.Rating] - col.Label(msg(c, title)) + col.Label(core.Msg(c, title)) col.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, title)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, core.Msg(c, title)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) }) ui.Row(c).Wrap().Gap(d.Unit() * 2).FocusGroup(ui.Horizontal).Children(func() { for _, r := range reasons { on := slices.Contains(value.Reasons, r) was := on - Toggle(c, &on, r, ToggleOptions{Disabled: blocked}) + input.Toggle(c, &on, r, input.ToggleOptions{Disabled: blocked}) if on != was && !col.IsDisabled() { if on { value.Reasons = append(slices.Clone(value.Reasons), r) @@ -85,11 +87,11 @@ func FeedbackForm(c *ui.Context, value *FeedbackValue, opts FeedbackFormOptions) } } }) - TextArea(c, &value.Comment, TextAreaOptions{Placeholder: msg(c, "feedback_form.comment"), Label: msg(c, "feedback_form.comment"), + input.TextArea(c, &value.Comment, input.TextAreaOptions{Placeholder: core.Msg(c, "feedback_form.comment"), Label: core.Msg(c, "feedback_form.comment"), MinHeight: 72, Disabled: blocked}) ui.Row(c).Gap(d.Unit() * 2).Justify(ui.End).Children(func() { - v.Cancelled = Button(c, msg(c, "feedback_form.cancel"), ButtonOptions{Variant: Ghost, Disabled: blocked}).Clicked() - v.Submitted = Button(c, msg(c, "feedback_form.submit"), ButtonOptions{Icon: icons.Must("check"), Loading: opts.Submitting, Disabled: opts.Disabled}).Clicked() + v.Cancelled = input.Button(c, core.Msg(c, "feedback_form.cancel"), input.ButtonOptions{Variant: input.Ghost, Disabled: blocked}).Clicked() + v.Submitted = input.Button(c, core.Msg(c, "feedback_form.submit"), input.ButtonOptions{Icon: icons.Must("check"), Loading: opts.Submitting, Disabled: opts.Disabled}).Clicked() }) }) if !col.IsDisabled() && col.Shortcut(0, ui.KeyEscape) { diff --git a/feedback_form_test.go b/chat/feedback_form_test.go similarity index 99% rename from feedback_form_test.go rename to chat/feedback_form_test.go index 8cb86e0..537055a 100644 --- a/feedback_form_test.go +++ b/chat/feedback_form_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "slices" diff --git a/file_message.go b/chat/file_message.go similarity index 84% rename from file_message.go rename to chat/file_message.go index 451d76c..3e21cf4 100644 --- a/file_message.go +++ b/chat/file_message.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileMessageOptions describe a file sent in a chat. @@ -43,8 +44,8 @@ func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult { if opts.Size < 0 { panic("mujica: FileMessage Size must not be negative") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() size := fileMessageSize(c, opts.Size) var r FileMessageResult r.Element = ui.Row(c).Gap(u).AlignItems(ui.Center).Padding(u, u, u, u*2).MaxWidth(360). @@ -52,7 +53,7 @@ func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult { disabled := r.Element.IsDisabled() r.Element.Children(func() { open := ui.ButtonBase(c).Grow(1).MinWidth(0).Gap(u * 3).Padding(u).Justify(ui.Start).Radius(theme.ControlRadius). - Label(fmt.Sprintf(msg(c, "file_message.open"), opts.Name, size)) + Label(fmt.Sprintf(core.Msg(c, "file_message.open"), opts.Name, size)) internal.Ring(open, k.Focus, theme.ControlRadius) if open.Hovered() { open.Background(k.SurfaceHover) @@ -61,14 +62,14 @@ func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult { ui.Icon(c, icons.Must(fileMessageIcon(opts.Name, opts.Type))).Size(24, 24).Shrink(0).TextColor(k.AccentText) ui.Column(c).MinWidth(0).Grow(1).Children(func() { ui.Text(c, opts.Name).SingleLine().FontWeight(600) - ui.Text(c, size).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, size).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) }) if open.Clicked() && !disabled { r.opened = true slog.Debug("mujica: file_message open") } - if iconAction(c, icons.Must("download"), fmt.Sprintf(msg(c, "file_message.download"), opts.Name)).Clicked() && !disabled { + if core.IconAction(c, icons.Must("download"), fmt.Sprintf(core.Msg(c, "file_message.download"), opts.Name)).Clicked() && !disabled { r.downloaded = true slog.Debug("mujica: file_message download") } @@ -85,7 +86,7 @@ func fileMessageSize(c *ui.Context, n int64) string { i++ } if i == 0 { - return fmt.Sprintf(msg(c, "file_message.bytes"), n) + return fmt.Sprintf(core.Msg(c, "file_message.bytes"), n) } return fmt.Sprintf("%.1f %s", v, units[i]) } diff --git a/file_message_test.go b/chat/file_message_test.go similarity index 93% rename from file_message_test.go rename to chat/file_message_test.go index 3ec84cf..863e4ff 100644 --- a/file_message_test.go +++ b/chat/file_message_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestFileMessageOpenDownloadAndSize(t *testing.T) { opened, downloaded, off := 0, 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(off).Children(func() { r := FileMessage(c, FileMessageOptions{Name: "treaty.pdf", Size: 2_621_440}) if r.Opened() { @@ -48,7 +50,7 @@ func TestFileMessageIconsAndSizes(t *testing.T) { t.Error("MIME type ignored") } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) for _, n := range []int64{0, 1023, 1024, 5 << 30} { ui.Text(c, fileMessageSize(c, n)) } @@ -67,7 +69,7 @@ func TestFileMessageNeedsName(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileMessage(c, FileMessageOptions{}) }, 100, 100) } diff --git a/chat/helpers_test.go b/chat/helpers_test.go new file mode 100644 index 0000000..06a1eda --- /dev/null +++ b/chat/helpers_test.go @@ -0,0 +1,36 @@ +package chat + +import ( + "image" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func encode(t *testing.T, img image.Image) []byte { + t.Helper() + return snaptest.Encode(t, img) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/image_grid.go b/chat/image_grid.go similarity index 79% rename from image_grid.go rename to chat/image_grid.go index 8161285..c5a8673 100644 --- a/image_grid.go +++ b/chat/image_grid.go @@ -1,12 +1,15 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // GridImage is one picture of an ImageGrid; the caller loads it. @@ -39,8 +42,8 @@ func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult { if n < 1 || n > 9 { panic("mujica: ImageGrid shows 1 to 9 images") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() cols := 3 switch n { case 1: @@ -55,12 +58,12 @@ func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult { r := ImageGridResult{retry: -1} r.Element = ui.Grid(c).Columns(cols).Gap(u).AlignSelf(ui.Start) st := ui.Local(r.Element, "viewer", func() imageGridState { return imageGridState{} }) - var viewable []ViewerImage + var viewable []data.ViewerImage var at []int for i, im := range images { if im.Source != nil && !im.Loading && im.Err == nil { at = append(at, i) - viewable = append(viewable, ViewerImage{Source: im.Source, Alt: im.Alt}) + viewable = append(viewable, data.ViewerImage{Source: im.Source, Alt: im.Alt}) } } disabled := r.Element.IsDisabled() @@ -76,7 +79,7 @@ func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult { b = ui.Box(c) } b.Size(cell, cell).Radius(theme.ControlRadius).Clip().Children(func() { - v := Image(c, im.Source, ImageOptions{Width: cell, Height: cell, Crop: true, Alt: im.Alt, Loading: im.Loading, Err: im.Err}) + v := display.Image(c, im.Source, display.ImageOptions{Width: cell, Height: cell, Crop: true, Alt: im.Alt, Loading: im.Loading, Err: im.Err}) if v.Retry && !disabled { r.retry = i slog.Debug("mujica: image_grid retry", "index", i) @@ -97,7 +100,7 @@ func ImageGrid(c *ui.Context, images []GridImage) ImageGridResult { } if st.open && len(viewable) > 0 { st.index = min(st.index, len(viewable)-1) - ImageViewer(c, &st.open, &st.index, viewable) + data.ImageViewer(c, &st.open, &st.index, viewable) } return r } diff --git a/image_grid_test.go b/chat/image_grid_test.go similarity index 94% rename from image_grid_test.go rename to chat/image_grid_test.go index e5e65fd..4f9eaf4 100644 --- a/image_grid_test.go +++ b/chat/image_grid_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "errors" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // imageGridPicture is a plain colored picture standing in for a photo. @@ -26,7 +28,7 @@ func TestImageGridLayoutsByCount(t *testing.T) { for n, cols := range map[int]int{1: 1, 2: 2, 3: 3, 4: 2, 5: 3, 9: 3} { var xs map[float32]bool tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ImageGrid(c, imageGridSet(n)) }, 900, 900) tt.Frame() @@ -49,7 +51,7 @@ func TestImageGridOpensViewerAndRetries(t *testing.T) { imgs[1] = GridImage{Alt: "Lost", Err: errors.New("404")} retried := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if i, ok := ImageGrid(c, imgs).Retried(); ok { retried = i } @@ -78,7 +80,7 @@ func TestImageGridCountLimits(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ImageGrid(c, make([]GridImage, n)) }, 100, 100) }() diff --git a/link_preview_card.go b/chat/link_preview_card.go similarity index 75% rename from link_preview_card.go rename to chat/link_preview_card.go index 697ace7..407c0e2 100644 --- a/link_preview_card.go +++ b/chat/link_preview_card.go @@ -1,10 +1,11 @@ -package mujica +package chat import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LinkPreview is a link's metadata, fetched by the caller. @@ -21,10 +22,10 @@ func LinkPreviewCard(c *ui.Context, p LinkPreview) *ui.Element { if p.URL == "" || p.Title == "" { panic("mujica: LinkPreviewCard needs URL and Title") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).MaxWidth(420).Gap(u*3).Padding(u*2).AlignItems(ui.Start).Justify(ui.Start). - Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Label(p.Title).Tooltip(p.URL).Transition(colorFade) + Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Label(p.Title).Tooltip(p.URL).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.CardRadius) if b.Hovered() { b.Background(k.SurfaceHover) @@ -35,10 +36,10 @@ func LinkPreviewCard(c *ui.Context, p LinkPreview) *ui.Element { if site == "" { site = p.URL } - ui.Text(c, site).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, site).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() ui.Text(c, p.Title).FontWeight(600).TextColor(k.AccentText).MaxLines(2) if p.Description != "" { - ui.Text(c, p.Description).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).MaxLines(2) + ui.Text(c, p.Description).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).MaxLines(2) } }) if p.Thumbnail != nil { diff --git a/link_preview_card_test.go b/chat/link_preview_card_test.go similarity index 91% rename from link_preview_card_test.go rename to chat/link_preview_card_test.go index 527f4e9..6265356 100644 --- a/link_preview_card_test.go +++ b/chat/link_preview_card_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var linkPreviewSample = LinkPreview{ @@ -16,7 +18,7 @@ func TestLinkPreviewCardShowsMetadataAndOpens(t *testing.T) { p := linkPreviewSample p.Thumbnail = imageGridPicture("#7D2034") tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if LinkPreviewCard(c, p).Clicked() { opened++ } @@ -36,7 +38,7 @@ func TestLinkPreviewCardShowsMetadataAndOpens(t *testing.T) { func TestLinkPreviewCardSiteFallsBackToURL(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) LinkPreviewCard(c, LinkPreview{URL: "https://a.io/x", Title: "X"}) }, 400, 200) if !tt.HasText("https://a.io/x") { @@ -48,7 +50,7 @@ func TestLinkPreviewCardSiteFallsBackToURL(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) LinkPreviewCard(c, LinkPreview{URL: "https://a.io"}) }, 100, 100) } diff --git a/markdown_view.go b/chat/markdown_view.go similarity index 90% rename from markdown_view.go rename to chat/markdown_view.go index d06a6e7..db11d78 100644 --- a/markdown_view.go +++ b/chat/markdown_view.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MarkdownResult is a MarkdownView's element and the link the user opened this frame. @@ -21,7 +22,7 @@ func (r MarkdownResult) Link() (string, bool) { return r.link, r.link != "" } // MarkdownView renders a Markdown subset; a growing src parses only its last block again. func MarkdownView(c *ui.Context, src string) MarkdownResult { - u := Density(c).Unit() + u := core.Density(c).Unit() var r MarkdownResult r.Element = ui.Column(c).Gap(u * 3).MinWidth(0) doc := ui.Local(r.Element, "doc", func() *internal.Markdown { return &internal.Markdown{} }) @@ -46,12 +47,12 @@ func MarkdownView(c *ui.Context, src string) MarkdownResult { // markdownBlock builds one block and returns a link opened in it. func markdownBlock(c *ui.Context, b *internal.MdBlock) string { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() switch b.Kind { case internal.BlockHeading: size := []float32{theme.H1Size, theme.H2Size, theme.H3Size}[min(b.Level, 3)-1] - t := ui.RichText(c).Font(theme.SerifFont).FontSize(FontSize(c, size)).FontWeight(600).Role(ui.RoleText) + t := ui.RichText(c).Font(theme.SerifFont).FontSize(core.FontSize(c, size)).FontWeight(600).Role(ui.RoleText) return markdownSpans(c, t, b.Spans) case internal.BlockParagraph: return markdownSpans(c, ui.RichText(c), b.Spans) @@ -96,7 +97,7 @@ func markdownBlock(c *ui.Context, b *internal.MdBlock) string { // markdownSpans fills paragraph t with styled spans; links are focusable and return their URL when used. func markdownSpans(c *ui.Context, t *ui.Element, spans []internal.MdSpan) string { - k := Tokens(c) + k := core.Tokens(c) var link string t.MinWidth(0).Children(func() { for _, sp := range spans { diff --git a/markdown_view_test.go b/chat/markdown_view_test.go similarity index 92% rename from markdown_view_test.go rename to chat/markdown_view_test.go index 9f39b2f..b70ad53 100644 --- a/markdown_view_test.go +++ b/chat/markdown_view_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const markdownViewSample = "# Decree\n\nBy order of **the Queen**, *all* guests use `the east door`.\n\n" + @@ -14,7 +16,7 @@ const markdownViewSample = "# Decree\n\nBy order of **the Queen**, *all* guests func TestMarkdownViewRendersSubset(t *testing.T) { src := markdownViewSample tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Children(func() { MarkdownView(c, src) }) }, 600, 900) for _, s := range []string{"Decree", "the Queen", "all", "the east door", "Arrive at nine", "court dress", @@ -28,7 +30,7 @@ func TestMarkdownViewRendersSubset(t *testing.T) { func TestMarkdownViewLinkByClickAndKeyboard(t *testing.T) { var got []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if url, ok := MarkdownView(c, "See [the guide](https://x.io/guide).").Link(); ok { got = append(got, url) } @@ -45,7 +47,7 @@ func TestMarkdownViewLinkByClickAndKeyboard(t *testing.T) { func TestMarkdownViewStreamsWithoutReparsingFinishedBlocks(t *testing.T) { src := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Children(func() { MarkdownView(c, src) }) }, 600, 900) for _, chunk := range strings.SplitAfter(markdownViewSample, " ") { diff --git a/message_actions.go b/chat/message_actions.go similarity index 86% rename from message_actions.go rename to chat/message_actions.go index ba28375..de9e099 100644 --- a/message_actions.go +++ b/chat/message_actions.go @@ -1,11 +1,12 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // MessageAction is one action on a chat message. @@ -77,8 +78,8 @@ func messageActionsFor(role MessageRole, rated bool) []MessageAction { // MessageActions is a toolbar of the actions a role's message offers; arrows move between them. func MessageActions(c *ui.Context, role MessageRole, opts MessageActionsOptions) MessageActionsResult { var r MessageActionsResult - r.Element = ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).FocusGroup(ui.Horizontal). - Role(ui.RoleToolbar).Label(msg(c, "message_actions.label")) + r.Element = ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).FocusGroup(ui.Horizontal). + Role(ui.RoleToolbar).Label(core.Msg(c, "message_actions.label")) disabled := r.Element.IsDisabled() r.Element.Children(func() { for _, a := range messageActionsFor(role, opts.Feedback != nil) { @@ -90,7 +91,7 @@ func MessageActions(c *ui.Context, role MessageRole, opts MessageActionsOptions) want = FeedbackDisliked } on := *opts.Feedback == want - if codeBlockToggle(c, ic[0], msg(c, ic[1]), &on) && !disabled { + if codeBlockToggle(c, ic[0], core.Msg(c, ic[1]), &on) && !disabled { hit = true if on { *opts.Feedback = want @@ -99,7 +100,7 @@ func MessageActions(c *ui.Context, role MessageRole, opts MessageActionsOptions) } } } else { - hit = iconAction(c, icons.Must(ic[0]), msg(c, ic[1])).Tooltip(msg(c, ic[1])).Clicked() && !disabled + hit = core.IconAction(c, icons.Must(ic[0]), core.Msg(c, ic[1])).Tooltip(core.Msg(c, ic[1])).Clicked() && !disabled } if hit { r.action = a diff --git a/message_actions_test.go b/chat/message_actions_test.go similarity index 94% rename from message_actions_test.go rename to chat/message_actions_test.go index 6c41739..cf55e5b 100644 --- a/message_actions_test.go +++ b/chat/message_actions_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMessageActionsByRole(t *testing.T) { @@ -15,7 +17,7 @@ func TestMessageActionsByRole(t *testing.T) { } { fb := FeedbackNone tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MessageActions(c, role, MessageActionsOptions{Feedback: &fb}) }, 400, 100) for _, name := range want { @@ -33,7 +35,7 @@ func TestMessageActionsChooseAndRate(t *testing.T) { fb := FeedbackNone var got []MessageAction tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if a, ok := MessageActions(c, MessageAssistant, MessageActionsOptions{Feedback: &fb}).Action(); ok { got = append(got, a) } @@ -69,7 +71,7 @@ func TestMessageActionsChooseAndRate(t *testing.T) { func TestMessageActionsHideRatingAndDisable(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { if _, ok := MessageActions(c, MessageAssistant, MessageActionsOptions{}).Action(); ok { n++ diff --git a/message_bubble.go b/chat/message_bubble.go similarity index 85% rename from message_bubble.go rename to chat/message_bubble.go index a4377ae..c89aa44 100644 --- a/message_bubble.go +++ b/chat/message_bubble.go @@ -1,10 +1,11 @@ -package mujica +package chat import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MessageRole is who a chat message comes from. @@ -53,9 +54,9 @@ func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, c if maxW == 0 { maxW = 560 } - k := Tokens(c) - u := Density(c).Unit() - label := msg(c, role.key()) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := core.Msg(c, role.key()) if opts.Name != "" { label += ", " + opts.Name } @@ -69,7 +70,7 @@ func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, c row.Children(func() { if role == MessageSystem { ui.Row(c).MaxWidth(maxW).Padding(u, u*3).Gap(u * 2).Radius(theme.ControlRadius). - Background(k.SurfaceHover).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)). + Background(k.SurfaceHover).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)). Children(content) return } @@ -78,7 +79,7 @@ func MessageBubble(c *ui.Context, role MessageRole, opts MessageBubbleOptions, c } ui.Column(c).Gap(u).MaxWidth(maxW).Shrink(1).MinWidth(0).AlignItems(messageBubbleAlign(role)).Children(func() { if opts.Name != "" { - ui.Text(c, opts.Name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, opts.Name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } b := ui.Column(c).Padding(u*2, u*3).Gap(u * 2).Radius(theme.CardRadius).MinWidth(0) switch role { diff --git a/message_bubble_test.go b/chat/message_bubble_test.go similarity index 86% rename from message_bubble_test.go rename to chat/message_bubble_test.go index df5676c..81016a0 100644 --- a/message_bubble_test.go +++ b/chat/message_bubble_test.go @@ -1,20 +1,23 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" ) func TestMessageBubbleRolesAlignAndLimit(t *testing.T) { var user, bot, sys ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(600).Children(func() { MessageBubble(c, MessageUser, MessageBubbleOptions{Name: "Ada", MaxWidth: 200}, func() { user = ui.Text(c, "A long user message that must wrap inside the bubble's width limit.").Bounds() }) - MessageBubble(c, MessageAssistant, MessageBubbleOptions{Avatar: func() { Avatar(c, "Bot", AvatarOptions{}) }}, func() { + MessageBubble(c, MessageAssistant, MessageBubbleOptions{Avatar: func() { display.Avatar(c, "Bot", display.AvatarOptions{}) }}, func() { bot = ui.Text(c, "Hello").Bounds() }) MessageBubble(c, MessageSystem, MessageBubbleOptions{}, func() { sys = ui.Text(c, "Joined").Bounds() }) @@ -42,7 +45,7 @@ func TestMessageBubbleBadOptions(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MessageBubble(c, MessageUser, MessageBubbleOptions{MaxWidth: -1}, func() {}) }, 100, 100) } @@ -51,7 +54,7 @@ func TestMessageBubbleSnapshot(t *testing.T) { snapshot(t, "message_bubble", 520, 300, func(c *ui.Context) { ui.Column(c).Padding(16).Gap(12).Children(func() { MessageBubble(c, MessageUser, MessageBubbleOptions{Name: "Marie"}, func() { ui.Text(c, "Is the ball tonight?") }) - MessageBubble(c, MessageAssistant, MessageBubbleOptions{Name: "Herald", Avatar: func() { Avatar(c, "Herald", AvatarOptions{}) }}, + MessageBubble(c, MessageAssistant, MessageBubbleOptions{Name: "Herald", Avatar: func() { display.Avatar(c, "Herald", display.AvatarOptions{}) }}, func() { ui.Text(c, "Yes, at nine, in the Hall of Mirrors.") }) MessageBubble(c, MessageSystem, MessageBubbleOptions{}, func() { ui.Text(c, "Herald joined the chat") }) MessageBubble(c, MessageError, MessageBubbleOptions{}, func() { ui.Text(c, "The messenger did not return.") }) diff --git a/message_editor.go b/chat/message_editor.go similarity index 73% rename from message_editor.go rename to chat/message_editor.go index 06439b5..5828f84 100644 --- a/message_editor.go +++ b/chat/message_editor.go @@ -1,10 +1,13 @@ -package mujica +package chat import ( "log/slog" "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // MessageEditorOptions configure a MessageEditor. @@ -30,19 +33,19 @@ func MessageEditor(c *ui.Context, draft *string, opts MessageEditorOptions) Mess if draft == nil { panic("mujica: MessageEditor needs a draft") } - u := Density(c).Unit() + u := core.Density(c).Unit() var r MessageEditorResult r.Element = ui.Column(c).Gap(u * 2).MinWidth(0) disabled := r.Element.IsDisabled() r.Element.Children(func() { - area := TextArea(c, draft, TextAreaOptions{Label: msg(c, "message_editor.label"), MaxHeight: 240}) + area := input.TextArea(c, draft, input.TextAreaOptions{Label: core.Msg(c, "message_editor.label"), MaxHeight: 240}) ok := strings.TrimSpace(*draft) != "" && *draft != opts.Original escape := area.Input.Shortcut(0, ui.KeyEscape) ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { - if Button(c, msg(c, "message_editor.cancel"), ButtonOptions{Variant: Secondary}).Clicked() || escape { + if input.Button(c, core.Msg(c, "message_editor.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() || escape { r.canceled = !disabled } - if Button(c, msg(c, "message_editor.save"), ButtonOptions{Disabled: !ok}).Clicked() || area.Submitted() && ok { + if input.Button(c, core.Msg(c, "message_editor.save"), input.ButtonOptions{Disabled: !ok}).Clicked() || area.Submitted() && ok { r.saved = !disabled } }) diff --git a/message_editor_test.go b/chat/message_editor_test.go similarity index 94% rename from message_editor_test.go rename to chat/message_editor_test.go index 1a75412..0f0f5bc 100644 --- a/message_editor_test.go +++ b/chat/message_editor_test.go @@ -1,14 +1,16 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func messageEditorTester(draft *string, saves, cancels *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MessageEditor(c, draft, MessageEditorOptions{Original: "Hello"}) if r.Saved() { *saves++ diff --git a/message_header.go b/chat/message_header.go similarity index 76% rename from message_header.go rename to chat/message_header.go index f45adde..5981a40 100644 --- a/message_header.go +++ b/chat/message_header.go @@ -1,12 +1,13 @@ -package mujica +package chat import ( "time" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MessageStatus is where a chat message is in its delivery. @@ -30,16 +31,16 @@ type MessageHeaderOptions struct { // MessageHeader shows a message's model, time and status as icon and text. func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) row := ui.Row(c).Gap(u * 2).AlignItems(ui.Center).FontSize(small).TextColor(k.TextMuted) row.Children(func() { if opts.Model != "" { ui.Text(c, opts.Model).FontWeight(600).TextColor(k.Text).SingleLine() } if !opts.Time.IsZero() { - ui.Text(c, opts.Time.Format(msg(c, "message_header.time"))).FontFeatures("tnum").SingleLine() + ui.Text(c, opts.Time.Format(core.Msg(c, "message_header.time"))).FontFeatures("tnum").SingleLine() } if opts.Status == MessageNoStatus { return @@ -59,11 +60,11 @@ func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element { } ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(color).Children(func() { if icon == "" { - Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, key)}) + core.Spinner(c, core.SpinnerOptions{Size: 12, Label: core.Msg(c, key)}) } else { ui.Icon(c, icons.Must(icon)).Size(12, 12) } - ui.Text(c, msg(c, key)).SingleLine() + ui.Text(c, core.Msg(c, key)).SingleLine() }) }) return row diff --git a/message_header_test.go b/chat/message_header_test.go similarity index 89% rename from message_header_test.go rename to chat/message_header_test.go index 61f95b6..bb4f538 100644 --- a/message_header_test.go +++ b/chat/message_header_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMessageHeaderFieldsAndStatus(t *testing.T) { @@ -13,7 +15,7 @@ func TestMessageHeaderFieldsAndStatus(t *testing.T) { MessageSending: "Sending", MessageStreaming: "Generating", MessageSent: "Sent", MessageFailed: "Failed", } { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: EnUS}) + core.Use(c, core.Settings{Locale: core.EnUS}) MessageHeader(c, MessageHeaderOptions{Model: "claude", Time: at, Status: status}) }, 400, 100) for _, s := range []string{"claude", "14:05", want} { @@ -23,7 +25,7 @@ func TestMessageHeaderFieldsAndStatus(t *testing.T) { } } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MessageHeader(c, MessageHeaderOptions{}) }, 400, 100) if len(tt.Texts()) != 0 { @@ -38,7 +40,7 @@ func TestMessageHeaderUnknownStatus(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MessageHeader(c, MessageHeaderOptions{Status: 99}) }, 100, 100) } diff --git a/message_list.go b/chat/message_list.go similarity index 85% rename from message_list.go rename to chat/message_list.go index fc5bf1c..f0aa9c7 100644 --- a/message_list.go +++ b/chat/message_list.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MessageListState keeps a MessageList's place and what the user has seen. @@ -49,7 +50,7 @@ func MessageList(c *ui.Context, s *MessageListState, n int, opts MessageListOpti if n < 0 { panic("mujica: MessageList with negative message count") } - u := Density(c).Unit() + u := core.Density(c).Unit() s.list.Key = func(i int) any { return opts.ID(i) } s.list.Label = opts.Label s.list.FollowEnd = true @@ -68,7 +69,7 @@ func MessageList(c *ui.Context, s *MessageListState, n int, opts MessageListOpti } row(i) }) - }).Fill().Justify(ui.End).Label(msg(c, "message_list.label")) + }).Fill().Justify(ui.End).Label(core.Msg(c, "message_list.label")) jump := ScrollToBottomButton(c, !at, r.unread) if jump.Clicked() && !r.Element.IsDisabled() { s.list.ScrollToEnd() @@ -87,22 +88,22 @@ func messageListSameDay(a, b time.Time) bool { // messageListDay is a separator naming the day: today, yesterday or the date. func messageListDay(c *ui.Context, t time.Time) { - k := Tokens(c) - u := Density(c).Unit() - label := t.Format(msg(c, "message_list.date")) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := t.Format(core.Msg(c, "message_list.date")) now := c.Now().In(t.Location()) switch { case messageListSameDay(t, now): - label = msg(c, "message_list.today") + label = core.Msg(c, "message_list.today") case messageListSameDay(t, now.AddDate(0, 0, -1)): - label = msg(c, "message_list.yesterday") + label = core.Msg(c, "message_list.yesterday") } ui.Row(c).Gap(u*3).AlignItems(ui.Center).Padding(u*2, 0).Role(ui.RoleText).Label(label).Children(func() { rule := func() { ui.Box(c).Grow(1).Height(theme.BorderWidth).Background(k.Border) } rule() - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() rule() }) } diff --git a/message_list_test.go b/chat/message_list_test.go similarity index 96% rename from message_list_test.go rename to chat/message_list_test.go index 5aaf0a4..cb4d897 100644 --- a/message_list_test.go +++ b/chat/message_list_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // messageListApp is a chat of n messages with stable IDs. @@ -25,7 +27,7 @@ func newMessageListApp(n int) *messageListApp { } func (a *messageListApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) a.built = 0 day := time.Date(2026, 3, 1, 9, 0, 0, 0, time.UTC) a.res = MessageList(c, &a.state, len(a.ids), MessageListOptions{ @@ -115,7 +117,7 @@ func TestMessageListNeedsID(t *testing.T) { }() var s MessageListState ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MessageList(c, &s, 1, MessageListOptions{}, func(int) {}) }, 200, 100) } diff --git a/mode_selector.go b/chat/mode_selector.go similarity index 80% rename from mode_selector.go rename to chat/mode_selector.go index a3ad842..d37e03a 100644 --- a/mode_selector.go +++ b/chat/mode_selector.go @@ -1,13 +1,14 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChatMode is how the assistant answers. @@ -36,21 +37,21 @@ type ModeSelectorOptions struct { } // ModeSelector switches conversation modes through clicks or arrow keys. -func ModeSelector(c *ui.Context, mode *ChatMode, opts ModeSelectorOptions) ChoiceResult { +func ModeSelector(c *ui.Context, mode *ChatMode, opts ModeSelectorOptions) core.ChoiceResult { if mode == nil || *mode < ModeChat || *mode > ModeAgent { panic("mujica: unknown ChatMode") } - k, u := Tokens(c), Density(c).Unit() + k, u := core.Tokens(c), core.Density(c).Unit() old, pick := *mode, int(*mode) wrapper := ui.Row(c).Shrink(0).Disabled(opts.Disabled) var track *ui.Element wrapper.Children(func() { parts := ui.SegmentedBase(c, &pick, len(chatModes)) track = parts.Track.Padding(2).Gap(2).Radius(theme.ControlRadius+2).Background(k.Background). - Border(theme.BorderWidth, k.ControlBorder).MinHeight(Density(c).ControlHeight()).Label(msg(c, "mode_selector.label")) + Border(theme.BorderWidth, k.ControlBorder).MinHeight(core.Density(c).ControlHeight()).Label(core.Msg(c, "mode_selector.label")) track.Children(func() { for i, m := range chatModes { - label := msg(c, m[0]) + label := core.Msg(c, m[0]) seg := parts.Segment(i).MinWidth(theme.MinHitSize).Padding(0, u*3).Gap(u * 1.5).Radius(theme.ControlRadius).Label(label) on := i == pick fg := k.TextMuted @@ -67,7 +68,7 @@ func ModeSelector(c *ui.Context, mode *ChatMode, opts ModeSelectorOptions) Choic seg.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(seg, Motion(c, theme.StateDuration), fg) + internal.Ease(seg, core.Motion(c, theme.StateDuration), fg) internal.Ring(seg, k.Focus, theme.ControlRadius) if opts.IconsOnly { seg.Tooltip(label) @@ -86,5 +87,5 @@ func ModeSelector(c *ui.Context, mode *ChatMode, opts ModeSelectorOptions) Choic *mode = ChatMode(pick) slog.Debug("mujica: mode_selector changed", "mode", pick) } - return ChoiceResult{Element: track, changed: changed} + return core.NewChoiceResult(track, changed) } diff --git a/mode_selector_test.go b/chat/mode_selector_test.go similarity index 84% rename from mode_selector_test.go rename to chat/mode_selector_test.go index 35c3542..0c7f09d 100644 --- a/mode_selector_test.go +++ b/chat/mode_selector_test.go @@ -1,16 +1,18 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestModeSelectorClickAndArrows(t *testing.T) { mode, n, off := ModeChat, 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ModeSelector(c, &mode, ModeSelectorOptions{Disabled: off}).Changed() { n++ } @@ -34,7 +36,7 @@ func TestModeSelectorClickAndArrows(t *testing.T) { func TestModeSelectorIconsOnlyAndBadMode(t *testing.T) { mode := ModeSearch tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ModeSelector(c, &mode, ModeSelectorOptions{IconsOnly: true}) }, 300, 100) if _, ok := tt.Find("Search"); !ok { @@ -47,7 +49,7 @@ func TestModeSelectorIconsOnlyAndBadMode(t *testing.T) { }() bad := ChatMode(9) ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ModeSelector(c, &bad, ModeSelectorOptions{}) }, 100, 100) } @@ -64,7 +66,7 @@ func TestModeSelectorSnapshot(t *testing.T) { func TestModeSelectorSelectionSurface(t *testing.T) { mode := ModeResearch - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ModeSelector(c, &mode, ModeSelectorOptions{}) }, 600, 100) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); ModeSelector(c, &mode, ModeSelectorOptions{}) }, 600, 100) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) b, _ := tt.Find("Deep research") pixel := tt.Image().RGBAAt(int(b.X+4), int(b.Y+4)) diff --git a/model_selector.go b/chat/model_selector.go similarity index 84% rename from model_selector.go rename to chat/model_selector.go index 284af56..9796d1e 100644 --- a/model_selector.go +++ b/chat/model_selector.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ModelOption is one model a ModelSelector offers. @@ -31,7 +34,7 @@ type ModelSelectorOptions struct { } // ModelSelector chooses models with capabilities and availability details. -func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts ModelSelectorOptions) ChoiceResult { +func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts ModelSelectorOptions) core.ChoiceResult { if value == nil { panic("mujica: ModelSelector needs value") } @@ -40,7 +43,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode panic("mujica: ModelOption needs ID, Name and non-negative ContextTokens") } } - k, u := Tokens(c), Density(c).Unit() + k, u := core.Tokens(c), core.Density(c).Unit() var tr *ui.Element ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { tr = ui.ButtonBase(c).Role(ui.RolePopUpButton) }) st := ui.Local(tr, "model-selector", func() modelSelectorState { return modelSelectorState{hi: -1, lastHi: -1} }) @@ -50,7 +53,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode } label := opts.Label if label == "" { - label = msg(c, "model_selector.label") + label = core.Msg(c, "model_selector.label") } tr.Label(label) old := *value @@ -108,17 +111,17 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode st.hi = modelSelectorIndex(models, *value) } cur, has := modelSelectorFind(models, *value) - selectField(c, tr, st.open, func() { + input.SelectField(c, tr, st.open, func() { if has { ui.Text(c, cur.Name).SingleLine().FontWeight(600).Grow(1).Shrink(1) } else { - ui.Text(c, msg(c, "model_selector.placeholder")).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) + ui.Text(c, core.Msg(c, "model_selector.placeholder")).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) } }) if st.open { ww, wh := c.Size() ui.PopoverBase(c, tr, &st.open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, tr.Bounds())) + core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, tr.Bounds())) a := tr.Bounds() room := max(a.Y, wh-a.Y-a.H) - 8 panel.Margin(4, 0, 0, 0).Padding(4).Width(min(float32(360), max(0, ww-8))).Height(min(float32(360), max(0, room))).Label(label) @@ -129,7 +132,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode ui.List(c, &st.list, len(models), func(i int) { m := models[i] hi, chosen := i == st.hi, m.ID == *value - row := ui.Row(c).MinHeight(Density(c).ControlHeight()).Disabled(m.Unavailable != "").Description(m.Unavailable). + row := ui.Row(c).MinHeight(core.Density(c).ControlHeight()).Disabled(m.Unavailable != "").Description(m.Unavailable). Padding(u*2, u*3, u*2, u*2).Gap(u * 2).AlignItems(ui.Start).Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(m.Name).Cursor(ui.CursorPointer).Selected(chosen) if chosen { row.Background(k.Selection).TextColor(k.Text) @@ -139,7 +142,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode if hi { row.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + 4, W: 2, H: r.H - 8}, k.AccentText, 1) }) } - row.Children(func() { selectCheck(c, chosen); modelSelectorRow(c, m) }) + row.Children(func() { input.SelectCheck(c, chosen); modelSelectorRow(c, m) }) if row.Clicked() && !row.IsDisabled() { choose(i) } @@ -150,7 +153,7 @@ func ModelSelector(c *ui.Context, value *string, models []ModelOption, opts Mode if changed { slog.Debug("mujica: model_selector changed", "model", *value) } - return ChoiceResult{Element: tr, changed: changed} + return core.NewChoiceResult(tr, changed) } type modelSelectorState struct { @@ -177,14 +180,14 @@ func modelSelectorIndex(models []ModelOption, id string) int { // modelSelectorRow shows a model's name, tags, context and unavailability. func modelSelectorRow(c *ui.Context, m ModelOption) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) ui.Column(c).Gap(u).Grow(1).MinWidth(0).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, m.Name).FontWeight(600).SingleLine() if m.ContextTokens > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "model_selector.context"), modelSelectorTokens(m.ContextTokens))). + ui.Text(c, fmt.Sprintf(core.Msg(c, "model_selector.context"), modelSelectorTokens(m.ContextTokens))). FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() } }) diff --git a/model_selector_test.go b/chat/model_selector_test.go similarity index 92% rename from model_selector_test.go rename to chat/model_selector_test.go index e898356..bbc4afa 100644 --- a/model_selector_test.go +++ b/chat/model_selector_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var modelsSample = []ModelOption{ @@ -15,7 +17,7 @@ var modelsSample = []ModelOption{ func modelTester(v *string, changes *int, off *bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if ModelSelector(c, v, modelsSample, ModelSelectorOptions{Disabled: *off}).Changed() { *changes++ @@ -75,7 +77,7 @@ func TestModelSelectorSnapshot(t *testing.T) { func TestModelSelectorAncestorDisabledCloses(t *testing.T) { v, off, changes := "herald", false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(off).Children(func() { if ModelSelector(c, &v, modelsSample, ModelSelectorOptions{}).Changed() { changes++ @@ -98,7 +100,10 @@ func TestModelSelectorLargeDataAndEnd(t *testing.T) { models[i] = ModelOption{ID: fmt.Sprint(i), Name: fmt.Sprintf("Model %d", i)} } v := "0" - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ModelSelector(c, &v, models, ModelSelectorOptions{}) }, 400, 300) + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + ModelSelector(c, &v, models, ModelSelectorOptions{}) + }, 400, 300) _ = tt.Click("Model") if n := len(tt.Texts()); n > 200 { t.Errorf("built %d labels, want <= 200", n) @@ -117,7 +122,7 @@ func TestModelSelectorPopupFitsWindow(t *testing.T) { models[i] = ModelOption{ID: fmt.Sprint(i), Name: fmt.Sprintf("Model %d", i)} } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { ModelSelector(c, &value, models, ModelSelectorOptions{}) }) }, 440, 330) _ = tt.Click("Model") diff --git a/parameter_panel.go b/chat/parameter_panel.go similarity index 60% rename from parameter_panel.go rename to chat/parameter_panel.go index 187c297..59ea7f8 100644 --- a/parameter_panel.go +++ b/chat/parameter_panel.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ModelParam is one generation parameter, as temperature or top-p. @@ -22,7 +24,7 @@ type ModelParam struct { } // ParameterPanel displays bounded generation sliders and reset actions. -func ParameterPanel(c *ui.Context, params []ModelParam) ChoiceResult { +func ParameterPanel(c *ui.Context, params []ModelParam) core.ChoiceResult { for _, p := range params { if p.Label == "" || p.Value == nil || !(p.Min < p.Max) || !(p.Step > 0) || !(p.Default >= p.Min && p.Default <= p.Max) { panic("mujica: ModelParam needs Label, Value, Min < Max, Step > 0 and Default in range") @@ -35,55 +37,55 @@ func ParameterPanel(c *ui.Context, params []ModelParam) ChoiceResult { if *p.Value < p.Min || *p.Value > p.Max { panic("mujica: ModelParam value must be in range") } - spec := numberInputResolve(NumberInputOptions{Min: p.Min, Max: p.Max, Step: p.Step, Precision: sliderDecimals(p.Min, p.Step)}) - if spec.stepBy(p.Default, 0) != p.Default { + spec := input.NumberInputResolve(input.NumberInputOptions{Min: p.Min, Max: p.Max, Step: p.Step, Precision: input.SliderDecimals(p.Min, p.Step)}) + if spec.StepBy(p.Default, 0) != p.Default { panic("mujica: ModelParam default must lie on the step grid") } - if spec.stepBy(*p.Value, 0) != *p.Value { + if spec.StepBy(*p.Value, 0) != *p.Value { panic("mujica: ModelParam value must lie on the step grid") } } - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) - var r ChoiceResult - r.Element = ui.Column(c).Gap(u * 4).MinWidth(240).Role(ui.RoleGroup).Label(msg(c, "parameter_panel.label")) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) + var r core.ChoiceResult + r.Element = ui.Column(c).Gap(u * 4).MinWidth(240).Role(ui.RoleGroup).Label(core.Msg(c, "parameter_panel.label")) disabled := r.Element.IsDisabled() set := func(p ModelParam, v float64) { if *p.Value != v && !disabled { - *p.Value, r.changed = v, true + *p.Value, r = v, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: parameter_panel set", "param", p.Label, "value", v) } } r.Element.Children(func() { for _, p := range params { format := func(v float64) string { - return strconv.FormatFloat(v, 'f', sliderDecimals(p.Min, p.Step, p.Default), 64) + return strconv.FormatFloat(v, 'f', input.SliderDecimals(p.Min, p.Step, p.Default), 64) } ui.Column(c).Key(p.Label).Gap(u).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, p.Label).FontWeight(600).Grow(1).SingleLine() ui.Text(c, format(*p.Value)).FontFeatures("tnum").TextColor(k.AccentText).SingleLine() ui.Row(c).Disabled(*p.Value == p.Default).Children(func() { - if iconAction(c, icons.Must("rotate-ccw"), fmt.Sprintf(msg(c, "parameter_panel.reset"), p.Label)). - Tooltip(fmt.Sprintf(msg(c, "parameter_panel.default"), format(p.Default))).Clicked() { + if core.IconAction(c, icons.Must("rotate-ccw"), fmt.Sprintf(core.Msg(c, "parameter_panel.reset"), p.Label)). + Tooltip(fmt.Sprintf(core.Msg(c, "parameter_panel.default"), format(p.Default))).Clicked() { set(p, p.Default) } }) }) v := *p.Value - if Slider(c, &v, SliderOptions{Min: p.Min, Max: p.Max, Step: p.Step, Label: p.Label, Disabled: disabled}).Changed() { + if input.Slider(c, &v, input.SliderOptions{Min: p.Min, Max: p.Max, Step: p.Step, Label: p.Label, Disabled: disabled}).Changed() { set(p, v) } ui.Row(c).Children(func() { ui.Text(c, format(p.Min)).FontSize(small).TextColor(k.TextMuted).Grow(1) - ui.Text(c, fmt.Sprintf(msg(c, "parameter_panel.default"), format(p.Default))).FontSize(small).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "parameter_panel.default"), format(p.Default))).FontSize(small).TextColor(k.TextMuted) ui.Text(c, format(p.Max)).FontSize(small).TextColor(k.TextMuted).Grow(1).TextAlign(ui.End) }) }) } ui.Row(c).Justify(ui.End).Children(func() { - if Button(c, msg(c, "parameter_panel.reset_all"), ButtonOptions{Variant: Ghost, Icon: icons.Must("rotate-ccw")}).Clicked() { + if input.Button(c, core.Msg(c, "parameter_panel.reset_all"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("rotate-ccw")}).Clicked() { for _, p := range params { set(p, p.Default) } diff --git a/parameter_panel_test.go b/chat/parameter_panel_test.go similarity index 92% rename from parameter_panel_test.go rename to chat/parameter_panel_test.go index 71ba177..478f42a 100644 --- a/parameter_panel_test.go +++ b/chat/parameter_panel_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func paramSample(temp, topP *float64) []ModelParam { @@ -18,7 +20,7 @@ func paramSample(temp, topP *float64) []ModelParam { func TestParameterPanelAdjustAndReset(t *testing.T) { temp, topP, n, off := 1.0, 0.9, 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Disabled(off).Children(func() { if ParameterPanel(c, paramSample(&temp, &topP)).Changed() { n++ @@ -58,7 +60,7 @@ func TestParameterPanelBadParam(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ParameterPanel(c, []ModelParam{{Label: "x", Value: &v, Min: 0, Max: 1, Step: 0.1, Default: 3}}) }, 100, 100) } @@ -79,7 +81,7 @@ func TestParameterPanelRejectsNonFiniteAndOutOfRange(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ParameterPanel(c, []ModelParam{{Label: "Temperature", Value: &v, Min: 0, Max: 2, Step: 0.1, Default: 1}}) }, 400, 300) }) @@ -94,7 +96,7 @@ func TestParameterPanelRequiresDefaultOnStepGrid(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ParameterPanel(c, []ModelParam{{Label: "Temperature", Value: &v, Min: 0, Max: 2, Step: 0.1, Default: 0.95}}) }, 400, 300) } @@ -107,7 +109,7 @@ func TestParameterPanelRequiresValueOnStepGrid(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ParameterPanel(c, []ModelParam{{Label: "Temperature", Value: &v, Min: 0, Max: 1, Step: 0.2, Default: 0.4}}) }, 400, 300) } diff --git a/paste_image_preview.go b/chat/paste_image_preview.go similarity index 81% rename from paste_image_preview.go rename to chat/paste_image_preview.go index 5b10791..cbb373a 100644 --- a/paste_image_preview.go +++ b/chat/paste_image_preview.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PastedImage contains a caller-decoded clipboard or dropped image. @@ -24,10 +25,10 @@ const pasteImageSize float32 = 72 // PasteImagePreview displays removable image thumbnails. func PasteImagePreview(c *ui.Context, images []PastedImage) RemoveResult { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r RemoveResult - r.Element = ui.Row(c).Gap(u * 2).Wrap().Role(ui.RoleList).Label(msg(c, "paste_image_preview.label")) + r.Element = ui.Row(c).Gap(u * 2).Wrap().Role(ui.RoleList).Label(core.Msg(c, "paste_image_preview.label")) disabled := r.Element.IsDisabled() r.Element.Children(func() { for _, im := range images { @@ -37,7 +38,7 @@ func PasteImagePreview(c *ui.Context, images []PastedImage) RemoveResult { ui.Box(c).Key(im.ID).Size(pasteImageSize, pasteImageSize).Role(ui.RoleListItem).Children(func() { ui.Image(c, im.Source).Fit(ui.Cover).Size(pasteImageSize, pasteImageSize).Radius(theme.ControlRadius). Border(theme.BorderWidth, k.Border).Label(im.Name) - x := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "paste_image_preview.remove"), im.Name)). + x := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "paste_image_preview.remove"), im.Name)). Absolute().Top(-u * 3).Right(-u * 3).Radius(theme.MinHitSize / 2) // A 20 DIP disc inside the 28 DIP hit area. x.Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/paste_image_preview_test.go b/chat/paste_image_preview_test.go similarity index 92% rename from paste_image_preview_test.go rename to chat/paste_image_preview_test.go index 98a8087..eeab905 100644 --- a/paste_image_preview_test.go +++ b/chat/paste_image_preview_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func pasteImages() []PastedImage { @@ -16,7 +18,7 @@ func pasteImages() []PastedImage { func TestPasteImagePreviewRemoves(t *testing.T) { imgs, off := pasteImages(), false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Disabled(off).Children(func() { if id, ok := PasteImagePreview(c, imgs).Removed(); ok { for i, im := range imgs { @@ -49,7 +51,7 @@ func TestPasteImagePreviewNeedsSource(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PasteImagePreview(c, []PastedImage{{ID: "x"}}) }, 100, 100) } @@ -63,7 +65,7 @@ func TestPasteImagePreviewSnapshot(t *testing.T) { func TestPasteImagePreviewKeyboard(t *testing.T) { removed := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if id, ok := PasteImagePreview(c, pasteImages()).Removed(); ok { removed = id diff --git a/project_knowledge_panel.go b/chat/project_knowledge_panel.go similarity index 67% rename from project_knowledge_panel.go rename to chat/project_knowledge_panel.go index 4a2cce1..b873cc8 100644 --- a/project_knowledge_panel.go +++ b/chat/project_knowledge_panel.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // KnowledgeFile is a project knowledge file, sized in tokens. @@ -47,8 +49,8 @@ func ProjectKnowledgePanel(c *ui.Context, files []KnowledgeFile, opts ProjectKno } used += f.Tokens } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Gap(d.Unit()*2).Padding(d.Unit()*3).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Disabled(opts.Disabled) v := ProjectKnowledgePanelView{Element: col} @@ -56,17 +58,17 @@ func ProjectKnowledgePanel(c *ui.Context, files []KnowledgeFile, opts ProjectKno ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { title := opts.Title if title == "" { - title = msg(c, "project_knowledge_panel.title") + title = core.Msg(c, "project_knowledge_panel.title") } - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1) - v.Add = Button(c, msg(c, "project_knowledge_panel.add"), ButtonOptions{Variant: Secondary, Icon: icons.Must("plus"), Disabled: opts.Disabled}).Clicked() + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1) + v.Add = input.Button(c, core.Msg(c, "project_knowledge_panel.add"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("plus"), Disabled: opts.Disabled}).Clicked() }) ui.Box(c).FillWidth().Height(9).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 6, 5, k.Ornament) }) projectKnowledgeMeter(c, k, used, opts.Capacity) if len(files) == 0 { - ui.Text(c, msg(c, "project_knowledge_panel.empty")).TextColor(k.TextMuted).Padding(d.Unit()*2, 0) + ui.Text(c, core.Msg(c, "project_knowledge_panel.empty")).TextColor(k.TextMuted).Padding(d.Unit()*2, 0) return } ui.Column(c).Gap(2).FocusGroup(ui.Vertical).Children(func() { @@ -74,9 +76,9 @@ func ProjectKnowledgePanel(c *ui.Context, files []KnowledgeFile, opts ProjectKno ui.Row(c).Key(f.ID).MinHeight(d.ControlHeight()).Gap(d.Unit()*2).AlignItems(ui.Center).Padding(0, d.Unit()).Children(func() { ui.Icon(c, icons.Must("file-text")).Size(16, 16).TextColor(k.TextMuted) ui.Text(c, f.Name).SingleLine().Grow(1).MinWidth(0) - ui.Text(c, fmt.Sprintf(msg(c, "project_knowledge_panel.tokens"), f.Tokens)). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) - x := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "project_knowledge_panel.remove"), f.Name)) + ui.Text(c, fmt.Sprintf(core.Msg(c, "project_knowledge_panel.tokens"), f.Tokens)). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + x := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "project_knowledge_panel.remove"), f.Name)) if x.Clicked() && !x.IsDisabled() { v.Removed = f.ID } @@ -92,17 +94,17 @@ func ProjectKnowledgePanel(c *ui.Context, files []KnowledgeFile, opts ProjectKno // projectKnowledgeMeter shows usage; limits get words and icons. func projectKnowledgeMeter(c *ui.Context, k theme.Tokens, used, capacity int) { - d := Density(c) + d := core.Density(c) frac := float64(used) / float64(capacity) ink, note, icon := k.AccentText, "", "" switch { case used > capacity: - ink, note, icon = k.Danger, msg(c, "project_knowledge_panel.over"), "circle-alert" + ink, note, icon = k.Danger, core.Msg(c, "project_knowledge_panel.over"), "circle-alert" case frac >= 0.8: - ink, note, icon = k.Warning, msg(c, "project_knowledge_panel.near"), "triangle-alert" + ink, note, icon = k.Warning, core.Msg(c, "project_knowledge_panel.near"), "triangle-alert" } pct := int(frac * 100) - label := fmt.Sprintf(msg(c, "project_knowledge_panel.usage"), used, capacity, pct) + label := fmt.Sprintf(core.Msg(c, "project_knowledge_panel.usage"), used, capacity, pct) ui.Column(c).Gap(d.Unit()).Role(ui.RoleMeter).Label(label).Children(func() { ui.Box(c).FillWidth().Height(6).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.Border, 3) @@ -112,10 +114,10 @@ func projectKnowledgeMeter(c *ui.Context, k theme.Tokens, used, capacity int) { if icon != "" { ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(ink) } - t := ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + t := ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) if note != "" { t.TextColor(ink) - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).TextColor(ink).Role(ui.RoleStatus) + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(ink).Role(ui.RoleStatus) } }) }) diff --git a/project_knowledge_panel_test.go b/chat/project_knowledge_panel_test.go similarity index 97% rename from project_knowledge_panel_test.go rename to chat/project_knowledge_panel_test.go index 1b6ac0f..7949f82 100644 --- a/project_knowledge_panel_test.go +++ b/chat/project_knowledge_panel_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func knowledgeFiles() []KnowledgeFile { @@ -96,7 +98,7 @@ func TestProjectKnowledgePanelRejectsBadCapacity(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ProjectKnowledgePanel(c, nil, ProjectKnowledgePanelOptions{}) }, 100, 100, 1) } diff --git a/project_list.go b/chat/project_list.go similarity index 83% rename from project_list.go rename to chat/project_list.go index 6c73018..ba38c93 100644 --- a/project_list.go +++ b/chat/project_list.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "cmp" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChatProject holds conversations; Folder groups projects, empty for none. @@ -51,7 +53,7 @@ func ProjectList(c *ui.Context, s *ProjectListState, projects []ChatProject, con if s.closed == nil { s.closed, s.open = map[string]bool{}, map[string]bool{} } - d := Density(c) + d := core.Density(c) col := ui.Column(c).Gap(2).FocusGroup(ui.Vertical).Role(ui.RoleTree).Label(opts.Label).Disabled(opts.Disabled) v := ProjectListView{Element: col} col.Children(func() { @@ -93,8 +95,8 @@ func ProjectList(c *ui.Context, s *ProjectListState, projects []ChatProject, con // projectListFolder is a folder heading on a CollapsibleBase trigger. func projectListFolder(c *ui.Context, title string, open *bool) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) parts := ui.CollapsibleBase(c, open) tr := parts.Trigger.MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center). Radius(theme.ControlRadius).Label(title) @@ -115,8 +117,8 @@ func projectListFolder(c *ui.Context, title string, open *bool) *ui.Element { } func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all []ChatProject, conversations func(string) []ChatConversation, indent float32, v *ProjectListView) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) convs := conversations(p.ID) chosen := s.Selected == p.ID var b *ui.Element @@ -152,7 +154,7 @@ func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all [ } } if len(convs) > 0 { - b.Description(msg(c, map[bool]string{true: "project_list.expanded", false: "project_list.collapsed"}[open])) + b.Description(core.Msg(c, map[bool]string{true: "project_list.expanded", false: "project_list.collapsed"}[open])) } b.Children(func() { ui.Box(c).Size(8, 8).Shrink(0).Draw(func(pt *ui.Painter, r ui.Rect) { @@ -161,12 +163,12 @@ func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all [ } }) ui.Text(c, p.Name).SingleLine().Grow(1).MinWidth(0) - ui.Text(c, fmt.Sprint(len(convs))).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprint(len(convs))).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) if len(convs) > 0 { chevron := map[bool]string{true: "chevron-down", false: "chevron-right"}[open] line.Children(func() { - t := iconAction(c, icons.Must(chevron), fmt.Sprintf(msg(c, map[bool]string{true: "project_list.collapse", false: "project_list.expand"}[open]), p.Name)) + t := core.IconAction(c, icons.Must(chevron), fmt.Sprintf(core.Msg(c, map[bool]string{true: "project_list.collapse", false: "project_list.expand"}[open]), p.Name)) if t.Clicked() && !t.IsDisabled() { s.open[p.ID] = !open } @@ -181,8 +183,8 @@ func projectListProject(c *ui.Context, s *ProjectListState, p ChatProject, all [ } func projectListConversation(c *ui.Context, conv ChatConversation, home ChatProject, all []ChatProject, indent float32, v *ProjectListView) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) ui.Row(c).Key("conv:" + conv.ID).AlignItems(ui.Center).Children(func() { b := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2, 0, d.Unit()*2+indent). Justify(ui.Start).Radius(theme.ControlRadius).Label(conv.Title).Drag(ConversationDrag{ID: conv.ID}) @@ -194,15 +196,15 @@ func projectListConversation(c *ui.Context, conv ChatConversation, home ChatProj b.Background(k.SurfaceHover) } b.Children(func() { - ui.Text(c, cmp.Or(conv.Title, msg(c, "conversation_list.untitled"))).SingleLine().TextColor(k.TextMuted) + ui.Text(c, cmp.Or(conv.Title, core.Msg(c, "conversation_list.untitled"))).SingleLine().TextColor(k.TextMuted) }) if b.Clicked() { v.Opened = conv.ID } - conversationItemMenu(c, fmt.Sprintf(msg(c, "project_list.move_to"), conv.Title), func(m *PopupMenu) { - m.Group(msg(c, "project_list.move_group"), func() { + conversationItemMenu(c, fmt.Sprintf(core.Msg(c, "project_list.move_to"), conv.Title), func(m *overlay.PopupMenu) { + m.Group(core.Msg(c, "project_list.move_group"), func() { for _, p := range all { - if p.ID != home.ID && m.Item(p.Name, PopupMenuItemOptions{}) { + if p.ID != home.ID && m.Item(p.Name, overlay.PopupMenuItemOptions{}) { v.Moved, v.MoveID, v.MoveTo = true, conv.ID, p.ID } } diff --git a/project_list_test.go b/chat/project_list_test.go similarity index 99% rename from project_list_test.go rename to chat/project_list_test.go index 4042954..7b1f71e 100644 --- a/project_list_test.go +++ b/chat/project_list_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "testing" diff --git a/prompt_composer.go b/chat/prompt_composer.go similarity index 80% rename from prompt_composer.go rename to chat/prompt_composer.go index 0c71675..379d3c0 100644 --- a/prompt_composer.go +++ b/chat/prompt_composer.go @@ -1,10 +1,13 @@ -package mujica +package chat import ( "log/slog" "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // PromptComposerOptions configure a PromptComposer. @@ -32,23 +35,23 @@ type promptHooksKey struct{} // PromptComposer edits prompts with configurable send shortcuts. // Submitted never fires for blank text. -func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) InputResult { +func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) input.InputResult { if value == nil { panic("mujica: PromptComposer needs a value") } if opts.MaxHeight < 0 { panic("mujica: PromptComposer MaxHeight must not be negative") } - u := Density(c).Unit() + u := core.Density(c).Unit() maxH := opts.MaxHeight if maxH == 0 { maxH = 200 } label := opts.Label if label == "" { - label = msg(c, "prompt_composer.label") + label = core.Msg(c, "prompt_composer.label") } - var r InputResult + var r input.InputResult r.Element = ui.Column(c).Gap(u * 2).MinWidth(0).Disabled(opts.Disabled) hooks := ui.Local(r.Element, promptHooksKey{}, func() *promptHooks { return &promptHooks{} }) h := *hooks @@ -57,8 +60,8 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) In if opts.Attachments != nil { ui.Row(c).Gap(u * 2).Wrap().Children(opts.Attachments) } - ta := TextAreaOptions{Label: label, Placeholder: opts.Placeholder, MaxHeight: maxH, - keys: func(ev ui.InputEvent) bool { + ta := input.TextAreaOptions{Label: label, Placeholder: opts.Placeholder, MaxHeight: maxH, + Keys: func(ev ui.InputEvent) bool { if r.Element.IsDisabled() { return true } @@ -75,9 +78,8 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) In if !opts.CmdEnterSends { ta.SubmitKey = ui.KeyEnter } - area := TextArea(c, value, ta) - r.Input, r.changed = area.Input, area.Changed() - r.submitted = area.Submitted() && !opts.Composing && strings.TrimSpace(*value) != "" + area := input.TextArea(c, value, ta) + r = input.NewInputResult(r.Element, area.Input, area.Changed(), area.Submitted() && !opts.Composing && strings.TrimSpace(*value) != "") if opts.Tools != nil || opts.Actions != nil { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Row(c).Gap(u).AlignItems(ui.Center).Grow(1).Children(func() { @@ -91,14 +93,14 @@ func PromptComposer(c *ui.Context, value *string, opts PromptComposerOptions) In }) } }) - if r.submitted { + if r.Submitted() { slog.Debug("mujica: prompt_composer submit", "chars", len(*value)) } return r } // promptComposerHook registers a handler before editor input. -func promptComposerHook(composer InputResult, f func(ui.InputEvent) bool) { +func promptComposerHook(composer input.InputResult, f func(ui.InputEvent) bool) { if composer.Element == nil { panic("mujica: menus need a built PromptComposer") } diff --git a/prompt_composer_test.go b/chat/prompt_composer_test.go similarity index 91% rename from prompt_composer_test.go rename to chat/prompt_composer_test.go index 8135c75..feaf949 100644 --- a/prompt_composer_test.go +++ b/chat/prompt_composer_test.go @@ -1,16 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func promptTester(text *string, sends *int, opts PromptComposerOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if PromptComposer(c, text, opts).Submitted() { *sends++ } @@ -57,7 +58,7 @@ func TestPromptComposerSlotsAndDisabled(t *testing.T) { t.Fatalf("disabled composer: sends %d text %q", sends, text) } tt = ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PromptComposer(c, &text, PromptComposerOptions{ Attachments: func() { ui.Text(c, "report.pdf") }, Tools: func() { ui.Text(c, "Web search") }, @@ -77,7 +78,7 @@ func TestPromptComposerSnapshot(t *testing.T) { ui.Column(c).Padding(16).Children(func() { PromptComposer(c, &text, PromptComposerOptions{ Attachments: func() { AttachmentChip(c, Attachment{Name: "guests.csv", Size: 4096}) }, - Tools: func() { iconAction(c, icons.Must("paperclip"), "Attach") }, + Tools: func() { core.IconAction(c, icons.Must("paperclip"), "Attach") }, Actions: func() { SendButton(c, SendButtonOptions{Shortcut: "Enter"}) }, }) }) @@ -88,7 +89,7 @@ func TestPromptComposerCompositionDoesNotSend(t *testing.T) { text, sends := "draft", 0 composing := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt", Composing: composing}).Submitted() { sends++ } diff --git a/prompt_library.go b/chat/prompt_library.go similarity index 84% rename from prompt_library.go rename to chat/prompt_library.go index 738566e..8ef59c1 100644 --- a/prompt_library.go +++ b/chat/prompt_library.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PromptTemplate is a reusable prompt; {{name}} marks a variable. @@ -78,24 +80,24 @@ func PromptLibrary(c *ui.Context, s *PromptLibraryState, templates []PromptTempl if s.Values == nil { s.Values = map[string]string{} } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) promptLibraryFilter(&s.shown, templates, s.Query, s.Category) row := ui.Row(c).Gap(d.Unit() * 4).AlignItems(ui.Stretch).Role(ui.RoleGroup).Label(opts.Label).Disabled(opts.Disabled) v := PromptLibraryView{Element: row} row.Children(func() { ui.Column(c).Width(150).Shrink(0).Gap(2).FocusGroup(ui.Vertical).Children(func() { - promptLibraryCategory(c, k, s, "", msg(c, "prompt_library.all")) + promptLibraryCategory(c, k, s, "", core.Msg(c, "prompt_library.all")) for _, cat := range s.shown.cats { promptLibraryCategory(c, k, s, cat, cat) } }) ui.Column(c).Width(240).Shrink(0).Gap(d.Unit() * 2).Children(func() { - SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "prompt_library.search"), Disabled: opts.Disabled}) + input.SearchInput(c, &s.Query, input.SearchInputOptions{Placeholder: core.Msg(c, "prompt_library.search"), Disabled: opts.Disabled}) ui.Scroll(c).MaxHeight(320).Children(func() { ui.Column(c).Gap(2).FocusGroup(ui.Vertical).Children(func() { if len(s.shown.ids) == 0 { - ui.Text(c, msg(c, "prompt_library.no_results")).TextColor(k.TextMuted).Padding(d.Unit() * 2) + ui.Text(c, core.Msg(c, "prompt_library.no_results")).TextColor(k.TextMuted).Padding(d.Unit() * 2) } for _, i := range s.shown.ids { t := templates[i] @@ -110,7 +112,7 @@ func PromptLibrary(c *ui.Context, s *PromptLibraryState, templates []PromptTempl ui.Column(c).Grow(1).MinWidth(200).Gap(d.Unit() * 2).Children(func() { i := slices.IndexFunc(templates, func(t PromptTemplate) bool { return t.ID == s.Selected }) if i < 0 { - ui.Text(c, msg(c, "prompt_library.pick")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "prompt_library.pick")).TextColor(k.TextMuted) return } v.Inserted = promptLibraryPreview(c, k, s, templates[i], opts.Disabled) @@ -154,7 +156,7 @@ func promptLibraryCategory(c *ui.Context, k theme.Tokens, s *PromptLibraryState, // selectableRow is a focusable row marked chosen with a diamond. func selectableRow(c *ui.Context, k theme.Tokens, key, label string, chosen bool) bool { - d := Density(c) + d := core.Density(c) b := ui.ButtonBase(c).Key(key).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).Justify(ui.Start). Radius(theme.ControlRadius).Label(label).Selected(chosen) b.Background(ui.Transparent).TextColor(k.Text) @@ -178,9 +180,9 @@ func selectableRow(c *ui.Context, k theme.Tokens, key, label string, chosen bool // promptLibraryPreview shows the body, variable fields and Insert. func promptLibraryPreview(c *ui.Context, k theme.Tokens, s *PromptLibraryState, t PromptTemplate, disabled bool) string { - d := Density(c) + d := core.Density(c) vars := PromptLibraryVariables(t.Body) - ui.Text(c, t.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, t.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) missing := false var spans []ui.Span at := 0 @@ -201,8 +203,8 @@ func promptLibraryPreview(c *ui.Context, k theme.Tokens, s *PromptLibraryState, for _, name := range vars { val := s.Values[name] ui.Column(c).Key("var:" + name).Children(func() { - FormField(c, name, FormFieldOptions{Required: true}, func() *ui.Element { - r := TextInput(c, &val, TextInputOptions{Label: name, Disabled: disabled}) + input.FormField(c, name, input.FormFieldOptions{Required: true}, func() *ui.Element { + r := input.TextInput(c, &val, input.TextInputOptions{Label: name, Disabled: disabled}) if r.Changed() { s.Values[name] = val } @@ -210,7 +212,7 @@ func promptLibraryPreview(c *ui.Context, k theme.Tokens, s *PromptLibraryState, }) }) } - insert := Button(c, msg(c, "prompt_library.insert"), ButtonOptions{Icon: icons.Must("plus"), Disabled: missing || disabled}).Clicked() + insert := input.Button(c, core.Msg(c, "prompt_library.insert"), input.ButtonOptions{Icon: icons.Must("plus"), Disabled: missing || disabled}).Clicked() if insert && !missing { return PromptLibraryFill(t.Body, s.Values) } diff --git a/prompt_library_test.go b/chat/prompt_library_test.go similarity index 99% rename from prompt_library_test.go rename to chat/prompt_library_test.go index 63f56e7..c6dfcc7 100644 --- a/prompt_library_test.go +++ b/chat/prompt_library_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "slices" diff --git a/quote_reply.go b/chat/quote_reply.go similarity index 57% rename from quote_reply.go rename to chat/quote_reply.go index ad82de1..f55a3b2 100644 --- a/quote_reply.go +++ b/chat/quote_reply.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QuoteReply shows the author and a one-line excerpt of a quoted message; Clicked jumps to it. @@ -15,19 +16,19 @@ func QuoteReply(c *ui.Context, author, excerpt string) *ui.Element { if author == "" { panic("mujica: QuoteReply needs an author") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() line := strings.Join(strings.Fields(excerpt), " ") b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MaxWidth(420).Gap(u*2).Padding(u, u*3, u, u*2).Justify(ui.Start). Radius(theme.ControlRadius).Background(k.SurfaceHover).BorderWidth(0, 0, 0, 3).BorderColor(k.AccentText). - Label(fmt.Sprintf(msg(c, "quote_reply.label"), author, line)).Transition(colorFade) + Label(fmt.Sprintf(core.Msg(c, "quote_reply.label"), author, line)).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.Selection) } b.Children(func() { - ui.Text(c, author).FontWeight(600).FontSize(FontSize(c, theme.CaptionSize)).SingleLine().Shrink(0) - ui.Text(c, line).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0).Shrink(1) + ui.Text(c, author).FontWeight(600).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine().Shrink(0) + ui.Text(c, line).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0).Shrink(1) }) return b } diff --git a/quote_reply_test.go b/chat/quote_reply_test.go similarity index 90% rename from quote_reply_test.go rename to chat/quote_reply_test.go index b9aa5b5..865669a 100644 --- a/quote_reply_test.go +++ b/chat/quote_reply_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestQuoteReplyJumpsAndFlattensExcerpt(t *testing.T) { jumps, off := 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(off).Children(func() { if QuoteReply(c, "Marie", "Let them\n eat cake.").Clicked() { jumps++ @@ -39,7 +41,7 @@ func TestQuoteReplyNeedsAuthor(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) QuoteReply(c, "", "x") }, 100, 100) } diff --git a/regenerate_menu.go b/chat/regenerate_menu.go similarity index 72% rename from regenerate_menu.go rename to chat/regenerate_menu.go index 95fe6c4..ce02421 100644 --- a/regenerate_menu.go +++ b/chat/regenerate_menu.go @@ -1,11 +1,13 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" ) // RegenerateKind is what a RegenerateMenu choice asks for. @@ -49,13 +51,13 @@ func RegenerateMenu(c *ui.Context, opts RegenerateMenuOptions) RegenerateMenuVie variants := opts.Variants if variants == nil { for _, key := range []string{"regenerate_menu.longer", "regenerate_menu.shorter", "regenerate_menu.simpler", "regenerate_menu.detailed"} { - variants = append(variants, msg(c, key)) + variants = append(variants, core.Msg(c, key)) } } var v RegenerateMenuView pick := func(ch RegenerateChoice) { v.Chosen, v.Choice = true, ch } - v.Element = DropdownMenu(c, msg(c, "regenerate_menu.regenerate"), DropdownMenuOptions{Icon: icons.Must("refresh-cw"), Disabled: opts.Disabled}, func(m *PopupMenu) { - if m.Item(msg(c, "regenerate_menu.again"), PopupMenuItemOptions{}) { + v.Element = overlay.DropdownMenu(c, core.Msg(c, "regenerate_menu.regenerate"), overlay.DropdownMenuOptions{Icon: icons.Must("refresh-cw"), Disabled: opts.Disabled}, func(m *overlay.PopupMenu) { + if m.Item(core.Msg(c, "regenerate_menu.again"), overlay.PopupMenuItemOptions{}) { pick(RegenerateChoice{Kind: RegenerateAgain}) } others := 0 @@ -64,17 +66,17 @@ func RegenerateMenu(c *ui.Context, opts RegenerateMenuOptions) RegenerateMenuVie others++ } } - m.Submenu(msg(c, "regenerate_menu.model"), PopupMenuItemOptions{Disabled: others == 0}, func(m *PopupMenu) { + m.Submenu(core.Msg(c, "regenerate_menu.model"), overlay.PopupMenuItemOptions{Disabled: others == 0}, func(m *overlay.PopupMenu) { for _, model := range opts.Models { - if model != opts.Current && m.Item(model, PopupMenuItemOptions{}) { + if model != opts.Current && m.Item(model, overlay.PopupMenuItemOptions{}) { pick(RegenerateChoice{Kind: RegenerateModel, Model: model}) } } }) m.Separator() - m.Group(msg(c, "regenerate_menu.variants"), func() { + m.Group(core.Msg(c, "regenerate_menu.variants"), func() { for _, variant := range variants { - if m.Item(variant, PopupMenuItemOptions{}) { + if m.Item(variant, overlay.PopupMenuItemOptions{}) { pick(RegenerateChoice{Kind: RegenerateVariant, Variant: variant}) } } diff --git a/regenerate_menu_test.go b/chat/regenerate_menu_test.go similarity index 99% rename from regenerate_menu_test.go rename to chat/regenerate_menu_test.go index 1f44c46..9a71405 100644 --- a/regenerate_menu_test.go +++ b/chat/regenerate_menu_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "testing" diff --git a/scroll_to_bottom_button.go b/chat/scroll_to_bottom_button.go similarity index 73% rename from scroll_to_bottom_button.go rename to chat/scroll_to_bottom_button.go index 19ac548..6676df2 100644 --- a/scroll_to_bottom_button.go +++ b/chat/scroll_to_bottom_button.go @@ -1,13 +1,14 @@ -package mujica +package chat import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ScrollToBottomButton shows while away is true, with the unread count; click it to return to the end. @@ -19,19 +20,19 @@ func ScrollToBottomButton(c *ui.Context, away bool, unread int) *ui.Element { if !away { return ui.Box(c).Absolute().Invisible() } - k := Tokens(c) - u := Density(c).Unit() - label := msg(c, "scroll_to_bottom_button.label") + k := core.Tokens(c) + u := core.Density(c).Unit() + label := core.Msg(c, "scroll_to_bottom_button.label") if unread > 0 { - label = fmt.Sprintf(msg(c, "scroll_to_bottom_button.unread"), unread) + label = fmt.Sprintf(core.Msg(c, "scroll_to_bottom_button.unread"), unread) } wrap := ui.Row(c).Absolute().Left(0).Right(0).Bottom(u * 3).Justify(ui.Center).PassThrough() var b *ui.Element wrap.Children(func() { b = ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*3).Gap(u*2). Radius(theme.MinHitSize/2).Label(label).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder). - TextColor(k.Text).Shadow(0, 2, 8, 0, ui.RGBA(0, 0, 0, 0.14)).Transition(colorFade) - b.Opacity(overlayEnter(c, b)) + TextColor(k.Text).Shadow(0, 2, 8, 0, ui.RGBA(0, 0, 0, 0.14)).Transition(core.ColorFade) + b.Opacity(core.OverlayEnter(c, b)) internal.Ring(b, k.Focus, theme.MinHitSize/2) if b.Hovered() { b.Background(k.SurfaceHover) diff --git a/scroll_to_bottom_button_test.go b/chat/scroll_to_bottom_button_test.go similarity index 90% rename from scroll_to_bottom_button_test.go rename to chat/scroll_to_bottom_button_test.go index 2ea4271..15c1c1d 100644 --- a/scroll_to_bottom_button_test.go +++ b/chat/scroll_to_bottom_button_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestScrollToBottomButtonShowsWhenAway(t *testing.T) { away, unread, clicks := false, 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Fill().Children(func() { if ScrollToBottomButton(c, away, unread).Clicked() { clicks++ @@ -39,7 +41,7 @@ func TestScrollToBottomButtonNegativeUnread(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ScrollToBottomButton(c, true, -1) }, 100, 100) } diff --git a/search_progress.go b/chat/search_progress.go similarity index 68% rename from search_progress.go rename to chat/search_progress.go index 1947378..e850c66 100644 --- a/search_progress.go +++ b/chat/search_progress.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SearchStage describes one caller-owned search step. @@ -33,11 +35,11 @@ type searchProgressState struct { } // SearchProgress displays keywords, read counts and collapsible stages. -func SearchProgress(c *ui.Context, opts SearchProgressOptions) ChoiceResult { +func SearchProgress(c *ui.Context, opts SearchProgressOptions) core.ChoiceResult { if opts.Query == "" || opts.Read < 0 || opts.Total < 0 || opts.Read > opts.Total { panic("mujica: SearchProgress needs a Query and 0 <= Read <= Total") } - r := ChoiceResult{Element: ui.Column(c).MinWidth(0)} + r := core.ChoiceResult{Element: ui.Column(c).MinWidth(0)} st := ui.Local(r.Element, "search", func() searchProgressState { return searchProgressState{open: true} }) if opts.Done && !st.done { // Finishing folds it once; the user may open it again. @@ -49,12 +51,12 @@ func SearchProgress(c *ui.Context, opts SearchProgressOptions) ChoiceResult { slog.Debug("mujica: search_progress restart") } st.done = opts.Done - title := fmt.Sprintf(msg(c, "search_progress.searching"), opts.Query) + title := fmt.Sprintf(core.Msg(c, "search_progress.searching"), opts.Query) if opts.Done { - title = fmt.Sprintf(msg(c, "search_progress.done"), opts.Query, opts.Read) + title = fmt.Sprintf(core.Msg(c, "search_progress.done"), opts.Query, opts.Read) } - r.Element.Children(func() { r.changed = searchProgressBody(c, st, title, opts) }) - if r.changed { + r.Element.Children(func() { r = core.NewChoiceResult(r.Element, searchProgressBody(c, st, title, opts)) }) + if r.Changed() { slog.Debug("mujica: search_progress fold", "open", st.open) } return r @@ -62,20 +64,20 @@ func SearchProgress(c *ui.Context, opts SearchProgressOptions) ChoiceResult { // searchProgressBody is the collapsible header and the stages. func searchProgressBody(c *ui.Context, st *searchProgressState, title string, opts SearchProgressOptions) bool { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) - return Collapsible(c, &st.open, CollapsibleOptions{Title: title, Trigger: func() { + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) + return data.Collapsible(c, &st.open, data.CollapsibleOptions{Title: title, Trigger: func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.TextMuted).Children(func() { if opts.Done { ui.Icon(c, icons.Must("search")).Size(14, 14) } else { - Spinner(c, SpinnerOptions{Size: 14, Label: title}) + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: title}) } ui.Text(c, title).FontSize(small).SingleLine() }) }}, func() { - ui.Text(c, fmt.Sprintf(msg(c, "search_progress.read"), opts.Read, opts.Total)).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "search_progress.read"), opts.Read, opts.Total)).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted) ui.Column(c).Gap(u).Role(ui.RoleList).Children(func() { for _, s := range opts.Stages { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() { diff --git a/search_progress_test.go b/chat/search_progress_test.go similarity index 94% rename from search_progress_test.go rename to chat/search_progress_test.go index 7afd323..c6b7ed7 100644 --- a/search_progress_test.go +++ b/chat/search_progress_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func searchStages(done int) []SearchStage { @@ -18,7 +20,7 @@ func searchStages(done int) []SearchStage { func TestSearchProgressFoldsWhenDone(t *testing.T) { opts := SearchProgressOptions{Query: "Versailles 1682", Read: 3, Total: 8, Stages: searchStages(2)} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SearchProgress(c, opts) }, 500, 300) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -45,7 +47,7 @@ func TestSearchProgressBadCounts(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SearchProgress(c, SearchProgressOptions{Query: "q", Read: 3, Total: 1}) }, 100, 100) } @@ -53,7 +55,7 @@ func TestSearchProgressBadCounts(t *testing.T) { func TestSearchProgressReopensForNextSearch(t *testing.T) { opts := SearchProgressOptions{Query: "first", Read: 1, Total: 2, Stages: []SearchStage{{Label: "First stage"}}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SearchProgress(c, opts) }, 400, 300) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/send_button.go b/chat/send_button.go similarity index 80% rename from send_button.go rename to chat/send_button.go index ca7aae0..1184152 100644 --- a/send_button.go +++ b/chat/send_button.go @@ -1,13 +1,14 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SendButtonOptions configure a SendButton. @@ -32,11 +33,11 @@ func (r SendResult) Stopped() bool { return r.stop } // SendButton sends the prompt, or stops the reply while streaming. func SendButton(c *ui.Context, opts SendButtonOptions) SendResult { - k := Tokens(c) - h := Density(c).ControlHeight() - label, icon := msg(c, "send_button.send"), "arrow-up" + k := core.Tokens(c) + h := core.Density(c).ControlHeight() + label, icon := core.Msg(c, "send_button.send"), "arrow-up" if opts.Streaming { - label, icon = msg(c, "send_button.stop"), "square" + label, icon = core.Msg(c, "send_button.stop"), "square" } tip := label if opts.Shortcut != "" && !opts.Streaming { @@ -46,7 +47,7 @@ func SendButton(c *ui.Context, opts SendButtonOptions) SendResult { var b *ui.Element r.Element = ui.Row(c).Shrink(0).Disabled(opts.Disabled && !opts.Streaming) r.Element.Children(func() { - b = ui.ButtonBase(c).Size(h, h).Radius(h / 2).Label(label).Tooltip(tip).Transition(colorFade) + b = ui.ButtonBase(c).Size(h, h).Radius(h / 2).Label(label).Tooltip(tip).Transition(core.ColorFade) internal.Ring(b, k.Focus, h/2) bg, fg := k.Accent, k.OnAccent switch { diff --git a/send_button_test.go b/chat/send_button_test.go similarity index 91% rename from send_button_test.go rename to chat/send_button_test.go index 0d8efe4..692e26c 100644 --- a/send_button_test.go +++ b/chat/send_button_test.go @@ -1,16 +1,18 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSendButtonSendStopDisabled(t *testing.T) { opts := SendButtonOptions{Shortcut: "Enter"} sends, stops := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SendButton(c, opts) if r.Sent() { sends++ diff --git a/shared_conversation_view.go b/chat/shared_conversation_view.go similarity index 66% rename from shared_conversation_view.go rename to chat/shared_conversation_view.go index cda049f..154d609 100644 --- a/shared_conversation_view.go +++ b/chat/shared_conversation_view.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "cmp" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SessionRole is who wrote a message; zero is SessionUser. @@ -40,30 +43,30 @@ type SharedConversationViewOptions struct { } // SharedConversationView shows a shared conversation read-only with Copy link. -func SharedConversationView(c *ui.Context, messages []SessionMessage, opts SharedConversationViewOptions) ChoiceResult { +func SharedConversationView(c *ui.Context, messages []SessionMessage, opts SharedConversationViewOptions) core.ChoiceResult { if opts.Title == "" { panic("mujica: SharedConversationView needs a Title") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Grow(1).MinHeight(0).Gap(d.Unit() * 3).Disabled(opts.Disabled) rows := *ui.Local(col, sharedConversationKey{}, func() *ui.ListState { return &ui.ListState{} }) copied := false col.Children(func() { ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { ui.Column(c).Grow(1).Gap(2).Children(func() { - ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text) - by := fmt.Sprintf(msg(c, "shared_conversation_view.shared_by"), cmp.Or(opts.SharedBy, msg(c, "shared_conversation_view.someone"))) + ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).TextColor(k.Text) + by := fmt.Sprintf(core.Msg(c, "shared_conversation_view.shared_by"), cmp.Or(opts.SharedBy, core.Msg(c, "shared_conversation_view.someone"))) if !opts.SharedAt.IsZero() { - by += " · " + dateSeparatorLabel(c, DateOf(opts.SharedAt), DateOf(c.Now())) + by += " · " + dateSeparatorLabel(c, core.DateOf(opts.SharedAt), core.DateOf(c.Now())) } - ui.Text(c, by).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, by).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Children(func() { ui.Icon(c, icons.Must("lock")).Size(14, 14).TextColor(k.TextMuted) - ui.Text(c, msg(c, "shared_conversation_view.read_only")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "shared_conversation_view.read_only")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) - copied = Button(c, msg(c, "shared_conversation_view.copy_link"), ButtonOptions{Variant: Secondary, Icon: icons.Must("copy"), + copied = input.Button(c, core.Msg(c, "shared_conversation_view.copy_link"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("copy"), Disabled: opts.Disabled}).Clicked() }) ui.Box(c).FillWidth().Height(9).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 6, 5, k.Ornament) }) @@ -73,18 +76,18 @@ func SharedConversationView(c *ui.Context, messages []SessionMessage, opts Share if copied { slog.Debug("mujica: shared_conversation_view copy_link") } - return ChoiceResult{Element: col, changed: copied} + return core.NewChoiceResult(col, copied) } type sharedConversationKey struct{} // sessionMessageView shows one read-only message with author and time. func sessionMessageView(c *ui.Context, k theme.Tokens, m SessionMessage) { - d := Density(c) + d := core.Density(c) if m.Role < SessionUser || m.Role > SessionSystem { panic("mujica: SessionMessage Role out of range") } - author := cmp.Or(m.Author, msg(c, sessionRoleKeys[m.Role])) + author := cmp.Or(m.Author, core.Msg(c, sessionRoleKeys[m.Role])) box := ui.Column(c).Gap(d.Unit()).Padding(d.Unit()*2, d.Unit()*3).Radius(theme.CardRadius) if m.Role == SessionUser { box.Background(k.Background).Border(theme.BorderWidth, k.Border) @@ -93,11 +96,11 @@ func sessionMessageView(c *ui.Context, k theme.Tokens, m SessionMessage) { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, author).Bold().TextColor(k.Text) if !m.Time.IsZero() { - ui.Text(c, m.Time.Format("15:04")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, m.Time.Format("15:04")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) // Plain text: Markdown is not rendered here. - Text(c, m.Text, TextOptions{Selectable: true}) + display.Text(c, m.Text, display.TextOptions{Selectable: true}) }) } diff --git a/shared_conversation_view_test.go b/chat/shared_conversation_view_test.go similarity index 95% rename from shared_conversation_view_test.go rename to chat/shared_conversation_view_test.go index 43fae0e..fc99d86 100644 --- a/shared_conversation_view_test.go +++ b/chat/shared_conversation_view_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSharedConversationViewShowsAndCopies(t *testing.T) { @@ -56,7 +58,7 @@ func TestSharedConversationViewDisabledAndChecks(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SharedConversationView(c, nil, SharedConversationViewOptions{}) }, 100, 100, 1) } diff --git a/slash_command_menu.go b/chat/slash_command_menu.go similarity index 87% rename from slash_command_menu.go rename to chat/slash_command_menu.go index a77e26e..263b022 100644 --- a/slash_command_menu.go +++ b/chat/slash_command_menu.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -7,8 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SlashCommand is one command of a SlashCommandMenu. @@ -30,7 +33,7 @@ func (r SlashCommandResult) Chosen() (SlashCommand, bool) { return r.chosen, r.o // SlashCommandMenu filters commands after a slash trigger. // Arrows navigate; Enter chooses; Tab completes; Escape dismisses. -func SlashCommandMenu(c *ui.Context, composer InputResult, value *string, commands []SlashCommand) SlashCommandResult { +func SlashCommandMenu(c *ui.Context, composer input.InputResult, value *string, commands []SlashCommand) SlashCommandResult { if composer.Element == nil || composer.Input == nil || value == nil { panic("mujica: SlashCommandMenu needs composer and value") } @@ -63,7 +66,7 @@ func SlashCommandMenu(c *ui.Context, composer InputResult, value *string, comman var r SlashCommandResult st := ui.Local(composer.Element, slashMenuKey{"slash"}, func() slashMenuState { return slashMenuState{} }) shown, rows := data.shown, data.rows - i, how, panel := slashMenuShow(c, composer, st, query, typing, rows, slashMenuOptions{empty: msg(c, "slash_command_menu.empty"), label: msg(c, "slash_command_menu.label")}) + i, how, panel := slashMenuShow(c, composer, st, query, typing, rows, slashMenuOptions{empty: core.Msg(c, "slash_command_menu.empty"), label: core.Msg(c, "slash_command_menu.label")}) r.Element = panel if i >= 0 { *value = "/" + shown[i].Name + " " @@ -113,7 +116,7 @@ const ( ) // slashMenuShow returns the selected row and activation method. -func slashMenuShow(c *ui.Context, composer InputResult, st *slashMenuState, query string, open bool, rows []slashMenuRow, o slashMenuOptions) (int, slashMenuHow, *ui.Element) { +func slashMenuShow(c *ui.Context, composer input.InputResult, st *slashMenuState, query string, open bool, rows []slashMenuRow, o slashMenuOptions) (int, slashMenuHow, *ui.Element) { if !open { st.dismissed = nil } @@ -176,9 +179,9 @@ func slashMenuShow(c *ui.Context, composer InputResult, st *slashMenuState, quer // slashMenuPanel builds visible rows and returns clicked choices. func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMenuRow, o slashMenuOptions) (int, *ui.Element) { - k := Tokens(c) - u := Density(c).Unit() - rowH := Density(c).ControlHeight() + k := core.Tokens(c) + u := core.Density(c).Unit() + rowH := core.Density(c).ControlHeight() headH := rowH * 0.75 h := 2 * u group := "" @@ -194,12 +197,12 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe ww, wh := c.Size() h = min(h+2*theme.BorderWidth, 280, max(0, wh-8)) w := min(max(a.W*0.6, 320), max(a.W, 200), max(0, ww-8)) - x, y := overlayPlace(c, a, w, h, u) + x, y := core.PlaceOverlay(c, a, w, h, u) clicked, pressed := -1, false var result *ui.Element ui.Overlay(c, func() { panel := ui.Column(c).Absolute().Left(x).Top(y).Width(w).Height(h).Role(ui.RoleList).Label(o.label) - overlayStyle(c, panel, overlayAnchored(c, panel, a)) + core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, a)) result = panel panel.HandleInput(func(ev ui.InputEvent) bool { if ev.Kind == ui.InputPointerDown { @@ -216,7 +219,7 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe if len(rows) == 0 { ui.Row(c).MinHeight(rowH).Padding(0, u*3).Gap(u * 2).AlignItems(ui.Center).TextColor(k.TextMuted).Children(func() { if o.loading { - Spinner(c, SpinnerOptions{Size: 14}) + core.Spinner(c, core.SpinnerOptions{Size: 14}) } ui.Text(c, o.empty).SingleLine() }) @@ -225,7 +228,7 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe r := rows[i] ui.Column(c).Children(func() { if r.group != "" && (i == 0 || r.group != rows[i-1].group) { - ui.Text(c, r.group).Height(headH).Padding(u*2, u*3, 0, u*3).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, r.group).Height(headH).Padding(u*2, u*3, 0, u*3).FontSize(core.FontSize(c, theme.CaptionSize)). FontWeight(600).TextColor(k.TextMuted).Role(ui.RoleText) } row := ui.Row(c).Key(i).MinHeight(rowH).Padding(0, u*3, 0, u*2).Gap(u * 2).AlignItems(ui.Center). @@ -247,7 +250,7 @@ func slashMenuPanel(c *ui.Context, a ui.Rect, st *slashMenuState, rows []slashMe ui.Icon(c, icons.Must(r.icon)).Size(16, 16).Shrink(0).TextColor(k.TextMuted) } ui.Text(c, r.label).SingleLine().Shrink(0).FontWeight(600) - ui.Text(c, r.detail).SingleLine().MinWidth(0).Shrink(1).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, r.detail).SingleLine().MinWidth(0).Shrink(1).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) }) }) }).Fill().Padding(u) diff --git a/slash_command_menu_test.go b/chat/slash_command_menu_test.go similarity index 97% rename from slash_command_menu_test.go rename to chat/slash_command_menu_test.go index f5bf8cd..70d083e 100644 --- a/slash_command_menu_test.go +++ b/chat/slash_command_menu_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var slashCommandsSample = []SlashCommand{ @@ -22,7 +24,7 @@ type slashTester struct { } func (s *slashTester) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Justify(ui.End).Padding(16).Children(func() { p := PromptComposer(c, &s.text, PromptComposerOptions{Label: "Prompt", Disabled: s.off}) if p.Submitted() { @@ -130,7 +132,7 @@ func TestSlashCommandMenuNarrowAndLargeData(t *testing.T) { text := "/" var panel *ui.Element tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt"}) panel = SlashCommandMenu(c, p, &text, cmds).Element }, 180, 120) @@ -168,7 +170,7 @@ func TestSlashCommandMenuQueryRevealsHighlight(t *testing.T) { } text := "/" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Justify(ui.End).Children(func() { p := PromptComposer(c, &text, PromptComposerOptions{Label: "Prompt"}) SlashCommandMenu(c, p, &text, commands) diff --git a/source_card.go b/chat/source_card.go similarity index 78% rename from source_card.go rename to chat/source_card.go index df857a4..a9baa28 100644 --- a/source_card.go +++ b/chat/source_card.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SourceCard displays source metadata; Clicked requests opening. @@ -16,10 +17,10 @@ func SourceCard(c *ui.Context, n int, src Source, st *CitationState) *ui.Element if n < 1 || st == nil || src.Title == "" || src.URL == "" { panic("mujica: SourceCard needs n >= 1, a state and a Source with Title and URL") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).Padding(u*2, u*3).AlignItems(ui.Start).Justify(ui.Start).Radius(theme.CardRadius). - Label(fmt.Sprintf(msg(c, "source_card.open"), src.Title)).Tooltip(src.URL).Transition(colorFade) + Label(fmt.Sprintf(core.Msg(c, "source_card.open"), src.Title)).Tooltip(src.URL).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.CardRadius) st.track(c, b, n) frame := c.Now() diff --git a/source_card_test.go b/chat/source_card_test.go similarity index 92% rename from source_card_test.go rename to chat/source_card_test.go index 4d6a804..7dcc9eb 100644 --- a/source_card_test.go +++ b/chat/source_card_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSourceCardOpensAndLights(t *testing.T) { @@ -11,7 +13,7 @@ func TestSourceCardOpensAndLights(t *testing.T) { opened, off := 0, false var bg []byte tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Width(360).Disabled(off).Children(func() { if SourceCard(c, 1, sourcesSample[0], &st).Clicked() { opened++ @@ -50,7 +52,7 @@ func TestSourceCardNeedsURL(t *testing.T) { }() var st CitationState ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SourceCard(c, 1, Source{Title: "x"}, &st) }, 100, 100) } diff --git a/sources_panel.go b/chat/sources_panel.go similarity index 71% rename from sources_panel.go rename to chat/sources_panel.go index b51aa86..8f39eea 100644 --- a/sources_panel.go +++ b/chat/sources_panel.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OpenResult carries the list and opened 1-based source number. @@ -24,10 +25,10 @@ func SourcesPanel(c *ui.Context, sources []Source, st *CitationState) OpenResult if st == nil || st.Active < 0 || st.Active > len(sources) { panic("mujica: SourcesPanel needs a state whose Active is 0 or a listed source") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r OpenResult - r.Element = ui.Column(c).Gap(u * 2).Grow(1).MinHeight(0).Role(ui.RoleList).Label(msg(c, "sources_panel.label")) + r.Element = ui.Column(c).Gap(u * 2).Grow(1).MinHeight(0).Role(ui.RoleList).Label(core.Msg(c, "sources_panel.label")) shown := ui.Local(r.Element, "active", func() int { return 0 }) located := ui.Local(r.Element, "locate", func() uint64 { return 0 }) list := ui.Local(r.Element, "list", func() ui.ListState { return ui.ListState{} }) @@ -36,14 +37,14 @@ func SourcesPanel(c *ui.Context, sources []Source, st *CitationState) OpenResult slog.Debug("mujica: sources_panel locate", "source", st.Active) } r.Element.Children(func() { - ui.Text(c, fmt.Sprintf("%s (%d)", msg(c, "sources_panel.label"), len(sources))).Font(theme.SerifFont). - FontSize(FontSize(c, theme.H3Size)).FontWeight(600) + ui.Text(c, fmt.Sprintf("%s (%d)", core.Msg(c, "sources_panel.label"), len(sources))).Font(theme.SerifFont). + FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600) if len(sources) == 0 { - ui.Text(c, msg(c, "sources_panel.empty")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "sources_panel.empty")).TextColor(k.TextMuted) return } ui.List(c, list, len(sources), func(i int) { - card := SourceCard(c, i+1, sources[i], st).MinHeight(Density(c).ControlHeight()).Role(ui.RoleListItem) + card := SourceCard(c, i+1, sources[i], st).MinHeight(core.Density(c).ControlHeight()).Role(ui.RoleListItem) if card.Clicked() && !card.IsDisabled() { r.opened = i + 1 } diff --git a/sources_panel_test.go b/chat/sources_panel_test.go similarity index 94% rename from sources_panel_test.go rename to chat/sources_panel_test.go index c97f582..24d7a19 100644 --- a/sources_panel_test.go +++ b/chat/sources_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package chat import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSourcesPanelOrderOpenAndScroll(t *testing.T) { @@ -15,7 +17,7 @@ func TestSourcesPanelOrderOpenAndScroll(t *testing.T) { var st CitationState opened := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(360).Height(300).Children(func() { if n, ok := SourcesPanel(c, many, &st).Opened(); ok { opened = n @@ -43,7 +45,7 @@ func TestSourcesPanelOrderOpenAndScroll(t *testing.T) { func TestSourcesPanelEmptyAndBadActive(t *testing.T) { var st CitationState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SourcesPanel(c, nil, &st) }, 300, 200) if !tt.HasText("No sources") { @@ -72,7 +74,7 @@ func TestSourcesPanelLargeData(t *testing.T) { } st := CitationState{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(360).Height(300).Children(func() { SourcesPanel(c, sources, &st) }) }, 400, 300) if n := len(tt.Texts()); n > 250 { @@ -94,7 +96,7 @@ func TestSourcesPanelRepeatsCitationLocate(t *testing.T) { var st CitationState actions := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Children(func() { if CitationBadge(c, 90, sources[89], &st).Changed() { actions++ diff --git a/stop_generating_button.go b/chat/stop_generating_button.go similarity index 67% rename from stop_generating_button.go rename to chat/stop_generating_button.go index 0d5fe29..a015fae 100644 --- a/stop_generating_button.go +++ b/chat/stop_generating_button.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StopGeneratingButtonOptions configure a StopGeneratingButton. @@ -18,12 +20,12 @@ type StopGeneratingButtonOptions struct { } // StopGeneratingButton asks to stop generating; Mod+. does too. -func StopGeneratingButton(c *ui.Context, opts StopGeneratingButtonOptions) ChoiceResult { +func StopGeneratingButton(c *ui.Context, opts StopGeneratingButtonOptions) core.ChoiceResult { if !opts.Generating { - return ChoiceResult{} + return core.ChoiceResult{} } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) mods := ui.Ctrl if runtime.GOOS == "darwin" { mods = ui.Cmd @@ -32,17 +34,17 @@ func StopGeneratingButton(c *ui.Context, opts StopGeneratingButtonOptions) Choic wrap := ui.Row(c).Shrink(0).Disabled(opts.Disabled) wrap.Children(func() { b = ui.ButtonBase(c).MinHeight(max(theme.MinHitSize, d.ControlHeight()-4)).Padding(0, d.Unit()*3).Gap(d.Unit()*2). - Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(msg(c, "stop_generating_button.stop")) + Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(core.Msg(c, "stop_generating_button.stop")) }) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { ui.Box(c).Size(10, 10).Shrink(0).Background(k.Text).Radius(1) - ui.Text(c, msg(c, "stop_generating_button.stop")).SingleLine() - Kbd(c, "Mod", ".") + ui.Text(c, core.Msg(c, "stop_generating_button.stop")).SingleLine() + display.Kbd(c, "Mod", ".") }) stop := b.Clicked() if c.Shortcut(mods, ui.KeyPeriod) { @@ -52,5 +54,5 @@ func StopGeneratingButton(c *ui.Context, opts StopGeneratingButtonOptions) Choic if stop { slog.Debug("mujica: stop_generating_button stop") } - return ChoiceResult{Element: b, changed: stop} + return core.NewChoiceResult(b, stop) } diff --git a/stop_generating_button_test.go b/chat/stop_generating_button_test.go similarity index 99% rename from stop_generating_button_test.go rename to chat/stop_generating_button_test.go index c6663b4..d9237d2 100644 --- a/stop_generating_button_test.go +++ b/chat/stop_generating_button_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "runtime" diff --git a/streaming_text.go b/chat/streaming_text.go similarity index 93% rename from streaming_text.go rename to chat/streaming_text.go index 8c658f4..6da3f8e 100644 --- a/streaming_text.go +++ b/chat/streaming_text.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "strings" @@ -6,6 +6,8 @@ import ( "unicode/utf8" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // StreamingTextOptions configure a StreamingText. @@ -34,7 +36,7 @@ func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.El if rate == 0 { rate = 80 } - k := Tokens(c) + k := core.Tokens(c) e := ui.Box(c).Label(text) st := ui.Local(e, "stream", func() streamingState { return streamingState{text: text, shown: float64(utf8.RuneCountInString(text)), at: c.Now()} @@ -46,7 +48,7 @@ func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.El } st.text = text now := c.Now() - if !opts.Smooth || Motion(c, time.Second) == 0 { + if !opts.Smooth || core.Motion(c, time.Second) == 0 { st.shown = total } else if st.shown < total { st.shown = min(total, st.shown+now.Sub(st.at).Seconds()*rate) diff --git a/streaming_text_test.go b/chat/streaming_text_test.go similarity index 96% rename from streaming_text_test.go rename to chat/streaming_text_test.go index a8499d3..1d50c91 100644 --- a/streaming_text_test.go +++ b/chat/streaming_text_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func streamingTester(text *string, opts StreamingTextOptions, reduce bool) *ui.Tester { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StreamingText(c, *text, opts) }, 400, 200) tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1}) diff --git a/suggestion_chips.go b/chat/suggestion_chips.go similarity index 88% rename from suggestion_chips.go rename to chat/suggestion_chips.go index c11de04..ce0e5f6 100644 --- a/suggestion_chips.go +++ b/chat/suggestion_chips.go @@ -1,13 +1,14 @@ -package mujica +package chat import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SuggestionChipsOptions configure SuggestionChips. @@ -33,8 +34,8 @@ func SuggestionChips(c *ui.Context, input *string, suggestions []string, opts Su if input == nil { panic("mujica: SuggestionChips needs an input") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) row := ui.Row(c).Wrap().Gap(d.Unit() * 2).FocusGroup(ui.Horizontal).Role(ui.RoleGroup).Label(opts.Label).Disabled(opts.Disabled) v := SuggestionChipsView{Element: row} icon := icons.Must("chevron-right") @@ -55,7 +56,7 @@ func SuggestionChips(c *ui.Context, input *string, suggestions []string, opts Su case b.Hovered(): b.Background(k.SurfaceHover) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { ui.Text(c, s).SingleLine() ui.Icon(c, icon).Size(14, 14).TextColor(k.TextMuted) diff --git a/suggestion_chips_test.go b/chat/suggestion_chips_test.go similarity index 95% rename from suggestion_chips_test.go rename to chat/suggestion_chips_test.go index 87eb259..a61434d 100644 --- a/suggestion_chips_test.go +++ b/chat/suggestion_chips_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var chipTexts = []string{"Plan a ball", "Write a sonnet", "Explain the minuet"} @@ -60,7 +62,7 @@ func TestSuggestionChipsRejectEmpty(t *testing.T) { }() in := "" ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SuggestionChips(c, &in, []string{""}, SuggestionChipsOptions{}) }, 100, 100, 1) } diff --git a/system_prompt_editor.go b/chat/system_prompt_editor.go similarity index 66% rename from system_prompt_editor.go rename to chat/system_prompt_editor.go index ffb0792..3ed05fb 100644 --- a/system_prompt_editor.go +++ b/chat/system_prompt_editor.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SystemPromptEditorOptions configure a SystemPromptEditor. @@ -41,8 +43,8 @@ func SystemPromptEditor(c *ui.Context, value *string, opts SystemPromptEditorOpt if opts.MaxLength < 0 { panic("mujica: SystemPromptEditor MaxLength is negative") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) blocked := opts.Disabled || opts.Saving col := ui.Column(c).Gap(d.Unit() * 2).Disabled(blocked) v := SystemPromptEditorView{Element: col} @@ -50,30 +52,30 @@ func SystemPromptEditor(c *ui.Context, value *string, opts SystemPromptEditorOpt over := opts.MaxLength > 0 && utf8.RuneCountInString(*value) > opts.MaxLength col.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "system_prompt_editor.title")).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1) + ui.Text(c, core.Msg(c, "system_prompt_editor.title")).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1) if dirty { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() { ui.Box(c).Size(8, 8).Draw(func(p *ui.Painter, r ui.Rect) { p.FillPath(internal.Diamond(r.X+4, r.Y+4, 8), k.Warning) }) - ui.Text(c, msg(c, "system_prompt_editor.unsaved")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Warning) + ui.Text(c, core.Msg(c, "system_prompt_editor.unsaved")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Warning) }) } }) - TextArea(c, value, TextAreaOptions{Label: msg(c, "system_prompt_editor.title"), MinHeight: 120, MaxHeight: 320, + input.TextArea(c, value, input.TextAreaOptions{Label: core.Msg(c, "system_prompt_editor.title"), MinHeight: 120, MaxHeight: 320, MaxLength: opts.MaxLength, Disabled: blocked}) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { // With MaxLength the TextArea shows "n / max" itself. count := "" if opts.MaxLength == 0 { - count = fmt.Sprintf(msg(c, "system_prompt_editor.count"), utf8.RuneCountInString(*value)) + count = fmt.Sprintf(core.Msg(c, "system_prompt_editor.count"), utf8.RuneCountInString(*value)) } - ui.Text(c, count).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) - if Button(c, msg(c, "system_prompt_editor.restore"), ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw"), + ui.Text(c, count).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) + if input.Button(c, core.Msg(c, "system_prompt_editor.restore"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("refresh-cw"), Disabled: blocked || *value == opts.Default}).Clicked() && !col.IsDisabled() { *value, v.Restored = opts.Default, true } - v.Save = Button(c, msg(c, "system_prompt_editor.save"), ButtonOptions{Loading: opts.Saving, Disabled: opts.Disabled || !dirty || over}).Clicked() + v.Save = input.Button(c, core.Msg(c, "system_prompt_editor.save"), input.ButtonOptions{Loading: opts.Saving, Disabled: opts.Disabled || !dirty || over}).Clicked() }) }) mods := ui.Ctrl diff --git a/system_prompt_editor_test.go b/chat/system_prompt_editor_test.go similarity index 99% rename from system_prompt_editor_test.go rename to chat/system_prompt_editor_test.go index ace4f4b..1adc948 100644 --- a/system_prompt_editor_test.go +++ b/chat/system_prompt_editor_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "testing" diff --git a/table_block.go b/chat/table_block.go similarity index 82% rename from table_block.go rename to chat/table_block.go index 55ccfc7..95022ef 100644 --- a/table_block.go +++ b/chat/table_block.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "log/slog" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TableBlockOptions configure a TableBlock. @@ -32,23 +33,23 @@ func TableBlock(c *ui.Context, header []string, rows [][]string, opts TableBlock panic("mujica: TableBlock row width differs from the header") } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r CopyResult r.Element = ui.Column(c).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface).Clip().MinWidth(0) copiedAt := ui.Local(r.Element, "copied", func() time.Time { return time.Time{} }) r.Element.Children(func() { ui.Row(c).Padding(0, u, 0, u*3).AlignItems(ui.Center).Background(k.SurfaceHover).Children(func() { - ui.Text(c, msg(c, "table_block.title")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) - icon, name := "copy", msg(c, "table_block.copy") + ui.Text(c, core.Msg(c, "table_block.title")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1) + icon, name := "copy", core.Msg(c, "table_block.copy") if left := codeBlockCopiedFor - c.Now().Sub(*copiedAt); left > 0 { - icon, name = "check", msg(c, "code_block.copied") + icon, name = "check", core.Msg(c, "code_block.copied") c.After(left) } - if iconAction(c, icons.Must(icon), name).Clicked() && !r.Element.IsDisabled() { + if core.IconAction(c, icons.Must(icon), name).Clicked() && !r.Element.IsDisabled() { c.WriteClipboard(tableBlockTSV(header, rows)) *copiedAt, r.copied = c.Now(), true - c.Announce(msg(c, "code_block.copied")) + c.Announce(core.Msg(c, "code_block.copied")) slog.Debug("mujica: table_block copy", "rows", len(rows)) } }) @@ -84,8 +85,8 @@ func tableBlockAlign(a []ui.Align, i int) ui.Align { } func tableBlockCell(c *ui.Context, s string, align ui.Align, head, last bool) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() cell := ui.Box(c).Padding(u*2, u*3).Role(ui.RoleCell) if !last { cell.BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border) diff --git a/table_block_test.go b/chat/table_block_test.go similarity index 92% rename from table_block_test.go rename to chat/table_block_test.go index c5bc804..13b2d75 100644 --- a/table_block_test.go +++ b/chat/table_block_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var ( @@ -15,7 +17,7 @@ func TestTableBlockCopiesTSVAndScrolls(t *testing.T) { copies := 0 var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(160).Children(func() { r := TableBlock(c, tableBlockHeader, tableBlockRows, TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}) w = r.Element.Bounds().W @@ -44,7 +46,7 @@ func TestTableBlockBadRows(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TableBlock(c, []string{"a", "b"}, [][]string{{"1"}}, TableBlockOptions{}) }, 100, 100) } diff --git a/thinking_block.go b/chat/thinking_block.go similarity index 64% rename from thinking_block.go rename to chat/thinking_block.go index e859cc7..d4d3921 100644 --- a/thinking_block.go +++ b/chat/thinking_block.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ThinkingBlockOptions configure a ThinkingBlock. @@ -20,28 +22,28 @@ type ThinkingBlockOptions struct { } // ThinkingBlock folds a model's reasoning under a header with its time; *open's first value sets the default. -func ThinkingBlock(c *ui.Context, open *bool, opts ThinkingBlockOptions, content func()) ChoiceResult { +func ThinkingBlock(c *ui.Context, open *bool, opts ThinkingBlockOptions, content func()) core.ChoiceResult { if opts.Elapsed < 0 || opts.Thinking && opts.Started.IsZero() { panic("mujica: ThinkingBlock needs Started while Thinking and a non-negative Elapsed") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() secs := int(opts.Elapsed.Round(time.Second).Seconds()) - title := fmt.Sprintf(msg(c, "thinking_block.done"), secs) + title := fmt.Sprintf(core.Msg(c, "thinking_block.done"), secs) if opts.Thinking { run := c.Now().Sub(opts.Started) secs = int(max(run, 0) / time.Second) - title = fmt.Sprintf(msg(c, "thinking_block.thinking"), secs) + title = fmt.Sprintf(core.Msg(c, "thinking_block.thinking"), secs) c.After(time.Second - run%time.Second) } - return Collapsible(c, open, CollapsibleOptions{Title: title, Trigger: func() { + return data.Collapsible(c, open, data.CollapsibleOptions{Title: title, Trigger: func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.TextMuted).Children(func() { if opts.Thinking { - Spinner(c, SpinnerOptions{Size: 14, Label: title}) + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: title}) } else { ui.Icon(c, icons.Must("brain")).Size(16, 16) } - ui.Text(c, title).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) }}, func() { ui.Column(c).Gap(u*2).Padding(0, 0, 0, u*3).BorderWidth(0, 0, 0, theme.BorderWidth).BorderColor(k.Border). diff --git a/thinking_block_test.go b/chat/thinking_block_test.go similarity index 92% rename from thinking_block_test.go rename to chat/thinking_block_test.go index 0688685..1afcba1 100644 --- a/thinking_block_test.go +++ b/chat/thinking_block_test.go @@ -1,17 +1,19 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestThinkingBlockFoldsAndTimes(t *testing.T) { open, thinking := false, true started := time.Now().Add(-5 * time.Second) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ThinkingBlock(c, &open, ThinkingBlockOptions{Thinking: thinking, Started: started, Elapsed: 12 * time.Second}, func() { ui.Text(c, "Weigh the guest list") }) @@ -38,7 +40,7 @@ func TestThinkingBlockFoldsAndTimes(t *testing.T) { func TestThinkingBlockDefaultOpen(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ThinkingBlock(c, &open, ThinkingBlockOptions{Elapsed: time.Second}, func() { ui.Text(c, "Notes") }) }, 400, 200) if !tt.HasText("Notes") { @@ -54,7 +56,7 @@ func TestThinkingBlockNeedsStart(t *testing.T) { }() open := false ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ThinkingBlock(c, &open, ThinkingBlockOptions{Thinking: true}, func() {}) }, 100, 100) } diff --git a/thinking_indicator.go b/chat/thinking_indicator.go similarity index 78% rename from thinking_indicator.go rename to chat/thinking_indicator.go index aa1f61d..4693938 100644 --- a/thinking_indicator.go +++ b/chat/thinking_indicator.go @@ -1,12 +1,13 @@ -package mujica +package chat import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ThinkingIndicatorOptions configure a ThinkingIndicator. @@ -17,11 +18,11 @@ type ThinkingIndicatorOptions struct { // ThinkingIndicator shows rising diamonds and a phase, still when reduced. func ThinkingIndicator(c *ui.Context, opts ThinkingIndicatorOptions) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) phase := opts.Phase if phase == "" { - phase = msg(c, "thinking_indicator.thinking") + phase = core.Msg(c, "thinking_indicator.thinking") } still := c.Preferences().ReduceMotion row := ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).MinHeight(d.ControlHeight()).Role(ui.RoleStatus).Label(phase) @@ -49,7 +50,7 @@ func ThinkingIndicator(c *ui.Context, opts ThinkingIndicatorOptions) *ui.Element if still { text += "…" } - ui.Text(c, text).FontSize(FontSize(c, theme.BodySize)).TextColor(k.TextMuted).Italic() + ui.Text(c, text).FontSize(core.FontSize(c, theme.BodySize)).TextColor(k.TextMuted).Italic() }) return row } diff --git a/thinking_indicator_test.go b/chat/thinking_indicator_test.go similarity index 99% rename from thinking_indicator_test.go rename to chat/thinking_indicator_test.go index 6fb8e22..4885ac5 100644 --- a/thinking_indicator_test.go +++ b/chat/thinking_indicator_test.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "bytes" diff --git a/token_counter.go b/chat/token_counter.go similarity index 68% rename from token_counter.go rename to chat/token_counter.go index fdcd918..577f8b2 100644 --- a/token_counter.go +++ b/chat/token_counter.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // tokenWarnShare sets the warning threshold. @@ -18,14 +19,14 @@ func TokenCounter(c *ui.Context, used, limit int) *ui.Element { if used < 0 || limit <= 0 { panic("mujica: TokenCounter needs used >= 0 and limit > 0") } - k := Tokens(c) - u := Density(c).Unit() - label := fmt.Sprintf(msg(c, "token_counter.label"), tokenThousands(used), tokenThousands(limit)) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := fmt.Sprintf(core.Msg(c, "token_counter.label"), tokenThousands(used), tokenThousands(limit)) color, icon := k.TextMuted, "" switch { case used > limit: color, icon = k.Danger, "circle-alert" - label += ", " + fmt.Sprintf(msg(c, "token_counter.over"), tokenThousands(used-limit)) + label += ", " + fmt.Sprintf(core.Msg(c, "token_counter.over"), tokenThousands(used-limit)) case float64(used) >= tokenWarnShare*float64(limit): color, icon = k.Warning, "triangle-alert" } @@ -34,7 +35,7 @@ func TokenCounter(c *ui.Context, used, limit int) *ui.Element { if icon != "" { ui.Icon(c, icons.Must(icon)).Size(12, 12) } - ui.Text(c, "≈ "+tokenThousands(used)+" / "+tokenThousands(limit)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, "≈ "+tokenThousands(used)+" / "+tokenThousands(limit)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) return row } diff --git a/token_counter_test.go b/chat/token_counter_test.go similarity index 91% rename from token_counter_test.go rename to chat/token_counter_test.go index 1e62f71..b6517b5 100644 --- a/token_counter_test.go +++ b/chat/token_counter_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTokenCounterLevels(t *testing.T) { @@ -17,7 +19,7 @@ func TestTokenCounterLevels(t *testing.T) { {9100, "≈ 9,100 / 8,000", "About 9,100 of 8,000 tokens, 1,100 over the limit"}, } { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TokenCounter(c, tc.used, 8000) }, 300, 60) if !tt.HasText(tc.text) { @@ -39,7 +41,7 @@ func TestTokenCounterBadLimit(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TokenCounter(c, 1, 0) }, 100, 100) } diff --git a/tool_toggle_menu.go b/chat/tool_toggle_menu.go similarity index 70% rename from tool_toggle_menu.go rename to chat/tool_toggle_menu.go index fc8c8a2..511a588 100644 --- a/tool_toggle_menu.go +++ b/chat/tool_toggle_menu.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,9 +6,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolItem is one tool a ToolToggleMenu turns on or off. @@ -45,9 +48,9 @@ func ToolToggleMenu(c *ui.Context, tools []ToolItem) ToolToggleResult { on++ } } - k := Tokens(c) - u := Density(c).Unit() - label := fmt.Sprintf(msg(c, "tool_toggle_menu.count"), on) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := fmt.Sprintf(core.Msg(c, "tool_toggle_menu.count"), on) var r ToolToggleResult b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Padding(0, u*2).Gap(u).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted) r.Element = b @@ -73,7 +76,7 @@ func ToolToggleMenu(c *ui.Context, tools []ToolItem) ToolToggleResult { if disabled { *open = false } - Popover(c, b, open, PopoverOptions{Title: msg(c, "tool_toggle_menu.label"), Width: 320}, func() { + overlay.Popover(c, b, open, overlay.PopoverOptions{Title: core.Msg(c, "tool_toggle_menu.label"), Width: 320}, func() { for _, t := range tools { toolToggleRow(c, t, &r) } @@ -86,16 +89,16 @@ func ToolToggleMenu(c *ui.Context, tools []ToolItem) ToolToggleResult { // toolToggleRow displays a switch or authorization action. func toolToggleRow(c *ui.Context, t ToolItem, r *ToolToggleResult) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) locked := t.NeedsAuth && !t.Authorized ui.Row(c).Key(t.ID).Gap(u * 3).AlignItems(ui.Center).Children(func() { ui.Column(c).Grow(1).MinWidth(0).Children(func() { - ui.Row(c).Gap(u).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Row(c).Gap(u).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Children(func() { if locked { ui.Text(c, t.Label).FontWeight(600).SingleLine() - } else if Switch(c, t.On, t.Label, SwitchOptions{}).Changed() { + } else if input.Switch(c, t.On, t.Label, input.SwitchOptions{}).Changed() { r.changed = t.ID } if t.NeedsAuth { @@ -103,19 +106,19 @@ func toolToggleRow(c *ui.Context, t ToolItem, r *ToolToggleResult) { if t.Authorized { icon = "shield-check" } - ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(k.Warning).Label(msg(c, "tool_toggle_menu.needs_auth")) + ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(k.Warning).Label(core.Msg(c, "tool_toggle_menu.needs_auth")) } }) if t.Description != "" { ui.Text(c, t.Description).FontSize(small).TextColor(k.TextMuted) } if locked { - ui.Text(c, msg(c, "tool_toggle_menu.needs_auth")).FontSize(small).TextColor(k.Warning) + ui.Text(c, core.Msg(c, "tool_toggle_menu.needs_auth")).FontSize(small).TextColor(k.Warning) } }) if locked { - full := fmt.Sprintf(msg(c, "tool_toggle_menu.authorize"), t.Label) - if Button(c, msg(c, "tool_toggle_menu.authorize_short"), ButtonOptions{Variant: Outline, Label: full}).Clicked() { + full := fmt.Sprintf(core.Msg(c, "tool_toggle_menu.authorize"), t.Label) + if input.Button(c, core.Msg(c, "tool_toggle_menu.authorize_short"), input.ButtonOptions{Variant: input.Outline, Label: full}).Clicked() { r.authorize = t.ID } } diff --git a/tool_toggle_menu_test.go b/chat/tool_toggle_menu_test.go similarity index 96% rename from tool_toggle_menu_test.go rename to chat/tool_toggle_menu_test.go index 74e69be..8d61292 100644 --- a/tool_toggle_menu_test.go +++ b/chat/tool_toggle_menu_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // toolMenuApp holds tools and what the menu reported. @@ -15,7 +17,7 @@ type toolMenuApp struct { } func (a *toolMenuApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).AlignItems(ui.Start).Disabled(a.off).Children(func() { r := ToolToggleMenu(c, []ToolItem{ {ID: "web", Label: "Web search", Description: "Search the open web", On: &a.web}, diff --git a/typing_indicator.go b/chat/typing_indicator.go similarity index 75% rename from typing_indicator.go rename to chat/typing_indicator.go index 8657c60..6c6d756 100644 --- a/typing_indicator.go +++ b/chat/typing_indicator.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // typingPeriod is one cycle of the dots, in milliseconds. @@ -18,15 +19,15 @@ func TypingIndicator(c *ui.Context, name string) *ui.Element { if name == "" { panic("mujica: TypingIndicator needs a name") } - k := Tokens(c) - u := Density(c).Unit() - label := fmt.Sprintf(msg(c, "typing_indicator.label"), name) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := fmt.Sprintf(core.Msg(c, "typing_indicator.label"), name) still := c.Preferences().ReduceMotion row := ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(u, u*3).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleStatus).Label(label).AlignSelf(ui.Start) row.Children(func() { if still { - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() return } ui.Box(c).Size(30, 12).Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/typing_indicator_test.go b/chat/typing_indicator_test.go similarity index 90% rename from typing_indicator_test.go rename to chat/typing_indicator_test.go index 963d98f..8467292 100644 --- a/typing_indicator_test.go +++ b/chat/typing_indicator_test.go @@ -1,15 +1,17 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTypingIndicatorMotionAndStatic(t *testing.T) { for _, reduce := range []bool{false, true} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TypingIndicator(c, "Herald") }, 300, 100) tt.SetPreferences(ui.Preferences{ReduceMotion: reduce, TextScale: 1}) @@ -37,7 +39,7 @@ func TestTypingIndicatorNeedsName(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TypingIndicator(c, "") }, 100, 100) } diff --git a/voice_input_button.go b/chat/voice_input_button.go similarity index 73% rename from voice_input_button.go rename to chat/voice_input_button.go index 4b6b7bc..718dacc 100644 --- a/voice_input_button.go +++ b/chat/voice_input_button.go @@ -1,4 +1,4 @@ -package mujica +package chat import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // VoiceInputOptions describe the caller's recording. @@ -24,25 +25,25 @@ type VoiceInputOptions struct { // VoiceInputButton requests recording changes and displays elapsed time. // Clicked requests recording start or stop for the current state. -func VoiceInputButton(c *ui.Context, opts VoiceInputOptions) ChoiceResult { +func VoiceInputButton(c *ui.Context, opts VoiceInputOptions) core.ChoiceResult { if opts.Recording && (opts.Started.IsZero() || opts.Denied) { panic("mujica: VoiceInputButton recording needs Started and no Denied") } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult r.Element = ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Shrink(0).Disabled(opts.Disabled) r.Element.Children(func() { elapsed := "" - label := msg(c, "voice_input_button.start") + label := core.Msg(c, "voice_input_button.start") icon := "mic" if opts.Recording { d := max(c.Now().Sub(opts.Started), 0) elapsed = audioClock(d.Truncate(time.Second)) - label, icon = fmt.Sprintf(msg(c, "voice_input_button.stop"), elapsed), "square" + label, icon = fmt.Sprintf(core.Msg(c, "voice_input_button.stop"), elapsed), "square" } - h := Density(c).ControlHeight() - b := ui.ButtonBase(c).Size(h, h).Radius(h / 2).Label(label).Tooltip(label).Transition(colorFade) + h := core.Density(c).ControlHeight() + b := ui.ButtonBase(c).Size(h, h).Radius(h / 2).Label(label).Tooltip(label).Transition(core.ColorFade) internal.Ring(b, k.Focus, h/2) switch { case opts.Recording: @@ -56,7 +57,7 @@ func VoiceInputButton(c *ui.Context, opts VoiceInputOptions) ChoiceResult { } b.Children(func() { ui.Icon(c, icons.Must(icon)).Size(16, 16) }) if b.Clicked() && !b.IsDisabled() { - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: voice_input_button press", "recording", opts.Recording) } switch { @@ -71,7 +72,7 @@ func VoiceInputButton(c *ui.Context, opts VoiceInputOptions) ChoiceResult { case opts.Denied: ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(k.Danger).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("mic-off")).Size(14, 14) - ui.Text(c, msg(c, "voice_input_button.denied")).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, core.Msg(c, "voice_input_button.denied")).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) } }) diff --git a/voice_input_button_test.go b/chat/voice_input_button_test.go similarity index 92% rename from voice_input_button_test.go rename to chat/voice_input_button_test.go index bf94b0c..c29658a 100644 --- a/voice_input_button_test.go +++ b/chat/voice_input_button_test.go @@ -1,17 +1,19 @@ -package mujica +package chat import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestVoiceInputButtonStartStopAndTime(t *testing.T) { var opts VoiceInputOptions presses := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if VoiceInputButton(c, opts).Changed() { presses++ if opts.Recording { @@ -37,7 +39,7 @@ func TestVoiceInputButtonDeniedAndDisabled(t *testing.T) { opts := VoiceInputOptions{Denied: true} presses := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if VoiceInputButton(c, opts).Changed() { presses++ } @@ -60,7 +62,7 @@ func TestVoiceInputButtonRecordingNeedsStart(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) VoiceInputButton(c, VoiceInputOptions{Recording: true}) }, 100, 100) } diff --git a/voice_waveform.go b/chat/voice_waveform.go similarity index 84% rename from voice_waveform.go rename to chat/voice_waveform.go index 754a88d..c1af4f9 100644 --- a/voice_waveform.go +++ b/chat/voice_waveform.go @@ -1,10 +1,11 @@ -package mujica +package chat import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // VoiceWaveformOptions configure a VoiceWaveform. @@ -37,13 +38,13 @@ func VoiceWaveform(c *ui.Context, levels []float32, opts VoiceWaveformOptions) * if w == 0 { w = 200 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() silent := len(levels) >= need for _, v := range levels[max(0, len(levels)-need):] { silent = silent && v < silence } - row := ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleMeter).Label(msg(c, "voice_waveform.label")) + row := ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleMeter).Label(core.Msg(c, "voice_waveform.label")) row.Children(func() { ui.Box(c).Size(w, 28).Shrink(0).Draw(func(p *ui.Painter, b ui.Rect) { n := int((b.W + audioBarGap) / (audioBarWidth + audioBarGap)) @@ -67,7 +68,7 @@ func VoiceWaveform(c *ui.Context, levels []float32, opts VoiceWaveformOptions) * if silent { ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(k.Warning).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("mic-off")).Size(14, 14) - ui.Text(c, msg(c, "voice_waveform.silent")).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, core.Msg(c, "voice_waveform.silent")).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) } }) diff --git a/voice_waveform_test.go b/chat/voice_waveform_test.go similarity index 92% rename from voice_waveform_test.go rename to chat/voice_waveform_test.go index 3de0f8e..2762301 100644 --- a/voice_waveform_test.go +++ b/chat/voice_waveform_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func voiceLevels(n int, v float32) []float32 { @@ -17,7 +19,7 @@ func voiceLevels(n int, v float32) []float32 { func TestVoiceWaveformSilenceHint(t *testing.T) { levels := append(voiceLevels(30, 0.6), voiceLevels(10, 0.01)...) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) VoiceWaveform(c, levels, VoiceWaveformOptions{SilentFor: 12}) }, 400, 100) if tt.HasText("No sound detected") { @@ -42,7 +44,7 @@ func TestVoiceWaveformBadLevels(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) VoiceWaveform(c, l, VoiceWaveformOptions{}) }, 100, 100) }() diff --git a/welcome_screen.go b/chat/welcome_screen.go similarity index 81% rename from welcome_screen.go rename to chat/welcome_screen.go index cd0d46e..061599d 100644 --- a/welcome_screen.go +++ b/chat/welcome_screen.go @@ -1,10 +1,11 @@ -package mujica +package chat import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WelcomeScreenOptions configure a WelcomeScreen. Greeting is required. @@ -22,13 +23,13 @@ func WelcomeScreen(c *ui.Context, opts WelcomeScreenOptions) CapabilityCardsView if opts.Greeting == "" { panic("mujica: WelcomeScreen needs a Greeting") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var v CapabilityCardsView col := ui.Column(c).FillWidth().MaxWidth(880).AlignSelf(ui.Center).AlignItems(ui.Center).Gap(d.Unit()*4). Padding(d.Unit()*8, d.Unit()*4).Disabled(opts.Disabled) col.Children(func() { - ui.Text(c, opts.Greeting).Font(theme.SerifFont).FontSize(FontSize(c, theme.H1Size)).TextColor(k.Text). + ui.Text(c, opts.Greeting).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H1Size)).TextColor(k.Text). TextAlign(ui.Center).Role(ui.RoleText) ui.Box(c).Width(220).Height(9).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 8, 6, k.Ornament) diff --git a/welcome_screen_test.go b/chat/welcome_screen_test.go similarity index 95% rename from welcome_screen_test.go rename to chat/welcome_screen_test.go index febf2ae..ebe66d3 100644 --- a/welcome_screen_test.go +++ b/chat/welcome_screen_test.go @@ -1,9 +1,11 @@ -package mujica +package chat import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestWelcomeScreenGreetsAndChooses(t *testing.T) { @@ -52,7 +54,7 @@ func TestWelcomeScreenNeedsGreeting(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) WelcomeScreen(c, WelcomeScreenOptions{}) }, 100, 100, 1) } diff --git a/chat_empty_state.go b/chat_empty_state.go deleted file mode 100644 index a386f0a..0000000 --- a/chat_empty_state.go +++ /dev/null @@ -1,58 +0,0 @@ -package mujica - -import ( - "cmp" - - "github.com/egoist/mygo/ui" - - "mujicaui/icons" -) - -// ChatEmptyKind is why a chat is empty; zero is ChatEmptyNew. -type ChatEmptyKind int - -const ( - // ChatEmptyNew: a new conversation with no messages. - ChatEmptyNew ChatEmptyKind = iota - // ChatEmptyNoResults: a conversation search matched nothing. - ChatEmptyNoResults - // ChatEmptyOffline: the connection is down. - ChatEmptyOffline -) - -// ChatEmptyStateOptions configure a ChatEmptyState; empty texts use built-in copy. -type ChatEmptyStateOptions struct { - Kind ChatEmptyKind - // Query, for ChatEmptyNoResults, is named in the description. - Query string - Title string - Description string - // Action replaces the kind's built-in button text. - Action string -} - -// ChatEmptyState explains an empty chat and offers the next step. -func ChatEmptyState(c *ui.Context, opts ChatEmptyStateOptions) EmptyStateResult { - switch opts.Kind { - case ChatEmptyNew: - return EmptyState(c, EmptyStateOptions{ - Title: cmp.Or(opts.Title, msg(c, "chat_empty_state.new.title")), - Description: cmp.Or(opts.Description, msg(c, "chat_empty_state.new.description")), - Icon: icons.Must("star"), - Action: cmp.Or(opts.Action, msg(c, "chat_empty_state.new.action")), - }) - case ChatEmptyNoResults: - return EmptyState(c, EmptyStateOptions{ - Kind: EmptyNoResults, Query: opts.Query, Title: opts.Title, Description: opts.Description, - Action: cmp.Or(opts.Action, msg(c, "chat_empty_state.no_results.action")), - }) - case ChatEmptyOffline: - return EmptyState(c, EmptyStateOptions{ - Title: cmp.Or(opts.Title, msg(c, "chat_empty_state.offline.title")), - Description: cmp.Or(opts.Description, msg(c, "chat_empty_state.offline.description")), - Icon: icons.Must("wifi-off"), - Action: cmp.Or(opts.Action, msg(c, "chat_empty_state.offline.action")), - }) - } - panic("mujica: ChatEmptyState Kind out of range") -} diff --git a/breakpoint_list.go b/code/breakpoint_list.go similarity index 70% rename from breakpoint_list.go rename to code/breakpoint_list.go index 1aab2b1..f7e484a 100644 --- a/breakpoint_list.go +++ b/code/breakpoint_list.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,9 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Breakpoint stops at File:Line when Condition holds. @@ -46,11 +49,11 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions } seen[b.ID] = true } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() var res BreakpointListResult - res.Element = codePanel(c).Disabled(opts.Disabled).Label(msg(c, "breakpoint_list.label")) + res.Element = CodePanel(c).Disabled(opts.Disabled).Label(core.Msg(c, "breakpoint_list.label")) edit := ui.Local(res.Element, "breakpoint_list.edit", func() breakpointEdit { return breakpointEdit{} }) disabled := res.Element.IsDisabled() if disabled { @@ -58,16 +61,16 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions } res.Element.Children(func() { if len(*bps) == 0 { - codeCaption(c, msg(c, "breakpoint_list.empty")).Padding(u * 4) + CodeCaption(c, core.Msg(c, "breakpoint_list.empty")).Padding(u * 4) return } - ScrollArea(c, nil, ScrollAreaOptions{}, func() { + layout.ScrollArea(c, nil, layout.ScrollAreaOptions{}, func() { for i := range *bps { b := &(*bps)[i] ui.Column(c).Key(b.ID).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { ui.Row(c).MinHeight(d.ControlHeight()).Gap(u * 2).AlignItems(ui.Center).Children(func() { on := b.Enabled - Checkbox(c, &on, "", CheckboxOptions{Disabled: opts.Disabled}).Label(fmt.Sprintf(msg(c, "breakpoint_list.enable"), path.Base(b.File), b.Line)) + input.Checkbox(c, &on, "", input.CheckboxOptions{Disabled: opts.Disabled}).Label(fmt.Sprintf(core.Msg(c, "breakpoint_list.enable"), path.Base(b.File), b.Line)) if on != b.Enabled && !disabled { b.Enabled, res.Toggled = on, b.ID slog.Debug("mujica: breakpoint_list toggle", "id", b.ID, "enabled", on) @@ -76,13 +79,13 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions res.Opened = b.ID slog.Debug("mujica: breakpoint_list open", "id", b.ID) } - if iconAction(c, icons.Must("funnel"), fmt.Sprintf(msg(c, "breakpoint_list.edit"), path.Base(b.File), b.Line)).Clicked() && !disabled { + if core.IconAction(c, icons.Must("funnel"), fmt.Sprintf(core.Msg(c, "breakpoint_list.edit"), path.Base(b.File), b.Line)).Clicked() && !disabled { *edit = breakpointEdit{b.ID, b.Condition} } }) switch { case edit.id == b.ID: - in := TextInput(c, &edit.draft, TextInputOptions{Placeholder: msg(c, "breakpoint_list.placeholder"), Label: msg(c, "breakpoint_list.condition")}) + in := input.TextInput(c, &edit.draft, input.TextInputOptions{Placeholder: core.Msg(c, "breakpoint_list.placeholder"), Label: core.Msg(c, "breakpoint_list.condition")}) in.Input.AutoFocus() if in.Input.Shortcut(0, ui.KeyEscape) { edit.id = "" @@ -91,7 +94,7 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions slog.Debug("mujica: breakpoint_list condition", "id", b.ID) } case b.Condition != "": - codeMono(c, fmt.Sprintf(msg(c, "breakpoint_list.when"), b.Condition)).TextColor(k.Warning).Padding(0, 0, 0, 28) + CodeMono(c, fmt.Sprintf(core.Msg(c, "breakpoint_list.when"), b.Condition)).TextColor(k.Warning).Padding(0, 0, 0, 28) } }) } @@ -102,8 +105,8 @@ func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions // breakpointLocation is a button opening the location. func breakpointLocation(c *ui.Context, b Breakpoint) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) e := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(theme.MinHitSize).Justify(ui.Start).Gap(d.Unit()*2).Padding(0, d.Unit()). Radius(theme.ControlRadius).Label(fmt.Sprintf("%s:%d", b.File, b.Line)) internal.Ring(e, k.Focus, theme.ControlRadius) @@ -121,6 +124,6 @@ func breakpointLocation(c *ui.Context, b Breakpoint) *ui.Element { dot.Background(k.Danger) } ui.Text(c, path.Base(b.File)).TextColor(fg).SingleLine() - codeCaption(c, fmt.Sprintf("%s:%d", path.Dir(b.File), b.Line)).MinWidth(0) + CodeCaption(c, fmt.Sprintf("%s:%d", path.Dir(b.File), b.Line)).MinWidth(0) }) } diff --git a/breakpoint_list_test.go b/code/breakpoint_list_test.go similarity index 97% rename from breakpoint_list_test.go rename to code/breakpoint_list_test.go index 0b02a10..f725f96 100644 --- a/breakpoint_list_test.go +++ b/code/breakpoint_list_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type breakpointProbe struct { @@ -13,7 +15,7 @@ type breakpointProbe struct { } func (p *breakpointProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := BreakpointList(c, &p.bps, BreakpointListOptions{Disabled: p.disabled}) r.Element.Height(300) for _, e := range []struct { diff --git a/call_stack.go b/code/call_stack.go similarity index 80% rename from call_stack.go rename to code/call_stack.go index 4a060d5..0ef030f 100644 --- a/call_stack.go +++ b/code/call_stack.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // StackFrame is one call of a thread's stack, innermost first. @@ -44,7 +45,7 @@ type CallStackOptions struct { } // CallStack lists frames by thread; choosing sets Current. -func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts CallStackOptions) ChoiceResult { +func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts CallStackOptions) core.ChoiceResult { if s == nil { panic("mujica: CallStack needs a state") } @@ -66,20 +67,20 @@ func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts Cal if !s.started || s.seen != s.Current || s.folds != s.Collapsed.Len() || s.selected >= len(s.flat) { s.started, s.seen, s.selected, s.folds = true, s.Current, at, s.Collapsed.Len() } - d := Density(c) - var res ChoiceResult - res.Element = codePanel(c).Disabled(opts.Disabled) + d := core.Density(c) + var res core.ChoiceResult + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { if len(threads) == 0 { - codeCaption(c, msg(c, "call_stack.empty")).Padding(d.Unit() * 4) + CodeCaption(c, core.Msg(c, "call_stack.empty")).Padding(d.Unit() * 4) return } was := s.selected s.rows.Selected = &s.selected s.rows.Key = func(i int) any { return s.flat[i] } s.rows.Label = func(i int) string { return callLabel(threads, s.flat[i]) } - list := codeRows(c, &s.rows, len(s.flat), func(i int) { callRowView(c, threads, s, s.flat[i], disabled) }) + list := CodeRows(c, &s.rows, len(s.flat), func(i int) { callRowView(c, threads, s, s.flat[i], disabled) }) if disabled { s.selected = was return @@ -92,7 +93,7 @@ func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts Cal switch { case r.frame >= 0 && s.selected != was: s.Current = t.Frames[r.frame].ID - s.seen, res.changed = s.Current, true + s.seen, res = s.Current, core.NewChoiceResult(res.Element, true) slog.Debug("mujica: call_stack frame", "thread", t.ID, "depth", r.frame) case r.frame < 0 && (list.Submitted() || !s.Collapsed.Has(t.ID) && list.Shortcut(0, ui.KeyLeft) || s.Collapsed.Has(t.ID) && list.Shortcut(0, ui.KeyRight)): @@ -127,14 +128,14 @@ func callRowView(c *ui.Context, threads []DebugThread, s *CallStackState, r call if s.Collapsed.Has(t.ID) { chev, key = "chevron-right", "search_panel.unfold" } - if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), t.Name)).Clicked() && !disabled { + if core.IconAction(c, icons.Must(chev), fmt.Sprintf(core.Msg(c, key), t.Name)).Clicked() && !disabled { callFold(s, t.ID) } ui.Text(c, t.Name).FontWeight(600).SingleLine().Grow(1).MinWidth(0) - codeCaption(c, fmt.Sprintf(msg(c, "call_stack.frames"), len(t.Frames))).Shrink(0) + CodeCaption(c, fmt.Sprintf(core.Msg(c, "call_stack.frames"), len(t.Frames))).Shrink(0) return } f := t.Frames[r.frame] - codeMono(c, f.Name).Grow(1).MinWidth(0).Margin(0, 0, 0, 20) - codeCaption(c, fmt.Sprintf("%s:%d", path.Base(f.File), f.Line)).Shrink(0) + CodeMono(c, f.Name).Grow(1).MinWidth(0).Margin(0, 0, 0, 20) + CodeCaption(c, fmt.Sprintf("%s:%d", path.Base(f.File), f.Line)).Shrink(0) } diff --git a/call_stack_test.go b/code/call_stack_test.go similarity index 97% rename from call_stack_test.go rename to code/call_stack_test.go index ff3610e..a018adc 100644 --- a/call_stack_test.go +++ b/code/call_stack_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func callStackSample() []DebugThread { @@ -25,7 +27,7 @@ type callProbe struct { } func (p *callProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CallStack(c, p.threads, &p.s, CallStackOptions{Disabled: p.disabled}) r.Element.Height(320) if r.Changed() { diff --git a/code_shared.go b/code/code_shared.go similarity index 61% rename from code_shared.go rename to code/code_shared.go index b60a254..b4ca2c5 100644 --- a/code_shared.go +++ b/code/code_shared.go @@ -1,34 +1,36 @@ -package mujica +package code import ( "strings" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) -// codeSize is the design size of code and terminal text. -const codeSize float32 = 13 +// CodeSize is the design size of code and terminal text. +const CodeSize float32 = 13 // codeFont is the scaled monospace code font. func codeFont(c *ui.Context) ui.Font { - return ui.Font{Family: theme.MonoFont, Size: FontSize(c, codeSize)} + return ui.Font{Family: theme.MonoFont, Size: core.FontSize(c, CodeSize)} } -// codeMono is a single-line monospace text. -func codeMono(c *ui.Context, s string) *ui.Element { - return ui.Text(c, s).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine() +// CodeMono is a single-line monospace text. +func CodeMono(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)).SingleLine() } -// codeCaption is muted caption text. -func codeCaption(c *ui.Context, s string) *ui.Element { - return ui.Text(c, s).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted).SingleLine() +// CodeCaption is muted caption text. +func CodeCaption(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted).SingleLine() } -// codeTokenColor colors a syntax token kind. -func codeTokenColor(k theme.Tokens, kind internal.TokenKind) ui.Color { +// CodeTokenColor colors a syntax token kind. +func CodeTokenColor(k theme.Tokens, kind internal.TokenKind) ui.Color { switch kind { case internal.TokKeyword: return k.AccentText @@ -70,9 +72,9 @@ func codeAnsiColor(k theme.Tokens, a internal.AnsiColor) ui.Color { return ui.Color{} } -// codeAnsiSpans turns ANSI text into themed spans. -func codeAnsiSpans(c *ui.Context, line string) []ui.Span { - k := Tokens(c) +// CodeAnsiSpans turns ANSI text into themed spans. +func CodeAnsiSpans(c *ui.Context, line string) []ui.Span { + k := core.Tokens(c) parts, _ := internal.ParseAnsi(line, internal.AnsiStyle{}) spans := make([]ui.Span, 0, len(parts)) for _, p := range parts { @@ -88,12 +90,12 @@ func codeAnsiSpans(c *ui.Context, line string) []ui.Span { return spans } -// codeMatchSpans marks each query match in text. -func codeMatchSpans(c *ui.Context, text, query string) []ui.Span { +// CodeMatchSpans marks each query match in text. +func CodeMatchSpans(c *ui.Context, text, query string) []ui.Span { if query == "" { return []ui.Span{{Text: text}} } - k := Tokens(c) + k := core.Tokens(c) lower, q := strings.ToLower(text), strings.ToLower(query) var spans []ui.Span for { @@ -113,23 +115,23 @@ func codeMatchSpans(c *ui.Context, text, query string) []ui.Span { return spans } -// codePanel is a framed surface holding a code tool. -func codePanel(c *ui.Context) *ui.Element { - k := Tokens(c) +// CodePanel is a framed surface holding a code tool. +func CodePanel(c *ui.Context) *ui.Element { + k := core.Tokens(c) return ui.Column(c).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Clip() } -// codeHeader is a panel's top strip of tools. -func codeHeader(c *ui.Context, fn func()) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() +// CodeHeader is a panel's top strip of tools. +func CodeHeader(c *ui.Context, fn func()) *ui.Element { + k := core.Tokens(c) + u := core.Density(c).Unit() return ui.Row(c).Shrink(0).MinHeight(theme.MinHitSize+2*u).Padding(u, u*2).Gap(u*2).AlignItems(ui.Center). BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(fn) } -// codeSelectMark draws a chosen row's diamond. -func codeSelectMark(c *ui.Context, on bool) { - k := Tokens(c) +// CodeSelectMark draws a chosen row's diamond. +func CodeSelectMark(c *ui.Context, on bool) { + k := core.Tokens(c) ui.Box(c).Size(8, 8).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { if on { p.FillPath(internal.Diamond(internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2), r.W), k.AccentText) @@ -137,32 +139,32 @@ func codeSelectMark(c *ui.Context, on bool) { }) } -// codeRows builds a growing virtual list with marks. -func codeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) *ui.Element { - d := Density(c) - restore := dataSelectTheme(c, 0) +// CodeRows builds a growing virtual list with marks. +func CodeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) *ui.Element { + d := core.Density(c) + restore := data.DataSelectTheme(c, 0) defer restore() e := ui.List(c, s, n, func(i int) { ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - codeSelectMark(c, s.Selected != nil && *s.Selected == i) + CodeSelectMark(c, s.Selected != nil && *s.Selected == i) row(i) }) }) - k := Tokens(c) + k := core.Tokens(c) internal.Ring(e, k.Focus, theme.ControlRadius) return e.Grow(1).MinHeight(d.ControlHeight() * 3) } -// codeBadge is a kind letter in a framed square. -func codeBadge(c *ui.Context, letter string, col ui.Color) { +// CodeBadge is a kind letter in a framed square. +func CodeBadge(c *ui.Context, letter string, col ui.Color) { ui.Box(c).Size(18, 18).Shrink(0).Center().Radius(theme.ControlRadius).Border(theme.BorderWidth, col). Role(ui.RoleNone).Children(func() { - ui.Text(c, letter).Font(theme.MonoFont).FontSize(FontSize(c, 11)).FontWeight(700).TextColor(col) + ui.Text(c, letter).Font(theme.MonoFont).FontSize(core.FontSize(c, 11)).FontWeight(700).TextColor(col) }) } // codeTextWidth returns the width of s in the code font. func codeTextWidth(c *ui.Context, s string) float32 { - w, _ := c.MeasureText(0, ui.Span{Text: s, Font: theme.MonoFont, Size: FontSize(c, codeSize)}) + w, _ := c.MeasureText(0, ui.Span{Text: s, Font: theme.MonoFont, Size: core.FontSize(c, CodeSize)}) return w } diff --git a/code_viewer.go b/code/code_viewer.go similarity index 83% rename from code_viewer.go rename to code/code_viewer.go index cfa9001..2df569f 100644 --- a/code_viewer.go +++ b/code/code_viewer.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -9,9 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CodeViewerOptions configure a CodeViewer. @@ -34,7 +36,7 @@ type CodeViewerState struct { shown []int inBlock []bool ends []int - data *string + Data *string n int lang string folds int @@ -78,7 +80,7 @@ func (s *CodeViewerState) GoTo(line int) { } // CodeViewer is a read-only, foldable, virtualized code view. -func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeViewerOptions) ChoiceResult { +func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeViewerOptions) core.ChoiceResult { if s == nil { panic("mujica: CodeViewer needs a state") } @@ -86,13 +88,13 @@ func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeView s.started, s.cursor = true, -1 } codeViewerPrepare(lines, s, opts.Language) - k := Tokens(c) - d := Density(c) - var res ChoiceResult - res.Element = codePanel(c).Disabled(opts.Disabled) + k := core.Tokens(c) + d := core.Density(c) + var res core.ChoiceResult + res.Element = CodePanel(c).Disabled(opts.Disabled) res.Element.Children(func() { if len(lines) == 0 { - codeCaption(c, msg(c, "code_viewer.empty")).Padding(d.Unit() * 4) + CodeCaption(c, core.Msg(c, "code_viewer.empty")).Padding(d.Unit() * 4) return } digits := len(fmt.Sprint(len(lines))) @@ -101,27 +103,27 @@ func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeView s.rows.Selection = &s.Selection s.rows.Key = func(i int) any { return s.shown[i] } s.rows.Label = func(i int) string { return fmt.Sprintf("%d %s", s.shown[i]+1, lines[s.shown[i]]) } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(s.shown), func(i int) { li := s.shown[i] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2, 0, 0).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - codeSelectMark(c, i == s.cursor) + CodeSelectMark(c, i == s.cursor) if s.ends[li] > li { - label := msg(c, "code_viewer.fold") + label := core.Msg(c, "code_viewer.fold") if s.Folded.Has(li) { - label = msg(c, "code_viewer.unfold") + label = core.Msg(c, "code_viewer.unfold") } chev := "chevron-down" if s.Folded.Has(li) { chev = "chevron-right" } - if iconAction(c, icons.Must(chev), fmt.Sprintf(label, li+1)).Clicked() && !res.Element.IsDisabled() { + if core.IconAction(c, icons.Must(chev), fmt.Sprintf(label, li+1)).Clicked() && !res.Element.IsDisabled() { codeViewerToggle(s, li) } } else { ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0) } - codeMono(c, fmt.Sprint(li+1)).Width(gutter).TextAlign(ui.End).TextColor(k.TextMuted).Shrink(0) + CodeMono(c, fmt.Sprint(li+1)).Width(gutter).TextAlign(ui.End).TextColor(k.TextMuted).Shrink(0) codeViewerLine(c, k, opts.Language, lines[li], s.inBlock[li]) }) }) @@ -129,7 +131,7 @@ func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeView list.Grow(1).MinHeight(d.ControlHeight() * 3).Label(opts.Label) internal.Ring(list, k.Focus, theme.ControlRadius) if list.Changed() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: code_viewer line", "line", s.Line()) } codeViewerKeys(c, list, lines, s) @@ -181,9 +183,9 @@ func codeViewerLine(c *ui.Context, k theme.Tokens, lang, line string, inBlock bo toks, _ := internal.Highlight(lang, line, inBlock) spans := make([]ui.Span, len(toks)) for i, t := range toks { - spans[i] = ui.Span{Text: t.Text, Color: codeTokenColor(k, t.Kind), Italic: t.Kind == internal.TokComment} + spans[i] = ui.Span{Text: t.Text, Color: CodeTokenColor(k, t.Kind), Italic: t.Kind == internal.TokComment} } - ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine().Grow(1).MinWidth(0) + ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)).SingleLine().Grow(1).MinWidth(0) } // codeViewerPrepare rescans code and folds when they change. @@ -192,8 +194,8 @@ func codeViewerPrepare(lines []string, s *CodeViewerState, lang string) { if len(lines) > 0 { first = &lines[0] } - if s.data != first || s.n != len(lines) || s.lang != lang { - s.data, s.n, s.lang, s.folds = first, len(lines), lang, -1 + if s.Data != first || s.n != len(lines) || s.lang != lang { + s.Data, s.n, s.lang, s.folds = first, len(lines), lang, -1 s.inBlock = slices.Grow(s.inBlock[:0], len(lines))[:len(lines)] in := false for i, l := range lines { diff --git a/code_viewer_test.go b/code/code_viewer_test.go similarity index 93% rename from code_viewer_test.go rename to code/code_viewer_test.go index 8e493c2..01bdd6a 100644 --- a/code_viewer_test.go +++ b/code/code_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) var codeViewerSample = []string{ @@ -24,7 +25,7 @@ var codeViewerSample = []string{ func codeViewerTester(lines []string, s *CodeViewerState, disabled bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CodeViewer(c, lines, s, CodeViewerOptions{Language: "go", Label: "Source", Disabled: disabled}).Element.Height(400) }, 520, 420) } @@ -96,9 +97,9 @@ func TestCodeViewerGoToOpensFold(t *testing.T) { func TestCodeViewerHighlightColors(t *testing.T) { ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - k := Tokens(c) - kw, str := codeTokenColor(k, internal.TokKeyword), codeTokenColor(k, internal.TokString) + core.Use(c, core.Settings{Mode: core.Light}) + k := core.Tokens(c) + kw, str := CodeTokenColor(k, internal.TokKeyword), CodeTokenColor(k, internal.TokString) if kw != k.AccentText || str != k.Success { t.Errorf("keyword %v string %v", kw, str) } diff --git a/command_history.go b/code/command_history.go similarity index 71% rename from command_history.go rename to code/command_history.go index d469546..a88dae5 100644 --- a/command_history.go +++ b/code/command_history.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -8,7 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // HistoryEntry is one command a shell ran. @@ -47,7 +49,7 @@ func CommandHistory(c *ui.Context, entries []HistoryEntry, s *CommandHistoryStat panic("mujica: CommandHistory needs a state") } var res CommandHistoryResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() replay := func(i int) { if disabled { @@ -58,25 +60,25 @@ func CommandHistory(c *ui.Context, entries []HistoryEntry, s *CommandHistoryStat slog.Debug("mujica: command_history replay", "index", i) } res.Element.Children(func() { - codeHeader(c, func() { - SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled, Placeholder: msg(c, "command_history.search")}).Element.Grow(1) + CodeHeader(c, func() { + input.SearchInput(c, &s.Query, input.SearchInputOptions{Disabled: opts.Disabled, Placeholder: core.Msg(c, "command_history.search")}).Element.Grow(1) }) commandHistoryFilter(entries, s) if len(s.shown) == 0 { - text := msg(c, "command_history.empty") + text := core.Msg(c, "command_history.empty") if len(entries) > 0 { - text = msg(c, "command_history.none") + text = core.Msg(c, "command_history.none") } - codeCaption(c, text).Padding(Density(c).Unit() * 4) + CodeCaption(c, text).Padding(core.Density(c).Unit() * 4) return } s.rows.Selected = &s.selected s.rows.Key = func(i int) any { return s.shown[i] } s.rows.Label = func(i int) string { return entries[s.shown[i]].Command } - list := codeRows(c, &s.rows, len(s.shown), func(i int) { + list := CodeRows(c, &s.rows, len(s.shown), func(i int) { e := entries[s.shown[i]] commandHistoryRow(c, e) - if iconAction(c, icons.Must("refresh-cw"), fmt.Sprintf(msg(c, "command_history.replay"), e.Command)).Clicked() { + if core.IconAction(c, icons.Must("refresh-cw"), fmt.Sprintf(core.Msg(c, "command_history.replay"), e.Command)).Clicked() { replay(s.shown[i]) } }) @@ -104,15 +106,15 @@ func commandHistoryFilter(entries []HistoryEntry, s *CommandHistoryState) { } func commandHistoryRow(c *ui.Context, e HistoryEntry) { - k := Tokens(c) + k := core.Tokens(c) ui.Column(c).Grow(1).MinWidth(0).Children(func() { - codeMono(c, e.Command) - ui.Row(c).Gap(Density(c).Unit() * 2).Children(func() { + CodeMono(c, e.Command) + ui.Row(c).Gap(core.Density(c).Unit() * 2).Children(func() { if !e.Time.IsZero() { - codeCaption(c, e.Time.Format("15:04:05")) + CodeCaption(c, e.Time.Format("15:04:05")) } if e.ExitCode != 0 { - codeCaption(c, fmt.Sprintf(msg(c, "command_history.exit"), e.ExitCode)).TextColor(k.Danger) + CodeCaption(c, fmt.Sprintf(core.Msg(c, "command_history.exit"), e.ExitCode)).TextColor(k.Danger) } }) }) diff --git a/command_history_test.go b/code/command_history_test.go similarity index 96% rename from command_history_test.go rename to code/command_history_test.go index 1f5f599..1ca505d 100644 --- a/command_history_test.go +++ b/code/command_history_test.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func commandHistorySample() []HistoryEntry { @@ -20,7 +22,7 @@ func commandHistorySample() []HistoryEntry { func commandHistoryTester(entries []HistoryEntry, s *CommandHistoryState, disabled bool, got *[]string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommandHistory(c, entries, s, CommandHistoryOptions{Disabled: disabled}) r.Element.Grow(1) if r.Replayed != nil { @@ -93,7 +95,7 @@ func TestCommandHistoryLargeIsVirtual(t *testing.T) { entries[i].Command = fmt.Sprintf("echo %d", i) } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommandHistory(c, entries, &CommandHistoryState{}, CommandHistoryOptions{}) }, 420, 320) rows := 0 diff --git a/completion_menu.go b/code/completion_menu.go similarity index 77% rename from completion_menu.go rename to code/completion_menu.go index 60c6ab9..276328d 100644 --- a/completion_menu.go +++ b/code/completion_menu.go @@ -1,11 +1,14 @@ -package mujica +package code import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Completion is one candidate of a CompletionMenu. @@ -93,26 +96,26 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl slog.Debug("mujica: completion_menu close") return res } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := anchor.Bounds() - overlayLayer(c, open) + core.OverlayLayer(c, open) res.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(4, 0, 0, 0).Padding(d.Unit()).Width(popoverWide(c, 360)).MaxHeight(popoverRoom(c, b)). - Label(msg(c, "completion_menu.label")).Role(ui.RoleList) + core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, b)) + panel.Margin(4, 0, 0, 0).Padding(d.Unit()).Width(overlay.PopoverWide(c, 360)).MaxHeight(overlay.PopoverRoom(c, b)). + Label(core.Msg(c, "completion_menu.label")).Role(ui.RoleList) chosen := s.Selected s.rows.Selected = &chosen s.rows.Label = func(i int) string { return items[i].Label } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(items), func(i int) { it := items[i] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - codeSelectMark(c, i == s.Selected) - codeBadge(c, symbolBadges[it.Kind], k.AccentText) - codeMono(c, it.Label).Grow(1).MinWidth(0) + CodeSelectMark(c, i == s.Selected) + CodeBadge(c, symbolBadges[it.Kind], k.AccentText) + CodeMono(c, it.Label).Grow(1).MinWidth(0) if it.Detail != "" { - codeCaption(c, it.Detail).MinWidth(0).Shrink(1) + CodeCaption(c, it.Detail).MinWidth(0).Shrink(1) } }) }).MaxHeight(d.ControlHeight() * completionRows).Shrink(1) @@ -121,7 +124,7 @@ func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Compl accept(chosen) } if doc := items[s.Selected].Doc; doc != "" { - ui.Text(c, doc).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit()*2). + ui.Text(c, doc).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit()*2). BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).Shrink(0) } }) diff --git a/completion_menu_test.go b/code/completion_menu_test.go similarity index 92% rename from completion_menu_test.go rename to code/completion_menu_test.go index 1ebe98a..608a65e 100644 --- a/completion_menu_test.go +++ b/code/completion_menu_test.go @@ -1,10 +1,13 @@ -package mujica +package code import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func completionSample() []Completion { @@ -24,9 +27,9 @@ type completionProbe struct { } func (p *completionProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { - in := TextInput(c, &p.text, TextInputOptions{Label: "Code"}) + in := input.TextInput(c, &p.text, input.TextInputOptions{Label: "Code"}) if in.Changed() { p.open = strings.HasPrefix(strings.ToLower(p.text), "se") } diff --git a/debug_toolbar.go b/code/debug_toolbar.go similarity index 91% rename from debug_toolbar.go rename to code/debug_toolbar.go index 01d88fd..f456ce3 100644 --- a/debug_toolbar.go +++ b/code/debug_toolbar.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -6,6 +6,9 @@ import ( "runtime" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) // DebugState is where a debug session is. @@ -103,18 +106,18 @@ func DebugToolbar(c *ui.Context, state DebugState, opts DebugToolbarOptions) Deb } keys := debugShortcuts() var res DebugToolbarResult - res.Element = Toolbar(c, ToolbarOptions{Label: msg(c, "debug_toolbar.label")}, func() { + res.Element = navigation.Toolbar(c, navigation.ToolbarOptions{Label: core.Msg(c, "debug_toolbar.label")}, func() { first := DebugContinue if state == DebugRunning { first = DebugPause } for _, a := range []DebugAction{first, DebugStepOver, DebugStepInto, DebugStepOut, DebugRestart, DebugStop} { - name := msg(c, debugKeys[a]) + name := core.Msg(c, debugKeys[a]) if a == DebugContinue && state == DebugStopped { - name = msg(c, "debug_toolbar.start") + name = core.Msg(c, "debug_toolbar.start") } on := DebugEnabled(state, a) && !opts.Disabled - b := ToolbarButton(c, name, ToolbarButtonOptions{Icon: debugIcons[a], Disabled: !on}) + b := navigation.ToolbarButton(c, name, navigation.ToolbarButtonOptions{Icon: debugIcons[a], Disabled: !on}) b.Tooltip(fmt.Sprintf("%s (%s)", name, keys[a].name)) if b.Clicked() && on && !b.IsDisabled() { res.Action = a diff --git a/debug_toolbar_test.go b/code/debug_toolbar_test.go similarity index 96% rename from debug_toolbar_test.go rename to code/debug_toolbar_test.go index be2afe1..aabf4df 100644 --- a/debug_toolbar_test.go +++ b/code/debug_toolbar_test.go @@ -1,14 +1,16 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func debugTester(state *DebugState, disabled bool, got *[]DebugAction) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if a := DebugToolbar(c, *state, DebugToolbarOptions{Disabled: disabled}).Action; a != DebugNone { *got = append(*got, a) } diff --git a/code/doc.go b/code/doc.go new file mode 100644 index 0000000..487996f --- /dev/null +++ b/code/doc.go @@ -0,0 +1,2 @@ +// Package code holds MujicaUI terminal and code views. +package code diff --git a/code/example_code_test.go b/code/example_code_test.go new file mode 100644 index 0000000..dc7bf37 --- /dev/null +++ b/code/example_code_test.go @@ -0,0 +1,278 @@ +package code_test + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleTerminal() { + var term code.TerminalState + term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + code.Terminal(c, &term, code.TerminalOptions{ + Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, + Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, + }).Size(380, 200) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTerminalTabs() { + tabs := []code.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} + active := "a" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.TerminalTabs(c, &tabs, &active, code.TerminalTabsOptions{}, func(id string) { + ui.Text(c, "terminal "+id) + }) + if r.Closed != "" { + fmt.Println("close pty", r.Closed) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTerminalSearch() { + var term code.TerminalState + term.Write("build ok\r\nerror: missing seal\r\n") + var search code.TerminalSearchState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Column(c).Gap(8).Children(func() { + code.TerminalSearch(c, &term, &search, code.TerminalSearchOptions{}) + code.Terminal(c, &term, code.TerminalOptions{}).Height(160) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommandHistory() { + history := []code.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} + var state code.CommandHistoryState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.CommandHistory(c, history, &state, code.CommandHistoryOptions{}) + r.Element.Height(240) + if r.Replayed != nil { + fmt.Println("run", r.Replayed.Command) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLogViewer() { + lines := []code.LogLine{{Level: code.LogInfo, Text: "court opened"}, {Level: code.LogError, Text: "seal missing"}} + var state code.LogViewerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + code.LogViewer(c, lines, &state, code.LogViewerOptions{}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProcessList() { + procs := []code.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} + var state code.ProcessListState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.ProcessList(c, procs, &state, code.ProcessListOptions{}) + r.Element.Height(240) + if r.End != nil { + fmt.Println("kill", r.End.PID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCodeViewer() { + lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} + var state code.CodeViewerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + code.CodeViewer(c, lines, &state, code.CodeViewerOptions{Language: "go"}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMinimap() { + lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} + view := code.MinimapView{Top: 0, Visible: 2} + draw := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if code.Minimap(c, lines, &view, code.MinimapOptions{Language: "go"}).Changed() { + fmt.Println("scroll editor to", view.Top) + } + } + ui.Render(draw, 400, 300, 1) +} + +func ExampleFindWidget() { + text := "the seal and the seal" + state := code.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if code.FindWidget(c, &text, &state, code.FindWidgetOptions{}).Replaced > 0 { + fmt.Println(text) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSearchPanel() { + results := []code.SearchFileResult{{Path: "court/seal.go", Matches: []code.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} + state := code.SearchPanelState{Query: "seal"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.SearchPanel(c, results, &state, code.SearchPanelOptions{}) + r.Element.Height(280) + if r.Opened != nil { + fmt.Println("open", r.Opened.Path, r.Opened.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProblemsPanel() { + problems := []code.Problem{{Severity: code.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} + var state code.ProblemsPanelState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.ProblemsPanel(c, problems, &state, code.ProblemsPanelOptions{}) + r.Element.Height(240) + if r.Opened != nil { + fmt.Println("go to", r.Opened.File, r.Opened.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOutputPanel() { + channels := []code.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} + var state code.OutputPanelState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.OutputPanel(c, channels, &state, code.OutputPanelOptions{}) + r.Element.Height(240) + if r.Cleared != "" { + channels[state.Channel].Lines = nil + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSymbolOutline() { + symbols := []code.Symbol{{Name: "Court", Kind: code.SymbolType, Line: 3, EndLine: 40, Children: []code.Symbol{ + {Name: "Seal", Kind: code.SymbolMethod, Line: 10, EndLine: 20}, + }}} + var state code.SymbolOutlineState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.SymbolOutline(c, symbols, &state, code.SymbolOutlineOptions{CursorLine: 12}) + r.Element.Height(240) + if r.Jumped != nil { + fmt.Println("go to line", r.Jumped.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCompletionMenu() { + text := "se" + open := true + items := []code.Completion{{Label: "Seal", Kind: code.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: code.SymbolFunction}} + var state code.CompletionMenuState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + in := input.TextInput(c, &text, input.TextInputOptions{Label: "Code"}) + if r := code.CompletionMenu(c, in.Input, &open, items, &state, code.CompletionMenuOptions{}); r.Accepted != nil { + text = r.Accepted.Label + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDebugToolbar() { + state := code.DebugPaused + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + switch code.DebugToolbar(c, state, code.DebugToolbarOptions{}).Action { + case code.DebugContinue: + state = code.DebugRunning + case code.DebugStop: + state = code.DebugStopped + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBreakpointList() { + bps := []code.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.BreakpointList(c, &bps, code.BreakpointListOptions{}) + r.Element.Height(200) + if r.Toggled != "" || r.Edited != "" { + fmt.Println("sync breakpoints with the debugger") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCallStack() { + threads := []code.DebugThread{{ID: "t1", Name: "main", Frames: []code.StackFrame{ + {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, + {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, + }}} + state := code.CallStackState{Current: "f1"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.CallStack(c, threads, &state, code.CallStackOptions{}) + r.Element.Height(200) + if r.Changed() { + fmt.Println("show frame", state.Current) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVariablesPanel() { + vars := []code.Variable{{Name: "decree", Value: "{…}", Children: []code.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} + var state code.VariablesPanelState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := code.VariablesPanel(c, vars, &state, code.VariablesPanelOptions{}) + r.Element.Height(200) + if r.Edited != nil { + fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHexViewer() { + data := []byte("MUJICA court ledger\x00\x01") + var state code.HexViewerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + code.HexViewer(c, data, &state, code.HexViewerOptions{}).Element.Height(160) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFlamegraph() { + root := &code.FlameNode{Name: "main", Value: 100, Children: []code.FlameNode{ + {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, + }} + var state code.FlamegraphState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + code.Flamegraph(c, root, &state, code.FlamegraphOptions{Unit: "ms"}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/find_widget.go b/code/find_widget.go similarity index 74% rename from find_widget.go rename to code/find_widget.go index 97f8629..df9306e 100644 --- a/find_widget.go +++ b/code/find_widget.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // FindState is a FindWidget's query, options and current match. @@ -59,10 +61,10 @@ func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOption if text == nil || s == nil { panic("mujica: FindWidget needs text and a state") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res FindWidgetResult - res.Element = codePanel(c).Padding(u * 2).Gap(u * 2).Disabled(opts.Disabled) + res.Element = CodePanel(c).Padding(u * 2).Gap(u * 2).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() findRun(*text, s) res.Element.Children(func() { @@ -72,11 +74,11 @@ func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOption if s.ShowReplace { chev = "chevron-down" } - if iconAction(c, icons.Must(chev), msg(c, "find_widget.toggleReplace")).Clicked() && !disabled { + if core.IconAction(c, icons.Must(chev), core.Msg(c, "find_widget.toggleReplace")).Clicked() && !disabled { s.ShowReplace = !s.ShowReplace } } - r := SearchInput(c, &s.Query, SearchInputOptions{Find: true, Matches: len(s.matches), Current: &s.Current, Disabled: opts.Disabled}) + r := input.SearchInput(c, &s.Query, input.SearchInputOptions{Find: true, Matches: len(s.matches), Current: &s.Current, Disabled: opts.Disabled}) r.Element.Grow(1).MinWidth(0) changed := r.Changed() || r.Moved() for _, o := range []struct { @@ -84,7 +86,7 @@ func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOption short string key string }{{&s.MatchCase, "Aa", "find_widget.case"}, {&s.WholeWord, "ab", "find_widget.word"}, {&s.Regex, ".*", "find_widget.regex"}} { - if Toggle(c, o.on, o.short, ToggleOptions{Label: msg(c, o.key), Disabled: opts.Disabled}).Tooltip(msg(c, o.key)).Changed() { + if input.Toggle(c, o.on, o.short, input.ToggleOptions{Label: core.Msg(c, o.key), Disabled: opts.Disabled}).Tooltip(core.Msg(c, o.key)).Changed() { changed = true } } @@ -95,20 +97,20 @@ func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOption } }) if s.err != "" { - ui.Text(c, fmt.Sprintf(msg(c, "find_widget.invalid"), s.err)).TextColor(k.Danger).Role(ui.RoleStatus). - FontSize(FontSize(c, 12)) + ui.Text(c, fmt.Sprintf(core.Msg(c, "find_widget.invalid"), s.err)).TextColor(k.Danger).Role(ui.RoleStatus). + FontSize(core.FontSize(c, 12)) } if !s.ShowReplace || opts.ReadOnly { return } ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(0, 0, 0, 28+u*2).Children(func() { - in := TextInput(c, &s.Replacement, TextInputOptions{Placeholder: msg(c, "find_widget.replace"), Label: msg(c, "find_widget.replace"), Disabled: opts.Disabled}) + in := input.TextInput(c, &s.Replacement, input.TextInputOptions{Placeholder: core.Msg(c, "find_widget.replace"), Label: core.Msg(c, "find_widget.replace"), Disabled: opts.Disabled}) in.Element.Grow(1).MinWidth(0) none := len(s.matches) == 0 - if (Button(c, msg(c, "find_widget.replaceOne"), ButtonOptions{Variant: Secondary, Disabled: none || opts.Disabled}).Clicked() || in.Submitted()) && !none && !disabled { + if (input.Button(c, core.Msg(c, "find_widget.replaceOne"), input.ButtonOptions{Variant: input.Secondary, Disabled: none || opts.Disabled}).Clicked() || in.Submitted()) && !none && !disabled { res.Replaced = findReplace(text, s, false) } - if Button(c, msg(c, "find_widget.replaceAll"), ButtonOptions{Variant: Secondary, Disabled: none || opts.Disabled}).Clicked() && !disabled { + if input.Button(c, core.Msg(c, "find_widget.replaceAll"), input.ButtonOptions{Variant: input.Secondary, Disabled: none || opts.Disabled}).Clicked() && !disabled { res.Replaced = findReplace(text, s, true) } }) diff --git a/find_widget_test.go b/code/find_widget_test.go similarity index 97% rename from find_widget_test.go rename to code/find_widget_test.go index 5a94aef..0458873 100644 --- a/find_widget_test.go +++ b/code/find_widget_test.go @@ -1,14 +1,16 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func findTester(text *string, s *FindState, opts FindWidgetOptions, replaced *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) *replaced += FindWidget(c, text, s, opts).Replaced }, 640, 200) } diff --git a/flamegraph.go b/code/flamegraph.go similarity index 82% rename from flamegraph.go rename to code/flamegraph.go index e3e3673..ed7728e 100644 --- a/flamegraph.go +++ b/code/flamegraph.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FlameNode is a profile frame; Value includes children. @@ -32,7 +35,7 @@ type FlamegraphOptions struct { // Unit follows values in the readout, as "ms". Unit string // Status shows loading or failure in place of the graph. - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -40,28 +43,28 @@ type FlamegraphOptions struct { // flameRow is a frame row's height, a full hit target. const flameRow = theme.MinHitSize -// flamegraphPalette colors frames at 3:1 on Surface. -func flamegraphPalette(k theme.Tokens) []ui.Color { +// FlamegraphPalette colors frames at 3:1 on Surface. +func FlamegraphPalette(k theme.Tokens) []ui.Color { // Ornament alone is too faint on ivory. return []ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Ornament.Mix(k.Text, 0.2), k.TextMuted} } // Flamegraph draws a zoomable, searchable profile. -func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts FlamegraphOptions) ChoiceResult { +func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts FlamegraphOptions) core.ChoiceResult { if s == nil { panic("mujica: Flamegraph needs a state") } - k := Tokens(c) - d := Density(c) - var res ChoiceResult - res.Element = codePanel(c).Disabled(opts.Disabled) + k := core.Tokens(c) + d := core.Density(c) + var res core.ChoiceResult + res.Element = CodePanel(c).Disabled(opts.Disabled) res.Element.Children(func() { - codeHeader(c, func() { - SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "flamegraph.search"), Disabled: opts.Disabled}).Element.Grow(1) + CodeHeader(c, func() { + input.SearchInput(c, &s.Query, input.SearchInputOptions{Placeholder: core.Msg(c, "flamegraph.search"), Disabled: opts.Disabled}).Element.Grow(1) }) - if root == nil || opts.Status != DataReady { + if root == nil || opts.Status != data.DataReady { ui.Column(c).Height(flameRow * 6).Children(func() { - res.changed = dataPlaceholder(c, opts.Status, opts.Error, msg(c, "flamegraph.empty"), msg(c, "flamegraph.empty")) + res = core.NewChoiceResult(res.Element, data.DataPlaceholder(c, opts.Status, opts.Error, core.Msg(c, "flamegraph.empty"), core.Msg(c, "flamegraph.empty"))) }) return } @@ -74,13 +77,13 @@ func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts Flamegr } q := strings.ToLower(strings.TrimSpace(s.Query)) depth := flameDepth(root) - g := ui.Box(c).Height(float32(depth)*flameRow).Shrink(0).Margin(d.Unit()*2, d.Unit()*3, 0, d.Unit()*3).Focusable().Label(msg(c, "flamegraph.label")) + g := ui.Box(c).Height(float32(depth)*flameRow).Shrink(0).Margin(d.Unit()*2, d.Unit()*3, 0, d.Unit()*3).Focusable().Label(core.Msg(c, "flamegraph.label")) internal.Ring(g, k.Focus, theme.ControlRadius) hover := ui.Local(g, "flamegraph.hover", func() []int { return nil }) zoom, focus := s.Zoom, s.Focus if !g.IsDisabled() { if flameInput(c, g, root, s, hover) { - res.changed = true + res = core.NewChoiceResult(res.Element, true) } } b := g.Bounds() @@ -88,14 +91,14 @@ func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts Flamegr if r.W != b.W { c.Invalidate() } - flamePaint(p, r, k, root, zoom, focus, q, g.Focused(), FontSize(c, theme.CaptionSize)) + flamePaint(p, r, k, root, zoom, focus, q, g.Focused(), core.FontSize(c, theme.CaptionSize)) }) readout := s.Focus if *hover != nil { readout = *hover } text := flameReadout(c, root, readout, opts.Unit) - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*3). + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*3). Role(ui.RoleStatus).Shrink(0).SingleLine() if g.Focused() && text != s.said { s.said = text @@ -219,7 +222,7 @@ func flameHit(root *FlameNode, zoom []int, fx float32, row int) []int { func flameColor(k theme.Tokens, name string) ui.Color { h := fnv.New32a() h.Write([]byte(name)) - pal := flamegraphPalette(k) + pal := FlamegraphPalette(k) return pal[h.Sum32()%uint32(len(pal))] } @@ -269,8 +272,8 @@ func flamePaint(p *ui.Painter, r ui.Rect, k theme.Tokens, root *FlameNode, zoom, func flameReadout(c *ui.Context, root *FlameNode, path []int, unit string) string { n := flameAt(root, path) if n == nil { - return msg(c, "flamegraph.hint") + return core.Msg(c, "flamegraph.hint") } share := 100 * n.Value / max(root.Value, 1e-12) - return fmt.Sprintf(msg(c, "flamegraph.readout"), n.Name, n.Value, unit, share) + return fmt.Sprintf(core.Msg(c, "flamegraph.readout"), n.Name, n.Value, unit, share) } diff --git a/flamegraph_test.go b/code/flamegraph_test.go similarity index 92% rename from flamegraph_test.go rename to code/flamegraph_test.go index ff6cda6..7d65602 100644 --- a/flamegraph_test.go +++ b/code/flamegraph_test.go @@ -1,11 +1,13 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func flameSample() *FlameNode { @@ -26,7 +28,7 @@ type flameProbe struct { } func (p *flameProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Flamegraph(c, p.root, &p.s, p.opts).Changed() { p.changed++ } @@ -34,7 +36,7 @@ func (p *flameProbe) view(c *ui.Context) { func TestFlamegraphPaletteContrast(t *testing.T) { for _, k := range []theme.Tokens{theme.Light(), theme.Dark()} { - for i, col := range flamegraphPalette(k) { + for i, col := range FlamegraphPalette(k) { if r := theme.Contrast(col, k.Surface); r < 3 { t.Errorf("dark=%v color %d contrast %.2f < 3", k.Dark, i, r) } @@ -104,7 +106,7 @@ func TestFlamegraphSearchAndStates(t *testing.T) { t.Fatalf("hit %v", hit) } _ = tt - p = &flameProbe{opts: FlamegraphOptions{Status: DataFailed, Error: "profile unreadable"}} + p = &flameProbe{opts: FlamegraphOptions{Status: data.DataFailed, Error: "profile unreadable"}} tt = ui.NewTester(p.view, 600, 300) if !tt.HasText("profile unreadable") { t.Fatalf("texts %q", tt.Texts()) diff --git a/code/helpers_test.go b/code/helpers_test.go new file mode 100644 index 0000000..2796ff7 --- /dev/null +++ b/code/helpers_test.go @@ -0,0 +1,24 @@ +package code + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/hex_viewer.go b/code/hex_viewer.go similarity index 85% rename from hex_viewer.go rename to code/hex_viewer.go index 7bce36c..1540604 100644 --- a/hex_viewer.go +++ b/code/hex_viewer.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // hexPerRow is how many bytes a row shows. @@ -33,7 +34,7 @@ type HexViewerOptions struct { } // HexViewer shows bytes as synced hex and ASCII columns. -func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOptions) ChoiceResult { +func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOptions) core.ChoiceResult { if s == nil { panic("mujica: HexViewer needs a state") } @@ -41,11 +42,11 @@ func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOpti if n > 0 && (s.Cursor < 0 || s.Cursor >= n || s.Anchor < 0 || s.Anchor >= n) { panic("mujica: HexViewer cursor outside the data") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) cw := codeTextWidth(c, strings.Repeat("0", hexPerRow*3)) / (hexPerRow * 3) - var res ChoiceResult - frame := codePanel(c).Focusable().Disabled(opts.Disabled).Label(msg(c, "hex_viewer.label")) + var res core.ChoiceResult + frame := CodePanel(c).Focusable().Disabled(opts.Disabled).Label(core.Msg(c, "hex_viewer.label")) res.Element = frame internal.Ring(frame, k.Focus, theme.ControlRadius) move := func(to int, extend bool) { @@ -58,17 +59,17 @@ func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOpti s.Anchor = to } s.rows.ScrollIntoView(to / hexPerRow) - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: hex_viewer select", "cursor", s.Cursor, "anchor", s.Anchor) } if n > 0 && !frame.IsDisabled() { hexKeys(c, frame, data, s, move) } a, b := s.Selection() - frame.Description(fmt.Sprintf(msg(c, "hex_viewer.at"), s.Cursor, b-a)) + frame.Description(fmt.Sprintf(core.Msg(c, "hex_viewer.at"), s.Cursor, b-a)) frame.Children(func() { if n == 0 { - codeCaption(c, msg(c, "hex_viewer.empty")).Padding(d.Unit() * 4) + CodeCaption(c, core.Msg(c, "hex_viewer.empty")).Padding(d.Unit() * 4) return } rows := (n + hexPerRow - 1) / hexPerRow @@ -76,7 +77,7 @@ func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOpti start := r * hexPerRow end := min(start+hexPerRow, n) ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(cw * 2).AlignItems(ui.Center).Children(func() { - codeMono(c, fmt.Sprintf("%08x", start)).TextColor(k.TextMuted).Shrink(0) + CodeMono(c, fmt.Sprintf("%08x", start)).TextColor(k.TextMuted).Shrink(0) hexColumn(c, s, data, start, end, cw, true, move) hexColumn(c, s, data, start, end, cw, false, move) }) @@ -87,7 +88,7 @@ func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOpti // hexColumn draws a row's hex or ASCII with marks. func hexColumn(c *ui.Context, s *HexViewerState, data []byte, start, end int, cw float32, hex bool, move func(int, bool)) { - k := Tokens(c) + k := core.Tokens(c) var b strings.Builder step := cw if hex { @@ -123,7 +124,7 @@ func hexColumn(c *ui.Context, s *HexViewerState, data []byte, start, end int, cw x, _, _ := box.PointerPosition() move(start+min(int(x/step), end-start-1), box.ClickModifiers()&ui.Shift != 0) } - box.Children(func() { codeMono(c, strings.TrimRight(b.String(), " ")) }) + box.Children(func() { CodeMono(c, strings.TrimRight(b.String(), " ")) }) } // hexKeys moves, extends and copies the selection. diff --git a/hex_viewer_test.go b/code/hex_viewer_test.go similarity index 97% rename from hex_viewer_test.go rename to code/hex_viewer_test.go index 609c656..a1bda22 100644 --- a/hex_viewer_test.go +++ b/code/hex_viewer_test.go @@ -1,10 +1,12 @@ -package mujica +package code import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type hexProbe struct { @@ -15,7 +17,7 @@ type hexProbe struct { } func (p *hexProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := HexViewer(c, p.data, &p.s, HexViewerOptions{Disabled: p.disabled}) r.Element.Height(300) if r.Changed() { diff --git a/log_viewer.go b/code/log_viewer.go similarity index 66% rename from log_viewer.go rename to code/log_viewer.go index 33099db..13ba952 100644 --- a/log_viewer.go +++ b/code/log_viewer.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LogLevel is a log line's severity. @@ -48,29 +50,29 @@ type LogViewerOptions struct { } // LogViewer shows filtered, highlighted log lines, virtualized. -func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewerOptions) ChoiceResult { +func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewerOptions) core.ChoiceResult { if s == nil { panic("mujica: LogViewer needs a state") } if !s.started { s.started, s.Follow = true, true } - var res ChoiceResult - res.Element = codePanel(c).Disabled(opts.Disabled) + var res core.ChoiceResult + res.Element = CodePanel(c).Disabled(opts.Disabled) res.Element.Children(func() { - codeHeader(c, func() { + CodeHeader(c, func() { for lv := LogDebug; lv <= LogError; lv++ { on := !s.Hidden[lv] - if Toggle(c, &on, logViewerName(c, lv), ToggleOptions{Disabled: opts.Disabled}).Changed() { + if input.Toggle(c, &on, LogViewerName(c, lv), input.ToggleOptions{Disabled: opts.Disabled}).Changed() { s.Hidden[lv] = !on - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: log_viewer level", "level", lv, "shown", on) } } - SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled, Placeholder: msg(c, "log_viewer.highlight")}).Element.Grow(1).MinWidth(0) + input.SearchInput(c, &s.Query, input.SearchInputOptions{Disabled: opts.Disabled, Placeholder: core.Msg(c, "log_viewer.highlight")}).Element.Grow(1).MinWidth(0) was := s.Follow - if Toggle(c, &s.Follow, msg(c, "log_viewer.follow"), ToggleOptions{Disabled: opts.Disabled}).Changed() { - res.changed = true + if input.Toggle(c, &s.Follow, core.Msg(c, "log_viewer.follow"), input.ToggleOptions{Disabled: opts.Disabled}).Changed() { + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: log_viewer follow", "on", s.Follow) if s.Follow && !was { s.rows.ScrollToEnd() @@ -79,13 +81,13 @@ func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewer }) logViewerFilter(lines, s) if len(s.shown) == 0 { - codeCaption(c, msg(c, "log_viewer.empty")).Padding(Density(c).Unit() * 4) + CodeCaption(c, core.Msg(c, "log_viewer.empty")).Padding(core.Density(c).Unit() * 4) return } s.rows.FollowEnd = s.Follow s.rows.Key = func(i int) any { return s.shown[i] } s.rows.Label = func(i int) string { return lines[s.shown[i]].Text } - d := Density(c) + d := core.Density(c) ui.List(c, &s.rows, len(s.shown), func(i int) { logViewerRow(c, lines[s.shown[i]], s.Query) }).Grow(1).MinHeight(d.ControlHeight()*3).Padding(d.Unit(), 0) @@ -110,21 +112,21 @@ func logViewerFilter(lines []LogLine, s *LogViewerState) { s.scanned = len(lines) } -func logViewerName(c *ui.Context, lv LogLevel) string { - return msg(c, [...]string{"log_viewer.debug", "log_viewer.info", "log_viewer.warn", "log_viewer.error"}[lv]) +func LogViewerName(c *ui.Context, lv LogLevel) string { + return core.Msg(c, [...]string{"log_viewer.debug", "log_viewer.info", "log_viewer.warn", "log_viewer.error"}[lv]) } func logViewerRow(c *ui.Context, l LogLine, query string) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := [...]ui.Color{k.TextMuted, k.Info, k.Warning, k.Danger}[l.Level] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { if !l.Time.IsZero() { - codeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0) + CodeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0) } - ui.Text(c, fmt.Sprintf("%-5s", logViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)). + ui.Text(c, fmt.Sprintf("%-5s", LogViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)). FontWeight(700).TextColor(col).SingleLine().Shrink(0) - ui.RichText(c, codeMatchSpans(c, l.Text, query)...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)). + ui.RichText(c, CodeMatchSpans(c, l.Text, query)...).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)). SingleLine().Grow(1).MinWidth(0) }) } diff --git a/log_viewer_test.go b/code/log_viewer_test.go similarity index 95% rename from log_viewer_test.go rename to code/log_viewer_test.go index 7d8c01c..1286fe3 100644 --- a/log_viewer_test.go +++ b/code/log_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func logViewerLines(n int) []LogLine { @@ -20,7 +22,7 @@ func logViewerLines(n int) []LogLine { func logViewerTester(lines *[]LogLine, s *LogViewerState, disabled bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) LogViewer(c, *lines, s, LogViewerOptions{Disabled: disabled}).Element.Height(400) }, 760, 420) } @@ -89,8 +91,8 @@ func TestLogViewerHighlight(t *testing.T) { } var spans []ui.Span ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - spans = codeMatchSpans(c, "disk nearly full", "FULL") + core.Use(c, core.Settings{}) + spans = CodeMatchSpans(c, "disk nearly full", "FULL") }, 10, 10) if len(spans) != 2 || spans[1].Text != "full" || !spans[1].Underline || spans[1].Background.A == 0 { t.Fatalf("highlight spans %+v", spans) diff --git a/minimap.go b/code/minimap.go similarity index 87% rename from minimap.go rename to code/minimap.go index 3ba6490..35b6c1e 100644 --- a/minimap.go +++ b/code/minimap.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MinimapView is the editor's first line and line count. @@ -30,31 +31,31 @@ const ( ) // Minimap draws code small and moves the visible frame. -func Minimap(c *ui.Context, lines []string, view *MinimapView, opts MinimapOptions) ChoiceResult { +func Minimap(c *ui.Context, lines []string, view *MinimapView, opts MinimapOptions) core.ChoiceResult { if view == nil { panic("mujica: Minimap needs a view") } if opts.Width < 0 || view.Visible < 0 || view.Top < 0 { panic("mujica: Minimap width, Top and Visible must not be negative") } - k := Tokens(c) + k := core.Tokens(c) w := opts.Width if w == 0 { w = 96 } n := len(lines) - var res ChoiceResult + var res core.ChoiceResult move := func(top int) { top = max(0, min(top, n-view.Visible)) if top != view.Top { view.Top = top - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: minimap move", "top", top) } } e := ui.Box(c).Width(w).Shrink(0).MinHeight(theme.MinHitSize*2).Focusable().Clip().Cursor(ui.CursorPointer). Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled). - Label(msg(c, "minimap.label")).Description(fmt.Sprintf(msg(c, "minimap.range"), min(view.Top+1, n), min(view.Top+view.Visible, n), n)) + Label(core.Msg(c, "minimap.label")).Description(fmt.Sprintf(core.Msg(c, "minimap.range"), min(view.Top+1, n), min(view.Top+view.Visible, n), n)) res.Element = e h := e.Bounds().H offset := minimapOffset(n, *view, h) @@ -124,7 +125,7 @@ func minimapStrokes(p *ui.Painter, k theme.Tokens, lang, line string, x0, y, x1 if r == ' ' && start >= 0 { x := x0 + float32(col+start)*minimapChar if x < x1 { - p.Fill(ui.Rect{X: x, Y: y, W: min(float32(i-start)*minimapChar, x1-x), H: minimapLine * 0.7}, codeTokenColor(k, t.Kind).Alpha(0.75), 0) + p.Fill(ui.Rect{X: x, Y: y, W: min(float32(i-start)*minimapChar, x1-x), H: minimapLine * 0.7}, CodeTokenColor(k, t.Kind).Alpha(0.75), 0) } start = -1 } diff --git a/minimap_test.go b/code/minimap_test.go similarity index 96% rename from minimap_test.go rename to code/minimap_test.go index c538fd0..df25733 100644 --- a/minimap_test.go +++ b/code/minimap_test.go @@ -1,10 +1,12 @@ -package mujica +package code import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func minimapLines(n int) []string { @@ -17,7 +19,7 @@ func minimapLines(n int) []string { func minimapTester(lines []string, v *MinimapView, disabled bool, moves *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := Minimap(c, lines, v, MinimapOptions{Language: "go", Disabled: disabled}) r.Element.Height(200) if r.Changed() { diff --git a/output_panel.go b/code/output_panel.go similarity index 65% rename from output_panel.go rename to code/output_panel.go index e7c00df..a11ad44 100644 --- a/output_panel.go +++ b/code/output_panel.go @@ -1,12 +1,14 @@ -package mujica +package code import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OutputChannel is a named stream of ANSI lines. @@ -50,24 +52,24 @@ func OutputPanel(c *ui.Context, channels []OutputChannel, s *OutputPanelState, o s.started, s.Follow, s.shown = true, true, s.Channel } var res OutputPanelResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { - codeHeader(c, func() { - options := make([]SelectOption[int], len(channels)) + CodeHeader(c, func() { + options := make([]input.SelectOption[int], len(channels)) for i, ch := range channels { - options[i] = SelectOption[int]{Value: i, Label: ch.Name} + options[i] = input.SelectOption[int]{Value: i, Label: ch.Name} } - if Select(c, &s.Channel, options, SelectOptions{Label: msg(c, "output_panel.channel"), Disabled: opts.Disabled}).Changed() { + if input.Select(c, &s.Channel, options, input.SelectOptions{Label: core.Msg(c, "output_panel.channel"), Disabled: opts.Disabled}).Changed() { res.Changed = true slog.Debug("mujica: output_panel channel", "channel", s.Channel) } ui.Spacer(c) was := s.Follow - if Toggle(c, &s.Follow, msg(c, "log_viewer.follow"), ToggleOptions{Disabled: opts.Disabled}).Changed() && s.Follow && !was { + if input.Toggle(c, &s.Follow, core.Msg(c, "log_viewer.follow"), input.ToggleOptions{Disabled: opts.Disabled}).Changed() && s.Follow && !was { s.rows.ScrollToEnd() } - if iconAction(c, icons.Must("trash-2"), msg(c, "output_panel.clear")).Disabled(opts.Disabled).Clicked() && !disabled { + if core.IconAction(c, icons.Must("trash-2"), core.Msg(c, "output_panel.clear")).Disabled(opts.Disabled).Clicked() && !disabled { res.Cleared = channels[s.Channel].Name slog.Debug("mujica: output_panel clear", "channel", s.Channel) } @@ -77,15 +79,15 @@ func OutputPanel(c *ui.Context, channels []OutputChannel, s *OutputPanelState, o } lines := channels[s.Channel].Lines if len(lines) == 0 { - codeCaption(c, msg(c, "output_panel.empty")).Padding(Density(c).Unit() * 4) + CodeCaption(c, core.Msg(c, "output_panel.empty")).Padding(core.Density(c).Unit() * 4) return } - d := Density(c) + d := core.Density(c) s.rows.FollowEnd = s.Follow s.rows.Label = func(i int) string { return lines[i] } ui.List(c, &s.rows, len(lines), func(i int) { ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).AlignItems(ui.Center).Children(func() { - ui.RichText(c, codeAnsiSpans(c, lines[i])...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).Grow(1).MinWidth(0) + ui.RichText(c, CodeAnsiSpans(c, lines[i])...).Font(theme.MonoFont).FontSize(core.FontSize(c, CodeSize)).Grow(1).MinWidth(0) }) }).Grow(1).MinHeight(d.ControlHeight()*3).Padding(d.Unit(), 0) }) diff --git a/output_panel_test.go b/code/output_panel_test.go similarity index 92% rename from output_panel_test.go rename to code/output_panel_test.go index e6cad68..ec5d57b 100644 --- a/output_panel_test.go +++ b/code/output_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package code import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type outputProbe struct { @@ -16,7 +18,7 @@ type outputProbe struct { } func (p *outputProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := OutputPanel(c, p.channels, &p.s, OutputPanelOptions{Disabled: p.disabled}) r.Element.Height(300) if r.Changed { @@ -77,9 +79,9 @@ func TestOutputPanelAnsiColors(t *testing.T) { var spans []ui.Span var ok ui.Color ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - ok = Tokens(c).Success - spans = codeAnsiSpans(c, "\x1b[1;32mPASS\x1b[0m court") + core.Use(c, core.Settings{Mode: core.Light}) + ok = core.Tokens(c).Success + spans = CodeAnsiSpans(c, "\x1b[1;32mPASS\x1b[0m court") }, 10, 10) if len(spans) != 2 || spans[0].Color != ok || spans[0].Weight != 700 || spans[1].Color.A != 0 { t.Fatalf("spans %+v", spans) diff --git a/problems_panel.go b/code/problems_panel.go similarity index 83% rename from problems_panel.go rename to code/problems_panel.go index 5602f75..2d0088e 100644 --- a/problems_panel.go +++ b/code/problems_panel.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "cmp" @@ -8,7 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // ProblemSeverity ranks a Problem. @@ -76,14 +78,14 @@ func ProblemsPanel(c *ui.Context, problems []Problem, s *ProblemsPanelState, opt counts[p.Severity]++ } var res ProblemsPanelResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { - codeHeader(c, func() { + CodeHeader(c, func() { for sv := ProblemError; sv <= ProblemInfo; sv++ { on := !s.Hidden[sv] - name := fmt.Sprintf(msg(c, problemsKeys[sv]), counts[sv]) - if Toggle(c, &on, name, ToggleOptions{Icon: icons.Must(problemsIcons[sv]), Disabled: opts.Disabled}).Changed() { + name := fmt.Sprintf(core.Msg(c, problemsKeys[sv]), counts[sv]) + if input.Toggle(c, &on, name, input.ToggleOptions{Icon: icons.Must(problemsIcons[sv]), Disabled: opts.Disabled}).Changed() { s.Hidden[sv] = !on slog.Debug("mujica: problems_panel filter", "severity", sv, "shown", on) } @@ -91,7 +93,7 @@ func ProblemsPanel(c *ui.Context, problems []Problem, s *ProblemsPanelState, opt }) problemsGroup(problems, s) if len(s.flat) == 0 { - codeCaption(c, msg(c, "problems_panel.empty")).Padding(Density(c).Unit() * 4) + CodeCaption(c, core.Msg(c, "problems_panel.empty")).Padding(core.Density(c).Unit() * 4) return } s.rows.Selected = &s.selected @@ -102,7 +104,7 @@ func ProblemsPanel(c *ui.Context, problems []Problem, s *ProblemsPanelState, opt } return s.flat[i].file } - list := codeRows(c, &s.rows, len(s.flat), func(i int) { problemsRow(c, problems, s, s.flat[i], disabled) }) + list := CodeRows(c, &s.rows, len(s.flat), func(i int) { problemsRow(c, problems, s, s.flat[i], disabled) }) if disabled || s.selected < 0 { return } @@ -170,13 +172,13 @@ func problemsGroup(problems []Problem, s *ProblemsPanelState) { } func problemsRow(c *ui.Context, problems []Problem, s *ProblemsPanelState, r problemRow, disabled bool) { - k := Tokens(c) + k := core.Tokens(c) if r.problem < 0 { chev, key := "chevron-down", "search_panel.fold" if s.Collapsed.Has(r.file) { chev, key = "chevron-right", "search_panel.unfold" } - if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), r.file)).Clicked() && !disabled { + if core.IconAction(c, icons.Must(chev), fmt.Sprintf(core.Msg(c, key), r.file)).Clicked() && !disabled { problemsFold(s, r.file) } ui.Text(c, r.file).FontWeight(600).SingleLine().Grow(1).MinWidth(0) @@ -185,10 +187,10 @@ func problemsRow(c *ui.Context, problems []Problem, s *ProblemsPanelState, r pro p := problems[r.problem] col := [3]ui.Color{k.Danger, k.Warning, k.Info}[p.Severity] ui.Icon(c, icons.Must(problemsIcons[p.Severity])).Size(16, 16).Shrink(0).TextColor(col).Margin(0, 0, 0, 28). - Label(msg(c, [3]string{"problems_panel.error", "problems_panel.warning", "problems_panel.info"}[p.Severity])) + Label(core.Msg(c, [3]string{"problems_panel.error", "problems_panel.warning", "problems_panel.info"}[p.Severity])) ui.Text(c, p.Message).SingleLine().Grow(1).MinWidth(0) if p.Source != "" { - codeCaption(c, p.Source).Shrink(0) + CodeCaption(c, p.Source).Shrink(0) } - codeCaption(c, fmt.Sprintf(msg(c, "problems_panel.at"), p.Line, p.Col)).FontFeatures("tnum").Shrink(0) + CodeCaption(c, fmt.Sprintf(core.Msg(c, "problems_panel.at"), p.Line, p.Col)).FontFeatures("tnum").Shrink(0) } diff --git a/problems_panel_test.go b/code/problems_panel_test.go similarity index 97% rename from problems_panel_test.go rename to code/problems_panel_test.go index 2ecf38d..9a2534d 100644 --- a/problems_panel_test.go +++ b/code/problems_panel_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func problemsSample() []Problem { @@ -22,7 +24,7 @@ type problemsProbe struct { } func (p *problemsProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ProblemsPanel(c, p.problems, &p.s, ProblemsPanelOptions{Disabled: p.disabled}) r.Element.Height(360) if r.Opened != nil { diff --git a/process_list.go b/code/process_list.go similarity index 68% rename from process_list.go rename to code/process_list.go index 73b608e..5ec4800 100644 --- a/process_list.go +++ b/code/process_list.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "cmp" @@ -9,9 +9,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Process is one row of a ProcessList. @@ -59,9 +62,9 @@ func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts Proce s.started, s.selected, s.Sort = true, -1, ui.SortOrder{Column: "cpu", Descending: true} } processListSort(procs, s) - k := Tokens(c) + k := core.Tokens(c) var res ProcessListResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() if disabled { s.confirm = false @@ -73,41 +76,41 @@ func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts Proce } res.Element.Children(func() { if len(procs) == 0 { - codeCaption(c, msg(c, "process_list.empty")).Padding(Density(c).Unit() * 4) + CodeCaption(c, core.Msg(c, "process_list.empty")).Padding(core.Density(c).Unit() * 4) return } cols := []ui.TableColumn{ - {Title: msg(c, "process_list.name"), ID: "name", Sortable: true, MinWidth: 120}, - {Title: msg(c, "process_list.pid"), ID: "pid", Width: 80, Align: ui.End, Sortable: true}, - {Title: msg(c, "process_list.cpu"), ID: "cpu", Width: 90, Align: ui.End, Sortable: true}, - {Title: msg(c, "process_list.memory"), ID: "memory", Width: 100, Align: ui.End, Sortable: true}, - {Title: msg(c, "process_list.actions"), ID: "end", Width: 56, Fixed: true, Align: ui.Center}, + {Title: core.Msg(c, "process_list.name"), ID: "name", Sortable: true, MinWidth: 120}, + {Title: core.Msg(c, "process_list.pid"), ID: "pid", Width: 80, Align: ui.End, Sortable: true}, + {Title: core.Msg(c, "process_list.cpu"), ID: "cpu", Width: 90, Align: ui.End, Sortable: true}, + {Title: core.Msg(c, "process_list.memory"), ID: "memory", Width: 100, Align: ui.End, Sortable: true}, + {Title: core.Msg(c, "process_list.actions"), ID: "end", Width: 56, Fixed: true, Align: ui.Center}, } s.rows.Sort = &s.Sort s.rows.Selected = &s.selected s.rows.Key = func(i int) any { return procs[s.order[i]].PID } s.rows.Label = func(i int) string { return procs[s.order[i]].Name } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) table := ui.Table(c, &s.rows, cols, len(procs), func(row, col int) { p := procs[s.order[row]] switch col { case 0: - ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { - codeSelectMark(c, s.selected == row) + ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { + CodeSelectMark(c, s.selected == row) ui.Text(c, p.Name).SingleLine() }) case 1: - codeMono(c, fmt.Sprint(p.PID)) + CodeMono(c, fmt.Sprint(p.PID)) case 2: - codeMono(c, fmt.Sprintf("%.1f%%", p.CPU)) + CodeMono(c, fmt.Sprintf("%.1f%%", p.CPU)) case 3: - codeMono(c, processListBytes(p.Memory)) + CodeMono(c, ProcessListBytes(p.Memory)) case 4: - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "process_list.end"), p.Name)).Clicked() { + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "process_list.end"), p.Name)).Clicked() { ask(p) } } - }).Grow(1).MinHeight(Density(c).ControlHeight() * 4) + }).Grow(1).MinHeight(core.Density(c).ControlHeight() * 4) restore() internal.Ring(table, k.Focus, theme.ControlRadius) if table.Changed() { @@ -118,10 +121,10 @@ func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts Proce } }) if s.confirm { - r := AlertDialog(c, &s.confirm, AlertDialogOptions{ - Title: fmt.Sprintf(msg(c, "process_list.confirm"), s.ending.Name, s.ending.PID), - Description: msg(c, "process_list.warning"), - ConfirmLabel: msg(c, "process_list.endButton"), + r := overlay.AlertDialog(c, &s.confirm, overlay.AlertDialogOptions{ + Title: fmt.Sprintf(core.Msg(c, "process_list.confirm"), s.ending.Name, s.ending.PID), + Description: core.Msg(c, "process_list.warning"), + ConfirmLabel: core.Msg(c, "process_list.endButton"), }) if r.Confirmed && !disabled { p := s.ending @@ -171,7 +174,7 @@ func processListSort(procs []Process, s *ProcessListState) { slog.Debug("mujica: process_list sort", "by", s.Sort.Column, "descending", s.Sort.Descending) } -func processListBytes(b uint64) string { +func ProcessListBytes(b uint64) string { const unit = 1024 switch { case b >= unit*unit*unit: diff --git a/process_list_test.go b/code/process_list_test.go similarity index 97% rename from process_list_test.go rename to code/process_list_test.go index a8a89cf..22af1e7 100644 --- a/process_list_test.go +++ b/code/process_list_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func processListSample() []Process { @@ -16,7 +18,7 @@ func processListSample() []Process { func processListTester(procs []Process, s *ProcessListState, disabled bool, ended *[]int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ProcessList(c, procs, s, ProcessListOptions{Disabled: disabled}) r.Element.Height(300) if r.End != nil { diff --git a/search_panel.go b/code/search_panel.go similarity index 81% rename from search_panel.go rename to code/search_panel.go index 6e14471..8a623f4 100644 --- a/search_panel.go +++ b/code/search_panel.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // SearchMatch is a hit; Start and End index Text. @@ -41,7 +44,7 @@ type searchRow struct{ file, match int } // SearchPanelOptions configure a SearchPanel. type SearchPanelOptions struct { // Status shows a search in progress or failed. - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -68,19 +71,19 @@ func SearchPanel(c *ui.Context, results []SearchFileResult, s *SearchPanelState, if !s.started { s.started, s.selected = true, -1 } - d := Density(c) + d := core.Density(c) var res SearchPanelResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { ui.Column(c).Shrink(0).Padding(d.Unit() * 2).Gap(d.Unit() * 2).Children(func() { - res.Changed = SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled}).Changed() + res.Changed = input.SearchInput(c, &s.Query, input.SearchInputOptions{Disabled: opts.Disabled}).Changed() ui.Row(c).Gap(d.Unit() * 2).Children(func() { for _, f := range []struct { v *string key string }{{&s.Include, "search_panel.include"}, {&s.Exclude, "search_panel.exclude"}} { - r := TextInput(c, f.v, TextInputOptions{Placeholder: msg(c, f.key), Label: msg(c, f.key), Disabled: opts.Disabled}) + r := input.TextInput(c, f.v, input.TextInputOptions{Placeholder: core.Msg(c, f.key), Label: core.Msg(c, f.key), Disabled: opts.Disabled}) r.Element.Grow(1).MinWidth(0) res.Changed = res.Changed || r.Changed() } @@ -90,23 +93,23 @@ func SearchPanel(c *ui.Context, results []SearchFileResult, s *SearchPanelState, slog.Debug("mujica: search_panel query", "query", s.Query, "include", s.Include, "exclude", s.Exclude) } searchPanelFlatten(results, s) - if len(results) == 0 || opts.Status != DataReady { - text := msg(c, "search_panel.idle") + if len(results) == 0 || opts.Status != data.DataReady { + text := core.Msg(c, "search_panel.idle") if s.Query != "" { - text = msg(c, "search_panel.none") + text = core.Msg(c, "search_panel.none") } - res.Changed = dataPlaceholder(c, opts.Status, opts.Error, text, text) || res.Changed + res.Changed = data.DataPlaceholder(c, opts.Status, opts.Error, text, text) || res.Changed return } total := 0 for _, f := range results { total += len(f.Matches) } - codeCaption(c, fmt.Sprintf(msg(c, "search_panel.summary"), total, len(results))).Padding(0, d.Unit()*3, d.Unit(), d.Unit()*3) + CodeCaption(c, fmt.Sprintf(core.Msg(c, "search_panel.summary"), total, len(results))).Padding(0, d.Unit()*3, d.Unit(), d.Unit()*3) s.rows.Selected = &s.selected s.rows.Key = func(i int) any { return s.flat[i] } s.rows.Label = func(i int) string { return searchPanelLabel(results, s.flat[i]) } - list := codeRows(c, &s.rows, len(s.flat), func(i int) { searchPanelRow(c, results, s, s.flat[i], disabled) }) + list := CodeRows(c, &s.rows, len(s.flat), func(i int) { searchPanelRow(c, results, s, s.flat[i], disabled) }) if list.Submitted() && !disabled && s.selected >= 0 { r := s.flat[s.selected] f := results[r.file] @@ -172,26 +175,26 @@ func searchPanelLabel(results []SearchFileResult, r searchRow) string { } func searchPanelRow(c *ui.Context, results []SearchFileResult, s *SearchPanelState, r searchRow, disabled bool) { - k := Tokens(c) + k := core.Tokens(c) f := results[r.file] if r.match < 0 { chev, key := "chevron-down", "search_panel.fold" if s.Collapsed.Has(f.Path) { chev, key = "chevron-right", "search_panel.unfold" } - if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), f.Path)).Clicked() && !disabled { + if core.IconAction(c, icons.Must(chev), fmt.Sprintf(core.Msg(c, key), f.Path)).Clicked() && !disabled { searchPanelFold(s, f.Path, !s.Collapsed.Has(f.Path)) } ui.Icon(c, icons.Must("file-text")).Size(16, 16).Shrink(0).TextColor(k.TextMuted) ui.Text(c, f.Path).FontWeight(600).SingleLine().Grow(1).MinWidth(0) - codeCaption(c, fmt.Sprint(len(f.Matches))).Shrink(0) + CodeCaption(c, fmt.Sprint(len(f.Matches))).Shrink(0) return } m := f.Matches[r.match] if m.Start < 0 || m.End > len(m.Text) || m.Start > m.End { panic("mujica: SearchMatch range outside its Text") } - codeMono(c, fmt.Sprint(m.Line)).Width(codeTextWidth(c, "99999")).TextColor(k.TextMuted).Shrink(0).Margin(0, 0, 0, 28) + CodeMono(c, fmt.Sprint(m.Line)).Width(codeTextWidth(c, "99999")).TextColor(k.TextMuted).Shrink(0).Margin(0, 0, 0, 28) from := max(0, m.Start-40) before := strings.TrimLeft(m.Text[from:m.Start], " \t") if from > 0 { diff --git a/search_panel_test.go b/code/search_panel_test.go similarity index 93% rename from search_panel_test.go rename to code/search_panel_test.go index 23e9102..16a4ffc 100644 --- a/search_panel_test.go +++ b/code/search_panel_test.go @@ -1,9 +1,12 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func searchPanelSample() []SearchFileResult { @@ -22,7 +25,7 @@ type searchPanelProbe struct { } func (p *searchPanelProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SearchPanel(c, p.results, &p.s, p.opts) r.Element.Height(400) if r.Changed { @@ -96,7 +99,7 @@ func TestSearchPanelKeyboard(t *testing.T) { } func TestSearchPanelStatusAndDisabled(t *testing.T) { - p := &searchPanelProbe{opts: SearchPanelOptions{Status: DataFailed, Error: "index unavailable"}} + p := &searchPanelProbe{opts: SearchPanelOptions{Status: data.DataFailed, Error: "index unavailable"}} tt := ui.NewTester(p.view, 560, 420) if !tt.HasText("index unavailable") { t.Fatalf("texts %q", tt.Texts()) diff --git a/symbol_outline.go b/code/symbol_outline.go similarity index 86% rename from symbol_outline.go rename to code/symbol_outline.go index e82bba1..ad6e84f 100644 --- a/symbol_outline.go +++ b/code/symbol_outline.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "log/slog" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SymbolKind is what a Symbol declares. @@ -75,20 +78,20 @@ func SymbolOutline(c *ui.Context, symbols []Symbol, s *SymbolOutlineState, opts s.started, s.cursor = true, -1 } symbolIndex(symbols, s) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var res SymbolOutlineResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { - codeHeader(c, func() { - SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "symbol_outline.filter"), Disabled: opts.Disabled}).Element.Grow(1) + CodeHeader(c, func() { + input.SearchInput(c, &s.Query, input.SearchInputOptions{Placeholder: core.Msg(c, "symbol_outline.filter"), Disabled: opts.Disabled}).Element.Grow(1) }) q := strings.ToLower(strings.TrimSpace(s.Query)) keep := symbolFilter(s, q) roots := symbolKept(s.roots, keep) if len(roots) == 0 { - codeCaption(c, msg(c, "symbol_outline.empty")).Padding(d.Unit() * 4) + CodeCaption(c, core.Msg(c, "symbol_outline.empty")).Padding(d.Unit() * 4) return } follow := opts.CursorLine != s.line @@ -107,20 +110,20 @@ func SymbolOutline(c *ui.Context, symbols []Symbol, s *SymbolOutlineState, opts } return symbolKept(s.nodes[i].kids, keep) } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) e := ui.Outline(c, &s.outline, roots, children, func(i int) { sym := s.nodes[i].sym if sym.Kind < SymbolFunction || sym.Kind > SymbolModule { panic("mujica: Symbol kind out of range") } ui.Row(c).MinHeight(d.ControlHeight() - 2*c.Theme().Space(1)).Gap(d.Unit() * 2).AlignItems(ui.Center).Grow(1).MinWidth(0).Children(func() { - codeBadge(c, symbolBadges[sym.Kind], [...]ui.Color{k.AccentText, k.AccentText, k.Info, k.Info, k.TextMuted, k.TextMuted, k.Warning, k.Success}[sym.Kind]) - ui.RichText(c, codeMatchSpans(c, sym.Name, q)...).SingleLine().MinWidth(0) + CodeBadge(c, symbolBadges[sym.Kind], [...]ui.Color{k.AccentText, k.AccentText, k.Info, k.Info, k.TextMuted, k.TextMuted, k.Warning, k.Success}[sym.Kind]) + ui.RichText(c, CodeMatchSpans(c, sym.Name, q)...).SingleLine().MinWidth(0) if sym.Detail != "" { - codeCaption(c, sym.Detail).MinWidth(0) + CodeCaption(c, sym.Detail).MinWidth(0) } ui.Spacer(c) - codeSelectMark(c, s.cursor >= 0 && s.cursor < s.outline.Rows() && s.outline.Item(s.cursor) == i) + CodeSelectMark(c, s.cursor >= 0 && s.cursor < s.outline.Rows() && s.outline.Item(s.cursor) == i) }) }) restore() diff --git a/symbol_outline_test.go b/code/symbol_outline_test.go similarity index 97% rename from symbol_outline_test.go rename to code/symbol_outline_test.go index 106bb33..b106bc4 100644 --- a/symbol_outline_test.go +++ b/code/symbol_outline_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func symbolSample() []Symbol { @@ -25,7 +27,7 @@ type symbolProbe struct { } func (p *symbolProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SymbolOutline(c, p.symbols, &p.s, p.opts) r.Element.Height(320) if r.Jumped != nil { diff --git a/terminal.go b/code/terminal.go similarity index 97% rename from terminal.go rename to code/terminal.go index b9ee8cf..0f30ee7 100644 --- a/terminal.go +++ b/code/terminal.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "log/slog" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TerminalOptions configure a Terminal. @@ -60,12 +61,12 @@ func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element if opts.Scrollback > 0 { s.limit = opts.Scrollback } - k := Tokens(c) + k := core.Tokens(c) font := codeFont(c) cw, ch, base := terminalMetrics(font) label := opts.Label if label == "" { - label = msg(c, "terminal.label") + label = core.Msg(c, "terminal.label") } e := ui.Box(c).Focusable().Cursor(ui.CursorText).Clip().Label(label).Background(k.Surface). Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled).MinHeight(ch * 4) diff --git a/terminal_search.go b/code/terminal_search.go similarity index 78% rename from terminal_search.go rename to code/terminal_search.go index c7bba0a..6a81ad3 100644 --- a/terminal_search.go +++ b/code/terminal_search.go @@ -1,10 +1,13 @@ -package mujica +package code import ( "log/slog" "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // TerminalSearchState is a search through a terminal's scrollback and screen. @@ -23,25 +26,25 @@ type TerminalSearchOptions struct { } // TerminalSearch finds Query in term and shows the current match. -func TerminalSearch(c *ui.Context, term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions) ChoiceResult { +func TerminalSearch(c *ui.Context, term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions) core.ChoiceResult { if term == nil || s == nil { panic("mujica: TerminalSearch needs a terminal and a state") } term.init() fresh := terminalSearchRun(term, s) - r := SearchInput(c, &s.Query, SearchInputOptions{Find: true, Matches: len(term.matches), Current: &s.Current, - Disabled: opts.Disabled, Label: msg(c, "terminal_search.label")}) - var res ChoiceResult + r := input.SearchInput(c, &s.Query, input.SearchInputOptions{Find: true, Matches: len(term.matches), Current: &s.Current, + Disabled: opts.Disabled, Label: core.Msg(c, "terminal_search.label")}) + var res core.ChoiceResult res.Element = r.Element if r.Changed() { s.Current = 0 terminalSearchRun(term, s) - res.changed = true + res = core.NewChoiceResult(res.Element, true) } if r.Moved() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) } - if res.changed || fresh { + if res.Changed() || fresh { term.current = s.Current if s.Current < len(term.matches) { term.reveal(term.matches[s.Current].pos.Line) diff --git a/terminal_search_test.go b/code/terminal_search_test.go similarity index 97% rename from terminal_search_test.go rename to code/terminal_search_test.go index 584e293..ee89f6e 100644 --- a/terminal_search_test.go +++ b/code/terminal_search_test.go @@ -1,15 +1,17 @@ -package mujica +package code import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func terminalSearchTester(term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions, moves *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { if TerminalSearch(c, term, s, opts).Changed() { *moves++ diff --git a/terminal_state.go b/code/terminal_state.go similarity index 99% rename from terminal_state.go rename to code/terminal_state.go index e3930fb..e3bc55e 100644 --- a/terminal_state.go +++ b/code/terminal_state.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "log/slog" @@ -8,7 +8,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // terminalCell is one grid character; zero is blank. diff --git a/terminal_tabs.go b/code/terminal_tabs.go similarity index 80% rename from terminal_tabs.go rename to code/terminal_tabs.go index fbc4c87..57d8d73 100644 --- a/terminal_tabs.go +++ b/code/terminal_tabs.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TerminalTab is one terminal of a TerminalTabs. @@ -48,8 +51,8 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi if len(*tabs) > 0 && at < 0 { panic("mujica: TerminalTabs active is not a tab ID") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res TerminalTabsResult res.Element = ui.Column(c).MinHeight(0).Disabled(opts.Disabled) st := ui.Local(res.Element, "terminal_tabs", func() terminalTabsState { return terminalTabsState{} }) @@ -83,7 +86,7 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi strip.Children(func() { idx := max(at, 0) base := ui.TabsBase(c, &idx, len(*tabs)) - base.List.Gap(u).Label(msg(c, "terminal_tabs.label")) + base.List.Gap(u).Label(core.Msg(c, "terminal_tabs.label")) base.List.Children(func() { for i, t := range *tabs { rename := func(title string) { @@ -101,7 +104,7 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi } }) if len(*tabs) == 0 { - codeCaption(c, msg(c, "terminal_tabs.empty")).Padding(u * 4) + CodeCaption(c, core.Msg(c, "terminal_tabs.empty")).Padding(u * 4) return } ui.Column(c).Grow(1).MinHeight(0).Padding(u*2, 0, 0, 0).Children(func() { content(*active) }) @@ -113,8 +116,8 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi title = t.Title } } - r := AlertDialog(c, &st.confirm, AlertDialogOptions{Title: fmt.Sprintf(msg(c, "terminal_tabs.confirm"), title), - Description: msg(c, "terminal_tabs.busy"), ConfirmLabel: msg(c, "terminal_tabs.close")}) + r := overlay.AlertDialog(c, &st.confirm, overlay.AlertDialogOptions{Title: fmt.Sprintf(core.Msg(c, "terminal_tabs.confirm"), title), + Description: core.Msg(c, "terminal_tabs.busy"), ConfirmLabel: core.Msg(c, "terminal_tabs.close")}) if r.Confirmed && !disabled { remove(st.closing) } @@ -124,12 +127,12 @@ func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts Termi // terminalTabsTab builds one tab with title and close. func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() tab.MinHeight(d.ControlHeight()).Padding(0, u, 0, u*3).Gap(u).AlignItems(ui.Center).Radius(theme.ControlRadius, theme.ControlRadius, 0, 0).Label(t.Title) if t.Busy { - tab.Description(msg(c, "terminal_tabs.running")) + tab.Description(core.Msg(c, "terminal_tabs.running")) } fg := k.TextMuted switch { @@ -140,7 +143,7 @@ func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st tab.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(tab, Motion(c, theme.StateDuration), fg) + internal.Ease(tab, core.Motion(c, theme.StateDuration), fg) tab.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) { if on { p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 2, W: r.W, H: 2}, k.AccentText, 0) @@ -157,7 +160,7 @@ func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st } tab.Children(func() { if st.editing == t.ID { - in := TextInput(c, &st.draft, TextInputOptions{Label: msg(c, "terminal_tabs.rename")}) + in := input.TextInput(c, &st.draft, input.TextInputOptions{Label: core.Msg(c, "terminal_tabs.rename")}) in.Element.Width(u * 36) in.Input.AutoFocus() switch { @@ -174,7 +177,7 @@ func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st if t.Busy { ui.Box(c).Size(6, 6).Shrink(0).Role(ui.RoleNone).Background(k.AccentText).Radius(3) } - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "terminal_tabs.closeTab"), t.Title)).Clicked() && !disabled { + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "terminal_tabs.closeTab"), t.Title)).Clicked() && !disabled { askClose(t) } }) diff --git a/terminal_tabs_test.go b/code/terminal_tabs_test.go similarity index 96% rename from terminal_tabs_test.go rename to code/terminal_tabs_test.go index e3964c9..5cb28d7 100644 --- a/terminal_tabs_test.go +++ b/code/terminal_tabs_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type terminalTabsProbe struct { @@ -15,7 +17,7 @@ type terminalTabsProbe struct { } func (p *terminalTabsProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TerminalTabs(c, &p.tabs, &p.active, TerminalTabsOptions{Disabled: p.disabled}, func(id string) { ui.Text(c, "Content of "+id) }) @@ -115,7 +117,7 @@ func TestTerminalTabsSnapshot(t *testing.T) { p := ui.Local(c.Root(), "snap", newTerminalTabsProbe) ui.Column(c).Padding(12).Children(func() { TerminalTabs(c, &(*p).tabs, &(*p).active, TerminalTabsOptions{}, func(id string) { - codeMono(c, "~/court $ make seal") + CodeMono(c, "~/court $ make seal") }) }) }) diff --git a/terminal_test.go b/code/terminal_test.go similarity index 98% rename from terminal_test.go rename to code/terminal_test.go index 88cad6b..91985e2 100644 --- a/terminal_test.go +++ b/code/terminal_test.go @@ -1,4 +1,4 @@ -package mujica +package code import ( "runtime" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTerminalStateText(t *testing.T) { @@ -85,7 +87,7 @@ func TestTerminalStateWrapsLongLines(t *testing.T) { func terminalTester(s *TerminalState, opts *TerminalOptions, typed *[]string) *ui.Tester { opts.Input = func(d string) { *typed = append(*typed, d) } return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Terminal(c, s, *opts).Size(400, 200) }, 420, 220) } diff --git a/variables_panel.go b/code/variables_panel.go similarity index 82% rename from variables_panel.go rename to code/variables_panel.go index 6e94759..4cbf3e4 100644 --- a/variables_panel.go +++ b/code/variables_panel.go @@ -1,12 +1,15 @@ -package mujica +package code import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Variable is a debugged value; Changed marks new values. @@ -53,29 +56,29 @@ func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts s.started, s.cursor = true, -1 } variablesIndex(vars, s) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var res VariablesPanelResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() if disabled { s.editing = "" } res.Element.Children(func() { if len(vars) == 0 { - codeCaption(c, msg(c, "variables_panel.empty")).Padding(d.Unit() * 4) + CodeCaption(c, core.Msg(c, "variables_panel.empty")).Padding(d.Unit() * 4) return } s.outline.List.Selected = &s.cursor s.outline.List.Label = func(i int) string { return s.byPath[s.outline.Item(i)].Name } children := func(p string) []string { return s.kids[p] } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) e := ui.Outline(c, &s.outline, s.roots, children, func(p string) { v := s.byPath[p] ui.Row(c).MinHeight(d.ControlHeight() - 2*c.Theme().Space(1)).Gap(d.Unit() * 2).AlignItems(ui.Center).Grow(1).MinWidth(0).Children(func() { - codeMono(c, v.Name).TextColor(k.Info).Shrink(0) + CodeMono(c, v.Name).TextColor(k.Info).Shrink(0) if s.editing == p { - in := TextInput(c, &s.draft, TextInputOptions{Label: msg(c, "variables_panel.value")}) + in := input.TextInput(c, &s.draft, input.TextInputOptions{Label: core.Msg(c, "variables_panel.value")}) in.Element.Grow(1).MinWidth(0) in.Input.AutoFocus() switch { @@ -108,19 +111,19 @@ func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts // variablesValue shows a value, marking changed ones. func variablesValue(c *ui.Context, v *Variable) { - k := Tokens(c) + k := core.Tokens(c) if v.Changed { ui.Box(c).Size(7, 7).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, r.W), k.AccentText) }) } - val := codeMono(c, v.Value).MinWidth(0) + val := CodeMono(c, v.Value).MinWidth(0) if v.Changed { - val.FontWeight(700).TextColor(k.AccentText).Description(msg(c, "variables_panel.changed")) + val.FontWeight(700).TextColor(k.AccentText).Description(core.Msg(c, "variables_panel.changed")) } ui.Spacer(c) if v.Type != "" { - codeCaption(c, v.Type).Shrink(0) + CodeCaption(c, v.Type).Shrink(0) } } diff --git a/variables_panel_test.go b/code/variables_panel_test.go similarity index 97% rename from variables_panel_test.go rename to code/variables_panel_test.go index 67bd40c..5b419b2 100644 --- a/variables_panel_test.go +++ b/code/variables_panel_test.go @@ -1,9 +1,11 @@ -package mujica +package code import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func variablesSample() []Variable { @@ -24,7 +26,7 @@ type variablesProbe struct { } func (p *variablesProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := VariablesPanel(c, p.vars, &p.s, VariablesPanelOptions{Disabled: p.disabled}) r.Element.Height(300) if r.Edited != nil { diff --git a/base_disabled_test.go b/core/base_disabled_test.go similarity index 99% rename from base_disabled_test.go rename to core/base_disabled_test.go index 8356e7a..128b64d 100644 --- a/base_disabled_test.go +++ b/core/base_disabled_test.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "testing" diff --git a/core/choice_result.go b/core/choice_result.go new file mode 100644 index 0000000..1b0e3da --- /dev/null +++ b/core/choice_result.go @@ -0,0 +1,17 @@ +package core + +import ( + "github.com/egoist/mygo/ui" +) + +// ChoiceResult is a choice control's element and whether its value changed this frame. +type ChoiceResult struct { + Element *ui.Element + changed bool +} + +// Changed reports whether the user changed the value in this frame. +func (r ChoiceResult) Changed() bool { return r.changed } + +// NewChoiceResult reports e and whether its value changed this frame. +func NewChoiceResult(e *ui.Element, changed bool) ChoiceResult { return ChoiceResult{e, changed} } diff --git a/datetime.go b/core/datetime.go similarity index 55% rename from datetime.go rename to core/datetime.go index 728dcaf..6968309 100644 --- a/datetime.go +++ b/core/datetime.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "fmt" @@ -7,9 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Date is a calendar day without clock or location; zero means none. @@ -29,7 +29,7 @@ func DateOf(t time.Time) Date { func (d Date) IsZero() bool { return d == Date{} } // Valid reports whether d names a real day. -func (d Date) Valid() bool { return !d.IsZero() && DateOf(d.civil()) == d } +func (d Date) Valid() bool { return !d.IsZero() && DateOf(d.Civil()) == d } // String formats d as 2006-01-02, or "" when empty. func (d Date) String() string { @@ -40,13 +40,13 @@ func (d Date) String() string { } // Format formats d with a Go time layout. -func (d Date) Format(layout string) string { return d.civil().Format(layout) } +func (d Date) Format(layout string) string { return d.Civil().Format(layout) } // Compare returns -1, 0 or 1 as d is before, equal to or after o. -func (d Date) Compare(o Date) int { return d.civil().Compare(o.civil()) } +func (d Date) Compare(o Date) int { return d.Civil().Compare(o.Civil()) } // AddDays returns d moved by n days. -func (d Date) AddDays(n int) Date { return DateOf(d.civil().AddDate(0, 0, n)) } +func (d Date) AddDays(n int) Date { return DateOf(d.Civil().AddDate(0, 0, n)) } // AddMonths returns d moved by n months, keeping the day within the month. func (d Date) AddMonths(n int) Date { @@ -56,74 +56,59 @@ func (d Date) AddMonths(n int) Date { } // Weekday returns the day of the week of d. -func (d Date) Weekday() time.Weekday { return d.civil().Weekday() } +func (d Date) Weekday() time.Weekday { return d.Civil().Weekday() } -// civil is d at midnight on a fixed clock, only for arithmetic. -func (d Date) civil() time.Time { return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.UTC) } +// Civil is d at midnight on a fixed clock, only for arithmetic. +func (d Date) Civil() time.Time { return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.UTC) } -// within reports whether d lies inside the optional bounds lo and hi. -func (d Date) within(lo, hi Date) bool { +// Within reports whether d lies inside the optional bounds lo and hi. +func (d Date) Within(lo, hi Date) bool { return (lo.IsZero() || d.Compare(lo) >= 0) && (hi.IsZero() || d.Compare(hi) <= 0) } -// DateTimeResult is a date or time component and what happened to it this frame. -type DateTimeResult struct { - Element *ui.Element - // Input is the text field inside, for labels and focus; nil without one. - Input *ui.Element - changed bool - canceled bool -} - -// Changed reports whether the component wrote a new value this frame. -func (r DateTimeResult) Changed() bool { return r.changed } - -// Canceled reports whether the user discarded a pending edit this frame. -func (r DateTimeResult) Canceled() bool { return r.canceled } - -// datetimeCheckDate panics when a non-empty date is not a real day. -func datetimeCheckDate(name string, d Date) { +// DatetimeCheckDate panics when a non-empty date is not a real day. +func DatetimeCheckDate(name string, d Date) { if !d.IsZero() && !d.Valid() { panic(fmt.Sprintf("mujica: %s %v is not a calendar date", name, d)) } } -// datetimeCheckBounds panics on invalid or inverted Min and Max. -func datetimeCheckBounds(name string, lo, hi Date) { - datetimeCheckDate(name+" Min", lo) - datetimeCheckDate(name+" Max", hi) +// DatetimeCheckBounds panics on invalid or inverted Min and Max. +func DatetimeCheckBounds(name string, lo, hi Date) { + DatetimeCheckDate(name+" Min", lo) + DatetimeCheckDate(name+" Max", hi) if !lo.IsZero() && !hi.IsZero() && lo.Compare(hi) > 0 { panic(fmt.Sprintf("mujica: %s Min %v is after Max %v", name, lo, hi)) } } -// datetimeToday returns today, or the caller's fixed day when set. -func datetimeToday(c *ui.Context, fixed Date) Date { +// DatetimeToday returns today, or the caller's fixed day when set. +func DatetimeToday(c *ui.Context, fixed Date) Date { if !fixed.IsZero() { - datetimeCheckDate("Today", fixed) + DatetimeCheckDate("Today", fixed) return fixed } return DateOf(c.Now()) } -// datetimeMonthName returns the localized name of m. -func datetimeMonthName(c *ui.Context, m time.Month) string { - return msg(c, fmt.Sprintf("calendar.month_%d", int(m))) +// DatetimeMonthName returns the localized name of m. +func DatetimeMonthName(c *ui.Context, m time.Month) string { + return Msg(c, fmt.Sprintf("calendar.month_%d", int(m))) } -// datetimeMonthTitle returns the localized title of a month, as "October 2026". -func datetimeMonthTitle(c *ui.Context, y int, m time.Month) string { - return fmt.Sprintf(msg(c, "calendar.title"), datetimeMonthName(c, m), y) +// DatetimeMonthTitle returns the localized title of a month, as "October 2026". +func DatetimeMonthTitle(c *ui.Context, y int, m time.Month) string { + return fmt.Sprintf(Msg(c, "calendar.title"), DatetimeMonthName(c, m), y) } -// datetimeDayLabel returns the localized spoken name of d. -func datetimeDayLabel(c *ui.Context, d Date) string { - wd := msg(c, fmt.Sprintf("calendar.weekday_long_%d", int(d.Weekday()))) - return fmt.Sprintf(msg(c, "calendar.day_label"), wd, datetimeMonthName(c, d.Month), d.Day, d.Year) +// DatetimeDayLabel returns the localized spoken name of d. +func DatetimeDayLabel(c *ui.Context, d Date) string { + wd := Msg(c, fmt.Sprintf("calendar.weekday_long_%d", int(d.Weekday()))) + return fmt.Sprintf(Msg(c, "calendar.day_label"), wd, DatetimeMonthName(c, d.Month), d.Day, d.Year) } -// datetimeField styles row as a single-line field: solid fill and fine border. -func datetimeField(c *ui.Context, row *ui.Element, invalid, readOnly bool) *ui.Element { +// DatetimeField styles row as a single-line field: solid fill and fine border. +func DatetimeField(c *ui.Context, row *ui.Element, invalid, readOnly bool) *ui.Element { k := Tokens(c) d := Density(c) bg, border := k.Surface, k.ControlBorder @@ -141,8 +126,8 @@ func datetimeField(c *ui.Context, row *ui.Element, invalid, readOnly bool) *ui.E AlignItems(ui.Center).Radius(theme.ControlRadius).Background(bg).Border(theme.BorderWidth, border) } -// datetimeTrigger is the popup button inside a field; Cmd+C copies its text. -func datetimeTrigger(c *ui.Context, text, placeholder, icon string) *ui.Element { +// DatetimeTrigger is the popup button inside a field; Cmd+C copies its text. +func DatetimeTrigger(c *ui.Context, text, placeholder, icon string) *ui.Element { k := Tokens(c) b := ui.ButtonBase(c).Grow(1).Shrink(1).Justify(ui.Start).Gap(Density(c).Unit() * 2). MinHeight(theme.MinHitSize).Radius(theme.ControlRadius).Role(ui.RolePopUpButton) @@ -161,35 +146,35 @@ func datetimeTrigger(c *ui.Context, text, placeholder, icon string) *ui.Element return b } -// datetimePopup shows fn below anchor while *open; a disabled or read-only anchor closes it. -func datetimePopup(c *ui.Context, anchor *ui.Element, open *bool, readOnly bool, fn func()) *ui.Element { +// DatetimePopup shows fn below anchor while *open; a disabled or read-only anchor closes it. +func DatetimePopup(c *ui.Context, anchor *ui.Element, open *bool, readOnly bool, fn func()) *ui.Element { d := Density(c) if *open && (readOnly || anchor.IsDisabled()) { slog.Debug("mujica: datetime popup closed by state", "readOnly", readOnly) *open = false } return ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + OverlayStyle(c, panel, OverlayEnter(c, panel)) panel.MaxWidth(d.ControlHeight()*7+d.Unit()*6+2*theme.BorderWidth).Margin(d.Unit(), 0, 0, 0).Padding(d.Unit() * 3).Gap(d.Unit() * 3) fn() }) } -// datetimeSelectedMark strokes the 1 DIP inset frame that marks a chosen cell. -func datetimeSelectedMark(p *ui.Painter, r ui.Rect, c ui.Color) { +// DatetimeSelectedMark strokes the 1 DIP inset frame that marks a chosen cell. +func DatetimeSelectedMark(p *ui.Painter, r ui.Rect, c ui.Color) { const inset = 2 p.Stroke(ui.Rect{X: r.X + inset, Y: r.Y + inset, W: r.W - 2*inset, H: r.H - 2*inset}, c, theme.ControlRadius/2, theme.BorderWidth) } -// datetimeError shows a field error in Danger beneath a control. -func datetimeError(c *ui.Context, text string) { +// DatetimeError shows a field error in Danger beneath a control. +func DatetimeError(c *ui.Context, text string) { if text == "" { return } ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).Danger).Role(ui.RoleStatus) } -// datetimeFooter lays out popup actions at the end of a row. -func datetimeFooter(c *ui.Context, fn func()) { +// DatetimeFooter lays out popup actions at the end of a row. +func DatetimeFooter(c *ui.Context, fn func()) { ui.Row(c).Gap(Density(c).Unit() * 2).Justify(ui.End).AlignItems(ui.Center).FillWidth().Children(fn) } diff --git a/docs_test.go b/core/docs_test.go similarity index 87% rename from docs_test.go rename to core/docs_test.go index c7e169a..f74f4e8 100644 --- a/docs_test.go +++ b/core/docs_test.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "os" @@ -57,12 +57,12 @@ func readGlob(t *testing.T, pattern string) map[string]string { // TestDocSnippetsAreCompiledExamples keeps each doc's first Go block, in order, inside one Example. func TestDocSnippetsAreCompiledExamples(t *testing.T) { var examples [][]string - for _, src := range readGlob(t, "example_*_test.go") { + for _, src := range readGlob(t, "../*/example_*_test.go") { for _, fn := range strings.Split(src, "\nfunc Example")[1:] { examples = append(examples, docsLines(fn)) } } - for d, src := range readGlob(t, "docs/components/*.md") { + for d, src := range readGlob(t, "../docs/components/*.md") { m := docsGoBlock.FindStringSubmatch(src) if m == nil { t.Errorf("%s has no Go example", d) @@ -83,17 +83,17 @@ func TestDocSnippetsAreCompiledExamples(t *testing.T) { // TestDocsMatchGalleryPages wants exactly one doc and one gallery page per component ID. func TestDocsMatchGalleryPages(t *testing.T) { - if n := len(readGlob(t, "docs/components/*.md")); n != 318 { + if n := len(readGlob(t, "../docs/components/*.md")); n != 318 { t.Fatalf("component docs = %d, want 318", n) } pages := map[string]int{} - for _, src := range readGlob(t, "examples/gallery/pages_*.go") { + for _, src := range readGlob(t, "../examples/gallery/pages_*.go") { for _, m := range docsPageID.FindAllStringSubmatch(src, -1) { pages[m[1]]++ } } docs := map[string]int{} - for d := range readGlob(t, "docs/components/*.md") { + for d := range readGlob(t, "../docs/components/*.md") { docs[filepath.Base(d)[:3]]++ } for id, n := range pages { diff --git a/core/example_feedback_test.go b/core/example_feedback_test.go new file mode 100644 index 0000000..c967ca6 --- /dev/null +++ b/core/example_feedback_test.go @@ -0,0 +1,15 @@ +package core_test + +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +func ExampleSpinner() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + core.Spinner(c, core.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/core/geometry.go b/core/geometry.go new file mode 100644 index 0000000..67208d0 --- /dev/null +++ b/core/geometry.go @@ -0,0 +1,28 @@ +package core + +import ( + "math" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/internal" +) + +// ProgressArc is the arc from turn a to turn b, clockwise from twelve o'clock. +func ProgressArc(pt *ui.Painter, cx, cy, r float32, a, b float32) *ui.Path { + var p ui.Path + n := max(2, int(math.Ceil(float64((b-a)*64)))) + for i := 0; i <= n; i++ { + t := float64(a + (b-a)*float32(i)/float32(n)) + x := cx + r*float32(math.Sin(2*math.Pi*t)) + y := cy - r*float32(math.Cos(2*math.Pi*t)) + if i == 0 { + p.MoveTo(internal.Snap(pt, x), internal.Snap(pt, y)) + } else { + p.LineTo(internal.Snap(pt, x), internal.Snap(pt, y)) + } + } + return &p +} + +func Abs32(v float32) float32 { return float32(math.Abs(float64(v))) } diff --git a/i18n.go b/core/i18n.go similarity index 84% rename from i18n.go rename to core/i18n.go index b0d08e9..e9c17f8 100644 --- a/i18n.go +++ b/core/i18n.go @@ -1,6 +1,8 @@ -package mujica +package core -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" +) // Locale picks the built-in copy. The zero value is English. type Locale string @@ -22,8 +24,8 @@ func addMessages(m map[string][2]string) { } } -// msg returns the window's copy for key. -func msg(c *ui.Context, key string) string { +// Msg returns the window's copy for key. +func Msg(c *ui.Context, key string) string { w := current(c) if s, ok := w.messages[key]; ok { return s diff --git a/core/icon_action.go b/core/icon_action.go new file mode 100644 index 0000000..cf6d29e --- /dev/null +++ b/core/icon_action.go @@ -0,0 +1,23 @@ +package core + +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// IconAction is a square, borderless button showing icon, named label. +func IconAction(c *ui.Context, icon *ui.SVG, label string) *ui.Element { + k := Tokens(c) + b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted) + internal.Ring(b, k.Focus, theme.ControlRadius) + switch { + case b.Pressed(): + b.Background(k.Selection).TextColor(k.Text) + case b.Hovered(): + b.Background(k.SurfaceHover).TextColor(k.Text) + } + b.Children(func() { ui.Icon(c, icon).Size(16, 16) }) + return b +} diff --git a/media_helpers_test.go b/core/media_helpers_test.go similarity index 98% rename from media_helpers_test.go rename to core/media_helpers_test.go index b6e7fc4..0a4a699 100644 --- a/media_helpers_test.go +++ b/core/media_helpers_test.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "image" diff --git a/core/media_icons.go b/core/media_icons.go new file mode 100644 index 0000000..b44a08b --- /dev/null +++ b/core/media_icons.go @@ -0,0 +1,53 @@ +package core + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" +) + +// MediaSVG wraps stroked 24×24 path data as an icon. +func MediaSVG(body string) *ui.SVG { + return ui.MustParseSVG([]byte(`` + body + ``)) +} + +// Media glyphs drawn for this group. +var ( + MediaPlay = MediaSVG(``) + MediaPause = MediaSVG(``) + MediaPrev = MediaSVG(``) + MediaNext = MediaSVG(``) + MediaRepeat = MediaSVG(``) + MediaRepeatOne = MediaSVG(``) + MediaShuffle = MediaSVG(``) + MediaVolume = MediaSVG(``) + MediaMute = MediaSVG(``) + MediaRotate = MediaSVG(``) + MediaRotateLeft = MediaSVG(``) + MediaMirror = MediaSVG(``) + MediaPiP = MediaSVG(``) + MediaFullscreen = MediaSVG(``) + MediaCaptions = MediaSVG(``) + MediaRecord = MediaSVG(``) + MediaStop = MediaSVG(``) + MediaMic = MediaSVG(``) + MediaCamera = MediaSVG(``) + MediaSpeaker = MediaSVG(``) + MediaRect = MediaSVG(``) + MediaArrow = MediaSVG(``) + MediaText = MediaSVG(``) + MediaPointer = MediaSVG(``) +) + +// MediaClock writes d as m:ss, or h:mm:ss past an hour. +func MediaClock(d time.Duration) string { + if d < 0 { + d = 0 + } + s := int(d / time.Second) + if s >= 3600 { + return fmt.Sprintf("%d:%02d:%02d", s/3600, s/60%60, s%60) + } + return fmt.Sprintf("%d:%02d", s/60, s%60) +} diff --git a/messages_account.go b/core/messages_account.go similarity index 99% rename from messages_account.go rename to core/messages_account.go index b0884d0..e5ed6ad 100644 --- a/messages_account.go +++ b/core/messages_account.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_agent.go b/core/messages_agent.go similarity index 99% rename from messages_agent.go rename to core/messages_agent.go index 6bd824e..40b7871 100644 --- a/messages_agent.go +++ b/core/messages_agent.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_calendar.go b/core/messages_calendar.go similarity index 98% rename from messages_calendar.go rename to core/messages_calendar.go index af24774..e4258ba 100644 --- a/messages_calendar.go +++ b/core/messages_calendar.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "fmt" @@ -6,12 +6,12 @@ import ( "github.com/egoist/mygo/ui" ) -// calendarCount formats a count, using "_one" for one. -func calendarCount(c *ui.Context, key string, n int) string { +// CalendarCount formats a count, using "_one" for one. +func CalendarCount(c *ui.Context, key string, n int) string { if n == 1 { key += "_one" } - return fmt.Sprintf(msg(c, key), n) + return fmt.Sprintf(Msg(c, key), n) } func init() { diff --git a/messages_charts1.go b/core/messages_charts1.go similarity index 99% rename from messages_charts1.go rename to core/messages_charts1.go index e207570..585c83e 100644 --- a/messages_charts1.go +++ b/core/messages_charts1.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_charts2.go b/core/messages_charts2.go similarity index 98% rename from messages_charts2.go rename to core/messages_charts2.go index a214257..84f8001 100644 --- a/messages_charts2.go +++ b/core/messages_charts2.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_chatinput.go b/core/messages_chatinput.go similarity index 99% rename from messages_chatinput.go rename to core/messages_chatinput.go index 5edeb29..285c706 100644 --- a/messages_chatinput.go +++ b/core/messages_chatinput.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_chatmsg.go b/core/messages_chatmsg.go similarity index 99% rename from messages_chatmsg.go rename to core/messages_chatmsg.go index 2ebff69..983e2a6 100644 --- a/messages_chatmsg.go +++ b/core/messages_chatmsg.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_chatsession.go b/core/messages_chatsession.go similarity index 99% rename from messages_chatsession.go rename to core/messages_chatsession.go index 94f4957..bfc515d 100644 --- a/messages_chatsession.go +++ b/core/messages_chatsession.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_choice.go b/core/messages_choice.go similarity index 99% rename from messages_choice.go rename to core/messages_choice.go index 5075a5a..6a5ae1c 100644 --- a/messages_choice.go +++ b/core/messages_choice.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_code.go b/core/messages_code.go similarity index 99% rename from messages_code.go rename to core/messages_code.go index 3f210e4..047ccd2 100644 --- a/messages_code.go +++ b/core/messages_code.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_data.go b/core/messages_data.go similarity index 99% rename from messages_data.go rename to core/messages_data.go index bdc75f2..6f54079 100644 --- a/messages_data.go +++ b/core/messages_data.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_datetime.go b/core/messages_datetime.go similarity index 99% rename from messages_datetime.go rename to core/messages_datetime.go index 2b4dd2d..a4eb09f 100644 --- a/messages_datetime.go +++ b/core/messages_datetime.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_devtools.go b/core/messages_devtools.go similarity index 99% rename from messages_devtools.go rename to core/messages_devtools.go index b1caaeb..b3e1129 100644 --- a/messages_devtools.go +++ b/core/messages_devtools.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_display.go b/core/messages_display.go similarity index 97% rename from messages_display.go rename to core/messages_display.go index b92688a..8fbdf3d 100644 --- a/messages_display.go +++ b/core/messages_display.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_essentials.go b/core/messages_essentials.go similarity index 99% rename from messages_essentials.go rename to core/messages_essentials.go index 4c8abe1..c07ad79 100644 --- a/messages_essentials.go +++ b/core/messages_essentials.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_feedback.go b/core/messages_feedback.go similarity index 99% rename from messages_feedback.go rename to core/messages_feedback.go index 527754c..cf18e72 100644 --- a/messages_feedback.go +++ b/core/messages_feedback.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_files.go b/core/messages_files.go similarity index 99% rename from messages_files.go rename to core/messages_files.go index 936f37a..fb8220a 100644 --- a/messages_files.go +++ b/core/messages_files.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_form.go b/core/messages_form.go similarity index 99% rename from messages_form.go rename to core/messages_form.go index 8366836..3c7e340 100644 --- a/messages_form.go +++ b/core/messages_form.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_git.go b/core/messages_git.go similarity index 99% rename from messages_git.go rename to core/messages_git.go index 445a4cd..99835c5 100644 --- a/messages_git.go +++ b/core/messages_git.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_input.go b/core/messages_input.go similarity index 98% rename from messages_input.go rename to core/messages_input.go index 5797897..d63d263 100644 --- a/messages_input.go +++ b/core/messages_input.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_layout.go b/core/messages_layout.go similarity index 96% rename from messages_layout.go rename to core/messages_layout.go index 5563c39..cc09021 100644 --- a/messages_layout.go +++ b/core/messages_layout.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_markets.go b/core/messages_markets.go similarity index 99% rename from messages_markets.go rename to core/messages_markets.go index cf1e5d4..f1d3cd6 100644 --- a/messages_markets.go +++ b/core/messages_markets.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_media.go b/core/messages_media.go similarity index 99% rename from messages_media.go rename to core/messages_media.go index fcb1f82..3a44fcd 100644 --- a/messages_media.go +++ b/core/messages_media.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_messaging.go b/core/messages_messaging.go similarity index 98% rename from messages_messaging.go rename to core/messages_messaging.go index c479840..67134f2 100644 --- a/messages_messaging.go +++ b/core/messages_messaging.go @@ -1,10 +1,11 @@ -package mujica +package core import ( - "github.com/egoist/mygo/ui" "strconv" "strings" "time" + + "github.com/egoist/mygo/ui" ) func init() { @@ -249,7 +250,7 @@ func init() { }) } -func messagingDate(c *ui.Context, date time.Time, key string) string { - weekday := msg(c, "calendar.weekday_long_"+strconv.Itoa(int(date.Weekday()))) - return strings.ReplaceAll(date.Format(msg(c, key)), "{weekday}", weekday) +func MessagingDate(c *ui.Context, date time.Time, key string) string { + weekday := Msg(c, "calendar.weekday_long_"+strconv.Itoa(int(date.Weekday()))) + return strings.ReplaceAll(date.Format(Msg(c, key)), "{weekday}", weekday) } diff --git a/messages_mktchart.go b/core/messages_mktchart.go similarity index 99% rename from messages_mktchart.go rename to core/messages_mktchart.go index 61c3fef..012597a 100644 --- a/messages_mktchart.go +++ b/core/messages_mktchart.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_motion.go b/core/messages_motion.go similarity index 99% rename from messages_motion.go rename to core/messages_motion.go index 3247d1a..cdc5d0e 100644 --- a/messages_motion.go +++ b/core/messages_motion.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_navigation.go b/core/messages_navigation.go similarity index 99% rename from messages_navigation.go rename to core/messages_navigation.go index 8474db7..47ef72c 100644 --- a/messages_navigation.go +++ b/core/messages_navigation.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_overlay.go b/core/messages_overlay.go similarity index 96% rename from messages_overlay.go rename to core/messages_overlay.go index c2d590c..e6103f3 100644 --- a/messages_overlay.go +++ b/core/messages_overlay.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_projects.go b/core/messages_projects.go similarity index 99% rename from messages_projects.go rename to core/messages_projects.go index 11adf92..6358efe 100644 --- a/messages_projects.go +++ b/core/messages_projects.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/messages_trading.go b/core/messages_trading.go similarity index 99% rename from messages_trading.go rename to core/messages_trading.go index 8f72fe7..c6140e8 100644 --- a/messages_trading.go +++ b/core/messages_trading.go @@ -1,4 +1,4 @@ -package mujica +package core func init() { addMessages(map[string][2]string{ diff --git a/motion_delivery_test.go b/core/motion_delivery_test.go similarity index 86% rename from motion_delivery_test.go rename to core/motion_delivery_test.go index f67cc27..48ae8b9 100644 --- a/motion_delivery_test.go +++ b/core/motion_delivery_test.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "go/ast" @@ -11,12 +11,12 @@ import ( ) func TestMotionDocsMatchExampleBodies(t *testing.T) { - src, err := os.ReadFile("example_motion_test.go") + src, err := os.ReadFile("../feedback/example_motion_test.go") if err != nil { t.Fatal(err) } fs := token.NewFileSet() - file, err := parser.ParseFile(fs, "example_motion_test.go", src, 0) + file, err := parser.ParseFile(fs, "../feedback/example_motion_test.go", src, 0) if err != nil { t.Fatal(err) } @@ -34,7 +34,7 @@ func TestMotionDocsMatchExampleBodies(t *testing.T) { examples[strings.TrimPrefix(fn.Name.Name, "Example")] = strings.TrimSpace(strings.Join(lines, "\n")) } count := 0 - for path, doc := range readGlob(t, "docs/components/*.md") { + for path, doc := range readGlob(t, "../docs/components/*.md") { id := filepath.Base(path)[:3] if id < "281" || id > "300" { continue diff --git a/overlay_layers.go b/core/overlay_layers.go similarity index 80% rename from overlay_layers.go rename to core/overlay_layers.go index 381fc35..ae96232 100644 --- a/overlay_layers.go +++ b/core/overlay_layers.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "log/slog" @@ -16,8 +16,8 @@ type overlayLayers struct { type overlayLayersKey struct{} -// overlayLayer marks the layer key open in this frame and reports whether it is on top. -func overlayLayer(c *ui.Context, key any) bool { +// OverlayLayer marks the layer key open in this frame and reports whether it is on top. +func OverlayLayer(c *ui.Context, key any) bool { s := ui.Local(c.Root(), overlayLayersKey{}, func() overlayLayers { return overlayLayers{} }) if now := c.Now(); now != s.frame { s.prev, s.frame = s.frame, now @@ -45,9 +45,9 @@ func overlayLayer(c *ui.Context, key any) bool { return at == len(s.keys)-1 } -// overlayEscape reports Escape for a window-level layer, delivered only while it is on top. -func overlayEscape(c *ui.Context, key any) bool { - if !overlayLayer(c, key) { +// OverlayEscape reports Escape for a window-level layer, delivered only while it is on top. +func OverlayEscape(c *ui.Context, key any) bool { + if !OverlayLayer(c, key) { return false } // The root is around every focus, so it takes Escape before MyGo's overlays. @@ -75,8 +75,8 @@ type overlaySiteState struct { holding bool // the site holds the fallback focus } -// overlaySite builds the invisible focus fallback at a layer's call site. -func overlaySite(c *ui.Context) (*ui.Element, *overlaySiteState) { +// OverlaySite builds the invisible focus fallback at a layer's call site. +func OverlaySite(c *ui.Context) (*ui.Element, *overlaySiteState) { site := ui.Box(c).Size(0, 0).FocusRing(false) st := ui.Local(site, "mujica.overlay.site", func() overlaySiteState { return overlaySiteState{} }) st.holding = st.holding && site.FocusWithin() @@ -88,13 +88,13 @@ func overlaySite(c *ui.Context) (*ui.Element, *overlaySiteState) { return site, st } -// shown records whether the focus is in the open layer's panel. -func (st *overlaySiteState) shown(panel *ui.Element) { +// Shown records whether the focus is in the open layer's panel. +func (st *overlaySiteState) Shown(panel *ui.Element) { st.focusIn, st.wait = panel != nil && panel.FocusWithin(), 0 } -// closed moves a lost focus to the site once MyGo has had its chance to restore it. -func (st *overlaySiteState) closed(c *ui.Context, site *ui.Element) { +// Closed moves a lost focus to the site once MyGo has had its chance to restore it. +func (st *overlaySiteState) Closed(c *ui.Context, site *ui.Element) { if st.focusIn { st.focusIn, st.wait = false, 2 // MyGo prunes the panel at the end of the next frame } diff --git a/core/overlay_motion.go b/core/overlay_motion.go new file mode 100644 index 0000000..5ed2bff --- /dev/null +++ b/core/overlay_motion.go @@ -0,0 +1,44 @@ +package core + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// OverlayEnter returns how far, 0 to 1, the popup born with e has entered. +func OverlayEnter(c *ui.Context, e *ui.Element) float32 { + born := ui.Local(e, "mujica.overlay.born", func() time.Time { return c.Now() }) + d := Motion(c, theme.PopupDuration) + if d == 0 { + return 1 + } + t := float32(c.Now().Sub(*born)) / float32(d) + if t >= 1 { + return 1 + } + c.AnimationFrame() + return ui.EaseOut(t) +} + +// OverlayStyle gives a popup panel the surface, edge and shadow of the theme. +func OverlayStyle(c *ui.Context, panel *ui.Element, p float32) { + k := Tokens(c) + panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius). + TextColor(k.Text).Opacity(p).Shadow(0, 6, 18, 0, ui.RGBA(0, 0, 0, 0.16*p)) +} + +// PlaceOverlay puts a w×h box below anchor a, above when there is no room, inside the window. +func PlaceOverlay(c *ui.Context, a ui.Rect, w, h, gap float32) (x, y float32) { + ww, wh := c.Size() + const edge = 4 + x, y = a.X, a.Y+a.H+gap + if y+h > wh-edge && a.Y-gap-h >= edge { + y = a.Y - gap - h + } + x = max(edge, min(x, ww-edge-w)) + y = max(edge, min(y, wh-edge-h)) + return x, y +} diff --git a/core/severity.go b/core/severity.go new file mode 100644 index 0000000..55283fb --- /dev/null +++ b/core/severity.go @@ -0,0 +1,59 @@ +package core + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// Severity is the meaning of a message. The zero value is SeverityInfo. +type Severity int + +const ( + SeverityInfo Severity = iota + SeveritySuccess + SeverityWarning + SeverityError +) + +func (s Severity) String() string { + switch s { + case SeverityInfo: + return "info" + case SeveritySuccess: + return "success" + case SeverityWarning: + return "warning" + case SeverityError: + return "error" + } + panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) +} + +// SeverityLook returns the color and icon that, with text, mark s. +func SeverityLook(k theme.Tokens, s Severity) (ui.Color, *ui.SVG) { + switch s { + case SeveritySuccess: + return k.Success, icons.Must("circle-check") + case SeverityWarning: + return k.Warning, icons.Must("triangle-alert") + case SeverityError: + return k.Danger, icons.Must("circle-x") + case SeverityInfo: + return k.Info, icons.Must("info") + } + panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) +} + +// AlertResult is what an Alert built and reported this frame. +type AlertResult struct { + // Element is nil while the alert is closed. + Element *ui.Element + // Action reports a click on the action button. + Action bool + // Closed reports the user dismissing the alert. + Closed bool +} diff --git a/core/snapshot_test.go b/core/snapshot_test.go new file mode 100644 index 0000000..d65ec40 --- /dev/null +++ b/core/snapshot_test.go @@ -0,0 +1,35 @@ +package core + +import ( + "image" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := Light + if mode == "dark" { + m = Dark + } + Use(c, Settings{Mode: m}) +} + +func encode(t *testing.T, img image.Image) []byte { + t.Helper() + return snaptest.Encode(t, img) +} diff --git a/spinner.go b/core/spinner.go similarity index 95% rename from spinner.go rename to core/spinner.go index 280f968..6c7bb14 100644 --- a/spinner.go +++ b/core/spinner.go @@ -1,4 +1,4 @@ -package mujica +package core import ( "fmt" @@ -6,7 +6,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // SpinnerOptions configure a Spinner. Size is 16 DIPs when zero. @@ -31,7 +31,7 @@ func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element { } label := opts.Label if label == "" { - label = msg(c, "loading") + label = Msg(c, "loading") } color := opts.Color if color.A == 0 { diff --git a/theme.go b/core/theme.go similarity index 83% rename from theme.go rename to core/theme.go index 9abb1a3..fefb76a 100644 --- a/theme.go +++ b/core/theme.go @@ -1,5 +1,5 @@ -// Package mujica is a court-gothic component library for MyGo native UI. -package mujica +// Package core holds MujicaUI's theme, copy and the types and helpers every category shares. +package core import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Mode chooses the appearance of a window. @@ -118,14 +118,27 @@ func current(c *ui.Context) windowTheme { return w } +// CurrentSettings returns settings that rebuild the window's theme, its tokens pinned, for offscreen renders. +func CurrentSettings(c *ui.Context) Settings { + w := current(c) + s := Settings{Mode: Light, Density: w.density, Locale: w.locale, Messages: w.messages} + t := w.tokens + if t.Dark { + s.Mode, s.Dark = Dark, &t + } else { + s.Light = &t + } + return s +} + // Tokens returns the window's colors. func Tokens(c *ui.Context) theme.Tokens { return current(c).tokens } // Density returns the window's density. func Density(c *ui.Context) theme.Density { return current(c).density } -// colorFade eases background and border colors between states; MyGo skips it under reduced motion. -var colorFade = ui.ElementTransition{Colors: true, Duration: theme.StateDuration} +// ColorFade eases background and border colors between states; MyGo skips it under reduced motion. +var ColorFade = ui.ElementTransition{Colors: true, Duration: theme.StateDuration} // Motion returns d, or zero when the desktop asks for reduced motion. func Motion(c *ui.Context, d time.Duration) time.Duration { diff --git a/theme_test.go b/core/theme_test.go similarity index 95% rename from theme_test.go rename to core/theme_test.go index cd9718c..97f1a70 100644 --- a/theme_test.go +++ b/core/theme_test.go @@ -1,11 +1,11 @@ -package mujica +package core import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestUseLineHeight(t *testing.T) { diff --git a/core/trend.go b/core/trend.go new file mode 100644 index 0000000..dae7e10 --- /dev/null +++ b/core/trend.go @@ -0,0 +1,96 @@ +package core + +import ( + "fmt" + "math" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/icons" +) + +// Trend picks the rise color: green, or red (China). +type Trend int + +type trendKey struct{} + +// UseTrend sets the window's rise and fall colors. +func UseTrend(c *ui.Context, t Trend) { + if t != GreenUp && t != RedUp { + panic(fmt.Sprintf("mujica: unknown Trend %d", t)) + } + *ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) = t +} + +// TrendColor colors a rise (sign > 0), fall or flat. +func TrendColor(c *ui.Context, sign int) ui.Color { + k := Tokens(c) + up, down := k.Success, k.Danger + if *ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) == RedUp { + up, down = k.Danger, k.Success + } + switch { + case sign > 0: + return up + case sign < 0: + return down + } + return k.TextMuted +} + +// TrendIcon is the arrow that says the direction without color. +func TrendIcon(sign int) *ui.SVG { + switch { + case sign > 0: + return icons.Must("arrow-up") + case sign < 0: + return icons.Must("arrow-down") + } + return icons.Must("minus") +} + +// TrendWord names the direction for assistive technology. +func TrendWord(c *ui.Context, sign int) string { + switch { + case sign > 0: + return Msg(c, "price_text.up") + case sign < 0: + return Msg(c, "price_text.down") + } + return Msg(c, "price_text.flat") +} + +func TrendSign(v float64) int { + switch { + case v > 0: + return 1 + case v < 0: + return -1 + } + return 0 +} + +// PriceDecimals resolves a Decimals option: zero means 2, negative panics. +func PriceDecimals(name string, d int) int { + if d < 0 { + panic("mujica: " + name + " with negative Decimals") + } + if d == 0 { + return 2 + } + return d +} + +// PriceUngrouped writes v with dec decimals and no separators. +func PriceUngrouped(v float64, dec int) string { + s := fmt.Sprintf("%.*f", dec, math.Abs(v)) + if v < 0 && s != fmt.Sprintf("%.*f", dec, 0.0) { + return "−" + s + } + return s +} + +const ( + GreenUp Trend = iota + RedUp +) diff --git a/accordion.go b/data/accordion.go similarity index 89% rename from accordion.go rename to data/accordion.go index 4e752f7..0b1aac0 100644 --- a/accordion.go +++ b/data/accordion.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "log/slog" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AccordionItem is one section of an Accordion. @@ -29,7 +30,7 @@ type AccordionOptions struct { } // Accordion shows sections whose open IDs live in *open, which the caller controls. -func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) ChoiceResult { +func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts AccordionOptions) core.ChoiceResult { seen := map[string]bool{} for _, it := range items { if it.ID == "" || seen[it.ID] || it.Content == nil { @@ -37,10 +38,10 @@ func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts Accord } seen[it.ID] = true } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() - var v ChoiceResult + var v core.ChoiceResult v.Element = ui.Column(c).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius) focusTo := ui.Local(v.Element, "focus", func() int { return -1 }) v.Element.Children(func() { @@ -66,10 +67,10 @@ func Accordion(c *ui.Context, open *[]string, items []AccordionItem, opts Accord *focusTo = j } if isOpen != was && !it.Disabled && accordionSet(open, it.ID, isOpen, opts) { - v.changed = true + v = core.NewChoiceResult(v.Element, true) } isOpen = slices.Contains(*open, it.ID) - prog := tr.Animate("mujica.open", collapsibleTarget(isOpen), Motion(c, theme.StateDuration)) + prog := tr.Animate("mujica.open", collapsibleTarget(isOpen), core.Motion(c, theme.StateDuration)) tr.Children(func() { ui.Text(c, it.Title).FontWeight(600).Grow(1).MinWidth(0) ui.Icon(c, icons.Must("chevron-down")).Size(16, 16).Rotate(180 * prog).TextColor(k.TextMuted).Shrink(0) diff --git a/accordion_test.go b/data/accordion_test.go similarity index 96% rename from accordion_test.go rename to data/accordion_test.go index a0b0778..0a33b40 100644 --- a/accordion_test.go +++ b/data/accordion_test.go @@ -1,15 +1,17 @@ -package mujica +package data import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func accordionHarness(open *[]string, opts *AccordionOptions, changes *int) func(c *ui.Context) { return func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) items := []AccordionItem{ {ID: "a", Title: "Choir", Content: func() { ui.Text(c, "Choir body") }}, {ID: "b", Title: "Nave", Content: func() { ui.Text(c, "Nave body") }}, @@ -103,7 +105,7 @@ func TestAccordionBadItems(t *testing.T) { }() var open []string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Accordion(c, &open, []AccordionItem{{ID: "a", Content: func() {}}, {ID: "a", Content: func() {}}}, AccordionOptions{}) }, 100, 100) } diff --git a/card.go b/data/card.go similarity index 82% rename from card.go rename to data/card.go index 44fbb7d..95aea58 100644 --- a/card.go +++ b/data/card.go @@ -1,10 +1,11 @@ -package mujica +package data import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CardOptions configure a Card. @@ -22,8 +23,8 @@ const cardInset float32 = 4 // Card groups content on a surface with an outline. It adds no click behavior. func Card(c *ui.Context, opts CardOptions, body func()) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() pad := u * 4 if opts.Decorated { pad += cardInset + 2 @@ -41,7 +42,7 @@ func Card(c *ui.Context, opts CardOptions, body func()) *ui.Element { } card.Children(func() { if opts.Title != "" { - ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).LineHeight(1.3) + ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).LineHeight(1.3) if opts.Decorated { ui.Box(c).Height(7).FillWidth().Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 6, 7, k.Ornament) diff --git a/card_test.go b/data/card_test.go similarity index 81% rename from card_test.go rename to data/card_test.go index 3d44603..3370ff8 100644 --- a/card_test.go +++ b/data/card_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "image/color" @@ -7,14 +7,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func cardView(decorated bool) func(c *ui.Context) { return func(c *ui.Context) { ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { Card(c, CardOptions{Title: "Archive", Decorated: decorated, Footer: func() { - Button(c, "Open", ButtonOptions{}) + input.Button(c, "Open", input.ButtonOptions{}) }}, func() { ui.Text(c, "Letters of the court") }).Width(260) @@ -24,7 +26,7 @@ func cardView(decorated bool) func(c *ui.Context) { func TestCardSlots(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) cardView(false)(c) }, 400, 300) for _, s := range []string{"Archive", "Letters of the court", "Open"} { @@ -42,7 +44,7 @@ func TestCardSlots(t *testing.T) { func TestCardNoTitleNoFooter(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Card(c, CardOptions{}, func() { ui.Text(c, "Only body") }) }, 300, 200) if got := tt.Texts(); len(got) != 1 || got[0] != "Only body" { @@ -54,7 +56,7 @@ func TestCardDecorationColor(t *testing.T) { for _, dec := range []bool{false, true} { var bounds ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { bounds = Card(c, CardOptions{Title: "T", Decorated: dec}, func() {}).Width(200).Bounds() }) @@ -76,20 +78,20 @@ func TestCardSnapshot(t *testing.T) { snapshot(t, "card", 640, 240, func(c *ui.Context) { ui.Row(c).Padding(20).Gap(20).AlignItems(ui.Start).Children(func() { Card(c, CardOptions{Title: "Archive", Footer: func() { - Button(c, "Open", ButtonOptions{Variant: Secondary}) + input.Button(c, "Open", input.ButtonOptions{Variant: input.Secondary}) }}, func() { ui.Text(c, "Letters of the court, sealed in wine-red wax.") }).Width(280) Card(c, CardOptions{Title: "Chapel", Decorated: true, Footer: func() { - Button(c, "Enter", ButtonOptions{}) + input.Button(c, "Enter", input.ButtonOptions{}) }}, func() { ui.Text(c, "Vespers at seven; candles by the north door.") }).Width(280) }) }) for _, d := range []theme.Density{theme.Compact, theme.Comfortable} { snapshot(t, "card-"+strings.ToLower(d.String()), 640, 260, func(c *ui.Context) { - mode := Light - if Tokens(c).Dark { - mode = Dark + mode := core.Light + if core.Tokens(c).Dark { + mode = core.Dark } - Use(c, Settings{Mode: mode, Density: d}) + core.Use(c, core.Settings{Mode: mode, Density: d}) cardView(true)(c) }) } diff --git a/carousel.go b/data/carousel.go similarity index 80% rename from carousel.go rename to data/carousel.go index 13c4406..1dea908 100644 --- a/carousel.go +++ b/data/carousel.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CarouselOptions configure a Carousel. @@ -32,7 +34,7 @@ type carouselState struct { } // Carousel shows slide *index of n with previous, next and position controls. -func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) ChoiceResult { +func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func(i int)) core.ChoiceResult { if n < 0 || opts.Interval < 0 { panic("mujica: Carousel with negative count or Interval") } @@ -40,9 +42,9 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func if interval == 0 { interval = 5 * time.Second } - k := Tokens(c) - u := Density(c).Unit() - var v ChoiceResult + k := core.Tokens(c) + u := core.Density(c).Unit() + var v core.ChoiceResult v.Element = ui.Column(c).Gap(u * 2).Label(opts.Label) st := ui.Local(v.Element, "state", func() carouselState { return carouselState{} }) if n > 0 && (*index < 0 || *index >= n) { @@ -61,8 +63,8 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func return } *index = i - v.changed = true - c.Announce(fmt.Sprintf(msg(c, "carousel.position"), i+1, n)) + v = core.NewChoiceResult(v.Element, true) + c.Announce(fmt.Sprintf(core.Msg(c, "carousel.position"), i+1, n)) slog.Debug("mujica: carousel slide", "index", i) } now := c.Now() @@ -91,14 +93,14 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func } v.Element.Children(func() { ui.Row(c).Grow(1).MinHeight(120).AlignItems(ui.Center).Gap(u * 2).Children(func() { - prev := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-left"), Label: msg(c, "carousel.prev"), Disabled: n < 2 || !wrap && *index == 0}) + prev := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("chevron-left"), Label: core.Msg(c, "carousel.prev"), Disabled: n < 2 || !wrap && *index == 0}) ui.Column(c).Grow(1).MinWidth(0).FillHeight().Clip().Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). - Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "carousel.position"), *index+1, n)).Children(func() { + Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "carousel.position"), *index+1, n)).Children(func() { if n > 0 { ui.Column(c).Key(*index).Grow(1).Children(func() { slide(*index) }) } }) - next := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("chevron-right"), Label: msg(c, "carousel.next"), Disabled: n < 2 || !wrap && *index == n-1}) + next := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("chevron-right"), Label: core.Msg(c, "carousel.next"), Disabled: n < 2 || !wrap && *index == n-1}) if prev.Clicked() { goTo(*index - 1) } @@ -132,9 +134,9 @@ func Carousel(c *ui.Context, index *int, n int, opts CarouselOptions, slide func // carouselDot is a position button: a filled diamond for the current slide. func carouselDot(c *ui.Context, i, n int, on bool) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius). - Label(fmt.Sprintf(msg(c, "carousel.position"), i+1, n)).Role(ui.RoleTab).Selected(on) + Label(fmt.Sprintf(core.Msg(c, "carousel.position"), i+1, n)).Role(ui.RoleTab).Selected(on) internal.Ring(b, k.Focus, theme.ControlRadius) hover := b.Hovered() b.Background(ui.Transparent).Draw(func(p *ui.Painter, r ui.Rect) { @@ -153,10 +155,10 @@ func carouselDot(c *ui.Context, i, n int, on bool) *ui.Element { // carouselPause is the pause button: two bars, or a play triangle while paused. func carouselPause(c *ui.Context, paused bool) *ui.Element { - k := Tokens(c) - label := msg(c, "carousel.pause") + k := core.Tokens(c) + label := core.Msg(c, "carousel.pause") if paused { - label = msg(c, "carousel.play") + label = core.Msg(c, "carousel.play") } b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label) internal.Ring(b, k.Focus, theme.ControlRadius) diff --git a/carousel_test.go b/data/carousel_test.go similarity index 95% rename from carousel_test.go rename to data/carousel_test.go index c2c0bda..6152713 100644 --- a/carousel_test.go +++ b/data/carousel_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type carouselHarness struct { @@ -15,7 +17,7 @@ type carouselHarness struct { } func (h *carouselHarness) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Size(400, 260).Children(func() { v := Carousel(c, &h.index, 4, h.opts, func(i int) { ui.Text(c, fmt.Sprintf("Window %d", i+1)).Padding(20) @@ -124,7 +126,7 @@ func TestCarouselSnapshot(t *testing.T) { ui.Column(c).Padding(16).Size(440, 240).Children(func() { Carousel(c, *i, 4, CarouselOptions{Autoplay: true, Label: "Windows"}, func(n int) { ui.Column(c).Grow(1).Center().Children(func() { - ui.Text(c, fmt.Sprintf("Rose window %d", n+1)).FontSize(FontSize(c, 18)) + ui.Text(c, fmt.Sprintf("Rose window %d", n+1)).FontSize(core.FontSize(c, 18)) }) }).Element.Grow(1) }) @@ -134,7 +136,7 @@ func TestCarouselSnapshot(t *testing.T) { func TestCarouselEmptyAutoplayKeys(t *testing.T) { index := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Carousel(c, &index, 0, CarouselOptions{Autoplay: true}, func(int) {}).Element.Size(300, 200) }, 320, 240) for range 4 { diff --git a/collapsible.go b/data/collapsible.go similarity index 78% rename from collapsible.go rename to data/collapsible.go index bb9b6e5..0a5104a 100644 --- a/collapsible.go +++ b/data/collapsible.go @@ -1,13 +1,14 @@ -package mujica +package data import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CollapsibleOptions configure a Collapsible. @@ -20,13 +21,13 @@ type CollapsibleOptions struct { } // Collapsible shows content below a trigger while *open; closed content is not built. -func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) ChoiceResult { +func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content func()) core.ChoiceResult { if opts.Title == "" && opts.Trigger == nil { panic("mujica: Collapsible needs Title or Trigger") } - k := Tokens(c) - u := Density(c).Unit() - var v ChoiceResult + k := core.Tokens(c) + u := core.Density(c).Unit() + var v core.ChoiceResult v.Element = ui.Column(c).Shrink(0) v.Element.Children(func() { was := *open @@ -35,9 +36,9 @@ func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content fun if opts.Disabled { *open = was } - v.changed = *open != was + v = core.NewChoiceResult(v.Element, *open != was) tr.Disabled(opts.Disabled).AlignSelf(ui.Start).AlignItems(ui.Center).Gap(u*2). - MinHeight(Density(c).ControlHeight()).Padding(0, u*3, 0, u*2).Radius(theme.ControlRadius) + MinHeight(core.Density(c).ControlHeight()).Padding(0, u*3, 0, u*2).Radius(theme.ControlRadius) if opts.Title != "" { tr.Label(opts.Title) } @@ -45,7 +46,7 @@ func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content fun if tr.Hovered() && !opts.Disabled { tr.Background(k.SurfaceHover) } - prog := tr.Animate("mujica.open", collapsibleTarget(*open), Motion(c, theme.StateDuration)) + prog := tr.Animate("mujica.open", collapsibleTarget(*open), core.Motion(c, theme.StateDuration)) tr.Children(func() { ui.Icon(c, icons.Must("chevron-right")).Size(16, 16).Rotate(90 * prog).TextColor(k.TextMuted) if opts.Trigger != nil { @@ -58,7 +59,7 @@ func Collapsible(c *ui.Context, open *bool, opts CollapsibleOptions, content fun panel.Padding(u*2, u*2, u*2, u*8).Gap(u * 2) } }) - if v.changed { + if v.Changed() { slog.Debug("mujica: collapsible toggle", "open", *open) } return v diff --git a/collapsible_test.go b/data/collapsible_test.go similarity index 86% rename from collapsible_test.go rename to data/collapsible_test.go index f6ecd90..66de506 100644 --- a/collapsible_test.go +++ b/data/collapsible_test.go @@ -1,25 +1,28 @@ -package mujica +package data import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestCollapsibleToggleAndFocus(t *testing.T) { open, changes, clicks := false, 0, 0 disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if Collapsible(c, &open, CollapsibleOptions{Title: "Details", Disabled: disabled}, func() { - if Button(c, "Inner", ButtonOptions{}).Clicked() { + if input.Button(c, "Inner", input.ButtonOptions{}).Clicked() { clicks++ } }).Changed() { changes++ } - Button(c, "After", ButtonOptions{}) + input.Button(c, "After", input.ButtonOptions{}) }) }, 300, 300) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -67,7 +70,7 @@ func TestCollapsibleNeedsTitle(t *testing.T) { }() open := false ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Collapsible(c, &open, CollapsibleOptions{}, func() {}) }, 100, 100) } @@ -88,12 +91,12 @@ func TestCollapsibleSnapshot(t *testing.T) { func TestCollapsibleClosingContentLeavesTabOrder(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { Collapsible(c, &open, CollapsibleOptions{Title: "Details"}, func() { - Button(c, "Inner", ButtonOptions{}) + input.Button(c, "Inner", input.ButtonOptions{}) }) - Button(c, "After", ButtonOptions{}) + input.Button(c, "After", input.ButtonOptions{}) }) }, 300, 300) tt.Key(0, ui.KeyTab) diff --git a/data_table.go b/data/data_table.go similarity index 93% rename from data_table.go rename to data/data_table.go index 9d36c5e..fe59696 100644 --- a/data_table.go +++ b/data/data_table.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "log/slog" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DataColumn is a column of a DataTable showing rows of type R. @@ -83,6 +84,9 @@ func (s *DataTableState[K]) Selected() (K, bool) { return s.sel, s.has } // Rows returns how many rows the table shows after filtering. func (s *DataTableState[K]) Rows() int { return len(s.cache.view) } +// VisibleRows returns the range of view rows built this frame. +func (s *DataTableState[K]) VisibleRows() (first, last int) { return s.rows.Visible() } + // DataTableView is a built DataTable and what happened to it in this frame. type DataTableView struct { Element *ui.Element @@ -109,8 +113,8 @@ func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows [] if !s.started { s.started, s.cursor = true, -1 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) before := s.Sort var v DataTableView v.Element = ui.Column(c).MinHeight(0).Gap(d.Unit() * 2) @@ -132,9 +136,9 @@ func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows [] if len(view) == 0 { empty := opts.Empty if len(rows) > 0 { - empty = msg(c, "data_table.noMatch") + empty = core.Msg(c, "data_table.noMatch") } - v.retry = dataPlaceholder(c, opts.Status, opts.Error, empty, msg(c, "list.empty")) + v.retry = DataPlaceholder(c, opts.Status, opts.Error, empty, core.Msg(c, "list.empty")) return } vis, cols := dataTableColumns(dataRowUnit(c), s, opts) @@ -147,7 +151,7 @@ func DataTable[R any, K comparable](c *ui.Context, s *DataTableState[K], rows [] } // Rows reach the data-row height of the density. unit := dataRowUnit(c) - restore := dataSelectTheme(c, unit) + restore := DataSelectTheme(c, unit) head := s.Sort e := ui.Table(c, &s.rows, cols, len(view), func(row, col int) { r := rows[view[row]] @@ -269,7 +273,7 @@ func dataTableMenu[R any, K comparable](opts DataTableOptions[R, K]) []tableColu // dataRowUnit is the table spacing unit: rows reach the density's height and fit scaled text. func dataRowUnit(c *ui.Context) float32 { // MyGo rows and the header are 8 units high with 1.5 units of padding above and below. - return max(Density(c).ControlHeight()/8, FontSize(c, theme.BodySize)*theme.LineHeight/5) + return max(core.Density(c).ControlHeight()/8, core.FontSize(c, theme.BodySize)*theme.LineHeight/5) } func dataTableColumns[R any, K comparable](unit float32, s *DataTableState[K], opts DataTableOptions[R, K]) ([]int, []ui.TableColumn) { @@ -292,7 +296,7 @@ func dataTableColumns[R any, K comparable](unit float32, s *DataTableState[K], o // dataTableMark draws the selection column: a check box, or a diamond on the chosen row. func dataTableMark[R any, K comparable](c *ui.Context, s *DataTableState[K], opts DataTableOptions[R, K], key K, row int) { - k := Tokens(c) + k := core.Tokens(c) if !opts.Multiple { if row == s.cursor { ui.Box(c).Size(8, 8).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { @@ -302,7 +306,7 @@ func dataTableMark[R any, K comparable](c *ui.Context, s *DataTableState[K], opt return } on := s.Selection.Has(key) - cb := ui.CheckboxBase(c, &on).Label(msg(c, "data_table.selectRow")).Size(theme.MinHitSize, theme.MinHitSize). + cb := ui.CheckboxBase(c, &on).Label(core.Msg(c, "data_table.selectRow")).Size(theme.MinHitSize, theme.MinHitSize). Justify(ui.Center).AlignItems(ui.Center).Radius(theme.ControlRadius) internal.Ring(cb, k.Focus, theme.ControlRadius) cb.Children(func() { dataTableCheckBox(c, on) }) diff --git a/data_table_columns.go b/data/data_table_columns.go similarity index 88% rename from data_table_columns.go rename to data/data_table_columns.go index 62494d8..07697bf 100644 --- a/data_table_columns.go +++ b/data/data_table_columns.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // tableColumn is what the column menu knows of a column. @@ -25,9 +27,9 @@ const tableWidthStep float32 = 16 // tableColumnMenu is the keyboard path to sort, move, resize and, with hidden, show columns. func tableColumnMenu(c *ui.Context, cols []tableColumn, sort *ui.SortOrder, layout *ui.TableLayout, hidden *map[string]bool) { - k := Tokens(c) - u := Density(c).Unit() - b := Button(c, msg(c, "data_table.columns"), ButtonOptions{Variant: Secondary, Icon: icons.Must("settings")}) + k := core.Tokens(c) + u := core.Density(c).Unit() + b := input.Button(c, core.Msg(c, "data_table.columns"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("settings")}) open := ui.Local(b, "open", func() bool { return false }) if b.Clicked() { *open = !*open @@ -61,7 +63,7 @@ func tableColumnMenu(c *ui.Context, cols []tableColumn, sort *ui.SortOrder, layo // tableColumnActions are one column's sort, move and width buttons. func tableColumnActions(c *ui.Context, col tableColumn, at int, order []string, sort *ui.SortOrder, layout *ui.TableLayout) { act := func(icon, key string, disabled bool) bool { - return Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must(icon), Label: fmt.Sprintf(msg(c, key), col.Title), Disabled: disabled}).Clicked() + return input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must(icon), Label: fmt.Sprintf(core.Msg(c, key), col.Title), Disabled: disabled}).Clicked() } if col.Sortable { for _, desc := range []bool{false, true} { @@ -100,8 +102,8 @@ func tableColumnActions(c *ui.Context, col tableColumn, at int, order []string, // tableVisibility is a column's show/hide check box; the last visible one cannot hide. func tableVisibility(c *ui.Context, col tableColumn, hidden *map[string]bool, visible int) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() on := !(*hidden)[col.ID] last := on && visible == 1 @@ -165,7 +167,7 @@ func tableSetWidth(l *ui.TableLayout, id string, w float32) { // dataTableCheckBox draws a 16 DIP check box: OnAccent check on Accent when on. func dataTableCheckBox(c *ui.Context, on bool) { - k := Tokens(c) + k := core.Tokens(c) box := ui.Box(c).Size(16, 16).Shrink(0).Radius(theme.ControlRadius).Center() if on { box.Background(k.Accent).TextColor(k.OnAccent).Children(func() { diff --git a/data_table_test.go b/data/data_table_test.go similarity index 98% rename from data_table_test.go rename to data/data_table_test.go index 5929626..7e1794a 100644 --- a/data_table_test.go +++ b/data/data_table_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "cmp" @@ -9,7 +9,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type relic struct { @@ -45,7 +46,7 @@ func newTableHarness(rows []relic) *tableHarness { } func (h *tableHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light, Density: h.density}) + core.Use(c, core.Settings{Mode: core.Light, Density: h.density}) h.c, h.seen = c, map[string]int{} ui.Column(c).Padding(10).Gap(8).Children(func() { cols := h.opts diff --git a/description_list.go b/data/description_list.go similarity index 79% rename from description_list.go rename to data/description_list.go index 93a7782..82a75d7 100644 --- a/description_list.go +++ b/data/description_list.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "log/slog" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DescriptionItem is a name and its value. @@ -58,8 +60,8 @@ func DescriptionList(c *ui.Context, items []DescriptionItem, opts DescriptionLis if minW == 0 { minW = 220 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() gap := u * 6 v := DescriptionListView{Element: ui.Grid(c).GapX(gap).GapY(u * 4).FillWidth(), copied: -1} st := ui.Local(v.Element, "state", func() descriptionState { return descriptionState{item: -1} }) @@ -86,21 +88,21 @@ func DescriptionList(c *ui.Context, items []DescriptionItem, opts DescriptionLis cell.ColumnSpan(it.Span) } cell.Children(func() { - ui.Text(c, it.Label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, it.Label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) ui.Row(c).Gap(u).MinHeight(theme.MinHitSize).AlignItems(ui.Center).Children(func() { ui.Text(c, it.Value).Shrink(1).MinWidth(0) if !it.Copyable { return } done := st.item == i && now.Before(st.until) - icon, label := icons.Must("copy"), msg(c, "description_list.copy")+" "+it.Label + icon, label := icons.Must("copy"), core.Msg(c, "description_list.copy")+" "+it.Label if done { - icon, label = icons.Must("check"), msg(c, "description_list.copied") + icon, label = icons.Must("check"), core.Msg(c, "description_list.copied") c.After(st.until.Sub(now)) } - if Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label}).MinHeight(theme.MinHitSize).Padding(0, u).Clicked() { + if input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icon, Label: label}).MinHeight(theme.MinHitSize).Padding(0, u).Clicked() { c.WriteClipboard(it.Value) - c.Announce(msg(c, "description_list.copied")) + c.Announce(core.Msg(c, "description_list.copied")) *st = descriptionState{item: i, until: now.Add(descriptionCopied)} v.copied = i slog.Debug("mujica: description_list copy", "item", i) diff --git a/description_list_test.go b/data/description_list_test.go similarity index 93% rename from description_list_test.go rename to data/description_list_test.go index ba7a710..fe2586c 100644 --- a/description_list_test.go +++ b/data/description_list_test.go @@ -1,9 +1,11 @@ -package mujica +package data import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var descItems = []DescriptionItem{ @@ -16,7 +18,7 @@ func TestDescriptionListColumnsAndCopy(t *testing.T) { width := float32(560) copied := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(width).Children(func() { if i, ok := DescriptionList(c, descItems, DescriptionListOptions{}).Copied(); ok { copied = i @@ -49,7 +51,7 @@ func TestDescriptionListColumnsAndCopy(t *testing.T) { func TestDescriptionListSpan(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(900).Children(func() { DescriptionList(c, []DescriptionItem{{Label: "A", Value: "a", Span: 2}, {Label: "B", Value: "b"}, {Label: "C", Value: "c"}}, DescriptionListOptions{Columns: 3}) }) @@ -69,7 +71,7 @@ func TestDescriptionListBadOptions(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DescriptionList(c, nil, DescriptionListOptions{Columns: -1}) }, 100, 100) } diff --git a/data/doc.go b/data/doc.go new file mode 100644 index 0000000..0a82371 --- /dev/null +++ b/data/doc.go @@ -0,0 +1,2 @@ +// Package data holds MujicaUI lists, tables, trees, cards and other content and data views. +package data diff --git a/example_data_test.go b/data/example_data_test.go similarity index 68% rename from example_data_test.go rename to data/example_data_test.go index 81cd578..65afac8 100644 --- a/example_data_test.go +++ b/data/example_data_test.go @@ -1,4 +1,4 @@ -package mujica_test +package data_test import ( "cmp" @@ -7,14 +7,16 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func ExampleCard() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { - mujica.Button(c, "Open", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + data.Card(c, data.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + input.Button(c, "Open", input.ButtonOptions{}) }}, func() { ui.Text(c, "Letters of the court, sealed in wax.") }).Width(280) @@ -24,10 +26,10 @@ func ExampleCard() { func ExampleList() { names := []string{"Matins", "Lauds", "Prime", "Terce"} - var state mujica.ListState[string] + var state data.ListState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + core.Use(c, core.Settings{}) + v := data.List(c, &state, len(names), data.ListOptions[string]{ Key: func(i int) string { return names[i] }, Label: func(i int) string { return names[i] }, }, func(i int) { ui.Text(c, names[i]) }) @@ -47,10 +49,10 @@ func ExampleDataTable() { Age int } rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} - var state mujica.DataTableState[int] - opts := mujica.DataTableOptions[relic, int]{ + var state data.DataTableState[int] + opts := data.DataTableOptions[relic, int]{ Key: func(r relic) int { return r.ID }, - Columns: []mujica.DataColumn[relic]{ + Columns: []data.DataColumn[relic]{ {ID: "name", Title: "Name", Sortable: true, Text: func(r relic) string { return r.Name }, Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, @@ -60,18 +62,18 @@ func ExampleDataTable() { }, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DataTable(c, &state, rows, opts).Element.Size(360, 240) + core.Use(c, core.Settings{}) + data.DataTable(c, &state, rows, opts).Element.Size(360, 240) } ui.Render(view, 400, 300, 1) } func ExampleTree() { kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} - var state mujica.TreeState[string] + var state data.TreeState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Tree(c, &state, mujica.TreeOptions[string]{ + core.Use(c, core.Settings{}) + data.Tree(c, &state, data.TreeOptions[string]{ Roots: []string{"Nave", "Sacristy"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, @@ -83,14 +85,14 @@ func ExampleTree() { func ExampleTreeTable() { kids := map[string][]string{"Vestry": {"Stole", "Alb"}} size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} - var state mujica.TreeTableState[string] + var state data.TreeTableState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + core.Use(c, core.Settings{}) + data.TreeTable(c, &state, data.TreeTableOptions[string]{ Roots: []string{"Vestry", "Organ"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, - Columns: []mujica.TreeColumn[string]{ + Columns: []data.TreeColumn[string]{ {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, Compare: cmp.Compare[string]}, {ID: "size", Title: "Size", Width: 80, Align: ui.End, @@ -104,11 +106,11 @@ func ExampleTreeTable() { func ExampleAccordion() { open := []string{"choir"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Accordion(c, &open, []mujica.AccordionItem{ + core.Use(c, core.Settings{}) + data.Accordion(c, &open, []data.AccordionItem{ {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, - }, mujica.AccordionOptions{}) + }, data.AccordionOptions{}) } ui.Render(view, 400, 300, 1) } @@ -116,8 +118,8 @@ func ExampleAccordion() { func ExampleCollapsible() { open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + core.Use(c, core.Settings{}) + data.Collapsible(c, &open, data.CollapsibleOptions{Title: "Details"}, func() { ui.Text(c, "Candles are lit at the seventh bell.") }) } @@ -126,10 +128,10 @@ func ExampleCollapsible() { func ExampleTimeline() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Timeline(c, []mujica.TimelineItem{ - {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, - {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + core.Use(c, core.Settings{}) + data.Timeline(c, []data.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: data.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: data.TimelineActive}, {Time: "18:00", Title: "Vespers"}, }) } @@ -138,12 +140,12 @@ func ExampleTimeline() { func ExampleDescriptionList() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DescriptionList(c, []mujica.DescriptionItem{ + core.Use(c, core.Settings{}) + data.DescriptionList(c, []data.DescriptionItem{ {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, {Label: "Shelf", Value: "North vault, row 7"}, {Label: "Seal", Value: "Wine-red wax", Span: -1}, - }, mujica.DescriptionListOptions{}) + }, data.DescriptionListOptions{}) } ui.Render(view, 400, 300, 1) } @@ -151,8 +153,8 @@ func ExampleDescriptionList() { func ExampleStatistic() { change := 12.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Statistic(c, 12840, mujica.StatisticOptions{ + core.Use(c, core.Settings{}) + data.Statistic(c, 12840, data.StatisticOptions{ Title: "Pilgrims", Unit: "souls", Precision: 0, Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", }) @@ -164,8 +166,8 @@ func ExampleImageViewer() { picture := ui.MustParseSVG([]byte(``)) open, index := true, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: "Rose window"}}) + core.Use(c, core.Settings{}) + data.ImageViewer(c, &open, &index, []data.ViewerImage{{Source: picture, Alt: "Rose window"}}) } ui.Render(view, 400, 300, 1) } @@ -173,8 +175,8 @@ func ExampleImageViewer() { func ExampleCarousel() { index := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + core.Use(c, core.Settings{}) + data.Carousel(c, &index, 3, data.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) }).Element.Size(360, 220) } diff --git a/data/helpers_test.go b/data/helpers_test.go new file mode 100644 index 0000000..a465ee6 --- /dev/null +++ b/data/helpers_test.go @@ -0,0 +1,24 @@ +package data + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/image_viewer.go b/data/image_viewer.go similarity index 84% rename from image_viewer.go rename to data/image_viewer.go index a5c3ee5..58933a1 100644 --- a/image_viewer.go +++ b/data/image_viewer.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ViewerImage is one picture of an ImageViewer. @@ -34,21 +36,21 @@ type viewerState struct { } // ImageViewer shows images[*index] over the window while *open, with zoom, pan and switching; Element is nil while closed. -func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) ChoiceResult { - var v ChoiceResult +func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) core.ChoiceResult { + var v core.ChoiceResult if !*open { return v } if len(images) == 0 || *index < 0 || *index >= len(images) { panic("mujica: ImageViewer index out of range") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() w, h := c.Size() v.Element = ui.DialogBase(c, open, func(back, panel *ui.Element) { back.Background(ui.RGBA(0, 0, 0, 0.55)) panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius). - Size(w-u*12, h-u*12).Padding(u * 3).Gap(u * 3).Label(msg(c, "image_viewer.title")) + Size(w-u*12, h-u*12).Padding(u * 3).Gap(u * 3).Label(core.Msg(c, "image_viewer.title")) st := ui.Local(panel, "view", func() viewerState { return viewerState{image: -1} }) if st.image != *index { *st = viewerState{image: *index, zoom: 1} @@ -57,9 +59,9 @@ func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) Ch if i < 0 || i >= len(images) || i == *index { return } - *index, v.changed = i, true + *index, v = i, core.NewChoiceResult(v.Element, true) *st = viewerState{image: i, zoom: 1} - c.Announce(fmt.Sprintf(msg(c, "image_viewer.position"), i+1, len(images))) + c.Announce(fmt.Sprintf(core.Msg(c, "image_viewer.position"), i+1, len(images))) slog.Debug("mujica: image_viewer image", "index", i) } zoom := func(f float32) { @@ -68,10 +70,10 @@ func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) Ch viewerKeys(panel, st, zoom, goTo, *index) img := images[*index] tool := func(icon, label string, disabled bool) bool { - return Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must(icon), Label: msg(c, label), Disabled: disabled}).Clicked() + return input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must(icon), Label: core.Msg(c, label), Disabled: disabled}).Clicked() } ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "image_viewer.position"), *index+1, len(images))).TextColor(k.TextMuted).Grow(1) + ui.Text(c, fmt.Sprintf(core.Msg(c, "image_viewer.position"), *index+1, len(images))).TextColor(k.TextMuted).Grow(1) if tool("zoom-out", "image_viewer.zoomOut", st.zoom <= viewerMinZoom) { zoom(1 / viewerZoomStep) } @@ -107,7 +109,7 @@ func ImageViewer(c *ui.Context, open *bool, index *int, images []ViewerImage) Ch } return true }) - iw, ih := viewerSize(img.Source) + iw, ih := ViewerSize(img.Source) place := func(r ui.Rect) (dw, dh, mx, my float32) { fit := float32(math.Min(float64(r.W/iw), float64(r.H/ih))) dw, dh = iw*fit*st.zoom, ih*fit*st.zoom @@ -164,8 +166,8 @@ func viewerKeys(panel *ui.Element, st *viewerState, zoom func(float32), goTo fun } } -// viewerSize returns a picture's own size in DIPs. -func viewerSize(src ui.ImageSource) (float32, float32) { +// ViewerSize returns a picture's own size in DIPs. +func ViewerSize(src ui.ImageSource) (float32, float32) { var w, h float32 switch s := src.(type) { case *ui.Bitmap: diff --git a/image_viewer_test.go b/data/image_viewer_test.go similarity index 97% rename from image_viewer_test.go rename to data/image_viewer_test.go index 30e98bd..748fa93 100644 --- a/image_viewer_test.go +++ b/data/image_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "image" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // viewerPicture is red on the left half and blue on the right. @@ -36,7 +38,7 @@ func newViewerHarness() *viewerHarness { } func (h *viewerHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if ImageViewer(c, &h.open, &h.index, h.images).Changed() { h.changes++ } diff --git a/list.go b/data/list.go similarity index 75% rename from list.go rename to data/list.go index 439d023..2c2ba2b 100644 --- a/list.go +++ b/data/list.go @@ -1,13 +1,15 @@ -package mujica +package data import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DataStatus is the load state of a data view's rows. @@ -46,8 +48,8 @@ type ListOptions[K comparable] struct { type ListState[K comparable] struct { // Selection holds the chosen keys of a Multiple list. Selection ui.Selection[K] - rows ui.ListState - grid ui.GridState + Rows ui.ListState + Grid ui.GridState cursor int started bool key K @@ -59,8 +61,8 @@ func (s *ListState[K]) Selected() (K, bool) { return s.key, s.has } // ScrollTo brings item i to the top of the list. func (s *ListState[K]) ScrollTo(i int) { - s.rows.ScrollTo(i, ui.Start) - s.grid.ScrollTo(i, ui.Start) + s.Rows.ScrollTo(i, ui.Start) + s.Grid.ScrollTo(i, ui.Start) } // ListView is a built List and what happened to it in this frame. @@ -90,12 +92,12 @@ func List[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[ s.started, s.cursor = true, -1 } listLocate(s, n, opts.Key) - k := Tokens(c) + k := core.Tokens(c) var v ListView v.Element = ui.Column(c).MinHeight(0) v.Element.Children(func() { if n == 0 { - v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + v.retry = DataPlaceholder(c, opts.Status, opts.Error, opts.Empty, core.Msg(c, "list.empty")) return } var e *ui.Element @@ -135,19 +137,19 @@ func listLocate[K comparable](s *ListState[K], n int, key func(int) K) { } func listRows[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) *ui.Element { - d := Density(c) - s.rows.Key = func(i int) any { return opts.Key(i) } - s.rows.Label = opts.Label - s.rows.Selected = &s.cursor + d := core.Density(c) + s.Rows.Key = func(i int) any { return opts.Key(i) } + s.Rows.Label = opts.Label + s.Rows.Selected = &s.cursor if opts.Multiple { - s.rows.Selection = &s.Selection + s.Rows.Selection = &s.Selection } else { - s.rows.Selection = nil + s.Rows.Selection = nil } - k := Tokens(c) - restore := dataSelectTheme(c, 0) + k := core.Tokens(c) + restore := DataSelectTheme(c, 0) defer restore() - return ui.List(c, &s.rows, n, func(i int) { + return ui.List(c, &s.Rows, n, func(i int) { ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Row(c).Grow(1).MinWidth(0).AlignItems(ui.Center).Gap(d.Unit() * 2).Children(func() { item(i) }) if listChosen(s, opts, i) { @@ -158,7 +160,7 @@ func listRows[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOpti } func listGrid[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOptions[K], item func(i int)) *ui.Element { - d := Density(c) + d := core.Density(c) w, h := opts.ItemWidth, opts.ItemHeight if w <= 0 { w = 160 @@ -166,21 +168,21 @@ func listGrid[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOpti if h <= 0 { h = 120 } - s.grid.Key = func(i int) any { return opts.Key(i) } - s.grid.Label = opts.Label - s.grid.Selected = &s.cursor + s.Grid.Key = func(i int) any { return opts.Key(i) } + s.Grid.Label = opts.Label + s.Grid.Selected = &s.cursor if opts.Multiple { - s.grid.Selection = &s.Selection + s.Grid.Selection = &s.Selection } else { - s.grid.Selection = nil + s.Grid.Selection = nil } // Chosen items take an AccentText ring over a Selection face. - k := Tokens(c) + k := core.Tokens(c) saved := c.Theme() t := *saved t.Accent = k.AccentText c.SetTheme(&t) - e := ui.GridView(c, &s.grid, n, w, h, func(i int) { + e := ui.GridView(c, &s.Grid, n, w, h, func(i int) { chosen := listChosen(s, opts, i) face := ui.Column(c).Grow(1).Padding(d.Unit() * 2).Gap(d.Unit()).Radius(theme.ControlRadius) if chosen { @@ -197,9 +199,9 @@ func listGrid[K comparable](c *ui.Context, s *ListState[K], n int, opts ListOpti return e } -// dataSelectTheme shows MyGo's chosen rows as Selection with Text; spacing > 0 sets the unit. -func dataSelectTheme(c *ui.Context, spacing float32) func() { - k := Tokens(c) +// DataSelectTheme shows MyGo's chosen rows as Selection with Text; spacing > 0 sets the unit. +func DataSelectTheme(c *ui.Context, spacing float32) func() { + k := core.Tokens(c) saved := c.Theme() t := *saved t.Accent, t.AccentText = k.Selection, k.Text @@ -217,21 +219,21 @@ func listChosen[K comparable](s *ListState[K], opts ListOptions[K], i int) bool return s.cursor == i } -// dataPlaceholder fills an empty view with its status and reports a retry. -func dataPlaceholder(c *ui.Context, st DataStatus, errText, empty, emptyDefault string) bool { - k := Tokens(c) - u := Density(c).Unit() +// DataPlaceholder fills an empty view with its status and reports a retry. +func DataPlaceholder(c *ui.Context, st DataStatus, errText, empty, emptyDefault string) bool { + k := core.Tokens(c) + u := core.Density(c).Unit() retry := false ui.Column(c).Grow(1).MinHeight(u * 30).Center().Gap(u * 3).Padding(u * 6).Children(func() { switch st { case DataLoading, DataLoadingMore, DataUnloaded: - Spinner(c, SpinnerOptions{Label: msg(c, "list.loading"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "list.loading"), ShowLabel: true}) case DataFailed: ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(20, 20) - ui.Text(c, dataError(c, errText)).Role(ui.RoleStatus) + ui.Text(c, DataError(c, errText)).Role(ui.RoleStatus) }) - retry = Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw")}).Clicked() + retry = input.Button(c, core.Msg(c, "list.retry"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("refresh-cw")}).Clicked() default: if empty == "" { empty = emptyDefault @@ -245,29 +247,29 @@ func dataPlaceholder(c *ui.Context, st DataStatus, errText, empty, emptyDefault // dataFooter shows appending and failed loads below rows and reports a retry. func dataFooter(c *ui.Context, st DataStatus, errText string) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() retry := false switch st { case DataLoadingMore: ui.Row(c).Padding(u*2, u*3).Shrink(0).Children(func() { - Spinner(c, SpinnerOptions{Label: msg(c, "list.loadingMore"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "list.loadingMore"), ShowLabel: true}) }) case DataFailed: ui.Row(c).Padding(u*2, u*3).Gap(u * 3).AlignItems(ui.Center).Shrink(0).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Grow(1).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) - ui.Text(c, dataError(c, errText)).Role(ui.RoleStatus) + ui.Text(c, DataError(c, errText)).Role(ui.RoleStatus) }) - retry = Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw")}).Clicked() + retry = input.Button(c, core.Msg(c, "list.retry"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("refresh-cw")}).Clicked() }) } return retry } -func dataError(c *ui.Context, s string) string { +func DataError(c *ui.Context, s string) string { if s == "" { - return msg(c, "list.failed") + return core.Msg(c, "list.failed") } return s } diff --git a/list_test.go b/data/list_test.go similarity index 97% rename from list_test.go rename to data/list_test.go index 6b8c4f8..03fea25 100644 --- a/list_test.go +++ b/data/list_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -8,7 +8,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type listHarness struct { @@ -33,7 +34,7 @@ func newListHarness(n int) *listHarness { } func (h *listHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) h.built = 0 ui.Column(c).Padding(10).Children(func() { v := List(c, &h.st, len(h.ids), h.opts, func(i int) { @@ -212,7 +213,7 @@ func TestListSnapshot(t *testing.T) { gopts.Grid, gopts.Multiple, gopts.ItemWidth, gopts.ItemHeight = true, false, 100, 64 List(c, *g, len(names), gopts, func(i int) { ui.Text(c, names[i]).FontWeight(600) - ui.Text(c, "Office").TextColor(Tokens(c).TextMuted) + ui.Text(c, "Office").TextColor(core.Tokens(c).TextMuted) }).Element.Size(360, 228) }) }) diff --git a/statistic.go b/data/statistic.go similarity index 67% rename from statistic.go rename to data/statistic.go index cee212c..6efccc6 100644 --- a/statistic.go +++ b/data/statistic.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "math" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StatisticOptions configure a Statistic. It formats values; it computes none. @@ -32,21 +33,21 @@ func Statistic(c *ui.Context, value float64, opts StatisticOptions) *ui.Element if opts.Precision < 0 { panic("mujica: Statistic with negative Precision") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() format := opts.Format if format == nil { - format = func(v float64) string { return statisticFormat(v, opts.Precision) } + format = func(v float64) string { return StatisticFormat(v, opts.Precision) } } num := opts.Prefix + format(value) + opts.Suffix spoken := strings.TrimSpace(opts.Title + " " + num + " " + opts.Unit) e := ui.Column(c).Gap(u).Role(ui.RoleGroup) e.Children(func() { if opts.Title != "" { - ui.Text(c, opts.Title).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, opts.Title).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } ui.Row(c).Gap(u).AlignItems(ui.End).Children(func() { - ui.Text(c, num).Font(theme.SerifFont).FontSize(FontSize(c, theme.H1Size)).FontWeight(600).LineHeight(1.2).SingleLine() + ui.Text(c, num).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H1Size)).FontWeight(600).LineHeight(1.2).SingleLine() if opts.Unit != "" { ui.Text(c, opts.Unit).TextColor(k.TextMuted).Padding(0, 0, u, 0) } @@ -55,14 +56,14 @@ func Statistic(c *ui.Context, value float64, opts StatisticOptions) *ui.Element return } ch := *opts.Change - col, icon, word := k.TextMuted, "minus", msg(c, "statistic.flat") + col, icon, word := k.TextMuted, "minus", core.Msg(c, "statistic.flat") switch { case ch > 0: - col, icon, word = k.Success, "arrow-up", msg(c, "statistic.up") + col, icon, word = k.Success, "arrow-up", core.Msg(c, "statistic.up") case ch < 0: - col, icon, word = k.Danger, "arrow-down", msg(c, "statistic.down") + col, icon, word = k.Danger, "arrow-down", core.Msg(c, "statistic.down") } - text := statisticSigned(ch, opts.Precision) + opts.ChangeUnit + text := StatisticSigned(ch, opts.Precision) + opts.ChangeUnit spoken += ", " + word + " " + text ui.Row(c).Gap(u).AlignItems(ui.Center).Wrap().Children(func() { ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(col).Children(func() { @@ -70,15 +71,15 @@ func Statistic(c *ui.Context, value float64, opts StatisticOptions) *ui.Element ui.Text(c, text).FontWeight(600).SingleLine() }) if opts.ChangeNote != "" { - ui.Text(c, opts.ChangeNote).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, opts.ChangeNote).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) }) return e.Label(spoken) } -// statisticFormat writes v with p decimals and comma-grouped thousands. -func statisticFormat(v float64, p int) string { +// StatisticFormat writes v with p decimals and comma-grouped thousands. +func StatisticFormat(v float64, p int) string { if math.IsNaN(v) || math.IsInf(v, 0) { return strconv.FormatFloat(v, 'f', -1, 64) } @@ -100,9 +101,9 @@ func statisticFormat(v float64, p int) string { return b.String() } -// statisticSigned writes a change with an explicit sign. -func statisticSigned(v float64, p int) string { - s := statisticFormat(math.Abs(v), p) +// StatisticSigned writes a change with an explicit sign. +func StatisticSigned(v float64, p int) string { + s := StatisticFormat(math.Abs(v), p) switch { case v > 0: return "+" + s diff --git a/statistic_test.go b/data/statistic_test.go similarity index 91% rename from statistic_test.go rename to data/statistic_test.go index 5bf9951..12555d6 100644 --- a/statistic_test.go +++ b/data/statistic_test.go @@ -1,10 +1,12 @@ -package mujica +package data import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStatisticFormat(t *testing.T) { @@ -21,7 +23,7 @@ func TestStatisticFormat(t *testing.T) { {0, 3, "0.000"}, {math.Inf(1), 0, "+Inf"}, } { - if got := statisticFormat(tc.v, tc.p); got != tc.want { + if got := StatisticFormat(tc.v, tc.p); got != tc.want { t.Errorf("format(%v, %d) = %q, want %q", tc.v, tc.p, got, tc.want) } } @@ -35,7 +37,7 @@ func TestStatisticChange(t *testing.T) { }{{3.26, "+3.3%", "up"}, {-1.5, "−1.5%", "down"}, {0, "0.0%", "unchanged"}} { ch := tc.change tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Statistic(c, 12840, StatisticOptions{Title: "Pilgrims", Prefix: "≈", Unit: "souls", Precision: 1, Change: &ch, ChangeUnit: "%", ChangeNote: "vs last feast"}) }, 400, 200) for _, s := range []string{"Pilgrims", "≈12,840.0", "souls", tc.text, "vs last feast"} { @@ -52,7 +54,7 @@ func TestStatisticChange(t *testing.T) { func TestStatisticCustomFormatAndBadPrecision(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Statistic(c, 0.5, StatisticOptions{Format: func(v float64) string { return "half" }}) }, 200, 100) if !tt.HasText("half") { @@ -64,7 +66,7 @@ func TestStatisticCustomFormatAndBadPrecision(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Statistic(c, 1, StatisticOptions{Precision: -1}) }, 100, 100) } @@ -83,7 +85,7 @@ func TestStatisticSnapshot(t *testing.T) { func TestStatisticTextScale(t *testing.T) { var h float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { h = Statistic(c, 7, StatisticOptions{}).Bounds().H }) }, 300, 300) tt.Frame() diff --git a/timeline.go b/data/timeline.go similarity index 80% rename from timeline.go rename to data/timeline.go index 745e51a..3fc5c6c 100644 --- a/timeline.go +++ b/data/timeline.go @@ -1,11 +1,12 @@ -package mujica +package data import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TimelineStatus marks an event's state with a shape and a word, not only color. @@ -29,8 +30,8 @@ type TimelineItem struct { // Timeline lists events top to bottom with markers joined by a line; long text wraps. func Timeline(c *ui.Context, items []TimelineItem) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() return ui.Column(c).Role(ui.RoleList).Children(func() { for i, it := range items { last := i == len(items)-1 @@ -44,10 +45,10 @@ func Timeline(c *ui.Context, items []TimelineItem) *ui.Element { ui.Column(c).Grow(1).MinWidth(0).Gap(u).Padding(0, 0, u*5, 0).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Wrap().Children(func() { if it.Time != "" { - ui.Text(c, it.Time).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, it.Time).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } if w := timelineWord(c, it.Status); w != "" { - ui.Text(c, w).FontSize(FontSize(c, theme.CaptionSize)).TextColor(timelineColor(k, it.Status)).FontWeight(600) + ui.Text(c, w).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(timelineColor(k, it.Status)).FontWeight(600) } }) if it.Title != "" { @@ -79,20 +80,20 @@ func timelineColor(k theme.Tokens, s TimelineStatus) ui.Color { func timelineWord(c *ui.Context, s TimelineStatus) string { switch s { case TimelineDone: - return msg(c, "timeline.done") + return core.Msg(c, "timeline.done") case TimelineActive: - return msg(c, "timeline.active") + return core.Msg(c, "timeline.active") case TimelineWarning: - return msg(c, "timeline.warning") + return core.Msg(c, "timeline.warning") case TimelineError: - return msg(c, "timeline.error") + return core.Msg(c, "timeline.error") } return "" } // timelineMarker draws a hollow diamond, or a status icon. func timelineMarker(c *ui.Context, s TimelineStatus) { - k := Tokens(c) + k := core.Tokens(c) col := timelineColor(k, s) box := ui.Box(c).Size(20, 20).Shrink(0).Center().TextColor(col) switch s { diff --git a/timeline_test.go b/data/timeline_test.go similarity index 94% rename from timeline_test.go rename to data/timeline_test.go index e7a6a92..94d5c0a 100644 --- a/timeline_test.go +++ b/data/timeline_test.go @@ -1,9 +1,11 @@ -package mujica +package data import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var timelineItems = []TimelineItem{ @@ -16,7 +18,7 @@ var timelineItems = []TimelineItem{ func TestTimelineOrderAndStatus(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(260).Children(func() { Timeline(c, timelineItems) }) }, 300, 600) var ys []float32 diff --git a/tree.go b/data/tree.go similarity index 74% rename from tree.go rename to data/tree.go index e7c745c..48ec1bb 100644 --- a/tree.go +++ b/data/tree.go @@ -1,13 +1,15 @@ -package mujica +package data import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TreeOptions configure a Tree of items of type K, which are also their keys. @@ -32,34 +34,34 @@ type TreeOptions[K comparable] struct { type TreeState[K comparable] struct { // Selection holds the chosen items of a Multiple tree. Selection ui.Selection[K] - tree treeCore[K] + Tree treeCore[K] } // Selected returns the chosen item, or the last chosen of a Multiple tree. -func (s *TreeState[K]) Selected() (K, bool) { return s.tree.sel, s.tree.has } +func (s *TreeState[K]) Selected() (K, bool) { return s.Tree.Sel, s.Tree.Has } // SetOpen opens or closes an item. -func (s *TreeState[K]) SetOpen(item K, open bool) { s.tree.setOpen(item, open) } +func (s *TreeState[K]) SetOpen(item K, open bool) { s.Tree.setOpen(item, open) } // IsOpen reports whether an item shows its children. -func (s *TreeState[K]) IsOpen(item K) bool { return s.tree.outline.Open.Has(item) } +func (s *TreeState[K]) IsOpen(item K) bool { return s.Tree.Outline.Open.Has(item) } // treeCore is the outline state shared by Tree and TreeTable. type treeCore[K comparable] struct { - outline ui.OutlineState[K] + Outline ui.OutlineState[K] cursor int started bool - sel K - has bool + Sel K + Has bool // asked holds the open items whose children Load was asked for. asked map[K]bool } func (t *treeCore[K]) setOpen(item K, open bool) { if open { - t.outline.Open.Add(item) + t.Outline.Open.Add(item) } else { - t.outline.Open.Remove(item) + t.Outline.Open.Remove(item) } } @@ -68,46 +70,46 @@ func (t *treeCore[K]) begin(label func(K) string, sel *ui.Selection[K], multiple if !t.started { t.started, t.cursor, t.asked = true, -1, map[K]bool{} } - t.outline.List.Selected = &t.cursor - t.outline.List.Label = func(i int) string { return label(t.outline.Item(i)) } - t.outline.List.Selection = nil + t.Outline.List.Selected = &t.cursor + t.Outline.List.Label = func(i int) string { return label(t.Outline.Item(i)) } + t.Outline.List.Selection = nil if multiple { - t.outline.List.Selection = sel + t.Outline.List.Selection = sel } } // clear drops the choice and row state when the tree has no items. func (t *treeCore[K]) clear(sel *ui.Selection[K]) { var zero K - t.cursor, t.sel, t.has = -1, zero, false + t.cursor, t.Sel, t.Has = -1, zero, false sel.Clear() clear(t.asked) - t.outline = ui.OutlineState[K]{Open: t.outline.Open} + t.Outline = ui.OutlineState[K]{Open: t.Outline.Open} } // end reads a new choice, or finds the chosen item again after rows moved. func (t *treeCore[K]) end(c *ui.Context, changed bool) { - rows := t.outline.Rows() + rows := t.Outline.Rows() at := t.cursor >= 0 && t.cursor < rows - if changed || !t.has { - t.has = at + if changed || !t.Has { + t.Has = at if at { - t.sel = t.outline.Item(t.cursor) + t.Sel = t.Outline.Item(t.cursor) } return } - if at && t.outline.Item(t.cursor) == t.sel { + if at && t.Outline.Item(t.cursor) == t.Sel { return } t.cursor = -1 for i := range rows { - if t.outline.Item(i) == t.sel { + if t.Outline.Item(i) == t.Sel { t.cursor = i break } } if t.cursor < 0 { - t.has = false + t.Has = false return } // The rows showed the old place: build them again at the new one. @@ -120,7 +122,7 @@ func (t *treeCore[K]) chosen(item K, sel *ui.Selection[K], multiple bool) bool { if multiple { return sel.Has(item) } - return t.has && t.sel == item + return t.Has && t.Sel == item } // itemStatus asks for unloaded children of an open item and shows loading or failure. @@ -129,7 +131,7 @@ func (t *treeCore[K]) itemStatus(c *ui.Context, item K, status func(K) DataStatu return } st := status(item) - open := t.outline.Open.Has(item) + open := t.Outline.Open.Has(item) if st != DataUnloaded || !open { delete(t.asked, item) } @@ -141,17 +143,17 @@ func (t *treeCore[K]) itemStatus(c *ui.Context, item K, status func(K) DataStatu slog.Debug("mujica: tree load", "item", item) load(item) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() switch st { case DataLoading: - Spinner(c, SpinnerOptions{Size: 14, Label: msg(c, "list.loading"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: core.Msg(c, "list.loading"), ShowLabel: true}) case DataFailed: ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(14, 14) - ui.Text(c, msg(c, "list.failed")).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, core.Msg(c, "list.failed")).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) - if Button(c, msg(c, "list.retry"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() { + if input.Button(c, core.Msg(c, "list.retry"), input.ButtonOptions{Variant: input.Ghost}).MinHeight(theme.MinHitSize).Clicked() { slog.Debug("mujica: tree retry", "item", item) load(item) } @@ -178,23 +180,23 @@ func Tree[K comparable](c *ui.Context, s *TreeState[K], opts TreeOptions[K]) Tre if opts.Children == nil || opts.Label == nil { panic("mujica: Tree needs Children and Label") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() - t := &s.tree + t := &s.Tree t.begin(opts.Label, &s.Selection, opts.Multiple) var v TreeView v.Element = ui.Column(c).MinHeight(0) v.Element.Children(func() { if len(opts.Roots) == 0 { t.clear(&s.Selection) - v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + v.retry = DataPlaceholder(c, opts.Status, opts.Error, opts.Empty, core.Msg(c, "list.empty")) return } // Outline rows pad by one unit above and below. h := d.ControlHeight() - 2*c.Theme().Space(1) - restore := dataSelectTheme(c, 0) - e := ui.Outline(c, &t.outline, opts.Roots, opts.Children, func(item K) { + restore := DataSelectTheme(c, 0) + e := ui.Outline(c, &t.Outline, opts.Roots, opts.Children, func(item K) { ui.Row(c).Grow(1).MinWidth(0).MinHeight(h).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, opts.Label(item)).SingleLine().Grow(1).MinWidth(0) t.itemStatus(c, item, opts.ItemStatus, opts.Load) diff --git a/tree_table.go b/data/tree_table.go similarity index 86% rename from tree_table.go rename to data/tree_table.go index ddbb3f5..c941810 100644 --- a/tree_table.go +++ b/data/tree_table.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "log/slog" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TreeColumn is a column of a TreeTable; the first shows the arrows. @@ -51,7 +52,7 @@ type TreeTableState[K comparable] struct { Layout ui.TableLayout // Selection holds the chosen items of a Multiple table. Selection ui.Selection[K] - tree treeCore[K] + Tree treeCore[K] sorted map[K]treeSorted[K] roots treeSorted[K] } @@ -65,20 +66,20 @@ type treeSorted[K comparable] struct { } // Selected returns the chosen item, or the last chosen of a Multiple table. -func (s *TreeTableState[K]) Selected() (K, bool) { return s.tree.sel, s.tree.has } +func (s *TreeTableState[K]) Selected() (K, bool) { return s.Tree.Sel, s.Tree.Has } // SetOpen opens or closes an item. -func (s *TreeTableState[K]) SetOpen(item K, open bool) { s.tree.setOpen(item, open) } +func (s *TreeTableState[K]) SetOpen(item K, open bool) { s.Tree.setOpen(item, open) } // IsOpen reports whether an item shows its children. -func (s *TreeTableState[K]) IsOpen(item K) bool { return s.tree.outline.Open.Has(item) } +func (s *TreeTableState[K]) IsOpen(item K) bool { return s.Tree.Outline.Open.Has(item) } // TreeTable shows a hierarchy in sortable, resizable columns, building only rows in view. func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTableOptions[K]) DataTableView { treeTableCheck(opts) - k := Tokens(c) - d := Density(c) - t := &s.tree + k := core.Tokens(c) + d := core.Density(c) + t := &s.Tree t.begin(opts.Label, &s.Selection, opts.Multiple) if s.sorted == nil { s.sorted = map[K]treeSorted[K]{} @@ -125,7 +126,7 @@ func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTable v.Element.Children(func() { if len(opts.Roots) == 0 { t.clear(&s.Selection) - v.retry = dataPlaceholder(c, opts.Status, opts.Error, opts.Empty, msg(c, "list.empty")) + v.retry = DataPlaceholder(c, opts.Status, opts.Error, opts.Empty, core.Msg(c, "list.empty")) return } ui.Row(c).Justify(ui.End).Children(func() { tableColumnMenu(c, menu, &s.Sort, &s.Layout, nil) }) @@ -137,9 +138,9 @@ func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTable } roots := sorted(opts.Roots, &s.roots) head := s.Sort - t.outline.List.Sort, t.outline.List.Columns = &s.Sort, s.Layout - restore := dataSelectTheme(c, dataRowUnit(c)) - e := ui.OutlineTable(c, &t.outline, cols, roots, children, func(item K, col int) { + t.Outline.List.Sort, t.Outline.List.Columns = &s.Sort, s.Layout + restore := DataSelectTheme(c, dataRowUnit(c)) + e := ui.OutlineTable(c, &t.Outline, cols, roots, children, func(item K, col int) { if col > 0 { opts.Columns[col].Cell(item) return @@ -155,7 +156,7 @@ func TreeTable[K comparable](c *ui.Context, s *TreeTableState[K], opts TreeTable restore() e.Grow(1).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) internal.Ring(e, k.Focus, theme.ControlRadius) - s.Layout = t.outline.List.Columns + s.Layout = t.Outline.List.Columns v.changed = e.Changed() && s.Sort == head v.submitted = e.Submitted() v.retry = dataFooter(c, opts.Status, opts.Error) diff --git a/tree_table_test.go b/data/tree_table_test.go similarity index 90% rename from tree_table_test.go rename to data/tree_table_test.go index efacea2..9340a52 100644 --- a/tree_table_test.go +++ b/data/tree_table_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "cmp" @@ -7,6 +7,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type ttHarness struct { @@ -46,7 +48,7 @@ func newTTHarness() *ttHarness { } func (h *ttHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) h.c = c ui.Column(c).Padding(10).Children(func() { v := TreeTable(c, &h.st, h.opts) @@ -151,7 +153,7 @@ func TestTreeTableLargeIsVirtualized(t *testing.T) { return func(n int) { rows[n] = true; ui.Text(c, strconv.Itoa(n)) } } view := func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) clear(rows) TreeTable(c, &st, TreeTableOptions[int]{Roots: roots, Children: kids, Label: strconv.Itoa, Columns: []TreeColumn[int]{{ID: "n", Title: "N", Cell: cell(c), Compare: cmp.Compare[int]}, {ID: "m", Title: "M", Cell: cell(c)}}}).Element.Size(320, 360) @@ -165,13 +167,13 @@ func TestTreeTableLargeIsVirtualized(t *testing.T) { st.SetOpen(r, true) } tt.Frame() - if len(rows) > bound || st.tree.outline.Rows() != 100_000 { - t.Fatalf("open: %d rows of %d", len(rows), st.tree.outline.Rows()) + if len(rows) > bound || st.Tree.Outline.Rows() != 100_000 { + t.Fatalf("open: %d rows of %d", len(rows), st.Tree.Outline.Rows()) } st.Sort = ui.SortOrder{Column: "n", Descending: true} tt.Frame() - if len(rows) > bound || st.tree.outline.Item(0) != 99_000 || st.tree.outline.Item(1) != 99_999 { - t.Fatalf("sorted: %d rows, first %d %d", len(rows), st.tree.outline.Item(0), st.tree.outline.Item(1)) + if len(rows) > bound || st.Tree.Outline.Item(0) != 99_000 || st.Tree.Outline.Item(1) != 99_999 { + t.Fatalf("sorted: %d rows, first %d %d", len(rows), st.Tree.Outline.Item(0), st.Tree.Outline.Item(1)) } } @@ -181,8 +183,8 @@ func TestTreeTableSnapshot(t *testing.T) { h := newTTHarness() h.st.SetOpen("Vestry", true) h.st.Sort = ui.SortOrder{Column: "name"} - h.st.tree.started, h.st.tree.cursor, h.st.tree.asked = true, 2, map[string]bool{} - h.st.tree.sel, h.st.tree.has = "Vestry", true + h.st.Tree.started, h.st.Tree.cursor, h.st.Tree.asked = true, 2, map[string]bool{} + h.st.Tree.Sel, h.st.Tree.Has = "Vestry", true return h }) (*h).c = c @@ -199,7 +201,7 @@ func TestTreeTableSelectionFollowsLargeSort(t *testing.T) { } var st TreeTableState[int] view := func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TreeTable(c, &st, TreeTableOptions[int]{Roots: []int{0}, Label: strconv.Itoa, Children: func(n int) []int { if n == 0 { @@ -217,8 +219,8 @@ func TestTreeTableSelectionFollowsLargeSort(t *testing.T) { } st.Sort = ui.SortOrder{Column: "n", Descending: true} tt.Frame() - if k, ok := st.Selected(); !ok || k != 3 || st.tree.outline.Item(st.tree.cursor) != 3 { - t.Fatalf("after sort: selected %d %v at row %d", k, ok, st.tree.cursor) + if k, ok := st.Selected(); !ok || k != 3 || st.Tree.Outline.Item(st.Tree.cursor) != 3 { + t.Fatalf("after sort: selected %d %v at row %d", k, ok, st.Tree.cursor) } } diff --git a/tree_test.go b/data/tree_test.go similarity index 90% rename from tree_test.go rename to data/tree_test.go index 02408e7..221fe37 100644 --- a/tree_test.go +++ b/data/tree_test.go @@ -1,4 +1,4 @@ -package mujica +package data import ( "fmt" @@ -8,7 +8,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type treeHarness struct { @@ -46,7 +47,7 @@ func newTreeHarness() *treeHarness { } func (h *treeHarness) view(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) h.built = map[string]bool{} opts := h.opts label := opts.Label @@ -148,7 +149,7 @@ func TestTreeBadOptions(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) var s TreeState[int] Tree(c, &s, TreeOptions[int]{Roots: []int{1}}) }, 100, 100) @@ -181,7 +182,7 @@ func TestTreeLargeIsVirtualized(t *testing.T) { var st TreeState[int] built := 0 view := func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) built = 0 Tree(c, &st, TreeOptions[int]{Roots: roots, Children: kids, Label: func(n int) string { built++ @@ -198,12 +199,12 @@ func TestTreeLargeIsVirtualized(t *testing.T) { st.SetOpen(r, true) } tt.Frame() - if built > bound || st.tree.outline.Rows() != 100_000 { - t.Fatalf("open: label calls %d rows %d", built, st.tree.outline.Rows()) + if built > bound || st.Tree.Outline.Rows() != 100_000 { + t.Fatalf("open: label calls %d rows %d", built, st.Tree.Outline.Rows()) } - st.tree.outline.List.ScrollTo(64_000, ui.Start) + st.Tree.Outline.List.ScrollTo(64_000, ui.Start) tt.Frame() - if built > bound || !tt.HasText(fmt.Sprintf("Node %d", st.tree.outline.Item(64_000))) { + if built > bound || !tt.HasText(fmt.Sprintf("Node %d", st.Tree.Outline.Item(64_000))) { t.Fatalf("scrolled: label calls %d texts %q", built, tt.Texts()) } } @@ -215,8 +216,8 @@ func TestTreeSnapshot(t *testing.T) { h.st.SetOpen("Nave", true) h.st.SetOpen("Crypt", true) h.status["Crypt"] = DataLoading - h.st.tree.started, h.st.tree.cursor, h.st.tree.asked = true, 1, map[string]bool{} - h.st.tree.sel, h.st.tree.has = "Pulpit", true + h.st.Tree.started, h.st.Tree.cursor, h.st.Tree.asked = true, 1, map[string]bool{} + h.st.Tree.Sel, h.st.Tree.Has = "Pulpit", true return h }) ui.Column(c).Padding(16).Children(func() { @@ -232,7 +233,7 @@ func TestTreeSelectionFollowsLargeReorder(t *testing.T) { } var st TreeState[int] view := func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) Tree(c, &st, TreeOptions[int]{Roots: roots, Children: func(int) []int { return nil }, Label: func(n int) string { return fmt.Sprintf("Node %d", n) }}).Element.Size(320, 360) } @@ -248,8 +249,8 @@ func TestTreeSelectionFollowsLargeReorder(t *testing.T) { roots = slices.Clone(roots) slices.Reverse(roots) tt.Frame() - if k, ok := st.Selected(); !ok || k != 2 || st.tree.cursor != 2997 { - t.Fatalf("after reversing: selected %d %v at row %d", k, ok, st.tree.cursor) + if k, ok := st.Selected(); !ok || k != 2 || st.Tree.cursor != 2997 { + t.Fatalf("after reversing: selected %d %v at row %d", k, ok, st.Tree.cursor) } } diff --git a/date_separator.go b/date_separator.go deleted file mode 100644 index 7e03af2..0000000 --- a/date_separator.go +++ /dev/null @@ -1,70 +0,0 @@ -package mujica - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - "mujicaui/internal" - "mujicaui/theme" -) - -// DateSeparatorOptions configure a DateSeparator. -type DateSeparatorOptions struct { - // Today decides "Today" and "Yesterday"; zero uses the window clock. - Today Date -} - -// DateSeparator marks where a day's messages begin, localized. -func DateSeparator(c *ui.Context, day Date, opts DateSeparatorOptions) *ui.Element { - if !day.Valid() { - panic("mujica: DateSeparator needs a valid date") - } - today := opts.Today - if today.IsZero() { - today = DateOf(c.Now()) - } - k := Tokens(c) - u := Density(c).Unit() - label := dateSeparatorLabel(c, day, today) - row := ui.Row(c).FillWidth().Gap(u*3).AlignItems(ui.Center).Padding(u*2, 0).Role(ui.RoleText).Label(label) - line := func() { - ui.Box(c).Grow(1).Height(theme.BorderWidth).Background(k.Border) - } - row.Children(func() { - line() - ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Shrink(0).Children(func() { - ui.Box(c).Size(6, 6).Draw(func(p *ui.Painter, r ui.Rect) { - p.FillPath(internal.Diamond(r.X+3, r.Y+3, 6), k.Ornament) - }) - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() - }) - line() - }) - return row -} - -// dateSeparatorLabel names day relative to today, in the window's language. -func dateSeparatorLabel(c *ui.Context, day, today Date) string { - switch day { - case today: - return msg(c, "date_separator.today") - case today.AddDays(-1): - return msg(c, "date_separator.yesterday") - } - return fmt.Sprintf(msg(c, "calendar.day_label"), msg(c, fmt.Sprintf("calendar.weekday_long_%d", day.Weekday())), - msg(c, fmt.Sprintf("calendar.month_%d", day.Month)), day.Day, day.Year) -} - -// dateSeparatorWhen is a short stamp: time, day or date. -func dateSeparatorWhen(c *ui.Context, t time.Time, today Date) string { - day := DateOf(t) - if day == today { - return t.Format("15:04") - } - if day == today.AddDays(-1) { - return msg(c, "date_separator.yesterday") - } - return fmt.Sprintf(msg(c, "date_separator.short"), msg(c, fmt.Sprintf("calendar.month_%d", day.Month)), day.Day) -} diff --git a/date_separator_test.go b/date_separator_test.go deleted file mode 100644 index bd68bc1..0000000 --- a/date_separator_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package mujica - -import ( - "testing" - - "github.com/egoist/mygo/ui" -) - -func TestDateSeparatorLabels(t *testing.T) { - today := DateOf(chatNow) - for _, tc := range []struct { - locale Locale - day Date - want string - }{ - {EnUS, today, "Today"}, - {EnUS, today.AddDays(-1), "Yesterday"}, - {EnUS, Date{2026, 9, 1}, "Tuesday, September 1, 2026"}, - {ZhCN, today, "今天"}, - {ZhCN, today.AddDays(-1), "昨天"}, - {ZhCN, Date{2026, 9, 1}, "2026年9月1日 星期二"}, - } { - tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: tc.locale}) - DateSeparator(c, tc.day, DateSeparatorOptions{Today: today}) - }, 500, 80) - if !tt.HasText(tc.want) { - t.Errorf("%s %v: texts %q", tc.locale, tc.day, tt.Texts()) - } - } -} - -func TestDateSeparatorShortStamp(t *testing.T) { - var got []string - ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - today := DateOf(chatNow) - got = []string{dateSeparatorWhen(c, chatNow, today), dateSeparatorWhen(c, chatNow.AddDate(0, 0, -1), today), - dateSeparatorWhen(c, chatNow.AddDate(0, -2, 0), today)} - }, 100, 100, 1) - if got[0] != "15:30" || got[1] != "Yesterday" || got[2] != "August 5" { - t.Fatalf("stamps %q", got) - } -} - -func TestDateSeparatorRejectsInvalid(t *testing.T) { - defer func() { - if recover() == nil { - t.Fatal("no panic") - } - }() - ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - DateSeparator(c, Date{2026, 2, 30}, DateSeparatorOptions{}) - }, 100, 100, 1) -} - -func TestDateSeparatorSnapshot(t *testing.T) { - snapshot(t, "date_separator", 520, 110, func(c *ui.Context) { - ui.Column(c).Padding(16).Children(func() { - DateSeparator(c, DateOf(chatNow), DateSeparatorOptions{Today: DateOf(chatNow)}) - DateSeparator(c, Date{2026, 9, 1}, DateSeparatorOptions{Today: DateOf(chatNow)}) - }) - }) -} diff --git a/agenda_view.go b/datetime/agenda_view.go similarity index 76% rename from agenda_view.go rename to datetime/agenda_view.go index e3401c1..f7dbc44 100644 --- a/agenda_view.go +++ b/datetime/agenda_view.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AgendaViewOptions configure an AgendaView. @@ -17,11 +18,11 @@ type AgendaViewOptions struct { // Version must change when events change in place. Version int // From is the first day listed; zero means today. - From Date + From core.Date // Days is how many days are listed; zero means 30. Days int // Today is marked; zero uses the system clock. - Today Date + Today core.Date Hour12 bool Disabled bool Label string @@ -29,7 +30,7 @@ type AgendaViewOptions struct { // agendaRow is a day heading (ev < 0) or event. type agendaRow struct { - day Date + day core.Date ev int } @@ -44,17 +45,17 @@ func AgendaView(c *ui.Context, events []CalendarEvent, opts AgendaViewOptions) C days = 30 } eventsCheck("AgendaView", events) - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) from := opts.From if from.IsZero() { from = today } - datetimeCheckDate("AgendaView From", from) + core.DatetimeCheckDate("AgendaView From", from) label := opts.Label if label == "" { - label = msg(c, "agenda_view.label") + label = core.Msg(c, "agenda_view.label") } r := CalendarViewResult{} root := ui.Column(c).Grow(1).MinHeight(0).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -67,7 +68,7 @@ func AgendaView(c *ui.Context, events []CalendarEvent, opts AgendaViewOptions) C st.Header = func(i int) bool { return rows[i].ev < 0 } st.Label = func(i int) string { if rows[i].ev < 0 { - return datetimeDayLabel(c, rows[i].day) + return core.DatetimeDayLabel(c, rows[i].day) } return events[rows[i].ev].Title } @@ -77,10 +78,10 @@ func AgendaView(c *ui.Context, events []CalendarEvent, opts AgendaViewOptions) C if row.ev < 0 { h := ui.Row(c).MinHeight(d.ControlHeight()).AlignItems(ui.Center).Gap(d.Unit() * 2).PaddingX(d.Unit() * 3).Background(k.Background).Role(ui.RoleColumnHeader) h.Children(func() { - t := ui.Text(c, datetimeDayLabel(c, row.day)).Font(theme.SerifFont).TextColor(k.Text) + t := ui.Text(c, core.DatetimeDayLabel(c, row.day)).Font(theme.SerifFont).TextColor(k.Text) if row.day == today { t.Bold().TextColor(k.AccentText) - ui.Text(c, msg(c, "agenda_view.today")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) + ui.Text(c, core.Msg(c, "agenda_view.today")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) } }) h.DrawOver(func(p *ui.Painter, rr ui.Rect) { internal.Rule(p, rr.X, rr.X+rr.W, rr.Y+rr.H-1, k.Ornament) }) @@ -92,7 +93,7 @@ func AgendaView(c *ui.Context, events []CalendarEvent, opts AgendaViewOptions) C }).Grow(1).MinHeight(d.ControlHeight() * 4).FocusGroup(ui.Vertical) list.Children(func() { if len(rows) == 0 { - ui.Text(c, fmt.Sprintf(msg(c, "agenda_view.empty"), days)).TextColor(k.TextMuted).Padding(d.Unit() * 3) + ui.Text(c, fmt.Sprintf(core.Msg(c, "agenda_view.empty"), days)).TextColor(k.TextMuted).Padding(d.Unit() * 3) } }) }) @@ -100,7 +101,7 @@ func AgendaView(c *ui.Context, events []CalendarEvent, opts AgendaViewOptions) C } // agendaRows flattens the span into headed days. -func agendaRows(events []CalendarEvent, hits []int, from Date, days int, loc *time.Location) []agendaRow { +func agendaRows(events []CalendarEvent, hits []int, from core.Date, days int, loc *time.Location) []agendaRow { buckets := make([][]int, days) for _, h := range hits { ev := events[h] @@ -124,24 +125,24 @@ func agendaRows(events []CalendarEvent, hits []int, from Date, days int, loc *ti // agendaItem is one event row; it reports a click. func agendaItem(c *ui.Context, ev CalendarEvent, loc *time.Location, hour12 bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) when := eventTimeText(c, ev, loc, hour12) b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(d.Unit() * 3).PaddingX(d.Unit() * 3).Label(ev.Title + ", " + when) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - col := eventColor(k, ev.Color) + col := EventTone(k, ev.Color) b.Children(func() { - ui.Text(c, when).Width(d.ControlHeight() * 3.5).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, when).Width(d.ControlHeight() * 3.5).Shrink(0).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") ui.Box(c).Size(8, 8).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, r.W), col) }) ui.Column(c).Grow(1).Shrink(1).Children(func() { ui.Text(c, ev.Title).TextColor(k.Text).SingleLine().Ellipsis("…") if ev.Location != "" { - ui.Text(c, ev.Location).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Ellipsis("…") + ui.Text(c, ev.Location).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Ellipsis("…") } }) }) diff --git a/agenda_view_test.go b/datetime/agenda_view_test.go similarity index 97% rename from agenda_view_test.go rename to datetime/agenda_view_test.go index 630c7bd..0ef38f5 100644 --- a/agenda_view_test.go +++ b/datetime/agenda_view_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,12 +6,14 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func agendaTester(events []CalendarEvent, opts AgendaViewOptions, opened *[]string) *ui.Tester { opts.Location, opts.Today = calTokyo, calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(500).Children(func() { if id, ok := AgendaView(c, events, opts).Opened(); ok { *opened = append(*opened, id) diff --git a/attendee_list.go b/datetime/attendee_list.go similarity index 71% rename from attendee_list.go rename to datetime/attendee_list.go index a1a8568..feb2f2e 100644 --- a/attendee_list.go +++ b/datetime/attendee_list.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AttendeeStatus is an attendee's reply. @@ -48,8 +50,8 @@ func (r AttendeeListResult) Removed() (int, bool) { return r.removed, r.removed // AttendeeList shows invitees, their replies and a summary. func AttendeeList(c *ui.Context, attendees []Attendee, opts AttendeeListOptions) AttendeeListResult { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var counts [4]int for _, a := range attendees { if a.Status < AttendeePending || a.Status > AttendeeDeclined { @@ -59,40 +61,40 @@ func AttendeeList(c *ui.Context, attendees []Attendee, opts AttendeeListOptions) } label := opts.Label if label == "" { - label = msg(c, "attendee_list.label") + label = core.Msg(c, "attendee_list.label") } r := AttendeeListResult{removed: -1} r.Element = ui.Column(c).Gap(d.Unit()).Role(ui.RoleList).Label(label).Disabled(opts.Disabled) r.Element.Children(func() { - summary := fmt.Sprintf(msg(c, "attendee_list.summary"), counts[AttendeeAccepted], counts[AttendeeTentative], + summary := fmt.Sprintf(core.Msg(c, "attendee_list.summary"), counts[AttendeeAccepted], counts[AttendeeTentative], counts[AttendeeDeclined], counts[AttendeePending]) - ui.Text(c, summary).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, summary).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) if len(attendees) == 0 { - ui.Text(c, msg(c, "attendee_list.empty")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "attendee_list.empty")).TextColor(k.TextMuted) } for i, a := range attendees { ui.Row(c).Key(i).MinHeight(d.ControlHeight()).Gap(d.Unit() * 2).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() { - Avatar(c, a.Name, AvatarOptions{Size: 28}) + display.Avatar(c, a.Name, display.AvatarOptions{Size: 28}) ui.Column(c).Grow(1).Shrink(1).Children(func() { name := a.Name if a.Organizer { - name += " · " + msg(c, "attendee_list.organizer") + name += " · " + core.Msg(c, "attendee_list.organizer") } if a.Optional { - name += " · " + msg(c, "attendee_list.optional") + name += " · " + core.Msg(c, "attendee_list.optional") } ui.Text(c, name).TextColor(k.Text).SingleLine().Ellipsis("…") if a.Email != "" { - ui.Text(c, a.Email).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Ellipsis("…") + ui.Text(c, a.Email).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().Ellipsis("…") } }) icon, fg := attendeeStatusLook(k, a.Status) ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Shrink(0).TextColor(fg).Children(func() { ui.Icon(c, icons.Must(icon)).Size(16, 16) - ui.Text(c, msg(c, attendeeStatusKeys[a.Status])).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, attendeeStatusKeys[a.Status])).FontSize(core.FontSize(c, theme.CaptionSize)) }) if opts.Removable && !a.Organizer { - b := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "attendee_list.remove"), a.Name)) + b := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "attendee_list.remove"), a.Name)) if b.Clicked() && !b.IsDisabled() { slog.Debug("mujica: attendee_list remove", "index", i) r.removed = i diff --git a/attendee_list_test.go b/datetime/attendee_list_test.go similarity index 96% rename from attendee_list_test.go rename to datetime/attendee_list_test.go index 1f9b350..11ba647 100644 --- a/attendee_list_test.go +++ b/datetime/attendee_list_test.go @@ -1,9 +1,11 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var calAttendees = []Attendee{ @@ -15,7 +17,7 @@ var calAttendees = []Attendee{ func attendeeTester(list *[]Attendee, opts AttendeeListOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if i, ok := AttendeeList(c, *list, opts).Removed(); ok { *list = append((*list)[:i:i], (*list)[i+1:]...) } diff --git a/availability_picker.go b/datetime/availability_picker.go similarity index 78% rename from availability_picker.go rename to datetime/availability_picker.go index b7dc491..5d2223c 100644 --- a/availability_picker.go +++ b/datetime/availability_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AvailabilityPerson is another person's free slots, [day][slot]. @@ -20,7 +21,7 @@ type AvailabilityPerson struct { // AvailabilityPickerOptions configure an AvailabilityPicker. type AvailabilityPickerOptions struct { // Days are the grid's columns; required. - Days []Date + Days []core.Date // From and To are hours; zero means 9 to 18. From, To int // SlotMinutes must divide an hour; zero means 30. @@ -39,7 +40,7 @@ type availabilityDrag struct { } // AvailabilityPicker paints free slots and counts others' overlap. -func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOptions) ChoiceResult { +func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOptions) core.ChoiceResult { from, to := opts.From, opts.To if from == 0 && to == 0 { from, to = 9, 18 @@ -53,11 +54,11 @@ func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOp for _, p := range opts.Others { availabilityCheck(p.Name, p.Free, len(opts.Days), slots) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "availability_picker.label") + label = core.Msg(c, "availability_picker.label") } gutter := d.ControlHeight() * 1.8 rowH := float32(theme.MinHitSize) @@ -101,8 +102,8 @@ func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOp ui.Row(c).Height(headH).Children(func() { ui.Box(c).Width(gutter).Shrink(0) for _, day := range opts.Days { - ui.Text(c, msg(c, "calendar.weekday_"+strconv.Itoa(int(day.Weekday())))+" "+strconv.Itoa(day.Day)).Grow(1).Basis(0). - TextAlign(ui.Center).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleColumnHeader) + ui.Text(c, core.Msg(c, "calendar.weekday_"+strconv.Itoa(int(day.Weekday())))+" "+strconv.Itoa(day.Day)).Grow(1).Basis(0). + TextAlign(ui.Center).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleColumnHeader) } }) for s := range slots { @@ -112,7 +113,7 @@ func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOp if m%60 == 0 { hour = timeText(c, TimeOfDay{m / 60, 0, 0}, timeConfig{hour12: opts.Hour12}) } - ui.Text(c, hour).Width(gutter).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, hour).Width(gutter).Shrink(0).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") for di, day := range opts.Days { free := 0 for _, p := range opts.Others { @@ -128,7 +129,7 @@ func AvailabilityPicker(c *ui.Context, mine *[][]bool, opts AvailabilityPickerOp if changed { slog.Debug("mujica: availability_picker change") } - return ChoiceResult{Element: grid, changed: changed} + return core.NewChoiceResult(grid, changed) } func availabilityCheck(name string, v [][]bool, days, slots int) { @@ -143,14 +144,14 @@ func availabilityCheck(name string, v [][]bool, days, slots int) { } // availabilityCell shows one slot's choice and free count. -func availabilityCell(c *ui.Context, day Date, m int, mine bool, free, others int, cursor, hour12 bool) { - k := Tokens(c) - text := datetimeDayLabel(c, day) + " " + timeText(c, TimeOfDay{m / 60, m % 60, 0}, timeConfig{hour12: hour12}) +func availabilityCell(c *ui.Context, day core.Date, m int, mine bool, free, others int, cursor, hour12 bool) { + k := core.Tokens(c) + text := core.DatetimeDayLabel(c, day) + " " + timeText(c, TimeOfDay{m / 60, m % 60, 0}, timeConfig{hour12: hour12}) if mine { - text += ", " + msg(c, "availability_picker.mine") + text += ", " + core.Msg(c, "availability_picker.mine") } if others > 0 { - text += ", " + fmt.Sprintf(msg(c, "availability_picker.others"), free, others) + text += ", " + fmt.Sprintf(core.Msg(c, "availability_picker.others"), free, others) } everyone := mine && others > 0 && free == others cell := ui.Box(c).Grow(1).Basis(0).MinWidth(0).FillHeight().Center().Role(ui.RoleCell).Label(text).BorderWidth(0, 0, theme.BorderWidth, theme.BorderWidth).BorderColor(k.Border) @@ -166,12 +167,12 @@ func availabilityCell(c *ui.Context, day Date, m int, mine bool, free, others in internal.Mark(p, ui.Rect{X: r.X + r.W - 16, Y: r.Y + 2, W: 12, H: 12}, "check", k.AccentText) } if cursor { - datetimeSelectedMark(p, r, k.Focus) + core.DatetimeSelectedMark(p, r, k.Focus) } }) cell.Children(func() { if others > 0 && free > 0 { - t := ui.Text(c, strconv.Itoa(free)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + t := ui.Text(c, strconv.Itoa(free)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") if everyone { t.Bold().TextColor(k.AccentText) } diff --git a/availability_picker_test.go b/datetime/availability_picker_test.go similarity index 95% rename from availability_picker_test.go rename to datetime/availability_picker_test.go index 86f47a3..e648395 100644 --- a/availability_picker_test.go +++ b/datetime/availability_picker_test.go @@ -1,9 +1,11 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func availabilityGrid(days, slots int) [][]bool { @@ -14,11 +16,11 @@ func availabilityGrid(days, slots int) [][]bool { return v } -var availabilityDays = []Date{oct(12), oct(13), oct(14)} +var availabilityDays = []core.Date{oct(12), oct(13), oct(14)} func availabilityTester(v *[][]bool, opts *AvailabilityPickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := *opts o.Days, o.From, o.To = availabilityDays, 9, 12 ui.Column(c).Padding(16).Children(func() { diff --git a/calendar.go b/datetime/calendar.go similarity index 65% rename from calendar.go rename to datetime/calendar.go index 8b04255..e46bd00 100644 --- a/calendar.go +++ b/datetime/calendar.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WeekStart is a month's first column; zero follows the locale. @@ -26,25 +27,25 @@ const ( // CalendarOptions configure a Calendar. type CalendarOptions struct { // Min and Max bound choosable days; zero is open. - Min, Max Date + Min, Max core.Date // DisableDate marks single days that cannot be chosen. - DisableDate func(Date) bool + DisableDate func(core.Date) bool WeekStart WeekStart // Today is marked; zero uses the system clock. - Today Date + Today core.Date Disabled bool // Label names the calendar for assistive technology. Label string } // Calendar shows one month and chooses a day into *value. -func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult { - datetimeCheckDate("Calendar value", *value) - datetimeCheckBounds("Calendar", opts.Min, opts.Max) +func Calendar(c *ui.Context, value *core.Date, opts CalendarOptions) DateTimeResult { + core.DatetimeCheckDate("Calendar value", *value) + core.DatetimeCheckBounds("Calendar", opts.Min, opts.Max) box := ui.Column(c) - cursor := ui.Local(box, "calendar-cursor", func() Date { return Date{} }) - seen := ui.Local(box, "calendar-seen", func() Date { return Date{} }) - today := datetimeToday(c, opts.Today) + cursor := ui.Local(box, "calendar-cursor", func() core.Date { return core.Date{} }) + seen := ui.Local(box, "calendar-seen", func() core.Date { return core.Date{} }) + today := core.DatetimeToday(c, opts.Today) if *seen != *value || cursor.IsZero() { *seen = *value *cursor = *value @@ -55,7 +56,7 @@ func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult { changed := false box.Disabled(opts.Disabled).Children(func() { picked, _ := calendarMonth(c, cursor, calendarConfig{ - chosen: func(d Date) bool { return d == *value }, + chosen: func(d core.Date) bool { return d == *value }, enabled: calendarEnabled(opts.Min, opts.Max, opts.DisableDate), today: today, weekStart: opts.WeekStart, @@ -72,22 +73,22 @@ func Calendar(c *ui.Context, value *Date, opts CalendarOptions) DateTimeResult { // calendarConfig is how calendarMonth marks and limits its days. type calendarConfig struct { - chosen func(Date) bool - inRange func(Date) bool - enabled func(Date) bool - today Date + chosen func(core.Date) bool + inRange func(core.Date) bool + enabled func(core.Date) bool + today core.Date weekStart WeekStart autoFocus bool label string } // calendarEnabled combines bounds and a day filter. -func calendarEnabled(lo, hi Date, disable func(Date) bool) func(Date) bool { - return func(d Date) bool { return d.within(lo, hi) && (disable == nil || !disable(d)) } +func calendarEnabled(lo, hi core.Date, disable func(core.Date) bool) func(core.Date) bool { + return func(d core.Date) bool { return d.Within(lo, hi) && (disable == nil || !disable(d)) } } -// calendarFirstWeekday resolves the first column for the window's locale. -func calendarFirstWeekday(c *ui.Context, w WeekStart) time.Weekday { +// CalendarFirstWeekday resolves the first column for the window's locale. +func CalendarFirstWeekday(c *ui.Context, w WeekStart) time.Weekday { switch w { case WeekStartSunday: return time.Sunday @@ -96,7 +97,7 @@ func calendarFirstWeekday(c *ui.Context, w WeekStart) time.Weekday { case WeekStartSaturday: return time.Saturday case WeekStartLocale: - if CurrentLocale(c) == ZhCN { + if core.CurrentLocale(c) == core.ZhCN { return time.Monday } return time.Sunday @@ -105,7 +106,7 @@ func calendarFirstWeekday(c *ui.Context, w WeekStart) time.Weekday { } // calendarSeek returns the first enabled day from d stepping by dir, or d. -func calendarSeek(d Date, dir int, enabled func(Date) bool) Date { +func calendarSeek(d core.Date, dir int, enabled func(core.Date) bool) core.Date { for i, x := 0, d; i < 31; i, x = i+1, x.AddDays(dir) { if enabled(x) { return x @@ -115,46 +116,46 @@ func calendarSeek(d Date, dir int, enabled func(Date) bool) Date { } // calendarMonth draws *cursor's month; returns picked and hovered days. -func calendarMonth(c *ui.Context, cursor *Date, cfg calendarConfig) (picked, hovered Date) { - k := Tokens(c) - d := Density(c) +func calendarMonth(c *ui.Context, cursor *core.Date, cfg calendarConfig) (picked, hovered core.Date) { + k := core.Tokens(c) + d := core.Density(c) cur := *cursor - title := datetimeMonthTitle(c, cur.Year, cur.Month) + title := core.DatetimeMonthTitle(c, cur.Year, cur.Month) label := cfg.label if label == "" { - label = msg(c, "calendar.label") + label = core.Msg(c, "calendar.label") } cell := d.ControlHeight() root := ui.Column(c).Gap(d.Unit() * 2).Shrink(0).Width(cell * 7).Role(ui.RoleGroup).Label(label) keyed := ui.Local(root, "calendar-keys", func() bool { return false }) - move := func(to Date, dir int, keys bool) { + move := func(to core.Date, dir int, keys bool) { *cursor = calendarSeek(to, dir, cfg.enabled) *keyed = keys } root.Children(func() { ui.Row(c).AlignItems(ui.Center).Gap(d.Unit()).Children(func() { - if iconAction(c, icons.Must("chevrons-left"), msg(c, "calendar.prev_year")).Clicked() { + if core.IconAction(c, icons.Must("chevrons-left"), core.Msg(c, "calendar.prev_year")).Clicked() { move(cur.AddMonths(-12), -1, false) } - if iconAction(c, icons.Must("chevron-left"), msg(c, "calendar.prev_month")).Clicked() { + if core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "calendar.prev_month")).Clicked() { move(cur.AddMonths(-1), -1, false) } - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.BodySize+2)).Bold(). + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.BodySize+2)).Bold(). TextColor(k.Text).Grow(1).TextAlign(ui.Center).SingleLine() - if iconAction(c, icons.Must("chevron-right"), msg(c, "calendar.next_month")).Clicked() { + if core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "calendar.next_month")).Clicked() { move(cur.AddMonths(1), 1, false) } - if iconAction(c, icons.Must("chevrons-right"), msg(c, "calendar.next_year")).Clicked() { + if core.IconAction(c, icons.Must("chevrons-right"), core.Msg(c, "calendar.next_year")).Clicked() { move(cur.AddMonths(12), 1, false) } }) - first := calendarFirstWeekday(c, cfg.weekStart) + first := CalendarFirstWeekday(c, cfg.weekStart) ui.Row(c).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).PaddingY(d.Unit()).Children(func() { for i := range 7 { wd := (int(first) + i) % 7 - ui.Text(c, msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleColumnHeader). - Label(msg(c, "calendar.weekday_long_"+strconv.Itoa(wd))) + ui.Text(c, core.Msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleColumnHeader). + Label(core.Msg(c, "calendar.weekday_long_"+strconv.Itoa(wd))) } }) days := ui.Column(c).Role(ui.RoleTable).Label(title).FocusGroup(ui.Horizontal | ui.Vertical) @@ -177,14 +178,14 @@ func calendarMonth(c *ui.Context, cursor *Date, cfg calendarConfig) (picked, hov case days.Shortcut(ui.Shift, ui.KeyPageDown): move(cur.AddMonths(12), 1, true) case days.Shortcut(0, ui.KeyHome): - move(Date{cur.Year, cur.Month, 1}, 1, true) + move(core.Date{Year: cur.Year, Month: cur.Month, Day: 1}, 1, true) case days.Shortcut(0, ui.KeyEnd): - move(Date{cur.Year, cur.Month, 1}.AddMonths(1).AddDays(-1), -1, true) + move(core.Date{Year: cur.Year, Month: cur.Month, Day: 1}.AddMonths(1).AddDays(-1), -1, true) default: keysMoved = false } cur = *cursor - start := Date{cur.Year, cur.Month, 1} + start := core.Date{Year: cur.Year, Month: cur.Month, Day: 1} start = start.AddDays(-((int(start.Weekday()) - int(first) + 7) % 7)) days.Children(func() { for w := range 6 { @@ -208,16 +209,16 @@ func calendarMonth(c *ui.Context, cursor *Date, cfg calendarConfig) (picked, hov } // calendarDay draws one day cell and reports whether it was picked or hovered. -func calendarDay(c *ui.Context, day, cur Date, cfg calendarConfig, size float32, focus, keyed bool) (picked, hovered bool) { - k := Tokens(c) +func calendarDay(c *ui.Context, day, cur core.Date, cfg calendarConfig, size float32, focus, keyed bool) (picked, hovered bool) { + k := core.Tokens(c) enabled := cfg.enabled(day) chosen := cfg.chosen(day) - sel := Date{} + sel := core.Date{} if chosen { sel = day } cell := ui.RadioBase(c, &sel, day).Size(size, size-4).Center().Radius(theme.ControlRadius). - Label(datetimeDayLabel(c, day)).Disabled(!enabled).FocusRing(false) + Label(core.DatetimeDayLabel(c, day)).Disabled(!enabled).FocusRing(false) if day == cur && enabled { if focus { cell.Focus() @@ -247,7 +248,7 @@ func calendarDay(c *ui.Context, day, cur Date, cfg calendarConfig, size float32, ring := cell.FocusVisible() || cell.Focused() && keyed cell.DrawOver(func(p *ui.Painter, r ui.Rect) { if chosen { - datetimeSelectedMark(p, r, k.OnAccent) + core.DatetimeSelectedMark(p, r, k.OnAccent) } if day == cfg.today { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H-5, 5), fg) diff --git a/calendar_day_view.go b/datetime/calendar_day_view.go similarity index 61% rename from calendar_day_view.go rename to datetime/calendar_day_view.go index a1856ab..c4c809a 100644 --- a/calendar_day_view.go +++ b/datetime/calendar_day_view.go @@ -1,9 +1,11 @@ -package mujica +package datetime import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // CalendarDayViewOptions configure a CalendarDayView. @@ -15,7 +17,7 @@ type CalendarDayViewOptions struct { // SlotMinutes is the grid step; zero means 30. SlotMinutes int // Today is marked; zero uses the system clock. - Today Date + Today core.Date // Now places the current-time line; zero uses the system clock. Now time.Time Hour12 bool @@ -24,27 +26,27 @@ type CalendarDayViewOptions struct { } // CalendarDayView shows one day's time grid. -func CalendarDayView(c *ui.Context, day *Date, events []CalendarEvent, opts CalendarDayViewOptions) CalendarViewResult { - datetimeCheckDate("CalendarDayView day", *day) +func CalendarDayView(c *ui.Context, day *core.Date, events []CalendarEvent, opts CalendarDayViewOptions) CalendarViewResult { + core.DatetimeCheckDate("CalendarDayView day", *day) if day.IsZero() { panic("mujica: CalendarDayView needs a day") } cfg := timeGridConfig{name: "calendar_day_view", loc: eventLocation("CalendarDayView", opts.Location), version: opts.Version, - slot: timeGridSlot("CalendarDayView", opts.SlotMinutes), today: datetimeToday(c, opts.Today), now: opts.Now, hour12: opts.Hour12} + slot: timeGridSlot("CalendarDayView", opts.SlotMinutes), today: core.DatetimeToday(c, opts.Today), now: opts.Now, hour12: opts.Hour12} eventsCheck("CalendarDayView", events) - title := datetimeDayLabel(c, *day) + title := core.DatetimeDayLabel(c, *day) label := opts.Label if label == "" { label = title } r := CalendarViewResult{} - root := ui.Column(c).Gap(Density(c).Unit()).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) + root := ui.Column(c).Gap(core.Density(c).Unit()).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) r.Element = root root.Children(func() { - calendarViewHeader(c, title, msg(c, "calendar_day_view.prev"), msg(c, "calendar_day_view.next"), func(dir int) { + calendarViewHeader(c, title, core.Msg(c, "calendar_day_view.prev"), core.Msg(c, "calendar_day_view.next"), func(dir int) { *day = day.AddDays(dir) }) - timeGrid(c, root, []Date{*day}, events, cfg, &r) + timeGrid(c, root, []core.Date{*day}, events, cfg, &r) }) return r } diff --git a/calendar_day_view_test.go b/datetime/calendar_day_view_test.go similarity index 91% rename from calendar_day_view_test.go rename to datetime/calendar_day_view_test.go index 8ffa166..456a6e0 100644 --- a/calendar_day_view_test.go +++ b/datetime/calendar_day_view_test.go @@ -1,17 +1,19 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func dayViewTester(day *Date, events []CalendarEvent, opts CalendarDayViewOptions, changes *[]CalendarChange) *ui.Tester { +func dayViewTester(day *core.Date, events []CalendarEvent, opts CalendarDayViewOptions, changes *[]CalendarChange) *ui.Tester { opts.Location, opts.Today = calTokyo, calendarToday opts.Now = time.Date(2026, time.October, 14, 10, 30, 0, 0, calTokyo) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ch, ok := CalendarDayView(c, day, events, opts).Changed(); ok { *changes = append(*changes, ch) } @@ -62,7 +64,7 @@ func TestCalendarDayViewNavigationSlotAndDisabled(t *testing.T) { t.Fatal("expected panic without a day") } }() - zero := Date{} + zero := core.Date{} dayViewTester(&zero, nil, CalendarDayViewOptions{}, &ch) } diff --git a/calendar_events.go b/datetime/calendar_events.go similarity index 88% rename from calendar_events.go rename to datetime/calendar_events.go index cf624e0..a4e3e00 100644 --- a/calendar_events.go +++ b/datetime/calendar_events.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EventColor picks an event's color from the theme's event palette. @@ -59,7 +60,7 @@ type CalendarViewResult struct { Element *ui.Element opened string change *CalendarChange - day Date + day core.Date } // Opened returns the event the user activated. @@ -74,14 +75,14 @@ func (r CalendarViewResult) Changed() (CalendarChange, bool) { } // DayChosen returns the day the user activated. -func (r CalendarViewResult) DayChosen() (Date, bool) { return r.day, !r.day.IsZero() } +func (r CalendarViewResult) DayChosen() (core.Date, bool) { return r.day, !r.day.IsZero() } // eventPalette maps EventColor to tokens at 3:1 on Surface. func eventPalette(k theme.Tokens) [6]ui.Color { return [6]ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Danger, k.TextMuted} } -func eventColor(k theme.Tokens, c EventColor) ui.Color { +func EventTone(k theme.Tokens, c EventColor) ui.Color { if c < EventAccent || c > EventNeutral { panic(fmt.Sprintf("mujica: EventColor %d out of range", c)) } @@ -182,14 +183,14 @@ func eventColumns(spans []eventSpan) (col, cols []int) { } // eventDayStart is d's first instant in loc. -func eventDayStart(d Date, loc *time.Location) time.Time { +func eventDayStart(d core.Date, loc *time.Location) time.Time { return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) } // eventTimeText formats an event's wall-clock span in loc. func eventTimeText(c *ui.Context, ev CalendarEvent, loc *time.Location, hour12 bool) string { if ev.AllDay { - return msg(c, "event_chip.all_day") + return core.Msg(c, "event_chip.all_day") } cfg := timeConfig{hour12: hour12} s, e := ev.Start.In(loc), ev.End.In(loc) @@ -202,10 +203,10 @@ func eventShift(c *ui.Context, ev CalendarEvent, days int, loc *time.Location) ( s, e := ev.Start.In(loc), ev.End.In(loc) ch := CalendarChange{Kind: CalendarMove, ID: ev.ID, AllDay: ev.AllDay} if ev.AllDay { - ch.Start, ch.End = eventDayStart(DateOf(s).AddDays(days), loc), eventDayStart(DateOf(e).AddDays(days), loc) + ch.Start, ch.End = eventDayStart(core.DateOf(s).AddDays(days), loc), eventDayStart(core.DateOf(e).AddDays(days), loc) return ch, "" } - day := DateOf(s).AddDays(days) + day := core.DateOf(s).AddDays(days) wall := TimeOfDay{s.Hour(), s.Minute(), s.Second()} at, n := dateTimeResolve(day, wall, loc) if p := eventWallProblem(c, day, wall, loc, n); p != "" { @@ -216,7 +217,7 @@ func eventShift(c *ui.Context, ev CalendarEvent, days int, loc *time.Location) ( } // eventWallProblem reports a wall time DST skips or repeats. -func eventWallProblem(c *ui.Context, day Date, wall TimeOfDay, loc *time.Location, n int) string { +func eventWallProblem(c *ui.Context, day core.Date, wall TimeOfDay, loc *time.Location, n int) string { key := "" switch { case n == 0: @@ -226,7 +227,7 @@ func eventWallProblem(c *ui.Context, day Date, wall TimeOfDay, loc *time.Locatio default: return "" } - return fmt.Sprintf(msg(c, key), day.String()+" "+fmt.Sprintf("%02d:%02d", wall.Hour, wall.Minute), loc.String()) + return fmt.Sprintf(core.Msg(c, key), day.String()+" "+fmt.Sprintf("%02d:%02d", wall.Hour, wall.Minute), loc.String()) } // eventLocation panics without a caller zone; views never assume UTC. diff --git a/calendar_month_view.go b/datetime/calendar_month_view.go similarity index 74% rename from calendar_month_view.go rename to datetime/calendar_month_view.go index 2d8a72b..e03bd65 100644 --- a/calendar_month_view.go +++ b/datetime/calendar_month_view.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CalendarMonthViewOptions configure a CalendarMonthView. @@ -23,7 +24,7 @@ type CalendarMonthViewOptions struct { // MaxPerDay is rows shown before "+N"; zero means 3. MaxPerDay int // Today is marked; zero uses the system clock. - Today Date + Today core.Date Hour12 bool Disabled bool Label string @@ -52,21 +53,21 @@ func CalendarMonthView(c *ui.Context, month *YearMonth, events []CalendarEvent, maxRows = 3 } eventsCheck("CalendarMonthView", events) - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) - first := calendarFirstWeekday(c, opts.WeekStart) - start := Date{month.Year, month.Month, 1} + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) + first := CalendarFirstWeekday(c, opts.WeekStart) + start := core.Date{Year: month.Year, Month: month.Month, Day: 1} start = start.AddDays(-((int(start.Weekday()) - int(first) + 7) % 7)) label := opts.Label if label == "" { - label = datetimeMonthTitle(c, month.Year, month.Month) + label = core.DatetimeMonthTitle(c, month.Year, month.Month) } r := CalendarViewResult{} root := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) r.Element = root problem := ui.Local(root, "month-view-problem", func() string { return "" }) - more := ui.Local(root, "month-view-more", func() Date { return Date{} }) + more := ui.Local(root, "month-view-more", func() core.Date { return core.Date{} }) days := eventsDerived(root, "month-view-days", events, opts.Version, eventDayStart(start, loc), eventDayStart(start.AddDays(42), loc), func(hits []int) []monthDay { return monthLayout(events, hits, start, loc) }) disabled := func() bool { return root.IsDisabled() } @@ -79,20 +80,20 @@ func CalendarMonthView(c *ui.Context, month *YearMonth, events []CalendarEvent, } root.Children(func() { ui.Row(c).AlignItems(ui.Center).Gap(d.Unit()).Children(func() { - if iconAction(c, icons.Must("chevron-left"), msg(c, "calendar.prev_month")).Clicked() && !disabled() { + if core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "calendar.prev_month")).Clicked() && !disabled() { *month = YearMonth{start.AddDays(7).AddMonths(-1).Year, start.AddDays(7).AddMonths(-1).Month} } - ui.Text(c, datetimeMonthTitle(c, month.Year, month.Month)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)). + ui.Text(c, core.DatetimeMonthTitle(c, month.Year, month.Month)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)). TextColor(k.Text).Grow(1).TextAlign(ui.Center) - if iconAction(c, icons.Must("chevron-right"), msg(c, "calendar.next_month")).Clicked() && !disabled() { + if core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "calendar.next_month")).Clicked() && !disabled() { *month = YearMonth{start.AddDays(7).AddMonths(1).Year, start.AddDays(7).AddMonths(1).Month} } }) ui.Row(c).Children(func() { for i := range 7 { wd := (int(first) + i) % 7 - ui.Text(c, msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Grow(1).Basis(0).TextAlign(ui.Center). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Label(msg(c, "calendar.weekday_long_"+strconv.Itoa(wd))) + ui.Text(c, core.Msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Grow(1).Basis(0).TextAlign(ui.Center). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Label(core.Msg(c, "calendar.weekday_long_"+strconv.Itoa(wd))) } }) ui.Column(c).Border(theme.BorderWidth, k.Border).Dividers(theme.BorderWidth, k.Border).FocusGroup(ui.Horizontal | ui.Vertical).Children(func() { @@ -106,13 +107,13 @@ func CalendarMonthView(c *ui.Context, month *YearMonth, events []CalendarEvent, }) } }) - datetimeError(c, *problem) + core.DatetimeError(c, *problem) }) return r } // monthLayout buckets events per day, concurrent ones side by side. -func monthLayout(events []CalendarEvent, hits []int, start Date, loc *time.Location) []monthDay { +func monthLayout(events []CalendarEvent, hits []int, start core.Date, loc *time.Location) []monthDay { out := make([]monthDay, 42) for i := range out { from, to := eventDayStart(start.AddDays(i), loc), eventDayStart(start.AddDays(i+1), loc) @@ -140,10 +141,10 @@ func monthLayout(events []CalendarEvent, hits []int, start Date, loc *time.Locat } // monthCell draws one day: number, chips and "+N". -func monthCell(c *ui.Context, day Date, month YearMonth, today Date, lay monthDay, events []CalendarEvent, maxRows int, - loc *time.Location, opts CalendarMonthViewOptions, r *CalendarViewResult, more *Date, apply func(CalendarChange, string)) { - k := Tokens(c) - d := Density(c) +func monthCell(c *ui.Context, day core.Date, month YearMonth, today core.Date, lay monthDay, events []CalendarEvent, maxRows int, + loc *time.Location, opts CalendarMonthViewOptions, r *CalendarViewResult, more *core.Date, apply func(CalendarChange, string)) { + k := core.Tokens(c) + d := core.Density(c) cell := ui.Column(c).Grow(1).Basis(0).MinWidth(0).MinHeight(d.ControlHeight() * 3.5).Padding(d.Unit()).Gap(d.Unit()).Clip() if id, ok := ui.Drop[monthDrag](cell); ok && !cell.IsDisabled() { for _, ev := range events { @@ -154,14 +155,14 @@ func monthCell(c *ui.Context, day Date, month YearMonth, today Date, lay monthDa } if _, over := ui.DragOver[monthDrag](cell); over { cell.Background(k.Selection) - cell.DrawOver(func(p *ui.Painter, rr ui.Rect) { datetimeSelectedMark(p, rr, k.Focus) }) + cell.DrawOver(func(p *ui.Painter, rr ui.Rect) { core.DatetimeSelectedMark(p, rr, k.Focus) }) } cell.Children(func() { fg := k.Text if day.Month != month.Month { fg = k.TextMuted } - num := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.MinHitSize / 2).Label(datetimeDayLabel(c, day)) + num := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.MinHitSize / 2).Label(core.DatetimeDayLabel(c, day)) internal.Ring(num, k.Focus, theme.MinHitSize/2) if day == today { num.Background(k.Selection).Border(theme.BorderWidth, k.AccentText) @@ -169,7 +170,7 @@ func monthCell(c *ui.Context, day Date, month YearMonth, today Date, lay monthDa num.Background(k.SurfaceHover) } num.Children(func() { - t := ui.Text(c, strconv.Itoa(day.Day)).TextColor(fg).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + t := ui.Text(c, strconv.Itoa(day.Day)).TextColor(fg).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") if day == today { t.Bold() } @@ -191,23 +192,23 @@ func monthCell(c *ui.Context, day Date, month YearMonth, today Date, lay monthDa } if hidden := lay.n - shown; hidden > 0 { b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Justify(ui.Start).PaddingX(d.Unit()).Radius(theme.ControlRadius). - Label(calendarCount(c, "calendar_month_view.more_label", hidden)) + Label(core.CalendarCount(c, "calendar_month_view.more_label", hidden)) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } b.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "calendar_month_view.more"), hidden)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) + ui.Text(c, fmt.Sprintf(core.Msg(c, "calendar_month_view.more"), hidden)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) }) if b.Clicked() && !b.IsDisabled() { *more = day } open := *more == day ui.PopoverBase(c, b, &open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) _, wh := c.Size() panel.Padding(d.Unit() * 2).Gap(d.Unit()).MinWidth(d.ControlHeight() * 6).MaxHeight(wh / 2) - ui.Text(c, datetimeDayLabel(c, day)).Font(theme.SerifFont).TextColor(k.Text) + ui.Text(c, core.DatetimeDayLabel(c, day)).Font(theme.SerifFont).TextColor(k.Text) ui.Scroll(c).Shrink(1).Children(func() { ui.Column(c).Gap(d.Unit()).Children(func() { for _, row := range lay.rows { @@ -219,7 +220,7 @@ func monthCell(c *ui.Context, day Date, month YearMonth, today Date, lay monthDa }) }) if !open && *more == day { - *more = Date{} + *more = core.Date{} } } }) @@ -243,6 +244,6 @@ func monthChip(c *ui.Context, ev CalendarEvent, loc *time.Location, opts Calenda } // monthDays counts calendar days from t's day to day. -func monthDays(t time.Time, day Date) int { - return int(day.civil().Sub(DateOf(t).civil()).Hours() / 24) +func monthDays(t time.Time, day core.Date) int { + return int(day.Civil().Sub(core.DateOf(t).Civil()).Hours() / 24) } diff --git a/calendar_month_view_test.go b/datetime/calendar_month_view_test.go similarity index 95% rename from calendar_month_view_test.go rename to datetime/calendar_month_view_test.go index e955450..c41245d 100644 --- a/calendar_month_view_test.go +++ b/datetime/calendar_month_view_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type monthProbe struct { @@ -14,7 +16,7 @@ type monthProbe struct { opts CalendarMonthViewOptions opened []string changes []CalendarChange - days []Date + days []core.Date disabled bool } @@ -24,7 +26,7 @@ func (p *monthProbe) tester() *ui.Tester { } p.opts.Location, p.opts.Today = calTokyo, calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := p.opts o.Disabled = p.disabled r := CalendarMonthView(c, &p.month, p.events, o) @@ -102,8 +104,8 @@ func TestCalendarMonthViewDSTGapReported(t *testing.T) { p := &monthProbe{month: YearMonth{2026, time.March}, events: []CalendarEvent{ev}} p.opts.Location = ny tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if ch, ok := CalendarMonthView(c, &p.month, p.events, CalendarMonthViewOptions{Location: ny, Today: Date{2026, time.March, 1}}).Changed(); ok { + core.Use(c, core.Settings{}) + if ch, ok := CalendarMonthView(c, &p.month, p.events, CalendarMonthViewOptions{Location: ny, Today: core.Date{Year: 2026, Month: time.March, Day: 1}}).Changed(); ok { p.changes = append(p.changes, ch) } }, 1000, 900) diff --git a/calendar_test.go b/datetime/calendar_test.go similarity index 66% rename from calendar_test.go rename to datetime/calendar_test.go index d1ce6ca..36d8562 100644 --- a/calendar_test.go +++ b/datetime/calendar_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,19 +6,21 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -var calendarToday = Date{2026, time.October, 5} +var calendarToday = core.Date{Year: 2026, Month: time.October, Day: 5} // datetimeTestLabel is the en-US spoken name of a day cell. -func datetimeTestLabel(d Date) string { +func datetimeTestLabel(d core.Date) string { return fmt.Sprintf("%v, %v %d, %d", d.Weekday(), d.Month, d.Day, d.Year) } -func calendarTester(value *Date, opts CalendarOptions, changes *int) *ui.Tester { +func calendarTester(value *core.Date, opts CalendarOptions, changes *int) *ui.Tester { opts.Today = calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if Calendar(c, value, opts).Changed() { *changes++ } @@ -26,7 +28,7 @@ func calendarTester(value *Date, opts CalendarOptions, changes *int) *ui.Tester } func TestCalendarClickChoosesDay(t *testing.T) { - v, n := Date{}, 0 + v, n := core.Date{}, 0 tt := calendarTester(&v, CalendarOptions{}, &n) if !tt.HasText("October 2026") { t.Fatalf("empty value should show today's month, got %v", tt.Texts()) @@ -34,7 +36,7 @@ func TestCalendarClickChoosesDay(t *testing.T) { if err := tt.Click("Monday, October 12, 2026"); err != nil { t.Fatal(err) } - if v != (Date{2026, time.October, 12}) || n != 1 { + if v != (core.Date{Year: 2026, Month: time.October, Day: 12}) || n != 1 { t.Fatalf("value %v changes %d", v, n) } if err := tt.Click("Monday, October 12, 2026"); err != nil { @@ -46,7 +48,7 @@ func TestCalendarClickChoosesDay(t *testing.T) { } func TestCalendarKeyboard(t *testing.T) { - v, n := Date{2026, time.October, 15}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 15}, 0 tt := calendarTester(&v, CalendarOptions{}, &n) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyTab) @@ -71,22 +73,22 @@ func TestCalendarKeyboard(t *testing.T) { } tt.Key(0, ui.KeyEnd) tt.Key(0, ui.KeyEnter) - if v != (Date{2025, time.November, 30}) || n != 1 { + if v != (core.Date{Year: 2025, Month: time.November, Day: 30}) || n != 1 { t.Fatalf("Enter chose %v (%d changes)", v, n) } tt.Key(0, ui.KeyHome) tt.Key(0, ui.KeySpace) - if v != (Date{2025, time.November, 1}) || n != 2 { + if v != (core.Date{Year: 2025, Month: time.November, Day: 1}) || n != 2 { t.Fatalf("Space chose %v", v) } } func TestCalendarLimits(t *testing.T) { - v, n := Date{2026, time.October, 10}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 10}, 0 opts := CalendarOptions{ - Min: Date{2026, time.October, 5}, - Max: Date{2026, time.October, 20}, - DisableDate: func(d Date) bool { return d.Weekday() == time.Sunday }, + Min: core.Date{Year: 2026, Month: time.October, Day: 5}, + Max: core.Date{Year: 2026, Month: time.October, Day: 20}, + DisableDate: func(d core.Date) bool { return d.Weekday() == time.Sunday }, } tt := calendarTester(&v, opts, &n) for _, label := range []string{"Thursday, October 1, 2026", "Sunday, October 11, 2026", "Wednesday, October 21, 2026"} { @@ -94,7 +96,7 @@ func TestCalendarLimits(t *testing.T) { t.Fatal(err) } } - if v != (Date{2026, time.October, 10}) || n != 0 { + if v != (core.Date{Year: 2026, Month: time.October, Day: 10}) || n != 0 { t.Fatalf("disabled days changed the value to %v", v) } if err := tt.Click("Saturday, October 10, 2026"); err != nil { @@ -109,12 +111,12 @@ func TestCalendarLimits(t *testing.T) { func TestCalendarWeekStart(t *testing.T) { for _, tc := range []struct { ws WeekStart - loc Locale + loc core.Locale first string - }{{WeekStartLocale, EnUS, "Sunday"}, {WeekStartLocale, ZhCN, "星期一"}, {WeekStartMonday, EnUS, "Monday"}, {WeekStartSaturday, EnUS, "Saturday"}} { - v := Date{2026, time.October, 5} + }{{WeekStartLocale, core.EnUS, "Sunday"}, {WeekStartLocale, core.ZhCN, "星期一"}, {WeekStartMonday, core.EnUS, "Monday"}, {WeekStartSaturday, core.EnUS, "Saturday"}} { + v := core.Date{Year: 2026, Month: time.October, Day: 5} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: tc.loc}) + core.Use(c, core.Settings{Locale: tc.loc}) Calendar(c, &v, CalendarOptions{WeekStart: tc.ws, Today: calendarToday}) }, 400, 400) texts := tt.Texts() @@ -132,11 +134,11 @@ func TestCalendarWeekStart(t *testing.T) { } func TestCalendarDisabled(t *testing.T) { - v, n := Date{2026, time.October, 10}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 10}, 0 tt := calendarTester(&v, CalendarOptions{Disabled: true}, &n) _ = tt.Click("Monday, October 12, 2026") _ = tt.Click("Next month") - if v != (Date{2026, time.October, 10}) || n != 0 || !tt.HasText("October 2026") { + if v != (core.Date{Year: 2026, Month: time.October, Day: 10}) || n != 0 || !tt.HasText("October 2026") { t.Fatalf("disabled calendar changed: %v", v) } } @@ -147,19 +149,19 @@ func TestCalendarRejectsInvalidValue(t *testing.T) { t.Fatal("expected panic for February 30") } }() - v := Date{2026, time.February, 30} + v := core.Date{Year: 2026, Month: time.February, Day: 30} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Calendar(c, &v, CalendarOptions{}) }, 400, 400) } func TestCalendarSnapshot(t *testing.T) { - v := Date{2026, time.October, 14} + v := core.Date{Year: 2026, Month: time.October, Day: 14} snapshot(t, "calendar", 320, 340, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { - Calendar(c, &v, CalendarOptions{Today: calendarToday, Max: Date{2026, time.October, 28}, - DisableDate: func(d Date) bool { return d.Day == 20 }}) + Calendar(c, &v, CalendarOptions{Today: calendarToday, Max: core.Date{Year: 2026, Month: time.October, Day: 28}, + DisableDate: func(d core.Date) bool { return d.Day == 20 }}) }) }) } diff --git a/calendar_time_grid.go b/datetime/calendar_time_grid.go similarity index 85% rename from calendar_time_grid.go rename to datetime/calendar_time_grid.go index 3b241bc..df5bb7e 100644 --- a/calendar_time_grid.go +++ b/datetime/calendar_time_grid.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // timeGridConfig is what the week and day views share. @@ -19,7 +20,7 @@ type timeGridConfig struct { loc *time.Location version int slot int - today Date + today core.Date now time.Time hour12 bool } @@ -59,9 +60,9 @@ func timeGridSlot(name string, slot int) int { } // timeGrid draws day columns and turns gestures into changes. -func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEvent, cfg timeGridConfig, r *CalendarViewResult) { - k := Tokens(c) - d := Density(c) +func timeGrid(c *ui.Context, root *ui.Element, days []core.Date, events []CalendarEvent, cfg timeGridConfig, r *CalendarViewResult) { + k := core.Tokens(c) + d := core.Density(c) hourH := d.ControlHeight() * 2 slotH := hourH * float32(cfg.slot) / 60 g := *ui.Local(root, "time-grid-gesture", func() *timeGesture { return &timeGesture{} }) @@ -91,8 +92,8 @@ func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEve ui.Row(c).Children(func() { ui.Box(c).Width(gutter).Shrink(0) for _, day := range days { - wd := msg(c, "calendar.weekday_"+strconv.Itoa(int(day.Weekday()))) - b := ui.ButtonBase(c).Grow(1).Basis(0).MinHeight(theme.MinHitSize).Gap(d.Unit()).Radius(theme.ControlRadius).Label(datetimeDayLabel(c, day)) + wd := core.Msg(c, "calendar.weekday_"+strconv.Itoa(int(day.Weekday()))) + b := ui.ButtonBase(c).Grow(1).Basis(0).MinHeight(theme.MinHitSize).Gap(d.Unit()).Radius(theme.ControlRadius).Label(core.DatetimeDayLabel(c, day)) internal.Ring(b, k.Focus, theme.ControlRadius) if day == cfg.today { b.Background(k.Selection) @@ -100,7 +101,7 @@ func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEve b.Background(k.SurfaceHover) } b.Children(func() { - ui.Text(c, wd).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, wd).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) t := ui.Text(c, strconv.Itoa(day.Day)).TextColor(k.Text).FontFeatures("tnum") if day == cfg.today { t.Bold().TextColor(k.AccentText) @@ -112,7 +113,7 @@ func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEve } }) ui.Row(c).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { - ui.Text(c, msg(c, "calendar_week_view.all_day")).Width(gutter).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). + ui.Text(c, core.Msg(c, "calendar_week_view.all_day")).Width(gutter).Shrink(0).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). Padding(d.Unit()) for i, day := range days { cell := ui.Column(c).Grow(1).Basis(0).MinWidth(0).MinHeight(theme.MinHitSize).Padding(d.Unit() / 2).Gap(d.Unit() / 2) @@ -135,7 +136,7 @@ func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEve ui.Column(c).Width(gutter).Shrink(0).Children(func() { for h := range 24 { ui.Text(c, timeText(c, TimeOfDay{h, 0, 0}, timeConfig{hour12: cfg.hour12})).Height(hourH). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum").PaddingX(d.Unit()) + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum").PaddingX(d.Unit()) } }) for i, day := range days { @@ -143,11 +144,11 @@ func timeGrid(c *ui.Context, root *ui.Element, days []Date, events []CalendarEve } }) }) - datetimeError(c, *problem) + core.DatetimeError(c, *problem) } // timeGridLayout places each day's events in minutes with side-by-side columns. -func timeGridLayout(events []CalendarEvent, hits []int, days []Date, loc *time.Location) []timeGridDay { +func timeGridLayout(events []CalendarEvent, hits []int, days []core.Date, loc *time.Location) []timeGridDay { out := make([]timeGridDay, len(days)) for i, day := range days { from, to := eventDayStart(day, loc), eventDayStart(day.AddDays(1), loc) @@ -183,12 +184,12 @@ func timeGridLayout(events []CalendarEvent, hits []int, days []Date, loc *time.L func timeWallMinutes(t time.Time) int { return t.Hour()*60 + t.Minute() } // timeColumn draws one day: lines, events, preview and cursor. -func timeColumn(c *ui.Context, i int, day Date, lay timeGridDay, events []CalendarEvent, cfg timeGridConfig, hourH, slotH float32, +func timeColumn(c *ui.Context, i int, day core.Date, lay timeGridDay, events []CalendarEvent, cfg timeGridConfig, hourH, slotH float32, g *timeGesture, cursor *int, r *CalendarViewResult, apply func(CalendarChange, string)) { - k := Tokens(c) + k := core.Tokens(c) slots := 24 * 60 / cfg.slot col := ui.Box(c).Grow(1).Basis(0).MinWidth(0).Height(hourH*24).Focusable(). - Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "calendar_week_view.column"), datetimeDayLabel(c, day))).BorderWidth(0, 0, 0, theme.BorderWidth).BorderColor(k.Border) + Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "calendar_week_view.column"), core.DatetimeDayLabel(c, day))).BorderWidth(0, 0, 0, theme.BorderWidth).BorderColor(k.Border) disabled := col.IsDisabled() slotOf := func(y float32) int { return max(0, min(slots-1, int(y/slotH))) } if !disabled { @@ -230,7 +231,7 @@ func timeColumn(c *ui.Context, i int, day Date, lay timeGridDay, events []Calend if focused { box := ui.Rect{X: rr.X + 2, Y: rr.Y + float32(cur)*slotH + 1, W: rr.W - 4, H: slotH - 2} p.Fill(box, k.Selection, theme.ControlRadius) - datetimeSelectedMark(p, box, k.Focus) + core.DatetimeSelectedMark(p, box, k.Focus) } if creating { box := ui.Rect{X: rr.X + 2, Y: rr.Y + float32(ga)*slotH, W: rr.W - 4, H: float32(gb-ga) * slotH} @@ -246,7 +247,7 @@ func timeColumn(c *ui.Context, i int, day Date, lay timeGridDay, events []Calend for _, pl := range lay.timed { timeChip(c, i, pl, events[pl.ev], cfg, hourH, slotH, g, r, apply) } - if day == DateOf(now.In(cfg.loc)) { + if day == core.DateOf(now.In(cfg.loc)) { CurrentTimeIndicator(c, CurrentTimeIndicatorOptions{Location: cfg.loc, Day: day, Now: cfg.now, Hour12: cfg.hour12}) } }) @@ -269,7 +270,7 @@ func timeChip(c *ui.Context, i int, pl timePlaced, ev CalendarEvent, cfg timeGri LeftPercent(100 * float32(pl.col) / float32(pl.cols)).WidthPercent(100 / float32(pl.cols)).Padding(1) box.Children(func() { chip := EventChip(c, ev, EventChipOptions{Location: cfg.loc, Hour12: cfg.hour12, Selected: mine, Inline: to-from < hourH*0.75}).Fill() - handle := ui.Box(c).Absolute().Left(0).Right(0).Bottom(0).Height(6).Cursor(ui.CursorResizeNS).Label(fmt.Sprintf(msg(c, "calendar_week_view.resize"), ev.Title)) + handle := ui.Box(c).Absolute().Left(0).Right(0).Bottom(0).Height(6).Cursor(ui.CursorResizeNS).Label(fmt.Sprintf(core.Msg(c, "calendar_week_view.resize"), ev.Title)) if chip.IsDisabled() { return } @@ -316,7 +317,7 @@ func timeChip(c *ui.Context, i int, pl timePlaced, ev CalendarEvent, cfg timeGri } // timeCreate makes an event over slots [a, b). -func timeCreate(c *ui.Context, day Date, a, b int, cfg timeGridConfig) (CalendarChange, string) { +func timeCreate(c *ui.Context, day core.Date, a, b int, cfg timeGridConfig) (CalendarChange, string) { s, sn := timeAt(day, a*cfg.slot, cfg.loc) e, en := timeAt(day, b*cfg.slot, cfg.loc) if p := eventWallProblem(c, timeDay(day, a*cfg.slot), timeWall(a*cfg.slot), cfg.loc, sn); p != "" { @@ -331,7 +332,7 @@ func timeCreate(c *ui.Context, day Date, a, b int, cfg timeGridConfig) (Calendar // timeMove shifts ev by days and minutes of wall time. func timeMove(c *ui.Context, ev CalendarEvent, days, minutes int, cfg timeGridConfig) (CalendarChange, string) { s := ev.Start.In(cfg.loc) - day := DateOf(s).AddDays(days) + day := core.DateOf(s).AddDays(days) m := timeWallMinutes(s) + minutes at, n := timeAt(day, m, cfg.loc) if p := eventWallProblem(c, timeDay(day, m), timeWall(m), cfg.loc, n); p != "" { @@ -343,23 +344,23 @@ func timeMove(c *ui.Context, ev CalendarEvent, days, minutes int, cfg timeGridCo // timeResize moves ev's end by minutes of wall time. func timeResize(c *ui.Context, ev CalendarEvent, minutes int, cfg timeGridConfig) (CalendarChange, string) { e := ev.End.In(cfg.loc) - day, m := DateOf(e), timeWallMinutes(e)+minutes + day, m := core.DateOf(e), timeWallMinutes(e)+minutes at, n := timeAt(day, m, cfg.loc) if p := eventWallProblem(c, timeDay(day, m), timeWall(m), cfg.loc, n); p != "" { return CalendarChange{}, p } if !at.After(ev.Start) { - return CalendarChange{}, msg(c, "calendar_week_view.too_short") + return CalendarChange{}, core.Msg(c, "calendar_week_view.too_short") } return CalendarChange{Kind: CalendarResize, ID: ev.ID, Start: ev.Start, End: at}, "" } // timeAt resolves minute m of day as wall time. -func timeAt(day Date, m int, loc *time.Location) (time.Time, int) { +func timeAt(day core.Date, m int, loc *time.Location) (time.Time, int) { return dateTimeResolve(timeDay(day, m), timeWall(m), loc) } -func timeDay(day Date, m int) Date { +func timeDay(day core.Date, m int) core.Date { return day.AddDays(int(math.Floor(float64(m) / (24 * 60)))) } diff --git a/calendar_week_view.go b/datetime/calendar_week_view.go similarity index 50% rename from calendar_week_view.go rename to datetime/calendar_week_view.go index bf24605..e788436 100644 --- a/calendar_week_view.go +++ b/datetime/calendar_week_view.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CalendarWeekViewOptions configure a CalendarWeekView. @@ -20,7 +21,7 @@ type CalendarWeekViewOptions struct { // SlotMinutes is the grid step; zero means 30. SlotMinutes int // Today is marked; zero uses the system clock. - Today Date + Today core.Date // Now places the current-time line; zero uses the system clock. Now time.Time Hour12 bool @@ -29,30 +30,30 @@ type CalendarWeekViewOptions struct { } // CalendarWeekView shows *week as a time grid; drags edit events. -func CalendarWeekView(c *ui.Context, week *Date, events []CalendarEvent, opts CalendarWeekViewOptions) CalendarViewResult { - datetimeCheckDate("CalendarWeekView week", *week) +func CalendarWeekView(c *ui.Context, week *core.Date, events []CalendarEvent, opts CalendarWeekViewOptions) CalendarViewResult { + core.DatetimeCheckDate("CalendarWeekView week", *week) if week.IsZero() { panic("mujica: CalendarWeekView needs a week") } cfg := timeGridConfig{name: "calendar_week_view", loc: eventLocation("CalendarWeekView", opts.Location), version: opts.Version, - slot: timeGridSlot("CalendarWeekView", opts.SlotMinutes), today: datetimeToday(c, opts.Today), now: opts.Now, hour12: opts.Hour12} + slot: timeGridSlot("CalendarWeekView", opts.SlotMinutes), today: core.DatetimeToday(c, opts.Today), now: opts.Now, hour12: opts.Hour12} eventsCheck("CalendarWeekView", events) - first := calendarFirstWeekday(c, opts.WeekStart) + first := CalendarFirstWeekday(c, opts.WeekStart) start := week.AddDays(-((int(week.Weekday()) - int(first) + 7) % 7)) - days := make([]Date, 7) + days := make([]core.Date, 7) for i := range days { days[i] = start.AddDays(i) } - title := weekViewTitle(c, start, start.AddDays(6)) + title := WeekViewTitle(c, start, start.AddDays(6)) label := opts.Label if label == "" { label = title } r := CalendarViewResult{} - root := ui.Column(c).Gap(Density(c).Unit()).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) + root := ui.Column(c).Gap(core.Density(c).Unit()).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) r.Element = root root.Children(func() { - calendarViewHeader(c, title, msg(c, "calendar_week_view.prev"), msg(c, "calendar_week_view.next"), func(dir int) { + calendarViewHeader(c, title, core.Msg(c, "calendar_week_view.prev"), core.Msg(c, "calendar_week_view.next"), func(dir int) { *week = week.AddDays(7 * dir) }) timeGrid(c, root, days, events, cfg, &r) @@ -62,19 +63,19 @@ func CalendarWeekView(c *ui.Context, week *Date, events []CalendarEvent, opts Ca // calendarViewHeader is the title row with previous and next buttons. func calendarViewHeader(c *ui.Context, title, prev, next string, step func(dir int)) { - k := Tokens(c) - ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit()).Children(func() { - if b := iconAction(c, icons.Must("chevron-left"), prev); b.Clicked() && !b.IsDisabled() { + k := core.Tokens(c) + ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit()).Children(func() { + if b := core.IconAction(c, icons.Must("chevron-left"), prev); b.Clicked() && !b.IsDisabled() { step(-1) } - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1).TextAlign(ui.Center) - if b := iconAction(c, icons.Must("chevron-right"), next); b.Clicked() && !b.IsDisabled() { + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1).TextAlign(ui.Center) + if b := core.IconAction(c, icons.Must("chevron-right"), next); b.Clicked() && !b.IsDisabled() { step(1) } }) } -// weekViewTitle names a span of days. -func weekViewTitle(c *ui.Context, a, b Date) string { - return fmt.Sprintf(msg(c, "calendar_week_view.title"), datetimeMonthName(c, a.Month), a.Day, datetimeMonthName(c, b.Month), b.Day, b.Year) +// WeekViewTitle names a span of days. +func WeekViewTitle(c *ui.Context, a, b core.Date) string { + return fmt.Sprintf(core.Msg(c, "calendar_week_view.title"), core.DatetimeMonthName(c, a.Month), a.Day, core.DatetimeMonthName(c, b.Month), b.Day, b.Year) } diff --git a/calendar_week_view_test.go b/datetime/calendar_week_view_test.go similarity index 98% rename from calendar_week_view_test.go rename to datetime/calendar_week_view_test.go index 63a5ea4..7ca7486 100644 --- a/calendar_week_view_test.go +++ b/datetime/calendar_week_view_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,10 +6,12 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type weekProbe struct { - week Date + week core.Date events []CalendarEvent opts CalendarWeekViewOptions changes []CalendarChange @@ -24,7 +26,7 @@ func (p *weekProbe) tester() *ui.Tester { p.opts.Location, p.opts.Today = calTokyo, calendarToday p.opts.Now = time.Date(2026, time.October, 14, 10, 30, 0, 0, calTokyo) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := p.opts o.Disabled = p.disabled r := CalendarWeekView(c, &p.week, p.events, o) diff --git a/countdown.go b/datetime/countdown.go similarity index 81% rename from countdown.go rename to datetime/countdown.go index 4afc585..7ac15ca 100644 --- a/countdown.go +++ b/datetime/countdown.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CountdownFormat chooses how the time left reads. @@ -42,7 +43,7 @@ func Countdown(c *ui.Context, deadline time.Time, opts CountdownOptions) Countdo if opts.Format < CountdownClock || opts.Format > CountdownWords { panic(fmt.Sprintf("mujica: Countdown Format %d out of range", opts.Format)) } - k := Tokens(c) + k := core.Tokens(c) now := opts.Now live := now.IsZero() if live { @@ -55,10 +56,10 @@ func Countdown(c *ui.Context, deadline time.Time, opts CountdownOptions) Countdo } label := opts.Label if label == "" { - label = msg(c, "countdown.label") + label = core.Msg(c, "countdown.label") } text := CountdownText(c, shown, opts.Format) - e := ui.Text(c, text).Font(theme.MonoFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text).FontFeatures("tnum"). + e := ui.Text(c, text).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.H2Size)).TextColor(k.Text).FontFeatures("tnum"). Label(label + ", " + CountdownText(c, shown, CountdownWords)) r := CountdownResult{Element: e} fired := ui.Local(e, "countdown-fired", func() time.Time { return time.Time{} }) @@ -66,7 +67,7 @@ func Countdown(c *ui.Context, deadline time.Time, opts CountdownOptions) Countdo case left == 0 && !fired.Equal(deadline): *fired, r.finished = deadline, true slog.Debug("mujica: countdown finished", "deadline", deadline) - c.Announce(fmt.Sprintf(msg(c, "countdown.done"), label)) + c.Announce(fmt.Sprintf(core.Msg(c, "countdown.done"), label)) case left > 0 && live: c.After(left % time.Second) if left%time.Second == 0 { @@ -86,7 +87,7 @@ func CountdownText(c *ui.Context, d time.Duration, f CountdownFormat) string { if f == CountdownClock { t := fmt.Sprintf("%02d:%02d:%02d", h, m, sec) if days > 0 { - t = fmt.Sprintf(msg(c, "countdown.days_clock"), days) + " " + t + t = fmt.Sprintf(core.Msg(c, "countdown.days_clock"), days) + " " + t } return t } @@ -96,7 +97,7 @@ func CountdownText(c *ui.Context, d time.Duration, f CountdownFormat) string { if out != "" { out += " " } - out += fmt.Sprintf(msg(c, []string{"countdown.d", "countdown.h", "countdown.m", "countdown.s"}[i]), v) + out += fmt.Sprintf(core.Msg(c, []string{"countdown.d", "countdown.h", "countdown.m", "countdown.s"}[i]), v) } } return out diff --git a/countdown_test.go b/datetime/countdown_test.go similarity index 92% rename from countdown_test.go rename to datetime/countdown_test.go index 470b2e0..fd3ac39 100644 --- a/countdown_test.go +++ b/datetime/countdown_test.go @@ -1,10 +1,12 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCountdownFormatsAndFinishesOnce(t *testing.T) { @@ -12,7 +14,7 @@ func TestCountdownFormatsAndFinishesOnce(t *testing.T) { now, n := deadline.Add(-(26*time.Hour + 3*time.Minute + 4*time.Second + 500*time.Millisecond)), 0 format := CountdownClock tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Countdown(c, deadline, CountdownOptions{Now: now, Format: format}).Finished() { n++ } @@ -22,7 +24,7 @@ func TestCountdownFormatsAndFinishesOnce(t *testing.T) { } var clock, words string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) clock, words = CountdownText(c, 26*time.Hour+3*time.Minute+5*time.Second, CountdownClock), CountdownText(c, 0, CountdownWords) }, 10, 10) if clock != "1d 02:03:05" || words != "0 s" { @@ -53,7 +55,7 @@ func TestCountdownInvalidFormat(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Countdown(c, time.Now(), CountdownOptions{Format: 7}) }, 10, 10) } diff --git a/cron_editor.go b/datetime/cron_editor.go similarity index 83% rename from cron_editor.go rename to datetime/cron_editor.go index cf36e19..ac2ee0c 100644 --- a/cron_editor.go +++ b/datetime/cron_editor.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CronSchedule is a parsed five-field cron expression. @@ -149,7 +151,7 @@ type CronRun struct { func (s CronSchedule) Next(t time.Time, loc *time.Location, n int) []CronRun { var out []CronRun w := t.In(loc) - day := DateOf(w) + day := core.DateOf(w) for i := 0; i < 5*366 && len(out) < n; i, day = i+1, day.AddDays(1) { if !s.dayMatches(day) { continue @@ -174,7 +176,7 @@ func (s CronSchedule) Next(t time.Time, loc *time.Location, n int) []CronRun { } // dayMatches applies cron's either-day-field rule. -func (s CronSchedule) dayMatches(d Date) bool { +func (s CronSchedule) dayMatches(d core.Date) bool { if s.fields[3]&(1< 0: - return fmt.Sprintf(msg(c, "calendar.after_max"), format(hi)) + return fmt.Sprintf(core.Msg(c, "calendar.after_max"), format(hi)) case !enabled(d): - return fmt.Sprintf(msg(c, "calendar.unavailable"), format(d)) + return fmt.Sprintf(core.Msg(c, "calendar.unavailable"), format(d)) } return "" } diff --git a/date_picker_test.go b/datetime/date_picker_test.go similarity index 67% rename from date_picker_test.go rename to datetime/date_picker_test.go index 45a97a8..f1e6bad 100644 --- a/date_picker_test.go +++ b/datetime/date_picker_test.go @@ -1,21 +1,24 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) -func datePickerTester(value *Date, opts DatePickerOptions, changes *int) *ui.Tester { +func datePickerTester(value *core.Date, opts DatePickerOptions, changes *int) *ui.Tester { opts.Today = calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Gap(8).Children(func() { if DatePicker(c, value, opts).Changed() { *changes++ } - Button(c, "Elsewhere", ButtonOptions{}) + input.Button(c, "Elsewhere", input.ButtonOptions{}) }) }, 420, 460) } @@ -27,28 +30,28 @@ func datePickerType(tt *ui.Tester, label, text string) { } func TestDatePickerTyping(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due"}, &n) datePickerType(tt, "Due", "2026-11-03") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.November, 3}) || n != 1 { + if v != (core.Date{Year: 2026, Month: time.November, Day: 3}) || n != 1 { t.Fatalf("Enter committed %v (%d)", v, n) } datePickerType(tt, "Due", "2026-12-24") if err := tt.Click("Elsewhere"); err != nil { t.Fatal(err) } - if v != (Date{2026, time.December, 24}) || n != 2 { + if v != (core.Date{Year: 2026, Month: time.December, Day: 24}) || n != 2 { t.Fatalf("blur committed %v (%d)", v, n) } } func TestDatePickerInvalidInputKept(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 - tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Max: Date{2026, time.December, 31}}, &n) + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 + tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Max: core.Date{Year: 2026, Month: time.December, Day: 31}}, &n) datePickerType(tt, "Due", "2026-02-30") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.October, 5}) || n != 0 { + if v != (core.Date{Year: 2026, Month: time.October, Day: 5}) || n != 0 { t.Fatalf("invalid day was corrected into %v", v) } if !tt.HasText("Enter a date like 2026-10-05.") { @@ -59,12 +62,12 @@ func TestDatePickerInvalidInputKept(t *testing.T) { tt.Key(0, ui.KeyBackspace) tt.Type("28") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.February, 28}) || n != 1 || tt.HasText("Enter a date like") { + if v != (core.Date{Year: 2026, Month: time.February, Day: 28}) || n != 1 || tt.HasText("Enter a date like") { t.Fatalf("edited text committed %v (%d)", v, n) } datePickerType(tt, "Due", "2027-01-02") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.February, 28}) || !tt.HasText("Choose 2026-12-31 or earlier.") { + if v != (core.Date{Year: 2026, Month: time.February, Day: 28}) || !tt.HasText("Choose 2026-12-31 or earlier.") { t.Fatalf("out of range accepted: %v %v", v, tt.Texts()) } datePickerType(tt, "Due", "") @@ -75,7 +78,7 @@ func TestDatePickerInvalidInputKept(t *testing.T) { } func TestDatePickerCalendarPopup(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due"}, &n) if err := tt.Click("Open calendar"); err != nil { t.Fatal(err) @@ -85,7 +88,7 @@ func TestDatePickerCalendarPopup(t *testing.T) { } tt.Key(0, ui.KeyRight) tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.October, 6}) || n != 1 || tt.HasText("October 2026") { + if v != (core.Date{Year: 2026, Month: time.October, Day: 6}) || n != 1 || tt.HasText("October 2026") { t.Fatalf("keyboard pick %v, popup open %v", v, tt.HasText("October 2026")) } _ = tt.Click("Due") @@ -94,24 +97,24 @@ func TestDatePickerCalendarPopup(t *testing.T) { t.Fatal("Down should open the calendar") } tt.Key(0, ui.KeyEscape) - if tt.HasText("October 2026") || v != (Date{2026, time.October, 6}) { + if tt.HasText("October 2026") || v != (core.Date{Year: 2026, Month: time.October, Day: 6}) { t.Fatal("Escape should close without change") } _ = tt.Click("Open calendar") if err := tt.Click("Thursday, October 22, 2026"); err != nil { t.Fatal(err) } - if v != (Date{2026, time.October, 22}) || n != 2 { + if v != (core.Date{Year: 2026, Month: time.October, Day: 22}) || n != 2 { t.Fatalf("click pick %v", v) } } func TestDatePickerClearAndLayout(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Clearable: true, Layout: "02/01/2006"}, &n) _ = tt.Click("Due") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.October, 5}) || n != 0 || tt.HasText("Enter a date like") { + if v != (core.Date{Year: 2026, Month: time.October, Day: 5}) || n != 0 || tt.HasText("Enter a date like") { t.Fatalf("shown text did not parse back with the layout: %v", tt.Texts()) } if err := tt.Click("Clear"); err != nil { @@ -122,13 +125,13 @@ func TestDatePickerClearAndLayout(t *testing.T) { } datePickerType(tt, "Due", "31/12/2026") tt.Key(0, ui.KeyEnter) - if v != (Date{2026, time.December, 31}) { + if v != (core.Date{Year: 2026, Month: time.December, Day: 31}) { t.Fatalf("custom layout parsed %v", v) } } func TestDatePickerDisabled(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Disabled: true}, &n) _ = tt.Click("Open calendar") if tt.HasText("October 2026") { @@ -137,7 +140,7 @@ func TestDatePickerDisabled(t *testing.T) { } func TestDatePickerSnapshot(t *testing.T) { - a, b, e := Date{2026, time.October, 14}, Date{}, Date{2026, time.October, 5} + a, b, e := core.Date{Year: 2026, Month: time.October, Day: 14}, core.Date{}, core.Date{Year: 2026, Month: time.October, Day: 5} snapshot(t, "date_picker", 640, 130, func(c *ui.Context) { ui.Row(c).Padding(16).Gap(16).AlignItems(ui.Start).Children(func() { ui.Column(c).Gap(12).Width(260).Children(func() { diff --git a/date_range_picker.go b/datetime/date_range_picker.go similarity index 63% rename from date_range_picker.go rename to datetime/date_range_picker.go index d659fb9..2593f93 100644 --- a/date_range_picker.go +++ b/datetime/date_range_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,12 +6,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DateRange is an inclusive span of days. The zero value means no range. type DateRange struct { - Start, End Date + Start, End core.Date } // IsZero reports whether r is the empty range. @@ -22,7 +24,7 @@ func (r DateRange) Days() int { if r.Start.IsZero() || r.End.IsZero() { return 0 } - return int(r.End.civil().Sub(r.Start.civil()).Hours()/24) + 1 + return int(r.End.Civil().Sub(r.Start.Civil()).Hours()/24) + 1 } // DateRangePickerOptions configure a DateRangePicker. @@ -30,12 +32,12 @@ type DateRangePickerOptions struct { // Layout is the Go time layout shown; empty uses 2006-01-02. Layout string // Min and Max bound both ends; zero leaves a side open. - Min, Max Date + Min, Max core.Date // DisableDate marks days that neither end nor the span may include. - DisableDate func(Date) bool + DisableDate func(core.Date) bool WeekStart WeekStart // Today is marked in the calendar; zero uses the system clock. - Today Date + Today core.Date Disabled bool // ReadOnly keeps the field focusable and copyable but blocks the popup. ReadOnly bool @@ -45,26 +47,26 @@ type DateRangePickerOptions struct { // DateRangePicker picks start and end; Confirm writes *value. func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOptions) DateTimeResult { - datetimeCheckDate("DateRangePicker Start", value.Start) - datetimeCheckDate("DateRangePicker End", value.End) + core.DatetimeCheckDate("DateRangePicker Start", value.Start) + core.DatetimeCheckDate("DateRangePicker End", value.End) if value.Start.IsZero() != value.End.IsZero() || !value.IsZero() && value.End.Compare(value.Start) < 0 { panic(fmt.Sprintf("mujica: DateRangePicker value %v..%v is not a range", value.Start, value.End)) } - datetimeCheckBounds("DateRangePicker", opts.Min, opts.Max) + core.DatetimeCheckBounds("DateRangePicker", opts.Min, opts.Max) layout := opts.Layout if layout == "" { layout = "2006-01-02" } - d := Density(c) - k := Tokens(c) - today := datetimeToday(c, opts.Today) + d := core.Density(c) + k := core.Tokens(c) + today := core.DatetimeToday(c, opts.Today) enabled := calendarEnabled(opts.Min, opts.Max, opts.DisableDate) col := ui.Column(c).Width(d.ControlHeight() * 9).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled) open := ui.Local(col, "date-range-open", func() bool { return false }) wasOpen := ui.Local(col, "date-range-was-open", func() bool { return false }) draft := ui.Local(col, "date-range-draft", func() DateRange { return DateRange{} }) - cursor := ui.Local(col, "date-range-cursor", func() Date { return Date{} }) - hover := ui.Local(col, "date-range-hover", func() Date { return Date{} }) + cursor := ui.Local(col, "date-range-cursor", func() core.Date { return core.Date{} }) + hover := ui.Local(col, "date-range-hover", func() core.Date { return core.Date{} }) problem := ui.Local(col, "date-range-error", func() string { return "" }) changed, canceled, done := false, false, false finish := func(to DateRange, how string) { @@ -77,7 +79,7 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption } label := opts.Label if label == "" { - label = msg(c, "date_range_picker.label") + label = core.Msg(c, "date_range_picker.label") } text := "" if !value.IsZero() { @@ -85,13 +87,13 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption } var field *ui.Element col.Children(func() { - field = datetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly) + field = core.DatetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly) field.Children(func() { - t := datetimeTrigger(c, text, msg(c, "date_range_picker.placeholder"), "calendar").Label(label).Description(text).Error(*problem) + t := core.DatetimeTrigger(c, text, core.Msg(c, "date_range_picker.placeholder"), "calendar").Label(label).Description(text).Error(*problem) if !opts.ReadOnly && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { *open = !*open if *open { - *draft, *problem, *hover = *value, "", Date{} + *draft, *problem, *hover = *value, "", core.Date{} *cursor = value.Start if cursor.IsZero() { *cursor = today @@ -100,9 +102,9 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption } } }) - datetimePopup(c, field, open, opts.ReadOnly, func() { + core.DatetimePopup(c, field, open, opts.ReadOnly, func() { ui.Text(c, dateRangeSummary(c, *draft, layout)).TextColor(k.TextMuted). - FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").Role(ui.RoleStatus) + FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").Role(ui.RoleStatus) end := draft.End if end.IsZero() && !draft.Start.IsZero() { end = *hover @@ -111,8 +113,8 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption } } picked, hovered := calendarMonth(c, cursor, calendarConfig{ - chosen: func(x Date) bool { return x == draft.Start || x == draft.End }, - inRange: func(x Date) bool { return !draft.Start.IsZero() && x.within(draft.Start, end) }, + chosen: func(x core.Date) bool { return x == draft.Start || x == draft.End }, + inRange: func(x core.Date) bool { return !draft.Start.IsZero() && x.Within(draft.Start, end) }, enabled: enabled, today: today, weekStart: opts.WeekStart, @@ -125,16 +127,16 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption if !picked.IsZero() { *problem = dateRangePick(c, draft, picked, enabled, layout) } - datetimeError(c, *problem) - datetimeFooter(c, func() { - if Button(c, msg(c, "calendar.clear"), ButtonOptions{Variant: Ghost, Disabled: value.IsZero() && draft.IsZero()}).Clicked() { + core.DatetimeError(c, *problem) + core.DatetimeFooter(c, func() { + if input.Button(c, core.Msg(c, "calendar.clear"), input.ButtonOptions{Variant: input.Ghost, Disabled: value.IsZero() && draft.IsZero()}).Clicked() { finish(DateRange{}, "clear") } - if Button(c, msg(c, "calendar.cancel"), ButtonOptions{Variant: Secondary}).Clicked() { + if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { *open = false } ready := !draft.Start.IsZero() && !draft.End.IsZero() - if Button(c, msg(c, "calendar.confirm"), ButtonOptions{Disabled: !ready}).Clicked() { + if input.Button(c, core.Msg(c, "calendar.confirm"), input.ButtonOptions{Disabled: !ready}).Clicked() { if p := dateRangeProblem(c, *draft, opts.Min, opts.Max, enabled, layout); p != "" { slog.Debug("mujica: date_range_picker invalid confirm", "draft", *draft) *problem = p @@ -157,30 +159,30 @@ func DateRangePicker(c *ui.Context, value *DateRange, opts DateRangePickerOption } // dateRangeProblem checks a whole draft against the current bounds and filter. -func dateRangeProblem(c *ui.Context, r DateRange, lo, hi Date, enabled func(Date) bool, layout string) string { - format := func(d Date) string { return d.Format(layout) } - for _, d := range []Date{r.Start, r.End} { +func dateRangeProblem(c *ui.Context, r DateRange, lo, hi core.Date, enabled func(core.Date) bool, layout string) string { + format := func(d core.Date) string { return d.Format(layout) } + for _, d := range []core.Date{r.Start, r.End} { if p := datePickerProblem(c, d, lo, hi, enabled, format); p != "" { return p } } for x := r.Start; x.Compare(r.End) <= 0; x = x.AddDays(1) { if !enabled(x) { - return fmt.Sprintf(msg(c, "date_range_picker.blocked"), format(x)) + return fmt.Sprintf(core.Msg(c, "date_range_picker.blocked"), format(x)) } } return "" } // dateRangePick applies a picked day to the draft and returns any problem. -func dateRangePick(c *ui.Context, draft *DateRange, day Date, enabled func(Date) bool, layout string) string { +func dateRangePick(c *ui.Context, draft *DateRange, day core.Date, enabled func(core.Date) bool, layout string) string { if draft.Start.IsZero() || !draft.End.IsZero() || day.Compare(draft.Start) < 0 { *draft = DateRange{Start: day} return "" } for x := draft.Start; x.Compare(day) <= 0; x = x.AddDays(1) { if !enabled(x) { - return fmt.Sprintf(msg(c, "date_range_picker.blocked"), x.Format(layout)) + return fmt.Sprintf(core.Msg(c, "date_range_picker.blocked"), x.Format(layout)) } } draft.End = day @@ -191,9 +193,9 @@ func dateRangePick(c *ui.Context, draft *DateRange, day Date, enabled func(Date) func dateRangeSummary(c *ui.Context, r DateRange, layout string) string { switch { case r.Start.IsZero(): - return msg(c, "date_range_picker.pick_start") + return core.Msg(c, "date_range_picker.pick_start") case r.End.IsZero(): - return fmt.Sprintf(msg(c, "date_range_picker.pick_end"), r.Start.Format(layout)) + return fmt.Sprintf(core.Msg(c, "date_range_picker.pick_end"), r.Start.Format(layout)) } - return fmt.Sprintf(msg(c, "date_range_picker.summary"), r.Start.Format(layout), r.End.Format(layout), r.Days()) + return fmt.Sprintf(core.Msg(c, "date_range_picker.summary"), r.Start.Format(layout), r.End.Format(layout), r.Days()) } diff --git a/date_range_picker_test.go b/datetime/date_range_picker_test.go similarity index 91% rename from date_range_picker_test.go rename to datetime/date_range_picker_test.go index 41a93d7..744432b 100644 --- a/date_range_picker_test.go +++ b/datetime/date_range_picker_test.go @@ -1,17 +1,19 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func dateRangeTester(value *DateRange, opts DateRangePickerOptions, changes, cancels *int) *ui.Tester { opts.Today = calendarToday opts.Label = "Stay" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { r := DateRangePicker(c, value, opts) if r.Changed() { @@ -24,7 +26,7 @@ func dateRangeTester(value *DateRange, opts DateRangePickerOptions, changes, can }, 420, 520) } -func oct(day int) Date { return Date{2026, time.October, day} } +func oct(day int) core.Date { return core.Date{Year: 2026, Month: time.October, Day: day} } func TestDateRangePickerPickAndConfirm(t *testing.T) { v, n, x := DateRange{}, 0, 0 @@ -78,7 +80,7 @@ func TestDateRangePickerCancelKeepsValue(t *testing.T) { func TestDateRangePickerLimits(t *testing.T) { v, n, x := DateRange{}, 0, 0 - opts := DateRangePickerOptions{Min: oct(5), DisableDate: func(d Date) bool { return d == oct(16) }} + opts := DateRangePickerOptions{Min: oct(5), DisableDate: func(d core.Date) bool { return d == oct(16) }} tt := dateRangeTester(&v, opts, &n, &x) _ = tt.Click("Stay") _ = tt.Click(datetimeTestLabel(oct(2))) @@ -121,7 +123,7 @@ func TestDateRangePickerKeyboard(t *testing.T) { } func TestDateRangeDays(t *testing.T) { - r := DateRange{Date{2026, time.March, 28}, Date{2026, time.April, 2}} + r := DateRange{core.Date{Year: 2026, Month: time.March, Day: 28}, core.Date{Year: 2026, Month: time.April, Day: 2}} if r.Days() != 6 || (DateRange{}).Days() != 0 { t.Fatalf("days %d", r.Days()) } diff --git a/date_time_picker.go b/datetime/date_time_picker.go similarity index 70% rename from date_time_picker.go rename to datetime/date_time_picker.go index ab1bc04..ab89f91 100644 --- a/date_time_picker.go +++ b/datetime/date_time_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DateTimePickerOptions configure a DateTimePicker. @@ -23,7 +25,7 @@ type DateTimePickerOptions struct { MinuteStep int WeekStart WeekStart // Today is marked in the calendar; zero uses the system clock. - Today Date + Today core.Date Disabled bool // ReadOnly keeps the field focusable and copyable but blocks the popup. ReadOnly bool @@ -41,23 +43,23 @@ func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) panic(fmt.Sprintf("mujica: DateTimePicker Min %v is after Max %v", opts.Min, opts.Max)) } cfg := timePickerConfig("DateTimePicker", TimePickerOptions{Seconds: opts.Seconds, Hour12: opts.Hour12, MinuteStep: opts.MinuteStep}) - cfg.label = msg(c, "date_time_picker.time") - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) - var lo, hi Date + cfg.label = core.Msg(c, "date_time_picker.time") + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) + var lo, hi core.Date if !opts.Min.IsZero() { - lo = DateOf(opts.Min.In(loc)) + lo = core.DateOf(opts.Min.In(loc)) } if !opts.Max.IsZero() { - hi = DateOf(opts.Max.In(loc)) + hi = core.DateOf(opts.Max.In(loc)) } col := ui.Column(c).Width(d.ControlHeight() * 9).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled) open := ui.Local(col, "date-time-open", func() bool { return false }) wasOpen := ui.Local(col, "date-time-was-open", func() bool { return false }) - day := ui.Local(col, "date-time-day", func() Date { return Date{} }) + day := ui.Local(col, "date-time-day", func() core.Date { return core.Date{} }) clock := ui.Local(col, "date-time-clock", func() TimeOfDay { return TimeOfDay{} }) - cursor := ui.Local(col, "date-time-cursor", func() Date { return Date{} }) + cursor := ui.Local(col, "date-time-cursor", func() core.Date { return core.Date{} }) problem := ui.Local(col, "date-time-error", func() string { return "" }) typedBad := ui.Local(col, "date-time-typed-bad", func() bool { return false }) if opts.Layout == "" { @@ -67,32 +69,32 @@ func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) changed, canceled, done := false, false, false label := opts.Label if label == "" { - label = msg(c, "date_time_picker.placeholder") + label = core.Msg(c, "date_time_picker.placeholder") } text := "" if !value.IsZero() { text = format(*value) } col.Children(func() { - field := datetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly) + field := core.DatetimeField(c, ui.Row(c), *problem != "", opts.ReadOnly) field.Children(func() { - t := datetimeTrigger(c, text, msg(c, "date_time_picker.placeholder"), "calendar").Label(label).Description(text).Error(*problem) + t := core.DatetimeTrigger(c, text, core.Msg(c, "date_time_picker.placeholder"), "calendar").Label(label).Description(text).Error(*problem) if !opts.ReadOnly && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { *open = !*open if *open { *day, *clock, *problem = today, TimeOfDay{}, "" if !value.IsZero() { w := value.In(loc) - *day, *clock = DateOf(w), TimeOfDay{w.Hour(), w.Minute(), w.Second()} + *day, *clock = core.DateOf(w), TimeOfDay{w.Hour(), w.Minute(), w.Second()} } *cursor = *day slog.Debug("mujica: date_time_picker open") } } }) - datetimePopup(c, field, open, opts.ReadOnly, func() { + core.DatetimePopup(c, field, open, opts.ReadOnly, func() { picked, _ := calendarMonth(c, cursor, calendarConfig{ - chosen: func(x Date) bool { return x == *day }, + chosen: func(x core.Date) bool { return x == *day }, enabled: calendarEnabled(lo, hi, nil), today: today, weekStart: opts.WeekStart, @@ -111,14 +113,14 @@ func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) case edited || *typedBad: *problem, *typedBad = "", false } - ui.Text(c, loc.String()).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, loc.String()).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) - datetimeError(c, *problem) - datetimeFooter(c, func() { - if Button(c, msg(c, "calendar.cancel"), ButtonOptions{Variant: Secondary}).Clicked() { + core.DatetimeError(c, *problem) + core.DatetimeFooter(c, func() { + if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { *open = false } - if Button(c, msg(c, "calendar.confirm"), ButtonOptions{}).Clicked() { + if input.Button(c, core.Msg(c, "calendar.confirm"), input.ButtonOptions{}).Clicked() { at, p := dateTimeConfirm(c, *day, *clock, loc, opts, cfg) if bad != "" { p = bad @@ -147,7 +149,7 @@ func DateTimePicker(c *ui.Context, value *time.Time, opts DateTimePickerOptions) } // dateTimeConfirm turns the draft into an instant, or explains why it cannot. -func dateTimeConfirm(c *ui.Context, d Date, t TimeOfDay, loc *time.Location, opts DateTimePickerOptions, cfg timeConfig) (time.Time, string) { +func dateTimeConfirm(c *ui.Context, d core.Date, t TimeOfDay, loc *time.Location, opts DateTimePickerOptions, cfg timeConfig) (time.Time, string) { if p := timePickerProblem(c, t, cfg); p != "" { return time.Time{}, p } @@ -156,20 +158,20 @@ func dateTimeConfirm(c *ui.Context, d Date, t TimeOfDay, loc *time.Location, opt switch { case n == 0: slog.Debug("mujica: date_time_picker gap", "wall", wall, "zone", loc.String()) - return at, fmt.Sprintf(msg(c, "date_time_picker.gap"), wall, loc.String()) + return at, fmt.Sprintf(core.Msg(c, "date_time_picker.gap"), wall, loc.String()) case n > 1: slog.Debug("mujica: date_time_picker ambiguous", "wall", wall, "zone", loc.String()) - return at, fmt.Sprintf(msg(c, "date_time_picker.ambiguous"), wall, loc.String()) + return at, fmt.Sprintf(core.Msg(c, "date_time_picker.ambiguous"), wall, loc.String()) case !opts.Min.IsZero() && at.Before(opts.Min): - return at, fmt.Sprintf(msg(c, "calendar.before_min"), dateTimeText(c, opts.Min.In(loc), opts.Layout, cfg)) + return at, fmt.Sprintf(core.Msg(c, "calendar.before_min"), dateTimeText(c, opts.Min.In(loc), opts.Layout, cfg)) case !opts.Max.IsZero() && at.After(opts.Max): - return at, fmt.Sprintf(msg(c, "calendar.after_max"), dateTimeText(c, opts.Max.In(loc), opts.Layout, cfg)) + return at, fmt.Sprintf(core.Msg(c, "calendar.after_max"), dateTimeText(c, opts.Max.In(loc), opts.Layout, cfg)) } return at, "" } // dateTimeResolve counts instants for a wall time: 0 gap, 2 overlap. -func dateTimeResolve(d Date, t TimeOfDay, loc *time.Location) (time.Time, int) { +func dateTimeResolve(d core.Date, t TimeOfDay, loc *time.Location) (time.Time, int) { wall := time.Date(d.Year, d.Month, d.Day, t.Hour, t.Minute, t.Second, 0, time.UTC) guess := time.Date(d.Year, d.Month, d.Day, t.Hour, t.Minute, t.Second, 0, loc) var hits []time.Time @@ -177,7 +179,7 @@ func dateTimeResolve(d Date, t TimeOfDay, loc *time.Location) (time.Time, int) { _, off := probe.Zone() u := wall.Add(-time.Duration(off) * time.Second).In(loc) y, m, dd := u.Date() - if (Date{y, m, dd}) != d || u.Hour() != t.Hour || u.Minute() != t.Minute || u.Second() != t.Second { + if (core.Date{Year: y, Month: m, Day: dd}) != d || u.Hour() != t.Hour || u.Minute() != t.Minute || u.Second() != t.Second { continue } seen := false diff --git a/date_time_picker_test.go b/datetime/date_time_picker_test.go similarity index 89% rename from date_time_picker_test.go rename to datetime/date_time_picker_test.go index 6da95c0..42ba499 100644 --- a/date_time_picker_test.go +++ b/datetime/date_time_picker_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "testing" @@ -6,6 +6,8 @@ import ( _ "time/tzdata" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func dateTimeTester(t *testing.T, value *time.Time, opts DateTimePickerOptions, changes, cancels *int) *ui.Tester { @@ -13,7 +15,7 @@ func dateTimeTester(t *testing.T, value *time.Time, opts DateTimePickerOptions, opts.Today = calendarToday opts.Label = "Meeting" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { r := DateTimePicker(c, value, opts) if r.Changed() { @@ -36,13 +38,13 @@ func newYork(t *testing.T) *time.Location { func TestDateTimeResolve(t *testing.T) { ny := newYork(t) - if _, n := dateTimeResolve(Date{2026, time.March, 8}, TimeOfDay{2, 30, 0}, ny); n != 0 { + if _, n := dateTimeResolve(core.Date{Year: 2026, Month: time.March, Day: 8}, TimeOfDay{2, 30, 0}, ny); n != 0 { t.Fatalf("spring-forward gap gave %d instants", n) } - if _, n := dateTimeResolve(Date{2026, time.November, 1}, TimeOfDay{1, 30, 0}, ny); n != 2 { + if _, n := dateTimeResolve(core.Date{Year: 2026, Month: time.November, Day: 1}, TimeOfDay{1, 30, 0}, ny); n != 2 { t.Fatalf("fall-back overlap gave %d instants", n) } - at, n := dateTimeResolve(Date{2026, time.July, 4}, TimeOfDay{12, 0, 0}, ny) + at, n := dateTimeResolve(core.Date{Year: 2026, Month: time.July, Day: 4}, TimeOfDay{12, 0, 0}, ny) if n != 1 || !at.Equal(time.Date(2026, time.July, 4, 16, 0, 0, 0, time.UTC)) { t.Fatalf("plain time %v (%d)", at, n) } @@ -83,7 +85,7 @@ func TestDateTimePickerReportsDSTGap(t *testing.T) { ny := newYork(t) v := time.Date(2026, time.March, 7, 2, 30, 0, 0, ny) n, x := 0, 0 - tt := dateTimeTester(t, &v, DateTimePickerOptions{Location: ny, Today: Date{2026, time.March, 1}}, &n, &x) + tt := dateTimeTester(t, &v, DateTimePickerOptions{Location: ny, Today: core.Date{Year: 2026, Month: time.March, Day: 1}}, &n, &x) _ = tt.Click("Meeting") _ = tt.Click("Sunday, March 8, 2026") _ = tt.Click("Confirm") diff --git a/datetime_grid.go b/datetime/datetime_grid.go similarity index 84% rename from datetime_grid.go rename to datetime/datetime_grid.go index 2229abb..47125a8 100644 --- a/datetime_grid.go +++ b/datetime/datetime_grid.go @@ -1,11 +1,12 @@ -package mujica +package datetime import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // datetimeGridConfig describes a page of month or year cells, addressed by ordinal. @@ -29,20 +30,20 @@ type datetimeGridConfig struct { // datetimeGrid draws the page of *cursor and returns the ordinal picked, if any. func datetimeGrid(c *ui.Context, cursor *int, cfg datetimeGridConfig) (picked int, ok bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) root := ui.Column(c).Gap(d.Unit() * 2).Shrink(0).Width(d.ControlHeight() * 7) keyed := ui.Local(root, "datetime-grid-keys", func() bool { return false }) root.Children(func() { start, _ := cfg.pageOf(*cursor) ui.Row(c).AlignItems(ui.Center).Gap(d.Unit()).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border). PaddingY(d.Unit()).Children(func() { - if iconAction(c, icons.Must("chevron-left"), cfg.prev).Clicked() { + if core.IconAction(c, icons.Must("chevron-left"), cfg.prev).Clicked() { *cursor, *keyed = *cursor-cfg.page, false } - ui.Text(c, cfg.title(start)).Font(theme.SerifFont).FontSize(FontSize(c, theme.BodySize+2)).Bold(). + ui.Text(c, cfg.title(start)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.BodySize+2)).Bold(). TextColor(k.Text).Grow(1).TextAlign(ui.Center).SingleLine() - if iconAction(c, icons.Must("chevron-right"), cfg.next).Clicked() { + if core.IconAction(c, icons.Must("chevron-right"), cfg.next).Clicked() { *cursor, *keyed = *cursor+cfg.page, false } }) @@ -91,8 +92,8 @@ func datetimeGrid(c *ui.Context, cursor *int, cfg datetimeGridConfig) (picked in // datetimeGridCell draws one month or year cell and reports whether it was picked. func datetimeGridCell(c *ui.Context, o, start, cursor int, cfg datetimeGridConfig, w float32, focus, keyed bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) enabled, chosen := cfg.enabled(o), cfg.chosen(o) sel := o - 1 if chosen { @@ -127,7 +128,7 @@ func datetimeGridCell(c *ui.Context, o, start, cursor int, cfg datetimeGridConfi ring := cell.FocusVisible() || cell.Focused() && keyed cell.DrawOver(func(p *ui.Painter, r ui.Rect) { if chosen { - datetimeSelectedMark(p, r, k.OnAccent) + core.DatetimeSelectedMark(p, r, k.OnAccent) } if current { p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H-6, 5), fg) @@ -172,22 +173,22 @@ type datetimeGridField struct { // datetimeGridPicker draws f and its popup; reports a pick or clear. func datetimeGridPicker(c *ui.Context, f datetimeGridField, cfg datetimeGridConfig) (col *ui.Element, picked int, ok, cleared bool) { - d := Density(c) + d := core.Density(c) col = ui.Column(c).Width(d.ControlHeight() * 6).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(f.disabled) open := ui.Local(col, "datetime-grid-open", func() bool { return false }) cursor := ui.Local(col, "datetime-grid-cursor", func() int { return 0 }) col.Children(func() { - field := datetimeField(c, ui.Row(c), false, f.readOnly) + field := core.DatetimeField(c, ui.Row(c), false, f.readOnly) field.Children(func() { - t := datetimeTrigger(c, f.text, f.placeholder, "calendar").Label(f.label).Description(f.text) + t := core.DatetimeTrigger(c, f.text, f.placeholder, "calendar").Label(f.label).Description(f.text) if !f.readOnly && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { *open, *cursor = !*open, f.home } - if f.clearable && f.hasValue && !f.readOnly && iconAction(c, icons.Must("x"), msg(c, "calendar.clear")).Clicked() { + if f.clearable && f.hasValue && !f.readOnly && core.IconAction(c, icons.Must("x"), core.Msg(c, "calendar.clear")).Clicked() { cleared = true } }) - datetimePopup(c, field, open, f.readOnly, func() { + core.DatetimePopup(c, field, open, f.readOnly, func() { cfg.autoFocus = true if picked, ok = datetimeGrid(c, cursor, cfg); ok { *open = false diff --git a/datetime/datetime_result.go b/datetime/datetime_result.go new file mode 100644 index 0000000..bcb3606 --- /dev/null +++ b/datetime/datetime_result.go @@ -0,0 +1,25 @@ +package datetime + +import ( + "github.com/egoist/mygo/ui" +) + +// DateTimeResult is a date or time component and what happened to it this frame. +type DateTimeResult struct { + Element *ui.Element + // Input is the text field inside, for labels and focus; nil without one. + Input *ui.Element + changed bool + canceled bool +} + +// Changed reports whether the component wrote a new value this frame. +func (r DateTimeResult) Changed() bool { return r.changed } + +// Canceled reports whether the user discarded a pending edit this frame. +func (r DateTimeResult) Canceled() bool { return r.canceled } + +// NewDateTimeResult reports a date or time control built from parts in another component. +func NewDateTimeResult(e, input *ui.Element, changed, canceled bool) DateTimeResult { + return DateTimeResult{e, input, changed, canceled} +} diff --git a/datetime_review2_test.go b/datetime/datetime_review2_test.go similarity index 88% rename from datetime_review2_test.go rename to datetime/datetime_review2_test.go index 3de77a7..f3f560c 100644 --- a/datetime_review2_test.go +++ b/datetime/datetime_review2_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "testing" @@ -6,14 +6,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestTimePickerDisabledIgnoresSegmentKeys(t *testing.T) { v, n := TimeOfDay{9, 30, 0}, 0 opts := TimePickerOptions{Label: "Alarm"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if TimePicker(c, &v, opts).Changed() { n++ } @@ -36,7 +38,7 @@ func TestTimeRangePickerDisabledIgnoresSegmentKeys(t *testing.T) { v, n := TimeRange{TimeOfDay{9, 0, 0}, TimeOfDay{17, 0, 0}}, 0 opts := TimeRangePickerOptions{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if TimeRangePicker(c, &v, opts).Changed() { n++ } @@ -112,7 +114,7 @@ func TestDateRangePickerConfirmChecksCurrentBounds(t *testing.T) { v, n := DateRange{}, 0 opts := DateRangePickerOptions{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) o := opts o.Today, o.Label = calendarToday, "Stay" if DateRangePicker(c, &v, o).Changed() { @@ -128,8 +130,8 @@ func TestDateRangePickerConfirmChecksCurrentBounds(t *testing.T) { if n != 0 || !v.IsZero() || !tt.HasText("Choose 2026-10-12 or earlier.") { t.Fatalf("out-of-range draft committed: %v %v", v, tt.Texts()) } - opts.Max = Date{} - opts.DisableDate = func(d Date) bool { return d == oct(11) } + opts.Max = core.Date{} + opts.DisableDate = func(d core.Date) bool { return d == oct(11) } tt.Frame() _ = tt.Click("Confirm") if n != 0 || !tt.HasText("The range cannot include 2026-10-11.") { @@ -163,15 +165,15 @@ func TestTimePickerPopupPickClearsSegmentError(t *testing.T) { } func TestFormFocusesDatePickerInput(t *testing.T) { - var st FormState - day := Date{} - var input *ui.Element + var st input.FormState + day := core.Date{} + var field *ui.Element tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - Form(c, &st, FormOptions{}, func(f *FormScope) { - FormItem(c, f, "debut", &day, FormItemOptions[Date]{Label: "Debut date", Required: true}, func() *ui.Element { + core.Use(c, core.Settings{}) + input.Form(c, &st, input.FormOptions{}, func(f *input.FormScope) { + input.FormItem(c, f, "debut", &day, input.FormItemOptions[core.Date]{Label: "Debut date", Required: true}, func() *ui.Element { r := DatePicker(c, &day, DatePickerOptions{Today: calendarToday}) - input = r.Input + field = r.Input return r.Input }) }) @@ -179,7 +181,7 @@ func TestFormFocusesDatePickerInput(t *testing.T) { if err := tt.Click("Submit"); err != nil { t.Fatal(err) } - if input == nil || !input.Focused() || !tt.Focused("Debut date") { + if field == nil || !field.Focused() || !tt.Focused("Debut date") { t.Fatal("a failed submit should focus the DatePicker's text box") } tt.Type("2026-10-12") @@ -207,7 +209,7 @@ func TestSelectedCellsHaveTheirOwnMark(t *testing.T) { } v := oct(14) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) Calendar(c, &v, CalendarOptions{Today: calendarToday}) }, 400, 400) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/datetime_review_test.go b/datetime/datetime_review_test.go similarity index 92% rename from datetime_review_test.go rename to datetime/datetime_review_test.go index a995fbf..85ec811 100644 --- a/datetime_review_test.go +++ b/datetime/datetime_review_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "image/color" @@ -7,13 +7,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // datePickerLive builds a DatePicker whose options a test changes between frames. -func datePickerLive(value *Date, opts *DatePickerOptions, changes *int) *ui.Tester { +func datePickerLive(value *core.Date, opts *DatePickerOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Gap(8).Children(func() { o := *opts o.Today, o.Label = calendarToday, "Due" @@ -25,7 +26,7 @@ func datePickerLive(value *Date, opts *DatePickerOptions, changes *int) *ui.Test } func TestDatePickerDisablingClosesPopup(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 opts := DatePickerOptions{} tt := datePickerLive(&v, &opts, &n) _ = tt.Click("Open calendar") @@ -47,7 +48,7 @@ func TestDatePickerDisablingClosesPopup(t *testing.T) { } func TestDatePickerDisabledBlocksInput(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due", Disabled: true}, &n) _ = tt.Click("Due") tt.Type("2027-01-01") @@ -81,7 +82,7 @@ func TestTimePickerKeepsInvalidHour(t *testing.T) { func TestTimeSegmentDescription(t *testing.T) { var got []string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) got = []string{timeSegmentDescription(c, timeHourPart, " 9", 1, 12), timeSegmentDescription(c, timePeriodPart, "PM", 0, 59)} }, 100, 100) if got[0] != "9, from 1 to 12" || got[1] != "PM" { @@ -121,7 +122,7 @@ func datetimeNear(c color.RGBA, w ui.Color) bool { } func TestDatePickerLayoutChangeReformats(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 opts := DatePickerOptions{} tt := datePickerLive(&v, &opts, &n) opts.Layout = "02/01/2006" @@ -158,7 +159,7 @@ func TestYearPickerHomeEndStayOnPage(t *testing.T) { } func TestDatePickerReadOnly(t *testing.T) { - v, n := Date{2026, time.October, 5}, 0 + v, n := core.Date{Year: 2026, Month: time.October, Day: 5}, 0 tt := datePickerTester(&v, DatePickerOptions{Label: "Due", ReadOnly: true, Clearable: true}, &n) _ = tt.Click("Due") if !tt.Focused("Due") { diff --git a/datetime/doc.go b/datetime/doc.go new file mode 100644 index 0000000..24de045 --- /dev/null +++ b/datetime/doc.go @@ -0,0 +1,2 @@ +// Package datetime holds MujicaUI date and time pickers and calendar views. +package datetime diff --git a/duration_picker.go b/datetime/duration_picker.go similarity index 80% rename from duration_picker.go rename to datetime/duration_picker.go index 9cbec32..d53b985 100644 --- a/duration_picker.go +++ b/datetime/duration_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // DurationPickerOptions configure a DurationPicker. @@ -20,7 +23,7 @@ type DurationPickerOptions struct { } // DurationPicker edits hours, minutes and seconds within a range. -func DurationPicker(c *ui.Context, value *time.Duration, opts DurationPickerOptions) ChoiceResult { +func DurationPicker(c *ui.Context, value *time.Duration, opts DurationPickerOptions) core.ChoiceResult { hi := opts.Max if hi == 0 { hi = 99*time.Hour + 59*time.Minute + 59*time.Second @@ -35,10 +38,10 @@ func DurationPicker(c *ui.Context, value *time.Duration, opts DurationPickerOpti case *value < 0 || *value%unit != 0: panic(fmt.Sprintf("mujica: DurationPicker value %v is negative or finer than the fields", *value)) } - d := Density(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "duration_picker.label") + label = core.Msg(c, "duration_picker.label") } col := ui.Column(c).Gap(d.Unit()).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) draft := ui.Local(col, "duration-draft", func() [3]float64 { return durationParts(*value) }) @@ -59,25 +62,25 @@ func DurationPicker(c *ui.Context, value *time.Duration, opts DurationPickerOpti } ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Children(func() { ui.Box(c).Width(d.ControlHeight() * 3.2).Children(func() { - r := NumberInput(c, &draft[i], NumberInputOptions{Min: 0, Max: f.max, Label: msg(c, f.key), Disabled: opts.Disabled}) + r := input.NumberInput(c, &draft[i], input.NumberInputOptions{Min: 0, Max: f.max, Label: core.Msg(c, f.key), Disabled: opts.Disabled}) edited = edited || r.Changed() }) - ui.Text(c, msg(c, f.key+"_unit")).TextColor(Tokens(c).TextMuted) + ui.Text(c, core.Msg(c, f.key+"_unit")).TextColor(core.Tokens(c).TextMuted) }) } }) total := time.Duration(draft[0])*time.Hour + time.Duration(draft[1])*time.Minute + time.Duration(draft[2])*time.Second problem := "" if total < opts.Min || total > hi { - problem = fmt.Sprintf(msg(c, "duration_picker.range"), DurationText(c, opts.Min), DurationText(c, hi)) + problem = fmt.Sprintf(core.Msg(c, "duration_picker.range"), DurationText(c, opts.Min), DurationText(c, hi)) } if edited && problem == "" && total != *value && !col.IsDisabled() { slog.Debug("mujica: duration_picker change", "from", *value, "to", total) *value, *seen, changed = total, total, true } - datetimeError(c, problem) + core.DatetimeError(c, problem) }) - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func durationParts(v time.Duration) [3]float64 { @@ -93,7 +96,7 @@ func DurationText(c *ui.Context, v time.Duration) string { if s != "" { s += " " } - s += fmt.Sprintf("%d %s", int(p[i]), msg(c, key)) + s += fmt.Sprintf("%d %s", int(p[i]), core.Msg(c, key)) } } return s diff --git a/duration_picker_test.go b/datetime/duration_picker_test.go similarity index 96% rename from duration_picker_test.go rename to datetime/duration_picker_test.go index 391f708..9497130 100644 --- a/duration_picker_test.go +++ b/datetime/duration_picker_test.go @@ -1,15 +1,17 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func durationTester(v *time.Duration, opts *DurationPickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if DurationPicker(c, v, *opts).Changed() { *n++ } diff --git a/event_chip.go b/datetime/event_chip.go similarity index 82% rename from event_chip.go rename to datetime/event_chip.go index cb81b1c..287a23f 100644 --- a/event_chip.go +++ b/datetime/event_chip.go @@ -1,12 +1,13 @@ -package mujica +package datetime import ( "time" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EventChipOptions configure an EventChip. @@ -23,13 +24,13 @@ type EventChipOptions struct { // EventChip is a clickable, truncating event block. func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) *ui.Element { loc := eventLocation("EventChip", opts.Location) - k := Tokens(c) - d := Density(c) - col := eventColor(k, ev.Color) + k := core.Tokens(c) + d := core.Density(c) + col := EventTone(k, ev.Color) when := eventTimeText(c, ev, loc, opts.Hour12) title := ev.Title if title == "" { - title = msg(c, "event_chip.untitled") + title = core.Msg(c, "event_chip.untitled") } b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Justify(ui.Start).AlignItems(ui.Start). Radius(theme.ControlRadius).Padding(d.Unit(), d.Unit()*2).PaddingX(d.Unit() * 2).Gap(d.Unit()). @@ -44,7 +45,7 @@ func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) *ui.Eleme case b.Hovered(): bg = col.Alpha(0.2).Over(k.Surface) } - b.Background(bg).Transition(colorFade) + b.Background(bg).Transition(core.ColorFade) if !opts.Inline { b.Column() } @@ -54,11 +55,11 @@ func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) *ui.Eleme selected := opts.Selected b.DrawOver(func(p *ui.Painter, r ui.Rect) { if selected { - datetimeSelectedMark(p, r, col) + core.DatetimeSelectedMark(p, r, col) } }) b.Children(func() { - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) if opts.Inline { ui.Text(c, eventStartText(c, ev, loc, opts.Hour12)).FontSize(small).TextColor(k.TextMuted).SingleLine().Shrink(0).FontFeatures("tnum") } @@ -73,7 +74,7 @@ func EventChip(c *ui.Context, ev CalendarEvent, opts EventChipOptions) *ui.Eleme // eventStartText is the start time alone, or "All day". func eventStartText(c *ui.Context, ev CalendarEvent, loc *time.Location, hour12 bool) string { if ev.AllDay { - return msg(c, "event_chip.all_day") + return core.Msg(c, "event_chip.all_day") } s := ev.Start.In(loc) return timeText(c, TimeOfDay{s.Hour(), s.Minute(), 0}, timeConfig{hour12: hour12}) diff --git a/event_chip_test.go b/datetime/event_chip_test.go similarity index 92% rename from event_chip_test.go rename to datetime/event_chip_test.go index ba6fbf7..e7d26c2 100644 --- a/event_chip_test.go +++ b/datetime/event_chip_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "strings" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) var calTokyo = func() *time.Location { @@ -29,7 +30,7 @@ func TestEventChipShowsZoneTimeAndClicks(t *testing.T) { ev.Start, ev.End = ev.Start.UTC(), ev.End.UTC() n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if EventChip(c, ev, EventChipOptions{Location: calTokyo}).Clicked() { n++ } @@ -50,7 +51,7 @@ func TestEventChipTruncatesAndAllDay(t *testing.T) { ev.AllDay, ev.Title = true, strings.Repeat("Long title ", 20) var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(120).Children(func() { w = EventChip(c, ev, EventChipOptions{Location: calTokyo, Inline: true}).Bounds().W }) @@ -65,7 +66,7 @@ func TestEventChipDisabledAndInvalid(t *testing.T) { ev := calEvent("c", 6, 9, 0, 30) n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if EventChip(c, ev, EventChipOptions{Location: calTokyo, Disabled: true}).Clicked() { n++ } @@ -78,7 +79,7 @@ func TestEventChipDisabledAndInvalid(t *testing.T) { } for _, f := range []func(){ func() { EventChip(nil, ev, EventChipOptions{}) }, - func() { ev.Color = 9; eventColor(theme.Light(), ev.Color) }, + func() { ev.Color = 9; EventTone(theme.Light(), ev.Color) }, } { func() { defer func() { diff --git a/event_editor.go b/datetime/event_editor.go similarity index 62% rename from event_editor.go rename to datetime/event_editor.go index 66ce4da..366a725 100644 --- a/event_editor.go +++ b/datetime/event_editor.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "log/slog" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // EventEditorOptions configure an EventEditor. @@ -42,10 +45,10 @@ type eventEditorState struct { // EventEditor edits a copy of *ev; Save checks and writes. func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) EventEditorResult { loc := eventLocation("EventEditor", opts.Location) - d := Density(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "event_editor.label") + label = core.Msg(c, "event_editor.label") } r := EventEditorResult{} root := ui.Column(c).Gap(d.Unit() * 4).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -58,38 +61,38 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even dr := &s.draft problems := eventEditorProblems(c, *dr, s.tried) root.Children(func() { - FormField(c, msg(c, "event_editor.title"), FormFieldOptions{Required: true, Error: problems["title"]}, func() *ui.Element { - return TextInput(c, &dr.Title, TextInputOptions{Label: msg(c, "event_editor.title"), Disabled: opts.Disabled, Error: problems["title"]}).Input + input.FormField(c, core.Msg(c, "event_editor.title"), input.FormFieldOptions{Required: true, Error: problems["title"]}, func() *ui.Element { + return input.TextInput(c, &dr.Title, input.TextInputOptions{Label: core.Msg(c, "event_editor.title"), Disabled: opts.Disabled, Error: problems["title"]}).Input }) - Checkbox(c, &dr.AllDay, msg(c, "event_editor.all_day"), CheckboxOptions{Disabled: opts.Disabled}) + input.Checkbox(c, &dr.AllDay, core.Msg(c, "event_editor.all_day"), input.CheckboxOptions{Disabled: opts.Disabled}) ui.Row(c).Gap(d.Unit() * 3).Wrap().AlignItems(ui.Start).Children(func() { for _, f := range []struct { key string t *time.Time }{{"event_editor.starts", &dr.Start}, {"event_editor.ends", &dr.End}} { - FormField(c, msg(c, f.key), FormFieldOptions{Error: problems[f.key]}, func() *ui.Element { + input.FormField(c, core.Msg(c, f.key), input.FormFieldOptions{Error: problems[f.key]}, func() *ui.Element { if dr.AllDay { - day := DateOf(f.t.In(loc)) - r := DatePicker(c, &day, DatePickerOptions{Label: msg(c, f.key), WeekStart: opts.WeekStart, Disabled: opts.Disabled}) + day := core.DateOf(f.t.In(loc)) + r := DatePicker(c, &day, DatePickerOptions{Label: core.Msg(c, f.key), WeekStart: opts.WeekStart, Disabled: opts.Disabled}) if r.Changed() { *f.t = eventDayStart(day, loc) } return r.Input } return DateTimePicker(c, f.t, DateTimePickerOptions{Location: loc, Hour12: opts.Hour12, MinuteStep: 5, - WeekStart: opts.WeekStart, Label: msg(c, f.key), Disabled: opts.Disabled}).Element + WeekStart: opts.WeekStart, Label: core.Msg(c, f.key), Disabled: opts.Disabled}).Element }) } }) - FormField(c, msg(c, "event_editor.where"), FormFieldOptions{}, func() *ui.Element { - return TextInput(c, &dr.Location, TextInputOptions{Label: msg(c, "event_editor.where"), Disabled: opts.Disabled}).Input + input.FormField(c, core.Msg(c, "event_editor.where"), input.FormFieldOptions{}, func() *ui.Element { + return input.TextInput(c, &dr.Location, input.TextInputOptions{Label: core.Msg(c, "event_editor.where"), Disabled: opts.Disabled}).Input }) - FormField(c, msg(c, "event_editor.guests"), FormFieldOptions{Description: msg(c, "event_editor.guests_help")}, func() *ui.Element { + input.FormField(c, core.Msg(c, "event_editor.guests"), input.FormFieldOptions{Description: core.Msg(c, "event_editor.guests_help")}, func() *ui.Element { names := make([]string, len(dr.Attendees)) for i, a := range dr.Attendees { names[i] = a.Name } - p := TagsInput(c, &names, TagsInputOptions{Label: msg(c, "event_editor.guests"), Disabled: opts.Disabled}) + p := input.TagsInput(c, &names, input.TagsInputOptions{Label: core.Msg(c, "event_editor.guests"), Disabled: opts.Disabled}) if p.Changed() { dr.Attendees = eventAttendees(dr.Attendees, names) } @@ -98,26 +101,26 @@ func EventEditor(c *ui.Context, ev *CalendarEvent, opts EventEditorOptions) Even if len(dr.Attendees) > 0 { AttendeeList(c, dr.Attendees, AttendeeListOptions{Disabled: opts.Disabled}) } - FormField(c, msg(c, "event_editor.reminders"), FormFieldOptions{}, func() *ui.Element { + input.FormField(c, core.Msg(c, "event_editor.reminders"), input.FormFieldOptions{}, func() *ui.Element { return ReminderPicker(c, &dr.Reminders, ReminderPickerOptions{Disabled: opts.Disabled}).Element }) var rec RecurrenceResult - FormField(c, msg(c, "recurrence_editor.repeat"), FormFieldOptions{}, func() *ui.Element { - rec = RecurrenceEditor(c, &dr.Repeat, RecurrenceEditorOptions{Start: DateOf(dr.Start.In(loc)), WeekStart: opts.WeekStart, Disabled: opts.Disabled}) + input.FormField(c, core.Msg(c, "recurrence_editor.repeat"), input.FormFieldOptions{}, func() *ui.Element { + rec = RecurrenceEditor(c, &dr.Repeat, RecurrenceEditorOptions{Start: core.DateOf(dr.Start.In(loc)), WeekStart: opts.WeekStart, Disabled: opts.Disabled}) return rec.Element }) - datetimeFooter(c, func() { - if Button(c, msg(c, "calendar.cancel"), ButtonOptions{Variant: Secondary}).Clicked() && !root.IsDisabled() { + core.DatetimeFooter(c, func() { + if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() && !root.IsDisabled() { slog.Debug("mujica: event_editor cancel", "id", ev.ID) *st, r.canceled = nil, true } - if Button(c, msg(c, "event_editor.save"), ButtonOptions{}).Clicked() && !root.IsDisabled() { + if input.Button(c, core.Msg(c, "event_editor.save"), input.ButtonOptions{}).Clicked() && !root.IsDisabled() { s.tried = true if len(eventEditorProblems(c, *dr, true)) == 0 && rec.Problem() == "" { slog.Debug("mujica: event_editor save", "id", ev.ID) out := eventCopy(*dr) if out.AllDay { - first, last := DateOf(out.Start.In(loc)), DateOf(out.End.Add(-time.Nanosecond).In(loc)) + first, last := core.DateOf(out.Start.In(loc)), core.DateOf(out.End.Add(-time.Nanosecond).In(loc)) out.Start, out.End = eventDayStart(first, loc), eventDayStart(last.AddDays(1), loc) } *ev, r.saved = out, true @@ -137,13 +140,13 @@ func eventEditorProblems(c *ui.Context, ev CalendarEvent, tried bool) map[string return p } if strings.TrimSpace(ev.Title) == "" { - p["title"] = msg(c, "event_editor.need_title") + p["title"] = core.Msg(c, "event_editor.need_title") } if ev.Start.IsZero() { - p["event_editor.starts"] = msg(c, "event_editor.need_start") + p["event_editor.starts"] = core.Msg(c, "event_editor.need_start") } if !ev.End.After(ev.Start) { - p["event_editor.ends"] = msg(c, "event_editor.end_order") + p["event_editor.ends"] = core.Msg(c, "event_editor.end_order") } return p } diff --git a/event_editor_test.go b/datetime/event_editor_test.go similarity index 96% rename from event_editor_test.go rename to datetime/event_editor_test.go index 0c14ffe..74d4bef 100644 --- a/event_editor_test.go +++ b/datetime/event_editor_test.go @@ -1,10 +1,12 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type editorProbe struct { @@ -15,7 +17,7 @@ type editorProbe struct { func (p *editorProbe) tester() *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(16).Children(func() { r := EventEditor(c, &p.ev, EventEditorOptions{Location: calTokyo, Disabled: p.disabled}) if r.Saved() { @@ -99,7 +101,7 @@ func TestEventEditorDisabled(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EventEditor(c, &p.ev, EventEditorOptions{}) }, 10, 10) } diff --git a/event_popover.go b/datetime/event_popover.go similarity index 70% rename from event_popover.go rename to datetime/event_popover.go index bbb243f..3613190 100644 --- a/event_popover.go +++ b/datetime/event_popover.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "log/slog" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EventPopoverOptions configure an EventPopover. @@ -38,24 +40,24 @@ func (r EventPopoverResult) Delete() bool { return r.del } // EventPopover summarizes an event beside anchor while *open. func EventPopover(c *ui.Context, anchor *ui.Element, open *bool, ev CalendarEvent, opts EventPopoverOptions) EventPopoverResult { loc := eventLocation("EventPopover", opts.Location) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r EventPopoverResult if *open && (opts.Disabled || anchor.IsDisabled()) { slog.Debug("mujica: event_popover close", "disabled", true) *open = false } r.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) ww, wh := c.Size() panel.Margin(d.Unit(), 0, 0, 0).Padding(d.Unit() * 4).Gap(d.Unit() * 2).Width(min(d.ControlHeight()*9, ww-16)).MaxHeight(wh - 16). Role(ui.RoleDialog).Label(ev.Title) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Box(c).Size(10, 10).Shrink(0).Draw(func(p *ui.Painter, rr ui.Rect) { - p.FillPath(internal.Diamond(rr.X+rr.W/2, rr.Y+rr.H/2, rr.W), eventColor(k, ev.Color)) + p.FillPath(internal.Diamond(rr.X+rr.W/2, rr.Y+rr.H/2, rr.W), EventTone(k, ev.Color)) }) - ui.Text(c, ev.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1).Shrink(1) - if iconAction(c, icons.Must("x"), msg(c, "event_popover.close")).Clicked() { + ui.Text(c, ev.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).Grow(1).Shrink(1) + if core.IconAction(c, icons.Must("x"), core.Msg(c, "event_popover.close")).Clicked() { *open = false } }) @@ -72,12 +74,12 @@ func EventPopover(c *ui.Context, anchor *ui.Element, open *bool, ev CalendarEven if opts.ReadOnly { return } - datetimeFooter(c, func() { - if Button(c, msg(c, "event_popover.delete"), ButtonOptions{Variant: Danger}).Clicked() { + core.DatetimeFooter(c, func() { + if input.Button(c, core.Msg(c, "event_popover.delete"), input.ButtonOptions{Variant: input.Danger}).Clicked() { slog.Debug("mujica: event_popover delete", "id", ev.ID) r.del, *open = true, false } - if Button(c, msg(c, "event_popover.edit"), ButtonOptions{Variant: Secondary}).Clicked() { + if input.Button(c, core.Msg(c, "event_popover.edit"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { slog.Debug("mujica: event_popover edit", "id", ev.ID) r.edit, *open = true, false } @@ -89,7 +91,7 @@ func EventPopover(c *ui.Context, anchor *ui.Element, open *bool, ev CalendarEven // eventPopoverLines lists icon and text pairs for the summary. func eventPopoverLines(c *ui.Context, ev CalendarEvent, loc *time.Location, hour12 bool) [][2]string { s := ev.Start.In(loc) - lines := [][2]string{{"clock", datetimeDayLabel(c, DateOf(s)) + " · " + eventTimeText(c, ev, loc, hour12)}} + lines := [][2]string{{"clock", core.DatetimeDayLabel(c, core.DateOf(s)) + " · " + eventTimeText(c, ev, loc, hour12)}} if ev.Location != "" { lines = append(lines, [2]string{"house", ev.Location}) } @@ -101,14 +103,14 @@ func eventPopoverLines(c *ui.Context, ev CalendarEvent, loc *time.Location, hour for _, a := range ev.Attendees { names = append(names, a.Name) } - lines = append(lines, [2]string{"user", calendarCount(c, "event_popover.guests", len(names)) + ": " + strings.Join(names, ", ")}) + lines = append(lines, [2]string{"user", core.CalendarCount(c, "event_popover.guests", len(names)) + ": " + strings.Join(names, ", ")}) } if len(ev.Reminders) > 0 { var rs []string for _, d := range ev.Reminders { rs = append(rs, ReminderText(c, d)) } - lines = append(lines, [2]string{"bell", strings.Join(rs, msg(c, "recurrence_editor.list_sep"))}) + lines = append(lines, [2]string{"bell", strings.Join(rs, core.Msg(c, "recurrence_editor.list_sep"))}) } return lines } diff --git a/event_popover_test.go b/datetime/event_popover_test.go similarity index 89% rename from event_popover_test.go rename to datetime/event_popover_test.go index cbd1ec2..eb25e70 100644 --- a/event_popover_test.go +++ b/datetime/event_popover_test.go @@ -1,10 +1,13 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func calRichEvent() CalendarEvent { @@ -25,9 +28,9 @@ type popoverProbe struct { func (p *popoverProbe) tester() *ui.Tester { p.opts.Location = calTokyo return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { - b := Button(c, "Details", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Details", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { p.open = true } @@ -87,7 +90,7 @@ func TestEventPopoverReadOnlyAndDisabled(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) open := true EventPopover(c, ui.Box(c), &open, calRichEvent(), EventPopoverOptions{}) }, 10, 10) @@ -96,7 +99,7 @@ func TestEventPopoverReadOnlyAndDisabled(t *testing.T) { func TestEventPopoverSnapshot(t *testing.T) { snapshot(t, "event_popover", 460, 400, func(c *ui.Context) { ui.Row(c).Padding(16).Children(func() { - b := Button(c, "Details", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Details", input.ButtonOptions{Variant: input.Secondary}) // Open once the anchor is laid out. frames := ui.Local(c.Root(), "frames", func() int { return 0 }) *frames++ diff --git a/datetime/example_calendar_test.go b/datetime/example_calendar_test.go new file mode 100644 index 0000000..133cfd7 --- /dev/null +++ b/datetime/example_calendar_test.go @@ -0,0 +1,255 @@ +package datetime_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" +) + +func ExampleCalendarMonthView() { + loc := time.Local + events := exampleEvents(loc) + month := datetime.YearMonth{Year: 2026, Month: time.October} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := datetime.CalendarMonthView(c, &month, events, datetime.CalendarMonthViewOptions{Location: loc}) + if ch, ok := r.Changed(); ok { + fmt.Println("move", ch.ID, "to", ch.Start) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCalendarWeekView() { + loc := time.Local + events := exampleEvents(loc) + week := core.Date{Year: 2026, Month: time.October, Day: 14} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := datetime.CalendarWeekView(c, &week, events, datetime.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) + if ch, ok := r.Changed(); ok && ch.Kind == datetime.CalendarCreate { + fmt.Println("new event", ch.Start, ch.End) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCalendarDayView() { + loc := time.Local + events := exampleEvents(loc) + day := core.Date{Year: 2026, Month: time.October, Day: 14} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := datetime.CalendarDayView(c, &day, events, datetime.CalendarDayViewOptions{Location: loc}).Opened(); ok { + fmt.Println("open", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAgendaView() { + loc := time.Local + events := exampleEvents(loc) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.AgendaViewOptions{Location: loc, From: core.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} + if id, ok := datetime.AgendaView(c, events, opts).Opened(); ok { + fmt.Println("open", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleYearView() { + loc := time.Local + events := exampleEvents(loc) + year := 2026 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if day, ok := datetime.YearView(c, &year, events, datetime.YearViewOptions{Location: loc}).DayChosen(); ok { + fmt.Println("go to", day) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEventChip() { + loc := time.Local + ev := exampleEvents(loc)[0] + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}).Clicked() { + fmt.Println("open", ev.ID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEventEditor() { + loc := time.Local + ev := exampleEvents(loc)[0] + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := datetime.EventEditor(c, &ev, datetime.EventEditorOptions{Location: loc}) + if r.Saved() { + fmt.Println("saved", ev.Title, ev.Start) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEventPopover() { + loc := time.Local + ev := exampleEvents(loc)[0] + open := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + chip := datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}) + if chip.Clicked() { + open = true + } + r := datetime.EventPopover(c, chip, &open, ev, datetime.EventPopoverOptions{Location: loc}) + if r.Delete() { + fmt.Println("delete", ev.ID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCurrentTimeIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Box(c).Size(200, 240).Children(func() { + datetime.CurrentTimeIndicator(c, datetime.CurrentTimeIndicatorOptions{Location: time.Local}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRecurrenceEditor() { + rule := datetime.Recurrence{Unit: datetime.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := datetime.RecurrenceEditor(c, &rule, datetime.RecurrenceEditorOptions{Start: core.Date{Year: 2026, Month: time.October, Day: 5}}) + if r.Changed() && r.Problem() == "" { + fmt.Println(datetime.RecurrenceSummary(c, rule)) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAvailabilityPicker() { + days := []core.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} + mine := [][]bool{make([]bool, 18), make([]bool, 18)} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.AvailabilityPicker(c, &mine, datetime.AvailabilityPickerOptions{Days: days}).Changed() { + fmt.Println("availability changed") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTimezoneSelect() { + zone := "Asia/Shanghai" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.TimezoneSelect(c, &zone, datetime.TimezoneSelectOptions{}).Changed() { + fmt.Println("zone", zone) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleReminderPicker() { + reminders := []time.Duration{15 * time.Minute} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.ReminderPicker(c, &reminders, datetime.ReminderPickerOptions{Max: 3}).Changed() { + fmt.Println("reminders", reminders) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAttendeeList() { + people := []datetime.Attendee{{Name: "Sakiko", Status: datetime.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if i, ok := datetime.AttendeeList(c, people, datetime.AttendeeListOptions{Removable: true}).Removed(); ok { + people = append(people[:i], people[i+1:]...) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWeekPicker() { + var week core.Date + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.WeekPicker(c, &week, datetime.WeekPickerOptions{WeekStart: datetime.WeekStartMonday, Clearable: true}).Changed() { + fmt.Println("week of", week) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDurationPicker() { + length := 90 * time.Minute + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} + if datetime.DurationPicker(c, &length, opts).Changed() { + fmt.Println("length", length) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCronEditor() { + expr := "0 9 * * MON-FRI" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.CronEditor(c, &expr, datetime.CronEditorOptions{Location: time.Local}).Changed() { + if _, err := datetime.ParseCron(expr); err == nil { + fmt.Println("schedule", expr) + } + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRelativeTime() { + posted := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.Local) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + datetime.RelativeTime(c, posted, datetime.RelativeTimeOptions{Location: time.Local}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCountdown() { + deadline := time.Date(2026, time.December, 31, 23, 59, 59, 0, time.Local) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.Countdown(c, deadline, datetime.CountdownOptions{}).Finished() { + fmt.Println("happy new year") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStopwatch() { + var sw datetime.StopwatchState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.Stopwatch(c, &sw, datetime.StopwatchOptions{}).Changed() { + fmt.Println("laps", len(sw.Laps)) + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/datetime/example_datetime_test.go b/datetime/example_datetime_test.go new file mode 100644 index 0000000..d9f3430 --- /dev/null +++ b/datetime/example_datetime_test.go @@ -0,0 +1,111 @@ +package datetime_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" +) + +func ExampleCalendar() { + day := core.Date{Year: 2026, Month: time.October, Day: 5} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if datetime.Calendar(c, &day, datetime.CalendarOptions{WeekStart: datetime.WeekStartMonday}).Changed() { + fmt.Println("chosen", day) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDatePicker() { + due := core.Date{Year: 2026, Month: time.October, Day: 5} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.DatePickerOptions{Label: "Due date", Clearable: true, + Min: core.Date{Year: 2026, Month: time.January, Day: 1}} + if datetime.DatePicker(c, &due, opts).Changed() { + fmt.Println("due", due) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDateRangePicker() { + var stay datetime.DateRange + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := datetime.DateRangePicker(c, &stay, datetime.DateRangePickerOptions{Label: "Stay"}) + if r.Changed() { + fmt.Println("stay", stay.Start, stay.End, stay.Days()) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTimePicker() { + alarm := datetime.TimeOfDay{Hour: 7, Minute: 30} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} + if datetime.TimePicker(c, &alarm, opts).Changed() { + fmt.Println("alarm", alarm) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTimeRangePicker() { + shift := datetime.TimeRange{Start: datetime.TimeOfDay{Hour: 22}, End: datetime.TimeOfDay{Hour: 6}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} + if datetime.TimeRangePicker(c, &shift, opts).Changed() { + fmt.Println("shift", shift.Start, shift.End, shift.Overnight()) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDateTimePicker() { + tokyo, err := time.LoadLocation("Asia/Tokyo") + if err != nil { + panic(err) + } + var meeting time.Time + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} + if datetime.DateTimePicker(c, &meeting, opts).Changed() { + fmt.Println("meeting", meeting) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMonthPicker() { + var month datetime.YearMonth + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.MonthPickerOptions{Label: "Billing month", Clearable: true} + if datetime.MonthPicker(c, &month, opts).Changed() { + fmt.Println("month", month) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleYearPicker() { + year := 2026 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + opts := datetime.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} + if datetime.YearPicker(c, &year, opts).Changed() { + fmt.Println("year", year) + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/datetime/example_helpers_test.go b/datetime/example_helpers_test.go new file mode 100644 index 0000000..17b28e6 --- /dev/null +++ b/datetime/example_helpers_test.go @@ -0,0 +1,15 @@ +package datetime_test + +import ( + "time" + + "github.com/ZacharyZhang-NY/MujicaUI/datetime" +) + +func exampleEvents(loc *time.Location) []datetime.CalendarEvent { + start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) + return []datetime.CalendarEvent{ + {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, + } +} diff --git a/datetime/helpers_test.go b/datetime/helpers_test.go new file mode 100644 index 0000000..f42330c --- /dev/null +++ b/datetime/helpers_test.go @@ -0,0 +1,24 @@ +package datetime + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/month_picker.go b/datetime/month_picker.go similarity index 82% rename from month_picker.go rename to datetime/month_picker.go index 766686d..df3a05c 100644 --- a/month_picker.go +++ b/datetime/month_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // YearMonth is a month of a year. The zero value means no month. @@ -42,7 +44,7 @@ type MonthPickerOptions struct { DisableMonth func(YearMonth) bool Clearable bool // Today marks the current month; zero uses the system clock. - Today Date + Today core.Date Disabled bool // ReadOnly keeps the field focusable and copyable but blocks the popup and clearing. ReadOnly bool @@ -60,9 +62,9 @@ func MonthPicker(c *ui.Context, value *YearMonth, opts MonthPickerOptions) DateT if !opts.Min.IsZero() && !opts.Max.IsZero() && opts.Min.ordinal() > opts.Max.ordinal() { panic(fmt.Sprintf("mujica: MonthPicker Min %v is after Max %v", opts.Min, opts.Max)) } - today := datetimeToday(c, opts.Today) + today := core.DatetimeToday(c, opts.Today) now := YearMonth{today.Year, today.Month} - title := func(m YearMonth) string { return datetimeMonthTitle(c, m.Year, m.Month) } + title := func(m YearMonth) string { return core.DatetimeMonthTitle(c, m.Year, m.Month) } home := *value if home.IsZero() { home = now @@ -73,19 +75,19 @@ func MonthPicker(c *ui.Context, value *YearMonth, opts MonthPickerOptions) DateT } label := opts.Label if label == "" { - label = msg(c, "month_picker.placeholder") + label = core.Msg(c, "month_picker.placeholder") } col, picked, ok, cleared := datetimeGridPicker(c, datetimeGridField{ - text: text, placeholder: msg(c, "month_picker.placeholder"), label: label, + text: text, placeholder: core.Msg(c, "month_picker.placeholder"), label: label, clearable: opts.Clearable, hasValue: !value.IsZero(), disabled: opts.Disabled, readOnly: opts.ReadOnly, home: home.ordinal(), }, datetimeGridConfig{ pageOf: func(o int) (int, int) { return yearMonthOf(o).Year * 12, 12 }, cols: 3, page: 12, - title: func(start int) string { return fmt.Sprintf(msg(c, "month_picker.year"), yearMonthOf(start).Year) }, - prev: msg(c, "calendar.prev_year"), - next: msg(c, "calendar.next_year"), - text: func(o int) string { return datetimeMonthName(c, yearMonthOf(o).Month) }, + title: func(start int) string { return fmt.Sprintf(core.Msg(c, "month_picker.year"), yearMonthOf(start).Year) }, + prev: core.Msg(c, "calendar.prev_year"), + next: core.Msg(c, "calendar.next_year"), + text: func(o int) string { return core.DatetimeMonthName(c, yearMonthOf(o).Month) }, label: func(o int) string { return title(yearMonthOf(o)) }, chosen: func(o int) bool { return !value.IsZero() && o == value.ordinal() }, current: func(o int) bool { return o == now.ordinal() }, diff --git a/month_picker_test.go b/datetime/month_picker_test.go similarity index 96% rename from month_picker_test.go rename to datetime/month_picker_test.go index 0d2f4bd..e414137 100644 --- a/month_picker_test.go +++ b/datetime/month_picker_test.go @@ -1,17 +1,19 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func monthPickerTester(value *YearMonth, opts MonthPickerOptions, changes *int) *ui.Tester { opts.Today = calendarToday opts.Label = "Billing month" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { if MonthPicker(c, value, opts).Changed() { *changes++ diff --git a/recurrence_editor.go b/datetime/recurrence_editor.go similarity index 59% rename from recurrence_editor.go rename to datetime/recurrence_editor.go index a7bd8b7..ccf19f7 100644 --- a/recurrence_editor.go +++ b/datetime/recurrence_editor.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RepeatUnit is how often a rule repeats; zero never does. @@ -39,13 +41,13 @@ type Recurrence struct { Weekdays []time.Weekday End RepeatEnd Count int - Until Date + Until core.Date } // RecurrenceEditorOptions configure a RecurrenceEditor. type RecurrenceEditorOptions struct { // Start is the first occurrence; Until must not precede it. - Start Date + Start core.Date WeekStart WeekStart Disabled bool Label string @@ -69,13 +71,13 @@ const recurrenceCustom = 5 // RecurrenceEditor edits a repeat rule and when it ends. func RecurrenceEditor(c *ui.Context, rule *Recurrence, opts RecurrenceEditorOptions) RecurrenceResult { recurrenceCheck(*rule) - d := Density(c) - k := Tokens(c) + d := core.Density(c) + k := core.Tokens(c) before := recurrenceKey(*rule) r := RecurrenceResult{} label := opts.Label if label == "" { - label = msg(c, "recurrence_editor.label") + label = core.Msg(c, "recurrence_editor.label") } r.Element = ui.Column(c).Gap(d.Unit() * 3).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) custom := ui.Local(r.Element, "recurrence-custom", func() bool { return rule.Interval > 1 }) @@ -84,11 +86,11 @@ func RecurrenceEditor(c *ui.Context, rule *Recurrence, opts RecurrenceEditorOpti if *custom && rule.Unit != RepeatNone { mode = recurrenceCustom } - modes := []SelectOption[int]{} + modes := []input.SelectOption[int]{} for i, key := range []string{"none", "daily", "weekly", "monthly", "yearly", "custom"} { - modes = append(modes, SelectOption[int]{Value: i, Label: msg(c, "recurrence_editor."+key)}) + modes = append(modes, input.SelectOption[int]{Value: i, Label: core.Msg(c, "recurrence_editor."+key)}) } - if Select(c, &mode, modes, SelectOptions{Label: msg(c, "recurrence_editor.repeat"), Disabled: opts.Disabled}).Changed() { + if input.Select(c, &mode, modes, input.SelectOptions{Label: core.Msg(c, "recurrence_editor.repeat"), Disabled: opts.Disabled}).Changed() { *custom = mode == recurrenceCustom switch { case *custom: @@ -109,34 +111,34 @@ func RecurrenceEditor(c *ui.Context, rule *Recurrence, opts RecurrenceEditorOpti } if *custom { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "recurrence_editor.every")) + ui.Text(c, core.Msg(c, "recurrence_editor.every")) ui.Box(c).Width(d.ControlHeight() * 4).Children(func() { n := float64(rule.Interval) - if NumberInput(c, &n, NumberInputOptions{Min: 1, Max: 999, Label: msg(c, "recurrence_editor.interval"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) { + if input.NumberInput(c, &n, input.NumberInputOptions{Min: 1, Max: 999, Label: core.Msg(c, "recurrence_editor.interval"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) { rule.Interval = int(n) } }) - units := []SelectOption[RepeatUnit]{} + units := []input.SelectOption[RepeatUnit]{} for u := RepeatDaily; u <= RepeatYearly; u++ { - units = append(units, SelectOption[RepeatUnit]{Value: u, Label: msg(c, recurrenceUnitKeys[u])}) + units = append(units, input.SelectOption[RepeatUnit]{Value: u, Label: core.Msg(c, recurrenceUnitKeys[u])}) } - Select(c, &rule.Unit, units, SelectOptions{Label: msg(c, "recurrence_editor.unit"), Disabled: opts.Disabled}) + input.Select(c, &rule.Unit, units, input.SelectOptions{Label: core.Msg(c, "recurrence_editor.unit"), Disabled: opts.Disabled}) }) } if rule.Unit == RepeatWeekly { - first := calendarFirstWeekday(c, opts.WeekStart) - items := []ToggleItem[time.Weekday]{} + first := CalendarFirstWeekday(c, opts.WeekStart) + items := []input.ToggleItem[time.Weekday]{} for i := range 7 { wd := time.Weekday((int(first) + i) % 7) - items = append(items, ToggleItem[time.Weekday]{Value: wd, Label: msg(c, fmt.Sprintf("calendar.weekday_%d", wd)), - Name: msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd))}) + items = append(items, input.ToggleItem[time.Weekday]{Value: wd, Label: core.Msg(c, fmt.Sprintf("calendar.weekday_%d", wd)), + Name: core.Msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd))}) } - ToggleGroup(c, &rule.Weekdays, items, ToggleGroupOptions{Multiple: true, AllowEmpty: true, Label: msg(c, "recurrence_editor.on_days"), Disabled: opts.Disabled}) + input.ToggleGroup(c, &rule.Weekdays, items, input.ToggleGroupOptions{Multiple: true, AllowEmpty: true, Label: core.Msg(c, "recurrence_editor.on_days"), Disabled: opts.Disabled}) } - ends := []RadioItem[RepeatEnd]{{Value: RepeatForever, Label: msg(c, "recurrence_editor.forever")}, - {Value: RepeatCount, Label: msg(c, "recurrence_editor.after")}, {Value: RepeatUntil, Label: msg(c, "recurrence_editor.until")}} + ends := []input.RadioItem[RepeatEnd]{{Value: RepeatForever, Label: core.Msg(c, "recurrence_editor.forever")}, + {Value: RepeatCount, Label: core.Msg(c, "recurrence_editor.after")}, {Value: RepeatUntil, Label: core.Msg(c, "recurrence_editor.until")}} ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Wrap().Children(func() { - if RadioGroup(c, &rule.End, ends, RadioGroupOptions{Horizontal: true, Label: msg(c, "recurrence_editor.ends"), Disabled: opts.Disabled}).Changed() { + if input.RadioGroup(c, &rule.End, ends, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "recurrence_editor.ends"), Disabled: opts.Disabled}).Changed() { if rule.End == RepeatCount && rule.Count == 0 { rule.Count = 10 } @@ -148,17 +150,17 @@ func RecurrenceEditor(c *ui.Context, rule *Recurrence, opts RecurrenceEditorOpti case RepeatCount: ui.Box(c).Width(d.ControlHeight() * 4).Children(func() { n := float64(rule.Count) - if NumberInput(c, &n, NumberInputOptions{Min: 1, Max: 999, Label: msg(c, "recurrence_editor.count"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) { + if input.NumberInput(c, &n, input.NumberInputOptions{Min: 1, Max: 999, Label: core.Msg(c, "recurrence_editor.count"), Disabled: opts.Disabled}).Changed() && !math.IsNaN(n) { rule.Count = int(n) } }) case RepeatUntil: - DatePicker(c, &rule.Until, DatePickerOptions{Label: msg(c, "recurrence_editor.until_date"), Min: opts.Start, Disabled: opts.Disabled}) + DatePicker(c, &rule.Until, DatePickerOptions{Label: core.Msg(c, "recurrence_editor.until_date"), Min: opts.Start, Disabled: opts.Disabled}) } }) r.problem = recurrenceProblem(c, *rule, opts.Start) - ui.Text(c, RecurrenceSummary(c, *rule)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) - datetimeError(c, r.problem) + ui.Text(c, RecurrenceSummary(c, *rule)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) + core.DatetimeError(c, r.problem) }) if after := recurrenceKey(*rule); after != before && !r.Element.IsDisabled() { slog.Debug("mujica: recurrence_editor change", "rule", after) @@ -179,20 +181,20 @@ func recurrenceCheck(r Recurrence) { if r.Unit != RepeatNone && r.Interval == 0 { panic("mujica: Recurrence repeats with Interval 0") } - datetimeCheckDate("Recurrence Until", r.Until) + core.DatetimeCheckDate("Recurrence Until", r.Until) } // recurrenceProblem explains why a rule cannot be saved. -func recurrenceProblem(c *ui.Context, r Recurrence, start Date) string { +func recurrenceProblem(c *ui.Context, r Recurrence, start core.Date) string { switch { case r.Unit == RepeatNone: return "" case r.Unit == RepeatWeekly && len(r.Weekdays) == 0: - return msg(c, "recurrence_editor.no_days") + return core.Msg(c, "recurrence_editor.no_days") case r.End == RepeatCount && r.Count < 1: - return msg(c, "recurrence_editor.bad_count") + return core.Msg(c, "recurrence_editor.bad_count") case r.End == RepeatUntil && (r.Until.IsZero() || !start.IsZero() && r.Until.Compare(start) < 0): - return msg(c, "recurrence_editor.bad_until") + return core.Msg(c, "recurrence_editor.bad_until") } return "" } @@ -200,24 +202,24 @@ func recurrenceProblem(c *ui.Context, r Recurrence, start Date) string { // RecurrenceSummary describes a rule in the window's language. func RecurrenceSummary(c *ui.Context, r Recurrence) string { if r.Unit == RepeatNone { - return msg(c, "recurrence_editor.never") + return core.Msg(c, "recurrence_editor.never") } - s := fmt.Sprintf(msg(c, "recurrence_editor.every_n"), r.Interval, msg(c, recurrenceUnitKeys[r.Unit])) + s := fmt.Sprintf(core.Msg(c, "recurrence_editor.every_n"), r.Interval, core.Msg(c, recurrenceUnitKeys[r.Unit])) if r.Interval == 1 { - s = msg(c, []string{"", "recurrence_editor.daily", "recurrence_editor.weekly", "recurrence_editor.monthly", "recurrence_editor.yearly"}[r.Unit]) + s = core.Msg(c, []string{"", "recurrence_editor.daily", "recurrence_editor.weekly", "recurrence_editor.monthly", "recurrence_editor.yearly"}[r.Unit]) } if r.Unit == RepeatWeekly && len(r.Weekdays) > 0 { var days []string for _, wd := range r.Weekdays { - days = append(days, msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd))) + days = append(days, core.Msg(c, fmt.Sprintf("calendar.weekday_long_%d", wd))) } - s += fmt.Sprintf(msg(c, "recurrence_editor.on"), strings.Join(days, msg(c, "recurrence_editor.list_sep"))) + s += fmt.Sprintf(core.Msg(c, "recurrence_editor.on"), strings.Join(days, core.Msg(c, "recurrence_editor.list_sep"))) } switch r.End { case RepeatCount: - s += calendarCount(c, "recurrence_editor.times", r.Count) + s += core.CalendarCount(c, "recurrence_editor.times", r.Count) case RepeatUntil: - s += fmt.Sprintf(msg(c, "recurrence_editor.until_text"), r.Until) + s += fmt.Sprintf(core.Msg(c, "recurrence_editor.until_text"), r.Until) } return s } diff --git a/recurrence_editor_test.go b/datetime/recurrence_editor_test.go similarity index 93% rename from recurrence_editor_test.go rename to datetime/recurrence_editor_test.go index 1f93803..ff22f46 100644 --- a/recurrence_editor_test.go +++ b/datetime/recurrence_editor_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "slices" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type recurrenceProbe struct { @@ -20,7 +22,7 @@ type recurrenceProbe struct { func (p *recurrenceProbe) tester() *ui.Tester { p.opts.Start = oct(5) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := p.opts o.Disabled = p.disabled r := RecurrenceEditor(c, &p.rule, o) @@ -82,7 +84,7 @@ func TestRecurrenceEditorCustomAndEnds(t *testing.T) { t.Fatalf("custom count %+v %q", p.rule, p.summary) } _ = tt.Click("On date") - if p.rule.End != RepeatUntil || p.rule.Until != (Date{2026, time.November, 5}) || p.problem != "" { + if p.rule.End != RepeatUntil || p.rule.Until != (core.Date{Year: 2026, Month: time.November, Day: 5}) || p.problem != "" { t.Fatalf("until %+v %q", p.rule, p.problem) } } @@ -94,7 +96,7 @@ func TestRecurrenceEditorProblems(t *testing.T) { if len(p.rule.Weekdays) != 0 || p.problem != "Choose at least one day." || !tt.HasText("Choose at least one day.") { t.Fatalf("no-day problem %+v %q", p.rule, p.problem) } - p.rule = Recurrence{Unit: RepeatDaily, Interval: 1, End: RepeatUntil, Until: Date{2026, time.September, 1}} + p.rule = Recurrence{Unit: RepeatDaily, Interval: 1, End: RepeatUntil, Until: core.Date{Year: 2026, Month: time.September, Day: 1}} tt.Frame() if p.problem != "The end date must not precede the start." { t.Fatalf("until problem %q", p.problem) diff --git a/relative_time.go b/datetime/relative_time.go similarity index 79% rename from relative_time.go rename to datetime/relative_time.go index eb91cfa..037bb0e 100644 --- a/relative_time.go +++ b/datetime/relative_time.go @@ -1,10 +1,12 @@ -package mujica +package datetime import ( "fmt" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // RelativeTimeOptions configure a RelativeTime. @@ -26,7 +28,7 @@ func RelativeTime(c *ui.Context, t time.Time, opts RelativeTimeOptions) *ui.Elem } text := RelativeTimeText(c, now.Sub(t)) w := t.In(loc) - exact := DateOf(w).String() + " " + timeText(c, TimeOfDay{w.Hour(), w.Minute(), 0}, timeConfig{hour12: opts.Hour12}) + " " + w.Format("MST") + exact := core.DateOf(w).String() + " " + timeText(c, TimeOfDay{w.Hour(), w.Minute(), 0}, timeConfig{hour12: opts.Hour12}) + " " + w.Format("MST") return ui.Text(c, text).Label(text + ", " + exact).Tooltip(exact).FontFeatures("tnum") } @@ -40,7 +42,7 @@ func RelativeTimeText(c *ui.Context, d time.Duration) string { var n int switch { case d < 45*time.Second: - return msg(c, "relative_time.now") + return core.Msg(c, "relative_time.now") case d < 45*time.Minute: key, n = "minutes", max(1, int(d/time.Minute)) case d < 22*time.Hour: @@ -52,11 +54,11 @@ func RelativeTimeText(c *ui.Context, d time.Duration) string { default: key, n = "years", max(1, int(d/(365*24*time.Hour))) } - unit := calendarCount(c, "relative_time."+key, n) + unit := core.CalendarCount(c, "relative_time."+key, n) if past { - return fmt.Sprintf(msg(c, "relative_time.ago"), unit) + return fmt.Sprintf(core.Msg(c, "relative_time.ago"), unit) } - return fmt.Sprintf(msg(c, "relative_time.in"), unit) + return fmt.Sprintf(core.Msg(c, "relative_time.in"), unit) } // relativeTimeNext is how long until the wording may change. diff --git a/relative_time_test.go b/datetime/relative_time_test.go similarity index 87% rename from relative_time_test.go rename to datetime/relative_time_test.go index 8501255..a6d5f10 100644 --- a/relative_time_test.go +++ b/datetime/relative_time_test.go @@ -1,23 +1,25 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestRelativeTimeWording(t *testing.T) { var en, zh []string cases := []time.Duration{10 * time.Second, -90 * time.Second, 3 * time.Minute, 5 * time.Hour, -25 * time.Hour, 40 * 24 * time.Hour, 800 * 24 * time.Hour} - for _, loc := range []Locale{EnUS, ZhCN} { + for _, loc := range []core.Locale{core.EnUS, core.ZhCN} { ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: loc}) + core.Use(c, core.Settings{Locale: loc}) var out []string for _, d := range cases { out = append(out, RelativeTimeText(c, d)) } - if loc == EnUS { + if loc == core.EnUS { en = out } else { zh = out @@ -50,7 +52,7 @@ func TestRelativeTimeShowsExactTimeAndStopsWhenHidden(t *testing.T) { at := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC) shown := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if shown { RelativeTime(c, at, RelativeTimeOptions{Location: calTokyo, Now: at.Add(3 * time.Hour)}) } @@ -68,7 +70,7 @@ func TestRelativeTimeShowsExactTimeAndStopsWhenHidden(t *testing.T) { t.Fatal("expected panic without Location") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); RelativeTime(c, at, RelativeTimeOptions{}) }, 10, 10) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); RelativeTime(c, at, RelativeTimeOptions{}) }, 10, 10) } func TestRelativeTimeSnapshot(t *testing.T) { diff --git a/reminder_picker.go b/datetime/reminder_picker.go similarity index 66% rename from reminder_picker.go rename to datetime/reminder_picker.go index 9d7c761..e91fc7f 100644 --- a/reminder_picker.go +++ b/datetime/reminder_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // ReminderPickerOptions configure a ReminderPicker. @@ -22,7 +25,7 @@ type ReminderPickerOptions struct { var reminderDefaults = []time.Duration{0, 5 * time.Minute, 15 * time.Minute, 30 * time.Minute, time.Hour, 24 * time.Hour, 7 * 24 * time.Hour} // ReminderPicker toggles reminder lead times, kept in choice order. -func ReminderPicker(c *ui.Context, value *[]time.Duration, opts ReminderPickerOptions) ChoiceResult { +func ReminderPicker(c *ui.Context, value *[]time.Duration, opts ReminderPickerOptions) core.ChoiceResult { choices := opts.Choices if len(choices) == 0 { choices = reminderDefaults @@ -41,15 +44,15 @@ func ReminderPicker(c *ui.Context, value *[]time.Duration, opts ReminderPickerOp panic(fmt.Sprintf("mujica: ReminderPicker Max %d is negative or below the value", opts.Max)) } full := opts.Max > 0 && len(*value) >= opts.Max - items := make([]ToggleItem[time.Duration], len(choices)) + items := make([]input.ToggleItem[time.Duration], len(choices)) for i, d := range choices { - items[i] = ToggleItem[time.Duration]{Value: d, Label: ReminderText(c, d), Disabled: full && !slices.Contains(*value, d)} + items[i] = input.ToggleItem[time.Duration]{Value: d, Label: ReminderText(c, d), Disabled: full && !slices.Contains(*value, d)} } label := opts.Label if label == "" { - label = msg(c, "reminder_picker.label") + label = core.Msg(c, "reminder_picker.label") } - r := ToggleGroup(c, value, items, ToggleGroupOptions{Multiple: true, AllowEmpty: true, Vertical: true, Label: label, Disabled: opts.Disabled}) + r := input.ToggleGroup(c, value, items, input.ToggleGroupOptions{Multiple: true, AllowEmpty: true, Vertical: true, Label: label, Disabled: opts.Disabled}) if r.Changed() { slices.SortFunc(*value, func(a, b time.Duration) int { return slices.Index(choices, a) - slices.Index(choices, b) }) slog.Debug("mujica: reminder_picker change", "value", *value) @@ -61,13 +64,13 @@ func ReminderPicker(c *ui.Context, value *[]time.Duration, opts ReminderPickerOp func ReminderText(c *ui.Context, d time.Duration) string { switch { case d == 0: - return msg(c, "reminder_picker.at_start") + return core.Msg(c, "reminder_picker.at_start") case d%(7*24*time.Hour) == 0: - return calendarCount(c, "reminder_picker.weeks", int(d/(7*24*time.Hour))) + return core.CalendarCount(c, "reminder_picker.weeks", int(d/(7*24*time.Hour))) case d%(24*time.Hour) == 0: - return calendarCount(c, "reminder_picker.days", int(d/(24*time.Hour))) + return core.CalendarCount(c, "reminder_picker.days", int(d/(24*time.Hour))) case d%time.Hour == 0: - return calendarCount(c, "reminder_picker.hours", int(d/time.Hour)) + return core.CalendarCount(c, "reminder_picker.hours", int(d/time.Hour)) } - return calendarCount(c, "reminder_picker.minutes", int(d/time.Minute)) + return core.CalendarCount(c, "reminder_picker.minutes", int(d/time.Minute)) } diff --git a/reminder_picker_test.go b/datetime/reminder_picker_test.go similarity index 94% rename from reminder_picker_test.go rename to datetime/reminder_picker_test.go index 69f1abf..a1288d4 100644 --- a/reminder_picker_test.go +++ b/datetime/reminder_picker_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "slices" @@ -6,11 +6,13 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func reminderTester(v *[]time.Duration, opts ReminderPickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ReminderPicker(c, v, opts).Changed() { *n++ } @@ -55,7 +57,7 @@ func TestReminderPickerMaxKeyboardDisabled(t *testing.T) { func TestReminderTextAndInvalid(t *testing.T) { var got []string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) got = []string{ReminderText(c, time.Minute), ReminderText(c, 2*time.Hour), ReminderText(c, 14*24*time.Hour)} }, 10, 10) if !slices.Equal(got, []string{"提前 1 分钟", "提前 2 小时", "提前 2 周"}) { diff --git a/stopwatch.go b/datetime/stopwatch.go similarity index 72% rename from stopwatch.go rename to datetime/stopwatch.go index 9fc6e24..9080c9e 100644 --- a/stopwatch.go +++ b/datetime/stopwatch.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StopwatchState is the caller's run and laps. @@ -40,9 +42,9 @@ type StopwatchOptions struct { } // Stopwatch measures time with laps, ticking while running and shown. -func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceResult { - k := Tokens(c) - d := Density(c) +func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) core.ChoiceResult { + k := core.Tokens(c) + d := core.Density(c) now := opts.Now live := now.IsZero() if live { @@ -50,7 +52,7 @@ func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceRe } label := opts.Label if label == "" { - label = msg(c, "stopwatch.label") + label = core.Msg(c, "stopwatch.label") } changed := false col := ui.Column(c).Gap(d.Unit() * 3).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -64,16 +66,16 @@ func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceRe } col.Children(func() { el := s.Elapsed(now) - ui.Text(c, StopwatchText(el)).Font(theme.MonoFont).FontSize(FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum"). + ui.Text(c, StopwatchText(el)).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum"). Label(label + ", " + StopwatchText(el)) ui.Row(c).Gap(d.Unit() * 2).Children(func() { - start, variant := "stopwatch.start", Primary + start, variant := "stopwatch.start", input.Primary if s.running { - start, variant = "stopwatch.pause", Secondary + start, variant = "stopwatch.pause", input.Secondary } else if el > 0 { start = "stopwatch.resume" } - if Button(c, msg(c, start), ButtonOptions{Variant: variant}).Clicked() { + if input.Button(c, core.Msg(c, start), input.ButtonOptions{Variant: variant}).Clicked() { act("toggle", func() { if s.running { s.base, s.running = s.Elapsed(now), false @@ -82,10 +84,10 @@ func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceRe } }) } - if Button(c, msg(c, "stopwatch.lap"), ButtonOptions{Variant: Secondary, Disabled: !s.running}).Clicked() { + if input.Button(c, core.Msg(c, "stopwatch.lap"), input.ButtonOptions{Variant: input.Secondary, Disabled: !s.running}).Clicked() { act("lap", func() { s.Laps = append(s.Laps, s.Elapsed(now)) }) } - if Button(c, msg(c, "stopwatch.reset"), ButtonOptions{Variant: Ghost, Disabled: s.running || el == 0 && len(s.Laps) == 0}).Clicked() { + if input.Button(c, core.Msg(c, "stopwatch.reset"), input.ButtonOptions{Variant: input.Ghost, Disabled: s.running || el == 0 && len(s.Laps) == 0}).Clicked() { act("reset", func() { *s = StopwatchState{} }) } }) @@ -99,7 +101,7 @@ func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceRe row := ui.Row(c).MinHeight(d.ControlHeight()).AlignItems(ui.Center).Gap(d.Unit() * 4).Role(ui.RoleListItem) row.DrawOver(func(p *ui.Painter, r ui.Rect) { internal.Rule(p, r.X, r.X+r.W, r.Y+r.H-1, k.Border) }) row.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "stopwatch.lap_n"), i+1)).Width(d.ControlHeight() * 2).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "stopwatch.lap_n"), i+1)).Width(d.ControlHeight() * 2).TextColor(k.TextMuted) ui.Text(c, StopwatchText(split)).Font(theme.MonoFont).FontFeatures("tnum").TextColor(k.Text).Grow(1) ui.Text(c, StopwatchText(s.Laps[i])).Font(theme.MonoFont).FontFeatures("tnum").TextColor(k.TextMuted) }) @@ -110,7 +112,7 @@ func Stopwatch(c *ui.Context, s *StopwatchState, opts StopwatchOptions) ChoiceRe if s.running && live { c.After(100*time.Millisecond - s.Elapsed(now)%(100*time.Millisecond)) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } // StopwatchText formats elapsed time as 01:02.3, with hours when needed. diff --git a/stopwatch_test.go b/datetime/stopwatch_test.go similarity index 95% rename from stopwatch_test.go rename to datetime/stopwatch_test.go index 8709593..764d4fd 100644 --- a/stopwatch_test.go +++ b/datetime/stopwatch_test.go @@ -1,10 +1,12 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStopwatchStartLapPauseReset(t *testing.T) { @@ -12,7 +14,7 @@ func TestStopwatchStartLapPauseReset(t *testing.T) { now := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC) n, disabled := 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Stopwatch(c, &s, StopwatchOptions{Now: now, Disabled: disabled}).Changed() { n++ } diff --git a/time_picker.go b/datetime/time_picker.go similarity index 89% rename from time_picker.go rename to datetime/time_picker.go index d0d0eb3..5559dac 100644 --- a/time_picker.go +++ b/datetime/time_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TimeOfDay is a wall-clock time with no date and no location. @@ -63,7 +64,7 @@ func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTim if !value.Valid() { panic(fmt.Sprintf("mujica: TimePicker value %v is not a time of day", *value)) } - col := ui.Column(c).Gap(Density(c).Unit()).AlignItems(ui.Start).Disabled(opts.Disabled) + col := ui.Column(c).Gap(core.Density(c).Unit()).AlignItems(ui.Start).Disabled(opts.Disabled) draft := ui.Local(col, "time-picker-draft", func() TimeOfDay { return *value }) seen := ui.Local(col, "time-picker-seen", func() TimeOfDay { return *value }) problem := ui.Local(col, "time-picker-error", func() string { return "" }) @@ -91,7 +92,7 @@ func TimePicker(c *ui.Context, value *TimeOfDay, opts TimePickerOptions) DateTim } *pending = false } - datetimeError(c, *problem) + core.DatetimeError(c, *problem) }) return DateTimeResult{Element: col, changed: changed} } @@ -129,11 +130,11 @@ func timePickerConfig(name string, opts TimePickerOptions) timeConfig { func timePickerProblem(c *ui.Context, t TimeOfDay, cfg timeConfig) string { switch { case t.Minute%cfg.minuteStep != 0: - return fmt.Sprintf(msg(c, "time_picker.minute_step"), cfg.minuteStep) + return fmt.Sprintf(core.Msg(c, "time_picker.minute_step"), cfg.minuteStep) case cfg.seconds && t.Second%cfg.secondStep != 0: - return fmt.Sprintf(msg(c, "time_picker.second_step"), cfg.secondStep) + return fmt.Sprintf(core.Msg(c, "time_picker.second_step"), cfg.secondStep) case t.Compare(cfg.min) < 0 || t.Compare(cfg.max) > 0: - return fmt.Sprintf(msg(c, "time_picker.out_of_range"), timeText(c, cfg.min, cfg), timeText(c, cfg.max, cfg)) + return fmt.Sprintf(core.Msg(c, "time_picker.out_of_range"), timeText(c, cfg.min, cfg), timeText(c, cfg.max, cfg)) } return "" } @@ -148,7 +149,7 @@ func timeText(c *ui.Context, t TimeOfDay, cfg timeConfig) string { s += fmt.Sprintf(":%02d", t.Second) } if cfg.hour12 { - s = fmt.Sprintf(msg(c, "time_picker.with_period"), s, timePeriod(c, t.Hour)) + s = fmt.Sprintf(core.Msg(c, "time_picker.with_period"), s, timePeriod(c, t.Hour)) } return s } @@ -162,9 +163,9 @@ func timeHour12(h int) int { func timePeriod(c *ui.Context, h int) string { if h < 12 { - return msg(c, "time_picker.am") + return core.Msg(c, "time_picker.am") } - return msg(c, "time_picker.pm") + return core.Msg(c, "time_picker.pm") } // timePart is one segment of a time editor. @@ -192,9 +193,9 @@ func timeParts(cfg timeConfig) []timePart { // timeEditor draws segments and popup; reports edits and invalid entry. func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Element, edited bool, bad string) { - k := Tokens(c) - d := Density(c) - field = datetimeField(c, ui.Row(c), cfg.problem != "", cfg.readOnly).Gap(d.Unit() * 2) + k := core.Tokens(c) + d := core.Density(c) + field = core.DatetimeField(c, ui.Row(c), cfg.problem != "", cfg.readOnly).Gap(d.Unit() * 2) open := ui.Local(field, "time-open", func() bool { return false }) fresh := ui.Local(field, "time-fresh", func() bool { return false }) bads := ui.Local(field, "time-bads", func() [4]timeBad { return [4]timeBad{} }) @@ -222,7 +223,7 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem if cfg.readOnly { return } - b := iconAction(c, icons.Must("clock"), msg(c, "time_picker.open")).Role(ui.RolePopUpButton) + b := core.IconAction(c, icons.Must("clock"), core.Msg(c, "time_picker.open")).Role(ui.RolePopUpButton) if b.Clicked() { *open, *fresh = !*open, true } @@ -230,7 +231,7 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem if !*open && !cfg.readOnly && field.Shortcut(ui.Alt, ui.KeyDown) { *open, *fresh = true, true } - datetimePopup(c, field, open, cfg.readOnly, func() { + core.DatetimePopup(c, field, open, cfg.readOnly, func() { if picked, ok := timeColumns(c, draft, cfg, *fresh); ok { bads[picked] = timeBad{} edited = true @@ -243,7 +244,7 @@ func timeEditor(c *ui.Context, draft *TimeOfDay, cfg timeConfig) (field *ui.Elem } if bads[part].text != "" && bad == "" { lo, hi := timeLimits(part, cfg) - bad = fmt.Sprintf(msg(c, "time_picker.part_range"), msg(c, timePartNames[part]), lo, hi) + bad = fmt.Sprintf(core.Msg(c, "time_picker.part_range"), core.Msg(c, timePartNames[part]), lo, hi) } } return field, edited, bad @@ -268,9 +269,9 @@ func timeLimits(part timePart, cfg timeConfig) (lo, hi int) { // timeSegment draws one segment; done reports a complete valid entry, bad an invalid one. func timeSegment(c *ui.Context, draft *TimeOfDay, part timePart, cfg timeConfig, bad *timeBad, edited *bool) (seg *ui.Element, done bool) { - k := Tokens(c) - d := Density(c) - name := msg(c, timePartNames[part]) + k := core.Tokens(c) + d := core.Density(c) + name := core.Msg(c, timePartNames[part]) if cfg.label != "" { name = cfg.label + ": " + name } @@ -363,7 +364,7 @@ func timeSegmentDescription(c *ui.Context, part timePart, text string, lo, hi in if part == timePeriodPart { return text } - return fmt.Sprintf(msg(c, "time_picker.segment"), strings.TrimSpace(text), lo, hi) + return fmt.Sprintf(core.Msg(c, "time_picker.segment"), strings.TrimSpace(text), lo, hi) } // timePartValue is the value a segment shows, so other segments' edits keep its error. diff --git a/time_picker_popup.go b/datetime/time_picker_popup.go similarity index 89% rename from time_picker_popup.go rename to datetime/time_picker_popup.go index 463afe7..626656b 100644 --- a/time_picker_popup.go +++ b/datetime/time_picker_popup.go @@ -1,18 +1,19 @@ -package mujica +package datetime import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // timeColumns lists choosable values per segment and reports whether a choice edited *draft. func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (picked timePart, edited bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) h := d.ControlHeight() - 4 within := func(lo, hi TimeOfDay) bool { return hi.Compare(cfg.min) >= 0 && lo.Compare(cfg.max) <= 0 } ui.Row(c).Gap(d.Unit()).Children(func() { @@ -37,7 +38,7 @@ func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (p case timePeriodPart: options = []TimeOfDay{{t.Hour % 12, t.Minute, t.Second}, {t.Hour%12 + 12, t.Minute, t.Second}} } - name := msg(c, timePartNames[part]) + name := core.Msg(c, timePartNames[part]) ui.Scroll(c).Height(h * 6).Width(d.ControlHeight() * 1.5).Children(func() { ui.Column(c).FocusGroup(ui.Vertical).Role(ui.RoleRadioGroup).Label(name).Children(func() { for _, o := range options { @@ -78,7 +79,7 @@ func timeColumns(c *ui.Context, draft *TimeOfDay, cfg timeConfig, fresh bool) (p } internal.Ring(item, k.Focus, theme.ControlRadius) if o == t { - item.Draw(func(p *ui.Painter, r ui.Rect) { datetimeSelectedMark(p, r, k.OnAccent) }) + item.Draw(func(p *ui.Painter, r ui.Rect) { core.DatetimeSelectedMark(p, r, k.OnAccent) }) } item.Children(func() { ui.Text(c, text).TextColor(fg).FontFeatures("tnum") }) if item.Clicked() { diff --git a/time_picker_test.go b/datetime/time_picker_test.go similarity index 97% rename from time_picker_test.go rename to datetime/time_picker_test.go index c6976d7..a519533 100644 --- a/time_picker_test.go +++ b/datetime/time_picker_test.go @@ -1,15 +1,17 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func timePickerTester(value *TimeOfDay, opts TimePickerOptions, changes *int) *ui.Tester { opts.Label = "Alarm" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { if TimePicker(c, value, opts).Changed() { *changes++ diff --git a/time_range_picker.go b/datetime/time_range_picker.go similarity index 86% rename from time_range_picker.go rename to datetime/time_range_picker.go index 66b8a61..dd00fc7 100644 --- a/time_range_picker.go +++ b/datetime/time_range_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TimeRange is a span of day; End before Start means next day. @@ -44,8 +45,8 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption if value.Overnight() && !opts.AllowOvernight { panic(fmt.Sprintf("mujica: TimeRangePicker value %v crosses midnight without AllowOvernight", *value)) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Gap(d.Unit()).AlignItems(ui.Start).Disabled(opts.Disabled).Role(ui.RoleGroup) if opts.Label != "" { col.Label(opts.Label) @@ -62,7 +63,7 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption edited, bad := false, "" ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { start, end := cfg, cfg - start.label, end.label = msg(c, "time_range_picker.start"), msg(c, "time_range_picker.end") + start.label, end.label = core.Msg(c, "time_range_picker.start"), core.Msg(c, "time_range_picker.end") start.problem, end.problem = *problem, *problem _, e1, b1 := timeEditor(c, &draft.Start, start) ui.Text(c, "–").TextColor(k.TextMuted).Role(ui.RoleNone) @@ -72,7 +73,7 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption bad = b2 } if draft.Overnight() && opts.AllowOvernight { - ui.Text(c, msg(c, "time_range_picker.next_day")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) + ui.Text(c, core.Msg(c, "time_range_picker.next_day")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) } }) p := bad @@ -91,7 +92,7 @@ func TimeRangePicker(c *ui.Context, value *TimeRange, opts TimeRangePickerOption } *pending = false } - datetimeError(c, *problem) + core.DatetimeError(c, *problem) }) return DateTimeResult{Element: col, changed: changed} } @@ -105,9 +106,9 @@ func timeRangeProblem(c *ui.Context, r TimeRange, cfg timeConfig, overnight bool } switch { case r.Start == r.End: - return msg(c, "time_range_picker.same") + return core.Msg(c, "time_range_picker.same") case r.Overnight() && !overnight: - return msg(c, "time_range_picker.order") + return core.Msg(c, "time_range_picker.order") } return "" } diff --git a/time_range_picker_test.go b/datetime/time_range_picker_test.go similarity index 95% rename from time_range_picker_test.go rename to datetime/time_range_picker_test.go index 44f3f2b..bb5fbb7 100644 --- a/time_range_picker_test.go +++ b/datetime/time_range_picker_test.go @@ -1,14 +1,16 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func timeRangeTester(value *TimeRange, opts TimeRangePickerOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { if TimeRangePicker(c, value, opts).Changed() { *changes++ diff --git a/timezone_select.go b/datetime/timezone_select.go similarity index 86% rename from timezone_select.go rename to datetime/timezone_select.go index c66fd90..6a39a01 100644 --- a/timezone_select.go +++ b/datetime/timezone_select.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,12 +6,14 @@ import ( "strings" "sync" "time" - _ "time/tzdata" // zones resolve offline, without the system database + _ "time/tzdata" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TimezoneSelectOptions configure a TimezoneSelect. @@ -67,7 +69,7 @@ func TimezoneOffset(loc *time.Location, t time.Time) string { } // TimezoneSelect searches zones by name or offset into *zone. -func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) ChoiceResult { +func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) core.ChoiceResult { zones, common := opts.Zones, opts.Common if len(zones) == 0 { zones = timezoneWorld @@ -85,11 +87,11 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) Cho if now.IsZero() { now = c.Now() } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "timezone_select.label") + label = core.Msg(c, "timezone_select.label") } old := *zone col := ui.Column(c).Width(d.ControlHeight() * 9).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled) @@ -102,16 +104,16 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) Cho text = TimezoneOffset(timezoneLoad(*zone), now) + " · " + timezoneName(*zone) } col.Children(func() { - field := datetimeField(c, ui.Row(c), false, false) + field := core.DatetimeField(c, ui.Row(c), false, false) field.Children(func() { - t := datetimeTrigger(c, text, msg(c, "timezone_select.placeholder"), "clock").Label(label).Description(text) + t := core.DatetimeTrigger(c, text, core.Msg(c, "timezone_select.placeholder"), "clock").Label(label).Description(text) if !t.IsDisabled() && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { *open, *query, *hi = !*open, "", 0 } }) - datetimePopup(c, field, open, false, func() { + core.DatetimePopup(c, field, open, false, func() { list := timezoneFilter(zones, common, *query, now) - in := TextInput(c, query, TextInputOptions{Placeholder: msg(c, "timezone_select.search"), Label: msg(c, "timezone_select.search")}) + in := input.TextInput(c, query, input.TextInputOptions{Placeholder: core.Msg(c, "timezone_select.search"), Label: core.Msg(c, "timezone_select.search")}) in.Input.AutoFocus() if in.Changed() { *hi = 0 @@ -130,7 +132,7 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) Cho _, wh := c.Size() ui.Scroll(c).MaxHeight(min(d.ControlHeight()*8, wh/2)).Children(func() { if len(list) == 0 { - ui.Text(c, msg(c, "timezone_select.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 2) + ui.Text(c, core.Msg(c, "timezone_select.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 2) } for i, z := range list { if i == 0 || z.common != list[i-1].common { @@ -138,7 +140,7 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) Cho if z.common { key = "timezone_select.common" } - ui.Text(c, msg(c, key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*2) + ui.Text(c, core.Msg(c, key)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*2) } if timezoneRow(c, z, i == *hi, i == *hi && *hi != *shown, z.name == *zone) { *zone, *open = z.name, false @@ -151,7 +153,7 @@ func TimezoneSelect(c *ui.Context, zone *string, opts TimezoneSelectOptions) Cho if changed { slog.Debug("mujica: timezone_select change", "from", old, "to", *zone) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } type timezoneEntry struct { @@ -183,8 +185,8 @@ func timezoneName(z string) string { // timezoneRow draws one zone; it reports a click. func timezoneRow(c *ui.Context, z timezoneEntry, highlighted, reveal, chosen bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(d.Unit() * 3).PaddingX(d.Unit() * 2).Radius(theme.ControlRadius). Label(z.offset + " " + timezoneName(z.name)) internal.Ring(b, k.Focus, theme.ControlRadius) diff --git a/timezone_select_test.go b/datetime/timezone_select_test.go similarity index 96% rename from timezone_select_test.go rename to datetime/timezone_select_test.go index 084a170..4260a6a 100644 --- a/timezone_select_test.go +++ b/datetime/timezone_select_test.go @@ -1,17 +1,19 @@ -package mujica +package datetime import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var tzNow = time.Date(2026, time.July, 1, 12, 0, 0, 0, time.UTC) func timezoneTester(zone *string, opts *TimezoneSelectOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := *opts o.Now = tzNow ui.Column(c).Padding(16).Children(func() { diff --git a/datetime/week_picker.go b/datetime/week_picker.go new file mode 100644 index 0000000..38a70e1 --- /dev/null +++ b/datetime/week_picker.go @@ -0,0 +1,97 @@ +package datetime + +import ( + "fmt" + "log/slog" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" +) + +// WeekPickerOptions configure a WeekPicker. +type WeekPickerOptions struct { + WeekStart WeekStart + // Min and Max bound the chosen week's days. + Min, Max core.Date + // Today is marked; zero uses the system clock. + Today core.Date + Clearable bool + Disabled bool + Label string +} + +// WeekPicker chooses a week; *value is its first day. +func WeekPicker(c *ui.Context, value *core.Date, opts WeekPickerOptions) core.ChoiceResult { + core.DatetimeCheckDate("WeekPicker value", *value) + core.DatetimeCheckBounds("WeekPicker", opts.Min, opts.Max) + first := CalendarFirstWeekday(c, opts.WeekStart) + weekOf := func(d core.Date) core.Date { return d.AddDays(-((int(d.Weekday()) - int(first) + 7) % 7)) } + if !value.IsZero() && weekOf(*value) != *value { + panic(fmt.Sprintf("mujica: WeekPicker value %v does not start a week", *value)) + } + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) + label := opts.Label + if label == "" { + label = core.Msg(c, "week_picker.label") + } + enabled := func(x core.Date) bool { + w := weekOf(x) + return w.AddDays(6).Within(opts.Min, core.Date{}) && w.Within(core.Date{}, opts.Max) + } + col := ui.Column(c).Width(d.ControlHeight() * 8).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled) + open := ui.Local(col, "week-picker-open", func() bool { return false }) + cursor := ui.Local(col, "week-picker-cursor", func() core.Date { return core.Date{} }) + hover := ui.Local(col, "week-picker-hover", func() core.Date { return core.Date{} }) + old := *value + text := "" + if !value.IsZero() { + text = WeekViewTitle(c, *value, value.AddDays(6)) + } + col.Children(func() { + field := core.DatetimeField(c, ui.Row(c), false, false) + field.Children(func() { + t := core.DatetimeTrigger(c, text, core.Msg(c, "week_picker.placeholder"), "calendar").Label(label).Description(text) + if !t.IsDisabled() && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { + *open, *hover = !*open, core.Date{} + *cursor = *value + if cursor.IsZero() { + *cursor = today + } + } + if opts.Clearable && !value.IsZero() { + if b := core.IconAction(c, icons.Must("x"), core.Msg(c, "calendar.clear")); b.Clicked() && !b.IsDisabled() { + *value = core.Date{} + } + } + }) + core.DatetimePopup(c, field, open, false, func() { + band := *hover + if band.IsZero() { + band = *cursor + } + picked, hovered := calendarMonth(c, cursor, calendarConfig{ + chosen: func(x core.Date) bool { return !value.IsZero() && weekOf(x) == *value }, + inRange: func(x core.Date) bool { return weekOf(x) == weekOf(band) }, + enabled: enabled, + today: today, + weekStart: opts.WeekStart, + autoFocus: true, + }) + if hovered != *hover { + *hover = hovered + c.Invalidate() + } + if !picked.IsZero() { + *value, *open = weekOf(picked), false + } + }) + }) + changed := *value != old + if changed { + slog.Debug("mujica: week_picker change", "from", old, "to", *value) + } + return core.NewChoiceResult(col, changed) +} diff --git a/week_picker_test.go b/datetime/week_picker_test.go similarity index 90% rename from week_picker_test.go rename to datetime/week_picker_test.go index d8c3d8f..59313d0 100644 --- a/week_picker_test.go +++ b/datetime/week_picker_test.go @@ -1,14 +1,16 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func weekPickerTester(v *Date, opts *WeekPickerOptions, n *int) *ui.Tester { +func weekPickerTester(v *core.Date, opts *WeekPickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := *opts o.Today, o.Label = calendarToday, "Sprint" ui.Column(c).Padding(16).Children(func() { @@ -20,7 +22,7 @@ func weekPickerTester(v *Date, opts *WeekPickerOptions, n *int) *ui.Tester { } func TestWeekPickerPicksWeekStart(t *testing.T) { - v, n := Date{}, 0 + v, n := core.Date{}, 0 opts := &WeekPickerOptions{} tt := weekPickerTester(&v, opts, &n) _ = tt.Click("Sprint") diff --git a/year_picker.go b/datetime/year_picker.go similarity index 86% rename from year_picker.go rename to datetime/year_picker.go index f6b990d..05f43d8 100644 --- a/year_picker.go +++ b/datetime/year_picker.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -6,6 +6,8 @@ import ( "strconv" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // YearPickerOptions configure a YearPicker. @@ -15,7 +17,7 @@ type YearPickerOptions struct { DisableYear func(int) bool Clearable bool // Today marks the current year; zero uses the system clock. - Today Date + Today core.Date Disabled bool // ReadOnly keeps the field focusable and copyable but blocks the popup and clearing. ReadOnly bool @@ -28,7 +30,7 @@ func YearPicker(c *ui.Context, value *int, opts YearPickerOptions) DateTimeResul if *value < 0 || opts.Min < 0 || opts.Max < 0 || opts.Min != 0 && opts.Max != 0 && opts.Min > opts.Max { panic(fmt.Sprintf("mujica: YearPicker value %d or range %d..%d is not valid", *value, opts.Min, opts.Max)) } - now := datetimeToday(c, opts.Today).Year + now := core.DatetimeToday(c, opts.Today).Year home := *value if home == 0 { home = now @@ -40,18 +42,18 @@ func YearPicker(c *ui.Context, value *int, opts YearPickerOptions) DateTimeResul } label := opts.Label if label == "" { - label = msg(c, "year_picker.placeholder") + label = core.Msg(c, "year_picker.placeholder") } col, picked, ok, cleared := datetimeGridPicker(c, datetimeGridField{ - text: text, placeholder: msg(c, "year_picker.placeholder"), label: label, + text: text, placeholder: core.Msg(c, "year_picker.placeholder"), label: label, clearable: opts.Clearable, hasValue: *value != 0, disabled: opts.Disabled, readOnly: opts.ReadOnly, home: home, }, datetimeGridConfig{ pageOf: func(o int) (int, int) { return decade(o) - 1, 12 }, cols: 3, page: 10, title: func(start int) string { return fmt.Sprintf("%d – %d", start+1, start+10) }, - prev: msg(c, "year_picker.prev"), - next: msg(c, "year_picker.next"), + prev: core.Msg(c, "year_picker.prev"), + next: core.Msg(c, "year_picker.next"), text: strconv.Itoa, label: strconv.Itoa, chosen: func(o int) bool { return o == *value }, diff --git a/year_picker_test.go b/datetime/year_picker_test.go similarity index 95% rename from year_picker_test.go rename to datetime/year_picker_test.go index da6b59b..0fc6aaf 100644 --- a/year_picker_test.go +++ b/datetime/year_picker_test.go @@ -1,16 +1,18 @@ -package mujica +package datetime import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func yearPickerTester(value *int, opts YearPickerOptions, changes *int) *ui.Tester { opts.Today = calendarToday opts.Label = "Vintage" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(16).Children(func() { if YearPicker(c, value, opts).Changed() { *changes++ diff --git a/year_view.go b/datetime/year_view.go similarity index 70% rename from year_view.go rename to datetime/year_view.go index 812c48d..97d3a77 100644 --- a/year_view.go +++ b/datetime/year_view.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // YearViewOptions configure a YearView. @@ -19,7 +20,7 @@ type YearViewOptions struct { Version int WeekStart WeekStart // Today is marked; zero uses the system clock. - Today Date + Today core.Date Disabled bool Label string } @@ -31,9 +32,9 @@ func YearView(c *ui.Context, year *int, events []CalendarEvent, opts YearViewOpt panic(fmt.Sprintf("mujica: YearView year %d is not valid", *year)) } eventsCheck("YearView", events) - d := Density(c) - today := datetimeToday(c, opts.Today) - first := Date{*year, time.January, 1} + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) + first := core.Date{Year: *year, Month: time.January, Day: 1} label := opts.Label if label == "" { label = strconv.Itoa(*year) @@ -42,22 +43,22 @@ func YearView(c *ui.Context, year *int, events []CalendarEvent, opts YearViewOpt root := ui.Column(c).Gap(d.Unit() * 3).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) r.Element = root counts := eventsDerived(root, "year-view-counts", events, opts.Version, eventDayStart(first, loc), eventDayStart(first.AddMonths(12), loc), - func(hits []int) map[Date]int { - m := map[Date]int{} + func(hits []int) map[core.Date]int { + m := map[core.Date]int{} for _, h := range hits { ev := events[h] - for day := DateOf(ev.Start.In(loc)); eventDayStart(day, loc).Before(ev.End); day = day.AddDays(1) { + for day := core.DateOf(ev.Start.In(loc)); eventDayStart(day, loc).Before(ev.End); day = day.AddDays(1) { m[day]++ } } return m }) - cursor := ui.Local(root, "year-view-cursor", func() Date { return Date{} }) + cursor := ui.Local(root, "year-view-cursor", func() core.Date { return core.Date{} }) if cursor.Year != *year { - *cursor = Date{*year, today.Month, min(today.Day, 28)} + *cursor = core.Date{Year: *year, Month: today.Month, Day: min(today.Day, 28)} } root.Children(func() { - calendarViewHeader(c, strconv.Itoa(*year), msg(c, "calendar.prev_year"), msg(c, "calendar.next_year"), func(dir int) { + calendarViewHeader(c, strconv.Itoa(*year), core.Msg(c, "calendar.prev_year"), core.Msg(c, "calendar.next_year"), func(dir int) { *year += dir }) grid := ui.Row(c).Wrap().AlignItems(ui.Start).Gap(d.Unit() * 4).FocusGroup(ui.Horizontal | ui.Vertical) @@ -89,20 +90,20 @@ func YearView(c *ui.Context, year *int, events []CalendarEvent, opts YearViewOpt } // yearMonth draws one small month and reports a day clicked. -func yearMonth(c *ui.Context, year int, m time.Month, counts map[Date]int, today, cursor Date, focus bool, ws WeekStart) (Date, bool) { - k := Tokens(c) +func yearMonth(c *ui.Context, year int, m time.Month, counts map[core.Date]int, today, cursor core.Date, focus bool, ws WeekStart) (core.Date, bool) { + k := core.Tokens(c) cell := theme.MinHitSize - first := calendarFirstWeekday(c, ws) - start := Date{year, m, 1} + first := CalendarFirstWeekday(c, ws) + start := core.Date{Year: year, Month: m, Day: 1} lead := (int(start.Weekday()) - int(first) + 7) % 7 - var picked Date + var picked core.Date ui.Column(c).Width(cell * 7).Gap(2).Children(func() { - ui.Text(c, datetimeMonthName(c, m)).Font(theme.SerifFont).TextColor(k.Text).Bold() + ui.Text(c, core.DatetimeMonthName(c, m)).Font(theme.SerifFont).TextColor(k.Text).Bold() ui.Row(c).Children(func() { for i := range 7 { wd := (int(first) + i) % 7 - ui.Text(c, msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "calendar.weekday_"+strconv.Itoa(wd))).Width(cell).TextAlign(ui.Center). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) last := start.AddMonths(1).AddDays(-1).Day @@ -114,11 +115,11 @@ func yearMonth(c *ui.Context, year int, m time.Month, counts map[Date]int, today ui.Box(c).Size(cell, cell) continue } - day := Date{year, m, n} + day := core.Date{Year: year, Month: m, Day: n} busy := counts[day] - text := datetimeDayLabel(c, day) + text := core.DatetimeDayLabel(c, day) if busy > 0 { - text += ", " + calendarCount(c, "year_view.events", busy) + text += ", " + core.CalendarCount(c, "year_view.events", busy) } b := ui.ButtonBase(c).Size(cell, cell).Radius(theme.ControlRadius).Label(text).Column() internal.Ring(b, k.Focus, theme.ControlRadius) @@ -139,7 +140,7 @@ func yearMonth(c *ui.Context, year int, m time.Month, counts map[Date]int, today } }) b.Children(func() { - t := ui.Text(c, strconv.Itoa(n)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).FontFeatures("tnum") + t := ui.Text(c, strconv.Itoa(n)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).FontFeatures("tnum") if day == today { t.Bold().TextColor(k.AccentText) } diff --git a/year_view_test.go b/datetime/year_view_test.go similarity index 79% rename from year_view_test.go rename to datetime/year_view_test.go index 6a42408..f80e88d 100644 --- a/year_view_test.go +++ b/datetime/year_view_test.go @@ -1,4 +1,4 @@ -package mujica +package datetime import ( "fmt" @@ -7,13 +7,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) -func yearViewTester(year *int, events []CalendarEvent, opts YearViewOptions, days *[]Date) *ui.Tester { +func yearViewTester(year *int, events []CalendarEvent, opts YearViewOptions, days *[]core.Date) *ui.Tester { opts.Location, opts.Today = calTokyo, calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if d, ok := YearView(c, year, events, opts).DayChosen(); ok { *days = append(*days, d) } @@ -27,7 +28,7 @@ func TestYearViewDensityMarks(t *testing.T) { } evs = append(evs, calEvent("x", 20, 9, 0, 30)) year := 2026 - var days []Date + var days []core.Date tt := yearViewTester(&year, evs, YearViewOptions{}, &days) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) if !tt.HasText("Wednesday, October 14, 2026, 4 events") || !tt.HasText("Tuesday, October 20, 2026, 1 event") { @@ -56,18 +57,18 @@ func TestYearViewDensityMarks(t *testing.T) { func TestYearViewClickKeysAndYearChange(t *testing.T) { year := 2026 - var days []Date + var days []core.Date tt := yearViewTester(&year, nil, YearViewOptions{}, &days) - _ = tt.Click(datetimeTestLabel(Date{2026, time.March, 3})) + _ = tt.Click(datetimeTestLabel(core.Date{Year: 2026, Month: time.March, Day: 3})) tt.Key(0, ui.KeyRight) tt.Key(0, ui.KeyDown) tt.Key(0, ui.KeyEnter) - if len(days) != 2 || days[0] != (Date{2026, time.March, 3}) || days[1] != (Date{2026, time.March, 11}) { + if len(days) != 2 || days[0] != (core.Date{Year: 2026, Month: time.March, Day: 3}) || days[1] != (core.Date{Year: 2026, Month: time.March, Day: 11}) { t.Fatalf("days %v", days) } - _ = tt.Click(datetimeTestLabel(Date{2026, time.December, 31})) + _ = tt.Click(datetimeTestLabel(core.Date{Year: 2026, Month: time.December, Day: 31})) tt.Key(0, ui.KeyRight) - if year != 2027 || !tt.Focused(datetimeTestLabel(Date{2027, time.January, 1})) { + if year != 2027 || !tt.Focused(datetimeTestLabel(core.Date{Year: 2027, Month: time.January, Day: 1})) { t.Fatalf("crossing the year: %d", year) } _ = tt.Click("Previous year") @@ -78,7 +79,7 @@ func TestYearViewClickKeysAndYearChange(t *testing.T) { func TestYearViewDisabledAndInvalid(t *testing.T) { year := 2026 - var days []Date + var days []core.Date tt := yearViewTester(&year, nil, YearViewOptions{Disabled: true}, &days) _ = tt.Click(datetimeTestLabel(oct(3))) _ = tt.Click("Next year") diff --git a/device_selector.go b/device_selector.go deleted file mode 100644 index e7b9e92..0000000 --- a/device_selector.go +++ /dev/null @@ -1,78 +0,0 @@ -package mujica - -import ( - "log/slog" - - "github.com/egoist/mygo/ui" - - "mujicaui/theme" -) - -// MediaDevice is one device; IDs are unique within a kind. -type MediaDevice struct { - ID, Name string -} - -// MediaDevices are the caller's lists; nil hides that kind. -type MediaDevices struct { - Microphones, Cameras, Speakers []MediaDevice -} - -// DeviceSelection holds each kind's chosen ID; empty is none. -type DeviceSelection struct { - Microphone, Camera, Speaker string -} - -// DeviceSelectorOptions configure a DeviceSelector. -type DeviceSelectorOptions struct { - Disabled bool -} - -// DeviceSelector chooses a microphone, camera and speaker. -func DeviceSelector(c *ui.Context, sel *DeviceSelection, devices MediaDevices, opts DeviceSelectorOptions) ChoiceResult { - old := *sel - k := Tokens(c) - u := Density(c).Unit() - col := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(msg(c, "device_selector.label")).Disabled(opts.Disabled) - col.Children(func() { - deviceRow(c, k, mediaMic, "device_selector.microphone", &sel.Microphone, devices.Microphones, opts.Disabled) - deviceRow(c, k, mediaCamera, "device_selector.camera", &sel.Camera, devices.Cameras, opts.Disabled) - deviceRow(c, k, mediaSpeaker, "device_selector.speaker", &sel.Speaker, devices.Speakers, opts.Disabled) - }) - changed := *sel != old - if changed { - slog.Debug("mujica: device_selector changed", "selection", *sel) - } - return ChoiceResult{Element: col, changed: changed} -} - -func deviceRow(c *ui.Context, k theme.Tokens, icon *ui.SVG, key string, id *string, list []MediaDevice, disabled bool) { - if list == nil { - return - } - options := make([]SelectOption[string], len(list)) - for i, d := range list { - if d.ID == "" { - panic("mujica: DeviceSelector device without an ID") - } - for _, o := range options[:i] { - if o.Value == d.ID { - panic("mujica: DeviceSelector duplicate device ID " + d.ID) - } - } - options[i] = SelectOption[string]{Value: d.ID, Label: d.Name} - } - if _, ok := selectLabel(options, *id); *id != "" && !ok { - panic("mujica: DeviceSelector selection " + *id + " is not in its list") - } - ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Icon(c, icon).Size(16, 16).TextColor(k.TextMuted) - ui.Text(c, msg(c, key)).Width(96).SingleLine() - hint := msg(c, "device_selector.choose") - if len(list) == 0 { - hint = msg(c, "device_selector.none") - } - Select(c, id, options, SelectOptions{Disabled: disabled || len(list) == 0, Label: msg(c, key), - Placeholder: hint, Empty: msg(c, "device_selector.none")}).Element.Grow(1) - }) -} diff --git a/alert_list.go b/devtools/alert_list.go similarity index 73% rename from alert_list.go rename to devtools/alert_list.go index 5d97579..3200c79 100644 --- a/alert_list.go +++ b/devtools/alert_list.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AlertLevel is an alert's level. The zero value is AlertInfo. @@ -60,16 +63,16 @@ func AlertList(c *ui.Context, alerts []AlertItem, opts AlertListOptions) AlertLi if maxH == 0 { maxH = 360 } - k := Tokens(c) + k := core.Tokens(c) var r AlertListResult r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled) locked := r.Element.IsDisabled() r.Element.Children(func() { if len(alerts) == 0 { - agentCaption(c, msg(c, "alert_list.empty")) + agent.AgentCaption(c, core.Msg(c, "alert_list.empty")) return } - agentRows(c, len(alerts), func(i int) { + agent.AgentRows(c, len(alerts), func(i int) { ack, silence := alertRow(c, alerts[i], locked) if ack && !locked { r.Ack = alerts[i].ID @@ -88,15 +91,15 @@ func AlertList(c *ui.Context, alerts []AlertItem, opts AlertListOptions) AlertLi // alertRow shows an alert with acknowledge and silence buttons. func alertRow(c *ui.Context, a AlertItem, locked bool) (ack, silence bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() - sev := [...]Severity{SeverityInfo, SeverityWarning, SeverityError}[a.Level] - word := msg(c, [...]string{"alert_list.info", "alert_list.warning", "alert_list.critical"}[a.Level]) + sev := [...]core.Severity{core.SeverityInfo, core.SeverityWarning, core.SeverityError}[a.Level] + word := core.Msg(c, [...]string{"alert_list.info", "alert_list.warning", "alert_list.critical"}[a.Level]) silenced := a.Silence.After(c.Now()) row := ui.Row(c).Key(a.ID).Gap(u*3).AlignItems(ui.Center).MinHeight(d.ControlHeight()+u*2).Padding(u*2, u*3). Label(word + ", " + a.Title) - if tone, _ := severityLook(k, sev); a.Level == AlertCritical && !a.Acked { + if tone, _ := core.SeverityLook(k, sev); a.Level == AlertCritical && !a.Acked { row.DrawOver(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y, W: 3, H: r.H}, tone, 0) }) } row.Children(func() { @@ -110,18 +113,18 @@ func alertRow(c *ui.Context, a AlertItem, locked bool) (ack, silence bool) { if !a.At.IsZero() { line += " · " + devtoolsAgo(c, a.At) } - agentCaption(c, line) + agent.AgentCaption(c, line) }) switch { case silenced: - agentCaption(c, fmt.Sprintf(msg(c, "alert_list.silenced"), a.Silence.Format("15:04"))) + agent.AgentCaption(c, fmt.Sprintf(core.Msg(c, "alert_list.silenced"), a.Silence.Format("15:04"))) case a.Acked: - agentCaption(c, msg(c, "alert_list.acked")) + agent.AgentCaption(c, core.Msg(c, "alert_list.acked")) default: - ack = Button(c, msg(c, "alert_list.ack"), ButtonOptions{Variant: Secondary, Disabled: locked}).Clicked() + ack = input.Button(c, core.Msg(c, "alert_list.ack"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked}).Clicked() } if !silenced { - silence = agentToggle(c, msg(c, "alert_list.silence"), "bell", locked) + silence = agent.AgentToggle(c, core.Msg(c, "alert_list.silence"), "bell", locked) } }) return ack, silence diff --git a/alert_list_test.go b/devtools/alert_list_test.go similarity index 96% rename from alert_list_test.go rename to devtools/alert_list_test.go index c3d136f..0761e88 100644 --- a/alert_list_test.go +++ b/devtools/alert_list_test.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func alertSample() []AlertItem { @@ -19,7 +21,7 @@ func alertSample() []AlertItem { func alertTester(alerts []AlertItem, opts AlertListOptions, got *[]AlertListResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := AlertList(c, alerts, opts) if r.Ack != "" || r.Silence != "" { *got = append(*got, r) diff --git a/api_request_builder.go b/devtools/api_request_builder.go similarity index 60% rename from api_request_builder.go rename to devtools/api_request_builder.go index 47452d8..60778d0 100644 --- a/api_request_builder.go +++ b/devtools/api_request_builder.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -8,8 +8,13 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ApiRequest holds request data sent by the caller. @@ -55,7 +60,7 @@ func ApiRequestBuilder(c *ui.Context, req *ApiRequest, opts ApiRequestBuilderOpt if !slices.Contains(apiMethods, req.Method) { panic("mujica: ApiRequestBuilder method " + req.Method + " is not offered") } - u := Density(c).Unit() + u := core.Density(c).Unit() var r ApiRequestResult r.Element = ui.Column(c).Shrink(0).Gap(u * 3).Disabled(opts.Disabled) st := ui.Local(r.Element, "mujica.apirequest", func() apiRequestState { return apiRequestState{} }) @@ -72,42 +77,42 @@ func ApiRequestBuilder(c *ui.Context, req *ApiRequest, opts ApiRequestBuilderOpt full, urlErr := apiRequestURL(*req) r.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() { - methods := make([]SelectOption[string], len(apiMethods)) + methods := make([]input.SelectOption[string], len(apiMethods)) for i, m := range apiMethods { - methods[i] = SelectOption[string]{Value: m, Label: m} + methods[i] = input.SelectOption[string]{Value: m, Label: m} } - r.Changed = Select(c, &req.Method, methods, SelectOptions{Label: msg(c, "api_request_builder.method"), Disabled: locked}).Changed() + r.Changed = input.Select(c, &req.Method, methods, input.SelectOptions{Label: core.Msg(c, "api_request_builder.method"), Disabled: locked}).Changed() shownErr := "" if req.URL != "" && urlErr != nil { - shownErr = msg(c, "api_request_builder.invalid_url") + shownErr = core.Msg(c, "api_request_builder.invalid_url") } - in := TextInput(c, &req.URL, TextInputOptions{Placeholder: "https://", Label: msg(c, "api_request_builder.url"), Disabled: locked, Error: shownErr}) + in := input.TextInput(c, &req.URL, input.TextInputOptions{Placeholder: "https://", Label: core.Msg(c, "api_request_builder.url"), Disabled: locked, Error: shownErr}) in.Element.Grow(1) r.Changed = r.Changed || in.Changed() ready := urlErr == nil && !opts.Sending && !locked - send := Button(c, msg(c, "api_request_builder.send"), ButtonOptions{Icon: icons.Must("upload"), Loading: opts.Sending, Disabled: locked || urlErr != nil}) + send := input.Button(c, core.Msg(c, "api_request_builder.send"), input.ButtonOptions{Icon: icons.Must("upload"), Loading: opts.Sending, Disabled: locked || urlErr != nil}) r.Send = ready && (send.Clicked() || in.Submitted()) }) if urlErr == nil { - codeMono(c, req.Method+" "+full).TextColor(Tokens(c).TextMuted) + code.CodeMono(c, req.Method+" "+full).TextColor(core.Tokens(c).TextMuted) } - tabs := []Tab{ - {Label: fmt.Sprintf(msg(c, "api_request_builder.params"), len(keyValueEnabled(req.Params))), Panel: func() { - r.Changed = KeyValueInput(c, &req.Params, KeyValueInputOptions{Label: msg(c, "api_request_builder.params_label"), Disabled: locked}).Changed() || r.Changed + tabs := []navigation.Tab{ + {Label: fmt.Sprintf(core.Msg(c, "api_request_builder.params"), len(keyValueEnabled(req.Params))), Panel: func() { + r.Changed = KeyValueInput(c, &req.Params, KeyValueInputOptions{Label: core.Msg(c, "api_request_builder.params_label"), Disabled: locked}).Changed() || r.Changed }}, - {Label: fmt.Sprintf(msg(c, "api_request_builder.headers"), len(keyValueEnabled(req.Headers))), Panel: func() { - r.Changed = KeyValueInput(c, &req.Headers, KeyValueInputOptions{Label: msg(c, "api_request_builder.headers_label"), Disabled: locked}).Changed() || r.Changed + {Label: fmt.Sprintf(core.Msg(c, "api_request_builder.headers"), len(keyValueEnabled(req.Headers))), Panel: func() { + r.Changed = KeyValueInput(c, &req.Headers, KeyValueInputOptions{Label: core.Msg(c, "api_request_builder.headers_label"), Disabled: locked}).Changed() || r.Changed }}, - {Label: msg(c, "api_request_builder.body"), Panel: func() { + {Label: core.Msg(c, "api_request_builder.body"), Panel: func() { if req.Method == "GET" || req.Method == "HEAD" { - agentCaption(c, msg(c, "api_request_builder.no_body")) + agent.AgentCaption(c, core.Msg(c, "api_request_builder.no_body")) } - body := TextArea(c, &req.Body, TextAreaOptions{Label: msg(c, "api_request_builder.body"), MinHeight: 120, MaxHeight: 320, Disabled: locked}) + body := input.TextArea(c, &req.Body, input.TextAreaOptions{Label: core.Msg(c, "api_request_builder.body"), MinHeight: 120, MaxHeight: 320, Disabled: locked}) body.Input.Font(theme.MonoFont) r.Changed = r.Changed || body.Changed() }}, } - Tabs(c, &st.tab, tabs, TabsOptions{Label: msg(c, "api_request_builder.parts")}) + navigation.Tabs(c, &st.tab, tabs, navigation.TabsOptions{Label: core.Msg(c, "api_request_builder.parts")}) }) if r.Send { slog.Debug("mujica: api_request_builder send", "method", req.Method) // Request URLs may contain secrets. diff --git a/api_request_builder_test.go b/devtools/api_request_builder_test.go similarity index 95% rename from api_request_builder_test.go rename to devtools/api_request_builder_test.go index 1013035..2dd5dd1 100644 --- a/api_request_builder_test.go +++ b/devtools/api_request_builder_test.go @@ -1,14 +1,16 @@ -package mujica +package devtools import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func apiTester(req *ApiRequest, opts *ApiRequestBuilderOptions, sends *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(8).Children(func() { if ApiRequestBuilder(c, req, *opts).Send { *sends++ @@ -96,7 +98,7 @@ func TestApiRequestBuilderDisabledPreservesDefault(t *testing.T) { var req ApiRequest sends := 0 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { ApiRequestBuilder(c, &req, ApiRequestBuilderOptions{}) }) }, 600, 400) if req.Method != "" || sends != 0 { @@ -108,7 +110,7 @@ func TestApiRequestBuilderAncestorDisabledTabs(t *testing.T) { disabled := false request := ApiRequest{URL: "https://court.example", Params: []KeyValue{{Key: "page", Value: "1", Enabled: true}}, Headers: []KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { ApiRequestBuilder(c, &request, ApiRequestBuilderOptions{}) }) diff --git a/dashboard_filter_bar.go b/devtools/dashboard_filter_bar.go similarity index 51% rename from dashboard_filter_bar.go rename to devtools/dashboard_filter_bar.go index b998da5..a9ab959 100644 --- a/dashboard_filter_bar.go +++ b/devtools/dashboard_filter_bar.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -8,7 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // DashboardFilters holds filters; zero uses caller defaults. @@ -31,7 +33,7 @@ type DashboardFilterBarOptions struct { var dashboardRanges = []time.Duration{15 * time.Minute, time.Hour, 24 * time.Hour, 7 * 24 * time.Hour} // DashboardFilterBar edits time, environment and tags with clear. -func DashboardFilterBar(c *ui.Context, f *DashboardFilters, opts DashboardFilterBarOptions) ChoiceResult { +func DashboardFilterBar(c *ui.Context, f *DashboardFilters, opts DashboardFilterBarOptions) core.ChoiceResult { if f == nil { panic("mujica: DashboardFilterBar needs filters") } @@ -47,39 +49,39 @@ func DashboardFilterBar(c *ui.Context, f *DashboardFilters, opts DashboardFilter panic("mujica: DashboardFilterBar tag is not offered") } } - u := Density(c).Unit() - var r ChoiceResult + u := core.Density(c).Unit() + var r core.ChoiceResult r.Element = ui.Row(c).Shrink(0).Wrap().Gap(u * 3).AlignItems(ui.Center).Disabled(opts.Disabled). - Label(msg(c, "dashboard_filter_bar.label")) + Label(core.Msg(c, "dashboard_filter_bar.label")) locked := r.Element.IsDisabled() r.Element.Children(func() { - rs := []SelectOption[time.Duration]{{Value: 0, Label: msg(c, "dashboard_filter_bar.default_range")}} + rs := []input.SelectOption[time.Duration]{{Value: 0, Label: core.Msg(c, "dashboard_filter_bar.default_range")}} for _, d := range ranges { - rs = append(rs, SelectOption[time.Duration]{Value: d, Label: fmt.Sprintf(msg(c, "dashboard_filter_bar.last"), dashboardRangeLabel(c, d))}) + rs = append(rs, input.SelectOption[time.Duration]{Value: d, Label: fmt.Sprintf(core.Msg(c, "dashboard_filter_bar.last"), dashboardRangeLabel(c, d))}) } if f.Range != 0 && !slices.Contains(ranges, f.Range) { panic("mujica: DashboardFilterBar Range is not offered") } - r.changed = Select(c, &f.Range, rs, SelectOptions{Label: msg(c, "dashboard_filter_bar.range"), Disabled: locked}).Changed() - envs := []SelectOption[string]{{Value: "", Label: msg(c, "dashboard_filter_bar.all_envs")}} + r = core.NewChoiceResult(r.Element, input.Select(c, &f.Range, rs, input.SelectOptions{Label: core.Msg(c, "dashboard_filter_bar.range"), Disabled: locked}).Changed()) + envs := []input.SelectOption[string]{{Value: "", Label: core.Msg(c, "dashboard_filter_bar.all_envs")}} for _, e := range opts.Envs { - envs = append(envs, SelectOption[string]{Value: e, Label: e}) + envs = append(envs, input.SelectOption[string]{Value: e, Label: e}) } - r.changed = Select(c, &f.Env, envs, SelectOptions{Label: msg(c, "dashboard_filter_bar.env"), Disabled: locked}).Changed() || r.changed + r = core.NewChoiceResult(r.Element, input.Select(c, &f.Env, envs, input.SelectOptions{Label: core.Msg(c, "dashboard_filter_bar.env"), Disabled: locked}).Changed() || r.Changed()) if len(opts.Tags) > 0 { - tags := make([]SelectOption[string], len(opts.Tags)) + tags := make([]input.SelectOption[string], len(opts.Tags)) for i, t := range opts.Tags { - tags[i] = SelectOption[string]{Value: t, Label: t} + tags[i] = input.SelectOption[string]{Value: t, Label: t} } - r.changed = MultiSelect(c, &f.Tags, tags, MultiSelectOptions{Placeholder: msg(c, "dashboard_filter_bar.tags"), - Label: msg(c, "dashboard_filter_bar.tags"), Disabled: locked}).Changed() || r.changed + r = core.NewChoiceResult(r.Element, input.MultiSelect(c, &f.Tags, tags, input.MultiSelectOptions{Placeholder: core.Msg(c, "dashboard_filter_bar.tags"), + Label: core.Msg(c, "dashboard_filter_bar.tags"), Disabled: locked}).Changed() || r.Changed()) } empty := f.Range == 0 && f.Env == "" && len(f.Tags) == 0 - if Button(c, msg(c, "dashboard_filter_bar.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Disabled: locked || empty}).Clicked() && !locked { - *f, r.changed = DashboardFilters{}, true + if input.Button(c, core.Msg(c, "dashboard_filter_bar.clear"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("x"), Disabled: locked || empty}).Clicked() && !locked { + *f, r = DashboardFilters{}, core.NewChoiceResult(r.Element, true) } }) - if r.changed { + if r.Changed() { slog.Debug("mujica: dashboard_filter_bar change", "range", f.Range, "env", f.Env, "tags", len(f.Tags)) } return r @@ -90,11 +92,11 @@ func dashboardRangeLabel(c *ui.Context, d time.Duration) string { switch { case d <= 0: case d%(24*time.Hour) == 0: - return fmt.Sprintf(msg(c, "dashboard_filter_bar.days"), int(d/(24*time.Hour))) + return fmt.Sprintf(core.Msg(c, "dashboard_filter_bar.days"), int(d/(24*time.Hour))) case d%time.Hour == 0: - return fmt.Sprintf(msg(c, "dashboard_filter_bar.hours"), int(d/time.Hour)) + return fmt.Sprintf(core.Msg(c, "dashboard_filter_bar.hours"), int(d/time.Hour)) case d%time.Minute == 0: - return fmt.Sprintf(msg(c, "dashboard_filter_bar.minutes"), int(d/time.Minute)) + return fmt.Sprintf(core.Msg(c, "dashboard_filter_bar.minutes"), int(d/time.Minute)) } panic("mujica: DashboardFilterBar ranges must be positive whole minutes") } diff --git a/dashboard_filter_bar_test.go b/devtools/dashboard_filter_bar_test.go similarity index 95% rename from dashboard_filter_bar_test.go rename to devtools/dashboard_filter_bar_test.go index b87c7cd..90af7e5 100644 --- a/dashboard_filter_bar_test.go +++ b/devtools/dashboard_filter_bar_test.go @@ -1,15 +1,17 @@ -package mujica +package devtools import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func filterTester(f *DashboardFilters, opts DashboardFilterBarOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(8).Children(func() { if DashboardFilterBar(c, f, opts).Changed() { *changes++ @@ -76,7 +78,7 @@ func TestDashboardFilterBarKeyboardAndAncestorDisabled(t *testing.T) { var f DashboardFilters disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { DashboardFilterBar(c, &f, filterOpts) }) }, 900, 420) _ = tt.Click("Environment") diff --git a/devtools/devtools_parts.go b/devtools/devtools_parts.go new file mode 100644 index 0000000..4460b6a --- /dev/null +++ b/devtools/devtools_parts.go @@ -0,0 +1,32 @@ +package devtools + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// devtoolsAgo formats elapsed time using localized copy. +func devtoolsAgo(c *ui.Context, t time.Time) string { + d := max(c.Now().Sub(t), 0).Truncate(time.Second) + return fmt.Sprintf(core.Msg(c, "devtools.ago"), agent.AgentDuration(d)) +} + +// devtoolsSeverity shows severity with an icon and word. +func devtoolsSeverity(c *ui.Context, s core.Severity, word string) { + tone, icon := core.SeverityLook(core.Tokens(c), s) + ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { + ui.Icon(c, icon).Size(14, 14).TextColor(tone) + ui.Text(c, word).TextColor(tone).FontSize(core.FontSize(c, theme.CaptionSize+1)).FontWeight(600).SingleLine() + }) +} + +// devtoolsTitle is a serif panel title. +func devtoolsTitle(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600) +} diff --git a/devtools/doc.go b/devtools/doc.go new file mode 100644 index 0000000..4fb9a07 --- /dev/null +++ b/devtools/doc.go @@ -0,0 +1,2 @@ +// Package devtools holds MujicaUI dashboards and developer tools. +package devtools diff --git a/devtools/example_devtools_test.go b/devtools/example_devtools_test.go new file mode 100644 index 0000000..bec0283 --- /dev/null +++ b/devtools/example_devtools_test.go @@ -0,0 +1,221 @@ +package devtools_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/devtools" +) + +func ExampleStatCard() { + value := "12,480" + opts := devtools.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.StatCard(c, value, opts).Changed() { + fmt.Println("retry") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleAlertList() { + alerts := []devtools.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: devtools.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.AlertList(c, alerts, devtools.AlertListOptions{}) + if r.Ack != "" { + fmt.Println("ack", r.Ack) + } + if r.Silence != "" { + fmt.Println("silence", r.Silence) + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleIncidentCard() { + incident := devtools.Incident{Title: "API latency", Status: devtools.IncidentMonitoring, Impact: devtools.ImpactMinor, Updates: []devtools.IncidentUpdate{{Status: devtools.IncidentIdentified, Text: "Slow query located"}, {Status: devtools.IncidentMonitoring, Text: "Recovery observed"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + devtools.IncidentCard(c, incident) + } + ui.Render(view, 720, 480, 1) +} + +func ExampleUptimeBar() { + days := []devtools.UptimeDay{{Date: core.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: core.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.UptimeBar(c, days, devtools.UptimeBarOptions{Frame: chart.ChartFrame{Label: "API uptime"}}) + if r.Changed() { + fmt.Println("day", r.Point) + } + if r.Retried() { + fmt.Println("retry") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleServiceStatus() { + services := []devtools.ServiceItem{{Name: "API", Health: devtools.ServiceOperational}, {Name: "Search", Health: devtools.ServiceDegraded, Note: "Slow queries"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + devtools.ServiceStatus(c, services, devtools.ServiceStatusOptions{}) + } + ui.Render(view, 720, 480, 1) +} + +func ExampleLogStream() { + lines := []code.LogLine{{Level: code.LogInfo, Text: "server ready"}, {Level: code.LogWarn, Text: "slow query"}} + var state devtools.LogStreamState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.LogStream(c, lines, &state, devtools.LogStreamOptions{}).Changed() { + fmt.Println("filter changed") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleTraceWaterfall() { + spans := []devtools.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.TraceWaterfall(c, spans, devtools.TraceWaterfallOptions{}).Changed() { + fmt.Println("span selected") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleResourceGauge() { + readings := []devtools.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.ResourceGauge(c, readings, devtools.ResourceGaugeOptions{}).Changed() { + fmt.Println("retry") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleSystemMonitor() { + times := []time.Time{time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC), time.Date(2026, 10, 5, 9, 0, 5, 0, time.UTC)} + metrics := []devtools.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.SystemMonitor(c, times, metrics, devtools.SystemMonitorOptions{}).Changed() { + fmt.Println("retry") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleRefreshIntervalSelector() { + interval := 30 * time.Second + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.RefreshIntervalSelector(c, &interval, devtools.RefreshIntervalSelectorOptions{}) + if r.Refresh { + fmt.Println("refresh") + } + if r.Changed { + fmt.Println("interval", interval) + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleDashboardFilterBar() { + var filters devtools.DashboardFilters + opts := devtools.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.DashboardFilterBar(c, &filters, opts).Changed() { + fmt.Println("filters changed") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleJsonViewer() { + data := `{"server":{"port":8080},"ready":true}` + var state devtools.JsonViewerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.JsonViewer(c, data, &state, devtools.JsonViewerOptions{}) + if r.Copied != "" { + fmt.Println("path copied") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleJsonEditor() { + text := `{"port":8080,"ready":true}` + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.JsonEditor(c, &text, devtools.JsonEditorOptions{}) + if r.Changed { + fmt.Println("valid", r.Valid) + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleSchemaTree() { + roots := []devtools.SchemaNode{{Name: "app", Children: []devtools.SchemaNode{{Name: "users", Kind: devtools.SchemaTable, Children: []devtools.SchemaNode{{Name: "id", Kind: devtools.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: devtools.SchemaIndex, Detail: "(id)"}}}}}} + var state devtools.SchemaTreeState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.SchemaTree(c, &state, roots, devtools.SchemaTreeOptions{}).Changed() { + path, _ := state.Selected() + fmt.Println("selected", path) + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleApiRequestBuilder() { + request := devtools.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := devtools.ApiRequestBuilder(c, &request, devtools.ApiRequestBuilderOptions{}) + if r.Send { + fmt.Println("send", request.Method) + } + if r.Changed { + fmt.Println("request changed") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleResponseViewer() { + response := &devtools.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: `{"ready":true}`, Headers: []devtools.KeyValue{{Key: "Content-Type", Value: "application/json"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.ResponseViewer(c, response, devtools.ResponseViewerOptions{}).Changed() { + fmt.Println("retry") + } + } + ui.Render(view, 720, 480, 1) +} + +func ExampleKeyValueInput() { + rows := []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if devtools.KeyValueInput(c, &rows, devtools.KeyValueInputOptions{}).Changed() { + fmt.Println("rows", len(rows)) + } + } + ui.Render(view, 720, 480, 1) +} diff --git a/devtools/helpers_test.go b/devtools/helpers_test.go new file mode 100644 index 0000000..b80e4b0 --- /dev/null +++ b/devtools/helpers_test.go @@ -0,0 +1,44 @@ +package devtools + +import ( + "testing" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// overlayUntil draws frames until cond holds, for at most 5 s of wall time. +func overlayUntil(tt *ui.Tester, cond func() bool) bool { + end := time.Now().Add(5 * time.Second) + for !cond() { + if time.Now().After(end) { + return false + } + time.Sleep(2 * time.Millisecond) + tt.Frame() + } + return true +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/incident_card.go b/devtools/incident_card.go similarity index 66% rename from incident_card.go rename to devtools/incident_card.go index 4491f8f..5869495 100644 --- a/incident_card.go +++ b/devtools/incident_card.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IncidentStatus tracks investigation; zero means IncidentInvestigating. @@ -55,28 +57,28 @@ func IncidentCard(c *ui.Context, inc Incident) *ui.Element { if inc.Impact < ImpactNone || inc.Impact > ImpactCritical { panic(fmt.Sprintf("mujica: unknown IncidentImpact %d", int(inc.Impact))) } - k := Tokens(c) - u := Density(c).Unit() - sev := [...]Severity{SeverityInfo, SeverityWarning, SeverityError, SeverityError}[inc.Impact] - impact := msg(c, [...]string{"incident_card.none", "incident_card.minor", "incident_card.major", "incident_card.critical"}[inc.Impact]) - card := agentCard(c).Label(inc.Title + ", " + status) + k := core.Tokens(c) + u := core.Density(c).Unit() + sev := [...]core.Severity{core.SeverityInfo, core.SeverityWarning, core.SeverityError, core.SeverityError}[inc.Impact] + impact := core.Msg(c, [...]string{"incident_card.none", "incident_card.minor", "incident_card.major", "incident_card.critical"}[inc.Impact]) + card := agent.AgentCard(c).Label(inc.Title + ", " + status) card.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { devtoolsTitle(c, inc.Title).Grow(1).Shrink(1) if inc.Status == IncidentResolved { - devtoolsSeverity(c, SeveritySuccess, status) + devtoolsSeverity(c, core.SeveritySuccess, status) } else { - devtoolsSeverity(c, SeverityWarning, status) + devtoolsSeverity(c, core.SeverityWarning, status) } }) ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { - devtoolsSeverity(c, sev, fmt.Sprintf(msg(c, "incident_card.impact"), impact)) + devtoolsSeverity(c, sev, fmt.Sprintf(core.Msg(c, "incident_card.impact"), impact)) if !inc.Started.IsZero() { - agentCaption(c, fmt.Sprintf(msg(c, "incident_card.started"), inc.Started.Format("2006-01-02 15:04"))) + agent.AgentCaption(c, fmt.Sprintf(core.Msg(c, "incident_card.started"), inc.Started.Format("2006-01-02 15:04"))) } }) if len(inc.Updates) > 0 { - agentRows(c, len(inc.Updates), func(i int) { incidentUpdate(c, k, inc.Updates[i], i == len(inc.Updates)-1) }).Shrink(1).MaxHeight(320) + agent.AgentRows(c, len(inc.Updates), func(i int) { incidentUpdate(c, k, inc.Updates[i], i == len(inc.Updates)-1) }).Shrink(1).MaxHeight(320) } }) return card @@ -84,7 +86,7 @@ func IncidentCard(c *ui.Context, inc Incident) *ui.Element { // incidentUpdate draws one timeline entry on a diamond rail. func incidentUpdate(c *ui.Context, k theme.Tokens, up IncidentUpdate, last bool) { - u := Density(c).Unit() + u := core.Density(c).Unit() ui.Row(c).Gap(u * 3).AlignItems(ui.Stretch).Children(func() { ui.Box(c).Width(12).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { cx := internal.Snap(p, r.X+r.W/2) @@ -98,7 +100,7 @@ func incidentUpdate(c *ui.Context, k theme.Tokens, up IncidentUpdate, last bool) ui.Row(c).Gap(u * 2).Children(func() { ui.Text(c, incidentStatusWord(c, up.Status)).FontWeight(600) if !up.At.IsZero() { - agentCaption(c, up.At.Format("15:04")) + agent.AgentCaption(c, up.At.Format("15:04")) } }) ui.Text(c, up.Text).TextColor(k.TextMuted) @@ -110,5 +112,5 @@ func incidentStatusWord(c *ui.Context, s IncidentStatus) string { if s < IncidentInvestigating || s > IncidentResolved { panic(fmt.Sprintf("mujica: unknown IncidentStatus %d", int(s))) } - return msg(c, [...]string{"incident_card.investigating", "incident_card.identified", "incident_card.monitoring", "incident_card.resolved"}[s]) + return core.Msg(c, [...]string{"incident_card.investigating", "incident_card.identified", "incident_card.monitoring", "incident_card.resolved"}[s]) } diff --git a/incident_card_test.go b/devtools/incident_card_test.go similarity index 88% rename from incident_card_test.go rename to devtools/incident_card_test.go index f352a00..ac441f6 100644 --- a/incident_card_test.go +++ b/devtools/incident_card_test.go @@ -1,10 +1,12 @@ -package mujica +package devtools import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func incidentSample() Incident { @@ -20,7 +22,7 @@ func incidentSample() Incident { func TestIncidentCardShowsStatusImpactTimeline(t *testing.T) { inc := incidentSample() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IncidentCard(c, inc) }, 560, 500) for _, s := range []string{"Checkout errors in eu-west", "Monitoring", "Impact: major", "Started 2026-10-05 21:04", @@ -43,7 +45,7 @@ func TestIncidentCardRejectsUnknownStatus(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IncidentCard(c, Incident{Title: "x", Status: 9}) }, 200, 200) } @@ -59,7 +61,7 @@ func TestIncidentCardManyUpdates(t *testing.T) { for i := range inc.Updates { inc.Updates[i].Text = "Update received" } - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); IncidentCard(c, inc) }, 560, 500) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); IncidentCard(c, inc) }, 560, 500) if n := len(tt.Texts()); n > 500 { t.Fatalf("built %d texts for 100000 updates", n) } diff --git a/json_editor.go b/devtools/json_editor.go similarity index 76% rename from json_editor.go rename to devtools/json_editor.go index 0265b0b..4c89b68 100644 --- a/json_editor.go +++ b/devtools/json_editor.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "bytes" @@ -11,9 +11,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // JsonEditorOptions configure a JsonEditor. @@ -48,7 +51,7 @@ func JsonEditor(c *ui.Context, text *string, opts JsonEditorOptions) JsonEditorR } label := opts.Label if label == "" { - label = msg(c, "json_editor.label") + label = core.Msg(c, "json_editor.label") } minH, maxH := opts.MinHeight, opts.MaxHeight if minH == 0 { @@ -60,8 +63,8 @@ func JsonEditor(c *ui.Context, text *string, opts JsonEditorOptions) JsonEditorR if minH > maxH { panic("mujica: JsonEditor MinHeight exceeds MaxHeight") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r JsonEditorResult r.Element = ui.Column(c).Shrink(0).Gap(u * 2).Disabled(opts.Disabled) chk := ui.Local(r.Element, "mujica.jsoneditor", func() jsonEditorCheck { return jsonEditorCheck{src: "\x00"} }) @@ -70,7 +73,7 @@ func JsonEditor(c *ui.Context, text *string, opts JsonEditorOptions) JsonEditorR jsonEditorValidate(chk, *text) ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, label).FontWeight(600).Grow(1) - if Button(c, msg(c, "json_editor.format"), ButtonOptions{Variant: Secondary, Disabled: locked || chk.err != ""}).Clicked() && !locked { + if input.Button(c, core.Msg(c, "json_editor.format"), input.ButtonOptions{Variant: input.Secondary, Disabled: locked || chk.err != ""}).Clicked() && !locked { var b bytes.Buffer if json.Indent(&b, []byte(*text), "", " ") == nil && b.String() != *text { *text, r.Changed = b.String(), true @@ -78,8 +81,8 @@ func JsonEditor(c *ui.Context, text *string, opts JsonEditorOptions) JsonEditorR } } }) - in := TextArea(c, text, TextAreaOptions{Label: label, MinHeight: minH, MaxHeight: maxH, Disabled: locked, Error: chk.err}) - in.Input.Font(theme.MonoFont).FontSize(FontSize(c, codeSize)) + in := input.TextArea(c, text, input.TextAreaOptions{Label: label, MinHeight: minH, MaxHeight: maxH, Disabled: locked, Error: chk.err}) + in.Input.Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)) r.Changed = r.Changed || in.Changed() jsonEditorValidate(chk, *text) if chk.err != "" { @@ -91,7 +94,7 @@ func JsonEditor(c *ui.Context, text *string, opts JsonEditorOptions) JsonEditorR } ui.Row(c).Gap(u).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("circle-check")).Size(14, 14).TextColor(k.Success) - ui.Text(c, msg(c, "json_editor.valid")).TextColor(k.Success).FontSize(FontSize(c, theme.CaptionSize+1)) + ui.Text(c, core.Msg(c, "json_editor.valid")).TextColor(k.Success).FontSize(core.FontSize(c, theme.CaptionSize+1)) }) }) r.Valid, r.Line, r.Column = chk.err == "", chk.line, chk.column @@ -146,16 +149,16 @@ func jsonEditorValidate(chk *jsonEditorCheck, text string) { // jsonEditorExcerpt shows an error line and column caret. func jsonEditorExcerpt(c *ui.Context, line string, n, col int) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ui.Column(c).Padding(u*2, u*3).Radius(theme.ControlRadius).Background(k.Background).Border(theme.BorderWidth, k.Border). - Label(fmt.Sprintf(msg(c, "json_editor.at"), n, col)).Children(func() { + Label(fmt.Sprintf(core.Msg(c, "json_editor.at"), n, col)).Children(func() { spans := []ui.Span{{Text: fmt.Sprintf("%4d | ", n), Color: k.TextMuted}} toks, _ := internal.Highlight("json", line, false) for _, t := range toks { - spans = append(spans, ui.Span{Text: t.Text, Color: codeTokenColor(k, t.Kind)}) + spans = append(spans, ui.Span{Text: t.Text, Color: code.CodeTokenColor(k, t.Kind)}) } - ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine() - codeMono(c, strings.Repeat(" ", 7+col-1)+"^").TextColor(k.Danger) + ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)).SingleLine() + code.CodeMono(c, strings.Repeat(" ", 7+col-1)+"^").TextColor(k.Danger) }) } diff --git a/json_editor_test.go b/devtools/json_editor_test.go similarity index 96% rename from json_editor_test.go rename to devtools/json_editor_test.go index f4f0933..7699bb4 100644 --- a/json_editor_test.go +++ b/devtools/json_editor_test.go @@ -1,14 +1,16 @@ -package mujica +package devtools import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func jsonEditorTester(text *string, opts JsonEditorOptions, last *JsonEditorResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) *last = JsonEditor(c, text, opts) }, 600, 460) } @@ -99,7 +101,7 @@ func TestJsonEditorAncestorDisabledFormat(t *testing.T) { text := `{"a":1}` disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { JsonEditor(c, &text, JsonEditorOptions{}) }) }, 600, 460) tt.Key(0, ui.KeyTab) diff --git a/json_tree.go b/devtools/json_tree.go similarity index 99% rename from json_tree.go rename to devtools/json_tree.go index fd570f5..ace109d 100644 --- a/json_tree.go +++ b/devtools/json_tree.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "bytes" diff --git a/json_viewer.go b/devtools/json_viewer.go similarity index 77% rename from json_viewer.go rename to devtools/json_viewer.go index 5ac4d89..2f6fa68 100644 --- a/json_viewer.go +++ b/devtools/json_viewer.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -7,9 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // JsonViewerState is a JsonViewer's open nodes, search and choice. @@ -63,9 +66,9 @@ func JsonViewer(c *ui.Context, data string, s *JsonViewerState, opts JsonViewerO } } } - u := Density(c).Unit() + u := core.Density(c).Unit() var r JsonViewerResult - r.Element = codePanel(c).Height(h) + r.Element = code.CodePanel(c).Height(h) if r.Element.IsDisabled() { for _, key := range []ui.Key{ui.KeyDown, ui.KeyUp, ui.KeyHome, ui.KeyEnd, ui.KeyPageDown, ui.KeyPageUp} { r.Element.Shortcut(0, key) @@ -73,15 +76,15 @@ func JsonViewer(c *ui.Context, data string, s *JsonViewerState, opts JsonViewerO } r.Element.Children(func() { if s.err != nil { - ui.Row(c).Padding(u * 3).Gap(u * 2).TextColor(Tokens(c).Danger).Role(ui.RoleStatus).Children(func() { + ui.Row(c).Padding(u * 3).Gap(u * 2).TextColor(core.Tokens(c).Danger).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) - ui.Text(c, fmt.Sprintf(msg(c, "json_viewer.invalid"), s.err)) + ui.Text(c, fmt.Sprintf(core.Msg(c, "json_viewer.invalid"), s.err)) }) return } jsonViewerSearch(s) - codeHeader(c, func() { - moved := SearchInput(c, &s.Query, SearchInputOptions{Find: true, Matches: len(s.matches), Current: &s.Current}).Moved() + code.CodeHeader(c, func() { + moved := input.SearchInput(c, &s.Query, input.SearchInputOptions{Find: true, Matches: len(s.matches), Current: &s.Current}).Moved() if moved && len(s.matches) > 0 { s.sel = s.matches[s.Current] s.stale = true @@ -90,8 +93,8 @@ func JsonViewer(c *ui.Context, data string, s *JsonViewerState, opts JsonViewerO if s.sel >= 0 && s.sel < len(s.tree.nodes) { path = s.tree.nodes[s.sel].path } - codeMono(c, path).Grow(1).MinWidth(0).SingleLine() - if Button(c, msg(c, "json_viewer.copy"), ButtonOptions{Variant: Ghost, Icon: icons.Must("copy"), Disabled: path == ""}).Clicked() && path != "" { + code.CodeMono(c, path).Grow(1).MinWidth(0).SingleLine() + if input.Button(c, core.Msg(c, "json_viewer.copy"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("copy"), Disabled: path == ""}).Clicked() && path != "" { c.WriteClipboard(path) r.Copied = path slog.Debug("mujica: json_viewer copy path", "path", path) @@ -107,7 +110,7 @@ func JsonViewer(c *ui.Context, data string, s *JsonViewerState, opts JsonViewerO was := at s.list.Selected = &at s.list.Label = func(i int) string { return s.tree.nodes[s.rows[i]].label() } - list := codeRows(c, &s.list, len(s.rows), func(i int) { jsonViewerRow(c, s, s.rows[i]) }) + list := code.CodeRows(c, &s.list, len(s.rows), func(i int) { jsonViewerRow(c, s, s.rows[i]) }) if !list.IsDisabled() && at != was && at >= 0 { s.sel = s.rows[at] } @@ -174,7 +177,7 @@ func jsonViewerRows(s *JsonViewerState) { // jsonViewerRow shows disclosure, key and value. func jsonViewerRow(c *ui.Context, s *JsonViewerState, i int) { - k := Tokens(c) + k := core.Tokens(c) n := s.tree.nodes[i] ui.Box(c).Width(float32(n.depth) * 16).Shrink(0) mark := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Center().Shrink(0) @@ -191,7 +194,7 @@ func jsonViewerRow(c *ui.Context, s *JsonViewerState, i int) { } var spans []ui.Span if n.key != "" { - for _, sp := range codeMatchSpans(c, n.key, s.Query) { + for _, sp := range code.CodeMatchSpans(c, n.key, s.Query) { sp.Color, sp.Weight = k.AccentText, max(sp.Weight, 600) spans = append(spans, sp) } @@ -201,17 +204,17 @@ func jsonViewerRow(c *ui.Context, s *JsonViewerState, i int) { case n.container() && s.open[n.path]: spans = append(spans, ui.Span{Text: n.value[:1], Color: k.TextMuted}) case n.container(): - spans = append(spans, ui.Span{Text: fmt.Sprintf(msg(c, "json_viewer.items"), n.value[:1], n.count, n.value[1:]), Color: k.TextMuted}) + spans = append(spans, ui.Span{Text: fmt.Sprintf(core.Msg(c, "json_viewer.items"), n.value[:1], n.count, n.value[1:]), Color: k.TextMuted}) default: tok, _ := internal.Highlight("json", n.value, false) col := k.Text if len(tok) > 0 { - col = codeTokenColor(k, tok[0].Kind) + col = code.CodeTokenColor(k, tok[0].Kind) } - for _, sp := range codeMatchSpans(c, n.value, s.Query) { + for _, sp := range code.CodeMatchSpans(c, n.value, s.Query) { sp.Color = col spans = append(spans, sp) } } - ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine().Grow(1).MinWidth(0) + ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)).SingleLine().Grow(1).MinWidth(0) } diff --git a/json_viewer_test.go b/devtools/json_viewer_test.go similarity index 96% rename from json_viewer_test.go rename to devtools/json_viewer_test.go index 5f9fc68..6925269 100644 --- a/json_viewer_test.go +++ b/devtools/json_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,13 +6,15 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const jsonSample = `{"ball":{"hall":"east wing","guests":[{"name":"Sakiko","seat":1},{"name":"Uika","seat":2}],"wine":null},"open":true,"two words":3.5}` func jsonViewerTester(data *string, s *JsonViewerState, opts JsonViewerOptions, copied *[]string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := JsonViewer(c, *data, s, opts); r.Copied != "" { *copied = append(*copied, r.Copied) } @@ -124,7 +126,7 @@ func TestJsonViewerAncestorDisabledKeepsTreeAndClipboard(t *testing.T) { disabled := false copied := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if JsonViewer(c, data, &s, JsonViewerOptions{}).Copied != "" { copied++ @@ -147,7 +149,7 @@ func TestJsonViewerAncestorDisabledSelection(t *testing.T) { var state JsonViewerState disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { JsonViewer(c, `{"a":1,"b":2}`, &state, JsonViewerOptions{}) }) diff --git a/key_value_input.go b/devtools/key_value_input.go similarity index 57% rename from key_value_input.go rename to devtools/key_value_input.go index 3c94780..e5ead7b 100644 --- a/key_value_input.go +++ b/devtools/key_value_input.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // KeyValue holds an editable pair and enable flag. @@ -28,19 +31,19 @@ type KeyValueInputOptions struct { } // KeyValueInput edits virtualized pairs with enable, add and remove. -func KeyValueInput(c *ui.Context, rows *[]KeyValue, opts KeyValueInputOptions) ChoiceResult { +func KeyValueInput(c *ui.Context, rows *[]KeyValue, opts KeyValueInputOptions) core.ChoiceResult { if rows == nil || opts.MaxHeight < 0 { panic("mujica: KeyValueInput needs rows") } kp, vp := opts.KeyPlaceholder, opts.ValuePlaceholder if kp == "" { - kp = msg(c, "key_value_input.key") + kp = core.Msg(c, "key_value_input.key") } if vp == "" { - vp = msg(c, "key_value_input.value") + vp = core.Msg(c, "key_value_input.value") } - u := Density(c).Unit() - var r ChoiceResult + u := core.Density(c).Unit() + var r core.ChoiceResult r.Element = ui.Column(c).Shrink(0).Gap(u * 2).Disabled(opts.Disabled).Label(opts.Label) locked := r.Element.IsDisabled() if locked { @@ -53,35 +56,35 @@ func KeyValueInput(c *ui.Context, rows *[]KeyValue, opts KeyValueInputOptions) C } remove := -1 r.Element.Children(func() { - agentRows(c, len(*rows), func(i int) { + agent.AgentRows(c, len(*rows), func(i int) { kv := &(*rows)[i] ui.Row(c).Key(i).Gap(u * 2).AlignItems(ui.Center).Children(func() { - name := fmt.Sprintf(msg(c, "key_value_input.row"), i+1) + name := fmt.Sprintf(core.Msg(c, "key_value_input.row"), i+1) on := kv.Enabled - box := Checkbox(c, &on, "", CheckboxOptions{Disabled: locked}).Label(name) + box := input.Checkbox(c, &on, "", input.CheckboxOptions{Disabled: locked}).Label(name) if box.Changed() && !locked { - kv.Enabled, r.changed = on, true + kv.Enabled, r = on, core.NewChoiceResult(r.Element, true) } - k := TextInput(c, &kv.Key, TextInputOptions{Placeholder: kp, Label: name + " " + kp, Disabled: locked}) + k := input.TextInput(c, &kv.Key, input.TextInputOptions{Placeholder: kp, Label: name + " " + kp, Disabled: locked}) k.Element.Grow(1).Basis(0) - v := TextInput(c, &kv.Value, TextInputOptions{Placeholder: vp, Label: name + " " + vp, Disabled: locked}) + v := input.TextInput(c, &kv.Value, input.TextInputOptions{Placeholder: vp, Label: name + " " + vp, Disabled: locked}) v.Element.Grow(2).Basis(0) - r.changed = r.changed || k.Changed() || v.Changed() - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "key_value_input.remove"), i+1)).Disabled(locked).Clicked() && !locked { + r = core.NewChoiceResult(r.Element, r.Changed() || k.Changed() || v.Changed()) + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "key_value_input.remove"), i+1)).Disabled(locked).Clicked() && !locked { remove = i } }) }).Shrink(1).MaxHeight(maxH) - if Button(c, msg(c, "key_value_input.add"), ButtonOptions{Variant: Ghost, Icon: icons.Must("plus"), Disabled: locked}).AlignSelf(ui.Start).Clicked() && !locked { + if input.Button(c, core.Msg(c, "key_value_input.add"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("plus"), Disabled: locked}).AlignSelf(ui.Start).Clicked() && !locked { *rows = append(*rows, KeyValue{Enabled: true}) - r.changed = true + r = core.NewChoiceResult(r.Element, true) } }) if remove >= 0 { *rows = append((*rows)[:remove:remove], (*rows)[remove+1:]...) - r.changed = true + r = core.NewChoiceResult(r.Element, true) } - if r.changed { + if r.Changed() { slog.Debug("mujica: key_value_input change", "rows", len(*rows)) } return r diff --git a/key_value_input_test.go b/devtools/key_value_input_test.go similarity index 95% rename from key_value_input_test.go rename to devtools/key_value_input_test.go index 491cdc6..3c0d273 100644 --- a/key_value_input_test.go +++ b/devtools/key_value_input_test.go @@ -1,14 +1,16 @@ -package mujica +package devtools import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func kvTester(rows *[]KeyValue, opts KeyValueInputOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if KeyValueInput(c, rows, opts).Changed() { *changes++ } @@ -79,7 +81,7 @@ func TestKeyValueInputManyRows(t *testing.T) { func TestKeyValueInputAncestorDisabledAndEmpty(t *testing.T) { var rows []KeyValue tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { KeyValueInput(c, &rows, KeyValueInputOptions{}) }) }, 600, 300) _ = tt.Click("Add row") diff --git a/log_stream.go b/devtools/log_stream.go similarity index 52% rename from log_stream.go rename to devtools/log_stream.go index 77f725d..cc6faf1 100644 --- a/log_stream.go +++ b/devtools/log_stream.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,21 +6,25 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LogStreamState is a LogStream's pause, level filter and place. type LogStreamState struct { Paused bool // MinLevel hides lines below it. - MinLevel LogLevel + MinLevel code.LogLevel // frozen holds the paused line count; later lines wait. frozen int shown []int scanned int - min LogLevel + min code.LogLevel rows ui.ListState - src []LogLine + src []code.LogLine started bool } @@ -32,47 +36,47 @@ type LogStreamOptions struct { } // LogStream shows virtualized ANSI logs with pause and level filters. -func LogStream(c *ui.Context, lines []LogLine, s *LogStreamState, opts LogStreamOptions) ChoiceResult { +func LogStream(c *ui.Context, lines []code.LogLine, s *LogStreamState, opts LogStreamOptions) core.ChoiceResult { if s == nil { panic("mujica: LogStream needs a state") } - if opts.Height < 0 || s.MinLevel < LogDebug || s.MinLevel > LogError { + if opts.Height < 0 || s.MinLevel < code.LogDebug || s.MinLevel > code.LogError { panic("mujica: LogStream needs Height >= 0 and a known MinLevel") } h := opts.Height if h == 0 { h = 320 } - var r ChoiceResult - r.Element = codePanel(c).Disabled(opts.Disabled).Height(h) + var r core.ChoiceResult + r.Element = code.CodePanel(c).Disabled(opts.Disabled).Height(h) locked := r.Element.IsDisabled() if !s.started { s.frozen, s.started = len(lines), true } r.Element.Children(func() { - codeHeader(c, func() { + code.CodeHeader(c, func() { was := s.Paused - label := msg(c, "log_stream.pause") + label := core.Msg(c, "log_stream.pause") if s.Paused { - label = msg(c, "log_stream.resume") + label = core.Msg(c, "log_stream.resume") } - if Toggle(c, &s.Paused, label, ToggleOptions{Disabled: locked}).Changed() && !locked { - r.changed = true + if input.Toggle(c, &s.Paused, label, input.ToggleOptions{Disabled: locked}).Changed() && !locked { + r = core.NewChoiceResult(r.Element, true) } else { s.Paused = was } - levels := make([]Segment, 4) - for lv := LogDebug; lv <= LogError; lv++ { - levels[lv] = Segment{Label: logViewerName(c, lv)} + levels := make([]navigation.Segment, 4) + for lv := code.LogDebug; lv <= code.LogError; lv++ { + levels[lv] = navigation.Segment{Label: code.LogViewerName(c, lv)} } pick := int(s.MinLevel) - SegmentedControl(c, &pick, levels, SegmentedControlOptions{Label: msg(c, "log_stream.level"), Disabled: locked}) - if LogLevel(pick) != s.MinLevel && !locked { - s.MinLevel, r.changed = LogLevel(pick), true + navigation.SegmentedControl(c, &pick, levels, navigation.SegmentedControlOptions{Label: core.Msg(c, "log_stream.level"), Disabled: locked}) + if code.LogLevel(pick) != s.MinLevel && !locked { + s.MinLevel, r = code.LogLevel(pick), core.NewChoiceResult(r.Element, true) } ui.Box(c).Grow(1) if s.Paused && len(lines) > s.frozen { - codeCaption(c, fmt.Sprintf(msg(c, "log_stream.waiting"), len(lines)-s.frozen)).Role(ui.RoleStatus) + code.CodeCaption(c, fmt.Sprintf(core.Msg(c, "log_stream.waiting"), len(lines)-s.frozen)).Role(ui.RoleStatus) } }) if !s.Paused { @@ -80,28 +84,28 @@ func LogStream(c *ui.Context, lines []LogLine, s *LogStreamState, opts LogStream } logStreamFilter(lines[:min(s.frozen, len(lines))], s) if len(s.shown) == 0 { - codeCaption(c, msg(c, "log_stream.empty")).Padding(Density(c).Unit() * 4) + code.CodeCaption(c, core.Msg(c, "log_stream.empty")).Padding(core.Density(c).Unit() * 4) return } s.rows.FollowEnd = !s.Paused s.rows.Key = func(i int) any { return s.shown[i] } - d := Density(c) + d := core.Density(c) ui.List(c, &s.rows, len(s.shown), func(i int) { logStreamRow(c, lines[s.shown[i]]) }).Grow(1).Padding(d.Unit(), 0) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: log_stream change", "paused", s.Paused, "min_level", s.MinLevel) } return r } // logStreamFilter updates filtered indices when lines or filters change. -func logStreamFilter(lines []LogLine, s *LogStreamState) { +func logStreamFilter(lines []code.LogLine, s *LogStreamState) { if s.min != s.MinLevel || s.scanned > len(lines) || len(lines) > 0 && (len(s.src) == 0 || &lines[0] != &s.src[0]) { s.min, s.scanned, s.shown = s.MinLevel, 0, s.shown[:0] } for i := s.scanned; i < len(lines); i++ { lv := lines[i].Level - if lv < LogDebug || lv > LogError { + if lv < code.LogDebug || lv > code.LogError { panic("mujica: LogLine level out of range") } if lv >= s.MinLevel { @@ -112,16 +116,16 @@ func logStreamFilter(lines []LogLine, s *LogStreamState) { } // logStreamRow shows one line: time, level word and ANSI text. -func logStreamRow(c *ui.Context, l LogLine) { - k := Tokens(c) - d := Density(c) +func logStreamRow(c *ui.Context, l code.LogLine) { + k := core.Tokens(c) + d := core.Density(c) col := [...]ui.Color{k.TextMuted, k.Info, k.Warning, k.Danger}[l.Level] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { if !l.Time.IsZero() { - codeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0) + code.CodeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0) } - ui.Text(c, fmt.Sprintf("%-5s", logViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)). + ui.Text(c, fmt.Sprintf("%-5s", code.LogViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)). FontWeight(700).TextColor(col).SingleLine().Shrink(0) - ui.RichText(c, codeAnsiSpans(c, l.Text)...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).Grow(1).MinWidth(0) + ui.RichText(c, code.CodeAnsiSpans(c, l.Text)...).Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)).Grow(1).MinWidth(0) }) } diff --git a/log_stream_test.go b/devtools/log_stream_test.go similarity index 68% rename from log_stream_test.go rename to devtools/log_stream_test.go index f8f1adb..2613182 100644 --- a/log_stream_test.go +++ b/devtools/log_stream_test.go @@ -1,23 +1,26 @@ -package mujica +package devtools import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func logStreamLines(n int) []LogLine { - out := make([]LogLine, n) +func logStreamLines(n int) []code.LogLine { + out := make([]code.LogLine, n) for i := range out { - out[i] = LogLine{Level: LogLevel(i % 4), Text: fmt.Sprintf("\x1b[1mline\x1b[0m %d", i)} + out[i] = code.LogLine{Level: code.LogLevel(i % 4), Text: fmt.Sprintf("\x1b[1mline\x1b[0m %d", i)} } return out } -func logStreamTester(lines *[]LogLine, s *LogStreamState, opts LogStreamOptions, changes *int) *ui.Tester { +func logStreamTester(lines *[]code.LogLine, s *LogStreamState, opts LogStreamOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if LogStream(c, *lines, s, opts).Changed() { *changes++ } @@ -50,11 +53,11 @@ func TestLogStreamLevelFilter(t *testing.T) { n := 0 tt := logStreamTester(&lines, &s, LogStreamOptions{}, &n) _ = tt.Click("WARN") - if s.MinLevel != LogWarn || tt.HasText("line 5") || !tt.HasText("line 6") || !tt.HasText("line 7") { + if s.MinLevel != code.LogWarn || tt.HasText("line 5") || !tt.HasText("line 6") || !tt.HasText("line 7") { t.Fatalf("filter: %v %q", s.MinLevel, tt.Texts()) } tt.Key(0, ui.KeyRight) - if s.MinLevel != LogError || tt.HasText("line 6") { + if s.MinLevel != code.LogError || tt.HasText("line 6") { t.Fatalf("keyboard filter: %v", s.MinLevel) } } @@ -66,10 +69,10 @@ func TestLogStreamDisabledEmptyMany(t *testing.T) { tt := logStreamTester(&lines, &s, LogStreamOptions{Disabled: true}, &n) _ = tt.Click("Pause") _ = tt.Click("WARN") - if n != 0 || s.Paused || s.MinLevel != LogDebug { + if n != 0 || s.Paused || s.MinLevel != code.LogDebug { t.Fatalf("disabled changed: %+v", s) } - none := []LogLine{} + none := []code.LogLine{} tt = logStreamTester(&none, &LogStreamState{}, LogStreamOptions{}, &n) if !tt.HasText("No log lines") { t.Fatal("empty text missing") @@ -83,12 +86,12 @@ func TestLogStreamDisabledEmptyMany(t *testing.T) { } func TestLogStreamSnapshot(t *testing.T) { - lines := []LogLine{ - {Level: LogInfo, Text: "server listening on :8080"}, - {Level: LogDebug, Text: "cache warm: 312 keys"}, - {Level: LogWarn, Text: "slow query \x1b[33m412 ms\x1b[0m on guests"}, - {Level: LogError, Text: "\x1b[31mpayment provider unreachable\x1b[0m"}, - {Level: LogInfo, Text: "retry 1 of 3 succeeded"}, + lines := []code.LogLine{ + {Level: code.LogInfo, Text: "server listening on :8080"}, + {Level: code.LogDebug, Text: "cache warm: 312 keys"}, + {Level: code.LogWarn, Text: "slow query \x1b[33m412 ms\x1b[0m on guests"}, + {Level: code.LogError, Text: "\x1b[31mpayment provider unreachable\x1b[0m"}, + {Level: code.LogInfo, Text: "retry 1 of 3 succeeded"}, } snapshot(t, "log_stream", 640, 260, func(c *ui.Context) { var s LogStreamState @@ -97,31 +100,31 @@ func TestLogStreamSnapshot(t *testing.T) { } func TestLogStreamAncestorDisabledAndEmptyFilter(t *testing.T) { - lines := []LogLine{{Level: LogInfo, Text: "ready"}} + lines := []code.LogLine{{Level: code.LogInfo, Text: "ready"}} var s LogStreamState disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { LogStream(c, lines, &s, LogStreamOptions{}) }) }, 700, 400) _ = tt.Click("WARN") - if !tt.HasText("No log lines") || s.MinLevel != LogWarn { + if !tt.HasText("No log lines") || s.MinLevel != code.LogWarn { t.Fatal("empty level filter") } disabled = true tt.Key(0, ui.KeyRight) _ = tt.Click("Pause") - if s.MinLevel != LogWarn || s.Paused { + if s.MinLevel != code.LogWarn || s.Paused { t.Fatal("disabled stream changed") } } func TestLogStreamReplacedLevels(t *testing.T) { - lines := []LogLine{{Level: LogInfo, Text: "ready"}} - s := LogStreamState{MinLevel: LogWarn} + lines := []code.LogLine{{Level: code.LogInfo, Text: "ready"}} + s := LogStreamState{MinLevel: code.LogWarn} changes := 0 tt := logStreamTester(&lines, &s, LogStreamOptions{}, &changes) - lines = []LogLine{{Level: LogError, Text: "failed"}} + lines = []code.LogLine{{Level: code.LogError, Text: "failed"}} tt.Frame() if !tt.HasText("failed") || tt.HasText("No log lines") { t.Fatal("replaced log levels retained old filter") @@ -136,7 +139,7 @@ func TestLogStreamInitiallyPausedKeepsCurrentLines(t *testing.T) { if !tt.HasText("line 3") { t.Fatal("initial pause hid existing lines") } - lines = append(lines, LogLine{Level: LogInfo, Text: "later"}) + lines = append(lines, code.LogLine{Level: code.LogInfo, Text: "later"}) tt.Frame() if tt.HasText("later") || !tt.HasText("1 new lines") { t.Fatal("initial pause did not freeze lines") diff --git a/refresh_interval_selector.go b/devtools/refresh_interval_selector.go similarity index 67% rename from refresh_interval_selector.go rename to devtools/refresh_interval_selector.go index af7e12a..a4be6e6 100644 --- a/refresh_interval_selector.go +++ b/devtools/refresh_interval_selector.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // RefreshIntervalSelectorOptions configure a RefreshIntervalSelector. @@ -41,7 +43,7 @@ func RefreshIntervalSelector(c *ui.Context, interval *time.Duration, opts Refres if choices == nil { choices = refreshDefaults } - options := []SelectOption[time.Duration]{{Value: 0, Label: msg(c, "refresh_interval_selector.off")}} + options := []input.SelectOption[time.Duration]{{Value: 0, Label: core.Msg(c, "refresh_interval_selector.off")}} known := *interval == 0 seen := map[time.Duration]bool{} for _, d := range choices { @@ -50,29 +52,29 @@ func RefreshIntervalSelector(c *ui.Context, interval *time.Duration, opts Refres } seen[d] = true known = known || d == *interval - label := fmt.Sprintf(msg(c, "refresh_interval_selector.seconds"), int(d/time.Second)) + label := fmt.Sprintf(core.Msg(c, "refresh_interval_selector.seconds"), int(d/time.Second)) if d%time.Minute == 0 { - label = fmt.Sprintf(msg(c, "refresh_interval_selector.minutes"), int(d/time.Minute)) + label = fmt.Sprintf(core.Msg(c, "refresh_interval_selector.minutes"), int(d/time.Minute)) } - options = append(options, SelectOption[time.Duration]{Value: d, Label: label}) + options = append(options, input.SelectOption[time.Duration]{Value: d, Label: label}) } if !known { panic("mujica: RefreshIntervalSelector interval is not offered") } - u := Density(c).Unit() + u := core.Density(c).Unit() var r RefreshResult r.Element = ui.Row(c).Shrink(0).Gap(u * 2).AlignItems(ui.Center).Disabled(opts.Disabled) st := ui.Local(r.Element, "mujica.refresh", func() refreshState { return refreshState{last: c.Now()} }) locked := r.Element.IsDisabled() r.Element.Children(func() { - label := msg(c, "refresh_interval_selector.refresh") + label := core.Msg(c, "refresh_interval_selector.refresh") if opts.Refreshing { - Spinner(c, SpinnerOptions{Size: 14, Label: msg(c, "refresh_interval_selector.refreshing")}) + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: core.Msg(c, "refresh_interval_selector.refreshing")}) } - if Button(c, label, ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw"), Disabled: locked || opts.Refreshing}).Clicked() && !locked { + if input.Button(c, label, input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("refresh-cw"), Disabled: locked || opts.Refreshing}).Clicked() && !locked { r.Refresh = true } - r.Changed = Select(c, interval, options, SelectOptions{Label: msg(c, "refresh_interval_selector.label"), Disabled: locked}).Changed() + r.Changed = input.Select(c, interval, options, input.SelectOptions{Label: core.Msg(c, "refresh_interval_selector.label"), Disabled: locked}).Changed() }) if r.Changed || opts.Refreshing { st.last = c.Now() diff --git a/refresh_interval_selector_test.go b/devtools/refresh_interval_selector_test.go similarity index 96% rename from refresh_interval_selector_test.go rename to devtools/refresh_interval_selector_test.go index 1d642b8..34fb06c 100644 --- a/refresh_interval_selector_test.go +++ b/devtools/refresh_interval_selector_test.go @@ -1,15 +1,17 @@ -package mujica +package devtools import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func refreshTester(interval *time.Duration, opts *RefreshIntervalSelectorOptions, refreshes, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := RefreshIntervalSelector(c, interval, *opts) if r.Refresh { *refreshes++ @@ -90,7 +92,7 @@ func TestRefreshIntervalSelectorKeyboardAncestorDisable(t *testing.T) { disabled := false changes, refreshes := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := RefreshIntervalSelector(c, &interval, RefreshIntervalSelectorOptions{}) if r.Changed { diff --git a/resource_gauge.go b/devtools/resource_gauge.go similarity index 58% rename from resource_gauge.go rename to devtools/resource_gauge.go index f6fc08f..f6dbfb3 100644 --- a/resource_gauge.go +++ b/devtools/resource_gauge.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,6 +6,11 @@ import ( "math" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) // ResourceReading is one resource's current use. @@ -23,13 +28,13 @@ type ResourceGaugeOptions struct { Warn, Critical float64 // Height of each dial; zero means 150. Height float32 - Status DataStatus + Status data.DataStatus Error string } // ResourceGauge shows resource usage with named threshold bands. -func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGaugeOptions) ChoiceResult { - if opts.Status != DataReady && opts.Status != DataLoading && opts.Status != DataFailed { +func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGaugeOptions) core.ChoiceResult { + if opts.Status != data.DataReady && opts.Status != data.DataLoading && opts.Status != data.DataFailed { panic("mujica: ResourceGauge needs a known status") } warn, crit := opts.Warn, opts.Critical @@ -46,13 +51,13 @@ func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGauge if h == 0 { h = 150 } - u := Density(c).Unit() + u := core.Density(c).Unit() row := ui.Row(c).Wrap().Gap(u * 4).Shrink(0) - result := ChoiceResult{Element: row} + result := core.ChoiceResult{Element: row} row.Children(func() { if len(readings) == 0 { ui.Column(c).Height(h).Children(func() { - result.changed = dataPlaceholder(c, opts.Status, opts.Error, msg(c, "resource_gauge.empty"), "") + result = core.NewChoiceResult(result.Element, data.DataPlaceholder(c, opts.Status, opts.Error, core.Msg(c, "resource_gauge.empty"), "")) }) return } @@ -63,23 +68,23 @@ func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGauge frac := r.Used / r.Total ui.Column(c).Key(r.Name).Width(200).Gap(u).AlignItems(ui.Center).Children(func() { ui.Text(c, r.Name).FontWeight(600) - gauge := Gauge(c, frac*100, GaugeOptions{ - Frame: ChartFrame{Label: r.Name, Height: h, Status: opts.Status, Error: opts.Error}, Min: 0, Max: 100, Unit: "%", + gauge := chart.Gauge(c, frac*100, chart.GaugeOptions{ + Frame: chart.ChartFrame{Label: r.Name, Height: h, Status: opts.Status, Error: opts.Error}, Min: 0, Max: 100, Unit: "%", Format: func(v float64) string { return fmt.Sprintf("%.0f", v) }, - Segments: []GaugeSegment{ - {To: warn * 100, Label: msg(c, "resource_gauge.normal"), Tone: GaugeGood}, - {To: crit * 100, Label: msg(c, "resource_gauge.high"), Tone: GaugeWarning}, - {To: 100, Label: msg(c, "resource_gauge.critical"), Tone: GaugeDanger}, + Segments: []chart.GaugeSegment{ + {To: warn * 100, Label: core.Msg(c, "resource_gauge.normal"), Tone: chart.GaugeGood}, + {To: crit * 100, Label: core.Msg(c, "resource_gauge.high"), Tone: chart.GaugeWarning}, + {To: 100, Label: core.Msg(c, "resource_gauge.critical"), Tone: chart.GaugeDanger}, }, }) - result.changed = result.changed || gauge.Retried() + result = core.NewChoiceResult(result.Element, result.Changed() || gauge.Retried()) if r.Unit != "" { - agentCaption(c, fmt.Sprintf("%s / %s %s", sandboxNum(r.Used), sandboxNum(r.Total), r.Unit)) + agent.AgentCaption(c, fmt.Sprintf("%s / %s %s", agent.SandboxNum(r.Used), agent.SandboxNum(r.Total), r.Unit)) } }) } }) - if result.changed { + if result.Changed() { slog.Debug("mujica: resource_gauge retry") } return result diff --git a/resource_gauge_test.go b/devtools/resource_gauge_test.go similarity index 84% rename from resource_gauge_test.go rename to devtools/resource_gauge_test.go index 38d4501..9a822e7 100644 --- a/resource_gauge_test.go +++ b/devtools/resource_gauge_test.go @@ -1,10 +1,13 @@ -package mujica +package devtools import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) var resourceSample = []ResourceReading{ @@ -15,7 +18,7 @@ var resourceSample = []ResourceReading{ func TestResourceGaugeThresholdsAndKeyboard(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ResourceGauge(c, resourceSample, ResourceGaugeOptions{}) }, 700, 300) if !tt.HasText("6.4 / 8 GB") || !tt.HasText("470 / 500 GB") { @@ -45,7 +48,7 @@ func TestResourceGaugeRejectsBadReading(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ResourceGauge(c, []ResourceReading{{Name: "CPU", Used: 5, Total: 4}}, ResourceGaugeOptions{}) }, 200, 200) } @@ -63,23 +66,23 @@ func TestResourceGaugeRejectsInfiniteTotal(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ResourceGauge(c, []ResourceReading{{Name: "CPU", Total: math.Inf(1)}}, ResourceGaugeOptions{}) }, 400, 300) } func TestResourceGaugeEmptyState(t *testing.T) { - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ResourceGauge(c, nil, ResourceGaugeOptions{}) }, 600, 300) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); ResourceGauge(c, nil, ResourceGaugeOptions{}) }, 600, 300) if !tt.HasText("No resources") { t.Fatal("empty resource state") } } func TestResourceGaugeLoadingErrorAndDisabledRetry(t *testing.T) { - status, disabled := DataLoading, false + status, disabled := data.DataLoading, false retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if ResourceGauge(c, resourceSample[:1], ResourceGaugeOptions{Status: status, Error: "offline"}).Changed() { retries++ @@ -89,7 +92,7 @@ func TestResourceGaugeLoadingErrorAndDisabledRetry(t *testing.T) { if !tt.HasText("Loading") { t.Fatal("loading resource") } - status = DataFailed + status = data.DataFailed tt.Frame() _ = tt.Click("Retry") if retries != 1 || !tt.HasText("offline") { diff --git a/response_viewer.go b/devtools/response_viewer.go similarity index 59% rename from response_viewer.go rename to devtools/response_viewer.go index f5e728b..899c67b 100644 --- a/response_viewer.go +++ b/devtools/response_viewer.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "encoding/json" @@ -10,7 +10,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ApiResponse is an HTTP response the caller received. @@ -26,7 +31,7 @@ type ApiResponse struct { // ResponseViewerOptions configure a ResponseViewer. type ResponseViewerOptions struct { // Status replaces the response while loading or failed. - Status DataStatus + Status data.DataStatus Error string // Height of the body; zero means 320. Height float32 @@ -40,8 +45,8 @@ type responseViewerState struct { } // ResponseViewer shows response metadata, body and headers. -func ResponseViewer(c *ui.Context, resp *ApiResponse, opts ResponseViewerOptions) ChoiceResult { - if opts.Status != DataReady && opts.Status != DataLoading && opts.Status != DataFailed { +func ResponseViewer(c *ui.Context, resp *ApiResponse, opts ResponseViewerOptions) core.ChoiceResult { + if opts.Status != data.DataReady && opts.Status != data.DataLoading && opts.Status != data.DataFailed { panic("mujica: ResponseViewer needs a known status") } if opts.Height < 0 || resp != nil && (resp.Status < 100 || resp.Status > 599 || resp.Size < 0 || resp.Duration < 0) { @@ -51,45 +56,45 @@ func ResponseViewer(c *ui.Context, resp *ApiResponse, opts ResponseViewerOptions if h == 0 { h = 320 } - u := Density(c).Unit() - var r ChoiceResult - r.Element = ui.Column(c).Shrink(0).Gap(u * 3).Label(msg(c, "response_viewer.label")) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = ui.Column(c).Shrink(0).Gap(u * 3).Label(core.Msg(c, "response_viewer.label")) st := ui.Local(r.Element, "mujica.response", func() responseViewerState { return responseViewerState{} }) r.Element.Children(func() { - if opts.Status != DataReady || resp == nil { + if opts.Status != data.DataReady || resp == nil { ui.Column(c).Height(h).Children(func() { - r.changed = dataPlaceholder(c, opts.Status, opts.Error, msg(c, "response_viewer.none"), "") + r = core.NewChoiceResult(r.Element, data.DataPlaceholder(c, opts.Status, opts.Error, core.Msg(c, "response_viewer.none"), "")) }) return } ui.Row(c).Gap(u * 4).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() { responseStatus(c, resp.Status) - agentCaption(c, agentDuration(resp.Duration)) - agentCaption(c, processListBytes(uint64(resp.Size))) + agent.AgentCaption(c, agent.AgentDuration(resp.Duration)) + agent.AgentCaption(c, code.ProcessListBytes(uint64(resp.Size))) }) if st.body != resp.Body { st.body, st.isJSON = resp.Body, json.Valid([]byte(resp.Body)) } - tabs := []Tab{ - {Label: msg(c, "response_viewer.body"), Panel: func() { + tabs := []navigation.Tab{ + {Label: core.Msg(c, "response_viewer.body"), Panel: func() { ui.Column(c).Height(h).Children(func() { switch { case resp.Body == "": - agentCaption(c, msg(c, "response_viewer.empty")) + agent.AgentCaption(c, core.Msg(c, "response_viewer.empty")) case st.isJSON: JsonViewer(c, resp.Body, &st.json, JsonViewerOptions{Height: h}) default: - agentCode(c, "", resp.Body, h) + agent.AgentCode(c, "", resp.Body, h) } }) }}, - {Label: fmt.Sprintf(msg(c, "response_viewer.headers"), len(resp.Headers)), Panel: func() { + {Label: fmt.Sprintf(core.Msg(c, "response_viewer.headers"), len(resp.Headers)), Panel: func() { ui.Column(c).Height(h).Children(func() { responseHeaders(c, resp.Headers, h) }) }}, } - Tabs(c, &st.tab, tabs, TabsOptions{Label: msg(c, "response_viewer.label")}) + navigation.Tabs(c, &st.tab, tabs, navigation.TabsOptions{Label: core.Msg(c, "response_viewer.label")}) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: response_viewer retry") } return r @@ -97,14 +102,14 @@ func ResponseViewer(c *ui.Context, resp *ApiResponse, opts ResponseViewerOptions // responseStatus shows "200 OK" with a severity icon and tone. func responseStatus(c *ui.Context, code int) { - sev := SeveritySuccess + sev := core.SeveritySuccess switch { case code >= 500: - sev = SeverityError + sev = core.SeverityError case code >= 400: - sev = SeverityWarning + sev = core.SeverityWarning case code >= 300 || code < 200: - sev = SeverityInfo + sev = core.SeverityInfo } text := strings.TrimSpace(fmt.Sprintf("%d %s", code, http.StatusText(code))) devtoolsSeverity(c, sev, text) @@ -112,14 +117,14 @@ func responseStatus(c *ui.Context, code int) { // responseHeaders lists headers as name and value rows. func responseHeaders(c *ui.Context, headers []KeyValue, h float32) { - k := Tokens(c) + k := core.Tokens(c) if len(headers) == 0 { - agentCaption(c, msg(c, "response_viewer.no_headers")) + agent.AgentCaption(c, core.Msg(c, "response_viewer.no_headers")) return } - agentWellRows(c, len(headers), func(i int) { + agent.AgentWellRows(c, len(headers), func(i int) { kv := headers[i] - ui.Row(c).MinHeight(Density(c).ControlHeight()).Gap(Density(c).Unit() * 3).Children(func() { + ui.Row(c).MinHeight(core.Density(c).ControlHeight()).Gap(core.Density(c).Unit() * 3).Children(func() { ui.Text(c, kv.Key).TextColor(k.AccentText).FontWeight(600).Width(200).Shrink(0) ui.Text(c, kv.Value).Selectable().Grow(1).Shrink(1) }) diff --git a/response_viewer_test.go b/devtools/response_viewer_test.go similarity index 89% rename from response_viewer_test.go rename to devtools/response_viewer_test.go index 27daf29..5443f9a 100644 --- a/response_viewer_test.go +++ b/devtools/response_viewer_test.go @@ -1,10 +1,13 @@ -package mujica +package devtools import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func responseSample() *ApiResponse { @@ -15,7 +18,7 @@ func responseSample() *ApiResponse { func responseTester(resp **ApiResponse, opts *ResponseViewerOptions, retries *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ResponseViewer(c, *resp, *opts).Changed() { *retries++ } @@ -52,7 +55,7 @@ func TestResponseViewerStates(t *testing.T) { if !tt.HasText("Send a request to see its response") { t.Fatalf("none: %q", tt.Texts()) } - opts = ResponseViewerOptions{Status: DataFailed, Error: "connection refused"} + opts = ResponseViewerOptions{Status: data.DataFailed, Error: "connection refused"} tt.Frame() _ = tt.Click("Retry") if n != 1 || !tt.HasText("connection refused") { @@ -81,7 +84,7 @@ func TestResponseViewerKeyboardDisabledAndLoading(t *testing.T) { retries := 0 opts := ResponseViewerOptions{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if ResponseViewer(c, resp, opts).Changed() { retries++ @@ -95,12 +98,12 @@ func TestResponseViewerKeyboardDisabledAndLoading(t *testing.T) { if !tt.HasText("X-Trace") { t.Fatal("keyboard header selection") } - opts.Status = DataLoading + opts.Status = data.DataLoading tt.Frame() if !tt.HasText("Loading") || tt.HasText("X-Trace") { t.Fatal("loading state") } - opts.Status = DataFailed + opts.Status = data.DataFailed opts.Error = "offline" disabled = true tt.Frame() @@ -119,7 +122,7 @@ func TestResponseViewerRejectsUnknownDataStatus(t *testing.T) { }() resp := responseSample() n := 0 - responseTester(&resp, &ResponseViewerOptions{Status: DataStatus(99)}, &n) + responseTester(&resp, &ResponseViewerOptions{Status: data.DataStatus(99)}, &n) } func TestResponseViewerEmptyBodyAndHeaders(t *testing.T) { @@ -138,7 +141,7 @@ func TestResponseViewerEmptyBodyAndHeaders(t *testing.T) { func TestResponseViewerAncestorDisabledTabs(t *testing.T) { disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { ResponseViewer(c, responseSample(), ResponseViewerOptions{}) }) diff --git a/schema_tree.go b/devtools/schema_tree.go similarity index 80% rename from schema_tree.go rename to devtools/schema_tree.go index 3dfc50c..1a59768 100644 --- a/schema_tree.go +++ b/devtools/schema_tree.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SchemaKind classifies nodes; zero means SchemaDatabase. @@ -54,7 +57,7 @@ type SchemaTreeOptions struct { } // SchemaTree shows database objects in a virtualized keyboard tree. -func SchemaTree(c *ui.Context, s *SchemaTreeState, roots []SchemaNode, opts SchemaTreeOptions) ChoiceResult { +func SchemaTree(c *ui.Context, s *SchemaTreeState, roots []SchemaNode, opts SchemaTreeOptions) core.ChoiceResult { if s == nil || opts.Height < 0 { panic("mujica: SchemaTree needs a state and Height >= 0") } @@ -63,10 +66,10 @@ func SchemaTree(c *ui.Context, s *SchemaTreeState, roots []SchemaNode, opts Sche h = 360 } schemaIndex(s, roots) - k := Tokens(c) - d := Density(c) - var r ChoiceResult - r.Element = codePanel(c).Height(h).Label(msg(c, "schema_tree.label")) + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult + r.Element = code.CodePanel(c).Height(h).Label(core.Msg(c, "schema_tree.label")) locked := r.Element.IsDisabled() if locked { copy := *s @@ -82,32 +85,32 @@ func SchemaTree(c *ui.Context, s *SchemaTreeState, roots []SchemaNode, opts Sche } r.Element.Children(func() { if len(s.roots) == 0 { - codeCaption(c, msg(c, "schema_tree.empty")).Padding(d.Unit() * 4) + code.CodeCaption(c, core.Msg(c, "schema_tree.empty")).Padding(d.Unit() * 4) return } was := s.selected s.outline.List.Selected = &s.selected s.outline.List.Label = func(i int) string { return s.nodes[s.outline.Item(i)].Name } - restore := dataSelectTheme(c, theme.MinHitSize/4) + restore := data.DataSelectTheme(c, theme.MinHitSize/4) e := ui.Outline(c, &s.outline, s.roots, func(p string) []string { return s.kids[p] }, func(p string) { n := s.nodes[p] ui.Row(c).Grow(1).MinWidth(0).MinHeight(d.ControlHeight() - 2*d.Unit()).Gap(d.Unit() * 2).AlignItems(ui.Center). Label(fmt.Sprintf("%s %s %s", schemaWord(c, n.Kind), n.Name, n.Detail)).Children(func() { sel, _ := s.Selected() - codeSelectMark(c, sel == p) + code.CodeSelectMark(c, sel == p) letter, col := schemaBadge(k, n.Kind) - codeBadge(c, letter, col) + code.CodeBadge(c, letter, col) ui.Text(c, n.Name).SingleLine().Shrink(0) if n.Detail != "" { - codeMono(c, n.Detail).TextColor(k.TextMuted).MinWidth(0) + code.CodeMono(c, n.Detail).TextColor(k.TextMuted).MinWidth(0) } }) }).Grow(1) restore() internal.Ring(e, k.Focus, theme.ControlRadius) - r.changed = !locked && s.selected != was + r = core.NewChoiceResult(r.Element, !locked && s.selected != was) }) - if r.changed { + if r.Changed() { p, _ := s.Selected() slog.Debug("mujica: schema_tree select", "path", p) } @@ -168,5 +171,5 @@ func schemaBadge(k theme.Tokens, kind SchemaKind) (string, ui.Color) { } func schemaWord(c *ui.Context, kind SchemaKind) string { - return msg(c, [...]string{"schema_tree.database", "schema_tree.table", "schema_tree.view", "schema_tree.column", "schema_tree.index"}[kind]) + return core.Msg(c, [...]string{"schema_tree.database", "schema_tree.table", "schema_tree.view", "schema_tree.column", "schema_tree.index"}[kind]) } diff --git a/schema_tree_test.go b/devtools/schema_tree_test.go similarity index 96% rename from schema_tree_test.go rename to devtools/schema_tree_test.go index 0f55243..35e1250 100644 --- a/schema_tree_test.go +++ b/devtools/schema_tree_test.go @@ -1,10 +1,12 @@ -package mujica +package devtools import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var schemaSample = []SchemaNode{{Name: "court", Kind: SchemaDatabase, Children: []SchemaNode{ @@ -18,7 +20,7 @@ var schemaSample = []SchemaNode{{Name: "court", Kind: SchemaDatabase, Children: func schemaTester(s *SchemaTreeState, roots []SchemaNode, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if SchemaTree(c, s, roots, SchemaTreeOptions{}).Changed() { *changes++ } @@ -94,7 +96,7 @@ func TestSchemaTreeAncestorDisabled(t *testing.T) { disabled := false changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if SchemaTree(c, &s, schemaSample, SchemaTreeOptions{}).Changed() { changes++ @@ -131,7 +133,7 @@ func TestSchemaTreeClearsSelectionWhenEmpty(t *testing.T) { var state SchemaTreeState roots := schemaSample tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SchemaTree(c, &state, roots, SchemaTreeOptions{}) }, 520, 420) if err := tt.Click("court"); err != nil { diff --git a/service_status.go b/devtools/service_status.go similarity index 68% rename from service_status.go rename to devtools/service_status.go index 95a0454..208dbd6 100644 --- a/service_status.go +++ b/devtools/service_status.go @@ -1,11 +1,14 @@ -package mujica +package devtools import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ServiceHealth is a service's state. The zero value is ServiceOperational. @@ -36,9 +39,9 @@ func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOpti if opts.MaxHeight < 0 { panic("mujica: ServiceStatus MaxHeight is negative") } - col := ui.Column(c).Shrink(0).Gap(Density(c).Unit() * 2) + col := ui.Column(c).Shrink(0).Gap(core.Density(c).Unit() * 2) summaryState := ui.Local(col, "mujica.services.summary", func() serviceSummary { return serviceSummary{key: "unset"} }) - if key := chartID(services); key != summaryState.key { + if key := chart.ChartID(services); key != summaryState.key { worst, counts := ServiceOperational, [3]int{} for i, s := range services { if s.Name == "" || s.Health < ServiceOperational || s.Health > ServiceOutage { @@ -54,30 +57,30 @@ func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOpti if maxH == 0 { maxH = 360 } - k := Tokens(c) - u := Density(c).Unit() - summary := msg(c, [...]string{"service_status.all_ok", "service_status.some_degraded", "service_status.some_down"}[worst]) + k := core.Tokens(c) + u := core.Density(c).Unit() + summary := core.Msg(c, [...]string{"service_status.all_ok", "service_status.some_degraded", "service_status.some_down"}[worst]) col.Label(summary) col.Children(func() { ui.Row(c).Gap(u*3).AlignItems(ui.Center).Padding(u*3).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border). Background(k.Surface).Role(ui.RoleStatus).Label(summary).Children(func() { devtoolsSeverity(c, serviceSeverity(worst), summary) ui.Box(c).Grow(1) - agentCaption(c, fmt.Sprintf(msg(c, "service_status.counts"), counts[0], counts[1], counts[2])) + agent.AgentCaption(c, fmt.Sprintf(core.Msg(c, "service_status.counts"), counts[0], counts[1], counts[2])) }) if len(services) == 0 { - agentCaption(c, msg(c, "service_status.empty")) + agent.AgentCaption(c, core.Msg(c, "service_status.empty")) return } - agentRows(c, len(services), func(i int) { + agent.AgentRows(c, len(services), func(i int) { s := services[i] word := serviceWord(c, s.Health) - ui.Row(c).Key(i).Gap(u*3).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Padding(u, u*3). + ui.Row(c).Key(i).Gap(u*3).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Padding(u, u*3). Label(s.Name + ", " + word).Children(func() { ui.Column(c).Grow(1).Shrink(1).Children(func() { ui.Text(c, s.Name).SingleLine() if s.Note != "" { - agentCaption(c, s.Note) + agent.AgentCaption(c, s.Note) } }) devtoolsSeverity(c, serviceSeverity(s.Health), word) @@ -88,12 +91,12 @@ func ServiceStatus(c *ui.Context, services []ServiceItem, opts ServiceStatusOpti return col } -func serviceSeverity(h ServiceHealth) Severity { - return [...]Severity{SeveritySuccess, SeverityWarning, SeverityError}[h] +func serviceSeverity(h ServiceHealth) core.Severity { + return [...]core.Severity{core.SeveritySuccess, core.SeverityWarning, core.SeverityError}[h] } func serviceWord(c *ui.Context, h ServiceHealth) string { - return msg(c, [...]string{"service_status.operational", "service_status.degraded", "service_status.outage"}[h]) + return core.Msg(c, [...]string{"service_status.operational", "service_status.degraded", "service_status.outage"}[h]) } type serviceSummary struct { diff --git a/service_status_test.go b/devtools/service_status_test.go similarity index 91% rename from service_status_test.go rename to devtools/service_status_test.go index 14da4c5..77d7a57 100644 --- a/service_status_test.go +++ b/devtools/service_status_test.go @@ -1,10 +1,12 @@ -package mujica +package devtools import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var servicesSample = []ServiceItem{ @@ -16,7 +18,7 @@ var servicesSample = []ServiceItem{ func TestServiceStatusSummaryAndRows(t *testing.T) { services := servicesSample tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ServiceStatus(c, services, ServiceStatusOptions{}) }, 520, 400) for _, s := range []string{"Some services down", "1 up · 1 degraded · 1 down", "Degraded", "Outage", "Provider unreachable"} { @@ -42,7 +44,7 @@ func TestServiceStatusManyVirtualized(t *testing.T) { many[i] = ServiceItem{Name: fmt.Sprint("svc ", i)} } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ServiceStatus(c, many, ServiceStatusOptions{}) }, 520, 400) if n := len(tt.Texts()); n > 300 { @@ -63,7 +65,7 @@ func TestServiceStatusRejectsUnknownHealth(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ServiceStatus(c, []ServiceItem{{Name: "API", Health: 99}}, ServiceStatusOptions{}) }, 400, 300) } diff --git a/stat_card.go b/devtools/stat_card.go similarity index 56% rename from stat_card.go rename to devtools/stat_card.go index 7b9fcec..d69e7a5 100644 --- a/stat_card.go +++ b/devtools/stat_card.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -7,8 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StatCardOptions configure a StatCard. Label is required. @@ -21,55 +25,57 @@ type StatCardOptions struct { LowerIsBetter bool // Trend draws a sparkline; nil hides it. Trend []float64 - Status DataStatus + Status data.DataStatus Error string } // StatCard shows values, changes, trends, loading and errors. -func StatCard(c *ui.Context, value string, opts StatCardOptions) ChoiceResult { +func StatCard(c *ui.Context, value string, opts StatCardOptions) core.ChoiceResult { if opts.Label == "" { panic("mujica: StatCard needs a Label") } if math.IsNaN(opts.Change) || math.IsInf(opts.Change, 0) { panic("mujica: StatCard Change must be finite") } - if opts.Status != DataReady && opts.Status != DataLoading && opts.Status != DataFailed { + if opts.Status != data.DataReady && opts.Status != data.DataLoading && opts.Status != data.DataFailed { panic(fmt.Sprintf("mujica: StatCard Status %d is not ready, loading or failed", int(opts.Status))) } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = agentCard(c).MinHeight(FontSize(c, 116)).MinWidth(180).Label(opts.Label) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = agent.AgentCard(c).MinHeight(core.FontSize(c, 116)).MinWidth(180).Label(opts.Label) r.Element.Children(func() { - agentCaption(c, opts.Label) + agent.AgentCaption(c, opts.Label) switch opts.Status { - case DataLoading: + case data.DataLoading: ui.Column(c).Grow(1).Justify(ui.Center).Children(func() { - Spinner(c, SpinnerOptions{Label: msg(c, "list.loading"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "list.loading"), ShowLabel: true}) }) return - case DataFailed: + case data.DataFailed: ui.Column(c).Grow(1).Justify(ui.Center).Gap(u).Children(func() { ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) - ui.Text(c, dataError(c, opts.Error)).Role(ui.RoleStatus).FontSize(FontSize(c, theme.CaptionSize+1)) + ui.Text(c, data.DataError(c, opts.Error)).Role(ui.RoleStatus).FontSize(core.FontSize(c, theme.CaptionSize+1)) + }) + ui.Row(c).Children(func() { + r = core.NewChoiceResult(r.Element, agent.AgentToggle(c, core.Msg(c, "list.retry"), "refresh-cw", false)) }) - ui.Row(c).Children(func() { r.changed = agentToggle(c, msg(c, "list.retry"), "refresh-cw", false) }) }) return } - ui.Text(c, value).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).FontWeight(600).FontFeatures("tnum").SingleLine() + ui.Text(c, value).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).FontWeight(600).FontFeatures("tnum").SingleLine() ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { if opts.HasChange { statChange(c, opts.Change, opts.LowerIsBetter) } ui.Box(c).Grow(1) if len(opts.Trend) > 0 { - Sparkline(c, opts.Trend, SparklineOptions{Frame: ChartFrame{Label: opts.Label}, Width: 88}) + chart.Sparkline(c, opts.Trend, chart.SparklineOptions{Frame: chart.ChartFrame{Label: opts.Label}, Width: 88}) } }) }) - if r.changed { + if r.Changed() { slog.Debug("mujica: stat_card retry", "label", opts.Label) } return r @@ -77,7 +83,7 @@ func StatCard(c *ui.Context, value string, opts StatCardOptions) ChoiceResult { // statChange shows a signed percentage and direction arrow. func statChange(c *ui.Context, change float64, lowerIsBetter bool) { - k := Tokens(c) + k := core.Tokens(c) icon, tone, sign := "minus", k.TextMuted, "" switch { case change > 0: @@ -93,8 +99,8 @@ func statChange(c *ui.Context, change float64, lowerIsBetter bool) { } } text := fmt.Sprintf("%s%.1f%%", sign, change*100) - ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Label(fmt.Sprintf(msg(c, "stat_card.change"), text)).Children(func() { + ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Label(fmt.Sprintf(core.Msg(c, "stat_card.change"), text)).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(tone) - ui.Text(c, text).TextColor(tone).FontSize(FontSize(c, theme.CaptionSize+1)).FontFeatures("tnum") + ui.Text(c, text).TextColor(tone).FontSize(core.FontSize(c, theme.CaptionSize+1)).FontFeatures("tnum") }) } diff --git a/stat_card_test.go b/devtools/stat_card_test.go similarity index 82% rename from stat_card_test.go rename to devtools/stat_card_test.go index 6d83a8a..d56f149 100644 --- a/stat_card_test.go +++ b/devtools/stat_card_test.go @@ -1,15 +1,18 @@ -package mujica +package devtools import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestStatCardValueChangeTrend(t *testing.T) { opts := StatCardOptions{Label: "Requests", Change: 0.124, HasChange: true, Trend: []float64{3, 5, 4, 8, 9}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatCard(c, "12,480", opts) }, 400, 200) for _, s := range []string{"Requests", "12,480", "+12.4%"} { @@ -30,10 +33,10 @@ func TestStatCardValueChangeTrend(t *testing.T) { } func TestStatCardLoadingErrorSameSize(t *testing.T) { - status, retries := DataReady, 0 + status, retries := data.DataReady, 0 var h []float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { r := StatCard(c, "42 ms", StatCardOptions{Label: "Latency", Status: status, Error: "timeout"}) h = append(h, r.Element.Bounds().H) @@ -44,13 +47,13 @@ func TestStatCardLoadingErrorSameSize(t *testing.T) { }, 400, 300) tt.Frame() ready := h[len(h)-1] - status = DataLoading + status = data.DataLoading tt.Frame() tt.Frame() if !tt.HasText("Loading") || h[len(h)-1] != ready { t.Fatalf("loading: %q height %v vs %v", tt.Texts(), h[len(h)-1], ready) } - status = DataFailed + status = data.DataFailed tt.Frame() tt.Frame() if !tt.HasText("timeout") || h[len(h)-1] != ready { @@ -67,17 +70,17 @@ func TestStatCardSnapshot(t *testing.T) { ui.Row(c).Padding(16).Gap(12).Children(func() { StatCard(c, "12,480", StatCardOptions{Label: "Requests", Change: 0.124, HasChange: true, Trend: []float64{3, 5, 4, 8, 6, 9}}) StatCard(c, "182 ms", StatCardOptions{Label: "p95 latency", Change: 0.08, HasChange: true, LowerIsBetter: true, Trend: []float64{120, 140, 130, 182}}) - StatCard(c, "", StatCardOptions{Label: "Errors", Status: DataFailed, Error: "Source unreachable"}) + StatCard(c, "", StatCardOptions{Label: "Errors", Status: data.DataFailed, Error: "Source unreachable"}) }) }) } func TestStatCardDisabledRetryAndKeyboardTrend(t *testing.T) { - status := DataReady + status := data.DataReady disabled := false retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if StatCard(c, "42", StatCardOptions{Label: "CPU", Trend: []float64{1, 4, 2}, Status: status, Error: "offline"}).Changed() { retries++ @@ -93,7 +96,7 @@ func TestStatCardDisabledRetryAndKeyboardTrend(t *testing.T) { if !announced || !tt.HasText("2") { t.Fatalf("trend boundary readout: %q", tt.Texts()) } - status, disabled = DataFailed, true + status, disabled = data.DataFailed, true tt.Frame() _ = tt.Click("Retry") tt.Key(0, ui.KeyEnter) @@ -103,7 +106,7 @@ func TestStatCardDisabledRetryAndKeyboardTrend(t *testing.T) { } func TestStatCardTextScaleKeepsCardHeight(t *testing.T) { - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); StatCard(c, "42", StatCardOptions{Label: "CPU"}) }, 600, 350) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); StatCard(c, "42", StatCardOptions{Label: "CPU"}) }, 600, 350) tt.SetPreferences(ui.Preferences{TextScale: 2, ReduceMotion: true}) bounds, ok := tt.Find("CPU") if !ok || bounds.H < 232 { diff --git a/devtools/system_monitor.go b/devtools/system_monitor.go new file mode 100644 index 0000000..22f69ba --- /dev/null +++ b/devtools/system_monitor.go @@ -0,0 +1,87 @@ +package devtools + +import ( + "fmt" + "log/slog" + "math" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// MonitorMetric holds samples aligned with the shared timestamps. +type MonitorMetric struct { + Name string + Unit string + Samples []float64 +} + +// SystemMonitorOptions configure a SystemMonitor. +type SystemMonitorOptions struct { + // Height of each chart; zero means 110. + Height float32 + Status data.DataStatus + Error string +} + +// SystemMonitor combines resource charts sampled by the caller. +func SystemMonitor(c *ui.Context, times []time.Time, metrics []MonitorMetric, opts SystemMonitorOptions) core.ChoiceResult { + if opts.Height < 0 || opts.Status != data.DataReady && opts.Status != data.DataLoading && opts.Status != data.DataFailed { + panic("mujica: SystemMonitor Height is negative") + } + for i, m := range metrics { + if m.Name == "" || len(m.Samples) != len(times) { + panic(fmt.Sprintf("mujica: SystemMonitor metric %d needs a Name and one sample per time", i)) + } + } + h := opts.Height + if h == 0 { + h = 110 + } + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = ui.Column(c).Shrink(0).Gap(u * 3) + r.Element.Children(func() { + if len(metrics) == 0 { + ui.Column(c).Height(h).Children(func() { + r = core.NewChoiceResult(r.Element, data.DataPlaceholder(c, opts.Status, opts.Error, core.Msg(c, "system_monitor.empty"), "")) + }) + return + } + for _, m := range metrics { + panel := ui.Column(c).Key(m.Name).Gap(u) + series := ui.Local(panel, "mujica.monitor.series", func() []chart.ChartSeries { return []chart.ChartSeries{{Name: m.Name, Values: m.Samples}} }) + (*series)[0].Name, (*series)[0].Values = m.Name, m.Samples + panel.Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { + ui.Text(c, m.Name).FontWeight(600).Grow(1) + last := math.NaN() + if n := len(m.Samples); n > 0 { + last = m.Samples[n-1] + } + if !math.IsNaN(last) && opts.Status == data.DataReady { + ui.Text(c, fmt.Sprintf("%s %s", agent.SandboxNum(last), m.Unit)).FontFeatures("tnum"). + FontSize(core.FontSize(c, theme.CaptionSize+1)).TextColor(k.Text).Role(ui.RoleStatus) + } + }) + unit := m.Unit + res := chart.LineChart(c, *series, chart.LineChartOptions{ + Frame: chart.ChartFrame{Label: m.Name, Height: h, Status: opts.Status, Error: opts.Error}, + Times: times, Format: func(v float64) string { return agent.SandboxNum(v) + " " + unit }, + }) + r = core.NewChoiceResult(r.Element, r.Changed() || res.Retried()) + }) + } + }) + if r.Changed() { + slog.Debug("mujica: system_monitor retry") + } + return r +} diff --git a/system_monitor_test.go b/devtools/system_monitor_test.go similarity index 85% rename from system_monitor_test.go rename to devtools/system_monitor_test.go index 3ad5339..0ebfb5a 100644 --- a/system_monitor_test.go +++ b/devtools/system_monitor_test.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "math" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func monitorSample(n int) ([]time.Time, []MonitorMetric) { @@ -24,7 +27,7 @@ func monitorSample(n int) ([]time.Time, []MonitorMetric) { func TestSystemMonitorLiveAndKeyboard(t *testing.T) { times, metrics := monitorSample(20) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SystemMonitor(c, times, metrics, SystemMonitorOptions{}) }, 600, 420) if !tt.HasText("3.6 GB") || !tt.HasText("20.0 %") { @@ -51,8 +54,8 @@ func TestSystemMonitorStatesAndRules(t *testing.T) { times, metrics := monitorSample(5) retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if SystemMonitor(c, times, metrics[:1], SystemMonitorOptions{Status: DataFailed, Error: "agent offline"}).Changed() { + core.Use(c, core.Settings{}) + if SystemMonitor(c, times, metrics[:1], SystemMonitorOptions{Status: data.DataFailed, Error: "agent offline"}).Changed() { retries++ } }, 600, 300) @@ -69,7 +72,7 @@ func TestSystemMonitorStatesAndRules(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SystemMonitor(c, times, []MonitorMetric{{Name: "x", Samples: []float64{1}}}, SystemMonitorOptions{}) }, 200, 200) } @@ -82,10 +85,10 @@ func TestSystemMonitorSnapshot(t *testing.T) { } func TestSystemMonitorEmptyLoadingAndDisabled(t *testing.T) { - status := DataReady + status := data.DataReady retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { if SystemMonitor(c, nil, nil, SystemMonitorOptions{Status: status, Error: "offline"}).Changed() { retries++ @@ -95,12 +98,12 @@ func TestSystemMonitorEmptyLoadingAndDisabled(t *testing.T) { if !tt.HasText("No resources") { t.Fatal("empty resources") } - status = DataLoading + status = data.DataLoading tt.Frame() if !tt.HasText("Loading") { t.Fatal("loading resources") } - status = DataFailed + status = data.DataFailed tt.Frame() _ = tt.Click("Retry") if retries != 0 { @@ -110,7 +113,10 @@ func TestSystemMonitorEmptyLoadingAndDisabled(t *testing.T) { func TestSystemMonitorManySamples(t *testing.T) { times, metrics := monitorSample(100000) - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); SystemMonitor(c, times, metrics[:1], SystemMonitorOptions{}) }, 600, 420) + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + SystemMonitor(c, times, metrics[:1], SystemMonitorOptions{}) + }, 600, 420) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnd) diff --git a/trace_waterfall.go b/devtools/trace_waterfall.go similarity index 77% rename from trace_waterfall.go rename to devtools/trace_waterfall.go index 82986a4..124b518 100644 --- a/trace_waterfall.go +++ b/devtools/trace_waterfall.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "cmp" @@ -9,9 +9,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WaterfallSpan holds timing; empty Parent marks a root. @@ -45,7 +48,7 @@ type traceState struct { } // TraceWaterfall shows timed span hierarchies and selected details. -func TraceWaterfall(c *ui.Context, spans []WaterfallSpan, opts TraceWaterfallOptions) ChoiceResult { +func TraceWaterfall(c *ui.Context, spans []WaterfallSpan, opts TraceWaterfallOptions) core.ChoiceResult { if opts.Height < 0 { panic("mujica: TraceWaterfall Height is negative") } @@ -53,25 +56,25 @@ func TraceWaterfall(c *ui.Context, spans []WaterfallSpan, opts TraceWaterfallOpt if h == 0 { h = 320 } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = codePanel(c).Label(msg(c, "trace_waterfall.label")) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = code.CodePanel(c).Label(core.Msg(c, "trace_waterfall.label")) st := ui.Local(r.Element, "mujica.trace", func() traceState { return traceState{} }) traceLayout(st, spans) r.Element.Children(func() { if len(st.rows) == 0 { - codeCaption(c, msg(c, "trace_waterfall.empty")).Padding(u * 4) + code.CodeCaption(c, core.Msg(c, "trace_waterfall.empty")).Padding(u * 4) return } - codeHeader(c, func() { - codeCaption(c, fmt.Sprintf(msg(c, "trace_waterfall.summary"), len(spans), agentDuration(st.total))).Grow(1) + code.CodeHeader(c, func() { + code.CodeCaption(c, fmt.Sprintf(core.Msg(c, "trace_waterfall.summary"), len(spans), agent.AgentDuration(st.total))).Grow(1) }) - agentRows(c, len(st.rows), func(i int) { + agent.AgentRows(c, len(st.rows), func(i int) { row := st.rows[i] s := spans[row.span] if traceSpanRow(c, s, row.depth, st.total, st.services[s.Service], s.ID == st.selected) { - st.selected, r.changed = s.ID, true + st.selected, r = s.ID, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: trace_waterfall select", "span", s.ID) } }).Height(h).FocusGroup(ui.Vertical) @@ -80,7 +83,7 @@ func TraceWaterfall(c *ui.Context, spans []WaterfallSpan, opts TraceWaterfallOpt ui.Column(c).Shrink(0).Gap(u).Padding(u*2, u*3).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border). Role(ui.RoleStatus).Children(func() { ui.Text(c, s.Name).FontWeight(600) - codeCaption(c, fmt.Sprintf(msg(c, "trace_waterfall.detail"), s.Service, agentDuration(s.Start), agentDuration(s.Duration))) + code.CodeCaption(c, fmt.Sprintf(core.Msg(c, "trace_waterfall.detail"), s.Service, agent.AgentDuration(s.Start), agent.AgentDuration(s.Duration))) }) } } @@ -136,11 +139,11 @@ func traceLayout(st *traceState, spans []WaterfallSpan) { // traceSpanRow shows span indentation, timing, selection and duration. func traceSpanRow(c *ui.Context, s WaterfallSpan, depth int, total time.Duration, slot int, selected bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() row := ui.ButtonBase(c).Key(s.ID).Justify(ui.Start).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2). - Label(fmt.Sprintf("%s, %s, %s", s.Name, s.Service, agentDuration(s.Duration))) + Label(fmt.Sprintf("%s, %s, %s", s.Name, s.Service, agent.AgentDuration(s.Duration))) internal.Ring(row, k.Focus, theme.ControlRadius) if selected { row.Background(k.Selection).DrawOver(func(p *ui.Painter, r ui.Rect) { @@ -157,9 +160,9 @@ func traceSpanRow(c *ui.Context, s WaterfallSpan, depth int, total time.Duration row.Children(func() { ui.Row(c).Width(220).Shrink(0).Gap(u).Padding(0, 0, 0, float32(depth)*u*4).AlignItems(ui.Center).Children(func() { if s.Error { - ui.Icon(c, icons.Must("circle-x")).Size(12, 12).TextColor(k.Danger).Label(msg(c, "trace_waterfall.error")) + ui.Icon(c, icons.Must("circle-x")).Size(12, 12).TextColor(k.Danger).Label(core.Msg(c, "trace_waterfall.error")) } - ui.Text(c, s.Name).FontSize(FontSize(c, theme.CaptionSize+1)).SingleLine().Shrink(1) + ui.Text(c, s.Name).FontSize(core.FontSize(c, theme.CaptionSize+1)).SingleLine().Shrink(1) }) ui.Box(c).Grow(1).Height(14).Draw(func(p *ui.Painter, r ui.Rect) { span := max(float64(total), 1) @@ -173,7 +176,7 @@ func traceSpanRow(c *ui.Context, s WaterfallSpan, depth int, total time.Duration } p.Fill(bar, col, 2) }) - ui.Text(c, agentDuration(s.Duration)).Width(64).TextAlign(ui.End).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, agent.AgentDuration(s.Duration)).Width(64).TextAlign(ui.End).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).FontFeatures("tnum").Shrink(0) }) return !row.IsDisabled() && row.Clicked() diff --git a/trace_waterfall_test.go b/devtools/trace_waterfall_test.go similarity index 96% rename from trace_waterfall_test.go rename to devtools/trace_waterfall_test.go index f154c79..b542672 100644 --- a/trace_waterfall_test.go +++ b/devtools/trace_waterfall_test.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var waterfallSample = []WaterfallSpan{ @@ -17,7 +19,7 @@ var waterfallSample = []WaterfallSpan{ func traceTester(spans []WaterfallSpan, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TraceWaterfall(c, spans, TraceWaterfallOptions{}).Changed() { *changes++ } @@ -81,7 +83,7 @@ func TestTraceWaterfallAncestorDisabled(t *testing.T) { disabled := false changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if TraceWaterfall(c, waterfallSample, TraceWaterfallOptions{}).Changed() { changes++ diff --git a/uptime_bar.go b/devtools/uptime_bar.go similarity index 83% rename from uptime_bar.go rename to devtools/uptime_bar.go index cfd74f2..ba55693 100644 --- a/uptime_bar.go +++ b/devtools/uptime_bar.go @@ -1,4 +1,4 @@ -package mujica +package devtools import ( "fmt" @@ -6,12 +6,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // UptimeDay holds availability; NaN marks missing data. type UptimeDay struct { - Date Date + Date core.Date Uptime float64 // Note says what happened, as "12 min outage". Note string @@ -19,13 +21,13 @@ type UptimeDay struct { // UptimeBarOptions configure an UptimeBar; Frame.Height 0 means 36. type UptimeBarOptions struct { - Frame ChartFrame + Frame chart.ChartFrame // Degraded and Down default to 0.999 and 0.95. Degraded, Down float64 } // UptimeBar shows daily availability with pointer and keyboard readouts. -func UptimeBar(c *ui.Context, days []UptimeDay, opts UptimeBarOptions) ChartResult { +func UptimeBar(c *ui.Context, days []UptimeDay, opts UptimeBarOptions) chart.ChartResult { deg, down := opts.Degraded, opts.Down if deg == 0 { deg = 0.999 @@ -40,17 +42,17 @@ func UptimeBar(c *ui.Context, days []UptimeDay, opts UptimeBarOptions) ChartResu if opts.Frame.Height == 0 { opts.Frame.Height = 36 } - k := Tokens(c) - ch := newChart(c, opts.Frame) + k := core.Tokens(c) + ch := chart.NewChart(c, opts.Frame) n := len(days) - cache := ui.Local(ch.res.Element, "mujica.uptime", func() uptimeCache { return uptimeCache{} }) - if cache.key != chartID(days) { + cache := ui.Local(ch.Res.Element, "mujica.uptime", func() uptimeCache { return uptimeCache{} }) + if cache.key != chart.ChartID(days) { for i, d := range days { if !math.IsNaN(d.Uptime) && (d.Uptime < 0 || d.Uptime > 1) { panic(fmt.Sprintf("mujica: UptimeBar day %d outside 0..1", i)) } } - cache.key, cache.width = chartID(days), -1 + cache.key, cache.width = chart.ChartID(days), -1 } band := func(r ui.Rect) internal.Band { return internal.Band{N: n, R0: r.X, R1: r.X + r.W, Pad: 0.25} } level := func(v float64) int { // 0 ok, 1 degraded, 2 down, 3 no data @@ -65,8 +67,8 @@ func UptimeBar(c *ui.Context, days []UptimeDay, opts UptimeBarOptions) ChartResu return 0 } pal := internal.Palette(k) - words := [...]string{msg(c, "uptime_bar.ok"), msg(c, "uptime_bar.degraded"), msg(c, "uptime_bar.down"), msg(c, "uptime_bar.nodata")} - res, _ := ch.build(ChartSpec{ + words := [...]string{core.Msg(c, "uptime_bar.ok"), core.Msg(c, "uptime_bar.degraded"), core.Msg(c, "uptime_bar.down"), core.Msg(c, "uptime_bar.nodata")} + res, _ := ch.Build(chart.ChartSpec{ Marks: n, Draw: func(p *ui.Painter, r ui.Rect, active int) { b := band(r) diff --git a/uptime_bar_test.go b/devtools/uptime_bar_test.go similarity index 77% rename from uptime_bar_test.go rename to devtools/uptime_bar_test.go index 6382516..013a824 100644 --- a/uptime_bar_test.go +++ b/devtools/uptime_bar_test.go @@ -1,15 +1,19 @@ -package mujica +package devtools import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func uptimeSample(n int) []UptimeDay { days := make([]UptimeDay, n) - start := Date{Year: 2026, Month: 9, Day: 1} + start := core.Date{Year: 2026, Month: 9, Day: 1} for i := range days { days[i] = UptimeDay{Date: start.AddDays(i), Uptime: 1} } @@ -19,12 +23,12 @@ func uptimeSample(n int) []UptimeDay { return days } -func uptimeTester(days []UptimeDay, frame ChartFrame, points *[]int) *ui.Tester { +func uptimeTester(days []UptimeDay, frame chart.ChartFrame, points *[]int) *ui.Tester { if frame.Label == "" { frame.Label = "API uptime" } return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { r := UptimeBar(c, days, UptimeBarOptions{Frame: frame}) *points = append(*points, r.Point) @@ -34,7 +38,7 @@ func uptimeTester(days []UptimeDay, frame ChartFrame, points *[]int) *ui.Tester func TestUptimeBarKeyboardReadout(t *testing.T) { var pts []int - tt := uptimeTester(uptimeSample(30), ChartFrame{}, &pts) + tt := uptimeTester(uptimeSample(30), chart.ChartFrame{}, &pts) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyRight) for range 7 { @@ -52,7 +56,7 @@ func TestUptimeBarKeyboardReadout(t *testing.T) { func TestUptimeBarHover(t *testing.T) { var pts []int - tt := uptimeTester(uptimeSample(30), ChartFrame{}, &pts) + tt := uptimeTester(uptimeSample(30), chart.ChartFrame{}, &pts) // The 4th of 30 bars across 500 DIPs starting at x = 10. tt.Move(10+500*3.5/30, 30) if pts[len(pts)-1] != 3 || !tt.HasText("2026-09-04 · Degraded · 99.75% · slow search") { @@ -62,11 +66,11 @@ func TestUptimeBarHover(t *testing.T) { func TestUptimeBarStatesAndRules(t *testing.T) { var pts []int - tt := uptimeTester(nil, ChartFrame{}, &pts) + tt := uptimeTester(nil, chart.ChartFrame{}, &pts) if !tt.HasText("No data") { t.Fatalf("empty: %q", tt.Texts()) } - tt = uptimeTester(uptimeSample(12), ChartFrame{Status: DataLoading}, &pts) + tt = uptimeTester(uptimeSample(12), chart.ChartFrame{Status: data.DataLoading}, &pts) if !tt.HasText("Loading") { t.Fatalf("loading: %q", tt.Texts()) } @@ -75,13 +79,13 @@ func TestUptimeBarStatesAndRules(t *testing.T) { t.Fatal("uptime above 1 did not panic") } }() - uptimeTester([]UptimeDay{{Uptime: 1.2}}, ChartFrame{}, &pts) + uptimeTester([]UptimeDay{{Uptime: 1.2}}, chart.ChartFrame{}, &pts) } func TestUptimeBarSnapshot(t *testing.T) { snapshot(t, "uptime_bar", 520, 110, func(c *ui.Context) { ui.Column(c).Padding(10).Children(func() { - UptimeBar(c, uptimeSample(45), UptimeBarOptions{Frame: ChartFrame{Label: "API uptime"}}) + UptimeBar(c, uptimeSample(45), UptimeBarOptions{Frame: chart.ChartFrame{Label: "API uptime"}}) }) }) } @@ -89,7 +93,7 @@ func TestUptimeBarSnapshot(t *testing.T) { func TestUptimeBarManyDaysBounded(t *testing.T) { days := uptimeSample(100000) var pts []int - tt := uptimeTester(days, ChartFrame{}, &pts) + tt := uptimeTester(days, chart.ChartFrame{}, &pts) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnd) if pts[len(pts)-1] != 99999 { diff --git a/devtools_parts.go b/devtools_parts.go deleted file mode 100644 index 47439c7..0000000 --- a/devtools_parts.go +++ /dev/null @@ -1,30 +0,0 @@ -package mujica - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - "mujicaui/theme" -) - -// devtoolsAgo formats elapsed time using localized copy. -func devtoolsAgo(c *ui.Context, t time.Time) string { - d := max(c.Now().Sub(t), 0).Truncate(time.Second) - return fmt.Sprintf(msg(c, "devtools.ago"), agentDuration(d)) -} - -// devtoolsSeverity shows severity with an icon and word. -func devtoolsSeverity(c *ui.Context, s Severity, word string) { - tone, icon := severityLook(Tokens(c), s) - ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Shrink(0).Children(func() { - ui.Icon(c, icon).Size(14, 14).TextColor(tone) - ui.Text(c, word).TextColor(tone).FontSize(FontSize(c, theme.CaptionSize+1)).FontWeight(600).SingleLine() - }) -} - -// devtoolsTitle is a serif panel title. -func devtoolsTitle(c *ui.Context, s string) *ui.Element { - return ui.Text(c, s).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600) -} diff --git a/avatar.go b/display/avatar.go similarity index 91% rename from avatar.go rename to display/avatar.go index 80c0082..9b13520 100644 --- a/avatar.go +++ b/display/avatar.go @@ -1,11 +1,12 @@ -package mujica +package display import ( "unicode" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AvatarOptions configure an Avatar. @@ -25,10 +26,10 @@ func Avatar(c *ui.Context, name string, opts AvatarOptions) *ui.Element { if size == 0 { size = 32 } - k := Tokens(c) + k := core.Tokens(c) label := name if label == "" { - label = msg(c, "avatar.unnamed") + label = core.Msg(c, "avatar.unnamed") } e := ui.Box(c).Size(size, size).Radius(size / 2).Clip().Shrink(0).Center().Role(ui.RoleImage).Label(label) initials := avatarInitials(name) diff --git a/avatar_group.go b/display/avatar_group.go similarity index 91% rename from avatar_group.go rename to display/avatar_group.go index 42d5f24..c791ac5 100644 --- a/avatar_group.go +++ b/display/avatar_group.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AvatarItem is one person of an AvatarGroup. @@ -51,7 +52,7 @@ func AvatarGroup(c *ui.Context, items []AvatarItem, opts AvatarGroupOptions) Ava if size == 0 { size = 32 } - k := Tokens(c) + k := core.Tokens(c) shown := min(limit, len(items)) ring := float32(2) row := ui.Row(c).AlignItems(ui.Center).Role(ui.RoleGroup) @@ -88,7 +89,7 @@ func avatarGroupMore(c *ui.Context, k theme.Tokens, open *bool, size, ring float var b *ui.Element ui.Box(c).Key("more").Margin(0, 0, 0, -size/4).Shrink(0).Children(func() { b = ui.CollapsibleBase(c, open).Trigger.Size(hit, hit).Center().Radius(hit / 2). - Label(fmt.Sprintf(msg(c, "avatar_group.more"), total)) + Label(fmt.Sprintf(core.Msg(c, "avatar_group.more"), total)) internal.Ring(b, k.Focus, hit/2) bg := k.Surface if b.Pressed() { @@ -100,7 +101,7 @@ func avatarGroupMore(c *ui.Context, k theme.Tokens, open *bool, size, ring float ui.Box(c).Padding(ring).Radius(size/2 + ring).Background(k.Background).Children(func() { disc := ui.Box(c).Size(size, size).Center().Radius(size/2).Background(bg). Border(theme.BorderWidth, k.ControlBorder).TextColor(k.Text) - disc.Transition(colorFade) + disc.Transition(core.ColorFade) disc.Children(func() { ui.Text(c, fmt.Sprintf("+%d", rest)).FontSize(size * 0.36).FontWeight(600).SingleLine() }) @@ -111,10 +112,10 @@ func avatarGroupMore(c *ui.Context, k theme.Tokens, open *bool, size, ring float } func avatarGroupList(c *ui.Context, k theme.Tokens, panel *ui.Element, items []AvatarItem) { - u := Density(c).Unit() + u := core.Density(c).Unit() panel.Margin(u, 0, 0, 0).Padding(u*2).Radius(theme.PopupRadius).Background(k.Surface). Border(theme.BorderWidth, k.Border).MinWidth(200) - if d := Motion(c, theme.PopupDuration); d > 0 { + if d := core.Motion(c, theme.PopupDuration); d > 0 { panel.Transition(ui.ElementTransition{Duration: d, Enter: &ui.Motion{Y: -4}}) } ui.Scroll(c).MaxHeight(240).Children(func() { diff --git a/avatar_group_test.go b/display/avatar_group_test.go similarity index 92% rename from avatar_group_test.go rename to display/avatar_group_test.go index d6e7dc0..9e594bb 100644 --- a/avatar_group_test.go +++ b/display/avatar_group_test.go @@ -1,9 +1,11 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var avatarGroupPeople = []AvatarItem{ @@ -13,7 +15,7 @@ var avatarGroupPeople = []AvatarItem{ func TestAvatarGroupMaxAndRest(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 3}) }, 400, 300) tt.Frame() @@ -27,7 +29,7 @@ func TestAvatarGroupMaxAndRest(t *testing.T) { func TestAvatarGroupNoOverflowNoButton(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AvatarGroup(c, avatarGroupPeople[:2], AvatarGroupOptions{}) }, 400, 300) tt.Frame() @@ -39,7 +41,7 @@ func TestAvatarGroupNoOverflowNoButton(t *testing.T) { func TestAvatarGroupExpandByKeyboard(t *testing.T) { var toggles []bool tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if v := AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 2}); v.Toggled { toggles = append(toggles, v.Open) } @@ -65,7 +67,7 @@ func TestAvatarGroupExpandByKeyboard(t *testing.T) { func TestAvatarGroupExpandByClick(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 4}) }, 400, 400) if err := tt.Click("+2"); err != nil { @@ -91,7 +93,7 @@ func TestAvatarGroupSnapshot(t *testing.T) { func TestAvatarGroupSmallButtonHitArea(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { AvatarGroup(c, avatarGroupPeople, AvatarGroupOptions{Max: 2, Size: 20}) }) diff --git a/avatar_test.go b/display/avatar_test.go similarity index 94% rename from avatar_test.go rename to display/avatar_test.go index ff44c7d..6fb3d7a 100644 --- a/avatar_test.go +++ b/display/avatar_test.go @@ -1,9 +1,11 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestAvatarInitials(t *testing.T) { @@ -24,7 +26,7 @@ func TestAvatarInitials(t *testing.T) { func TestAvatarFallbacks(t *testing.T) { var size ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { size = Avatar(c, "Sakiko Togawa", AvatarOptions{Size: 48}).Bounds() Avatar(c, "", AvatarOptions{}) diff --git a/badge.go b/display/badge.go similarity index 89% rename from badge.go rename to display/badge.go index fea4b2a..c1562b8 100644 --- a/badge.go +++ b/display/badge.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BadgeTone is a badge's emphasis. The zero value is BadgeAccent. @@ -92,19 +93,19 @@ func badgeText(opts BadgeOptions) (shown, full string) { } func badgePill(c *ui.Context, opts BadgeOptions, shown, full string) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) bg, fg, border := badgeColors(k, opts.Tone) label := full if opts.Label != "" { label = opts.Label + " " + full } - p := ui.Box(c).Row().Center().MinWidth(FontSize(c, 18)).MinHeight(FontSize(c, 18)).Padding(0, 5).Radius(theme.CardRadius). + p := ui.Box(c).Row().Center().MinWidth(core.FontSize(c, 18)).MinHeight(core.FontSize(c, 18)).Padding(0, 5).Radius(theme.CardRadius). Background(bg).Shrink(0).Role(ui.RoleStatus).Label(label) if border.A > 0 { p.Border(theme.BorderWidth, border) } p.Children(func() { - ui.Text(c, shown).FontSize(FontSize(c, theme.CaptionSize-1)).FontWeight(600).TextColor(fg). + ui.Text(c, shown).FontSize(core.FontSize(c, theme.CaptionSize-1)).FontWeight(600).TextColor(fg). FontFeatures("tnum").SingleLine() }) return p diff --git a/badge_test.go b/display/badge_test.go similarity index 94% rename from badge_test.go rename to display/badge_test.go index fc9029d..1bd23cd 100644 --- a/badge_test.go +++ b/display/badge_test.go @@ -1,9 +1,11 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBadgeText(t *testing.T) { @@ -27,7 +29,7 @@ func TestBadgeText(t *testing.T) { func TestBadgeReadsFullValue(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Badge(c, BadgeOptions{Count: 1204, Label: "Unread"}, nil) }, 300, 100) tt.Frame() @@ -46,7 +48,7 @@ func TestBadgeCornerPosition(t *testing.T) { }{{BadgeTopRight, true, false}, {BadgeTopLeft, false, false}, {BadgeBottomRight, true, true}, {BadgeBottomLeft, false, true}} { var host ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(40).Children(func() { Badge(c, BadgeOptions{Count: 3, Position: tc.pos}, func() { host = ui.Box(c).Size(60, 60).Bounds() @@ -89,7 +91,7 @@ func TestBadgeBadOptionsPanic(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) build(c) }, 100, 100) tt.Frame() @@ -117,7 +119,7 @@ func TestBadgeTextScale(t *testing.T) { var w [2]float32 for i, s := range []float32{1, 2} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { w[i] = Badge(c, BadgeOptions{Text: "Draft"}, nil).Bounds().W }) diff --git a/display/doc.go b/display/doc.go new file mode 100644 index 0000000..181c3d2 --- /dev/null +++ b/display/doc.go @@ -0,0 +1,2 @@ +// Package display holds MujicaUI text, headings, icons, images, avatars and other read-only display. +package display diff --git a/display/example_display_test.go b/display/example_display_test.go new file mode 100644 index 0000000..8cd8708 --- /dev/null +++ b/display/example_display_test.go @@ -0,0 +1,137 @@ +package display_test + +import ( + "errors" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" +) + +func ExampleText() { + name := "Coronation.md" + saveErr := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + display.Text(c, "Order ID: 4821-AX", display.TextOptions{Selectable: true}) + display.Text(c, "A long note that stops after two lines.", display.TextOptions{MaxLines: 2}) + if display.EditableText(c, &name, display.EditableTextOptions{Error: saveErr}).Changed() { + saveErr = "" // save name here; set saveErr on failure + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHeading() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + display.Heading(c, "The Grand Hall", display.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) + display.Heading(c, "Guest Ledger", display.HeadingOptions{Level: 2}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleIcon() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Row(c).Gap(8).Children(func() { + display.Icon(c, "star", display.IconOptions{}) + display.Icon(c, "circle-check", display.IconOptions{Size: 20, Tone: display.IconSuccess, Label: "Done"}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImage() { + var photo *ui.Bitmap // decode once with ui.DecodeBitmap, outside the view + loading, loadErr := false, errors.New("timeout") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + img := display.Image(c, photo, display.ImageOptions{ + Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", + Loading: loading, Err: loadErr, + }) + if img.Retry { + loading, loadErr = true, nil // start loading again here + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAvatar() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Row(c).Gap(8).Children(func() { + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{Size: 40}) + display.Avatar(c, "", display.AvatarOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAvatarGroup() { + people := []display.AvatarItem{ + {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, + {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + display.AvatarGroup(c, people, display.AvatarGroupOptions{Max: 3}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBadge() { + unread := 120 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + display.Badge(c, display.BadgeOptions{Text: "New", Tone: display.BadgeNeutral}, nil) + display.Badge(c, display.BadgeOptions{Count: unread, Label: "Unread", Position: display.BadgeTopRight}, func() { + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTag() { + tags := []string{"Baroque", "Gothic"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Row(c).Gap(8).Children(func() { + for i, t := range tags { + var v display.TagView + ui.Box(c).Key(t).Children(func() { + v = display.Tag(c, t, display.TagOptions{Icon: "star", Clickable: true, Closable: true}) + }) + if v.Closed { + tags = append(tags[:i:i], tags[i+1:]...) + } + } + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLink() { + details := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + display.Link(c, "Read the charter", display.LinkOptions{URL: "https://example.com/charter"}) + if display.Link(c, "Show details", display.LinkOptions{}).Clicked() { + details = !details + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleKbd() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Row(c).Gap(6).Children(func() { + display.Text(c, "Command palette", display.TextOptions{}) + display.Kbd(c, "Mod", "Shift", "P") + }) + } + ui.Render(view, 400, 300, 1) +} diff --git a/heading.go b/display/heading.go similarity index 70% rename from heading.go rename to display/heading.go index 87a6ba1..ccb14e0 100644 --- a/heading.go +++ b/display/heading.go @@ -1,10 +1,11 @@ -package mujica +package display import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // HeadingOptions configure a Heading; zero is level 1. @@ -19,15 +20,15 @@ type HeadingOptions struct { // Heading shows a serif title scaled with desktop text. func Heading(c *ui.Context, title string, opts HeadingOptions) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) size := headingSize(opts.Level) - u := Density(c).Unit() + u := core.Density(c).Unit() col := ui.Column(c).Gap(u).Shrink(0) col.Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, size)).FontWeight(600). + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, size)).FontWeight(600). LineHeight(1.25).TextColor(k.Text) if opts.Subtitle != "" { - ui.Text(c, opts.Subtitle).FontSize(FontSize(c, theme.BodySize)).LineHeight(theme.LineHeight).TextColor(k.TextMuted) + ui.Text(c, opts.Subtitle).FontSize(core.FontSize(c, theme.BodySize)).LineHeight(theme.LineHeight).TextColor(k.TextMuted) } if opts.Divider { ui.Box(c).FillWidth().Height(9).Margin(u, 0, 0, 0).Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/heading_test.go b/display/heading_test.go similarity index 93% rename from heading_test.go rename to display/heading_test.go index 0121972..6416b8d 100644 --- a/heading_test.go +++ b/display/heading_test.go @@ -1,15 +1,17 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func headingHeights(t *testing.T, p ui.Preferences) (h1, h2, h3 float32) { t.Helper() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { h1 = Heading(c, "Court", HeadingOptions{}).Bounds().H h2 = Heading(c, "Court", HeadingOptions{Level: 2}).Bounds().H @@ -39,7 +41,7 @@ func TestHeadingTextScale(t *testing.T) { func TestHeadingSubtitleAndDivider(t *testing.T) { var plain, full float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { plain = Heading(c, "Archive", HeadingOptions{Level: 2}).Bounds().H full = Heading(c, "Ledger", HeadingOptions{Level: 2, Subtitle: "Entries since 1802", Divider: true}).Bounds().H @@ -61,7 +63,7 @@ func TestHeadingBadLevelPanics(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Heading(c, "x", HeadingOptions{Level: 4}) }, 100, 100) tt.Frame() diff --git a/display/helpers_test.go b/display/helpers_test.go new file mode 100644 index 0000000..23acbdb --- /dev/null +++ b/display/helpers_test.go @@ -0,0 +1,24 @@ +package display + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/icon.go b/display/icon.go similarity index 90% rename from icon.go rename to display/icon.go index 6db5450..76bfcf0 100644 --- a/icon.go +++ b/display/icon.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IconTone is an icon's semantic color; zero inherits text color. @@ -46,7 +47,7 @@ func Icon(c *ui.Context, name string, opts IconOptions) *ui.Element { if size == 0 { size = 16 } - k := Tokens(c) + k := core.Tokens(c) svg, ok := icons.Get(name) if !ok { return iconMissing(c, name, size, k) @@ -62,7 +63,7 @@ func Icon(c *ui.Context, name string, opts IconOptions) *ui.Element { } func iconMissing(c *ui.Context, name string, size float32, k theme.Tokens) *ui.Element { - text := fmt.Sprintf(msg(c, "icon.missing"), name) + text := fmt.Sprintf(core.Msg(c, "icon.missing"), name) e := ui.Box(c).Size(size, size).Shrink(0).Role(ui.RoleImage).Label(text).Tooltip(text) logged := ui.Local(e, iconMissingKey{}, func() string { return "" }) if *logged != name { diff --git a/icon_test.go b/display/icon_test.go similarity index 93% rename from icon_test.go rename to display/icon_test.go index 32ada91..383ae8e 100644 --- a/icon_test.go +++ b/display/icon_test.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "bytes" @@ -7,12 +7,14 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIconSizeAndLabel(t *testing.T) { var def, big ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { def = Icon(c, "star", IconOptions{}).Bounds() big = Icon(c, "bell", IconOptions{Size: 24, Tone: IconAccent, Label: "Alerts"}).Bounds() @@ -34,7 +36,7 @@ func TestIconMissingFallbackIsVisibleAndLogged(t *testing.T) { defer slog.SetDefault(old) var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { r = Icon(c, "no-such-icon", IconOptions{Size: 20}).Bounds() }) @@ -64,7 +66,7 @@ func TestIconBadOptionsPanic(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Icon(c, name, opts) }, 100, 100) tt.Frame() diff --git a/image.go b/display/image.go similarity index 82% rename from image.go rename to display/image.go index eddfa71..4b3adf1 100644 --- a/image.go +++ b/display/image.go @@ -1,12 +1,13 @@ -package mujica +package display import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ImageOptions configure an Image. @@ -60,8 +61,8 @@ func Image(c *ui.Context, src ui.ImageSource, opts ImageOptions) ImageView { // imageLayers builds the state layers over the hidden picture. func imageLayers(c *ui.Context, box *ui.Element, opts ImageOptions) (retry bool) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() room := box.Bounds() // Build again when the box changed, settling the error text. if last := ui.Local(box, imageRoomKey{}, func() ui.Rect { return ui.Rect{W: -1} }); *last != room { @@ -74,23 +75,23 @@ func imageLayers(c *ui.Context, box *ui.Element, opts ImageOptions) (retry bool) Radius(theme.ControlRadius).Background(k.SurfaceHover).Border(theme.BorderWidth, k.Border) switch { case opts.Err != nil: - box.Role(ui.RoleGroup).Label(msg(c, "image.error")).Description(opts.Alt).Error(opts.Err.Error()) + box.Role(ui.RoleGroup).Label(core.Msg(c, "image.error")).Description(opts.Alt).Error(opts.Err.Error()) ui.Row(c).Absolute().Left(0).Top(0).Right(0).Bottom(0).Center().Gap(u * 2).Children(func() { if imageRetry(c, k, opts.Err.Error()).Clicked() { retry = true slog.Debug("mujica: image retry", "alt", opts.Alt) } if room.W >= theme.MinHitSize+u*4+imageErrorTextWidth && room.H >= theme.MinHitSize { - ui.Text(c, msg(c, "image.error")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() + ui.Text(c, core.Msg(c, "image.error")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() } }) case opts.Loading: - box.Role(ui.RoleImage).Label(msg(c, "image.loading")).Description(opts.Alt) + box.Role(ui.RoleImage).Label(core.Msg(c, "image.loading")).Description(opts.Alt) back.Children(func() { - Spinner(c, SpinnerOptions{Size: max(8, min(20, room.H-4)), Label: msg(c, "image.loading")}) + core.Spinner(c, core.SpinnerOptions{Size: max(8, min(20, room.H-4)), Label: core.Msg(c, "image.loading")}) }) default: - box.Role(ui.RoleImage).Label(msg(c, "image.placeholder")).Description(opts.Alt) + box.Role(ui.RoleImage).Label(core.Msg(c, "image.placeholder")).Description(opts.Alt) back.Children(func() { Icon(c, "image", IconOptions{Size: 24, Tone: IconMuted}) }) } } @@ -124,7 +125,7 @@ func imageSized(c *ui.Context, src ui.ImageSource, opts ImageOptions, layers fun // imageRetry is the 28 DIP icon-only retry button. func imageRetry(c *ui.Context, k theme.Tokens, problem string) *ui.Element { b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Radius(theme.ControlRadius). - Label(msg(c, "image.retry")).Description(problem).Tooltip(problem) + Label(core.Msg(c, "image.retry")).Description(problem).Tooltip(problem) bg := k.Surface if b.Pressed() { bg = k.Selection @@ -133,7 +134,7 @@ func imageRetry(c *ui.Context, k theme.Tokens, problem string) *ui.Element { } b.Background(bg).Border(theme.BorderWidth, k.ControlBorder).TextColor(k.Danger) internal.Ring(b, k.Focus, theme.ControlRadius) - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { Icon(c, "refresh-cw", IconOptions{Size: 16}) }) return b } diff --git a/image_state_test.go b/display/image_state_test.go similarity index 94% rename from image_state_test.go rename to display/image_state_test.go index 4c26d16..0e8a217 100644 --- a/image_state_test.go +++ b/display/image_state_test.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "errors" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestImageErrorTextNeedsRoom(t *testing.T) { @@ -16,7 +17,7 @@ func TestImageErrorTextNeedsRoom(t *testing.T) { }{{200, true}, {80, false}} { ready := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ready { Image(c, nil, ImageOptions{Width: tc.w, Height: 60, Err: errors.New("404")}) } @@ -46,7 +47,7 @@ func imageStateBoxes(t *testing.T, opts ImageOptions, parent func(c *ui.Context, o.Loading, o.Err = state.Loading, state.Err var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) parent(c, func() { r = Image(c, bmp, o).Element.Bounds() }) }, 400, 400) tt.Frame() @@ -95,7 +96,7 @@ func TestImageRetryUnclippedInSmallBox(t *testing.T) { retries := 0 var box ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Row(c).Padding(40).AlignItems(ui.Start).Children(func() { ui.Row(c).Width(40).AlignItems(ui.Start).Children(func() { v := Image(c, imageTestBitmap(), ImageOptions{Err: errors.New("timeout")}) diff --git a/image_test.go b/display/image_test.go similarity index 94% rename from image_test.go rename to display/image_test.go index 32318be..27bb8f0 100644 --- a/image_test.go +++ b/display/image_test.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "errors" @@ -7,6 +7,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func imageTestBitmap() *ui.Bitmap { @@ -23,7 +25,7 @@ func TestImageLoadedSizes(t *testing.T) { bmp := imageTestBitmap() var natural, sized ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { natural = Image(c, bmp, ImageOptions{Alt: "Gallery"}).Element.Bounds() sized = Image(c, bmp, ImageOptions{Width: 50, Height: 50, Crop: true}).Element.Bounds() @@ -54,7 +56,7 @@ func TestImageStates(t *testing.T) { } { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { r = Image(c, tc.src, tc.opts).Element.Bounds() }) @@ -72,7 +74,7 @@ func TestImageStates(t *testing.T) { func TestImageRetry(t *testing.T) { retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Image(c, nil, ImageOptions{Width: 200, Height: 140, Err: errors.New("timeout")}).Retry { retries++ } @@ -97,7 +99,7 @@ func TestImageNegativeSizePanics(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Image(c, nil, ImageOptions{Width: -1}) }, 100, 100) tt.Frame() @@ -131,7 +133,7 @@ func TestImageKeepsSizeWhileLoading(t *testing.T) { } { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { r = Image(c, bmp, tc.opts).Element.Bounds() }) @@ -149,7 +151,7 @@ func TestImageStatesShareBoxWhenShrunk(t *testing.T) { for _, opts := range []ImageOptions{{}, {Loading: true}, {Err: errors.New("x")}} { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Width(40).AlignItems(ui.Start).Children(func() { r = Image(c, bmp, opts).Element.Bounds() }) @@ -165,7 +167,7 @@ func TestImageStatesShareBoxWhenShrunk(t *testing.T) { func TestImageRetryInSmallBox(t *testing.T) { retries := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Width(40).AlignItems(ui.Start).Children(func() { if Image(c, imageTestBitmap(), ImageOptions{Err: errors.New("timeout")}).Retry { retries++ diff --git a/kbd.go b/display/kbd.go similarity index 73% rename from kbd.go rename to display/kbd.go index 46462f2..6dfe449 100644 --- a/kbd.go +++ b/display/kbd.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "runtime" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Kbd shows keys as caps; "Mod" is Command or Ctrl. @@ -14,26 +15,26 @@ func Kbd(c *ui.Context, keys ...string) *ui.Element { if len(keys) == 0 { panic("mujica: Kbd needs at least one key") } - k := Tokens(c) + k := core.Tokens(c) mac := runtime.GOOS == "darwin" spoken := make([]string, len(keys)) for i, key := range keys { if key == "" { panic("mujica: Kbd key must not be empty") } - _, spoken[i] = kbdKey(key, mac) + _, spoken[i] = KbdKey(key, mac) } row := ui.Row(c).Gap(2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleText).Label(strings.Join(spoken, "+")) row.Children(func() { for i, key := range keys { if i > 0 && !mac { - ui.Text(c, "+").FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, "+").FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } - face, _ := kbdKey(key, mac) - ui.Box(c).Key(i).Row().Center().MinWidth(FontSize(c, 20)).MinHeight(FontSize(c, 20)).Padding(0, 5). + face, _ := KbdKey(key, mac) + ui.Box(c).Key(i).Row().Center().MinWidth(core.FontSize(c, 20)).MinHeight(core.FontSize(c, 20)).Padding(0, 5). Radius(theme.ControlRadius).Background(k.Surface).BorderColor(k.ControlBorder). BorderWidth(1, 1, 2, 1).Children(func() { - ui.Text(c, face).Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() + ui.Text(c, face).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() }) } }) @@ -57,8 +58,8 @@ var kbdNames = map[string]struct{ macCap, macWord, other string }{ "right": {"→", "Right", "→"}, } -// kbdKey returns how key shows and reads on the platform. -func kbdKey(key string, mac bool) (shown, spoken string) { +// KbdKey returns how key shows and reads on the platform. +func KbdKey(key string, mac bool) (shown, spoken string) { n, ok := kbdNames[strings.ToLower(key)] if !ok { if len([]rune(key)) == 1 { diff --git a/kbd_test.go b/display/kbd_test.go similarity index 91% rename from kbd_test.go rename to display/kbd_test.go index c3606a3..589d88f 100644 --- a/kbd_test.go +++ b/display/kbd_test.go @@ -1,10 +1,12 @@ -package mujica +package display import ( "runtime" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestKbdKeyPlatforms(t *testing.T) { @@ -22,7 +24,7 @@ func TestKbdKeyPlatforms(t *testing.T) { {"k", false, "K", "K"}, {"F5", true, "F5", "F5"}, } { - shown, spoken := kbdKey(tc.key, tc.mac) + shown, spoken := KbdKey(tc.key, tc.mac) if shown != tc.shown || spoken != tc.spoken { t.Errorf("%q mac=%v: %q %q", tc.key, tc.mac, shown, spoken) } @@ -31,7 +33,7 @@ func TestKbdKeyPlatforms(t *testing.T) { func TestKbdShowsPlatformCombo(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Kbd(c, "Mod", "Shift", "p") }, 300, 100) tt.Frame() @@ -60,7 +62,7 @@ func TestKbdEmptyPanics(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Kbd(c, keys...) }, 100, 100) tt.Frame() @@ -83,7 +85,7 @@ func TestKbdTextScale(t *testing.T) { var h [2]float32 for i, s := range []float32{1, 2} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { h[i] = Kbd(c, "Mod", "K").Bounds().H }) diff --git a/link.go b/display/link.go similarity index 82% rename from link.go rename to display/link.go index f2a4c94..af08037 100644 --- a/link.go +++ b/display/link.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LinkOptions configure a Link; empty URL means in-app action. @@ -24,8 +25,8 @@ func Link(c *ui.Context, label string, opts LinkOptions) *ui.Element { if label == "" { panic("mujica: Link needs a label") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() external := false problem := opts.Error target := opts.URL @@ -35,7 +36,7 @@ func Link(c *ui.Context, label string, opts LinkOptions) *ui.Element { if !ok { external, target = false, "" if problem == "" { - problem = fmt.Sprintf(msg(c, "link.invalid"), opts.URL) + problem = fmt.Sprintf(core.Msg(c, "link.invalid"), opts.URL) } } } @@ -55,7 +56,7 @@ func Link(c *ui.Context, label string, opts LinkOptions) *ui.Element { }) if problem != "" { l.Error(problem) - ui.Text(c, problem).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, problem).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)) } }) if l.Clicked() { diff --git a/link_test.go b/display/link_test.go similarity index 92% rename from link_test.go rename to display/link_test.go index 3b85ae4..2e8c001 100644 --- a/link_test.go +++ b/display/link_test.go @@ -1,15 +1,17 @@ -package mujica +package display import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestLinkOpensURL(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Link(c, "Charter", LinkOptions{URL: "https://example.com/charter"}) }, 300, 100) if err := tt.Click("Charter"); err != nil { @@ -26,7 +28,7 @@ func TestLinkOpensURL(t *testing.T) { func TestLinkAction(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Link(c, "Show details", LinkOptions{}).Clicked() { n++ } @@ -41,7 +43,7 @@ func TestLinkAction(t *testing.T) { func TestLinkInvalidURLIsNotOpened(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Link(c, "Broken", LinkOptions{URL: "https://"}).Clicked() { n++ } @@ -59,7 +61,7 @@ func TestLinkInvalidURLIsNotOpened(t *testing.T) { func TestLinkCallerError(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) Link(c, "手册", LinkOptions{URL: "https://example.com", Error: "没有可以打开此链接的应用"}) }, 400, 100) tt.Frame() diff --git a/tag.go b/display/tag.go similarity index 85% rename from tag.go rename to display/tag.go index f103651..6a70549 100644 --- a/tag.go +++ b/display/tag.go @@ -1,4 +1,4 @@ -package mujica +package display import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TagOptions configure a Tag; zero is a plain label. @@ -35,8 +36,8 @@ func Tag(c *ui.Context, label string, opts TagOptions) TagView { if label == "" { panic("mujica: Tag needs a label") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var v TagView box := ui.Row(c).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Radius(theme.ControlRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border).Shrink(0).Disabled(opts.Disabled) @@ -56,7 +57,7 @@ func Tag(c *ui.Context, label string, opts TagOptions) TagView { } else if main.Hovered() { main.Background(k.SurfaceHover) } - main.Transition(colorFade) + main.Transition(core.ColorFade) } if opts.Closable { main.Padding(0, u, 0, u*2) @@ -65,7 +66,7 @@ func Tag(c *ui.Context, label string, opts TagOptions) TagView { if opts.Icon != "" { Icon(c, opts.Icon, IconOptions{Size: 14}) } - ui.Text(c, label).FontSize(FontSize(c, theme.BodySize-1)).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.BodySize-1)).SingleLine() }) if opts.Clickable && main.Clicked() { v.Clicked = true @@ -90,7 +91,7 @@ func Tag(c *ui.Context, label string, opts TagOptions) TagView { func tagClose(c *ui.Context, k theme.Tokens, label string) *ui.Element { b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize). - Radius(theme.ControlRadius).Label(fmt.Sprintf(msg(c, "tag.remove"), label)) + Radius(theme.ControlRadius).Label(fmt.Sprintf(core.Msg(c, "tag.remove"), label)) fg := k.TextMuted if b.Pressed() { b.Background(k.Selection) @@ -100,7 +101,7 @@ func tagClose(c *ui.Context, k theme.Tokens, label string) *ui.Element { fg = k.Text } internal.Ring(b, k.Focus, theme.ControlRadius) - b.Transition(colorFade) + b.Transition(core.ColorFade) b.TextColor(fg).Children(func() { Icon(c, "x", IconOptions{Size: 12}) }) diff --git a/tag_test.go b/display/tag_test.go similarity index 95% rename from tag_test.go rename to display/tag_test.go index e3c3586..7c37710 100644 --- a/tag_test.go +++ b/display/tag_test.go @@ -1,16 +1,18 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type tagCounts struct{ clicked, closed int } func tagTester(opts TagOptions, n *tagCounts) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { v := Tag(c, "Velvet", opts) if v.Clicked { @@ -94,7 +96,7 @@ func TestTagSnapshot(t *testing.T) { func TestTagDisabledAncestorBlocksDelete(t *testing.T) { closed, off := 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Disabled(off).Children(func() { if Tag(c, "Velvet", TagOptions{Clickable: true, Closable: true}).Closed { closed++ @@ -115,7 +117,7 @@ func TestTagTextScale(t *testing.T) { var h [2]float32 for i, s := range []float32{1, 2} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { h[i] = Tag(c, "Velvet", TagOptions{}).Element.Bounds().W }) diff --git a/text.go b/display/text.go similarity index 87% rename from text.go rename to display/text.go index bdf0503..6d2c69c 100644 --- a/text.go +++ b/display/text.go @@ -1,12 +1,13 @@ -package mujica +package display import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TextOptions configure a Text. The zero value wraps every line. @@ -31,7 +32,7 @@ func Text(c *ui.Context, s string, opts TextOptions) *ui.Element { } if opts.Selectable { e.Selectable().Focusable() - internal.Ring(e, Tokens(c).Focus, theme.ControlRadius) + internal.Ring(e, core.Tokens(c).Focus, theme.ControlRadius) } return e } @@ -66,8 +67,8 @@ type editableTextKey struct{} // EditableText is Text's editable mode with confirm and cancel. func EditableText(c *ui.Context, value *string, opts EditableTextOptions) EditableTextView { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() line := k.ControlBorder if opts.Error != "" { line = k.Danger @@ -77,11 +78,11 @@ func EditableText(c *ui.Context, value *string, opts EditableTextOptions) Editab ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { v = editableTextField(c, k, value, opts, line) if opts.Saving { - Spinner(c, SpinnerOptions{Size: 12, Label: msg(c, "editable_text.saving")}) + core.Spinner(c, core.SpinnerOptions{Size: 12, Label: core.Msg(c, "editable_text.saving")}) } }) if opts.Error != "" { - ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)) } }) switch { @@ -94,7 +95,7 @@ func EditableText(c *ui.Context, value *string, opts EditableTextOptions) Editab } func editableTextField(c *ui.Context, k theme.Tokens, value *string, opts EditableTextOptions, line ui.Color) EditableTextView { - pad := Density(c).Unit() / 2 + pad := core.Density(c).Unit() / 2 box := ui.Row(c).AlignItems(ui.Center).MinWidth(theme.MinHitSize * 2).MinHeight(theme.MinHitSize). Radius(theme.ControlRadius).Disabled(opts.Saving) v := EditableTextView{Element: box} @@ -109,7 +110,7 @@ func editableTextField(c *ui.Context, k theme.Tokens, value *string, opts Editab if box.Hovered() { box.Background(k.SurfaceHover) } - box.Transition(colorFade) + box.Transition(core.ColorFade) if !box.IsDisabled() && (box.DoubleClicked() || box.Shortcut(0, ui.KeyEnter)) { st.editing, st.fresh, st.draft = true, true, *value } else { diff --git a/text_test.go b/display/text_test.go similarity index 90% rename from text_test.go rename to display/text_test.go index 3836edc..a7e4bfa 100644 --- a/text_test.go +++ b/display/text_test.go @@ -1,16 +1,18 @@ -package mujica +package display import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestTextSelectAndCopy(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Text(c, "ID-4821", TextOptions{Selectable: true}) }, 300, 100) if err := tt.Click("ID-4821"); err != nil { @@ -25,7 +27,7 @@ func TestTextSelectAndCopy(t *testing.T) { func TestTextSelectableTakesTab(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Text(c, "ID-4821", TextOptions{Selectable: true}) }, 300, 100) tt.Key(0, ui.KeyTab) @@ -43,11 +45,11 @@ func TestEditableTextBlurConfirms(t *testing.T) { name := "a" changed := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if EditableText(c, &name, EditableTextOptions{}).Changed() { changed++ } - Button(c, "Other", ButtonOptions{}) + input.Button(c, "Other", input.ButtonOptions{}) }, 300, 120) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyEnter) @@ -62,7 +64,7 @@ func TestEditableTextBlurConfirms(t *testing.T) { func TestTextNotSelectableByDefault(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Text(c, "plain", TextOptions{}) }, 300, 100) _ = tt.Click("plain") @@ -77,7 +79,7 @@ func TestTextMaxLinesCuts(t *testing.T) { long := "The court gathers in the hall of mirrors while candles burn low and the music fades away." var free, cut float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(120).Children(func() { free = Text(c, long, TextOptions{}).Bounds().H cut = Text(c, long, TextOptions{MaxLines: 1}).Bounds().H @@ -96,7 +98,7 @@ func TestTextNegativeMaxLinesPanics(t *testing.T) { } }() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Text(c, "x", TextOptions{MaxLines: -1}) }, 100, 100) tt.Frame() @@ -106,7 +108,7 @@ func TestEditableTextConfirm(t *testing.T) { name := "report.pdf" changed := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if EditableText(c, &name, EditableTextOptions{Label: "File name"}).Changed() { changed++ } @@ -131,7 +133,7 @@ func TestEditableTextCancel(t *testing.T) { name := "report.pdf" changed, canceled := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) v := EditableText(c, &name, EditableTextOptions{}) if v.Changed() { changed++ @@ -155,7 +157,7 @@ func TestEditableTextCancel(t *testing.T) { func TestEditableTextSavingBlocksAndErrorShows(t *testing.T) { name := "notes" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EditableText(c, &name, EditableTextOptions{Saving: true, Error: "disk full"}) }, 300, 120) tt.Key(0, ui.KeyTab) @@ -189,10 +191,10 @@ func TestTextSnapshot(t *testing.T) { func TestEditableTextInputFocusRing(t *testing.T) { name := "report.pdf" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Padding(20).Gap(20).AlignItems(ui.Start).Children(func() { EditableText(c, &name, EditableTextOptions{Label: "File name"}) - Button(c, "Other", ButtonOptions{}) + input.Button(c, "Other", input.ButtonOptions{}) }) }, 300, 160) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/docs/components/001-app-shell.md b/docs/components/001-app-shell.md index 699415f..7d700c9 100644 --- a/docs/components/001-app-shell.md +++ b/docs/components/001-app-shell.md @@ -8,8 +8,8 @@ ```go var collapsed bool -mujica.AppShell(c, &collapsed, mujica.AppShellOptions{ - TitleBar: func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) }, +layout.AppShell(c, &collapsed, layout.AppShellOptions{ + TitleBar: func() { layout.TitleBar(c, "Court Records", layout.TitleBarOptions{}) }, Sidebar: func() { ui.Text(c, "Chronicles") }, Content: func() { ui.Text(c, "The Grand Hall") }, }) diff --git a/docs/components/002-title-bar.md b/docs/components/002-title-bar.md index 9b9dd1b..ca10ee7 100644 --- a/docs/components/002-title-bar.md +++ b/docs/components/002-title-bar.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{ - Trailing: func() { mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}) }, +layout.TitleBar(c, "Court Records", layout.TitleBarOptions{ + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, }) ``` diff --git a/docs/components/003-status-bar.md b/docs/components/003-status-bar.md index 2610f7a..2374c44 100644 --- a/docs/components/003-status-bar.md +++ b/docs/components/003-status-bar.md @@ -7,9 +7,9 @@ ## 最小示例 ```go -bar := mujica.StatusBar(c, mujica.StatusBarOptions{ - Left: []mujica.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, - Right: []mujica.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, +bar := layout.StatusBar(c, layout.StatusBarOptions{ + Left: []layout.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, + Right: []layout.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, }) if bar.Clicked == "bell" { opened++ diff --git a/docs/components/004-container.md b/docs/components/004-container.md index abe01ee..a235f16 100644 --- a/docs/components/004-container.md +++ b/docs/components/004-container.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.Container(c, mujica.ContainerOptions{MaxWidth: 320, Center: true}, func() { +layout.Container(c, layout.ContainerOptions{MaxWidth: 320, Center: true}, func() { ui.Text(c, "A centered column of prose.") }) ``` diff --git a/docs/components/005-stack.md b/docs/components/005-stack.md index 55278e5..55c3313 100644 --- a/docs/components/005-stack.md +++ b/docs/components/005-stack.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -mujica.Stack(c, mujica.StackOptions{Horizontal: true, Gap: mujica.SpaceM}, func() { - mujica.Button(c, "Save", mujica.ButtonOptions{}) +layout.Stack(c, layout.StackOptions{Horizontal: true, Gap: layout.SpaceM}, func() { + input.Button(c, "Save", input.ButtonOptions{}) if showDraft { - mujica.Button(c, "Draft", mujica.ButtonOptions{Variant: mujica.Secondary}) + input.Button(c, "Draft", input.ButtonOptions{Variant: input.Secondary}) } - mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Ghost}) + input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Ghost}) }) ``` diff --git a/docs/components/006-grid.md b/docs/components/006-grid.md index c22bac8..8c48bf1 100644 --- a/docs/components/006-grid.md +++ b/docs/components/006-grid.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { - mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) +layout.Grid(c, layout.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { + layout.GridCell(c, layout.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) ui.Text(c, "Name") ui.Text(c, "Ada") }) diff --git a/docs/components/007-scroll-area.md b/docs/components/007-scroll-area.md index 40f742e..8cf78c0 100644 --- a/docs/components/007-scroll-area.md +++ b/docs/components/007-scroll-area.md @@ -8,7 +8,7 @@ ```go var offset ui.ScrollState -mujica.ScrollArea(c, &offset, mujica.ScrollAreaOptions{Label: "Log"}, func() { +layout.ScrollArea(c, &offset, layout.ScrollAreaOptions{Label: "Log"}, func() { for range 50 { ui.Text(c, "A line of the log") } diff --git a/docs/components/008-split-pane.md b/docs/components/008-split-pane.md index 9a4659d..fc5171c 100644 --- a/docs/components/008-split-pane.md +++ b/docs/components/008-split-pane.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -split := mujica.SplitPaneState{Size: 160} -mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst}, +split := layout.SplitPaneState{Size: 160} +layout.SplitPane(c, &split, layout.SplitPaneOptions{MinFirst: 120, Collapsible: layout.SplitPaneFirst}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }, ).Element.Fill() diff --git a/docs/components/009-aspect-ratio.md b/docs/components/009-aspect-ratio.md index 8837483..db4be13 100644 --- a/docs/components/009-aspect-ratio.md +++ b/docs/components/009-aspect-ratio.md @@ -8,7 +8,7 @@ ```go ui.Box(c).Size(320, 240).Children(func() { - mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: 16.0 / 9}, func() { + layout.AspectRatio(c, layout.AspectRatioOptions{Ratio: 16.0 / 9}, func() { ui.Text(c, "16 : 9") }) }) diff --git a/docs/components/010-divider.md b/docs/components/010-divider.md index bc938df..23bff3c 100644 --- a/docs/components/010-divider.md +++ b/docs/components/010-divider.md @@ -8,8 +8,8 @@ ```go ui.Column(c).Padding(16).Gap(12).Children(func() { - mujica.Divider(c, mujica.DividerOptions{Ornament: true}) - mujica.Divider(c, mujica.DividerOptions{Label: "Chapter II"}) + layout.Divider(c, layout.DividerOptions{Ornament: true}) + layout.Divider(c, layout.DividerOptions{Label: "Chapter II"}) }) ``` diff --git a/docs/components/011-text.md b/docs/components/011-text.md index 963b7f9..1cf5667 100644 --- a/docs/components/011-text.md +++ b/docs/components/011-text.md @@ -10,10 +10,10 @@ name := "Coronation.md" saveErr := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) - mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) - if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { + core.Use(c, core.Settings{}) + display.Text(c, "Order ID: 4821-AX", display.TextOptions{Selectable: true}) + display.Text(c, "A long note that stops after two lines.", display.TextOptions{MaxLines: 2}) + if display.EditableText(c, &name, display.EditableTextOptions{Error: saveErr}).Changed() { saveErr = "" // save name here; set saveErr on failure } } diff --git a/docs/components/012-heading.md b/docs/components/012-heading.md index 17425b4..0735bd3 100644 --- a/docs/components/012-heading.md +++ b/docs/components/012-heading.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) - mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2}) + core.Use(c, core.Settings{}) + display.Heading(c, "The Grand Hall", display.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) + display.Heading(c, "Guest Ledger", display.HeadingOptions{Level: 2}) } ``` diff --git a/docs/components/013-icon.md b/docs/components/013-icon.md index 886485f..a1efd0b 100644 --- a/docs/components/013-icon.md +++ b/docs/components/013-icon.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(8).Children(func() { - mujica.Icon(c, "star", mujica.IconOptions{}) - mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) + display.Icon(c, "star", display.IconOptions{}) + display.Icon(c, "circle-check", display.IconOptions{Size: 20, Tone: display.IconSuccess, Label: "Done"}) }) } ``` diff --git a/docs/components/014-image.md b/docs/components/014-image.md index 37cf062..240b243 100644 --- a/docs/components/014-image.md +++ b/docs/components/014-image.md @@ -10,8 +10,8 @@ var photo *ui.Bitmap // decode once with ui.DecodeBitmap, outside the view loading, loadErr := false, errors.New("timeout") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - img := mujica.Image(c, photo, mujica.ImageOptions{ + core.Use(c, core.Settings{}) + img := display.Image(c, photo, display.ImageOptions{ Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", Loading: loading, Err: loadErr, }) diff --git a/docs/components/015-avatar.md b/docs/components/015-avatar.md index 8fca682..0a91a5b 100644 --- a/docs/components/015-avatar.md +++ b/docs/components/015-avatar.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(8).Children(func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) - mujica.Avatar(c, "", mujica.AvatarOptions{}) + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{Size: 40}) + display.Avatar(c, "", display.AvatarOptions{}) }) } ``` diff --git a/docs/components/016-avatar-group.md b/docs/components/016-avatar-group.md index f024548..383770d 100644 --- a/docs/components/016-avatar-group.md +++ b/docs/components/016-avatar-group.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -people := []mujica.AvatarItem{ +people := []display.AvatarItem{ {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3}) + core.Use(c, core.Settings{}) + display.AvatarGroup(c, people, display.AvatarGroupOptions{Max: 3}) } ``` diff --git a/docs/components/017-badge.md b/docs/components/017-badge.md index 6a1bebd..64655ed 100644 --- a/docs/components/017-badge.md +++ b/docs/components/017-badge.md @@ -9,10 +9,10 @@ ```go unread := 120 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) - mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) + core.Use(c, core.Settings{}) + display.Badge(c, display.BadgeOptions{Text: "New", Tone: display.BadgeNeutral}, nil) + display.Badge(c, display.BadgeOptions{Count: unread, Label: "Unread", Position: display.BadgeTopRight}, func() { + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{}) }) } ``` diff --git a/docs/components/018-tag.md b/docs/components/018-tag.md index e780a67..0185a35 100644 --- a/docs/components/018-tag.md +++ b/docs/components/018-tag.md @@ -9,12 +9,12 @@ ```go tags := []string{"Baroque", "Gothic"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(8).Children(func() { for i, t := range tags { - var v mujica.TagView + var v display.TagView ui.Box(c).Key(t).Children(func() { - v = mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) + v = display.Tag(c, t, display.TagOptions{Icon: "star", Clickable: true, Closable: true}) }) if v.Closed { tags = append(tags[:i:i], tags[i+1:]...) diff --git a/docs/components/019-link.md b/docs/components/019-link.md index a6a760f..33ca872 100644 --- a/docs/components/019-link.md +++ b/docs/components/019-link.md @@ -9,9 +9,9 @@ ```go details := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) - if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { + core.Use(c, core.Settings{}) + display.Link(c, "Read the charter", display.LinkOptions{URL: "https://example.com/charter"}) + if display.Link(c, "Show details", display.LinkOptions{}).Clicked() { details = !details } } diff --git a/docs/components/020-kbd.md b/docs/components/020-kbd.md index 76d278e..08a53eb 100644 --- a/docs/components/020-kbd.md +++ b/docs/components/020-kbd.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(6).Children(func() { - mujica.Text(c, "Command palette", mujica.TextOptions{}) - mujica.Kbd(c, "Mod", "Shift", "P") + display.Text(c, "Command palette", display.TextOptions{}) + display.Kbd(c, "Mod", "Shift", "P") }) } ``` diff --git a/docs/components/021-button.md b/docs/components/021-button.md index e22d459..b7e3490 100644 --- a/docs/components/021-button.md +++ b/docs/components/021-button.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.Button(c, "Save", mujica.ButtonOptions{Icon: icons.Must("check")}).Clicked() { +if input.Button(c, "Save", input.ButtonOptions{Icon: icons.Must("check")}).Clicked() { fmt.Println("saved") } ``` diff --git a/docs/components/022-icon-button.md b/docs/components/022-icon-button.md index 9efa694..6829cfe 100644 --- a/docs/components/022-icon-button.md +++ b/docs/components/022-icon-button.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.IconButton(c, icons.Must("settings"), "Settings", mujica.IconButtonOptions{Variant: mujica.Ghost}).Clicked() { +if input.IconButton(c, icons.Must("settings"), "Settings", input.IconButtonOptions{Variant: input.Ghost}).Clicked() { fmt.Println("open settings") } ``` diff --git a/docs/components/023-button-group.md b/docs/components/023-button-group.md index db7a2fd..96d76c6 100644 --- a/docs/components/023-button-group.md +++ b/docs/components/023-button-group.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} -if i := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { +items := []input.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} +if i := input.ButtonGroup(c, items, input.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { fmt.Println("chose", items[i].Label) } ``` diff --git a/docs/components/024-text-input.md b/docs/components/024-text-input.md index 6cdd2f5..430fe28 100644 --- a/docs/components/024-text-input.md +++ b/docs/components/024-text-input.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.TextInput(c, &name, mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) +r := input.TextInput(c, &name, input.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) if r.Submitted() { fmt.Println("hello", name) } diff --git a/docs/components/025-text-area.md b/docs/components/025-text-area.md index 1204c78..64a6b95 100644 --- a/docs/components/025-text-area.md +++ b/docs/components/025-text-area.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.TextArea(c, ¬es, mujica.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) +r := input.TextArea(c, ¬es, input.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) if r.Submitted() { fmt.Println("send", notes) } diff --git a/docs/components/026-number-input.md b/docs/components/026-number-input.md index a2f5618..561126a 100644 --- a/docs/components/026-number-input.md +++ b/docs/components/026-number-input.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.NumberInput(c, &copies, mujica.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { +if input.NumberInput(c, &copies, input.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { fmt.Println("copies", copies) } ``` diff --git a/docs/components/027-password-input.md b/docs/components/027-password-input.md index 20d4cab..a5c6c1a 100644 --- a/docs/components/027-password-input.md +++ b/docs/components/027-password-input.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.PasswordInput(c, &password, mujica.PasswordInputOptions{Label: "Password"}).Submitted() { +if input.PasswordInput(c, &password, input.PasswordInputOptions{Label: "Password"}).Submitted() { fmt.Println("signing in") } ``` diff --git a/docs/components/028-search-input.md b/docs/components/028-search-input.md index c1d6ca8..6e29811 100644 --- a/docs/components/028-search-input.md +++ b/docs/components/028-search-input.md @@ -11,7 +11,7 @@ matches := 0 if query != "" { matches = strings.Count(text, query) } -r := mujica.SearchInput(c, &query, mujica.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) +r := input.SearchInput(c, &query, input.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) if r.Moved() { fmt.Println("show match", current) } diff --git a/docs/components/029-checkbox.md b/docs/components/029-checkbox.md index 6b892b7..190ec91 100644 --- a/docs/components/029-checkbox.md +++ b/docs/components/029-checkbox.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -mujica.CheckboxGroup(c, "Notifications", []mujica.CheckboxItem{ +input.CheckboxGroup(c, "Notifications", []input.CheckboxItem{ {Label: "Mail", Value: &mail}, {Label: "Calendar", Value: &calendar}, -}, mujica.CheckboxGroupOptions{}) +}, input.CheckboxGroupOptions{}) ``` 签名: diff --git a/docs/components/030-radio-group.md b/docs/components/030-radio-group.md index 7b166d3..55bc1c8 100644 --- a/docs/components/030-radio-group.md +++ b/docs/components/030-radio-group.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} -if mujica.RadioGroup(c, &size, items, mujica.RadioGroupOptions{Label: "Size"}).Changed() { +items := []input.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} +if input.RadioGroup(c, &size, items, input.RadioGroupOptions{Label: "Size"}).Changed() { fmt.Println("size", size) } ``` diff --git a/docs/components/031-switch.md b/docs/components/031-switch.md index 5b5b718..b0d094e 100644 --- a/docs/components/031-switch.md +++ b/docs/components/031-switch.md @@ -9,8 +9,8 @@ ```go wifi := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Switch(c, &wifi, "Wi-Fi", mujica.SwitchOptions{}).Changed() { + core.Use(c, core.Settings{}) + if input.Switch(c, &wifi, "Wi-Fi", input.SwitchOptions{}).Changed() { log.Println("Wi-Fi:", wifi) } } diff --git a/docs/components/032-toggle.md b/docs/components/032-toggle.md index db685a9..4c730c9 100644 --- a/docs/components/032-toggle.md +++ b/docs/components/032-toggle.md @@ -9,8 +9,8 @@ ```go bold := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Toggle(c, &bold, "", mujica.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) + core.Use(c, core.Settings{}) + input.Toggle(c, &bold, "", input.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/033-toggle-group.md b/docs/components/033-toggle-group.md index 4361ce2..4b4be3d 100644 --- a/docs/components/033-toggle-group.md +++ b/docs/components/033-toggle-group.md @@ -8,14 +8,14 @@ ```go align := []string{"left"} -items := []mujica.ToggleItem[string]{ +items := []input.ToggleItem[string]{ {Value: "left", Label: "Left"}, {Value: "center", Label: "Center"}, {Value: "right", Label: "Right"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToggleGroup(c, &align, items, mujica.ToggleGroupOptions{Label: "Align"}) + core.Use(c, core.Settings{}) + input.ToggleGroup(c, &align, items, input.ToggleGroupOptions{Label: "Align"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/034-slider.md b/docs/components/034-slider.md index 129d48e..9742326 100644 --- a/docs/components/034-slider.md +++ b/docs/components/034-slider.md @@ -9,8 +9,8 @@ ```go volume := 40.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Slider(c, &volume, mujica.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) + core.Use(c, core.Settings{}) + input.Slider(c, &volume, input.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/035-range-slider.md b/docs/components/035-range-slider.md index afa352a..66272ca 100644 --- a/docs/components/035-range-slider.md +++ b/docs/components/035-range-slider.md @@ -9,8 +9,8 @@ ```go low, high := 20.0, 80.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RangeSlider(c, &low, &high, mujica.RangeSliderOptions{Step: 10, Label: "Price"}) + core.Use(c, core.Settings{}) + input.RangeSlider(c, &low, &high, input.RangeSliderOptions{Step: 10, Label: "Price"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/036-select.md b/docs/components/036-select.md index 5e0745e..c7f6502 100644 --- a/docs/components/036-select.md +++ b/docs/components/036-select.md @@ -8,14 +8,14 @@ ```go plan := "" -options := []mujica.SelectOption[string]{ +options := []input.SelectOption[string]{ {Value: "free", Label: "Free"}, {Value: "pro", Label: "Pro"}, {Value: "team", Label: "Team", Disabled: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Select(c, &plan, options, mujica.SelectOptions{Clearable: true, Label: "Plan"}) + core.Use(c, core.Settings{}) + input.Select(c, &plan, options, input.SelectOptions{Clearable: true, Label: "Plan"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/037-multi-select.md b/docs/components/037-multi-select.md index ba56e64..7b5d4e2 100644 --- a/docs/components/037-multi-select.md +++ b/docs/components/037-multi-select.md @@ -8,14 +8,14 @@ ```go tags := []int{1} -options := []mujica.SelectOption[int]{ +options := []input.SelectOption[int]{ {Value: 1, Label: "Rose"}, {Value: 2, Label: "Lily"}, {Value: 3, Label: "Iris"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiSelect(c, &tags, options, mujica.MultiSelectOptions{Max: 2, Label: "Flowers"}) + core.Use(c, core.Settings{}) + input.MultiSelect(c, &tags, options, input.MultiSelectOptions{Max: 2, Label: "Flowers"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/038-combobox.md b/docs/components/038-combobox.md index ac2a450..cb96b56 100644 --- a/docs/components/038-combobox.md +++ b/docs/components/038-combobox.md @@ -10,8 +10,8 @@ font := "" fonts := []string{"Garamond", "Georgia", "Didot"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Combobox(c, &font, fonts, mujica.ComboboxOptions{AllowCustom: true, Label: "Font"}) + core.Use(c, core.Settings{}) + r := input.Combobox(c, &font, fonts, input.ComboboxOptions{AllowCustom: true, Label: "Font"}) if r.QueryChanged() { log.Println("search:", r.Query()) } diff --git a/docs/components/039-cascader.md b/docs/components/039-cascader.md index 58ad106..61a967a 100644 --- a/docs/components/039-cascader.md +++ b/docs/components/039-cascader.md @@ -8,15 +8,15 @@ ```go var path []string -level := func(p []string) mujica.CascaderLevel { +level := func(p []string) input.CascaderLevel { if len(p) == 0 { - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "fr", Label: "France"}}} + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "fr", Label: "France"}}} } - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Cascader(c, &path, mujica.CascaderOptions{Level: level, Label: "City"}) + core.Use(c, core.Settings{}) + input.Cascader(c, &path, input.CascaderOptions{Level: level, Label: "City"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/040-tree-select.md b/docs/components/040-tree-select.md index d49adee..d882e90 100644 --- a/docs/components/040-tree-select.md +++ b/docs/components/040-tree-select.md @@ -8,15 +8,15 @@ ```go var roles []string -nodes := []mujica.TreeSelectNode{ - {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ +nodes := []input.TreeSelectNode{ + {ID: "court", Label: "Court", Children: []input.TreeSelectNode{ {ID: "king", Label: "King"}, {ID: "queen", Label: "Queen"}, }}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeSelect(c, &roles, nodes, mujica.TreeSelectOptions{Multiple: true, Label: "Roles"}) + core.Use(c, core.Settings{}) + input.TreeSelect(c, &roles, nodes, input.TreeSelectOptions{Multiple: true, Label: "Roles"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/041-form.md b/docs/components/041-form.md index 5258920..4518710 100644 --- a/docs/components/041-form.md +++ b/docs/components/041-form.md @@ -9,13 +9,13 @@ ## 最小示例 ```go -var state mujica.FormState +var state input.FormState name := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { - return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input + core.Use(c, core.Settings{}) + res := input.Form(c, &state, input.FormOptions{}, func(f *input.FormScope) { + input.FormItem(c, f, "name", &name, input.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + return input.InputGroup(c, &name, input.InputGroupOptions{Invalid: state.Error("name") != ""}).Input }) }) if res.Submitted() { diff --git a/docs/components/042-form-field.md b/docs/components/042-form-field.md index eaca697..f347f32 100644 --- a/docs/components/042-form-field.md +++ b/docs/components/042-form-field.md @@ -11,9 +11,9 @@ ```go email := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FormField(c, "Email", mujica.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { - return mujica.InputGroup(c, &email, mujica.InputGroupOptions{}).Input + core.Use(c, core.Settings{}) + input.FormField(c, "Email", input.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { + return input.InputGroup(c, &email, input.InputGroupOptions{}).Input }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/043-input-group.md b/docs/components/043-input-group.md index a9a86ba..d274721 100644 --- a/docs/components/043-input-group.md +++ b/docs/components/043-input-group.md @@ -11,10 +11,10 @@ ```go site := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - g := mujica.InputGroup(c, &site, mujica.InputGroupOptions{ + core.Use(c, core.Settings{}) + g := input.InputGroup(c, &site, input.InputGroupOptions{ Prefix: "https://", Label: "Site", - Buttons: []mujica.InputGroupButton{{Label: "Open"}}, + Buttons: []input.InputGroupButton{{Label: "Open"}}, }) if g.Clicked == 0 { c.OpenURL("https://" + site) diff --git a/docs/components/044-pin-input.md b/docs/components/044-pin-input.md index 0954adb..4086383 100644 --- a/docs/components/044-pin-input.md +++ b/docs/components/044-pin-input.md @@ -11,8 +11,8 @@ ```go code := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PinInput(c, &code, mujica.PinInputOptions{Length: 6}).Completed() { + core.Use(c, core.Settings{}) + if input.PinInput(c, &code, input.PinInputOptions{Length: 6}).Completed() { fmt.Println("verify", code) } } diff --git a/docs/components/045-tags-input.md b/docs/components/045-tags-input.md index 61ff072..eb3b4df 100644 --- a/docs/components/045-tags-input.md +++ b/docs/components/045-tags-input.md @@ -11,8 +11,8 @@ ```go tags := []string{"gothic"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TagsInput(c, &tags, mujica.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) + core.Use(c, core.Settings{}) + input.TagsInput(c, &tags, input.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/046-rating.md b/docs/components/046-rating.md index 35f1ad1..70b8c3e 100644 --- a/docs/components/046-rating.md +++ b/docs/components/046-rating.md @@ -11,8 +11,8 @@ ```go stars := 3.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Rating(c, &stars, mujica.RatingOptions{Step: 0.5, ShowValue: true}) + core.Use(c, core.Settings{}) + input.Rating(c, &stars, input.RatingOptions{Step: 0.5, ShowValue: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/047-color-picker.md b/docs/components/047-color-picker.md index 1929278..81f1079 100644 --- a/docs/components/047-color-picker.md +++ b/docs/components/047-color-picker.md @@ -11,8 +11,8 @@ ```go tint := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPicker(c, &tint, mujica.ColorPickerOptions{Label: "Tint"}) + core.Use(c, core.Settings{}) + input.ColorPicker(c, &tint, input.ColorPickerOptions{Label: "Tint"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/048-file-picker.md b/docs/components/048-file-picker.md index f974065..4458e3e 100644 --- a/docs/components/048-file-picker.md +++ b/docs/components/048-file-picker.md @@ -11,8 +11,8 @@ ```go var paths []string view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePicker(c, &paths, mujica.FilePickerOptions{ + core.Use(c, core.Settings{}) + input.FilePicker(c, &paths, input.FilePickerOptions{ Multiple: true, Filters: []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}}, }) diff --git a/docs/components/049-file-drop-zone.md b/docs/components/049-file-drop-zone.md index 4168091..86c893d 100644 --- a/docs/components/049-file-drop-zone.md +++ b/docs/components/049-file-drop-zone.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -var files []mujica.DroppedFile +var files []input.DroppedFile view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileDropZone(c, &files, mujica.FileDropZoneOptions{ + core.Use(c, core.Settings{}) + input.FileDropZone(c, &files, input.FileDropZoneOptions{ Extensions: []string{"png", "jpg"}, MaxFiles: 3, MaxSize: 5 << 20, }) } diff --git a/docs/components/050-transfer.md b/docs/components/050-transfer.md index 7be5399..ff84bac 100644 --- a/docs/components/050-transfer.md +++ b/docs/components/050-transfer.md @@ -10,10 +10,10 @@ ```go chosen := []string{"uika"} -items := []mujica.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} +items := []input.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Transfer(c, &chosen, items, mujica.TransferOptions{}) + core.Use(c, core.Settings{}) + input.Transfer(c, &chosen, items, input.TransferOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/051-calendar.md b/docs/components/051-calendar.md index a971a8f..3b41999 100644 --- a/docs/components/051-calendar.md +++ b/docs/components/051-calendar.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -day := mujica.Date{Year: 2026, Month: time.October, Day: 5} +day := core.Date{Year: 2026, Month: time.October, Day: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Calendar(c, &day, mujica.CalendarOptions{WeekStart: mujica.WeekStartMonday}).Changed() { + core.Use(c, core.Settings{}) + if datetime.Calendar(c, &day, datetime.CalendarOptions{WeekStart: datetime.WeekStartMonday}).Changed() { fmt.Println("chosen", day) } } diff --git a/docs/components/052-date-picker.md b/docs/components/052-date-picker.md index abe7c9a..7174d89 100644 --- a/docs/components/052-date-picker.md +++ b/docs/components/052-date-picker.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -due := mujica.Date{Year: 2026, Month: time.October, Day: 5} +due := core.Date{Year: 2026, Month: time.October, Day: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DatePickerOptions{Label: "Due date", Clearable: true, - Min: mujica.Date{Year: 2026, Month: time.January, Day: 1}} - if mujica.DatePicker(c, &due, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DatePickerOptions{Label: "Due date", Clearable: true, + Min: core.Date{Year: 2026, Month: time.January, Day: 1}} + if datetime.DatePicker(c, &due, opts).Changed() { fmt.Println("due", due) } } diff --git a/docs/components/053-date-range-picker.md b/docs/components/053-date-range-picker.md index a95e3db..c1184c1 100644 --- a/docs/components/053-date-range-picker.md +++ b/docs/components/053-date-range-picker.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var stay mujica.DateRange +var stay datetime.DateRange view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DateRangePicker(c, &stay, mujica.DateRangePickerOptions{Label: "Stay"}) + core.Use(c, core.Settings{}) + r := datetime.DateRangePicker(c, &stay, datetime.DateRangePickerOptions{Label: "Stay"}) if r.Changed() { fmt.Println("stay", stay.Start, stay.End, stay.Days()) } diff --git a/docs/components/054-time-picker.md b/docs/components/054-time-picker.md index d57d23f..4cb828f 100644 --- a/docs/components/054-time-picker.md +++ b/docs/components/054-time-picker.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -alarm := mujica.TimeOfDay{Hour: 7, Minute: 30} +alarm := datetime.TimeOfDay{Hour: 7, Minute: 30} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} - if mujica.TimePicker(c, &alarm, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} + if datetime.TimePicker(c, &alarm, opts).Changed() { fmt.Println("alarm", alarm) } } diff --git a/docs/components/055-time-range-picker.md b/docs/components/055-time-range-picker.md index e3c0fd0..989af9d 100644 --- a/docs/components/055-time-range-picker.md +++ b/docs/components/055-time-range-picker.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -shift := mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 22}, End: mujica.TimeOfDay{Hour: 6}} +shift := datetime.TimeRange{Start: datetime.TimeOfDay{Hour: 22}, End: datetime.TimeOfDay{Hour: 6}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} - if mujica.TimeRangePicker(c, &shift, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} + if datetime.TimeRangePicker(c, &shift, opts).Changed() { fmt.Println("shift", shift.Start, shift.End, shift.Overnight()) } } diff --git a/docs/components/056-date-time-picker.md b/docs/components/056-date-time-picker.md index 1adcf08..dc9444a 100644 --- a/docs/components/056-date-time-picker.md +++ b/docs/components/056-date-time-picker.md @@ -13,9 +13,9 @@ if err != nil { } var meeting time.Time view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} - if mujica.DateTimePicker(c, &meeting, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} + if datetime.DateTimePicker(c, &meeting, opts).Changed() { fmt.Println("meeting", meeting) } } diff --git a/docs/components/057-month-picker.md b/docs/components/057-month-picker.md index 4179854..52cc2e4 100644 --- a/docs/components/057-month-picker.md +++ b/docs/components/057-month-picker.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var month mujica.YearMonth +var month datetime.YearMonth view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: true} - if mujica.MonthPicker(c, &month, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.MonthPickerOptions{Label: "Billing month", Clearable: true} + if datetime.MonthPicker(c, &month, opts).Changed() { fmt.Println("month", month) } } diff --git a/docs/components/058-year-picker.md b/docs/components/058-year-picker.md index 4e1d204..e858991 100644 --- a/docs/components/058-year-picker.md +++ b/docs/components/058-year-picker.md @@ -9,9 +9,9 @@ ```go year := 2026 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} - if mujica.YearPicker(c, &year, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} + if datetime.YearPicker(c, &year, opts).Changed() { fmt.Println("year", year) } } diff --git a/docs/components/059-tabs.md b/docs/components/059-tabs.md index 9b1b930..5980316 100644 --- a/docs/components/059-tabs.md +++ b/docs/components/059-tabs.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -if mujica.Tabs(c, &tab, []mujica.Tab{ +if navigation.Tabs(c, &tab, []navigation.Tab{ {Label: "General", Panel: func() { ui.Text(c, "General settings") }}, {Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }}, {Label: "Archive", Disabled: true}, -}, mujica.TabsOptions{Label: "Settings"}).Changed() { +}, navigation.TabsOptions{Label: "Settings"}).Changed() { fmt.Println("tab", tab) } ``` diff --git a/docs/components/060-segmented-control.md b/docs/components/060-segmented-control.md index 5802afa..737b11a 100644 --- a/docs/components/060-segmented-control.md +++ b/docs/components/060-segmented-control.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -if mujica.SegmentedControl(c, &view, []mujica.Segment{ +if navigation.SegmentedControl(c, &view, []navigation.Segment{ {Label: "List", Icon: icons.Must("list"), HideLabel: true}, {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true}, -}, mujica.SegmentedControlOptions{Label: "View"}).Changed() { +}, navigation.SegmentedControlOptions{Label: "View"}).Changed() { fmt.Println("view", view) } ``` diff --git a/docs/components/061-breadcrumb.md b/docs/components/061-breadcrumb.md index 31829d4..3b1c4f1 100644 --- a/docs/components/061-breadcrumb.md +++ b/docs/components/061-breadcrumb.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok { +if i, ok := navigation.Breadcrumb(c, path, navigation.BreadcrumbOptions{}).Chosen(); ok { path = path[:i+1] } ``` diff --git a/docs/components/062-pagination.md b/docs/components/062-pagination.md index d4fa70e..6c1693e 100644 --- a/docs/components/062-pagination.md +++ b/docs/components/062-pagination.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { +if navigation.Pagination(c, &page, &size, navigation.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { fmt.Println("load page", page, "of size", size) } ``` diff --git a/docs/components/063-steps.md b/docs/components/063-steps.md index 3d83ccc..9dfd8a8 100644 --- a/docs/components/063-steps.md +++ b/docs/components/063-steps.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -mujica.Steps(c, ¤t, []mujica.Step{ +navigation.Steps(c, ¤t, []navigation.Step{ {Title: "Account", Clickable: true}, {Title: "Profile", Description: "Name and seal"}, {Title: "Confirm"}, -}, mujica.StepsOptions{Label: "Sign-up"}) +}, navigation.StepsOptions{Label: "Sign-up"}) ``` 签名:`func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceResult` diff --git a/docs/components/064-sidebar.md b/docs/components/064-sidebar.md index 3b76d6b..73373a4 100644 --- a/docs/components/064-sidebar.md +++ b/docs/components/064-sidebar.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -mujica.Sidebar(c, &selected, []mujica.SidebarSection{ - {Title: "Mail", Items: []mujica.SidebarItem{ +navigation.Sidebar(c, &selected, []navigation.SidebarSection{ + {Title: "Mail", Items: []navigation.SidebarItem{ {ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: 3}, - {Label: "Folders", Items: []mujica.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, + {Label: "Folders", Items: []navigation.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, }}, -}, mujica.SidebarOptions{}) +}, navigation.SidebarOptions{}) ``` 签名:`func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) ChoiceResult` diff --git a/docs/components/065-navigation-menu.md b/docs/components/065-navigation-menu.md index 032f112..9ee8978 100644 --- a/docs/components/065-navigation-menu.md +++ b/docs/components/065-navigation-menu.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -mujica.NavigationMenu(c, router, []mujica.NavigationLink{ +navigation.NavigationMenu(c, router, []navigation.NavigationLink{ {Label: "Home", Path: "/"}, - {Label: "Library", Items: []mujica.NavigationLink{ + {Label: "Library", Items: []navigation.NavigationLink{ {Label: "Poems", Path: "/library/poems", Description: "Verse of the court"}, }}, -}, mujica.NavigationMenuOptions{History: true}) +}, navigation.NavigationMenuOptions{History: true}) router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) }) ``` @@ -58,6 +58,6 @@ router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) }) ## 限制 - MyGo 历史按钮的提示文字固定为英文 “Back” / “Forward”(`ui/router.go:713`);本组件覆盖了无障碍名称,但提示仍为英文。 -- 历史按钮的外观来自 MyGo 主题按钮样式(经 `mujica.Use` 映射为 MujicaUI 颜色)。 +- 历史按钮的外观来自 MyGo 主题按钮样式(经 `core.Use` 映射为 MujicaUI 颜色)。 - 弹层打开时在 160 ms 内淡入,减少动画时直接显示。关闭时立即消失:弹层关闭后 MyGo 不再构建其元素,公开接口无法为已移除的浮层做淡出。 - 下拉面板最高为锚点上方或下方可用空间,超出时在面板内滚动;方向键移动焦点时自动滚入视野。 diff --git a/docs/components/066-menubar.md b/docs/components/066-menubar.md index c78c291..07d2bc9 100644 --- a/docs/components/066-menubar.md +++ b/docs/components/066-menubar.md @@ -7,15 +7,15 @@ ## 最小示例 ```go -res := mujica.Menubar(c, []mujica.MenubarMenu{ - {Label: "File", Items: []mujica.MenubarItem{ - {ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, - {Label: "Export", Items: []mujica.MenubarItem{{ID: "pdf", Label: "PDF"}}}, +res := navigation.Menubar(c, []navigation.MenubarMenu{ + {Label: "File", Items: []navigation.MenubarItem{ + {ID: "new", Label: "New", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {Label: "Export", Items: []navigation.MenubarItem{{ID: "pdf", Label: "PDF"}}}, }}, - {Label: "View", Items: []mujica.MenubarItem{ - {ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: wrap}, + {Label: "View", Items: []navigation.MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: navigation.MenubarCheck, Checked: wrap}, }}, -}, mujica.MenubarOptions{}) +}, navigation.MenubarOptions{}) if id, ok := res.Chosen(); ok && id == "wrap" { wrap = !wrap } diff --git a/docs/components/067-toolbar.md b/docs/components/067-toolbar.md index 45aeda3..a130f15 100644 --- a/docs/components/067-toolbar.md +++ b/docs/components/067-toolbar.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() { - mujica.ToolbarGroup(c, func() { - mujica.ToolbarButton(c, "Bold", mujica.ToolbarButtonOptions{Icon: icons.Must("bold")}) - mujica.ToolbarButton(c, "Italic", mujica.ToolbarButtonOptions{Icon: icons.Must("italic")}) +navigation.Toolbar(c, navigation.ToolbarOptions{Label: "Format"}, func() { + navigation.ToolbarGroup(c, func() { + navigation.ToolbarButton(c, "Bold", navigation.ToolbarButtonOptions{Icon: icons.Must("bold")}) + navigation.ToolbarButton(c, "Italic", navigation.ToolbarButtonOptions{Icon: icons.Must("italic")}) }) - mujica.ToolbarSeparator(c) - if mujica.ToolbarButton(c, "Copy", mujica.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { + navigation.ToolbarSeparator(c) + if navigation.ToolbarButton(c, "Copy", navigation.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { fmt.Println("copy") } }) diff --git a/docs/components/068-command-palette.md b/docs/components/068-command-palette.md index 9fa21f3..e897793 100644 --- a/docs/components/068-command-palette.md +++ b/docs/components/068-command-palette.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -res := mujica.CommandPalette(c, &open, []mujica.Command{ +res := navigation.CommandPalette(c, &open, []navigation.Command{ {ID: "new", Label: "New document", Group: "File"}, {ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"night"}}, -}, mujica.CommandPaletteOptions{}) +}, navigation.CommandPaletteOptions{}) if id, ok := res.Chosen(); ok { fmt.Println("run", id) } diff --git a/docs/components/069-card.md b/docs/components/069-card.md index e959146..d8dad86 100644 --- a/docs/components/069-card.md +++ b/docs/components/069-card.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { - mujica.Button(c, "Open", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + data.Card(c, data.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + input.Button(c, "Open", input.ButtonOptions{}) }}, func() { ui.Text(c, "Letters of the court, sealed in wax.") }).Width(280) diff --git a/docs/components/070-list.md b/docs/components/070-list.md index d8f9667..e56c173 100644 --- a/docs/components/070-list.md +++ b/docs/components/070-list.md @@ -8,10 +8,10 @@ ```go names := []string{"Matins", "Lauds", "Prime", "Terce"} -var state mujica.ListState[string] +var state data.ListState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + core.Use(c, core.Settings{}) + v := data.List(c, &state, len(names), data.ListOptions[string]{ Key: func(i int) string { return names[i] }, Label: func(i int) string { return names[i] }, }, func(i int) { ui.Text(c, names[i]) }) diff --git a/docs/components/071-data-table.md b/docs/components/071-data-table.md index f62dbb4..4f2a509 100644 --- a/docs/components/071-data-table.md +++ b/docs/components/071-data-table.md @@ -13,10 +13,10 @@ type relic struct { Age int } rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} -var state mujica.DataTableState[int] -opts := mujica.DataTableOptions[relic, int]{ +var state data.DataTableState[int] +opts := data.DataTableOptions[relic, int]{ Key: func(r relic) int { return r.ID }, - Columns: []mujica.DataColumn[relic]{ + Columns: []data.DataColumn[relic]{ {ID: "name", Title: "Name", Sortable: true, Text: func(r relic) string { return r.Name }, Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, @@ -26,8 +26,8 @@ opts := mujica.DataTableOptions[relic, int]{ }, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DataTable(c, &state, rows, opts).Element.Size(360, 240) + core.Use(c, core.Settings{}) + data.DataTable(c, &state, rows, opts).Element.Size(360, 240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/072-tree.md b/docs/components/072-tree.md index 65d4244..d8a81eb 100644 --- a/docs/components/072-tree.md +++ b/docs/components/072-tree.md @@ -8,10 +8,10 @@ ```go kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} -var state mujica.TreeState[string] +var state data.TreeState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Tree(c, &state, mujica.TreeOptions[string]{ + core.Use(c, core.Settings{}) + data.Tree(c, &state, data.TreeOptions[string]{ Roots: []string{"Nave", "Sacristy"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, diff --git a/docs/components/073-tree-table.md b/docs/components/073-tree-table.md index ae408ad..0f35efd 100644 --- a/docs/components/073-tree-table.md +++ b/docs/components/073-tree-table.md @@ -9,14 +9,14 @@ ```go kids := map[string][]string{"Vestry": {"Stole", "Alb"}} size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} -var state mujica.TreeTableState[string] +var state data.TreeTableState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + core.Use(c, core.Settings{}) + data.TreeTable(c, &state, data.TreeTableOptions[string]{ Roots: []string{"Vestry", "Organ"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, - Columns: []mujica.TreeColumn[string]{ + Columns: []data.TreeColumn[string]{ {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, Compare: cmp.Compare[string]}, {ID: "size", Title: "Size", Width: 80, Align: ui.End, diff --git a/docs/components/074-accordion.md b/docs/components/074-accordion.md index df78411..0dd7eee 100644 --- a/docs/components/074-accordion.md +++ b/docs/components/074-accordion.md @@ -9,11 +9,11 @@ ```go open := []string{"choir"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Accordion(c, &open, []mujica.AccordionItem{ + core.Use(c, core.Settings{}) + data.Accordion(c, &open, []data.AccordionItem{ {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, - }, mujica.AccordionOptions{}) + }, data.AccordionOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/075-collapsible.md b/docs/components/075-collapsible.md index 9747308..db2f791 100644 --- a/docs/components/075-collapsible.md +++ b/docs/components/075-collapsible.md @@ -9,8 +9,8 @@ ```go open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + core.Use(c, core.Settings{}) + data.Collapsible(c, &open, data.CollapsibleOptions{Title: "Details"}, func() { ui.Text(c, "Candles are lit at the seventh bell.") }) } diff --git a/docs/components/076-timeline.md b/docs/components/076-timeline.md index 5512437..29a3c9b 100644 --- a/docs/components/076-timeline.md +++ b/docs/components/076-timeline.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Timeline(c, []mujica.TimelineItem{ - {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, - {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + core.Use(c, core.Settings{}) + data.Timeline(c, []data.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: data.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: data.TimelineActive}, {Time: "18:00", Title: "Vespers"}, }) } diff --git a/docs/components/077-description-list.md b/docs/components/077-description-list.md index 5fce41d..a084bcf 100644 --- a/docs/components/077-description-list.md +++ b/docs/components/077-description-list.md @@ -8,12 +8,12 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DescriptionList(c, []mujica.DescriptionItem{ + core.Use(c, core.Settings{}) + data.DescriptionList(c, []data.DescriptionItem{ {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, {Label: "Shelf", Value: "North vault, row 7"}, {Label: "Seal", Value: "Wine-red wax", Span: -1}, - }, mujica.DescriptionListOptions{}) + }, data.DescriptionListOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/078-statistic.md b/docs/components/078-statistic.md index d04bcc0..f07ff9b 100644 --- a/docs/components/078-statistic.md +++ b/docs/components/078-statistic.md @@ -9,8 +9,8 @@ ```go change := 12.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Statistic(c, 12840, mujica.StatisticOptions{ + core.Use(c, core.Settings{}) + data.Statistic(c, 12840, data.StatisticOptions{ Title: "Pilgrims", Unit: "souls", Precision: 0, Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", }) diff --git a/docs/components/079-image-viewer.md b/docs/components/079-image-viewer.md index d0f59b4..fa4844e 100644 --- a/docs/components/079-image-viewer.md +++ b/docs/components/079-image-viewer.md @@ -10,8 +10,8 @@ picture := ui.MustParseSVG([]byte(``)) open, index := true, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: "Rose window"}}) + core.Use(c, core.Settings{}) + data.ImageViewer(c, &open, &index, []data.ViewerImage{{Source: picture, Alt: "Rose window"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/080-carousel.md b/docs/components/080-carousel.md index 6233f17..4a92189 100644 --- a/docs/components/080-carousel.md +++ b/docs/components/080-carousel.md @@ -9,8 +9,8 @@ ```go index := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + core.Use(c, core.Settings{}) + data.Carousel(c, &index, 3, data.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) }).Element.Size(360, 220) } diff --git a/docs/components/081-dialog.md b/docs/components/081-dialog.md index bbb63dd..7f2b01d 100644 --- a/docs/components/081-dialog.md +++ b/docs/components/081-dialog.md @@ -10,19 +10,19 @@ open := true name := "Ophelia" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Rename…", input.ButtonOptions{}).Clicked() { open = true } - mujica.Dialog(c, &open, mujica.DialogOptions{ + overlay.Dialog(c, &open, overlay.DialogOptions{ Title: "Rename the chamber", Description: "The new name shows in every hall.", Decorated: true, Actions: func() { - if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}).Clicked() { open = false } - if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Rename", input.ButtonOptions{}).Clicked() { open = false } }, diff --git a/docs/components/082-alert-dialog.md b/docs/components/082-alert-dialog.md index d3a8a18..946f03d 100644 --- a/docs/components/082-alert-dialog.md +++ b/docs/components/082-alert-dialog.md @@ -9,11 +9,11 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Delete archive", input.ButtonOptions{Variant: input.Danger}).Clicked() { open = true } - r := mujica.AlertDialog(c, &open, mujica.AlertDialogOptions{ + r := overlay.AlertDialog(c, &open, overlay.AlertDialogOptions{ Title: "Delete the archive?", Description: "Twelve letters will be destroyed.", ConfirmText: "archive", diff --git a/docs/components/083-drawer.md b/docs/components/083-drawer.md index 5f2a080..49cc7c0 100644 --- a/docs/components/083-drawer.md +++ b/docs/components/083-drawer.md @@ -9,11 +9,11 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Filters", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Filters", input.ButtonOptions{}).Clicked() { open = true } - mujica.Drawer(c, &open, mujica.DrawerOptions{Title: "Filters", Side: mujica.DrawerRight, Size: 320}, func() { + overlay.Drawer(c, &open, overlay.DrawerOptions{Title: "Filters", Side: overlay.DrawerRight, Size: 320}, func() { ui.Text(c, "Guests are placed by rank.") }) } diff --git a/docs/components/084-popover.md b/docs/components/084-popover.md index 96b43a4..05bc63b 100644 --- a/docs/components/084-popover.md +++ b/docs/components/084-popover.md @@ -9,12 +9,12 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Guests", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { open = !open } - mujica.Popover(c, b, &open, mujica.PopoverOptions{Title: "Guest list"}, func() { + overlay.Popover(c, b, &open, overlay.PopoverOptions{Title: "Guest list"}, func() { ui.Text(c, "Twelve guests confirmed.") }) } diff --git a/docs/components/085-tooltip.md b/docs/components/085-tooltip.md index 0700c27..9edda47 100644 --- a/docs/components/085-tooltip.md +++ b/docs/components/085-tooltip.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) - mujica.Tooltip(c, b, "Seal the letter with wax", mujica.TooltipOptions{}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) + overlay.Tooltip(c, b, "Seal the letter with wax", overlay.TooltipOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/086-hover-card.md b/docs/components/086-hover-card.md index 16701ac..421aaf5 100644 --- a/docs/components/086-hover-card.md +++ b/docs/components/086-hover-card.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost}) - mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() { + core.Use(c, core.Settings{}) + b := input.Button(c, "Lady Sakiko", input.ButtonOptions{Variant: input.Ghost}) + overlay.HoverCard(c, b, overlay.HoverCardOptions{}, func() { ui.Text(c, "Keyboardist of the court.") }) } diff --git a/docs/components/087-dropdown-menu.md b/docs/components/087-dropdown-menu.md index d47d21f..53959cc 100644 --- a/docs/components/087-dropdown-menu.md +++ b/docs/components/087-dropdown-menu.md @@ -9,19 +9,19 @@ ```go pinned := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{}, func(m *mujica.PopupMenu) { + core.Use(c, core.Settings{}) + overlay.DropdownMenu(c, "Actions", overlay.DropdownMenuOptions{}, func(m *overlay.PopupMenu) { m.Group("Letter", func() { - if m.Item("Rename", mujica.PopupMenuItemOptions{}) { + if m.Item("Rename", overlay.PopupMenuItemOptions{}) { fmt.Println("rename") } - m.Check("Pinned", &pinned, mujica.PopupMenuItemOptions{KeepOpen: true}) + m.Check("Pinned", &pinned, overlay.PopupMenuItemOptions{KeepOpen: true}) }) - m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) { - m.Item("Archive", mujica.PopupMenuItemOptions{}) + m.Submenu("Move to", overlay.PopupMenuItemOptions{}, func(m *overlay.PopupMenu) { + m.Item("Archive", overlay.PopupMenuItemOptions{}) }) m.Separator() - m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: true}) + m.Item("Burn", overlay.PopupMenuItemOptions{Disabled: true}) }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/088-context-menu.md b/docs/components/088-context-menu.md index f4f0117..d6e5893 100644 --- a/docs/components/088-context-menu.md +++ b/docs/components/088-context-menu.md @@ -8,15 +8,15 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) row := ui.Row(c).Padding(12).Children(func() { ui.Text(c, "Letter to the Countess") }) - mujica.ContextMenu(c, row, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Reply", mujica.PopupMenuItemOptions{}) { + overlay.ContextMenu(c, row, overlay.ContextMenuOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Reply", overlay.PopupMenuItemOptions{}) { fmt.Println("reply") } - m.Item("Delete", mujica.PopupMenuItemOptions{}) + m.Item("Delete", overlay.PopupMenuItemOptions{}) }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/089-popconfirm.md b/docs/components/089-popconfirm.md index 5d29d3e..b2f7317 100644 --- a/docs/components/089-popconfirm.md +++ b/docs/components/089-popconfirm.md @@ -9,12 +9,12 @@ ```go open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { open = true } - if mujica.Popconfirm(c, b, &open, mujica.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { + if overlay.Popconfirm(c, b, &open, overlay.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { fmt.Println("archived") } } diff --git a/docs/components/090-overlay.md b/docs/components/090-overlay.md index 4b20709..35cd17b 100644 --- a/docs/components/090-overlay.md +++ b/docs/components/090-overlay.md @@ -9,8 +9,8 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Overlay(c, &open, mujica.OverlayOptions{Place: mujica.OverlayBottomRight, Label: "Now playing"}, func() { + core.Use(c, core.Settings{}) + overlay.Overlay(c, &open, overlay.OverlayOptions{Place: overlay.OverlayBottomRight, Label: "Now playing"}, func() { ui.Text(c, "Symbol I: △") }) } diff --git a/docs/components/091-alert.md b/docs/components/091-alert.md index fef8f3d..9b7e684 100644 --- a/docs/components/091-alert.md +++ b/docs/components/091-alert.md @@ -8,8 +8,8 @@ ```go open := true -r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ - Severity: mujica.SeverityError, +r := feedback.Alert(c, &open, "The archive could not be read.", feedback.AlertOptions{ + Severity: core.SeverityError, Title: "Import failed", Action: "Retry", }) diff --git a/docs/components/092-toast.md b/docs/components/092-toast.md index 63a6f59..d3ab329 100644 --- a/docs/components/092-toast.md +++ b/docs/components/092-toast.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { - mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) +if input.Button(c, "Delete note", input.ButtonOptions{Variant: input.Danger}).Clicked() { + feedback.ShowToast(c, feedback.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) } // Last in the view: -if r := mujica.Toaster(c); r.Action == "note-1" { +if r := feedback.Toaster(c); r.Action == "note-1" { // restore the note } ``` diff --git a/docs/components/093-progress.md b/docs/components/093-progress.md index ebfc47f..ed7ef0a 100644 --- a/docs/components/093-progress.md +++ b/docs/components/093-progress.md @@ -7,9 +7,9 @@ ## 最小示例 ```go -mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) -mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) -mujica.Meter(c, 93, mujica.MeterOptions{ +feedback.Progress(c, 0.62, feedback.ProgressOptions{Label: "Uploading", ShowValue: true}) +feedback.Progress(c, 0, feedback.ProgressOptions{Shape: feedback.ProgressRing, Indeterminate: true, Label: "Connecting"}) +feedback.Meter(c, 93, feedback.MeterOptions{ Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, }) diff --git a/docs/components/094-spinner.md b/docs/components/094-spinner.md index f4698c3..6c60dca 100644 --- a/docs/components/094-spinner.md +++ b/docs/components/094-spinner.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) +core.Spinner(c, core.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) ``` 签名:`func Spinner(c *ui.Context, opts SpinnerOptions) *ui.Element` diff --git a/docs/components/095-skeleton.md b/docs/components/095-skeleton.md index 418cf1f..b21610a 100644 --- a/docs/components/095-skeleton.md +++ b/docs/components/095-skeleton.md @@ -8,9 +8,9 @@ ```go ui.Row(c).Padding(16).Gap(12).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + feedback.Skeleton(c, feedback.SkeletonOptions{Shape: feedback.SkeletonCircle}) ui.Column(c).Grow(1).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + feedback.Skeleton(c, feedback.SkeletonOptions{Lines: 2}) }) }) ``` diff --git a/docs/components/096-empty-state.md b/docs/components/096-empty-state.md index 4c4d822..cba8b2a 100644 --- a/docs/components/096-empty-state.md +++ b/docs/components/096-empty-state.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -r := mujica.EmptyState(c, mujica.EmptyStateOptions{ - Kind: mujica.EmptyNoResults, +r := feedback.EmptyState(c, feedback.EmptyStateOptions{ + Kind: feedback.EmptyNoResults, Query: query, Action: "Clear search", }) diff --git a/docs/components/097-result.md b/docs/components/097-result.md index 30d0285..1017a46 100644 --- a/docs/components/097-result.md +++ b/docs/components/097-result.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -r := mujica.Result(c, mujica.ResultOptions{ - Kind: mujica.ResultForbidden, +r := feedback.Result(c, feedback.ResultOptions{ + Kind: feedback.ResultForbidden, Description: "You need the Editor role to change billing.", Actions: []string{"Request access", "Go back"}, }) diff --git a/docs/components/098-status-indicator.md b/docs/components/098-status-indicator.md index 63d659d..71c3e8f 100644 --- a/docs/components/098-status-indicator.md +++ b/docs/components/098-status-indicator.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) -mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{}) +feedback.StatusIndicator(c, feedback.StatusSuccess, feedback.StatusIndicatorOptions{Label: "API online"}) +feedback.StatusIndicator(c, feedback.StatusOffline, feedback.StatusIndicatorOptions{}) ``` 签名:`func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) *ui.Element` diff --git a/docs/components/099-notification-center.md b/docs/components/099-notification-center.md index 95b16fa..6b907eb 100644 --- a/docs/components/099-notification-center.md +++ b/docs/components/099-notification-center.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) +r := feedback.NotificationCenter(c, &items, feedback.NotificationCenterOptions{}) if r.Action == "a" { // open the build log } diff --git a/docs/components/100-loading-overlay.md b/docs/components/100-loading-overlay.md index e94b819..2d097bc 100644 --- a/docs/components/100-loading-overlay.md +++ b/docs/components/100-loading-overlay.md @@ -7,9 +7,9 @@ ## 最小示例 ```go -r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { +r := feedback.LoadingOverlay(c, loading, feedback.LoadingOverlayOptions{Cancel: "Cancel"}, func() { ui.Text(c, "Quarterly report") - mujica.Button(c, "Export", mujica.ButtonOptions{}) + input.Button(c, "Export", input.ButtonOptions{}) }) if r.Canceled { loading = false diff --git a/docs/components/101-line-chart.md b/docs/components/101-line-chart.md index aefc23b..749565a 100644 --- a/docs/components/101-line-chart.md +++ b/docs/components/101-line-chart.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -series := []mujica.ChartSeries{ +series := []chart.ChartSeries{ {Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}}, {Name: "Candles", Values: []float64{5, 2, 6, 7, 1}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineChart(c, series, mujica.LineChartOptions{Frame: mujica.ChartFrame{Label: "Chapel"}}) + core.Use(c, core.Settings{}) + chart.LineChart(c, series, chart.LineChartOptions{Frame: chart.ChartFrame{Label: "Chapel"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/102-area-chart.md b/docs/components/102-area-chart.md index bf747b7..6a4c770 100644 --- a/docs/components/102-area-chart.md +++ b/docs/components/102-area-chart.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -series := []mujica.ChartSeries{ +series := []chart.ChartSeries{ {Name: "Matins", Values: []float64{3, 4, 6, 5}}, {Name: "Vespers", Values: []float64{2, 3, 2, 4}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: mujica.ChartFrame{Label: "Attendance"}, Stacked: true}) + core.Use(c, core.Settings{}) + chart.AreaChart(c, series, chart.AreaChartOptions{Frame: chart.ChartFrame{Label: "Attendance"}, Stacked: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/103-bar-chart.md b/docs/components/103-bar-chart.md index d162090..324e205 100644 --- a/docs/components/103-bar-chart.md +++ b/docs/components/103-bar-chart.md @@ -8,10 +8,10 @@ ```go categories := []string{"Nave", "Choir", "Crypt"} -series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} +series := []chart.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarChart(c, categories, series, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Tithes"}, Labels: true}) + core.Use(c, core.Settings{}) + chart.BarChart(c, categories, series, chart.BarChartOptions{Frame: chart.ChartFrame{Label: "Tithes"}, Labels: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/104-pie-chart.md b/docs/components/104-pie-chart.md index a2e760c..6026733 100644 --- a/docs/components/104-pie-chart.md +++ b/docs/components/104-pie-chart.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} +slices := []chart.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: mujica.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) + core.Use(c, core.Settings{}) + chart.PieChart(c, slices, chart.PieChartOptions{Frame: chart.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/105-scatter-chart.md b/docs/components/105-scatter-chart.md index baf1af2..8895368 100644 --- a/docs/components/105-scatter-chart.md +++ b/docs/components/105-scatter-chart.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var selected []mujica.ScatterKey -groups := []mujica.ScatterGroup{{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} +var selected []chart.ScatterKey +groups := []chart.ScatterGroup{{Name: "Monks", Points: []chart.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScatterChart(c, &selected, groups, mujica.ScatterChartOptions{Frame: mujica.ChartFrame{Label: "Choir"}, Bubbles: true}) + core.Use(c, core.Settings{}) + chart.ScatterChart(c, &selected, groups, chart.ScatterChartOptions{Frame: chart.ChartFrame{Label: "Choir"}, Bubbles: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/106-radar-chart.md b/docs/components/106-radar-chart.md index 35a017e..0ffdcac 100644 --- a/docs/components/106-radar-chart.md +++ b/docs/components/106-radar-chart.md @@ -8,10 +8,10 @@ ```go axes := []string{"Voice", "Latin", "Chant", "Organ"} -series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} +series := []chart.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RadarChart(c, axes, series, mujica.RadarChartOptions{Frame: mujica.ChartFrame{Label: "Skills"}}) + core.Use(c, core.Settings{}) + chart.RadarChart(c, axes, series, chart.RadarChartOptions{Frame: chart.ChartFrame{Label: "Skills"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/107-heatmap-chart.md b/docs/components/107-heatmap-chart.md index b931ea2..690df9d 100644 --- a/docs/components/107-heatmap-chart.md +++ b/docs/components/107-heatmap-chart.md @@ -10,8 +10,8 @@ rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"} values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: mujica.ChartFrame{Label: "Hours"}}) + core.Use(c, core.Settings{}) + chart.HeatmapChart(c, rows, cols, values, chart.HeatmapChartOptions{Frame: chart.ChartFrame{Label: "Hours"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/108-calendar-heatmap.md b/docs/components/108-calendar-heatmap.md index 26f6a30..ef10d77 100644 --- a/docs/components/108-calendar-heatmap.md +++ b/docs/components/108-calendar-heatmap.md @@ -8,10 +8,10 @@ ```go year := 2026 -values := map[mujica.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} +values := map[core.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CalendarHeatmap(c, &year, values, mujica.CalendarHeatmapOptions{Frame: mujica.ChartFrame{Label: "Prayers"}}) + core.Use(c, core.Settings{}) + chart.CalendarHeatmap(c, &year, values, chart.CalendarHeatmapOptions{Frame: chart.ChartFrame{Label: "Prayers"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/109-histogram.md b/docs/components/109-histogram.md index 3817d33..b177b22 100644 --- a/docs/components/109-histogram.md +++ b/docs/components/109-histogram.md @@ -9,8 +9,8 @@ ```go values := []float64{1, 2, 2, 3, 4, 5, 9, 10} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Histogram(c, values, mujica.HistogramOptions{Frame: mujica.ChartFrame{Label: "Ages"}, BinWidth: 5}) + core.Use(c, core.Settings{}) + chart.Histogram(c, values, chart.HistogramOptions{Frame: chart.ChartFrame{Label: "Ages"}, BinWidth: 5}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/110-box-plot.md b/docs/components/110-box-plot.md index bd80246..a24b594 100644 --- a/docs/components/110-box-plot.md +++ b/docs/components/110-box-plot.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} +groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}}) + core.Use(c, core.Settings{}) + chart.BoxPlot(c, groups, chart.BoxPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/111-waterfall-chart.md b/docs/components/111-waterfall-chart.md index 2dfb8c9..7b8eaf5 100644 --- a/docs/components/111-waterfall-chart.md +++ b/docs/components/111-waterfall-chart.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -steps := []mujica.WaterfallStep{ +steps := []chart.WaterfallStep{ {Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, - {Label: "Year", Kind: mujica.WaterfallTotal}, + {Label: "Year", Kind: chart.WaterfallTotal}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: mujica.ChartFrame{Label: "Ledger"}}) + core.Use(c, core.Settings{}) + chart.WaterfallChart(c, steps, chart.WaterfallChartOptions{Frame: chart.ChartFrame{Label: "Ledger"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/112-funnel-chart.md b/docs/components/112-funnel-chart.md index 436180c..8f69e33 100644 --- a/docs/components/112-funnel-chart.md +++ b/docs/components/112-funnel-chart.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} +stages := []chart.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: mujica.ChartFrame{Label: "Order"}}) + core.Use(c, core.Settings{}) + chart.FunnelChart(c, stages, chart.FunnelChartOptions{Frame: chart.ChartFrame{Label: "Order"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/113-bullet-chart.md b/docs/components/113-bullet-chart.md index aab7ffa..00659d8 100644 --- a/docs/components/113-bullet-chart.md +++ b/docs/components/113-bullet-chart.md @@ -9,8 +9,8 @@ ```go target := 250.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BulletChart(c, 270, mujica.BulletChartOptions{Frame: mujica.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) + core.Use(c, core.Settings{}) + chart.BulletChart(c, 270, chart.BulletChartOptions{Frame: chart.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/114-sparkline.md b/docs/components/114-sparkline.md index 7ff932f..cb9ae10 100644 --- a/docs/components/114-sparkline.md +++ b/docs/components/114-sparkline.md @@ -9,8 +9,8 @@ ```go values := []float64{3, 5, 4, 7, 6, 9} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: mujica.ChartFrame{Label: "Bells"}, Kind: mujica.SparklineArea}) + core.Use(c, core.Settings{}) + chart.Sparkline(c, values, chart.SparklineOptions{Frame: chart.ChartFrame{Label: "Bells"}, Kind: chart.SparklineArea}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/115-gauge.md b/docs/components/115-gauge.md index 64360d8..dbd5d63 100644 --- a/docs/components/115-gauge.md +++ b/docs/components/115-gauge.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}} +segments := []chart.GaugeSegment{{To: 60, Label: "Calm", Tone: chart.GaugeGood}, {To: 100, Label: "Crowded", Tone: chart.GaugeDanger}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Gauge(c, 72, mujica.GaugeOptions{Frame: mujica.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) + core.Use(c, core.Settings{}) + chart.Gauge(c, 72, chart.GaugeOptions{Frame: chart.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/116-treemap.md b/docs/components/116-treemap.md index e2347b4..e51bffc 100644 --- a/docs/components/116-treemap.md +++ b/docs/components/116-treemap.md @@ -8,13 +8,13 @@ ```go var path []string -root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, +root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, {Name: "Kitchen", Value: 25}, }} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Treemap(c, &path, root, mujica.TreemapOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) + core.Use(c, core.Settings{}) + chart.Treemap(c, &path, root, chart.TreemapOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/117-sunburst.md b/docs/components/117-sunburst.md index 908abbb..f1f06fb 100644 --- a/docs/components/117-sunburst.md +++ b/docs/components/117-sunburst.md @@ -8,13 +8,13 @@ ```go var path []string -root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, +root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, {Name: "Kitchen", Value: 25}, }} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sunburst(c, &path, root, mujica.SunburstOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) + core.Use(c, core.Settings{}) + chart.Sunburst(c, &path, root, chart.SunburstOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/118-sankey-chart.md b/docs/components/118-sankey-chart.md index f3568a3..48f9538 100644 --- a/docs/components/118-sankey-chart.md +++ b/docs/components/118-sankey-chart.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} -links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} +nodes := []chart.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} +links := []chart.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: mujica.ChartFrame{Label: "Money"}}) + core.Use(c, core.Settings{}) + chart.SankeyChart(c, nodes, links, chart.SankeyChartOptions{Frame: chart.ChartFrame{Label: "Money"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/119-network-graph.md b/docs/components/119-network-graph.md index 5677d5e..8da127a 100644 --- a/docs/components/119-network-graph.md +++ b/docs/components/119-network-graph.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -var state mujica.NetworkGraphState -nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} -edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} +var state chart.NetworkGraphState +nodes := []chart.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} +edges := []chart.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.NetworkGraph(c, &state, nodes, edges, mujica.NetworkGraphOptions{Frame: mujica.ChartFrame{Label: "House"}, Force: true}) + core.Use(c, core.Settings{}) + chart.NetworkGraph(c, &state, nodes, edges, chart.NetworkGraphOptions{Frame: chart.ChartFrame{Label: "House"}, Force: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/120-realtime-chart.md b/docs/components/120-realtime-chart.md index edb1c70..3be5655 100644 --- a/docs/components/120-realtime-chart.md +++ b/docs/components/120-realtime-chart.md @@ -7,15 +7,15 @@ ## 最小示例 ```go -buf := mujica.NewRealtimeBuffer(600, "Bell") +buf := chart.NewRealtimeBuffer(600, "Bell") start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) for i := range 60 { buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7)) } -var state mujica.RealtimeChartState +var state chart.RealtimeChartState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RealtimeChart(c, &state, buf, mujica.RealtimeChartOptions{Frame: mujica.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) + core.Use(c, core.Settings{}) + chart.RealtimeChart(c, &state, buf, chart.RealtimeChartOptions{Frame: chart.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/121-chart-legend.md b/docs/components/121-chart-legend.md index 00648b1..f02349e 100644 --- a/docs/components/121-chart-legend.md +++ b/docs/components/121-chart-legend.md @@ -9,10 +9,10 @@ ```go hidden := map[string]bool{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - items := []mujica.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} - mujica.ChartLegend(c, &hidden, items, mujica.ChartLegendOptions{MaxItems: 1}) + core.Use(c, core.Settings{}) + k := core.Tokens(c) + items := []chart.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} + chart.ChartLegend(c, &hidden, items, chart.ChartLegendOptions{MaxItems: 1}) } ui.Render(view, 400, 200, 1) ``` diff --git a/docs/components/122-chart-tooltip.md b/docs/components/122-chart-tooltip.md index 9d3e9bc..8e0af0f 100644 --- a/docs/components/122-chart-tooltip.md +++ b/docs/components/122-chart-tooltip.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - mujica.ChartTooltip(c, 40, 40, []mujica.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) + core.Use(c, core.Settings{}) + k := core.Tokens(c) + chart.ChartTooltip(c, 40, 40, []chart.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) } ui.Render(view, 400, 200, 1) ``` diff --git a/docs/components/123-chart-crosshair.md b/docs/components/123-chart-crosshair.md index 93c7be7..70cb32c 100644 --- a/docs/components/123-chart-crosshair.md +++ b/docs/components/123-chart-crosshair.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + hair := chart.ChartCrosshair(c, chart.ChartCrosshairOptions{X: &x, Y: &y}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} x.Draw(p, plot) diff --git a/docs/components/124-chart-axis.md b/docs/components/124-chart-axis.md index 62a17c6..4aa0cc8 100644 --- a/docs/components/124-chart-axis.md +++ b/docs/components/124-chart-axis.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - axis := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Kind: mujica.AxisLog, Min: 1, Max: 1000}) + core.Use(c, core.Settings{}) + axis := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Kind: chart.AxisLog, Min: 1, Max: 1000}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { axis.Draw(p, ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}) }) diff --git a/docs/components/125-chart-grid.md b/docs/components/125-chart-grid.md index 490bdf8..46ff153 100644 --- a/docs/components/125-chart-grid.md +++ b/docs/components/125-chart-grid.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - grid := mujica.ChartGrid(c, mujica.ChartGridOptions{X: &x, Y: &y, Minor: 2}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + grid := chart.ChartGrid(c, chart.ChartGridOptions{X: &x, Y: &y, Minor: 2}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} grid.Draw(p, plot) diff --git a/docs/components/126-chart-brush.md b/docs/components/126-chart-brush.md index 6c35456..efaf3e5 100644 --- a/docs/components/126-chart-brush.md +++ b/docs/components/126-chart-brush.md @@ -9,8 +9,8 @@ ```go value := [2]float64{20, 60} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartBrush(c, &value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) + core.Use(c, core.Settings{}) + chart.ChartBrush(c, &value, chart.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) } ui.Render(view, 400, 160, 1) ``` diff --git a/docs/components/127-chart-annotation.md b/docs/components/127-chart-annotation.md index 682bd16..335dd7b 100644 --- a/docs/components/127-chart-annotation.md +++ b/docs/components/127-chart-annotation.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -notes := []mujica.ChartNote{{Kind: mujica.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"}} +notes := []chart.ChartNote{{Kind: chart.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: chart.NoteBand, Value: 6, End: 8, Text: "Lent"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + annotation := chart.ChartAnnotation(c, notes, chart.ChartAnnotationOptions{X: &x, Y: &y}) box := ui.Box(c).Fill().Focusable().Label("Annotations") b := box.Bounds() annotation.Drag(box, ui.Rect{X: 60, Y: 20, W: b.W - 80, H: b.H - 60}) diff --git a/docs/components/128-chart-export.md b/docs/components/128-chart-export.md index 9286b82..e87c6f0 100644 --- a/docs/components/128-chart-export.md +++ b/docs/components/128-chart-export.md @@ -8,10 +8,10 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - data, err := mujica.ChartExport(c, func(c *ui.Context) { - mujica.SparkBar(c, []float64{3, -2, 5}, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}}) - }, mujica.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) + core.Use(c, core.Settings{}) + data, err := chart.ChartExport(c, func(c *ui.Context) { + chart.SparkBar(c, []float64{3, -2, 5}, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}}) + }, chart.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) if err != nil { panic(err) } diff --git a/docs/components/129-violin-plot.md b/docs/components/129-violin-plot.md index 25e5c94..de5354d 100644 --- a/docs/components/129-violin-plot.md +++ b/docs/components/129-violin-plot.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} +groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) + core.Use(c, core.Settings{}) + chart.ViolinPlot(c, groups, chart.ViolinPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/130-parallel-coordinates.md b/docs/components/130-parallel-coordinates.md index 220d642..2e72248 100644 --- a/docs/components/130-parallel-coordinates.md +++ b/docs/components/130-parallel-coordinates.md @@ -11,8 +11,8 @@ filters := map[string][2]float64{} axes := []string{"Age", "Hymns", "Vigils"} rows := [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ParallelCoordinates(c, &filters, axes, rows, mujica.ParallelCoordinatesOptions{Frame: mujica.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) + core.Use(c, core.Settings{}) + chart.ParallelCoordinates(c, &filters, axes, rows, chart.ParallelCoordinatesOptions{Frame: chart.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/131-chord-diagram.md b/docs/components/131-chord-diagram.md index 3dc1776..d0bf232 100644 --- a/docs/components/131-chord-diagram.md +++ b/docs/components/131-chord-diagram.md @@ -10,8 +10,8 @@ names := []string{"Nave", "Choir", "Crypt"} matrix := [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: mujica.ChartFrame{Label: "Paths"}}) + core.Use(c, core.Settings{}) + chart.ChordDiagram(c, names, matrix, chart.ChordDiagramOptions{Frame: chart.ChartFrame{Label: "Paths"}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/132-spark-bar.md b/docs/components/132-spark-bar.md index dd6644a..f81fc39 100644 --- a/docs/components/132-spark-bar.md +++ b/docs/components/132-spark-bar.md @@ -9,8 +9,8 @@ ```go values := []float64{3, -2, 4, 1, -3} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SparkBar(c, values, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}, BarWidth: 10}) + core.Use(c, core.Settings{}) + chart.SparkBar(c, values, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}, BarWidth: 10}) } ui.Render(view, 400, 100, 1) ``` diff --git a/docs/components/133-trend-indicator.md b/docs/components/133-trend-indicator.md index 173ccde..6217546 100644 --- a/docs/components/133-trend-indicator.md +++ b/docs/components/133-trend-indicator.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TrendIndicator(c, 12.5, mujica.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) + core.Use(c, core.Settings{}) + chart.TrendIndicator(c, 12.5, chart.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) } ui.Render(view, 400, 100, 1) ``` diff --git a/docs/components/138-density-plot.md b/docs/components/138-density-plot.md index a97920c..4617635 100644 --- a/docs/components/138-density-plot.md +++ b/docs/components/138-density-plot.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -groups := []mujica.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} +groups := []chart.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: mujica.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) + core.Use(c, core.Settings{}) + chart.DensityPlot(c, groups, chart.DensityPlotOptions{Frame: chart.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/140-chart-empty-state.md b/docs/components/140-chart-empty-state.md index 54dc64b..fedd3a4 100644 --- a/docs/components/140-chart-empty-state.md +++ b/docs/components/140-chart-empty-state.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartEmptyState(c, mujica.DataFailed, mujica.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) + core.Use(c, core.Settings{}) + chart.ChartEmptyState(c, data.DataFailed, chart.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) } ui.Render(view, 400, 240, 1) ``` diff --git a/docs/components/141-candlestick-chart.md b/docs/components/141-candlestick-chart.md index 4cfdd0e..ee27c47 100644 --- a/docs/components/141-candlestick-chart.md +++ b/docs/components/141-candlestick-chart.md @@ -8,15 +8,15 @@ K 线;涨跌色可配置(红涨或绿涨);缩放平移;万根级只绘 ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/142-ohlc-chart.md b/docs/components/142-ohlc-chart.md index 733ae36..25974fb 100644 --- a/docs/components/142-ohlc-chart.md +++ b/docs/components/142-ohlc-chart.md @@ -8,15 +8,15 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OHLCChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.OHLCChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/143-heikin-ashi-chart.md b/docs/components/143-heikin-ashi-chart.md index 3893d3c..82c71e7 100644 --- a/docs/components/143-heikin-ashi-chart.md +++ b/docs/components/143-heikin-ashi-chart.md @@ -8,16 +8,16 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView raw := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeikinAshiChart(c, candles, &window, &raw, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.HeikinAshiChart(c, candles, &window, &raw, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/144-line-quote-chart.md b/docs/components/144-line-quote-chart.md index 7b610b2..7426e7d 100644 --- a/docs/components/144-line-quote-chart.md +++ b/docs/components/144-line-quote-chart.md @@ -8,10 +8,10 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - points := []mujica.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} + points := []chart.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineQuoteChart(c, points, mujica.LineQuoteChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) + core.Use(c, core.Settings{}) + chart.LineQuoteChart(c, points, chart.LineQuoteChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/145-volume-chart.md b/docs/components/145-volume-chart.md index 9721a7e..f971763 100644 --- a/docs/components/145-volume-chart.md +++ b/docs/components/145-volume-chart.md @@ -8,15 +8,15 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeChart(c, candles, &window, mujica.VolumeChartOptions{Frame: mujica.ChartFrame{Label: "Volume"}}) + core.Use(c, core.Settings{}) + chart.VolumeChart(c, candles, &window, chart.VolumeChartOptions{Frame: chart.ChartFrame{Label: "Volume"}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/146-depth-chart.md b/docs/components/146-depth-chart.md index 11cfcf1..9fb2461 100644 --- a/docs/components/146-depth-chart.md +++ b/docs/components/146-depth-chart.md @@ -7,11 +7,11 @@ ## 最小示例 ```go - bids := []mujica.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} - asks := []mujica.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} + bids := []chart.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} + asks := []chart.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.DepthChart(c, bids, asks, chart.DepthChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/147-volume-profile.md b/docs/components/147-volume-profile.md index 14cb854..bf2bcd7 100644 --- a/docs/components/147-volume-profile.md +++ b/docs/components/147-volume-profile.md @@ -7,10 +7,10 @@ ## 最小示例 ```go - bins := []mujica.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} + bins := []chart.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.VolumeProfile(c, bins, chart.VolumeProfileOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/148-multi-chart-layout.md b/docs/components/148-multi-chart-layout.md index 718ae22..6f2fd59 100644 --- a/docs/components/148-multi-chart-layout.md +++ b/docs/components/148-multi-chart-layout.md @@ -7,11 +7,11 @@ ## 最小示例 ```go - state := mujica.MultiChartState{Count: 2} + state := chart.MultiChartState{Count: 2} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiChartLayout(c, &state, mujica.MultiChartLayoutOptions{Height: 200}, func(i int, cell *mujica.ChartCell) { - mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: "Symbol"}) + core.Use(c, core.Settings{}) + chart.MultiChartLayout(c, &state, chart.MultiChartLayoutOptions{Height: 200}, func(i int, cell *chart.ChartCell) { + input.TextInput(c, &cell.Symbol, input.TextInputOptions{Label: "Symbol"}) }) } ui.Render(view, 640, 400, 1) diff --git a/docs/components/149-chart-sync.md b/docs/components/149-chart-sync.md index 391f64f..d5a1a4e 100644 --- a/docs/components/149-chart-sync.md +++ b/docs/components/149-chart-sync.md @@ -8,17 +8,17 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - var sync mujica.ChartSync + var window chart.MarketView + var sync chart.ChartSync view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) sync.Link(c, "price") - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/150-time-range-selector.md b/docs/components/150-time-range-selector.md index 25cfaca..213fee9 100644 --- a/docs/components/150-time-range-selector.md +++ b/docs/components/150-time-range-selector.md @@ -7,10 +7,10 @@ ## 最小示例 ```go - value := mujica.MarketRange{Preset: mujica.Range1M} + value := chart.MarketRange{Preset: chart.Range1M} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeRangeSelector(c, &value, mujica.TimeRangeSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.TimeRangeSelector(c, &value, chart.TimeRangeSelectorOptions{}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/151-interval-selector.md b/docs/components/151-interval-selector.md index e64c82a..a7ab36a 100644 --- a/docs/components/151-interval-selector.md +++ b/docs/components/151-interval-selector.md @@ -7,11 +7,11 @@ ## 最小示例 ```go - value := mujica.Interval("1h") - favorites := []mujica.Interval{"1m", "1h", "1D"} + value := chart.Interval("1h") + favorites := []chart.Interval{"1m", "1h", "1D"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IntervalSelector(c, &value, &favorites, mujica.IntervalSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IntervalSelector(c, &value, &favorites, chart.IntervalSelectorOptions{}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/152-chart-type-switcher.md b/docs/components/152-chart-type-switcher.md index 29d8291..b4168fd 100644 --- a/docs/components/152-chart-type-switcher.md +++ b/docs/components/152-chart-type-switcher.md @@ -7,10 +7,10 @@ K 线、美国线、折线、面积、平均 K 线切换。 ## 最小示例 ```go - value := mujica.ChartCandles + value := chart.ChartCandles view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartTypeSwitcher(c, &value, mujica.ChartTypeSwitcherOptions{}) + core.Use(c, core.Settings{}) + chart.ChartTypeSwitcher(c, &value, chart.ChartTypeSwitcherOptions{}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/153-indicator-overlay.md b/docs/components/153-indicator-overlay.md index 00e4897..3a684ce 100644 --- a/docs/components/153-indicator-overlay.md +++ b/docs/components/153-indicator-overlay.md @@ -8,16 +8,16 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - overlays := []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{2}}} + var window chart.MarketView + overlays := []chart.IndicatorOverlay{{Kind: chart.IndicatorMA, Params: []float64{2}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/154-indicator-pane.md b/docs/components/154-indicator-pane.md index 0dbb6fb..9a03715 100644 --- a/docs/components/154-indicator-pane.md +++ b/docs/components/154-indicator-pane.md @@ -8,16 +8,16 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - panes := mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorRSI, Params: []float64{2}}}} + var window chart.MarketView + panes := chart.IndicatorPanes{Panes: []chart.IndicatorPaneSpec{{Kind: chart.IndicatorRSI, Params: []float64{2}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorPane(c, candles, &window, &panes, mujica.IndicatorPaneOptions{Height: 200, Label: "BTC"}) + core.Use(c, core.Settings{}) + chart.IndicatorPane(c, candles, &window, &panes, chart.IndicatorPaneOptions{Height: 200, Label: "BTC"}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/155-indicator-selector.md b/docs/components/155-indicator-selector.md index f9fa444..d79eade 100644 --- a/docs/components/155-indicator-selector.md +++ b/docs/components/155-indicator-selector.md @@ -7,11 +7,11 @@ ## 最小示例 ```go - var overlays []mujica.IndicatorOverlay - var panes mujica.IndicatorPanes + var overlays []chart.IndicatorOverlay + var panes chart.IndicatorPanes view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorSelector(c, &overlays, &panes, mujica.IndicatorSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IndicatorSelector(c, &overlays, &panes, chart.IndicatorSelectorOptions{}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/156-drawing-tools.md b/docs/components/156-drawing-tools.md index bd3e095..2339485 100644 --- a/docs/components/156-drawing-tools.md +++ b/docs/components/156-drawing-tools.md @@ -8,16 +8,16 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - drawings := mujica.DrawingTools{Tool: mujica.ToolTrendLine} + var window chart.MarketView + drawings := chart.DrawingTools{Tool: chart.ToolTrendLine} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/157-drawing-toolbar.md b/docs/components/157-drawing-toolbar.md index 931b8a5..457c1b7 100644 --- a/docs/components/157-drawing-toolbar.md +++ b/docs/components/157-drawing-toolbar.md @@ -7,10 +7,10 @@ ## 最小示例 ```go - var drawings mujica.DrawingTools + var drawings chart.DrawingTools view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DrawingToolbar(c, &drawings, mujica.DrawingToolbarOptions{Horizontal: true}) + core.Use(c, core.Settings{}) + chart.DrawingToolbar(c, &drawings, chart.DrawingToolbarOptions{Horizontal: true}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/158-compare-symbol.md b/docs/components/158-compare-symbol.md index 8774375..a56a365 100644 --- a/docs/components/158-compare-symbol.md +++ b/docs/components/158-compare-symbol.md @@ -8,17 +8,17 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - symbols := []mujica.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} + var window chart.MarketView + symbols := []chart.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CompareLegend(c, &symbols) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) + core.Use(c, core.Settings{}) + chart.CompareLegend(c, &symbols) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/159-price-alert-line.md b/docs/components/159-price-alert-line.md index 67175ea..54a5f9a 100644 --- a/docs/components/159-price-alert-line.md +++ b/docs/components/159-price-alert-line.md @@ -8,17 +8,17 @@ ```go at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - alerts := []mujica.PriceAlertLine{{ID: "take-profit", Price: 105}} + var window chart.MarketView + alerts := []chart.PriceAlertLine{{ID: "take-profit", Price: 105}} triggered := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert mujica.PriceAlertLine, last mujica.Candle) { + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert chart.PriceAlertLine, last chart.Candle) { triggered = alert.ID }}) ui.Text(c, triggered) diff --git a/docs/components/160-candle-countdown.md b/docs/components/160-candle-countdown.md index 9023ad9..25e162e 100644 --- a/docs/components/160-candle-countdown.md +++ b/docs/components/160-candle-countdown.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandleCountdown(c, "1h", mujica.CandleCountdownOptions{Location: time.UTC}) + core.Use(c, core.Settings{}) + chart.CandleCountdown(c, "1h", chart.CandleCountdownOptions{Location: time.UTC}) } ui.Render(view, 640, 400, 1) ``` diff --git a/docs/components/161-ticker-tape.md b/docs/components/161-ticker-tape.md index e97ec6a..5dc0bd3 100644 --- a/docs/components/161-ticker-tape.md +++ b/docs/components/161-ticker-tape.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -items := []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} +items := []finance.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TickerTape(c, items, mujica.TickerTapeOptions{}) + core.Use(c, core.Settings{}) + finance.TickerTape(c, items, finance.TickerTapeOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/162-quote-card.md b/docs/components/162-quote-card.md index 6df9188..3d19635 100644 --- a/docs/components/162-quote-card.md +++ b/docs/components/162-quote-card.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -quote := mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} +quote := finance.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuoteCard(c, quote, mujica.QuoteCardOptions{}) + core.Use(c, core.Settings{}) + finance.QuoteCard(c, quote, finance.QuoteCardOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/163-price-text.md b/docs/components/163-price-text.md index a13c7a7..9de00ea 100644 --- a/docs/components/163-price-text.md +++ b/docs/components/163-price-text.md @@ -10,9 +10,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UseTrend(c, mujica.RedUp) - mujica.PriceText(c, 18234.5, mujica.PriceTextOptions{Compare: true, Reference: 18000}) + core.Use(c, core.Settings{}) + core.UseTrend(c, core.RedUp) + finance.PriceText(c, 18234.5, finance.PriceTextOptions{Compare: true, Reference: 18000}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/164-price-change-badge.md b/docs/components/164-price-change-badge.md index af8f3d3..d1ec005 100644 --- a/docs/components/164-price-change-badge.md +++ b/docs/components/164-price-change-badge.md @@ -10,8 +10,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriceChangeBadge(c, -5.62, -1.35, mujica.PriceChangeBadgeOptions{}) + core.Use(c, core.Settings{}) + finance.PriceChangeBadge(c, -5.62, -1.35, finance.PriceChangeBadgeOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/165-order-book.md b/docs/components/165-order-book.md index a06c386..89ef139 100644 --- a/docs/components/165-order-book.md +++ b/docs/components/165-order-book.md @@ -9,14 +9,14 @@ ## 最小示例 ```go -var state mujica.OrderBookState -book := mujica.OrderBookData{ - Bids: []mujica.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, - Asks: []mujica.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, +var state finance.OrderBookState +book := finance.OrderBookData{ + Bids: []finance.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, + Asks: []finance.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.OrderBook(c, &state, book, mujica.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { + core.Use(c, core.Settings{}) + if price, side, ok := finance.OrderBook(c, &state, book, finance.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { fmt.Println("order at", price, side) } } diff --git a/docs/components/166-level2-quotes.md b/docs/components/166-level2-quotes.md index 1b4d9bf..a777f54 100644 --- a/docs/components/166-level2-quotes.md +++ b/docs/components/166-level2-quotes.md @@ -9,13 +9,13 @@ ## 最小示例 ```go -data := mujica.Level2Data{ - Bids: []mujica.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, - Asks: []mujica.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, +data := finance.Level2Data{ + Bids: []finance.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, + Asks: []finance.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{}) + core.Use(c, core.Settings{}) + finance.Level2Quotes(c, data, finance.Level2QuotesOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/167-time-and-sales.md b/docs/components/167-time-and-sales.md index d68511f..55d7e6c 100644 --- a/docs/components/167-time-and-sales.md +++ b/docs/components/167-time-and-sales.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -var state mujica.TimeAndSalesState -trades := []mujica.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: mujica.SideBuy}} +var state finance.TimeAndSalesState +trades := []finance.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: finance.SideBuy}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeAndSales(c, &state, trades, mujica.TimeAndSalesOptions{LargeSize: 1000}) + core.Use(c, core.Settings{}) + finance.TimeAndSales(c, &state, trades, finance.TimeAndSalesOptions{LargeSize: 1000}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/168-depth-ladder.md b/docs/components/168-depth-ladder.md index 2421ee9..0e9da14 100644 --- a/docs/components/168-depth-ladder.md +++ b/docs/components/168-depth-ladder.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -var state mujica.DepthLadderState -book := mujica.OrderBookData{Bids: []mujica.BookLevel{{Price: 100, Size: 5}}, Asks: []mujica.BookLevel{{Price: 100.01, Size: 2}}} +var state finance.DepthLadderState +book := finance.OrderBookData{Bids: []finance.BookLevel{{Price: 100, Size: 5}}, Asks: []finance.BookLevel{{Price: 100.01, Size: 2}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.DepthLadder(c, &state, 100, book, mujica.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { + core.Use(c, core.Settings{}) + if price, side, ok := finance.DepthLadder(c, &state, 100, book, finance.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { fmt.Println("place", side, "at", price) } } diff --git a/docs/components/169-spread-indicator.md b/docs/components/169-spread-indicator.md index f071535..a36b8ce 100644 --- a/docs/components/169-spread-indicator.md +++ b/docs/components/169-spread-indicator.md @@ -10,8 +10,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{WarnPercent: 0.5}) + core.Use(c, core.Settings{}) + finance.SpreadIndicator(c, 99.95, 100.05, finance.SpreadIndicatorOptions{WarnPercent: 0.5}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/170-bid-ask-bar.md b/docs/components/170-bid-ask-bar.md index 6ef8828..d28dc74 100644 --- a/docs/components/170-bid-ask-bar.md +++ b/docs/components/170-bid-ask-bar.md @@ -10,8 +10,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BidAskBar(c, 6234, 3820, mujica.BidAskBarOptions{}) + core.Use(c, core.Settings{}) + finance.BidAskBar(c, 6234, 3820, finance.BidAskBarOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/171-order-entry.md b/docs/components/171-order-entry.md index fdbceb3..62412c5 100644 --- a/docs/components/171-order-entry.md +++ b/docs/components/171-order-entry.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -var state mujica.OrderEntryState +var state finance.OrderEntryState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if order, ok := mujica.OrderEntry(c, &state, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { + core.Use(c, core.Settings{}) + if order, ok := finance.OrderEntry(c, &state, finance.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { fmt.Println("place", order.Quantity) state.EndSubmit("") } diff --git a/docs/components/172-quick-trade-buttons.md b/docs/components/172-quick-trade-buttons.md index a5e2a07..2039445 100644 --- a/docs/components/172-quick-trade-buttons.md +++ b/docs/components/172-quick-trade-buttons.md @@ -11,8 +11,8 @@ ```go quantity := 10.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if side, q, ok := mujica.QuickTradeButtons(c, &quantity, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { + core.Use(c, core.Settings{}) + if side, q, ok := finance.QuickTradeButtons(c, &quantity, finance.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { fmt.Println(side, q) } } diff --git a/docs/components/173-position-table.md b/docs/components/173-position-table.md index 95443d8..5455ed4 100644 --- a/docs/components/173-position-table.md +++ b/docs/components/173-position-table.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -var state mujica.DataTableState[string] -rows := []mujica.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} +var state data.DataTableState[string] +rows := []finance.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.PositionTable(c, &state, rows, mujica.PositionTableOptions{}).Acted(); ok { + core.Use(c, core.Settings{}) + if id, ok := finance.PositionTable(c, &state, rows, finance.PositionTableOptions{}).Acted(); ok { fmt.Println("close", id) } } diff --git a/docs/components/174-order-table.md b/docs/components/174-order-table.md index aa1d93d..c2e90e3 100644 --- a/docs/components/174-order-table.md +++ b/docs/components/174-order-table.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -var state mujica.OrderTableState -rows := []mujica.OrderRow{{ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial}} +var state finance.OrderTableState +rows := []finance.OrderRow{{ID: "o1", Symbol: "MUJC", Side: finance.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: finance.OrderPartial}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OrderTable(c, &state, rows, mujica.OrderTableOptions{}).Acted(); ok { + core.Use(c, core.Settings{}) + if id, ok := finance.OrderTable(c, &state, rows, finance.OrderTableOptions{}).Acted(); ok { fmt.Println("cancel", id) } } diff --git a/docs/components/175-trade-history-table.md b/docs/components/175-trade-history-table.md index 429704f..8a41fcc 100644 --- a/docs/components/175-trade-history-table.md +++ b/docs/components/175-trade-history-table.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -var state mujica.TradeHistoryTableState -rows := []mujica.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: mujica.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} +var state finance.TradeHistoryTableState +rows := []finance.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: finance.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if fills, ok := mujica.TradeHistoryTable(c, &state, rows, mujica.TradeHistoryTableOptions{}).Exported(); ok { + core.Use(c, core.Settings{}) + if fills, ok := finance.TradeHistoryTable(c, &state, rows, finance.TradeHistoryTableOptions{}).Exported(); ok { fmt.Println("export", len(fills)) } } diff --git a/docs/components/176-pnl-display.md b/docs/components/176-pnl-display.md index 947e0b6..358e9a7 100644 --- a/docs/components/176-pnl-display.md +++ b/docs/components/176-pnl-display.md @@ -11,8 +11,8 @@ ```go hidden := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, mujica.PnLDisplayOptions{Currency: "$"}) + core.Use(c, core.Settings{}) + finance.PnLDisplay(c, finance.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, finance.PnLDisplayOptions{Currency: "$"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/177-leverage-slider.md b/docs/components/177-leverage-slider.md index 31ecb1e..d403edd 100644 --- a/docs/components/177-leverage-slider.md +++ b/docs/components/177-leverage-slider.md @@ -11,8 +11,8 @@ ```go leverage := 5.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LeverageSlider(c, &leverage, mujica.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { + core.Use(c, core.Settings{}) + if finance.LeverageSlider(c, &leverage, finance.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { fmt.Println("leverage", leverage) } } diff --git a/docs/components/178-margin-indicator.md b/docs/components/178-margin-indicator.md index e3d3332..d7a2eb0 100644 --- a/docs/components/178-margin-indicator.md +++ b/docs/components/178-margin-indicator.md @@ -10,8 +10,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarginIndicator(c, 0.86, mujica.MarginIndicatorOptions{}) + core.Use(c, core.Settings{}) + finance.MarginIndicator(c, 0.86, finance.MarginIndicatorOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/179-risk-meter.md b/docs/components/179-risk-meter.md index 208b797..6a18eb3 100644 --- a/docs/components/179-risk-meter.md +++ b/docs/components/179-risk-meter.md @@ -10,8 +10,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RiskMeter(c, 4, mujica.RiskMeterOptions{}) + core.Use(c, core.Settings{}) + finance.RiskMeter(c, 4, finance.RiskMeterOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/180-order-confirm-dialog.md b/docs/components/180-order-confirm-dialog.md index 00225c1..30a139d 100644 --- a/docs/components/180-order-confirm-dialog.md +++ b/docs/components/180-order-confirm-dialog.md @@ -11,9 +11,9 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OrderConfirmDialog(c, &open, mujica.OrderConfirmDialogOptions{ - Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, + core.Use(c, core.Settings{}) + r := finance.OrderConfirmDialog(c, &open, finance.OrderConfirmDialogOptions{ + Lines: []finance.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, Risk: "Leveraged products can lose more than the deposit.", Acknowledge: "I understand the risk", }) diff --git a/docs/components/181-option-chain.md b/docs/components/181-option-chain.md index 7052dac..ca55352 100644 --- a/docs/components/181-option-chain.md +++ b/docs/components/181-option-chain.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.OptionChainState -rows := []mujica.OptionStrike{{Strike: 100, Call: mujica.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: mujica.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} +var state finance.OptionChainState +rows := []finance.OptionStrike{{Strike: 100, Call: finance.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: finance.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, mujica.OptionChainOptions{Spot: 100.4}) + core.Use(c, core.Settings{}) + r := finance.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, finance.OptionChainOptions{Spot: 100.4}) if r.Changed || r.Submitted || r.ExpiryChanged { fmt.Println(state.Expiry, state.Strike, state.Side) } diff --git a/docs/components/182-greeks-table.md b/docs/components/182-greeks-table.md index d78cefd..ce2fcfd 100644 --- a/docs/components/182-greeks-table.md +++ b/docs/components/182-greeks-table.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -rows := []mujica.GreeksRow{{Label: "AAPL 100 C", Greeks: mujica.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} +rows := []finance.GreeksRow{{Label: "AAPL 100 C", Greeks: finance.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GreeksTable(c, rows, mujica.GreeksTableOptions{Notes: true}) + core.Use(c, core.Settings{}) + finance.GreeksTable(c, rows, finance.GreeksTableOptions{Notes: true}) } ui.Render(view, 960, 440, 1) ``` diff --git a/docs/components/183-payoff-diagram.md b/docs/components/183-payoff-diagram.md index 4094ea1..0ef799d 100644 --- a/docs/components/183-payoff-diagram.md +++ b/docs/components/183-payoff-diagram.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -legs := []mujica.OptionLeg{{Kind: mujica.LegCall, Strike: 100, Premium: 3, Quantity: 1}} +legs := []finance.OptionLeg{{Kind: finance.LegCall, Strike: 100, Premium: 3, Quantity: 1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PayoffDiagram(c, legs, mujica.PayoffDiagramOptions{Frame: mujica.ChartFrame{Label: "Call payoff"}, Spot: 103}) + core.Use(c, core.Settings{}) + r := finance.PayoffDiagram(c, legs, finance.PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Call payoff"}, Spot: 103}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } diff --git a/docs/components/184-watchlist.md b/docs/components/184-watchlist.md index d3ae8c4..8128b86 100644 --- a/docs/components/184-watchlist.md +++ b/docs/components/184-watchlist.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.WatchlistState -groups := []mujica.WatchGroup{{Name: "Technology", Items: []mujica.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []mujica.WatchItem{{Symbol: "XOM", Price: 117.4}}}} +var state finance.WatchlistState +groups := []finance.WatchGroup{{Name: "Technology", Items: []finance.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []finance.WatchItem{{Symbol: "XOM", Price: 117.4}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Watchlist(c, &state, groups, mujica.WatchlistOptions{}) + core.Use(c, core.Settings{}) + r := finance.Watchlist(c, &state, groups, finance.WatchlistOptions{}) if r.Changed || r.Submitted || r.Moved { fmt.Println(state.Selected, r.Rows, r.To) } diff --git a/docs/components/185-symbol-search.md b/docs/components/185-symbol-search.md index 9879e92..c4748c3 100644 --- a/docs/components/185-symbol-search.md +++ b/docs/components/185-symbol-search.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.SymbolSearchState -rows := []mujica.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} +var state finance.SymbolSearchState +rows := []finance.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolSearch(c, &state, rows, mujica.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) + core.Use(c, core.Settings{}) + r := finance.SymbolSearch(c, &state, rows, finance.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) if r.Changed || r.Picked || r.Retry { fmt.Println(state.Query, state.Market, r.Chosen.Code) } diff --git a/docs/components/186-symbol-badge.md b/docs/components/186-symbol-badge.md index 0f9edba..3dbfd86 100644 --- a/docs/components/186-symbol-badge.md +++ b/docs/components/186-symbol-badge.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -symbol := mujica.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} +symbol := finance.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolBadge(c, symbol, mujica.SymbolBadgeOptions{Clickable: true}) + core.Use(c, core.Settings{}) + r := finance.SymbolBadge(c, symbol, finance.SymbolBadgeOptions{Clickable: true}) if r.Changed() { fmt.Println(symbol.Code) } diff --git a/docs/components/187-market-heatmap.md b/docs/components/187-market-heatmap.md index 9e36f9a..ffe717c 100644 --- a/docs/components/187-market-heatmap.md +++ b/docs/components/187-market-heatmap.md @@ -8,10 +8,10 @@ ```go var path []string -root := mujica.HeatNode{Name: "Market", Children: []mujica.HeatNode{{Name: "Technology", Children: []mujica.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} +root := chart.HeatNode{Name: "Market", Children: []chart.HeatNode{{Name: "Technology", Children: []chart.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketHeatmap(c, &path, root, mujica.MarketHeatmapOptions{Frame: mujica.ChartFrame{Label: "Market"}}) + core.Use(c, core.Settings{}) + r := chart.MarketHeatmap(c, &path, root, chart.MarketHeatmapOptions{Frame: chart.ChartFrame{Label: "Market"}}) if r.Changed() || r.ValueChanged() || r.Retried() { fmt.Println(path, r.Point) } diff --git a/docs/components/188-screener.md b/docs/components/188-screener.md index 0eedfe4..6e50046 100644 --- a/docs/components/188-screener.md +++ b/docs/components/188-screener.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -var state mujica.ScreenerState -fields := []mujica.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} -rows := []mujica.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} +var state finance.ScreenerState +fields := []finance.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} +rows := []finance.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Screener(c, &state, fields, rows, mujica.ScreenerOptions{}) + core.Use(c, core.Settings{}) + r := finance.Screener(c, &state, fields, rows, finance.ScreenerOptions{}) if r.Changed || r.Save || r.Table.Changed() || r.Table.Submitted() || r.Table.SortChanged() || r.Table.Retried() { fmt.Println(state.Conditions) } diff --git a/docs/components/189-market-overview.md b/docs/components/189-market-overview.md index d51df94..a3dccdc 100644 --- a/docs/components/189-market-overview.md +++ b/docs/components/189-market-overview.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -indices := []mujica.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} +indices := []finance.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketOverview(c, indices, mujica.MarketOverviewOptions{Breadth: mujica.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) + core.Use(c, core.Settings{}) + r := finance.MarketOverview(c, indices, finance.MarketOverviewOptions{Breadth: finance.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) if r.Chosen >= 0 { fmt.Println(indices[r.Chosen].Name) } diff --git a/docs/components/190-index-card.md b/docs/components/190-index-card.md index 1ee8e95..367d9cc 100644 --- a/docs/components/190-index-card.md +++ b/docs/components/190-index-card.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -quote := mujica.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} +quote := finance.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.IndexCard(c, quote, mujica.IndexCardOptions{Clickable: true}) + core.Use(c, core.Settings{}) + r := finance.IndexCard(c, quote, finance.IndexCardOptions{Clickable: true}) if r.Changed() { fmt.Println(quote.Name) } diff --git a/docs/components/191-market-status.md b/docs/components/191-market-status.md index 1bee179..28b7f90 100644 --- a/docs/components/191-market-status.md +++ b/docs/components/191-market-status.md @@ -9,8 +9,8 @@ ```go now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarketStatus(c, mujica.MarketOpen, mujica.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: mujica.MarketAfterHours}) + core.Use(c, core.Settings{}) + finance.MarketStatus(c, finance.MarketOpen, finance.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: finance.MarketAfterHours}) } ui.Render(view, 960, 440, 1) ``` diff --git a/docs/components/192-trading-session-clock.md b/docs/components/192-trading-session-clock.md index 67239a0..bd69dad 100644 --- a/docs/components/192-trading-session-clock.md +++ b/docs/components/192-trading-session-clock.md @@ -8,10 +8,10 @@ ```go now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) -markets := []mujica.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []mujica.TradingSession{{Phase: mujica.MarketOpen, Start: 570, End: 960}}}} +markets := []finance.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []finance.TradingSession{{Phase: finance.MarketOpen, Start: 570, End: 960}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TradingSessionClock(c, markets, mujica.TradingSessionClockOptions{Now: now}) + core.Use(c, core.Settings{}) + finance.TradingSessionClock(c, markets, finance.TradingSessionClockOptions{Now: now}) } ui.Render(view, 960, 440, 1) ``` diff --git a/docs/components/193-economic-calendar.md b/docs/components/193-economic-calendar.md index 6f1a6d4..69f70d6 100644 --- a/docs/components/193-economic-calendar.md +++ b/docs/components/193-economic-calendar.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.EconomicCalendarState -rows := []mujica.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} +var state finance.EconomicCalendarState +rows := []finance.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EconomicCalendar(c, &state, rows, mujica.EconomicCalendarOptions{}) + core.Use(c, core.Settings{}) + r := finance.EconomicCalendar(c, &state, rows, finance.EconomicCalendarOptions{}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { fmt.Println(state.Table.Selected()) } diff --git a/docs/components/194-earnings-calendar.md b/docs/components/194-earnings-calendar.md index 9efde0e..ba0f8e4 100644 --- a/docs/components/194-earnings-calendar.md +++ b/docs/components/194-earnings-calendar.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.DataTableState[string] -rows := []mujica.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: mujica.Date{Year: 2026, Month: 10, Day: 30}, Timing: mujica.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} +var state data.DataTableState[string] +rows := []finance.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: finance.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EarningsCalendar(c, &state, rows, mujica.EarningsCalendarOptions{}) + core.Use(c, core.Settings{}) + r := finance.EarningsCalendar(c, &state, rows, finance.EarningsCalendarOptions{}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { fmt.Println(state.Selected()) } diff --git a/docs/components/195-news-feed.md b/docs/components/195-news-feed.md index 7d340db..95a1e46 100644 --- a/docs/components/195-news-feed.md +++ b/docs/components/195-news-feed.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.NewsFeedState -rows := []mujica.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} +var state finance.NewsFeedState +rows := []finance.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.NewsFeed(c, &state, rows, mujica.NewsFeedOptions{}) + core.Use(c, core.Settings{}) + r := finance.NewsFeed(c, &state, rows, finance.NewsFeedOptions{}) if r.Opened != "" || r.Symbol != "" || r.Retry { fmt.Println(r.Opened, r.Symbol) } diff --git a/docs/components/196-sentiment-gauge.md b/docs/components/196-sentiment-gauge.md index 43c5d1f..64c4d33 100644 --- a/docs/components/196-sentiment-gauge.md +++ b/docs/components/196-sentiment-gauge.md @@ -9,8 +9,8 @@ ```go value := 72.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SentimentGauge(c, value, mujica.SentimentGaugeOptions{Label: "Fear & Greed"}) + core.Use(c, core.Settings{}) + finance.SentimentGauge(c, value, finance.SentimentGaugeOptions{Label: "Fear & Greed"}) } ui.Render(view, 960, 440, 1) ``` diff --git a/docs/components/197-portfolio-summary.md b/docs/components/197-portfolio-summary.md index f911137..074763f 100644 --- a/docs/components/197-portfolio-summary.md +++ b/docs/components/197-portfolio-summary.md @@ -8,10 +8,10 @@ ```go account := "" -accounts := []mujica.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} +accounts := []finance.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PortfolioSummary(c, &account, accounts, mujica.PortfolioSummaryOptions{}) + core.Use(c, core.Settings{}) + r := finance.PortfolioSummary(c, &account, accounts, finance.PortfolioSummaryOptions{}) if r.Changed() { fmt.Println(account) } diff --git a/docs/components/198-asset-allocation-chart.md b/docs/components/198-asset-allocation-chart.md index 48080f9..c01b17d 100644 --- a/docs/components/198-asset-allocation-chart.md +++ b/docs/components/198-asset-allocation-chart.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -items := []mujica.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} +items := []finance.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AssetAllocationChart(c, items, mujica.AssetAllocationChartOptions{Frame: mujica.ChartFrame{Label: "Allocation"}}) + core.Use(c, core.Settings{}) + r := finance.AssetAllocationChart(c, items, finance.AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } diff --git a/docs/components/199-performance-chart.md b/docs/components/199-performance-chart.md index 5140744..d49ba4c 100644 --- a/docs/components/199-performance-chart.md +++ b/docs/components/199-performance-chart.md @@ -11,8 +11,8 @@ values := []float64{100, 110, 95, 120} benchmark := []float64{100, 105, 108, 112} times := []time.Time{time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PerformanceChart(c, values, mujica.PerformanceChartOptions{Frame: mujica.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) + core.Use(c, core.Settings{}) + r := finance.PerformanceChart(c, values, finance.PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } diff --git a/docs/components/200-currency-converter.md b/docs/components/200-currency-converter.md index b580f81..f1dd1dd 100644 --- a/docs/components/200-currency-converter.md +++ b/docs/components/200-currency-converter.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -state := mujica.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} -currencies := []mujica.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} +state := finance.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} +currencies := []finance.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} rate := 149.37 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CurrencyConverter(c, &state, currencies, mujica.CurrencyConverterOptions{Rate: rate}) + core.Use(c, core.Settings{}) + r := finance.CurrencyConverter(c, &state, currencies, finance.CurrencyConverterOptions{Rate: rate}) if r.PairChanged { rate = 0 fmt.Println(state.From, state.To) diff --git a/docs/components/201-chat-container.md b/docs/components/201-chat-container.md index 2c5b04d..9127a08 100644 --- a/docs/components/201-chat-container.md +++ b/docs/components/201-chat-container.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ - List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, +chat.ChatContainer(c, &thread, len(msgs), chat.ChatContainerOptions{ + List: chat.MessageListOptions{ID: func(i int) string { return msgs[i] }}, Input: func() { ui.Text(c, "Composer goes here") }, }, func(i int) { ui.Text(c, msgs[i]) }) ``` diff --git a/docs/components/202-message-list.md b/docs/components/202-message-list.md index 0e2bc60..a95ccf1 100644 --- a/docs/components/202-message-list.md +++ b/docs/components/202-message-list.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ +r := chat.MessageList(c, &list, len(ids), chat.MessageListOptions{ ID: func(i int) string { return ids[i] }, }, func(i int) { ui.Text(c, "Message "+ids[i]) }) fmt.Println("unread", r.Unread()) diff --git a/docs/components/203-message-bubble.md b/docs/components/203-message-bubble.md index 96349f0..de98d4d 100644 --- a/docs/components/203-message-bubble.md +++ b/docs/components/203-message-bubble.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { +chat.MessageBubble(c, chat.MessageAssistant, chat.MessageBubbleOptions{Name: "Herald"}, func() { ui.Text(c, "The ball begins at nine.") }) ``` diff --git a/docs/components/204-message-header.md b/docs/components/204-message-header.md index f545f94..f9d41e5 100644 --- a/docs/components/204-message-header.md +++ b/docs/components/204-message-header.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent}) +chat.MessageHeader(c, chat.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: chat.MessageSent}) ``` 签名:`func MessageHeader(c *ui.Context, opts MessageHeaderOptions) *ui.Element` diff --git a/docs/components/205-streaming-text.md b/docs/components/205-streaming-text.md index 552c2cf..3154a4e 100644 --- a/docs/components/205-streaming-text.md +++ b/docs/components/205-streaming-text.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true}) +chat.StreamingText(c, reply, chat.StreamingTextOptions{Streaming: true, Smooth: true}) ``` 签名:`func StreamingText(c *ui.Context, text string, opts StreamingTextOptions) *ui.Element` diff --git a/docs/components/206-markdown-view.md b/docs/components/206-markdown-view.md index e1b35b0..46a74c3 100644 --- a/docs/components/206-markdown-view.md +++ b/docs/components/206-markdown-view.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { +if url, ok := chat.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { fmt.Println("open", url) } ``` diff --git a/docs/components/207-code-block.md b/docs/components/207-code-block.md index 87b6375..87aa52a 100644 --- a/docs/components/207-code-block.md +++ b/docs/components/207-code-block.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true}) +chat.CodeBlock(c, "func bow() {}", chat.CodeBlockOptions{Language: "go", LineNumbers: true}) ``` 签名:`func CodeBlock(c *ui.Context, code string, opts CodeBlockOptions) CopyResult` diff --git a/docs/components/208-table-block.md b/docs/components/208-table-block.md index 5891e29..309d225 100644 --- a/docs/components/208-table-block.md +++ b/docs/components/208-table-block.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { +if chat.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, chat.TableBlockOptions{}).Copied() { fmt.Println("copied as TSV") } ``` diff --git a/docs/components/209-image-grid.md b/docs/components/209-image-grid.md index ae3ffa4..93d4b72 100644 --- a/docs/components/209-image-grid.md +++ b/docs/components/209-image-grid.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { +if i, ok := chat.ImageGrid(c, []chat.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { fmt.Println("reload", i) } ``` diff --git a/docs/components/210-file-message.md b/docs/components/210-file-message.md index 5bfc0f5..e937c61 100644 --- a/docs/components/210-file-message.md +++ b/docs/components/210-file-message.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) +r := chat.FileMessage(c, chat.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) if r.Downloaded() { fmt.Println("save treaty.pdf") } diff --git a/docs/components/211-audio-message.md b/docs/components/211-audio-message.md index f8c8879..3d2b5e8 100644 --- a/docs/components/211-audio-message.md +++ b/docs/components/211-audio-message.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) +r := chat.AudioMessage(c, chat.AudioMessageOptions{Duration: time.Minute, Position: pos}) if d, ok := r.Seek(); ok { pos = d } diff --git a/docs/components/212-link-preview-card.md b/docs/components/212-link-preview-card.md index c96bc48..a80dead 100644 --- a/docs/components/212-link-preview-card.md +++ b/docs/components/212-link-preview-card.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} -if mujica.LinkPreviewCard(c, p).Clicked() { +p := chat.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} +if chat.LinkPreviewCard(c, p).Clicked() { fmt.Println("open", p.URL) } ``` diff --git a/docs/components/213-quote-reply.md b/docs/components/213-quote-reply.md index 6f7857b..783f89a 100644 --- a/docs/components/213-quote-reply.md +++ b/docs/components/213-quote-reply.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { +if chat.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { fmt.Println("jump to the quoted message") } ``` diff --git a/docs/components/214-message-actions.md b/docs/components/214-message-actions.md index 6828031..f5ff9ee 100644 --- a/docs/components/214-message-actions.md +++ b/docs/components/214-message-actions.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { +if a, ok := chat.MessageActions(c, chat.MessageAssistant, chat.MessageActionsOptions{Feedback: &rating}).Action(); ok { fmt.Println("action", a) } ``` diff --git a/docs/components/215-message-editor.md b/docs/components/215-message-editor.md index 5d6b0d1..f41f849 100644 --- a/docs/components/215-message-editor.md +++ b/docs/components/215-message-editor.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { +if chat.MessageEditor(c, &draft, chat.MessageEditorOptions{Original: "Hello"}).Saved() { fmt.Println("new branch from", draft) } ``` diff --git a/docs/components/216-branch-navigator.md b/docs/components/216-branch-navigator.md index 8ab7bcc..98fb111 100644 --- a/docs/components/216-branch-navigator.md +++ b/docs/components/216-branch-navigator.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -if mujica.BranchNavigator(c, &branch, 3).Changed() { +if chat.BranchNavigator(c, &branch, 3).Changed() { fmt.Println("show branch", branch) } ``` diff --git a/docs/components/217-error-message.md b/docs/components/217-error-message.md index 441891c..8d0502a 100644 --- a/docs/components/217-error-message.md +++ b/docs/components/217-error-message.md @@ -7,8 +7,8 @@ ## 最小示例 ```go -opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} -if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { +opts := chat.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} +if chat.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { fmt.Println("send again") } ``` diff --git a/docs/components/218-thinking-block.md b/docs/components/218-thinking-block.md index fb2ca98..1dd6cd6 100644 --- a/docs/components/218-thinking-block.md +++ b/docs/components/218-thinking-block.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { +chat.ThinkingBlock(c, &open, chat.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { ui.Text(c, "Weigh the guest list.") }) ``` diff --git a/docs/components/219-scroll-to-bottom-button.md b/docs/components/219-scroll-to-bottom-button.md index 1cd7550..818d28c 100644 --- a/docs/components/219-scroll-to-bottom-button.md +++ b/docs/components/219-scroll-to-bottom-button.md @@ -8,7 +8,7 @@ ```go ui.Box(c).Fill().Children(func() { - if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + if chat.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { list.ScrollToEnd() } }) diff --git a/docs/components/220-typing-indicator.md b/docs/components/220-typing-indicator.md index b0094d6..45eba39 100644 --- a/docs/components/220-typing-indicator.md +++ b/docs/components/220-typing-indicator.md @@ -7,7 +7,7 @@ ## 最小示例 ```go -mujica.TypingIndicator(c, "Herald") +chat.TypingIndicator(c, "Herald") ``` 签名:`func TypingIndicator(c *ui.Context, name string) *ui.Element` diff --git a/docs/components/221-prompt-composer.md b/docs/components/221-prompt-composer.md index 935fb98..62126fb 100644 --- a/docs/components/221-prompt-composer.md +++ b/docs/components/221-prompt-composer.md @@ -9,8 +9,8 @@ ```go var prompt string view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{Placeholder: "Ask the herald"}) + core.Use(c, core.Settings{}) + r := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{Placeholder: "Ask the herald"}) if r.Submitted() { fmt.Println("send", prompt) prompt = "" diff --git a/docs/components/222-attachment-chip.md b/docs/components/222-attachment-chip.md index 0d08e1f..8473e43 100644 --- a/docs/components/222-attachment-chip.md +++ b/docs/components/222-attachment-chip.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AttachmentChip(c, mujica.Attachment{Name: "guests.csv", Size: 4096, Uploading: true, Progress: 0.4}) + core.Use(c, core.Settings{}) + r := chat.AttachmentChip(c, chat.Attachment{Name: "guests.csv", Size: 4096, Uploading: true, Progress: 0.4}) if r.Removed() { fmt.Println("cancel the upload") } diff --git a/docs/components/223-paste-image-preview.md b/docs/components/223-paste-image-preview.md index 7a8568a..bfd3d61 100644 --- a/docs/components/223-paste-image-preview.md +++ b/docs/components/223-paste-image-preview.md @@ -8,10 +8,10 @@ ```go pic := ui.MustParseSVG([]byte(``)) -images := []mujica.PastedImage{{ID: "p1", Source: pic, Name: "screenshot"}} +images := []chat.PastedImage{{ID: "p1", Source: pic, Name: "screenshot"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.PasteImagePreview(c, images).Removed(); ok { + core.Use(c, core.Settings{}) + if id, ok := chat.PasteImagePreview(c, images).Removed(); ok { fmt.Println("remove", id) } } diff --git a/docs/components/224-drag-drop-overlay.md b/docs/components/224-drag-drop-overlay.md index 4010a02..c0e8de5 100644 --- a/docs/components/224-drag-drop-overlay.md +++ b/docs/components/224-drag-drop-overlay.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) images := func(path string) bool { return strings.HasSuffix(path, ".png") } - r := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: images}, func() { + r := chat.DragDropOverlay(c, chat.DragDropOverlayOptions{Accept: images}, func() { ui.Text(c, "Drag images into the chat") }) for _, path := range r.Accepted() { diff --git a/docs/components/225-voice-input-button.md b/docs/components/225-voice-input-button.md index 22f3541..230d121 100644 --- a/docs/components/225-voice-input-button.md +++ b/docs/components/225-voice-input-button.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var rec mujica.VoiceInputOptions +var rec chat.VoiceInputOptions view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.VoiceInputButton(c, rec).Changed() { - rec = mujica.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()} + core.Use(c, core.Settings{}) + if chat.VoiceInputButton(c, rec).Changed() { + rec = chat.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()} } } ui.Render(view, 400, 300, 1) diff --git a/docs/components/226-voice-waveform.md b/docs/components/226-voice-waveform.md index 2d6c7e0..34d8dfa 100644 --- a/docs/components/226-voice-waveform.md +++ b/docs/components/226-voice-waveform.md @@ -9,8 +9,8 @@ ```go levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{}) + core.Use(c, core.Settings{}) + chat.VoiceWaveform(c, levels, chat.VoiceWaveformOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/227-slash-command-menu.md b/docs/components/227-slash-command-menu.md index 12d060c..992cf04 100644 --- a/docs/components/227-slash-command-menu.md +++ b/docs/components/227-slash-command-menu.md @@ -8,11 +8,11 @@ ```go var prompt string -commands := []mujica.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}} +commands := []chat.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - composer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{}) - if cmd, ok := mujica.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok { + core.Use(c, core.Settings{}) + composer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{}) + if cmd, ok := chat.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok { fmt.Println("run", cmd.Name) } } diff --git a/docs/components/228-context-mention-menu.md b/docs/components/228-context-mention-menu.md index 223edb0..ff189de 100644 --- a/docs/components/228-context-mention-menu.md +++ b/docs/components/228-context-mention-menu.md @@ -8,13 +8,13 @@ ```go var prompt string -var found []mujica.ContextItem +var found []chat.ContextItem view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - composer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{}) - r := mujica.ContextMentionMenu(c, composer, &prompt, found, mujica.ContextMentionOptions{}) + core.Use(c, core.Settings{}) + composer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{}) + r := chat.ContextMentionMenu(c, composer, &prompt, found, chat.ContextMentionOptions{}) if r.QueryChanged() { - found = []mujica.ContextItem{{ID: "f1", Label: r.Query() + ".md", Kind: mujica.ContextFile}} + found = []chat.ContextItem{{ID: "f1", Label: r.Query() + ".md", Kind: chat.ContextFile}} } if item, ok := r.Chosen(); ok { fmt.Println("attach", item.ID) diff --git a/docs/components/229-context-chips.md b/docs/components/229-context-chips.md index b2e4e74..1111b0a 100644 --- a/docs/components/229-context-chips.md +++ b/docs/components/229-context-chips.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -items := []mujica.ContextItem{{ID: "f1", Label: "guests.csv", Kind: mujica.ContextFile}} +items := []chat.ContextItem{{ID: "f1", Label: "guests.csv", Kind: chat.ContextFile}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.ContextChips(c, items, mujica.ContextChipsOptions{}).Removed(); ok { + core.Use(c, core.Settings{}) + if id, ok := chat.ContextChips(c, items, chat.ContextChipsOptions{}).Removed(); ok { fmt.Println("detach", id) } } diff --git a/docs/components/230-model-selector.md b/docs/components/230-model-selector.md index 0d3206b..48bd330 100644 --- a/docs/components/230-model-selector.md +++ b/docs/components/230-model-selector.md @@ -8,10 +8,10 @@ ```go model := "herald" -models := []mujica.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision"}, ContextTokens: 200000}} +models := []chat.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision"}, ContextTokens: 200000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ModelSelector(c, &model, models, mujica.ModelSelectorOptions{}).Changed() { + core.Use(c, core.Settings{}) + if chat.ModelSelector(c, &model, models, chat.ModelSelectorOptions{}).Changed() { fmt.Println("model", model) } } diff --git a/docs/components/231-mode-selector.md b/docs/components/231-mode-selector.md index 2fe54da..cd30666 100644 --- a/docs/components/231-mode-selector.md +++ b/docs/components/231-mode-selector.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -mode := mujica.ModeChat +mode := chat.ModeChat view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ModeSelector(c, &mode, mujica.ModeSelectorOptions{}).Changed() { + core.Use(c, core.Settings{}) + if chat.ModeSelector(c, &mode, chat.ModeSelectorOptions{}).Changed() { fmt.Println("mode", mode) } } diff --git a/docs/components/232-tool-toggle-menu.md b/docs/components/232-tool-toggle-menu.md index a28700f..6022ef9 100644 --- a/docs/components/232-tool-toggle-menu.md +++ b/docs/components/232-tool-toggle-menu.md @@ -9,9 +9,9 @@ ```go web := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - tools := []mujica.ToolItem{{ID: "web", Label: "Web search", On: &web}} - if id, ok := mujica.ToolToggleMenu(c, tools).Changed(); ok { + core.Use(c, core.Settings{}) + tools := []chat.ToolItem{{ID: "web", Label: "Web search", On: &web}} + if id, ok := chat.ToolToggleMenu(c, tools).Changed(); ok { fmt.Println("toggled", id) } } diff --git a/docs/components/233-parameter-panel.md b/docs/components/233-parameter-panel.md index c235f26..6e7ff3d 100644 --- a/docs/components/233-parameter-panel.md +++ b/docs/components/233-parameter-panel.md @@ -9,9 +9,9 @@ ```go temperature := 1.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - params := []mujica.ModelParam{{Label: "Temperature", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}} - mujica.ParameterPanel(c, params) + core.Use(c, core.Settings{}) + params := []chat.ModelParam{{Label: "Temperature", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}} + chat.ParameterPanel(c, params) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/234-token-counter.md b/docs/components/234-token-counter.md index aa0db0f..e985cd3 100644 --- a/docs/components/234-token-counter.md +++ b/docs/components/234-token-counter.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TokenCounter(c, 1234, 8000) + core.Use(c, core.Settings{}) + chat.TokenCounter(c, 1234, 8000) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/235-context-window-meter.md b/docs/components/235-context-window-meter.md index b8d31bb..79cd53b 100644 --- a/docs/components/235-context-window-meter.md +++ b/docs/components/235-context-window-meter.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -segments := []mujica.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: 58000}} +segments := []chat.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: 58000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ContextWindowMeter(c, segments, 128000, mujica.ContextWindowMeterOptions{}) + core.Use(c, core.Settings{}) + chat.ContextWindowMeter(c, segments, 128000, chat.ContextWindowMeterOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/236-send-button.md b/docs/components/236-send-button.md index d80d5af..c4f7469 100644 --- a/docs/components/236-send-button.md +++ b/docs/components/236-send-button.md @@ -9,8 +9,8 @@ ```go streaming := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: streaming, Shortcut: "Enter"}) + core.Use(c, core.Settings{}) + r := chat.SendButton(c, chat.SendButtonOptions{Streaming: streaming, Shortcut: "Enter"}) if r.Sent() || r.Stopped() { streaming = r.Sent() } diff --git a/docs/components/237-citation-badge.md b/docs/components/237-citation-badge.md index cb4e3f4..02ae894 100644 --- a/docs/components/237-citation-badge.md +++ b/docs/components/237-citation-badge.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var cites mujica.CitationState -src := mujica.Source{Title: "The Palace of Versailles", URL: "https://example.com/versailles"} +var cites chat.CitationState +src := chat.Source{Title: "The Palace of Versailles", URL: "https://example.com/versailles"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CitationBadge(c, 1, src, &cites).Changed() { + core.Use(c, core.Settings{}) + if chat.CitationBadge(c, 1, src, &cites).Changed() { fmt.Println("show source", cites.Active) } } diff --git a/docs/components/238-source-card.md b/docs/components/238-source-card.md index 374d589..8ac9279 100644 --- a/docs/components/238-source-card.md +++ b/docs/components/238-source-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var cites mujica.CitationState -src := mujica.Source{Title: "Hall of Mirrors", URL: "https://example.org/hall", Snippet: "Seventeen arches."} +var cites chat.CitationState +src := chat.Source{Title: "Hall of Mirrors", URL: "https://example.org/hall", Snippet: "Seventeen arches."} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SourceCard(c, 1, src, &cites).Clicked() { + core.Use(c, core.Settings{}) + if chat.SourceCard(c, 1, src, &cites).Clicked() { fmt.Println("open", src.URL) } } diff --git a/docs/components/239-sources-panel.md b/docs/components/239-sources-panel.md index ca911d8..244a001 100644 --- a/docs/components/239-sources-panel.md +++ b/docs/components/239-sources-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var cites mujica.CitationState -sources := []mujica.Source{{Title: "Hall of Mirrors", URL: "https://example.org/hall"}} +var cites chat.CitationState +sources := []chat.Source{{Title: "Hall of Mirrors", URL: "https://example.org/hall"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if n, ok := mujica.SourcesPanel(c, sources, &cites).Opened(); ok { + core.Use(c, core.Settings{}) + if n, ok := chat.SourcesPanel(c, sources, &cites).Opened(); ok { fmt.Println("open", sources[n-1].URL) } } diff --git a/docs/components/240-search-progress.md b/docs/components/240-search-progress.md index f171062..cf46c65 100644 --- a/docs/components/240-search-progress.md +++ b/docs/components/240-search-progress.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - stages := []mujica.SearchStage{{Label: "Searching the web", Done: true}, {Label: "Reading sources"}} - mujica.SearchProgress(c, mujica.SearchProgressOptions{Query: "Versailles", Read: 3, Total: 8, Stages: stages}) + core.Use(c, core.Settings{}) + stages := []chat.SearchStage{{Label: "Searching the web", Done: true}, {Label: "Reading sources"}} + chat.SearchProgress(c, chat.SearchProgressOptions{Query: "Versailles", Read: 3, Total: 8, Stages: stages}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/241-conversation-list.md b/docs/components/241-conversation-list.md index c2d1703..f091507 100644 --- a/docs/components/241-conversation-list.md +++ b/docs/components/241-conversation-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var state mujica.ConversationListState -convs := []mujica.ChatConversation{ +var state chat.ConversationListState +convs := []chat.ChatConversation{ {ID: "1", Title: "Court etiquette", Updated: time.Now()}, {ID: "2", Title: "Royal letters", Updated: time.Now().AddDate(0, 0, -30), Pinned: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConversationList(c, &state, convs, mujica.ConversationListOptions{Label: "History"}, nil).Changed() { + core.Use(c, core.Settings{}) + if chat.ConversationList(c, &state, convs, chat.ConversationListOptions{Label: "History"}, nil).Changed() { log.Println("open", state.Selected) } } diff --git a/docs/components/242-conversation-item.md b/docs/components/242-conversation-item.md index 84246ff..55160e2 100644 --- a/docs/components/242-conversation-item.md +++ b/docs/components/242-conversation-item.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -conv := mujica.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} +conv := chat.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{}) + core.Use(c, core.Settings{}) + v := chat.ConversationItem(c, &conv, chat.ConversationItemOptions{}) if v.Deleted { log.Println("delete", conv.ID) } diff --git a/docs/components/243-conversation-search.md b/docs/components/243-conversation-search.md index 4195cdc..fa9b9e9 100644 --- a/docs/components/243-conversation-search.md +++ b/docs/components/243-conversation-search.md @@ -8,10 +8,10 @@ ```go query := "seat" -hits := []mujica.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} +hits := []chat.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationSearch(c, &query, hits, mujica.ConversationSearchOptions{}) + core.Use(c, core.Settings{}) + v := chat.ConversationSearch(c, &query, hits, chat.ConversationSearchOptions{}) if v.QueryChanged { log.Println("search", query) } diff --git a/docs/components/244-project-list.md b/docs/components/244-project-list.md index a3ed67b..fda23eb 100644 --- a/docs/components/244-project-list.md +++ b/docs/components/244-project-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var state mujica.ProjectListState -projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} -convs := func(id string) []mujica.ChatConversation { - return []mujica.ChatConversation{{ID: "1", Title: "Menu"}} +var state chat.ProjectListState +projects := []chat.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} +convs := func(id string) []chat.ChatConversation { + return []chat.ChatConversation{{ID: "1", Title: "Menu"}} } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ProjectList(c, &state, projects, convs, mujica.ProjectListOptions{}); v.Moved { + core.Use(c, core.Settings{}) + if v := chat.ProjectList(c, &state, projects, convs, chat.ProjectListOptions{}); v.Moved { log.Println("move", v.MoveID, "to", v.MoveTo) } } diff --git a/docs/components/245-project-knowledge-panel.md b/docs/components/245-project-knowledge-panel.md index 158c9e5..0c54230 100644 --- a/docs/components/245-project-knowledge-panel.md +++ b/docs/components/245-project-knowledge-panel.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -files := []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} +files := []chat.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ProjectKnowledgePanel(c, files, mujica.ProjectKnowledgePanelOptions{Capacity: 200000}) + core.Use(c, core.Settings{}) + v := chat.ProjectKnowledgePanel(c, files, chat.ProjectKnowledgePanelOptions{Capacity: 200000}) if v.Add { log.Println("pick files") } diff --git a/docs/components/246-welcome-screen.md b/docs/components/246-welcome-screen.md index 910f58f..afb51a0 100644 --- a/docs/components/246-welcome-screen.md +++ b/docs/components/246-welcome-screen.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -cards := []mujica.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} +cards := []chat.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { + core.Use(c, core.Settings{}) + if v := chat.WelcomeScreen(c, chat.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { log.Println("ask", v.Example) } } diff --git a/docs/components/247-suggestion-chips.md b/docs/components/247-suggestion-chips.md index e8ae72f..06700c6 100644 --- a/docs/components/247-suggestion-chips.md +++ b/docs/components/247-suggestion-chips.md @@ -9,8 +9,8 @@ ```go input := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{}) + core.Use(c, core.Settings{}) + chat.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, chat.SuggestionChipsOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/248-prompt-library.md b/docs/components/248-prompt-library.md index bd57cd9..418b88e 100644 --- a/docs/components/248-prompt-library.md +++ b/docs/components/248-prompt-library.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.PromptLibraryState -templates := []mujica.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} +var state chat.PromptLibraryState +templates := []chat.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.PromptLibrary(c, &state, templates, mujica.PromptLibraryOptions{}); v.Inserted != "" { + core.Use(c, core.Settings{}) + if v := chat.PromptLibrary(c, &state, templates, chat.PromptLibraryOptions{}); v.Inserted != "" { log.Println("insert", v.Inserted) } } diff --git a/docs/components/249-capability-cards.md b/docs/components/249-capability-cards.md index 0c1c886..f068172 100644 --- a/docs/components/249-capability-cards.md +++ b/docs/components/249-capability-cards.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -cards := []mujica.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} +cards := []chat.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.CapabilityCards(c, cards, mujica.CapabilityCardsOptions{}); v.Example != "" { + core.Use(c, core.Settings{}) + if v := chat.CapabilityCards(c, cards, chat.CapabilityCardsOptions{}); v.Example != "" { log.Println("ask", v.Example) } } diff --git a/docs/components/250-feedback-form.md b/docs/components/250-feedback-form.md index 3fa8600..3469e2c 100644 --- a/docs/components/250-feedback-form.md +++ b/docs/components/250-feedback-form.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var value mujica.FeedbackValue +var value chat.FeedbackValue view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FeedbackForm(c, &value, mujica.FeedbackFormOptions{Rating: mujica.FeedbackDown}).Submitted { + core.Use(c, core.Settings{}) + if chat.FeedbackForm(c, &value, chat.FeedbackFormOptions{Rating: chat.FeedbackDown}).Submitted { log.Println("reasons", value.Reasons) } } diff --git a/docs/components/251-date-separator.md b/docs/components/251-date-separator.md index edd1bdc..257fb40 100644 --- a/docs/components/251-date-separator.md +++ b/docs/components/251-date-separator.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DateSeparator(c, mujica.DateOf(time.Now()), mujica.DateSeparatorOptions{}) + core.Use(c, core.Settings{}) + chat.DateSeparator(c, core.DateOf(time.Now()), chat.DateSeparatorOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/252-thinking-indicator.md b/docs/components/252-thinking-indicator.md index 355da7b..fcf9165 100644 --- a/docs/components/252-thinking-indicator.md +++ b/docs/components/252-thinking-indicator.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: "Searching the archives"}) + core.Use(c, core.Settings{}) + chat.ThinkingIndicator(c, chat.ThinkingIndicatorOptions{Phase: "Searching the archives"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/253-stop-generating-button.md b/docs/components/253-stop-generating-button.md index 33620d7..96a8879 100644 --- a/docs/components/253-stop-generating-button.md +++ b/docs/components/253-stop-generating-button.md @@ -9,8 +9,8 @@ ```go generating := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: generating}).Changed() { + core.Use(c, core.Settings{}) + if chat.StopGeneratingButton(c, chat.StopGeneratingButtonOptions{Generating: generating}).Changed() { generating = false } } diff --git a/docs/components/254-system-prompt-editor.md b/docs/components/254-system-prompt-editor.md index 27fdb0b..3fefb13 100644 --- a/docs/components/254-system-prompt-editor.md +++ b/docs/components/254-system-prompt-editor.md @@ -9,8 +9,8 @@ ```go prompt, saved := "You are helpful.", "You are helpful." view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemPromptEditor(c, &prompt, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { + core.Use(c, core.Settings{}) + if chat.SystemPromptEditor(c, &prompt, chat.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { saved = prompt } } diff --git a/docs/components/255-cost-estimator.md b/docs/components/255-cost-estimator.md index aa9bded..20ca6af 100644 --- a/docs/components/255-cost-estimator.md +++ b/docs/components/255-cost-estimator.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) + core.Use(c, core.Settings{}) + chat.CostEstimator(c, chat.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/256-shared-conversation-view.md b/docs/components/256-shared-conversation-view.md index cda30ed..30df950 100644 --- a/docs/components/256-shared-conversation-view.md +++ b/docs/components/256-shared-conversation-view.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}, {Role: mujica.SessionAssistant, Text: "Good day."}} +msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}, {Role: chat.SessionAssistant, Text: "Good day."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SharedConversationView(c, msgs, mujica.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { + core.Use(c, core.Settings{}) + if chat.SharedConversationView(c, msgs, chat.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { c.WriteClipboard("https://example.com/share/1") } } diff --git a/docs/components/257-conversation-export.md b/docs/components/257-conversation-export.md index 61a5f86..ca2c56d 100644 --- a/docs/components/257-conversation-export.md +++ b/docs/components/257-conversation-export.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ConversationExportState -msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}} +var state chat.ConversationExportState +msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ConversationExport(c, &state, msgs, mujica.ConversationExportOptions{Title: "Greeting"}); v.Exported { + core.Use(c, core.Settings{}) + if v := chat.ConversationExport(c, &state, msgs, chat.ConversationExportOptions{Title: "Greeting"}); v.Exported { log.Println(len(v.Content), "bytes to save") } } diff --git a/docs/components/258-regenerate-menu.md b/docs/components/258-regenerate-menu.md index c22739d..5a61efd 100644 --- a/docs/components/258-regenerate-menu.md +++ b/docs/components/258-regenerate-menu.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { + core.Use(c, core.Settings{}) + if v := chat.RegenerateMenu(c, chat.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { log.Println("retry", v.Choice.Kind, v.Choice.Model, v.Choice.Variant) } } diff --git a/docs/components/259-artifact-card.md b/docs/components/259-artifact-card.md index ed8601b..66268f8 100644 --- a/docs/components/259-artifact-card.md +++ b/docs/components/259-artifact-card.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactCard(c, mujica.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { + core.Use(c, core.Settings{}) + if chat.ArtifactCard(c, chat.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { log.Println("open artifact panel") } } diff --git a/docs/components/260-chat-empty-state.md b/docs/components/260-chat-empty-state.md index 3b3852e..3d57b05 100644 --- a/docs/components/260-chat-empty-state.md +++ b/docs/components/260-chat-empty-state.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyOffline}).Action { + core.Use(c, core.Settings{}) + if chat.ChatEmptyState(c, chat.ChatEmptyStateOptions{Kind: chat.ChatEmptyOffline}).Action { log.Println("reconnect") } } diff --git a/docs/components/261-tool-call-card.md b/docs/components/261-tool-call-card.md index 71f100f..f608096 100644 --- a/docs/components/261-tool-call-card.md +++ b/docs/components/261-tool-call-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -call := mujica.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", - Result: "{\"lines\":40}", State: mujica.AgentDone, Duration: 420 * time.Millisecond} +call := agent.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", + Result: "{\"lines\":40}", State: agent.AgentDone, Duration: 420 * time.Millisecond} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{OpenArgs: true}) + core.Use(c, core.Settings{}) + agent.ToolCallCard(c, call, agent.ToolCallCardOptions{OpenArgs: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/262-tool-call-group.md b/docs/components/262-tool-call-group.md index 76f405a..e8b52ab 100644 --- a/docs/components/262-tool-call-group.md +++ b/docs/components/262-tool-call-group.md @@ -8,13 +8,13 @@ ```go open := false -calls := []mujica.ToolCall{ - {ID: "a", Name: "list_dir", State: mujica.AgentDone, Duration: 300 * time.Millisecond}, - {ID: "b", Name: "read_file", State: mujica.AgentFailed, Error: "not found"}, +calls := []agent.ToolCall{ + {ID: "a", Name: "list_dir", State: agent.AgentDone, Duration: 300 * time.Millisecond}, + {ID: "b", Name: "read_file", State: agent.AgentFailed, Error: "not found"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallGroup(c, &open, calls, mujica.ToolCallGroupOptions{}) + core.Use(c, core.Settings{}) + agent.ToolCallGroup(c, &open, calls, agent.ToolCallGroupOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/263-tool-approval-dialog.md b/docs/components/263-tool-approval-dialog.md index 20932bf..9e1d0e6 100644 --- a/docs/components/263-tool-approval-dialog.md +++ b/docs/components/263-tool-approval-dialog.md @@ -9,8 +9,8 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolApprovalDialog(c, &open, mujica.ToolApprovalOptions{ + core.Use(c, core.Settings{}) + r := agent.ToolApprovalDialog(c, &open, agent.ToolApprovalOptions{ Tool: "delete_files", Args: "{\"glob\":\"*.tmp\"}", Reason: "Clean the build folder.", }) if r.Allowed { diff --git a/docs/components/264-permission-prompt.md b/docs/components/264-permission-prompt.md index c3b4e9e..3baef06 100644 --- a/docs/components/264-permission-prompt.md +++ b/docs/components/264-permission-prompt.md @@ -9,11 +9,11 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PermissionPrompt(c, &open, mujica.PermissionPromptOptions{ + core.Use(c, core.Settings{}) + r := agent.PermissionPrompt(c, &open, agent.PermissionPromptOptions{ Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger", }) - if r.Choice == mujica.PermissionAllowAlways { + if r.Choice == agent.PermissionAllowAlways { fmt.Println("remember the grant") } } diff --git a/docs/components/265-agent-step-list.md b/docs/components/265-agent-step-list.md index 5f87bed..d54b6dc 100644 --- a/docs/components/265-agent-step-list.md +++ b/docs/components/265-agent-step-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -steps := []mujica.AgentStep{ - {ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: time.Second}, - {ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Detail: "Table 4 is full."}, - {ID: "send", Title: "Send invitations", State: mujica.AgentPending}, +steps := []agent.AgentStep{ + {ID: "read", Title: "Read the guest list", State: agent.AgentDone, Duration: time.Second}, + {ID: "seat", Title: "Assign the seats", State: agent.AgentFailed, Detail: "Table 4 is full."}, + {ID: "send", Title: "Send invitations", State: agent.AgentPending}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.AgentStepList(c, steps, mujica.AgentStepListOptions{}); r.Retry != "" { + core.Use(c, core.Settings{}) + if r := agent.AgentStepList(c, steps, agent.AgentStepListOptions{}); r.Retry != "" { fmt.Println("retry", r.Retry) } } diff --git a/docs/components/266-agent-plan.md b/docs/components/266-agent-plan.md index d2f4ec1..c4160e4 100644 --- a/docs/components/266-agent-plan.md +++ b/docs/components/266-agent-plan.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -items := []mujica.PlanItem{ - {ID: "a", Text: "Read the guest list", State: mujica.AgentDone}, - {ID: "b", Text: "Assign the seats", State: mujica.AgentRunning}, +items := []agent.PlanItem{ + {ID: "a", Text: "Read the guest list", State: agent.AgentDone}, + {ID: "b", Text: "Assign the seats", State: agent.AgentRunning}, {ID: "c", Text: "Order the flowers"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentPlan(c, &items, mujica.AgentPlanOptions{}) + core.Use(c, core.Settings{}) + agent.AgentPlan(c, &items, agent.AgentPlanOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/267-agent-status.md b/docs/components/267-agent-status.md index 15637fc..4bc87dd 100644 --- a/docs/components/267-agent-status.md +++ b/docs/components/267-agent-status.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentStatus(c, mujica.AgentWaiting, mujica.AgentStatusOptions{}) + core.Use(c, core.Settings{}) + agent.AgentStatus(c, agent.AgentWaiting, agent.AgentStatusOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/268-agent-progress.md b/docs/components/268-agent-progress.md index b25e5d1..9cab6e1 100644 --- a/docs/components/268-agent-progress.md +++ b/docs/components/268-agent-progress.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentProgress(c, 0.4, mujica.AgentProgressOptions{Action: "Reading ledger.md"}) + core.Use(c, core.Settings{}) + agent.AgentProgress(c, 0.4, agent.AgentProgressOptions{Action: "Reading ledger.md"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/269-sub-agent-tree.md b/docs/components/269-sub-agent-tree.md index 5349566..d9cef12 100644 --- a/docs/components/269-sub-agent-tree.md +++ b/docs/components/269-sub-agent-tree.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", State: mujica.AgentRunning, Children: []mujica.SubAgent{ - {ID: "cook", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting}, +agents := []agent.SubAgent{{ID: "lead", Name: "Steward", State: agent.AgentRunning, Children: []agent.SubAgent{ + {ID: "cook", Name: "Cook", Task: "Plan the menu", State: agent.AgentWaiting}, }}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{}) + core.Use(c, core.Settings{}) + agent.SubAgentTree(c, agents, agent.SubAgentTreeOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/270-file-change-card.md b/docs/components/270-file-change-card.md index 045a63f..df4c554 100644 --- a/docs/components/270-file-change-card.md +++ b/docs/components/270-file-change-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -decision := mujica.FilePending -change := mujica.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} +decision := agent.FilePending +change := agent.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FileChangeCard(c, &decision, change, mujica.FileChangeCardOptions{}).OpenDiff { + core.Use(c, core.Settings{}) + if agent.FileChangeCard(c, &decision, change, agent.FileChangeCardOptions{}).OpenDiff { fmt.Println("open the full diff") } } diff --git a/docs/components/271-multi-file-diff-review.md b/docs/components/271-multi-file-diff-review.md index b60c4cb..cec4fd4 100644 --- a/docs/components/271-multi-file-diff-review.md +++ b/docs/components/271-multi-file-diff-review.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -files := []mujica.FileChange{ +files := []agent.FileChange{ {Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"}, {Path: "menu.md", Old: "soup\n", New: "soup\ntart\n"}, } -decisions := make([]mujica.FileDecision, len(files)) +decisions := make([]agent.FileDecision, len(files)) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiFileDiffReview(c, &decisions, files, mujica.MultiFileDiffReviewOptions{}) + core.Use(c, core.Settings{}) + agent.MultiFileDiffReview(c, &decisions, files, agent.MultiFileDiffReviewOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/272-command-execution-card.md b/docs/components/272-command-execution-card.md index 8d0ca9d..8cbfc80 100644 --- a/docs/components/272-command-execution-card.md +++ b/docs/components/272-command-execution-card.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -run := mujica.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} +run := agent.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CommandExecutionCard(c, run, mujica.CommandExecutionCardOptions{}).Changed() { + core.Use(c, core.Settings{}) + if agent.CommandExecutionCard(c, run, agent.CommandExecutionCardOptions{}).Changed() { fmt.Println("abort the process") } } diff --git a/docs/components/273-screenshot-stream.md b/docs/components/273-screenshot-stream.md index 2c2b652..593fe00 100644 --- a/docs/components/273-screenshot-stream.md +++ b/docs/components/273-screenshot-stream.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var st mujica.ScreenshotStreamState -var frames []mujica.ScreenshotFrame // appended as the agent captures them +var st agent.ScreenshotStreamState +var frames []agent.ScreenshotFrame // appended as the agent captures them view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenshotStream(c, &st, frames, mujica.ScreenshotStreamOptions{}) + core.Use(c, core.Settings{}) + agent.ScreenshotStream(c, &st, frames, agent.ScreenshotStreamOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/274-artifact-panel.md b/docs/components/274-artifact-panel.md index 58164e4..c77871a 100644 --- a/docs/components/274-artifact-panel.md +++ b/docs/components/274-artifact-panel.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -art := mujica.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} +art := agent.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactPanel(c, art, mujica.ArtifactPanelOptions{}).Exported { + core.Use(c, core.Settings{}) + if agent.ArtifactPanel(c, art, agent.ArtifactPanelOptions{}).Exported { fmt.Println("save the file") } } diff --git a/docs/components/275-artifact-version-switcher.md b/docs/components/275-artifact-version-switcher.md index 0cabdb2..7ed5460 100644 --- a/docs/components/275-artifact-version-switcher.md +++ b/docs/components/275-artifact-version-switcher.md @@ -8,10 +8,10 @@ ```go current := 1 -versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} +versions := []agent.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactVersionSwitcher(c, ¤t, versions, mujica.ArtifactVersionSwitcherOptions{}).Compare { + core.Use(c, core.Settings{}) + if agent.ArtifactVersionSwitcher(c, ¤t, versions, agent.ArtifactVersionSwitcherOptions{}).Compare { fmt.Println("diff v1 and v2") } } diff --git a/docs/components/276-sandbox-status.md b/docs/components/276-sandbox-status.md index 495f6da..0f22c73 100644 --- a/docs/components/276-sandbox-status.md +++ b/docs/components/276-sandbox-status.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -info := mujica.SandboxInfo{Name: "sandbox-7", State: mujica.SandboxRunning, - Resources: []mujica.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} +info := agent.SandboxInfo{Name: "sandbox-7", State: agent.SandboxRunning, + Resources: []agent.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SandboxStatus(c, info, mujica.SandboxStatusOptions{}) + core.Use(c, core.Settings{}) + agent.SandboxStatus(c, info, agent.SandboxStatusOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/277-mcp-server-list.md b/docs/components/277-mcp-server-list.md index 82a5d34..9a5f76d 100644 --- a/docs/components/277-mcp-server-list.md +++ b/docs/components/277-mcp-server-list.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -servers := []mujica.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12}} +servers := []agent.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: agent.MCPConnected, Tools: 12}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MCPServerList(c, servers, mujica.MCPServerListOptions{}); r.Action == mujica.MCPStop { + core.Use(c, core.Settings{}) + if r := agent.MCPServerList(c, servers, agent.MCPServerListOptions{}); r.Action == agent.MCPStop { fmt.Println("stop", r.Server) } } diff --git a/docs/components/278-memory-panel.md b/docs/components/278-memory-panel.md index e9ff905..c30ba4e 100644 --- a/docs/components/278-memory-panel.md +++ b/docs/components/278-memory-panel.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -entries := []mujica.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} +entries := []agent.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemoryPanel(c, &entries, mujica.MemoryPanelOptions{}); r.Deleted != "" { + core.Use(c, core.Settings{}) + if r := agent.MemoryPanel(c, &entries, agent.MemoryPanelOptions{}); r.Deleted != "" { fmt.Println("forget", r.Deleted) } } diff --git a/docs/components/279-checkpoint-list.md b/docs/components/279-checkpoint-list.md index 3854182..c04b55f 100644 --- a/docs/components/279-checkpoint-list.md +++ b/docs/components/279-checkpoint-list.md @@ -8,10 +8,10 @@ ```go current := "c2" -points := []mujica.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} +points := []agent.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CheckpointList(c, ¤t, points, mujica.CheckpointListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if agent.CheckpointList(c, ¤t, points, agent.CheckpointListOptions{}).Changed() { fmt.Println("restore", current) } } diff --git a/docs/components/280-human-input-request.md b/docs/components/280-human-input-request.md index 288fbba..5253d79 100644 --- a/docs/components/280-human-input-request.md +++ b/docs/components/280-human-input-request.md @@ -9,8 +9,8 @@ Agent 向用户提问:显示问题、可选答案与自由回答,并提示 ```go answer := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.HumanInputRequest(c, &answer, mujica.HumanInputRequestOptions{ + core.Use(c, core.Settings{}) + r := agent.HumanInputRequest(c, &answer, agent.HumanInputRequestOptions{ Question: "Which hall?", Options: []string{"East wing", "Great hall"}, AllowFree: true, }) if r.Submitted { diff --git a/docs/components/281-interrupt-button.md b/docs/components/281-interrupt-button.md index 1133545..e056370 100644 --- a/docs/components/281-interrupt-button.md +++ b/docs/components/281-interrupt-button.md @@ -9,8 +9,8 @@ ```go running, stopping := true, false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.InterruptButton(c, running, mujica.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) + core.Use(c, core.Settings{}) + r := feedback.InterruptButton(c, running, feedback.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) if r.Changed() { stopping = true // cancel the run, then clear running and stopping } diff --git a/docs/components/282-token-usage-chart.md b/docs/components/282-token-usage-chart.md index 1e04edb..8770d74 100644 --- a/docs/components/282-token-usage-chart.md +++ b/docs/components/282-token-usage-chart.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -usage := []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} +usage := []feedback.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{}) + core.Use(c, core.Settings{}) + feedback.TokenUsageChart(c, usage, feedback.TokenUsageChartOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/283-cost-breakdown.md b/docs/components/283-cost-breakdown.md index 982741d..5ba303c 100644 --- a/docs/components/283-cost-breakdown.md +++ b/docs/components/283-cost-breakdown.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var state mujica.CostBreakdownState -items := []mujica.CostItem{ +var state feedback.CostBreakdownState +items := []feedback.CostItem{ {Model: "opus", Tool: "search", InputTokens: 1000, OutputTokens: 200, Cost: 0.30}, {Model: "haiku", Tool: "search", InputTokens: 4000, OutputTokens: 500, Cost: 0.05}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostBreakdown(c, &state, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostByModel}).Element.Height(260) + core.Use(c, core.Settings{}) + feedback.CostBreakdown(c, &state, items, feedback.CostBreakdownOptions{GroupBy: feedback.CostByModel}).Element.Height(260) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/284-trace-viewer.md b/docs/components/284-trace-viewer.md index 065e2ed..0e3d185 100644 --- a/docs/components/284-trace-viewer.md +++ b/docs/components/284-trace-viewer.md @@ -7,15 +7,15 @@ ## 最小示例 ```go -var state mujica.TraceViewerState -spans := []mujica.TraceSpan{ +var state feedback.TraceViewerState +spans := []feedback.TraceSpan{ {ID: "run", Name: "agent.run", End: 900 * time.Millisecond}, {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, - Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}}}, + Attrs: []feedback.TraceAttr{{Key: "model", Value: "opus"}}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TraceViewer(c, &state, spans, mujica.TraceViewerOptions{}).Element.Height(260) + core.Use(c, core.Settings{}) + feedback.TraceViewer(c, &state, spans, feedback.TraceViewerOptions{}).Element.Height(260) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/285-eval-result-table.md b/docs/components/285-eval-result-table.md index 44d75ce..ce36921 100644 --- a/docs/components/285-eval-result-table.md +++ b/docs/components/285-eval-result-table.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.DataTableState[string] -cases := []mujica.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} +var state data.DataTableState[string] +cases := []feedback.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EvalResultTable(c, &state, cases, mujica.EvalResultTableOptions{}) + core.Use(c, core.Settings{}) + r := feedback.EvalResultTable(c, &state, cases, feedback.EvalResultTableOptions{}) r.Table.Element.Height(260) if id, ok := r.Diff(); ok { _ = id // open expected and actual output side by side diff --git a/docs/components/286-tool-registry-panel.md b/docs/components/286-tool-registry-panel.md index 853a105..03c67d3 100644 --- a/docs/components/286-tool-registry-panel.md +++ b/docs/components/286-tool-registry-panel.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -var state mujica.ListState[string] -tools := []mujica.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, - Params: []mujica.ToolParam{{Name: "query", Type: "string", Required: true}}}} +var state data.ListState[string] +tools := []feedback.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, + Params: []feedback.ToolParam{{Name: "query", Type: "string", Required: true}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolRegistryPanel(c, &state, tools, mujica.ToolRegistryPanelOptions{}) + core.Use(c, core.Settings{}) + r := feedback.ToolRegistryPanel(c, &state, tools, feedback.ToolRegistryPanelOptions{}) r.Element.Height(260) if name, on, ok := r.Toggled(); ok && name == tools[0].Name { tools[0].Enabled = on diff --git a/docs/components/287-dots-loader.md b/docs/components/287-dots-loader.md index 0e741c9..9822044 100644 --- a/docs/components/287-dots-loader.md +++ b/docs/components/287-dots-loader.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Thinking", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.DotsLoader(c, feedback.LoaderOptions{Label: "Thinking", ShowLabel: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/288-bars-loader.md b/docs/components/288-bars-loader.md index 57355b3..fd70f64 100644 --- a/docs/components/288-bars-loader.md +++ b/docs/components/288-bars-loader.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarsLoader(c, mujica.LoaderOptions{Size: 20, Label: "Indexing"}) + core.Use(c, core.Settings{}) + feedback.BarsLoader(c, feedback.LoaderOptions{Size: 20, Label: "Indexing"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/289-pulse-loader.md b/docs/components/289-pulse-loader.md index 6646e34..a57fde0 100644 --- a/docs/components/289-pulse-loader.md +++ b/docs/components/289-pulse-loader.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PulseLoader(c, mujica.LoaderOptions{Label: "Listening", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.PulseLoader(c, feedback.LoaderOptions{Label: "Listening", ShowLabel: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/290-orbit-loader.md b/docs/components/290-orbit-loader.md index 0238440..72d901d 100644 --- a/docs/components/290-orbit-loader.md +++ b/docs/components/290-orbit-loader.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OrbitLoader(c, mujica.LoaderOptions{Size: 24, Label: "Connecting"}) + core.Use(c, core.Settings{}) + feedback.OrbitLoader(c, feedback.LoaderOptions{Size: 24, Label: "Connecting"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/291-wave-loader.md b/docs/components/291-wave-loader.md index d60949a..3fc0dc6 100644 --- a/docs/components/291-wave-loader.md +++ b/docs/components/291-wave-loader.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaveLoader(c, mujica.LoaderOptions{Label: "Speaking", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.WaveLoader(c, feedback.LoaderOptions{Label: "Speaking", ShowLabel: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/292-shimmer.md b/docs/components/292-shimmer.md index 563be7e..5b05200 100644 --- a/docs/components/292-shimmer.md +++ b/docs/components/292-shimmer.md @@ -9,8 +9,8 @@ ```go loading := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Shimmer(c, loading, mujica.ShimmerOptions{Height: 48}, func() { + core.Use(c, core.Settings{}) + feedback.Shimmer(c, loading, feedback.ShimmerOptions{Height: 48}, func() { ui.Text(c, "Report ready") }) } diff --git a/docs/components/293-count-up.md b/docs/components/293-count-up.md index 686a205..d303848 100644 --- a/docs/components/293-count-up.md +++ b/docs/components/293-count-up.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CountUp(c, 12840, mujica.CountUpOptions{Duration: 800 * time.Millisecond}) + core.Use(c, core.Settings{}) + feedback.CountUp(c, 12840, feedback.CountUpOptions{Duration: 800 * time.Millisecond}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/294-typewriter.md b/docs/components/294-typewriter.md index d2e4596..d3457cd 100644 --- a/docs/components/294-typewriter.md +++ b/docs/components/294-typewriter.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Typewriter(c, "The court assembles at dusk.", mujica.TypewriterOptions{Speed: 30, Skippable: true}) + core.Use(c, core.Settings{}) + r := feedback.Typewriter(c, "The court assembles at dusk.", feedback.TypewriterOptions{Speed: 30, Skippable: true}) if r.Changed() { // show the next message } diff --git a/docs/components/295-shimmer-text.md b/docs/components/295-shimmer-text.md index a9cefab..876af8d 100644 --- a/docs/components/295-shimmer-text.md +++ b/docs/components/295-shimmer-text.md @@ -9,8 +9,8 @@ ```go thinking := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShimmerText(c, "Thinking…", mujica.ShimmerTextOptions{Active: thinking}) + core.Use(c, core.Settings{}) + feedback.ShimmerText(c, "Thinking…", feedback.ShimmerTextOptions{Active: thinking}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/296-stagger.md b/docs/components/296-stagger.md index ac1db7d..1db967a 100644 --- a/docs/components/296-stagger.md +++ b/docs/components/296-stagger.md @@ -9,8 +9,8 @@ ```go guests := []string{"Sakiko", "Uika", "Mutsumi"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Stagger(c, len(guests), mujica.StaggerOptions{}, func(i int) { + core.Use(c, core.Settings{}) + feedback.Stagger(c, len(guests), feedback.StaggerOptions{}, func(i int) { ui.Text(c, guests[i]) }) } diff --git a/docs/components/297-reorder-transition.md b/docs/components/297-reorder-transition.md index 7ddc9c5..1ad3ab0 100644 --- a/docs/components/297-reorder-transition.md +++ b/docs/components/297-reorder-transition.md @@ -9,8 +9,8 @@ ```go order := []string{"Sakiko", "Uika", "Mutsumi"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReorderTransition(c, order, mujica.ReorderTransitionOptions{Gap: 4}, func(name string) { + core.Use(c, core.Settings{}) + feedback.ReorderTransition(c, order, feedback.ReorderTransitionOptions{Gap: 4}, func(name string) { ui.Text(c, name) }) } diff --git a/docs/components/298-layout-transition.md b/docs/components/298-layout-transition.md index cb3cffb..d693431 100644 --- a/docs/components/298-layout-transition.md +++ b/docs/components/298-layout-transition.md @@ -9,8 +9,8 @@ ```go open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: true}, func() { + core.Use(c, core.Settings{}) + feedback.LayoutTransition(c, feedback.LayoutTransitionOptions{Clip: true}, func() { ui.Text(c, "Program") if open { ui.Text(c, "Overture · Masquerade · Finale") diff --git a/docs/components/299-presence.md b/docs/components/299-presence.md index 82b0c42..d1b1602 100644 --- a/docs/components/299-presence.md +++ b/docs/components/299-presence.md @@ -9,8 +9,8 @@ ```go show := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Presence(c, show, mujica.PresenceOptions{Kind: mujica.PresenceSlide}, func() { + core.Use(c, core.Settings{}) + feedback.Presence(c, show, feedback.PresenceOptions{Kind: feedback.PresenceSlide}, func() { ui.Text(c, "Saved") }) } diff --git a/docs/components/300-blink-highlight.md b/docs/components/300-blink-highlight.md index 533e567..3538317 100644 --- a/docs/components/300-blink-highlight.md +++ b/docs/components/300-blink-highlight.md @@ -9,8 +9,8 @@ ```go price := 101.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlinkHighlight(c, price, mujica.BlinkHighlightOptions{}) + core.Use(c, core.Settings{}) + feedback.BlinkHighlight(c, price, feedback.BlinkHighlightOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/301-diff-viewer.md b/docs/components/301-diff-viewer.md index e002e9f..d2e7c9b 100644 --- a/docs/components/301-diff-viewer.md +++ b/docs/components/301-diff-viewer.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -var state mujica.DiffViewerState +var state git.DiffViewerState from := "func main() {\n\tprintln(\"hi\")\n}\n" to := "func main() {\n\tprintln(\"hello\")\n}\n" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffViewer(c, &state, from, to, mujica.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) + core.Use(c, core.Settings{}) + git.DiffViewer(c, &state, from, to, git.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/302-inline-diff.md b/docs/components/302-inline-diff.md index f3e5410..6891fbf 100644 --- a/docs/components/302-inline-diff.md +++ b/docs/components/302-inline-diff.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.InlineDiff(c, "The court meets at noon", "The court meets at dusk", mujica.InlineDiffOptions{}) + core.Use(c, core.Settings{}) + git.InlineDiff(c, "The court meets at noon", "The court meets at dusk", git.InlineDiffOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/303-three-way-merge.md b/docs/components/303-three-way-merge.md index c03a2be..7024be8 100644 --- a/docs/components/303-three-way-merge.md +++ b/docs/components/303-three-way-merge.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ThreeWayMergeState +var state git.ThreeWayMergeState base, local, remote := "a\nb\nc", "a\nB1\nc", "a\nB2\nc" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ThreeWayMerge(c, &state, base, local, remote, mujica.ThreeWayMergeOptions{}) + core.Use(c, core.Settings{}) + r := git.ThreeWayMerge(c, &state, base, local, remote, git.ThreeWayMergeOptions{}) r.Element.Height(280) if done, total := state.Resolved(); r.Changed() && done == total { _ = state.Result() // write the merged file diff --git a/docs/components/304-conflict-resolver.md b/docs/components/304-conflict-resolver.md index 8f7fed8..396675f 100644 --- a/docs/components/304-conflict-resolver.md +++ b/docs/components/304-conflict-resolver.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ConflictResolverState +var state git.ConflictResolverState text := "a\n<<<<<<< HEAD\nmine\n=======\ntheirs\n>>>>>>> feature\nb" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConflictResolver(c, &state, text, mujica.ConflictResolverOptions{}).Changed() { + core.Use(c, core.Settings{}) + if git.ConflictResolver(c, &state, text, git.ConflictResolverOptions{}).Changed() { _ = state.Result() // save the file once every block is resolved } } diff --git a/docs/components/305-changes-list.md b/docs/components/305-changes-list.md index 54ae845..4b46d58 100644 --- a/docs/components/305-changes-list.md +++ b/docs/components/305-changes-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var state mujica.ChangesListState -files := []mujica.ChangedFile{ - {Path: "main.go", Status: mujica.GitModified, Staged: true}, - {Path: "notes.txt", Status: mujica.GitUntracked}, +var state git.ChangesListState +files := []git.ChangedFile{ + {Path: "main.go", Status: git.GitModified, Staged: true}, + {Path: "notes.txt", Status: git.GitUntracked}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ChangesList(c, &state, files, mujica.ChangesListOptions{}) + core.Use(c, core.Settings{}) + r := git.ChangesList(c, &state, files, git.ChangesListOptions{}) r.Element.Height(240) if action, path, ok := r.Action(); ok { _, _ = action, path // run git add or git restore --staged diff --git a/docs/components/306-commit-input.md b/docs/components/306-commit-input.md index 4676a1a..6cec505 100644 --- a/docs/components/306-commit-input.md +++ b/docs/components/306-commit-input.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -msg := mujica.CommitMessage{} +msg := git.CommitMessage{} busy := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitInput(c, &msg, mujica.CommitInputOptions{Busy: busy}) + core.Use(c, core.Settings{}) + r := git.CommitInput(c, &msg, git.CommitInputOptions{Busy: busy}) if r.Committed() || r.Amended() { busy = true // run git commit, then clear busy } diff --git a/docs/components/307-commit-graph.md b/docs/components/307-commit-graph.md index 82398ea..df32d9b 100644 --- a/docs/components/307-commit-graph.md +++ b/docs/components/307-commit-graph.md @@ -7,16 +7,16 @@ ## 最小示例 ```go -var state mujica.CommitGraphState -commits := []mujica.Commit{ +var state git.CommitGraphState +commits := []git.Commit{ {Hash: "c3", Parents: []string{"c2", "b1"}, Subject: "Merge branch", Refs: []string{"main"}}, {Hash: "b1", Parents: []string{"c1"}, Subject: "Feature"}, {Hash: "c2", Parents: []string{"c1"}, Subject: "Fix"}, {Hash: "c1", Subject: "Initial commit"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CommitGraph(c, &state, commits, mujica.CommitGraphOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + git.CommitGraph(c, &state, commits, git.CommitGraphOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/308-commit-list.md b/docs/components/308-commit-list.md index 1cc292b..8a03df3 100644 --- a/docs/components/308-commit-list.md +++ b/docs/components/308-commit-list.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ListState[string] -commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} +var state data.ListState[string] +commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitList(c, &state, commits, mujica.CommitListOptions{}) + core.Use(c, core.Settings{}) + r := git.CommitList(c, &state, commits, git.CommitListOptions{}) r.Element.Height(240) if hash, ok := state.Selected(); ok && r.Submitted() { _ = hash // show the commit diff --git a/docs/components/309-branch-selector.md b/docs/components/309-branch-selector.md index e567aec..9e24695 100644 --- a/docs/components/309-branch-selector.md +++ b/docs/components/309-branch-selector.md @@ -8,10 +8,10 @@ ```go branch := "main" -branches := []mujica.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} +branches := []git.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchSelector(c, &branch, branches, mujica.BranchSelectorOptions{AllowCreate: true}) + core.Use(c, core.Settings{}) + r := git.BranchSelector(c, &branch, branches, git.BranchSelectorOptions{AllowCreate: true}) if name, ok := r.Created(); ok { _ = name // run git switch -c name } diff --git a/docs/components/310-branch-list.md b/docs/components/310-branch-list.md index cec3aa1..604d1a4 100644 --- a/docs/components/310-branch-list.md +++ b/docs/components/310-branch-list.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -var state mujica.ListState[string] -branches := []mujica.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} -actions := []mujica.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} +var state data.ListState[string] +branches := []git.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} +actions := []git.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchList(c, &state, branches, mujica.BranchListOptions{Actions: actions}) + core.Use(c, core.Settings{}) + r := git.BranchList(c, &state, branches, git.BranchListOptions{Actions: actions}) r.Element.Height(200) if action, name, ok := r.Action(); ok { _, _ = action, name // run the action on the branch diff --git a/docs/components/311-tag-list.md b/docs/components/311-tag-list.md index 13d9b89..1b0eba4 100644 --- a/docs/components/311-tag-list.md +++ b/docs/components/311-tag-list.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ListState[string] -tags := []mujica.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} +var state data.ListState[string] +tags := []git.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TagList(c, &state, tags, mujica.TagListOptions{}) + core.Use(c, core.Settings{}) + r := git.TagList(c, &state, tags, git.TagListOptions{}) r.Element.Height(200) if _, hash, ok := r.Action(); ok { _ = hash // show the tagged commit diff --git a/docs/components/312-stash-list.md b/docs/components/312-stash-list.md index f7901bd..cd3f914 100644 --- a/docs/components/312-stash-list.md +++ b/docs/components/312-stash-list.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ListState[string] -stashes := []mujica.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} +var state data.ListState[string] +stashes := []git.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.StashList(c, &state, stashes, mujica.StashListOptions{}) + core.Use(c, core.Settings{}) + r := git.StashList(c, &state, stashes, git.StashListOptions{}) r.Element.Height(200) if action, ref, ok := r.Action(); ok { _, _ = action, ref // run git stash apply, pop or drop diff --git a/docs/components/313-blame-view.md b/docs/components/313-blame-view.md index ba5eada..8d468f5 100644 --- a/docs/components/313-blame-view.md +++ b/docs/components/313-blame-view.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var state mujica.BlameViewState -lines := []mujica.BlameLine{ +var state git.BlameViewState +lines := []git.BlameLine{ {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "package main"}, {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "func main() {}"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlameView(c, &state, lines, mujica.BlameViewOptions{Language: "go"}).Height(240) + core.Use(c, core.Settings{}) + git.BlameView(c, &state, lines, git.BlameViewOptions{Language: "go"}).Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/314-file-history.md b/docs/components/314-file-history.md index 5aab49a..97f192c 100644 --- a/docs/components/314-file-history.md +++ b/docs/components/314-file-history.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var state mujica.ListState[string] -commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} +var state data.ListState[string] +commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.FileHistory(c, &state, "main.go", commits, mujica.FileHistoryOptions{}) + core.Use(c, core.Settings{}) + r := git.FileHistory(c, &state, "main.go", commits, git.FileHistoryOptions{}) r.Element.Height(240) if _, hash, ok := r.Action(); ok { _ = hash // open the diff of main.go at hash diff --git a/docs/components/315-pull-request-card.md b/docs/components/315-pull-request-card.md index a5ad61a..54c9b38 100644 --- a/docs/components/315-pull-request-card.md +++ b/docs/components/315-pull-request-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -pr := mujica.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", - ChecksPassed: 3, Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} +pr := git.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", + ChecksPassed: 3, Reviewers: []git.Reviewer{{Name: "Uika", State: git.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{}).Changed() { + core.Use(c, core.Settings{}) + if git.PullRequestCard(c, pr, git.PullRequestCardOptions{}).Changed() { // open the pull request } } diff --git a/docs/components/316-review-comment.md b/docs/components/316-review-comment.md index 6ed4b9a..c24e7cf 100644 --- a/docs/components/316-review-comment.md +++ b/docs/components/316-review-comment.md @@ -7,15 +7,15 @@ ## 最小示例 ```go -thread := mujica.ReviewThread{File: "main.go", Line: 12, Messages: []mujica.ReviewMessage{ +thread := git.ReviewThread{File: "main.go", Line: 12, Messages: []git.ReviewMessage{ {Author: "Uika", When: time.Now(), Body: "Can this return early?"}, }} draft := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ReviewComment(c, thread, &draft, mujica.ReviewCommentOptions{}) + core.Use(c, core.Settings{}) + r := git.ReviewComment(c, thread, &draft, git.ReviewCommentOptions{}) if text, ok := r.Replied(); ok { - thread.Messages = append(thread.Messages, mujica.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) + thread.Messages = append(thread.Messages, git.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) } if r.Resolved() || r.Reopened() { thread.Resolved = !thread.Resolved diff --git a/docs/components/317-diff-stat.md b/docs/components/317-diff-stat.md index fc4143b..b1e6a4d 100644 --- a/docs/components/317-diff-stat.md +++ b/docs/components/317-diff-stat.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffStat(c, 120, 8, mujica.DiffStatOptions{}) + core.Use(c, core.Settings{}) + git.DiffStat(c, 120, 8, git.DiffStatOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/318-git-status-badge.md b/docs/components/318-git-status-badge.md index c4f7ac1..7621bf7 100644 --- a/docs/components/318-git-status-badge.md +++ b/docs/components/318-git-status-badge.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GitStatusBadge(c, mujica.GitModified, mujica.GitStatusBadgeOptions{ShowLabel: true}) + core.Use(c, core.Settings{}) + git.GitStatusBadge(c, git.GitModified, git.GitStatusBadgeOptions{ShowLabel: true}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/319-command-block.md b/docs/components/319-command-block.md index ee28831..26d038f 100644 --- a/docs/components/319-command-block.md +++ b/docs/components/319-command-block.md @@ -8,10 +8,10 @@ ```go open := true -run := mujica.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} +run := agent.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandBlock(c, run, &open, mujica.CommandBlockOptions{}) + core.Use(c, core.Settings{}) + r := git.CommandBlock(c, run, &open, git.CommandBlockOptions{}) if what, ok := r.Copied(); ok { _ = what // "command" or "output" is on the clipboard } diff --git a/docs/components/320-ansi-text.md b/docs/components/320-ansi-text.md index a5255f7..2852f86 100644 --- a/docs/components/320-ansi-text.md +++ b/docs/components/320-ansi-text.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", mujica.AnsiTextOptions{}) + core.Use(c, core.Settings{}) + git.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", git.AnsiTextOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/321-terminal.md b/docs/components/321-terminal.md index 9c06f2c..00af6dc 100644 --- a/docs/components/321-terminal.md +++ b/docs/components/321-terminal.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var term mujica.TerminalState +var term code.TerminalState term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{ + core.Use(c, core.Settings{}) + code.Terminal(c, &term, code.TerminalOptions{ Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, }).Size(380, 200) diff --git a/docs/components/322-terminal-tabs.md b/docs/components/322-terminal-tabs.md index 47e9d2d..d655f73 100644 --- a/docs/components/322-terminal-tabs.md +++ b/docs/components/322-terminal-tabs.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} +tabs := []code.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} active := "a" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { + core.Use(c, core.Settings{}) + r := code.TerminalTabs(c, &tabs, &active, code.TerminalTabsOptions{}, func(id string) { ui.Text(c, "terminal "+id) }) if r.Closed != "" { diff --git a/docs/components/323-terminal-search.md b/docs/components/323-terminal-search.md index 36c840b..87123ad 100644 --- a/docs/components/323-terminal-search.md +++ b/docs/components/323-terminal-search.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var term mujica.TerminalState +var term code.TerminalState term.Write("build ok\r\nerror: missing seal\r\n") -var search mujica.TerminalSearchState +var search code.TerminalSearchState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(8).Children(func() { - mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) + code.TerminalSearch(c, &term, &search, code.TerminalSearchOptions{}) + code.Terminal(c, &term, code.TerminalOptions{}).Height(160) }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/324-command-history.md b/docs/components/324-command-history.md index 69be25f..fc493e7 100644 --- a/docs/components/324-command-history.md +++ b/docs/components/324-command-history.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} -var state mujica.CommandHistoryState +history := []code.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} +var state code.CommandHistoryState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) + core.Use(c, core.Settings{}) + r := code.CommandHistory(c, history, &state, code.CommandHistoryOptions{}) r.Element.Height(240) if r.Replayed != nil { fmt.Println("run", r.Replayed.Command) diff --git a/docs/components/325-log-viewer.md b/docs/components/325-log-viewer.md index 720a92e..65cde3e 100644 --- a/docs/components/325-log-viewer.md +++ b/docs/components/325-log-viewer.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} -var state mujica.LogViewerState +lines := []code.LogLine{{Level: code.LogInfo, Text: "court opened"}, {Level: code.LogError, Text: "seal missing"}} +var state code.LogViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + code.LogViewer(c, lines, &state, code.LogViewerOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/326-process-list.md b/docs/components/326-process-list.md index d0523e5..91e6761 100644 --- a/docs/components/326-process-list.md +++ b/docs/components/326-process-list.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} -var state mujica.ProcessListState +procs := []code.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} +var state code.ProcessListState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) + core.Use(c, core.Settings{}) + r := code.ProcessList(c, procs, &state, code.ProcessListOptions{}) r.Element.Height(240) if r.End != nil { fmt.Println("kill", r.End.PID) diff --git a/docs/components/327-code-viewer.md b/docs/components/327-code-viewer.md index b91d213..fd18998 100644 --- a/docs/components/327-code-viewer.md +++ b/docs/components/327-code-viewer.md @@ -8,10 +8,10 @@ ```go lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} -var state mujica.CodeViewerState +var state code.CodeViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) + core.Use(c, core.Settings{}) + code.CodeViewer(c, lines, &state, code.CodeViewerOptions{Language: "go"}).Element.Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/328-minimap.md b/docs/components/328-minimap.md index 60a0a9e..5aa30ea 100644 --- a/docs/components/328-minimap.md +++ b/docs/components/328-minimap.md @@ -8,10 +8,10 @@ ```go lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} -view := mujica.MinimapView{Top: 0, Visible: 2} +view := code.MinimapView{Top: 0, Visible: 2} draw := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { + core.Use(c, core.Settings{}) + if code.Minimap(c, lines, &view, code.MinimapOptions{Language: "go"}).Changed() { fmt.Println("scroll editor to", view.Top) } } diff --git a/docs/components/329-find-widget.md b/docs/components/329-find-widget.md index 2617d9b..c0c2d9a 100644 --- a/docs/components/329-find-widget.md +++ b/docs/components/329-find-widget.md @@ -8,10 +8,10 @@ ```go text := "the seal and the seal" -state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} +state := code.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { + core.Use(c, core.Settings{}) + if code.FindWidget(c, &text, &state, code.FindWidgetOptions{}).Replaced > 0 { fmt.Println(text) } } diff --git a/docs/components/330-search-panel.md b/docs/components/330-search-panel.md index c9dc6e9..9746f66 100644 --- a/docs/components/330-search-panel.md +++ b/docs/components/330-search-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} -state := mujica.SearchPanelState{Query: "seal"} +results := []code.SearchFileResult{{Path: "court/seal.go", Matches: []code.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} +state := code.SearchPanelState{Query: "seal"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.SearchPanel(c, results, &state, code.SearchPanelOptions{}) r.Element.Height(280) if r.Opened != nil { fmt.Println("open", r.Opened.Path, r.Opened.Line) diff --git a/docs/components/331-problems-panel.md b/docs/components/331-problems-panel.md index cea7623..f7986cd 100644 --- a/docs/components/331-problems-panel.md +++ b/docs/components/331-problems-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} -var state mujica.ProblemsPanelState +problems := []code.Problem{{Severity: code.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} +var state code.ProblemsPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.ProblemsPanel(c, problems, &state, code.ProblemsPanelOptions{}) r.Element.Height(240) if r.Opened != nil { fmt.Println("go to", r.Opened.File, r.Opened.Line) diff --git a/docs/components/332-output-panel.md b/docs/components/332-output-panel.md index 5349439..18d01a9 100644 --- a/docs/components/332-output-panel.md +++ b/docs/components/332-output-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} -var state mujica.OutputPanelState +channels := []code.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} +var state code.OutputPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.OutputPanel(c, channels, &state, code.OutputPanelOptions{}) r.Element.Height(240) if r.Cleared != "" { channels[state.Channel].Lines = nil diff --git a/docs/components/333-symbol-outline.md b/docs/components/333-symbol-outline.md index f04dbfa..ae0491a 100644 --- a/docs/components/333-symbol-outline.md +++ b/docs/components/333-symbol-outline.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ - {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, +symbols := []code.Symbol{{Name: "Court", Kind: code.SymbolType, Line: 3, EndLine: 40, Children: []code.Symbol{ + {Name: "Seal", Kind: code.SymbolMethod, Line: 10, EndLine: 20}, }}} -var state mujica.SymbolOutlineState +var state code.SymbolOutlineState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) + core.Use(c, core.Settings{}) + r := code.SymbolOutline(c, symbols, &state, code.SymbolOutlineOptions{CursorLine: 12}) r.Element.Height(240) if r.Jumped != nil { fmt.Println("go to line", r.Jumped.Line) diff --git a/docs/components/334-completion-menu.md b/docs/components/334-completion-menu.md index cbc27a1..7b6aa0e 100644 --- a/docs/components/334-completion-menu.md +++ b/docs/components/334-completion-menu.md @@ -9,12 +9,12 @@ ```go text := "se" open := true -items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} -var state mujica.CompletionMenuState +items := []code.Completion{{Label: "Seal", Kind: code.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: code.SymbolFunction}} +var state code.CompletionMenuState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) - if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { + core.Use(c, core.Settings{}) + in := input.TextInput(c, &text, input.TextInputOptions{Label: "Code"}) + if r := code.CompletionMenu(c, in.Input, &open, items, &state, code.CompletionMenuOptions{}); r.Accepted != nil { text = r.Accepted.Label } } diff --git a/docs/components/335-debug-toolbar.md b/docs/components/335-debug-toolbar.md index 7560c0b..c2bd361 100644 --- a/docs/components/335-debug-toolbar.md +++ b/docs/components/335-debug-toolbar.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -state := mujica.DebugPaused +state := code.DebugPaused view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { - case mujica.DebugContinue: - state = mujica.DebugRunning - case mujica.DebugStop: - state = mujica.DebugStopped + core.Use(c, core.Settings{}) + switch code.DebugToolbar(c, state, code.DebugToolbarOptions{}).Action { + case code.DebugContinue: + state = code.DebugRunning + case code.DebugStop: + state = code.DebugStopped } } ui.Render(view, 400, 300, 1) diff --git a/docs/components/336-breakpoint-list.md b/docs/components/336-breakpoint-list.md index 488efca..eb86723 100644 --- a/docs/components/336-breakpoint-list.md +++ b/docs/components/336-breakpoint-list.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} +bps := []code.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) + core.Use(c, core.Settings{}) + r := code.BreakpointList(c, &bps, code.BreakpointListOptions{}) r.Element.Height(200) if r.Toggled != "" || r.Edited != "" { fmt.Println("sync breakpoints with the debugger") diff --git a/docs/components/337-call-stack.md b/docs/components/337-call-stack.md index a5a2b69..97a65e1 100644 --- a/docs/components/337-call-stack.md +++ b/docs/components/337-call-stack.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ +threads := []code.DebugThread{{ID: "t1", Name: "main", Frames: []code.StackFrame{ {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, }}} -state := mujica.CallStackState{Current: "f1"} +state := code.CallStackState{Current: "f1"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) + core.Use(c, core.Settings{}) + r := code.CallStack(c, threads, &state, code.CallStackOptions{}) r.Element.Height(200) if r.Changed() { fmt.Println("show frame", state.Current) diff --git a/docs/components/338-variables-panel.md b/docs/components/338-variables-panel.md index c68a88c..8dd406a 100644 --- a/docs/components/338-variables-panel.md +++ b/docs/components/338-variables-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} -var state mujica.VariablesPanelState +vars := []code.Variable{{Name: "decree", Value: "{…}", Children: []code.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} +var state code.VariablesPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.VariablesPanel(c, vars, &state, code.VariablesPanelOptions{}) r.Element.Height(200) if r.Edited != nil { fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) diff --git a/docs/components/339-hex-viewer.md b/docs/components/339-hex-viewer.md index 3a4a448..4efe001 100644 --- a/docs/components/339-hex-viewer.md +++ b/docs/components/339-hex-viewer.md @@ -8,10 +8,10 @@ ```go data := []byte("MUJICA court ledger\x00\x01") -var state mujica.HexViewerState +var state code.HexViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) + core.Use(c, core.Settings{}) + code.HexViewer(c, data, &state, code.HexViewerOptions{}).Element.Height(160) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/340-flamegraph.md b/docs/components/340-flamegraph.md index 21f12c8..d0e12b0 100644 --- a/docs/components/340-flamegraph.md +++ b/docs/components/340-flamegraph.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ +root := &code.FlameNode{Name: "main", Value: 100, Children: []code.FlameNode{ {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, }} -var state mujica.FlamegraphState +var state code.FlamegraphState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) + core.Use(c, core.Settings{}) + code.Flamegraph(c, root, &state, code.FlamegraphOptions{Unit: "ms"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/341-image-cropper.md b/docs/components/341-image-cropper.md index bef9a87..6aa8afe 100644 --- a/docs/components/341-image-cropper.md +++ b/docs/components/341-image-cropper.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -crop := mujica.ImageCrop{} // empty: starts fitted to the image +crop := media.ImageCrop{} // empty: starts fitted to the image view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageCropper(c, examplePhoto, &crop, mujica.ImageCropperOptions{Aspect: 1}).Changed() { + core.Use(c, core.Settings{}) + if media.ImageCropper(c, examplePhoto, &crop, media.ImageCropperOptions{Aspect: 1}).Changed() { _ = crop.Rect // in pixels of the photo turned by crop.Rotation } } diff --git a/docs/components/342-image-annotator.md b/docs/components/342-image-annotator.md index c947050..7df4185 100644 --- a/docs/components/342-image-annotator.md +++ b/docs/components/342-image-annotator.md @@ -8,11 +8,11 @@ ```go photo := ui.NewBitmap(examplePhoto) -var notes []mujica.Annotation +var notes []media.Annotation view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageAnnotator(c, photo, ¬es, mujica.ImageAnnotatorOptions{}).Changed() { - data, _ := mujica.ExportAnnotations(notes) + core.Use(c, core.Settings{}) + if media.ImageAnnotator(c, photo, ¬es, media.ImageAnnotatorOptions{}).Changed() { + data, _ := media.ExportAnnotations(notes) _ = data // save the JSON } } diff --git a/docs/components/343-image-thumbnail.md b/docs/components/343-image-thumbnail.md index eaf0c67..841b586 100644 --- a/docs/components/343-image-thumbnail.md +++ b/docs/components/343-image-thumbnail.md @@ -10,8 +10,8 @@ photo := ui.NewBitmap(examplePhoto) selected := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageThumbnail(c, photo, mujica.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { + core.Use(c, core.Settings{}) + if media.ImageThumbnail(c, photo, media.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { selected = !selected } } diff --git a/docs/components/344-lightbox.md b/docs/components/344-lightbox.md index 5c5103a..cbb7bce 100644 --- a/docs/components/344-lightbox.md +++ b/docs/components/344-lightbox.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -images := []mujica.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} +images := []data.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} open, index := false, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "View photos", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "View photos", input.ButtonOptions{}).Clicked() { open = true } - mujica.Lightbox(c, &open, &index, images) + media.Lightbox(c, &open, &index, images) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/345-video-player.md b/docs/components/345-video-player.md index 7a9beca..5bb3938 100644 --- a/docs/components/345-video-player.md +++ b/docs/components/345-video-player.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -st := mujica.VideoState{Duration: 4 * time.Minute, Rate: 1} +st := media.VideoState{Duration: 4 * time.Minute, Rate: 1} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{ + core.Use(c, core.Settings{}) + media.VideoPlayer(c, st, media.VideoPlayerOptions{ OnPlay: func() { st.Playing = true }, // start your decoder OnPause: func() { st.Playing = false }, // pause it OnSeek: func(d time.Duration) { st.Position = d }, diff --git a/docs/components/346-video-scrubber.md b/docs/components/346-video-scrubber.md index 6ada0d3..33d5cbc 100644 --- a/docs/components/346-video-scrubber.md +++ b/docs/components/346-video-scrubber.md @@ -9,10 +9,10 @@ ```go pos := 30 * time.Second view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoScrubber(c, &pos, mujica.VideoScrubberOptions{ + core.Use(c, core.Settings{}) + media.VideoScrubber(c, &pos, media.VideoScrubberOptions{ Duration: 2 * time.Minute, - Buffered: []mujica.MediaRange{{Start: 0, End: time.Minute}}, + Buffered: []media.MediaRange{{Start: 0, End: time.Minute}}, }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/347-video-thumbnail-strip.md b/docs/components/347-video-thumbnail-strip.md index e36b6ef..c844a8a 100644 --- a/docs/components/347-video-thumbnail-strip.md +++ b/docs/components/347-video-thumbnail-strip.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -frames := []mujica.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} +frames := []media.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} current := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.VideoThumbnailStrip(c, ¤t, frames, mujica.VideoThumbnailStripOptions{}).Changed() { + core.Use(c, core.Settings{}) + if media.VideoThumbnailStrip(c, ¤t, frames, media.VideoThumbnailStripOptions{}).Changed() { _ = frames[current].At // seek the player there } } diff --git a/docs/components/348-audio-player.md b/docs/components/348-audio-player.md index 426e71d..856fdff 100644 --- a/docs/components/348-audio-player.md +++ b/docs/components/348-audio-player.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -st := mujica.AudioState{Title: "Overture", Duration: 3 * time.Minute} +st := media.AudioState{Title: "Overture", Duration: 3 * time.Minute} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioPlayer(c, st, mujica.AudioPlayerOptions{ + core.Use(c, core.Settings{}) + media.AudioPlayer(c, st, media.AudioPlayerOptions{ OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, OnSeek: func(d time.Duration) { st.Position = d }, diff --git a/docs/components/349-audio-waveform.md b/docs/components/349-audio-waveform.md index 674f8ad..1ee9f71 100644 --- a/docs/components/349-audio-waveform.md +++ b/docs/components/349-audio-waveform.md @@ -8,10 +8,10 @@ ```go samples := make([]float32, 48000) // from your decoder, -1 to 1 -pos, sel := time.Duration(0), mujica.MediaRange{} +pos, sel := time.Duration(0), media.MediaRange{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioWaveform(c, samples, mujica.AudioWaveformOptions{ + core.Use(c, core.Settings{}) + media.AudioWaveform(c, samples, media.AudioWaveformOptions{ Duration: time.Second, Position: pos, Selection: &sel, OnSeek: func(d time.Duration) { pos = d }, }) diff --git a/docs/components/350-audio-spectrum.md b/docs/components/350-audio-spectrum.md index b7e2994..2a88039 100644 --- a/docs/components/350-audio-spectrum.md +++ b/docs/components/350-audio-spectrum.md @@ -9,8 +9,8 @@ ```go bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate() view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{}) + core.Use(c, core.Settings{}) + media.AudioSpectrum(c, bands, media.AudioSpectrumOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/351-volume-control.md b/docs/components/351-volume-control.md index ef6eb3d..7bf1f01 100644 --- a/docs/components/351-volume-control.md +++ b/docs/components/351-volume-control.md @@ -9,8 +9,8 @@ ```go volume, muted := 0.8, false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeControl(c, &volume, &muted, mujica.VolumeControlOptions{}) + core.Use(c, core.Settings{}) + media.VolumeControl(c, &volume, &muted, media.VolumeControlOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/352-playback-speed-control.md b/docs/components/352-playback-speed-control.md index 9a7e829..3f3391a 100644 --- a/docs/components/352-playback-speed-control.md +++ b/docs/components/352-playback-speed-control.md @@ -9,8 +9,8 @@ ```go rate := 1.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PlaybackSpeedControl(c, &rate, mujica.PlaybackSpeedControlOptions{}) + core.Use(c, core.Settings{}) + media.PlaybackSpeedControl(c, &rate, media.PlaybackSpeedControlOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/353-media-controls.md b/docs/components/353-media-controls.md index 3d35038..70e0769 100644 --- a/docs/components/353-media-controls.md +++ b/docs/components/353-media-controls.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -st := mujica.MediaControlsState{} +st := media.MediaControlsState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MediaControls(c, st, mujica.MediaControlsOptions{ + core.Use(c, core.Settings{}) + media.MediaControls(c, st, media.MediaControlsOptions{ OnPrevious: func() {}, OnNext: func() {}, OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, - OnLoop: func(m mujica.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, + OnLoop: func(m media.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/354-playlist.md b/docs/components/354-playlist.md index 85bac64..1a70298 100644 --- a/docs/components/354-playlist.md +++ b/docs/components/354-playlist.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -items := []mujica.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} +items := []media.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} current := "a" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.Playlist(c, &items, mujica.PlaylistOptions{Current: current}); r.Played != "" { + core.Use(c, core.Settings{}) + if r := media.Playlist(c, &items, media.PlaylistOptions{Current: current}); r.Played != "" { current = r.Played } } diff --git a/docs/components/355-subtitle-editor.md b/docs/components/355-subtitle-editor.md index eb921bc..58172a2 100644 --- a/docs/components/355-subtitle-editor.md +++ b/docs/components/355-subtitle-editor.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -cues := []mujica.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} +cues := []media.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubtitleEditor(c, &cues, mujica.SubtitleEditorOptions{}) - if len(mujica.CheckSubtitles(cues)) > 0 { + core.Use(c, core.Settings{}) + media.SubtitleEditor(c, &cues, media.SubtitleEditorOptions{}) + if len(media.CheckSubtitles(cues)) > 0 { _ = cues // block saving until the issues are fixed } } diff --git a/docs/components/356-screen-recorder-controls.md b/docs/components/356-screen-recorder-controls.md index d0dcb96..f1040e1 100644 --- a/docs/components/356-screen-recorder-controls.md +++ b/docs/components/356-screen-recorder-controls.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -st := mujica.RecorderState{} +st := media.RecorderState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenRecorderControls(c, st, mujica.ScreenRecorderControlsOptions{ - OnStart: func() { st.Status = mujica.RecorderRecording }, - OnPause: func() { st.Status = mujica.RecorderPaused }, - OnResume: func() { st.Status = mujica.RecorderRecording }, - OnStop: func() { st = mujica.RecorderState{} }, + core.Use(c, core.Settings{}) + media.ScreenRecorderControls(c, st, media.ScreenRecorderControlsOptions{ + OnStart: func() { st.Status = media.RecorderRecording }, + OnPause: func() { st.Status = media.RecorderPaused }, + OnResume: func() { st.Status = media.RecorderRecording }, + OnStop: func() { st = media.RecorderState{} }, }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/357-camera-preview.md b/docs/components/357-camera-preview.md index e7d4aa5..035338f 100644 --- a/docs/components/357-camera-preview.md +++ b/docs/components/357-camera-preview.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -frame := mujica.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame +frame := media.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame mirror := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CameraPreview(c, frame, &mirror, mujica.CameraPreviewOptions{}) + core.Use(c, core.Settings{}) + media.CameraPreview(c, frame, &mirror, media.CameraPreviewOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/358-mic-level-meter.md b/docs/components/358-mic-level-meter.md index 29152ea..16f9d7f 100644 --- a/docs/components/358-mic-level-meter.md +++ b/docs/components/358-mic-level-meter.md @@ -9,8 +9,8 @@ ```go level := float32(0.4) // the input's peak, 0 to 1 of full scale view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) + core.Use(c, core.Settings{}) + media.MicLevelMeter(c, level, media.MicLevelMeterOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/359-device-selector.md b/docs/components/359-device-selector.md index 06f202b..e02f6e7 100644 --- a/docs/components/359-device-selector.md +++ b/docs/components/359-device-selector.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -devices := mujica.MediaDevices{ - Microphones: []mujica.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, - Speakers: []mujica.MediaDevice{{ID: "out-1", Name: "Headphones"}}, +devices := media.MediaDevices{ + Microphones: []media.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, + Speakers: []media.MediaDevice{{ID: "out-1", Name: "Headphones"}}, } -sel := mujica.DeviceSelection{Microphone: "mic-1"} +sel := media.DeviceSelection{Microphone: "mic-1"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DeviceSelector(c, &sel, devices, mujica.DeviceSelectorOptions{}) + core.Use(c, core.Settings{}) + media.DeviceSelector(c, &sel, devices, media.DeviceSelectorOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/360-image-compare.md b/docs/components/360-image-compare.md index cc03a72..3f95081 100644 --- a/docs/components/360-image-compare.md +++ b/docs/components/360-image-compare.md @@ -10,8 +10,8 @@ before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto) split := 0.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ImageCompare(c, before, after, &split, mujica.ImageCompareOptions{}) + core.Use(c, core.Settings{}) + media.ImageCompare(c, before, after, &split, media.ImageCompareOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/361-kanban-board.md b/docs/components/361-kanban-board.md index ab3fd4e..2a1cb93 100644 --- a/docs/components/361-kanban-board.md +++ b/docs/components/361-kanban-board.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.KanbanBoard(c, &tasks, mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) + core.Use(c, core.Settings{}) + r := project.KanbanBoard(c, &tasks, project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) if move, ok := r.Moved(); ok { log.Println("move", move.ID, move.To, move.Index) } diff --git a/docs/components/362-task-list.md b/docs/components/362-task-list.md index b707d4f..3ff63ee 100644 --- a/docs/components/362-task-list.md +++ b/docs/components/362-task-list.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} -filter := mujica.TaskShowAll +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} +filter := project.TaskShowAll view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskList(c, tasks, &filter, mujica.TaskListOptions{GroupBy: mujica.TaskGroupStatus, Statuses: statuses}) + core.Use(c, core.Settings{}) + r := project.TaskList(c, tasks, &filter, project.TaskListOptions{GroupBy: project.TaskGroupStatus, Statuses: statuses}) if id, ok := r.Changed(); ok { log.Println("edit", id) } diff --git a/docs/components/363-task-item.md b/docs/components/363-task-item.md index 3c7b7ba..c4397fa 100644 --- a/docs/components/363-task-item.md +++ b/docs/components/363-task-item.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} +task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskItem(c, &task, mujica.TaskItemOptions{}) + core.Use(c, core.Settings{}) + r := project.TaskItem(c, &task, project.TaskItemOptions{}) if id, ok := r.Changed(); ok { log.Println("edit", id) } diff --git a/docs/components/364-task-detail-panel.md b/docs/components/364-task-detail-panel.md index 0736810..598ad06 100644 --- a/docs/components/364-task-detail-panel.md +++ b/docs/components/364-task-detail-panel.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskDetailPanel(c, &task, mujica.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) + core.Use(c, core.Settings{}) + r := project.TaskDetailPanel(c, &task, project.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) if r.Changed() { log.Println("task edited", task.ID) } diff --git a/docs/components/365-subtask-list.md b/docs/components/365-subtask-list.md index c6e656c..7536775 100644 --- a/docs/components/365-subtask-list.md +++ b/docs/components/365-subtask-list.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -subtasks := []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} +subtasks := []project.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SubtaskList(c, &subtasks, mujica.SubtaskListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.SubtaskList(c, &subtasks, project.SubtaskListOptions{}).Changed() { log.Println("subtasks changed", len(subtasks)) } } diff --git a/docs/components/368-sprint-board.md b/docs/components/368-sprint-board.md index b3ca3aa..486a717 100644 --- a/docs/components/368-sprint-board.md +++ b/docs/components/368-sprint-board.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} -sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}} +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} +sprint := project.Sprint{Name: "Release sprint", Goal: "Ship", Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SprintBoard(c, sprint, &tasks, mujica.SprintBoardOptions{Board: mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) + core.Use(c, core.Settings{}) + r := project.SprintBoard(c, sprint, &tasks, project.SprintBoardOptions{Board: project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) if move, ok := r.Moved(); ok { log.Println("move", move.ID, move.To) } diff --git a/docs/components/369-burndown-chart.md b/docs/components/369-burndown-chart.md index 241a4dd..7fd01da 100644 --- a/docs/components/369-burndown-chart.md +++ b/docs/components/369-burndown-chart.md @@ -8,10 +8,10 @@ ```go remaining := []float64{40, 32, 25, 18} -opts := mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} +opts := project.BurndownChartOptions{Frame: chart.ChartFrame{Label: "Release burndown"}, Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BurndownChart(c, remaining, opts) + core.Use(c, core.Settings{}) + r := project.BurndownChart(c, remaining, opts) if r.Changed() { log.Println("point", r.Point) } diff --git a/docs/components/370-priority-indicator.md b/docs/components/370-priority-indicator.md index 76fe81e..3973788 100644 --- a/docs/components/370-priority-indicator.md +++ b/docs/components/370-priority-indicator.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -priority := mujica.PriorityHigh +priority := project.PriorityHigh view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriorityIndicator(c, priority, mujica.PriorityIndicatorOptions{}) + core.Use(c, core.Settings{}) + project.PriorityIndicator(c, priority, project.PriorityIndicatorOptions{}) } ui.Render(view, 1000, 700, 1) ``` diff --git a/docs/components/371-status-select.md b/docs/components/371-status-select.md index e8b5603..592099d 100644 --- a/docs/components/371-status-select.md +++ b/docs/components/371-status-select.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} value := "todo" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSelect(c, &value, statuses, mujica.StatusSelectOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.StatusSelect(c, &value, statuses, project.StatusSelectOptions{}).Changed() { log.Println("status", value) } } diff --git a/docs/components/372-assignee-picker.md b/docs/components/372-assignee-picker.md index 3d5c42e..0957e79 100644 --- a/docs/components/372-assignee-picker.md +++ b/docs/components/372-assignee-picker.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -members := []mujica.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} +members := []project.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} ids := []string{"ann"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AssigneePicker(c, &ids, members, mujica.AssigneePickerOptions{Max: 2}).Changed() { + core.Use(c, core.Settings{}) + if project.AssigneePicker(c, &ids, members, project.AssigneePickerOptions{Max: 2}).Changed() { log.Println("assignees", ids) } } diff --git a/docs/components/373-due-date-picker.md b/docs/components/373-due-date-picker.md index 10e8c4b..727aa22 100644 --- a/docs/components/373-due-date-picker.md +++ b/docs/components/373-due-date-picker.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var due mujica.Date +var due core.Date view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DueDatePicker(c, &due, mujica.DueDatePickerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.DueDatePicker(c, &due, project.DueDatePickerOptions{}).Changed() { log.Println("due", due) } } diff --git a/docs/components/374-label-manager.md b/docs/components/374-label-manager.md index 5a188f0..1c685e6 100644 --- a/docs/components/374-label-manager.md +++ b/docs/components/374-label-manager.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -labels := []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}} +labels := []project.TaskLabel{{ID: "ui", Name: "UI", Color: datetime.EventInfo}} selected := []string{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LabelManager(c, &labels, &selected, mujica.LabelManagerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.LabelManager(c, &labels, &selected, project.LabelManagerOptions{}).Changed() { log.Println("labels changed", len(labels), selected) } } diff --git a/docs/components/375-issue-card.md b/docs/components/375-issue-card.md index ffa2eed..580e211 100644 --- a/docs/components/375-issue-card.md +++ b/docs/components/375-issue-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} +statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueCard(c, task, mujica.IssueCardOptions{Statuses: statuses}).Clicked() { + core.Use(c, core.Settings{}) + if project.IssueCard(c, task, project.IssueCardOptions{Statuses: statuses}).Clicked() { log.Println("open", task.ID) } } diff --git a/docs/components/376-issue-id-badge.md b/docs/components/376-issue-id-badge.md index b966df7..5bf5ce9 100644 --- a/docs/components/376-issue-id-badge.md +++ b/docs/components/376-issue-id-badge.md @@ -9,8 +9,8 @@ ```go id := "ENG-123" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueIdBadge(c, id, mujica.IssueIdBadgeOptions{Copyable: true}).Changed() { + core.Use(c, core.Settings{}) + if project.IssueIdBadge(c, id, project.IssueIdBadgeOptions{Copyable: true}).Changed() { log.Println("copied", id) } } diff --git a/docs/components/377-milestone-progress.md b/docs/components/377-milestone-progress.md index 1f454c9..3903fe4 100644 --- a/docs/components/377-milestone-progress.md +++ b/docs/components/377-milestone-progress.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -milestone := mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} +milestone := project.Milestone{Title: "Release", Due: core.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MilestoneProgress(c, milestone, mujica.MilestoneProgressOptions{}) + core.Use(c, core.Settings{}) + project.MilestoneProgress(c, milestone, project.MilestoneProgressOptions{}) } ui.Render(view, 1000, 700, 1) ``` diff --git a/docs/components/378-workload-view.md b/docs/components/378-workload-view.md index 487f1bd..c9ce5d4 100644 --- a/docs/components/378-workload-view.md +++ b/docs/components/378-workload-view.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -loads := []mujica.Workload{{Member: mujica.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} +loads := []project.Workload{{Member: project.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkloadView(c, loads, mujica.WorkloadViewOptions{}) + core.Use(c, core.Settings{}) + project.WorkloadView(c, loads, project.WorkloadViewOptions{}) } ui.Render(view, 1000, 700, 1) ``` diff --git a/docs/components/379-time-tracker.md b/docs/components/379-time-tracker.md index ccc53b9..8c22c0d 100644 --- a/docs/components/379-time-tracker.md +++ b/docs/components/379-time-tracker.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var state mujica.TimeTrackerState +var state project.TimeTrackerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimeTracker(c, &state, mujica.TimeTrackerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.TimeTracker(c, &state, project.TimeTrackerOptions{}).Changed() { log.Println("tracked", state.Total(c.Now())) } } diff --git a/docs/components/380-pomodoro-timer.md b/docs/components/380-pomodoro-timer.md index 5ab2793..0b680fe 100644 --- a/docs/components/380-pomodoro-timer.md +++ b/docs/components/380-pomodoro-timer.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var state mujica.PomodoroState +var state project.PomodoroState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PomodoroTimer(c, &state, mujica.PomodoroTimerOptions{Focus: 25 * time.Minute}) + core.Use(c, core.Settings{}) + r := project.PomodoroTimer(c, &state, project.PomodoroTimerOptions{Focus: 25 * time.Minute}) if r.Changed() { log.Println("timer", state.Phase(), state.Running()) } diff --git a/docs/components/381-file-explorer.md b/docs/components/381-file-explorer.md index bc5ccf8..637dee0 100644 --- a/docs/components/381-file-explorer.md +++ b/docs/components/381-file-explorer.md @@ -7,17 +7,17 @@ ## 最小示例 ```go -var state mujica.FileExplorerState -opts := mujica.FileExplorerOptions{ +var state files.FileExplorerState +opts := files.FileExplorerOptions{ Roots: []string{"/court"}, Folders: func(string) []string { return nil }, - Entries: func(string) []mujica.FileEntry { - return []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} + Entries: func(string) []files.FileEntry { + return []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} }, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileExplorer(c, &state, opts).Element.Height(240) + core.Use(c, core.Settings{}) + files.FileExplorer(c, &state, opts).Element.Height(240) } ui.Render(view, 600, 300, 1) ``` diff --git a/docs/components/382-file-grid.md b/docs/components/382-file-grid.md index f0af769..d7aeea3 100644 --- a/docs/components/382-file-grid.md +++ b/docs/components/382-file-grid.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -entries := []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} -var state mujica.FileGridState +entries := []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} +var state files.FileGridState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileGrid(c, entries, &state, mujica.FileGridOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.FileGrid(c, entries, &state, files.FileGridOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/383-file-icon.md b/docs/components/383-file-icon.md index 155a18f..1817ed0 100644 --- a/docs/components/383-file-icon.md +++ b/docs/components/383-file-icon.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileIcon(c, "seal.go", mujica.FileIconOptions{Size: 32}) + core.Use(c, core.Settings{}) + files.FileIcon(c, "seal.go", files.FileIconOptions{Size: 32}) } ui.Render(view, 100, 80, 1) ``` diff --git a/docs/components/384-file-preview.md b/docs/components/384-file-preview.md index 9b8f407..04ad362 100644 --- a/docs/components/384-file-preview.md +++ b/docs/components/384-file-preview.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -file := mujica.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} +file := files.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePreview(c, file, mujica.FilePreviewOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.FilePreview(c, file, files.FilePreviewOptions{}).Element.Height(240) } ui.Render(view, 500, 300, 1) ``` diff --git a/docs/components/385-path-bar.md b/docs/components/385-path-bar.md index 173287d..a96ca87 100644 --- a/docs/components/385-path-bar.md +++ b/docs/components/385-path-bar.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PathBar(c, "/court/chronicles/seals", mujica.PathBarOptions{}) + core.Use(c, core.Settings{}) + files.PathBar(c, "/court/chronicles/seals", files.PathBarOptions{}) } ui.Render(view, 400, 100, 1) ``` diff --git a/docs/components/386-rename-inline.md b/docs/components/386-rename-inline.md index b02d9c3..4d4f90c 100644 --- a/docs/components/386-rename-inline.md +++ b/docs/components/386-rename-inline.md @@ -9,8 +9,8 @@ ```go name, editing := "seal.txt", true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RenameInline(c, &name, &editing, mujica.RenameInlineOptions{}) + core.Use(c, core.Settings{}) + files.RenameInline(c, &name, &editing, files.RenameInlineOptions{}) } ui.Render(view, 400, 100, 1) ``` diff --git a/docs/components/387-file-operation-progress.md b/docs/components/387-file-operation-progress.md index 6321e7f..06420c9 100644 --- a/docs/components/387-file-operation-progress.md +++ b/docs/components/387-file-operation-progress.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -op := mujica.FileOperation{Kind: mujica.FileCopy, Done: 2, Total: 10, Current: "seal.go"} +op := files.FileOperation{Kind: files.FileCopy, Done: 2, Total: 10, Current: "seal.go"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{}) + core.Use(c, core.Settings{}) + files.FileOperationProgress(c, op, files.FileOperationOptions{}) } ui.Render(view, 500, 240, 1) ``` diff --git a/docs/components/388-transfer-queue.md b/docs/components/388-transfer-queue.md index 8f92405..99472d3 100644 --- a/docs/components/388-transfer-queue.md +++ b/docs/components/388-transfer-queue.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -items := []mujica.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 25, Total: 100}} -var state mujica.TransferQueueState +items := []files.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: files.TransferActive, Done: 25, Total: 100}} +var state files.TransferQueueState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TransferQueue(c, items, &state, mujica.TransferQueueOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.TransferQueue(c, items, &state, files.TransferQueueOptions{}).Element.Height(240) } ui.Render(view, 500, 300, 1) ``` diff --git a/docs/components/389-storage-usage.md b/docs/components/389-storage-usage.md index 75eae34..c40bace 100644 --- a/docs/components/389-storage-usage.md +++ b/docs/components/389-storage-usage.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -segments := []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} +segments := []files.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.StorageUsage(c, segments, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) + core.Use(c, core.Settings{}) + files.StorageUsage(c, segments, files.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) } ui.Render(view, 500, 240, 1) ``` diff --git a/docs/components/390-recent-files.md b/docs/components/390-recent-files.md index 691f8a3..fc2fefe 100644 --- a/docs/components/390-recent-files.md +++ b/docs/components/390-recent-files.md @@ -8,11 +8,11 @@ ```go now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) -files := []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}} -var state mujica.RecentFilesState +recent := []files.RecentFile{{Path: "/court/seal.go", Opened: now}} +var state files.RecentFilesState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecentFiles(c, files, &state, mujica.RecentFilesOptions{Now: now}).Element.Height(240) + core.Use(c, core.Settings{}) + files.RecentFiles(c, recent, &state, files.RecentFilesOptions{Now: now}).Element.Height(240) } ui.Render(view, 500, 300, 1) ``` diff --git a/docs/components/391-archive-viewer.md b/docs/components/391-archive-viewer.md index 0b11285..514d398 100644 --- a/docs/components/391-archive-viewer.md +++ b/docs/components/391-archive-viewer.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -entries := []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} -var state mujica.ArchiveViewerState +entries := []files.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} +var state files.ArchiveViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ArchiveViewer(c, entries, &state, mujica.ArchiveViewerOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.ArchiveViewer(c, entries, &state, files.ArchiveViewerOptions{}).Element.Height(240) } ui.Render(view, 600, 300, 1) ``` diff --git a/docs/components/392-presence-avatars.md b/docs/components/392-presence-avatars.md index 99a6fb1..08e2573 100644 --- a/docs/components/392-presence-avatars.md +++ b/docs/components/392-presence-avatars.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -people := []mujica.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} +people := []files.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{}) + core.Use(c, core.Settings{}) + files.PresenceAvatars(c, people, files.PresenceAvatarsOptions{}) } ui.Render(view, 300, 100, 1) ``` diff --git a/docs/components/394-reactions.md b/docs/components/394-reactions.md index 333e190..2ff1577 100644 --- a/docs/components/394-reactions.md +++ b/docs/components/394-reactions.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -items := []mujica.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} +items := []files.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Reactions(c, items, mujica.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) + core.Use(c, core.Settings{}) + files.Reactions(c, items, files.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) } ui.Render(view, 400, 100, 1) ``` diff --git a/docs/components/395-share-dialog.md b/docs/components/395-share-dialog.md index 606c262..6e8d0b4 100644 --- a/docs/components/395-share-dialog.md +++ b/docs/components/395-share-dialog.md @@ -8,13 +8,13 @@ ```go open := false -members := []mujica.ShareMember{{ID: "ada", Name: "Ada", Permission: mujica.PermissionEdit, Owner: true}} +members := []files.ShareMember{{ID: "ada", Name: "Ada", Permission: files.PermissionEdit, Owner: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Share", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Share", input.ButtonOptions{}).Clicked() { open = true } - mujica.ShareDialog(c, &open, &members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) + files.ShareDialog(c, &open, &members, files.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) } ui.Render(view, 600, 400, 1) ``` diff --git a/docs/components/396-permission-select.md b/docs/components/396-permission-select.md index 52daacf..249b23b 100644 --- a/docs/components/396-permission-select.md +++ b/docs/components/396-permission-select.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -value := mujica.PermissionView +value := files.PermissionView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PermissionSelect(c, &value, mujica.PermissionSelectOptions{}) + core.Use(c, core.Settings{}) + files.PermissionSelect(c, &value, files.PermissionSelectOptions{}) } ui.Render(view, 300, 100, 1) ``` diff --git a/docs/components/399-live-indicator.md b/docs/components/399-live-indicator.md index 0a368b9..153ccf7 100644 --- a/docs/components/399-live-indicator.md +++ b/docs/components/399-live-indicator.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LiveIndicator(c, mujica.LiveOffline, mujica.LiveIndicatorOptions{}) + core.Use(c, core.Settings{}) + files.LiveIndicator(c, files.LiveOffline, files.LiveIndicatorOptions{}) } ui.Render(view, 500, 100, 1) ``` diff --git a/docs/components/400-remote-cursor.md b/docs/components/400-remote-cursor.md index 131b437..cc31a75 100644 --- a/docs/components/400-remote-cursor.md +++ b/docs/components/400-remote-cursor.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} +carets := []files.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Size(300, 100).Children(func() { - mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{ + files.RemoteCursor(c, carets, files.RemoteCursorOptions{ Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} }, }) }) diff --git a/docs/components/401-channel-list.md b/docs/components/401-channel-list.md index c19fae9..c86fbdb 100644 --- a/docs/components/401-channel-list.md +++ b/docs/components/401-channel-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -groups := []mujica.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []mujica.Channel{ +groups := []messaging.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []messaging.Channel{ {ID: "general", Name: "general", Unread: 3}, {ID: "rehearsal", Name: "rehearsal", Pinned: true}, }}} selected := "general" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChannelList(c, &selected, groups, mujica.ChannelListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.ChannelList(c, &selected, groups, messaging.ChannelListOptions{}).Changed() { _ = selected // show that channel } } diff --git a/docs/components/402-chat-message.md b/docs/components/402-chat-message.md index 6514ec7..9d19e08 100644 --- a/docs/components/402-chat-message.md +++ b/docs/components/402-chat-message.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -prev := mujica.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} -m := mujica.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} +prev := messaging.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} +m := messaging.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChatMessage(c, prev, mujica.ChatMessageOptions{}) - mujica.ChatMessage(c, m, mujica.ChatMessageOptions{ - Continued: mujica.ChatContinues(prev, m), - Reactions: func() { mujica.Tag(c, "👍 2", mujica.TagOptions{}) }, + core.Use(c, core.Settings{}) + messaging.ChatMessage(c, prev, messaging.ChatMessageOptions{}) + messaging.ChatMessage(c, m, messaging.ChatMessageOptions{ + Continued: messaging.ChatContinues(prev, m), + Reactions: func() { display.Tag(c, "👍 2", display.TagOptions{}) }, }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/403-unread-divider.md b/docs/components/403-unread-divider.md index acf022b..815d71d 100644 --- a/docs/components/403-unread-divider.md +++ b/docs/components/403-unread-divider.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UnreadDivider(c, 3) + core.Use(c, core.Settings{}) + messaging.UnreadDivider(c, 3) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/404-read-receipt.md b/docs/components/404-read-receipt.md index 59938ff..c105f83 100644 --- a/docs/components/404-read-receipt.md +++ b/docs/components/404-read-receipt.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -readers := []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} +readers := []display.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReadReceipt(c, mujica.ReceiptRead, mujica.ReadReceiptOptions{Readers: readers}) + core.Use(c, core.Settings{}) + messaging.ReadReceipt(c, messaging.ReceiptRead, messaging.ReadReceiptOptions{Readers: readers}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/405-member-list.md b/docs/components/405-member-list.md index 3c5b809..a169bce 100644 --- a/docs/components/405-member-list.md +++ b/docs/components/405-member-list.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -var s mujica.MemberListState -members := []mujica.Member{ - {ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"}, - {ID: "2", Name: "Umiri", Presence: mujica.PresenceOffline}, +var s messaging.MemberListState +members := []messaging.Member{ + {ID: "1", Name: "Sakiko", Presence: messaging.PresenceOnline, Role: "Leader"}, + {ID: "2", Name: "Umiri", Presence: messaging.PresenceOffline}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemberList(c, &s, members, mujica.MemberListOptions{}); r.Opened != "" { + core.Use(c, core.Settings{}) + if r := messaging.MemberList(c, &s, members, messaging.MemberListOptions{}); r.Opened != "" { _ = r.Opened // show that member's profile } } diff --git a/docs/components/406-user-profile-card.md b/docs/components/406-user-profile-card.md index 778b25b..4c0868f 100644 --- a/docs/components/406-user-profile-card.md +++ b/docs/components/406-user-profile-card.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -profile := mujica.UserProfile{Name: "Sakiko Togawa", Presence: mujica.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} +profile := messaging.UserProfile{Name: "Sakiko Togawa", Presence: messaging.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UserProfileCard(c, profile, mujica.UserProfileCardOptions{Actions: func() { - mujica.Button(c, "Message", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + messaging.UserProfileCard(c, profile, messaging.UserProfileCardOptions{Actions: func() { + input.Button(c, "Message", input.ButtonOptions{}) }}) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/407-online-status.md b/docs/components/407-online-status.md index 2edd031..d2264e6 100644 --- a/docs/components/407-online-status.md +++ b/docs/components/407-online-status.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OnlineStatus(c, mujica.PresenceAway, mujica.OnlineStatusOptions{}) + core.Use(c, core.Settings{}) + messaging.OnlineStatus(c, messaging.PresenceAway, messaging.OnlineStatusOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/408-status-setter.md b/docs/components/408-status-setter.md index 7e1f96b..17be930 100644 --- a/docs/components/408-status-setter.md +++ b/docs/components/408-status-setter.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -status := mujica.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} +status := messaging.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSetter(c, &status, mujica.StatusSetterOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.StatusSetter(c, &status, messaging.StatusSetterOptions{}).Changed() { _ = status // publish it; clear it once status.Until passes } } diff --git a/docs/components/409-call-controls.md b/docs/components/409-call-controls.md index c0ad5dc..201a7b6 100644 --- a/docs/components/409-call-controls.md +++ b/docs/components/409-call-controls.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -st := mujica.CallState{} +st := messaging.CallState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CallControls(c, st, mujica.CallControlsOptions{ + core.Use(c, core.Settings{}) + messaging.CallControls(c, st, messaging.CallControlsOptions{ OnMute: func(on bool) { st.Muted = on }, OnCamera: func(off bool) { st.CameraOff = off }, OnShare: func(on bool) { st.Sharing = on }, diff --git a/docs/components/410-video-call-grid.md b/docs/components/410-video-call-grid.md index 2d1b852..f0e236e 100644 --- a/docs/components/410-video-call-grid.md +++ b/docs/components/410-video-call-grid.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -people := []mujica.CallParticipant{ +people := []messaging.CallParticipant{ {ID: "1", Name: "Sakiko", Speaking: true}, // Frame: the latest decoded frame {ID: "2", Name: "Uika", Muted: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{}) + core.Use(c, core.Settings{}) + messaging.VideoCallGrid(c, people, messaging.VideoCallGridOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/411-incoming-call-dialog.md b/docs/components/411-incoming-call-dialog.md index 26290c3..35734b3 100644 --- a/docs/components/411-incoming-call-dialog.md +++ b/docs/components/411-incoming-call-dialog.md @@ -9,8 +9,8 @@ ```go ringing := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncomingCallDialog(c, &ringing, mujica.IncomingCall{Caller: "Sakiko", Video: true}, mujica.IncomingCallOptions{ + core.Use(c, core.Settings{}) + messaging.IncomingCallDialog(c, &ringing, messaging.IncomingCall{Caller: "Sakiko", Video: true}, messaging.IncomingCallOptions{ OnAccept: func(video bool) {}, // join, with the camera on when video OnDecline: func() {}, }) diff --git a/docs/components/412-emoji-picker.md b/docs/components/412-emoji-picker.md index a59a711..3f88671 100644 --- a/docs/components/412-emoji-picker.md +++ b/docs/components/412-emoji-picker.md @@ -10,8 +10,8 @@ var recent []string tone := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { + core.Use(c, core.Settings{}) + if r := messaging.EmojiPicker(c, messaging.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { _ = r.Picked // insert it } } diff --git a/docs/components/413-pinned-messages.md b/docs/components/413-pinned-messages.md index 3d27874..e0afac0 100644 --- a/docs/components/413-pinned-messages.md +++ b/docs/components/413-pinned-messages.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -pins := []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} +pins := []messaging.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PinnedMessages(c, pins, mujica.PinnedMessagesOptions{}) + core.Use(c, core.Settings{}) + r := messaging.PinnedMessages(c, pins, messaging.PinnedMessagesOptions{}) if r.Unpinned != "" { pins = pins[:0] // unpin it } diff --git a/docs/components/414-thread-panel.md b/docs/components/414-thread-panel.md index 99abd32..4de9e6d 100644 --- a/docs/components/414-thread-panel.md +++ b/docs/components/414-thread-panel.md @@ -8,12 +8,12 @@ ```go open := true -root := mujica.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} -var replies []mujica.ChatEntry +root := messaging.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} +var replies []messaging.ChatEntry view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.ThreadPanel(c, &open, root, replies, mujica.ThreadPanelOptions{}); r.Sent != "" { - replies = append(replies, mujica.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) + core.Use(c, core.Settings{}) + if r := messaging.ThreadPanel(c, &open, root, replies, messaging.ThreadPanelOptions{}); r.Sent != "" { + replies = append(replies, messaging.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) } } ui.Render(view, 400, 300, 1) diff --git a/docs/components/415-mail-list.md b/docs/components/415-mail-list.md index 758e294..05742a3 100644 --- a/docs/components/415-mail-list.md +++ b/docs/components/415-mail-list.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var s mujica.ListState[string] -mails := []mujica.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} +var s data.ListState[string] +mails := []messaging.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailList(c, &s, mails, mujica.MailListOptions{}); r.Starred != "" { + core.Use(c, core.Settings{}) + if r := messaging.MailList(c, &s, mails, messaging.MailListOptions{}); r.Starred != "" { mails[0].Starred = !mails[0].Starred } } diff --git a/docs/components/416-mail-reader.md b/docs/components/416-mail-reader.md index 6f8283b..a9228ed 100644 --- a/docs/components/416-mail-reader.md +++ b/docs/components/416-mail-reader.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -m := mujica.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", - Attachments: []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}}} +m := messaging.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", + Attachments: []messaging.MailAttachment{{Name: "score.pdf", Size: 2048}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailReader(c, m); r.Link != "" { + core.Use(c, core.Settings{}) + if r := messaging.MailReader(c, m); r.Link != "" { c.OpenURL(r.Link) } } diff --git a/docs/components/417-mail-composer.md b/docs/components/417-mail-composer.md index e046cf7..48f6d54 100644 --- a/docs/components/417-mail-composer.md +++ b/docs/components/417-mail-composer.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -draft := mujica.MailDraft{To: []string{"uika@example.com"}} +draft := messaging.MailDraft{To: []string{"uika@example.com"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MailComposer(c, &draft, mujica.MailComposerOptions{ - OnSaveDraft: func(d mujica.MailDraft) {}, // store d + core.Use(c, core.Settings{}) + r := messaging.MailComposer(c, &draft, messaging.MailComposerOptions{ + OnSaveDraft: func(d messaging.MailDraft) {}, // store d }) if r.Sent { - draft = mujica.MailDraft{} + draft = messaging.MailDraft{} } } ui.Render(view, 400, 300, 1) diff --git a/docs/components/418-recipient-input.md b/docs/components/418-recipient-input.md index 5e79205..dda5dca 100644 --- a/docs/components/418-recipient-input.md +++ b/docs/components/418-recipient-input.md @@ -8,10 +8,10 @@ ```go var to []string -contacts := []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} +contacts := []messaging.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecipientInput(c, &to, mujica.RecipientInputOptions{Contacts: contacts, Label: "To"}) + core.Use(c, core.Settings{}) + messaging.RecipientInput(c, &to, messaging.RecipientInputOptions{Contacts: contacts, Label: "To"}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/419-quoted-text.md b/docs/components/419-quoted-text.md index 921d92b..7df6864 100644 --- a/docs/components/419-quoted-text.md +++ b/docs/components/419-quoted-text.md @@ -9,8 +9,8 @@ ```go open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuotedText(c, "> Can you send the score?", &open) + core.Use(c, core.Settings{}) + messaging.QuotedText(c, "> Can you send the score?", &open) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/420-snooze-picker.md b/docs/components/420-snooze-picker.md index 5a8ee40..fd7a47a 100644 --- a/docs/components/420-snooze-picker.md +++ b/docs/components/420-snooze-picker.md @@ -9,8 +9,8 @@ ```go var until time.Time view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SnoozePicker(c, &until, mujica.SnoozePickerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.SnoozePicker(c, &until, messaging.SnoozePickerOptions{}).Changed() { _ = until // hide the mail until then } } diff --git a/docs/components/421-calendar-month-view.md b/docs/components/421-calendar-month-view.md index d889eb4..de3371e 100644 --- a/docs/components/421-calendar-month-view.md +++ b/docs/components/421-calendar-month-view.md @@ -9,10 +9,10 @@ ```go loc := time.Local events := exampleEvents(loc) -month := mujica.YearMonth{Year: 2026, Month: time.October} +month := datetime.YearMonth{Year: 2026, Month: time.October} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarMonthView(c, &month, events, mujica.CalendarMonthViewOptions{Location: loc}) + core.Use(c, core.Settings{}) + r := datetime.CalendarMonthView(c, &month, events, datetime.CalendarMonthViewOptions{Location: loc}) if ch, ok := r.Changed(); ok { fmt.Println("move", ch.ID, "to", ch.Start) } diff --git a/docs/components/422-calendar-week-view.md b/docs/components/422-calendar-week-view.md index b59e941..73f2a8f 100644 --- a/docs/components/422-calendar-week-view.md +++ b/docs/components/422-calendar-week-view.md @@ -9,11 +9,11 @@ ```go loc := time.Local events := exampleEvents(loc) -week := mujica.Date{Year: 2026, Month: time.October, Day: 14} +week := core.Date{Year: 2026, Month: time.October, Day: 14} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarWeekView(c, &week, events, mujica.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) - if ch, ok := r.Changed(); ok && ch.Kind == mujica.CalendarCreate { + core.Use(c, core.Settings{}) + r := datetime.CalendarWeekView(c, &week, events, datetime.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) + if ch, ok := r.Changed(); ok && ch.Kind == datetime.CalendarCreate { fmt.Println("new event", ch.Start, ch.End) } } diff --git a/docs/components/423-calendar-day-view.md b/docs/components/423-calendar-day-view.md index e5e67e0..9709c2a 100644 --- a/docs/components/423-calendar-day-view.md +++ b/docs/components/423-calendar-day-view.md @@ -9,10 +9,10 @@ ```go loc := time.Local events := exampleEvents(loc) -day := mujica.Date{Year: 2026, Month: time.October, Day: 14} +day := core.Date{Year: 2026, Month: time.October, Day: 14} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.CalendarDayView(c, &day, events, mujica.CalendarDayViewOptions{Location: loc}).Opened(); ok { + core.Use(c, core.Settings{}) + if id, ok := datetime.CalendarDayView(c, &day, events, datetime.CalendarDayViewOptions{Location: loc}).Opened(); ok { fmt.Println("open", id) } } diff --git a/docs/components/424-agenda-view.md b/docs/components/424-agenda-view.md index ec0f37f..32198aa 100644 --- a/docs/components/424-agenda-view.md +++ b/docs/components/424-agenda-view.md @@ -10,9 +10,9 @@ loc := time.Local events := exampleEvents(loc) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.AgendaViewOptions{Location: loc, From: mujica.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} - if id, ok := mujica.AgendaView(c, events, opts).Opened(); ok { + core.Use(c, core.Settings{}) + opts := datetime.AgendaViewOptions{Location: loc, From: core.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} + if id, ok := datetime.AgendaView(c, events, opts).Opened(); ok { fmt.Println("open", id) } } diff --git a/docs/components/425-year-view.md b/docs/components/425-year-view.md index 52b4c49..68732d7 100644 --- a/docs/components/425-year-view.md +++ b/docs/components/425-year-view.md @@ -11,8 +11,8 @@ loc := time.Local events := exampleEvents(loc) year := 2026 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if day, ok := mujica.YearView(c, &year, events, mujica.YearViewOptions{Location: loc}).DayChosen(); ok { + core.Use(c, core.Settings{}) + if day, ok := datetime.YearView(c, &year, events, datetime.YearViewOptions{Location: loc}).DayChosen(); ok { fmt.Println("go to", day) } } diff --git a/docs/components/426-event-chip.md b/docs/components/426-event-chip.md index 1f716e1..032602d 100644 --- a/docs/components/426-event-chip.md +++ b/docs/components/426-event-chip.md @@ -10,8 +10,8 @@ loc := time.Local ev := exampleEvents(loc)[0] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}).Clicked() { + core.Use(c, core.Settings{}) + if datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}).Clicked() { fmt.Println("open", ev.ID) } } diff --git a/docs/components/427-event-editor.md b/docs/components/427-event-editor.md index 18300fc..18a276f 100644 --- a/docs/components/427-event-editor.md +++ b/docs/components/427-event-editor.md @@ -10,8 +10,8 @@ loc := time.Local ev := exampleEvents(loc)[0] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EventEditor(c, &ev, mujica.EventEditorOptions{Location: loc}) + core.Use(c, core.Settings{}) + r := datetime.EventEditor(c, &ev, datetime.EventEditorOptions{Location: loc}) if r.Saved() { fmt.Println("saved", ev.Title, ev.Start) } diff --git a/docs/components/428-event-popover.md b/docs/components/428-event-popover.md index 4f8639f..cfa3e64 100644 --- a/docs/components/428-event-popover.md +++ b/docs/components/428-event-popover.md @@ -11,12 +11,12 @@ loc := time.Local ev := exampleEvents(loc)[0] open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}) + core.Use(c, core.Settings{}) + chip := datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}) if chip.Clicked() { open = true } - r := mujica.EventPopover(c, chip, &open, ev, mujica.EventPopoverOptions{Location: loc}) + r := datetime.EventPopover(c, chip, &open, ev, datetime.EventPopoverOptions{Location: loc}) if r.Delete() { fmt.Println("delete", ev.ID) } diff --git a/docs/components/429-current-time-indicator.md b/docs/components/429-current-time-indicator.md index eb9f42b..0271d4c 100644 --- a/docs/components/429-current-time-indicator.md +++ b/docs/components/429-current-time-indicator.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Size(200, 240).Children(func() { - mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local}) + datetime.CurrentTimeIndicator(c, datetime.CurrentTimeIndicatorOptions{Location: time.Local}) }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/430-recurrence-editor.md b/docs/components/430-recurrence-editor.md index e8c754a..33a4e4e 100644 --- a/docs/components/430-recurrence-editor.md +++ b/docs/components/430-recurrence-editor.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -rule := mujica.Recurrence{Unit: mujica.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} +rule := datetime.Recurrence{Unit: datetime.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RecurrenceEditor(c, &rule, mujica.RecurrenceEditorOptions{Start: mujica.Date{Year: 2026, Month: time.October, Day: 5}}) + core.Use(c, core.Settings{}) + r := datetime.RecurrenceEditor(c, &rule, datetime.RecurrenceEditorOptions{Start: core.Date{Year: 2026, Month: time.October, Day: 5}}) if r.Changed() && r.Problem() == "" { - fmt.Println(mujica.RecurrenceSummary(c, rule)) + fmt.Println(datetime.RecurrenceSummary(c, rule)) } } ui.Render(view, 400, 300, 1) diff --git a/docs/components/431-availability-picker.md b/docs/components/431-availability-picker.md index 98c219f..93dd512 100644 --- a/docs/components/431-availability-picker.md +++ b/docs/components/431-availability-picker.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -days := []mujica.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} +days := []core.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} mine := [][]bool{make([]bool, 18), make([]bool, 18)} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AvailabilityPicker(c, &mine, mujica.AvailabilityPickerOptions{Days: days}).Changed() { + core.Use(c, core.Settings{}) + if datetime.AvailabilityPicker(c, &mine, datetime.AvailabilityPickerOptions{Days: days}).Changed() { fmt.Println("availability changed") } } diff --git a/docs/components/432-timezone-select.md b/docs/components/432-timezone-select.md index 90662d6..a40a5d2 100644 --- a/docs/components/432-timezone-select.md +++ b/docs/components/432-timezone-select.md @@ -9,8 +9,8 @@ ```go zone := "Asia/Shanghai" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimezoneSelect(c, &zone, mujica.TimezoneSelectOptions{}).Changed() { + core.Use(c, core.Settings{}) + if datetime.TimezoneSelect(c, &zone, datetime.TimezoneSelectOptions{}).Changed() { fmt.Println("zone", zone) } } diff --git a/docs/components/433-reminder-picker.md b/docs/components/433-reminder-picker.md index 497de8c..bd20c2e 100644 --- a/docs/components/433-reminder-picker.md +++ b/docs/components/433-reminder-picker.md @@ -9,8 +9,8 @@ ```go reminders := []time.Duration{15 * time.Minute} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ReminderPicker(c, &reminders, mujica.ReminderPickerOptions{Max: 3}).Changed() { + core.Use(c, core.Settings{}) + if datetime.ReminderPicker(c, &reminders, datetime.ReminderPickerOptions{Max: 3}).Changed() { fmt.Println("reminders", reminders) } } diff --git a/docs/components/434-attendee-list.md b/docs/components/434-attendee-list.md index 7d22502..4102450 100644 --- a/docs/components/434-attendee-list.md +++ b/docs/components/434-attendee-list.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -people := []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} +people := []datetime.Attendee{{Name: "Sakiko", Status: datetime.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if i, ok := mujica.AttendeeList(c, people, mujica.AttendeeListOptions{Removable: true}).Removed(); ok { + core.Use(c, core.Settings{}) + if i, ok := datetime.AttendeeList(c, people, datetime.AttendeeListOptions{Removable: true}).Removed(); ok { people = append(people[:i], people[i+1:]...) } } diff --git a/docs/components/435-week-picker.md b/docs/components/435-week-picker.md index bd8f730..34dbe5c 100644 --- a/docs/components/435-week-picker.md +++ b/docs/components/435-week-picker.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var week mujica.Date +var week core.Date view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.WeekPicker(c, &week, mujica.WeekPickerOptions{WeekStart: mujica.WeekStartMonday, Clearable: true}).Changed() { + core.Use(c, core.Settings{}) + if datetime.WeekPicker(c, &week, datetime.WeekPickerOptions{WeekStart: datetime.WeekStartMonday, Clearable: true}).Changed() { fmt.Println("week of", week) } } diff --git a/docs/components/436-duration-picker.md b/docs/components/436-duration-picker.md index cd5034c..c5f8436 100644 --- a/docs/components/436-duration-picker.md +++ b/docs/components/436-duration-picker.md @@ -9,9 +9,9 @@ ```go length := 90 * time.Minute view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} - if mujica.DurationPicker(c, &length, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} + if datetime.DurationPicker(c, &length, opts).Changed() { fmt.Println("length", length) } } diff --git a/docs/components/437-cron-editor.md b/docs/components/437-cron-editor.md index 8861cb4..7842ea4 100644 --- a/docs/components/437-cron-editor.md +++ b/docs/components/437-cron-editor.md @@ -9,9 +9,9 @@ ```go expr := "0 9 * * MON-FRI" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CronEditor(c, &expr, mujica.CronEditorOptions{Location: time.Local}).Changed() { - if _, err := mujica.ParseCron(expr); err == nil { + core.Use(c, core.Settings{}) + if datetime.CronEditor(c, &expr, datetime.CronEditorOptions{Location: time.Local}).Changed() { + if _, err := datetime.ParseCron(expr); err == nil { fmt.Println("schedule", expr) } } diff --git a/docs/components/438-relative-time.md b/docs/components/438-relative-time.md index 7e218bd..ec65118 100644 --- a/docs/components/438-relative-time.md +++ b/docs/components/438-relative-time.md @@ -9,8 +9,8 @@ ```go posted := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.Local) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RelativeTime(c, posted, mujica.RelativeTimeOptions{Location: time.Local}) + core.Use(c, core.Settings{}) + datetime.RelativeTime(c, posted, datetime.RelativeTimeOptions{Location: time.Local}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/439-countdown.md b/docs/components/439-countdown.md index 31b8db2..3ac364d 100644 --- a/docs/components/439-countdown.md +++ b/docs/components/439-countdown.md @@ -9,8 +9,8 @@ ```go deadline := time.Date(2026, time.December, 31, 23, 59, 59, 0, time.Local) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Countdown(c, deadline, mujica.CountdownOptions{}).Finished() { + core.Use(c, core.Settings{}) + if datetime.Countdown(c, deadline, datetime.CountdownOptions{}).Finished() { fmt.Println("happy new year") } } diff --git a/docs/components/440-stopwatch.md b/docs/components/440-stopwatch.md index b74f9de..ffc2765 100644 --- a/docs/components/440-stopwatch.md +++ b/docs/components/440-stopwatch.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -var sw mujica.StopwatchState +var sw datetime.StopwatchState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Stopwatch(c, &sw, mujica.StopwatchOptions{}).Changed() { + core.Use(c, core.Settings{}) + if datetime.Stopwatch(c, &sw, datetime.StopwatchOptions{}).Changed() { fmt.Println("laps", len(sw.Laps)) } } diff --git a/docs/components/442-stat-card.md b/docs/components/442-stat-card.md index 481ee55..106fb4f 100644 --- a/docs/components/442-stat-card.md +++ b/docs/components/442-stat-card.md @@ -8,10 +8,10 @@ ```go value := "12,480" -opts := mujica.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} +opts := devtools.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatCard(c, value, opts).Changed() { + core.Use(c, core.Settings{}) + if devtools.StatCard(c, value, opts).Changed() { fmt.Println("retry") } } diff --git a/docs/components/443-alert-list.md b/docs/components/443-alert-list.md index d8472d4..1760c0d 100644 --- a/docs/components/443-alert-list.md +++ b/docs/components/443-alert-list.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -alerts := []mujica.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: mujica.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} +alerts := []devtools.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: devtools.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AlertList(c, alerts, mujica.AlertListOptions{}) + core.Use(c, core.Settings{}) + r := devtools.AlertList(c, alerts, devtools.AlertListOptions{}) if r.Ack != "" { fmt.Println("ack", r.Ack) } diff --git a/docs/components/444-incident-card.md b/docs/components/444-incident-card.md index 98f4d80..bafa0de 100644 --- a/docs/components/444-incident-card.md +++ b/docs/components/444-incident-card.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -incident := mujica.Incident{Title: "API latency", Status: mujica.IncidentMonitoring, Impact: mujica.ImpactMinor, Updates: []mujica.IncidentUpdate{{Status: mujica.IncidentIdentified, Text: "Slow query located"}, {Status: mujica.IncidentMonitoring, Text: "Recovery observed"}}} +incident := devtools.Incident{Title: "API latency", Status: devtools.IncidentMonitoring, Impact: devtools.ImpactMinor, Updates: []devtools.IncidentUpdate{{Status: devtools.IncidentIdentified, Text: "Slow query located"}, {Status: devtools.IncidentMonitoring, Text: "Recovery observed"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncidentCard(c, incident) + core.Use(c, core.Settings{}) + devtools.IncidentCard(c, incident) } ui.Render(view, 720, 480, 1) ``` diff --git a/docs/components/445-uptime-bar.md b/docs/components/445-uptime-bar.md index b52ed3e..ab5399c 100644 --- a/docs/components/445-uptime-bar.md +++ b/docs/components/445-uptime-bar.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -days := []mujica.UptimeDay{{Date: mujica.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: mujica.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} +days := []devtools.UptimeDay{{Date: core.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: core.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.UptimeBar(c, days, mujica.UptimeBarOptions{Frame: mujica.ChartFrame{Label: "API uptime"}}) + core.Use(c, core.Settings{}) + r := devtools.UptimeBar(c, days, devtools.UptimeBarOptions{Frame: chart.ChartFrame{Label: "API uptime"}}) if r.Changed() { fmt.Println("day", r.Point) } diff --git a/docs/components/446-service-status.md b/docs/components/446-service-status.md index 2692c51..8cfb2f4 100644 --- a/docs/components/446-service-status.md +++ b/docs/components/446-service-status.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -services := []mujica.ServiceItem{{Name: "API", Health: mujica.ServiceOperational}, {Name: "Search", Health: mujica.ServiceDegraded, Note: "Slow queries"}} +services := []devtools.ServiceItem{{Name: "API", Health: devtools.ServiceOperational}, {Name: "Search", Health: devtools.ServiceDegraded, Note: "Slow queries"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ServiceStatus(c, services, mujica.ServiceStatusOptions{}) + core.Use(c, core.Settings{}) + devtools.ServiceStatus(c, services, devtools.ServiceStatusOptions{}) } ui.Render(view, 720, 480, 1) ``` diff --git a/docs/components/447-log-stream.md b/docs/components/447-log-stream.md index 951d541..1140df1 100644 --- a/docs/components/447-log-stream.md +++ b/docs/components/447-log-stream.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "server ready"}, {Level: mujica.LogWarn, Text: "slow query"}} -var state mujica.LogStreamState +lines := []code.LogLine{{Level: code.LogInfo, Text: "server ready"}, {Level: code.LogWarn, Text: "slow query"}} +var state devtools.LogStreamState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LogStream(c, lines, &state, mujica.LogStreamOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.LogStream(c, lines, &state, devtools.LogStreamOptions{}).Changed() { fmt.Println("filter changed") } } diff --git a/docs/components/448-trace-waterfall.md b/docs/components/448-trace-waterfall.md index a297948..f578a2b 100644 --- a/docs/components/448-trace-waterfall.md +++ b/docs/components/448-trace-waterfall.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -spans := []mujica.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} +spans := []devtools.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TraceWaterfall(c, spans, mujica.TraceWaterfallOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.TraceWaterfall(c, spans, devtools.TraceWaterfallOptions{}).Changed() { fmt.Println("span selected") } } diff --git a/docs/components/449-resource-gauge.md b/docs/components/449-resource-gauge.md index 47b9336..6e686b2 100644 --- a/docs/components/449-resource-gauge.md +++ b/docs/components/449-resource-gauge.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -readings := []mujica.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} +readings := []devtools.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResourceGauge(c, readings, mujica.ResourceGaugeOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.ResourceGauge(c, readings, devtools.ResourceGaugeOptions{}).Changed() { fmt.Println("retry") } } diff --git a/docs/components/450-system-monitor.md b/docs/components/450-system-monitor.md index 71bce85..5f028e4 100644 --- a/docs/components/450-system-monitor.md +++ b/docs/components/450-system-monitor.md @@ -8,10 +8,10 @@ ```go times := []time.Time{time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC), time.Date(2026, 10, 5, 9, 0, 5, 0, time.UTC)} -metrics := []mujica.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} +metrics := []devtools.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemMonitor(c, times, metrics, mujica.SystemMonitorOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.SystemMonitor(c, times, metrics, devtools.SystemMonitorOptions{}).Changed() { fmt.Println("retry") } } diff --git a/docs/components/451-refresh-interval-selector.md b/docs/components/451-refresh-interval-selector.md index 5dd56da..bf8a302 100644 --- a/docs/components/451-refresh-interval-selector.md +++ b/docs/components/451-refresh-interval-selector.md @@ -9,8 +9,8 @@ ```go interval := 30 * time.Second view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RefreshIntervalSelector(c, &interval, mujica.RefreshIntervalSelectorOptions{}) + core.Use(c, core.Settings{}) + r := devtools.RefreshIntervalSelector(c, &interval, devtools.RefreshIntervalSelectorOptions{}) if r.Refresh { fmt.Println("refresh") } diff --git a/docs/components/452-dashboard-filter-bar.md b/docs/components/452-dashboard-filter-bar.md index b5f3cf5..08824c8 100644 --- a/docs/components/452-dashboard-filter-bar.md +++ b/docs/components/452-dashboard-filter-bar.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var filters mujica.DashboardFilters -opts := mujica.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} +var filters devtools.DashboardFilters +opts := devtools.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DashboardFilterBar(c, &filters, opts).Changed() { + core.Use(c, core.Settings{}) + if devtools.DashboardFilterBar(c, &filters, opts).Changed() { fmt.Println("filters changed") } } diff --git a/docs/components/453-json-viewer.md b/docs/components/453-json-viewer.md index 5abd3ce..a4fe41a 100644 --- a/docs/components/453-json-viewer.md +++ b/docs/components/453-json-viewer.md @@ -8,10 +8,10 @@ ```go data := `{"server":{"port":8080},"ready":true}` -var state mujica.JsonViewerState +var state devtools.JsonViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonViewer(c, data, &state, mujica.JsonViewerOptions{}) + core.Use(c, core.Settings{}) + r := devtools.JsonViewer(c, data, &state, devtools.JsonViewerOptions{}) if r.Copied != "" { fmt.Println("path copied") } diff --git a/docs/components/454-json-editor.md b/docs/components/454-json-editor.md index 2bb8043..88255c3 100644 --- a/docs/components/454-json-editor.md +++ b/docs/components/454-json-editor.md @@ -9,8 +9,8 @@ ```go text := `{"port":8080,"ready":true}` view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonEditor(c, &text, mujica.JsonEditorOptions{}) + core.Use(c, core.Settings{}) + r := devtools.JsonEditor(c, &text, devtools.JsonEditorOptions{}) if r.Changed { fmt.Println("valid", r.Valid) } diff --git a/docs/components/455-schema-tree.md b/docs/components/455-schema-tree.md index 8eedc87..3093ce2 100644 --- a/docs/components/455-schema-tree.md +++ b/docs/components/455-schema-tree.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -roots := []mujica.SchemaNode{{Name: "app", Children: []mujica.SchemaNode{{Name: "users", Kind: mujica.SchemaTable, Children: []mujica.SchemaNode{{Name: "id", Kind: mujica.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: mujica.SchemaIndex, Detail: "(id)"}}}}}} -var state mujica.SchemaTreeState +roots := []devtools.SchemaNode{{Name: "app", Children: []devtools.SchemaNode{{Name: "users", Kind: devtools.SchemaTable, Children: []devtools.SchemaNode{{Name: "id", Kind: devtools.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: devtools.SchemaIndex, Detail: "(id)"}}}}}} +var state devtools.SchemaTreeState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SchemaTree(c, &state, roots, mujica.SchemaTreeOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.SchemaTree(c, &state, roots, devtools.SchemaTreeOptions{}).Changed() { path, _ := state.Selected() fmt.Println("selected", path) } diff --git a/docs/components/456-api-request-builder.md b/docs/components/456-api-request-builder.md index 90b2de9..de4880a 100644 --- a/docs/components/456-api-request-builder.md +++ b/docs/components/456-api-request-builder.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -request := mujica.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} +request := devtools.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ApiRequestBuilder(c, &request, mujica.ApiRequestBuilderOptions{}) + core.Use(c, core.Settings{}) + r := devtools.ApiRequestBuilder(c, &request, devtools.ApiRequestBuilderOptions{}) if r.Send { fmt.Println("send", request.Method) } diff --git a/docs/components/457-response-viewer.md b/docs/components/457-response-viewer.md index e90b164..360e523 100644 --- a/docs/components/457-response-viewer.md +++ b/docs/components/457-response-viewer.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -response := &mujica.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: `{"ready":true}`, Headers: []mujica.KeyValue{{Key: "Content-Type", Value: "application/json"}}} +response := &devtools.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: `{"ready":true}`, Headers: []devtools.KeyValue{{Key: "Content-Type", Value: "application/json"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResponseViewer(c, response, mujica.ResponseViewerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.ResponseViewer(c, response, devtools.ResponseViewerOptions{}).Changed() { fmt.Println("retry") } } diff --git a/docs/components/458-key-value-input.md b/docs/components/458-key-value-input.md index 46acde6..fb98995 100644 --- a/docs/components/458-key-value-input.md +++ b/docs/components/458-key-value-input.md @@ -7,10 +7,10 @@ ## 最小示例 ```go -rows := []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} +rows := []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.KeyValueInput(c, &rows, mujica.KeyValueInputOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.KeyValueInput(c, &rows, devtools.KeyValueInputOptions{}).Changed() { fmt.Println("rows", len(rows)) } } diff --git a/docs/components/461-settings-layout.md b/docs/components/461-settings-layout.md index 295bee6..e13d3d7 100644 --- a/docs/components/461-settings-layout.md +++ b/docs/components/461-settings-layout.md @@ -11,13 +11,13 @@ ```go page := "" sound := true -pages := []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} +pages := []account.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SettingsLayout(c, &page, pages, mujica.SettingsLayoutOptions{}, func(id string) { - mujica.SettingsSection(c, "Notifications", func() { - mujica.SettingsRow(c, "Sound", mujica.SettingsRowOptions{ID: "sound"}, func() *ui.Element { - return mujica.Switch(c, &sound, "", mujica.SwitchOptions{}) + core.Use(c, core.Settings{}) + account.SettingsLayout(c, &page, pages, account.SettingsLayoutOptions{}, func(id string) { + account.SettingsSection(c, "Notifications", func() { + account.SettingsRow(c, "Sound", account.SettingsRowOptions{ID: "sound"}, func() *ui.Element { + return input.Switch(c, &sound, "", input.SwitchOptions{}) }) }) }) diff --git a/docs/components/462-settings-search.md b/docs/components/462-settings-search.md index 3a87a89..5caaa17 100644 --- a/docs/components/462-settings-search.md +++ b/docs/components/462-settings-search.md @@ -10,10 +10,10 @@ ```go query, page := "", "general" -entries := []mujica.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} +entries := []account.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if e, ok := mujica.SettingsSearch(c, &query, entries, mujica.SettingsSearchOptions{}).Chosen(); ok { + core.Use(c, core.Settings{}) + if e, ok := account.SettingsSearch(c, &query, entries, account.SettingsSearchOptions{}).Chosen(); ok { page = e.Page } fmt.Println("page", page) diff --git a/docs/components/463-theme-selector.md b/docs/components/463-theme-selector.md index f5531f2..85f12fe 100644 --- a/docs/components/463-theme-selector.md +++ b/docs/components/463-theme-selector.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -mode := mujica.System +mode := core.System view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) - mujica.ThemeSelector(c, &mode, mujica.ThemeSelectorOptions{}) + core.Use(c, core.Settings{Mode: mode}) + account.ThemeSelector(c, &mode, account.ThemeSelectorOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/464-accent-color-picker.md b/docs/components/464-accent-color-picker.md index 1a94706..ce18f04 100644 --- a/docs/components/464-accent-color-picker.md +++ b/docs/components/464-accent-color-picker.md @@ -11,8 +11,8 @@ ```go accent := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AccentColorPicker(c, &accent, mujica.AccentColorPickerOptions{}) + core.Use(c, core.Settings{}) + account.AccentColorPicker(c, &accent, account.AccentColorPickerOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/465-shortcut-recorder.md b/docs/components/465-shortcut-recorder.md index 1824e88..3d0311f 100644 --- a/docs/components/465-shortcut-recorder.md +++ b/docs/components/465-shortcut-recorder.md @@ -9,12 +9,12 @@ ## 最小示例 ```go -palette := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} +palette := account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShortcutRecorder(c, &palette, mujica.ShortcutRecorderOptions{ + core.Use(c, core.Settings{}) + account.ShortcutRecorder(c, &palette, account.ShortcutRecorderOptions{ Label: "Command palette", - Taken: []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, + Taken: []account.ShortcutBinding{{Action: "Save", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, }) } ui.Render(view, 400, 300, 1) diff --git a/docs/components/466-keyboard-shortcuts-list.md b/docs/components/466-keyboard-shortcuts-list.md index 03d299b..da23379 100644 --- a/docs/components/466-keyboard-shortcuts-list.md +++ b/docs/components/466-keyboard-shortcuts-list.md @@ -10,12 +10,12 @@ ```go query := "" -groups := []mujica.ShortcutGroup{{Title: "General", Bindings: []mujica.ShortcutBinding{ - {Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, +groups := []account.ShortcutGroup{{Title: "General", Bindings: []account.ShortcutBinding{ + {Action: "Command palette", Shortcut: account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, }}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.KeyboardShortcutsList(c, &query, groups, mujica.KeyboardShortcutsListOptions{}) + core.Use(c, core.Settings{}) + account.KeyboardShortcutsList(c, &query, groups, account.KeyboardShortcutsListOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/467-login-form.md b/docs/components/467-login-form.md index 7747215..6068286 100644 --- a/docs/components/467-login-form.md +++ b/docs/components/467-login-form.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -var state mujica.LoginFormState +var state account.LoginFormState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LoginForm(c, &state, mujica.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { + core.Use(c, core.Settings{}) + account.LoginForm(c, &state, account.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { // Submit credentials through the caller-provided auth service. state.EndSubmit("") }}) diff --git a/docs/components/468-oauth-buttons.md b/docs/components/468-oauth-buttons.md index 12df8ea..70b35e3 100644 --- a/docs/components/468-oauth-buttons.md +++ b/docs/components/468-oauth-buttons.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -providers := []mujica.OAuthProvider{{ID: "sso", Name: "Company SSO"}} +providers := []account.OAuthProvider{{ID: "sso", Name: "Company SSO"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{}).Chosen(); ok { + core.Use(c, core.Settings{}) + if id, ok := account.OAuthButtons(c, providers, account.OAuthButtonsOptions{}).Chosen(); ok { fmt.Println("start sign-in with", id) } } diff --git a/docs/components/469-two-factor-input.md b/docs/components/469-two-factor-input.md index 4c0db44..c0df099 100644 --- a/docs/components/469-two-factor-input.md +++ b/docs/components/469-two-factor-input.md @@ -12,8 +12,8 @@ code := "" resendAt := time.Now().Add(30 * time.Second) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TwoFactorInput(c, &code, mujica.TwoFactorInputOptions{ResendAt: resendAt}) + core.Use(c, core.Settings{}) + r := account.TwoFactorInput(c, &code, account.TwoFactorInputOptions{ResendAt: resendAt}) if r.Completed() { fmt.Println("verify the code") } diff --git a/docs/components/470-profile-editor.md b/docs/components/470-profile-editor.md index f29d295..9242839 100644 --- a/docs/components/470-profile-editor.md +++ b/docs/components/470-profile-editor.md @@ -9,11 +9,11 @@ ## 最小示例 ```go -saved := mujica.Profile{Name: "Sakiko", Bio: "Keyboard."} +saved := account.Profile{Name: "Sakiko", Bio: "Keyboard."} profile := saved view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ProfileEditor(c, &profile, mujica.ProfileEditorOptions{Saved: saved}).Save() { + core.Use(c, core.Settings{}) + if account.ProfileEditor(c, &profile, account.ProfileEditorOptions{Saved: saved}).Save() { saved = profile } } diff --git a/docs/components/471-account-switcher.md b/docs/components/471-account-switcher.md index a680d88..878b8cd 100644 --- a/docs/components/471-account-switcher.md +++ b/docs/components/471-account-switcher.md @@ -10,10 +10,10 @@ ```go current := "a" -accounts := []mujica.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} +accounts := []account.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AccountSwitcher(c, ¤t, accounts, mujica.AccountSwitcherOptions{}).Add() { + core.Use(c, core.Settings{}) + if account.AccountSwitcher(c, ¤t, accounts, account.AccountSwitcherOptions{}).Add() { fmt.Println("sign in another account") } } diff --git a/docs/components/472-workspace-switcher.md b/docs/components/472-workspace-switcher.md index f7bbb97..f59dbcd 100644 --- a/docs/components/472-workspace-switcher.md +++ b/docs/components/472-workspace-switcher.md @@ -10,10 +10,10 @@ ```go current := "ave" -spaces := []mujica.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} +spaces := []account.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkspaceSwitcher(c, ¤t, spaces, mujica.WorkspaceSwitcherOptions{}) + core.Use(c, core.Settings{}) + account.WorkspaceSwitcher(c, ¤t, spaces, account.WorkspaceSwitcherOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/473-user-menu.md b/docs/components/473-user-menu.md index 3576b7e..b52a247 100644 --- a/docs/components/473-user-menu.md +++ b/docs/components/473-user-menu.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -status := mujica.UserStatusOnline +status := account.UserStatusOnline view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.UserMenu(c, &status, mujica.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { + core.Use(c, core.Settings{}) + if account.UserMenu(c, &status, account.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { fmt.Println("sign out") } } diff --git a/docs/components/474-session-list.md b/docs/components/474-session-list.md index dc7769b..f4c1b7a 100644 --- a/docs/components/474-session-list.md +++ b/docs/components/474-session-list.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -sessions := []mujica.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} +sessions := []account.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.SessionList(c, sessions, mujica.SessionListOptions{}).Revoked(); ok { + core.Use(c, core.Settings{}) + if id, ok := account.SessionList(c, sessions, account.SessionListOptions{}).Revoked(); ok { fmt.Println("sign out", id) } } diff --git a/docs/components/475-api-key-manager.md b/docs/components/475-api-key-manager.md index 7593dbc..eb7bc82 100644 --- a/docs/components/475-api-key-manager.md +++ b/docs/components/475-api-key-manager.md @@ -9,11 +9,11 @@ API 密钥管理。 ## 最小示例 ```go -var state mujica.ApiKeyManagerState -keys := []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} +var state account.ApiKeyManagerState +keys := []account.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if name, ok := mujica.ApiKeyManager(c, &state, keys, mujica.ApiKeyManagerOptions{}).Create(); ok { + core.Use(c, core.Settings{}) + if name, ok := account.ApiKeyManager(c, &state, keys, account.ApiKeyManagerOptions{}).Create(); ok { fmt.Println("create", name) state.Reveal("secret from your key service") } diff --git a/docs/components/476-usage-quota.md b/docs/components/476-usage-quota.md index 49b33cf..3b34dc2 100644 --- a/docs/components/476-usage-quota.md +++ b/docs/components/476-usage-quota.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -items := []mujica.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} +items := []account.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UsageQuota(c, items, mujica.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) + core.Use(c, core.Settings{}) + account.UsageQuota(c, items, account.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/477-onboarding-wizard.md b/docs/components/477-onboarding-wizard.md index 562cf5b..2c47c84 100644 --- a/docs/components/477-onboarding-wizard.md +++ b/docs/components/477-onboarding-wizard.md @@ -10,10 +10,10 @@ ```go step := 0 -steps := []mujica.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} +steps := []account.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.OnboardingWizard(c, &step, steps, mujica.OnboardingWizardOptions{}).Finished() { + core.Use(c, core.Settings{}) + if account.OnboardingWizard(c, &step, steps, account.OnboardingWizardOptions{}).Finished() { fmt.Println("onboarded") } } diff --git a/docs/components/478-coachmark.md b/docs/components/478-coachmark.md index 2241266..b79a6d3 100644 --- a/docs/components/478-coachmark.md +++ b/docs/components/478-coachmark.md @@ -11,9 +11,9 @@ ```go open, step := true, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - compose := mujica.Button(c, "Compose", mujica.ButtonOptions{}) - mujica.Coachmark(c, &open, &step, []mujica.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) + core.Use(c, core.Settings{}) + compose := input.Button(c, "Compose", input.ButtonOptions{}) + account.Coachmark(c, &open, &step, []account.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/479-whats-new-dialog.md b/docs/components/479-whats-new-dialog.md index e957c31..0c545aa 100644 --- a/docs/components/479-whats-new-dialog.md +++ b/docs/components/479-whats-new-dialog.md @@ -10,10 +10,10 @@ ```go open, hide := true, false -releases := []mujica.Release{{Version: "1.1", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "500 components."}}}} +releases := []account.Release{{Version: "1.1", Changes: []account.ReleaseChange{{Kind: account.ChangeNew, Text: "500 components."}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WhatsNewDialog(c, &open, &hide, releases, mujica.WhatsNewDialogOptions{}) + core.Use(c, core.Settings{}) + account.WhatsNewDialog(c, &open, &hide, releases, account.WhatsNewDialogOptions{}) } ui.Render(view, 400, 300, 1) ``` diff --git a/docs/components/480-feedback-widget.md b/docs/components/480-feedback-widget.md index 9b89cb1..7cace68 100644 --- a/docs/components/480-feedback-widget.md +++ b/docs/components/480-feedback-widget.md @@ -9,10 +9,10 @@ ## 最小示例 ```go -var state mujica.FeedbackWidgetState +var state account.FeedbackWidgetState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if f, ok := mujica.FeedbackWidget(c, &state, mujica.FeedbackWidgetOptions{}).Submitted(); ok { + core.Use(c, core.Settings{}) + if f, ok := account.FeedbackWidget(c, &state, account.FeedbackWidgetOptions{}).Submitted(); ok { fmt.Println("send", f.Kind) state.EndSubmit("") } diff --git a/docs/components/481-split-button.md b/docs/components/481-split-button.md index d2385f2..71cabae 100644 --- a/docs/components/481-split-button.md +++ b/docs/components/481-split-button.md @@ -8,9 +8,9 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Save as…", mujica.PopupMenuItemOptions{}) { + core.Use(c, core.Settings{}) + r := overlay.SplitButton(c, "Save", overlay.SplitButtonOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Save as…", overlay.PopupMenuItemOptions{}) { // ask for a file name } }) diff --git a/docs/components/482-copy-button.md b/docs/components/482-copy-button.md index 4c76b83..fd381f8 100644 --- a/docs/components/482-copy-button.md +++ b/docs/components/482-copy-button.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CopyButton(c, "https://example.com/invite/42", mujica.CopyButtonOptions{Label: "Copy link"}) + core.Use(c, core.Settings{}) + input.CopyButton(c, "https://example.com/invite/42", input.CopyButtonOptions{Label: "Copy link"}) } ``` diff --git a/docs/components/483-hold-to-confirm.md b/docs/components/483-hold-to-confirm.md index 22fa635..e27258e 100644 --- a/docs/components/483-hold-to-confirm.md +++ b/docs/components/483-hold-to-confirm.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{}).Changed() { + core.Use(c, core.Settings{}) + if input.HoldToConfirm(c, "Delete project", input.HoldToConfirmOptions{}).Changed() { // delete the project } } diff --git a/docs/components/484-bulk-action-bar.md b/docs/components/484-bulk-action-bar.md index 1d0b1c0..36256e8 100644 --- a/docs/components/484-bulk-action-bar.md +++ b/docs/components/484-bulk-action-bar.md @@ -9,9 +9,9 @@ ```go selected := 3 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BulkActionBar(c, selected, mujica.BulkActionBarOptions{}, func() { - mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + r := input.BulkActionBar(c, selected, input.BulkActionBarOptions{}, func() { + input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) }) if r.Changed() { selected = 0 diff --git a/docs/components/485-scrub-input.md b/docs/components/485-scrub-input.md index 1a6fc45..6149eeb 100644 --- a/docs/components/485-scrub-input.md +++ b/docs/components/485-scrub-input.md @@ -9,8 +9,8 @@ ```go opacity := 0.6 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScrubInput(c, "Opacity", &opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.1}) + core.Use(c, core.Settings{}) + input.ScrubInput(c, "Opacity", &opacity, input.ScrubInputOptions{Max: 1, Step: 0.1}) } ``` diff --git a/docs/components/486-currency-input.md b/docs/components/486-currency-input.md index 9da7a7a..3b1a327 100644 --- a/docs/components/486-currency-input.md +++ b/docs/components/486-currency-input.md @@ -9,8 +9,8 @@ ```go cents := int64(123450) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CurrencyInput(c, ¢s, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price"}) + core.Use(c, core.Settings{}) + input.CurrencyInput(c, ¢s, input.CurrencyInputOptions{Symbol: "$", Label: "Price"}) } ``` diff --git a/docs/components/487-masked-input.md b/docs/components/487-masked-input.md index 7b15439..9a2f98d 100644 --- a/docs/components/487-masked-input.md +++ b/docs/components/487-masked-input.md @@ -9,8 +9,8 @@ ```go phone := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MaskedInput(c, &phone, mujica.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) + core.Use(c, core.Settings{}) + input.MaskedInput(c, &phone, input.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) } ``` diff --git a/docs/components/488-mention-input.md b/docs/components/488-mention-input.md index 8f8472b..b6a4727 100644 --- a/docs/components/488-mention-input.md +++ b/docs/components/488-mention-input.md @@ -7,14 +7,14 @@ ## 最小示例 ```go -text, mentions := "", []mujica.Mention(nil) -people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} +text, mentions := "", []input.Mention(nil) +people := []input.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MentionInput(c, &text, &mentions, mujica.MentionInputOptions{ + core.Use(c, core.Settings{}) + input.MentionInput(c, &text, &mentions, input.MentionInputOptions{ Label: "Comment", - Candidates: func(q string) []mujica.MentionCandidate { - var out []mujica.MentionCandidate + Candidates: func(q string) []input.MentionCandidate { + var out []input.MentionCandidate for _, p := range people { if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { out = append(out, p) diff --git a/docs/components/489-choice-chips.md b/docs/components/489-choice-chips.md index 3d61812..33f2706 100644 --- a/docs/components/489-choice-chips.md +++ b/docs/components/489-choice-chips.md @@ -9,10 +9,10 @@ ```go var status []string view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChoiceChips(c, &status, []mujica.ToggleItem[string]{ + core.Use(c, core.Settings{}) + input.ChoiceChips(c, &status, []input.ToggleItem[string]{ {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, - }, mujica.ChoiceChipsOptions{Multiple: true, Label: "Status"}) + }, input.ChoiceChipsOptions{Multiple: true, Label: "Status"}) } ``` diff --git a/docs/components/490-knob.md b/docs/components/490-knob.md index 0e77e1e..ee5db57 100644 --- a/docs/components/490-knob.md +++ b/docs/components/490-knob.md @@ -9,8 +9,8 @@ ```go gain := 35.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Knob(c, &gain, mujica.KnobOptions{Label: "Gain", Ticks: 11}) + core.Use(c, core.Settings{}) + input.Knob(c, &gain, input.KnobOptions{Label: "Gain", Ticks: 11}) } ``` diff --git a/docs/components/491-vertical-slider.md b/docs/components/491-vertical-slider.md index 6aee4e4..909d382 100644 --- a/docs/components/491-vertical-slider.md +++ b/docs/components/491-vertical-slider.md @@ -9,8 +9,8 @@ ```go level := 40.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VerticalSlider(c, &level, mujica.VerticalSliderOptions{Step: 10, Label: "Level"}) + core.Use(c, core.Settings{}) + input.VerticalSlider(c, &level, input.VerticalSliderOptions{Step: 10, Label: "Level"}) } ``` diff --git a/docs/components/492-color-palette.md b/docs/components/492-color-palette.md index 4ce538f..e0cfa1a 100644 --- a/docs/components/492-color-palette.md +++ b/docs/components/492-color-palette.md @@ -9,12 +9,12 @@ ```go color := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPalette(c, &color, []mujica.ColorSwatch{ + core.Use(c, core.Settings{}) + input.ColorPalette(c, &color, []input.ColorSwatch{ {Color: ui.Hex("#7D2034"), Name: "Wine"}, {Color: ui.Hex("#AD9164"), Name: "Old gold"}, {Color: ui.Hex("#355E80"), Name: "Slate blue"}, - }, mujica.ColorPaletteOptions{Custom: true, Label: "Tag color"}) + }, input.ColorPaletteOptions{Custom: true, Label: "Tag color"}) } ``` diff --git a/docs/components/493-signature-pad.md b/docs/components/493-signature-pad.md index bbd9b13..0fb01ed 100644 --- a/docs/components/493-signature-pad.md +++ b/docs/components/493-signature-pad.md @@ -7,11 +7,11 @@ ## 最小示例 ```go -var strokes [][]mujica.SignaturePoint +var strokes [][]input.SignaturePoint view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SignaturePad(c, &strokes, mujica.SignaturePadOptions{}).Changed() { - _ = mujica.SignaturePath(strokes) // SVG path data to store + core.Use(c, core.Settings{}) + if input.SignaturePad(c, &strokes, input.SignaturePadOptions{}).Changed() { + _ = input.SignaturePath(strokes) // SVG path data to store } } ``` diff --git a/docs/components/494-banner.md b/docs/components/494-banner.md index 688a2ad..4448743 100644 --- a/docs/components/494-banner.md +++ b/docs/components/494-banner.md @@ -9,9 +9,9 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Banner(c, &open, "Maintenance tonight at 22:00.", mujica.BannerOptions{ - Severity: mujica.SeverityWarning, + core.Use(c, core.Settings{}) + r := input.Banner(c, &open, "Maintenance tonight at 22:00.", input.BannerOptions{ + Severity: core.SeverityWarning, Action: "Details", }) if r.Action { diff --git a/docs/components/495-undo-toast.md b/docs/components/495-undo-toast.md index fe9fe22..43aa9bb 100644 --- a/docs/components/495-undo-toast.md +++ b/docs/components/495-undo-toast.md @@ -9,8 +9,8 @@ ```go open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UndoToast(c, &open, "Note deleted", mujica.UndoToastOptions{ + core.Use(c, core.Settings{}) + feedback.UndoToast(c, &open, "Note deleted", feedback.UndoToastOptions{ OnUndo: func() { // restore the note }, diff --git a/docs/components/496-masonry.md b/docs/components/496-masonry.md index 6925378..a8c65d8 100644 --- a/docs/components/496-masonry.md +++ b/docs/components/496-masonry.md @@ -9,8 +9,8 @@ ```go titles := []string{"Sea", "Forest", "Desert", "Glacier"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Masonry(c, len(titles), mujica.MasonryOptions{ + core.Use(c, core.Settings{}) + input.Masonry(c, len(titles), input.MasonryOptions{ Key: func(i int) string { return titles[i] }, }, func(i int) { ui.Text(c, titles[i]) diff --git a/docs/components/497-resizable-panel-group.md b/docs/components/497-resizable-panel-group.md index 6bc2bfe..d040089 100644 --- a/docs/components/497-resizable-panel-group.md +++ b/docs/components/497-resizable-panel-group.md @@ -9,11 +9,11 @@ ```go var sizes []float32 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ResizablePanelGroup(c, &sizes, []mujica.ResizablePanel{ + core.Use(c, core.Settings{}) + r := input.ResizablePanelGroup(c, &sizes, []input.ResizablePanel{ {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, - }, mujica.ResizablePanelGroupOptions{}) + }, input.ResizablePanelGroupOptions{}) if r.Committed { // save sizes } diff --git a/docs/components/498-dock-panel.md b/docs/components/498-dock-panel.md index 3f37046..d3ca659 100644 --- a/docs/components/498-dock-panel.md +++ b/docs/components/498-dock-panel.md @@ -7,13 +7,13 @@ ## 最小示例 ```go -var layout mujica.DockLayout +var layout input.DockLayout view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DockPanel(c, &layout, []mujica.DockItem{ + core.Use(c, core.Settings{}) + r := input.DockPanel(c, &layout, []input.DockItem{ {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, - }, mujica.DockPanelOptions{}) + }, input.DockPanelOptions{}) if r.Changed() { // save layout } diff --git a/docs/components/499-qr-code.md b/docs/components/499-qr-code.md index 4ec22d3..5b0d066 100644 --- a/docs/components/499-qr-code.md +++ b/docs/components/499-qr-code.md @@ -8,8 +8,8 @@ ```go view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QRCode(c, "https://example.com/mujica", mujica.QRCodeOptions{Level: mujica.QRQuartile}) + core.Use(c, core.Settings{}) + input.QRCode(c, "https://example.com/mujica", input.QRCodeOptions{Level: input.QRQuartile}) } ``` diff --git a/docs/components/500-activity-feed.md b/docs/components/500-activity-feed.md index 7af1758..3ac9302 100644 --- a/docs/components/500-activity-feed.md +++ b/docs/components/500-activity-feed.md @@ -7,12 +7,12 @@ ## 最小示例 ```go -events := []mujica.Activity{ +events := []feedback.Activity{ {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ActivityFeed(c, events, mujica.ActivityFeedOptions{}).Size(380, 280) + core.Use(c, core.Settings{}) + feedback.ActivityFeed(c, events, feedback.ActivityFeedOptions{}).Size(380, 280) } ``` diff --git a/docs/upstream.md b/docs/upstream.md index 8242b76..f3a7b8e 100644 --- a/docs/upstream.md +++ b/docs/upstream.md @@ -6,16 +6,16 @@ MyGo 已有能力纳入下列组件,不增加计数。 | MyGo 能力 | MujicaUI 组件 | 文件 | | --- | --- | --- | -| Primary button、Menu button | Button 主按钮变体;DropdownMenu | `button.go`、`dropdown_menu.go` | -| Stepper、Number input | NumberInput 仅步进模式与输入模式 | `number_input.go` | -| Search field、Find bar | SearchInput 搜索模式与查找栏模式 | `search_input.go` | -| Autocomplete、Combobox | Combobox 自由输入与选项选择 | `combobox.go` | -| Token field、Editable text | TagsInput;Text 的 `EditableText` | `tags_input.go`、`text.go` | -| List、Grid view | List 列表模式与网格模式 | `list.go` | -| Table、Outline、Tree | DataTable;TreeTable;Tree | `data_table.go`、`tree_table.go`、`tree.go` | -| Date input、Time input | DatePicker;TimePicker | `date_picker.go`、`time_picker.go` | -| Back and forward buttons | NavigationMenu 历史导航 | `navigation_menu.go` | -| Meter、Progress bar | Progress 计量模式与进度模式 | `progress.go` | +| Primary button、Menu button | Button 主按钮变体;DropdownMenu | `input/button.go`、`overlay/dropdown_menu.go` | +| Stepper、Number input | NumberInput 仅步进模式与输入模式 | `input/number_input.go` | +| Search field、Find bar | SearchInput 搜索模式与查找栏模式 | `input/search_input.go` | +| Autocomplete、Combobox | Combobox 自由输入与选项选择 | `input/combobox.go` | +| Token field、Editable text | TagsInput;Text 的 `EditableText` | `input/tags_input.go`、`display/text.go` | +| List、Grid view | List 列表模式与网格模式 | `data/list.go` | +| Table、Outline、Tree | DataTable;TreeTable;Tree | `data/data_table.go`、`data/tree_table.go`、`data/tree.go` | +| Date input、Time input | DatePicker;TimePicker | `datetime/date_picker.go`、`datetime/time_picker.go` | +| Back and forward buttons | NavigationMenu 历史导航 | `navigation/navigation_menu.go` | +| Meter、Progress bar | Progress 计量模式与进度模式 | `feedback/progress.go` | ## 上游缺口(待提交 MyGo) diff --git a/example_account_test.go b/example_account_test.go deleted file mode 100644 index 3292bbe..0000000 --- a/example_account_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleSettingsLayout() { - page := "" - sound := true - pages := []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SettingsLayout(c, &page, pages, mujica.SettingsLayoutOptions{}, func(id string) { - mujica.SettingsSection(c, "Notifications", func() { - mujica.SettingsRow(c, "Sound", mujica.SettingsRowOptions{ID: "sound"}, func() *ui.Element { - return mujica.Switch(c, &sound, "", mujica.SwitchOptions{}) - }) - }) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSettingsSearch() { - query, page := "", "general" - entries := []mujica.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if e, ok := mujica.SettingsSearch(c, &query, entries, mujica.SettingsSearchOptions{}).Chosen(); ok { - page = e.Page - } - fmt.Println("page", page) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleThemeSelector() { - mode := mujica.System - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) - mujica.ThemeSelector(c, &mode, mujica.ThemeSelectorOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAccentColorPicker() { - accent := ui.Hex("#7D2034") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AccentColorPicker(c, &accent, mujica.AccentColorPickerOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleShortcutRecorder() { - palette := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShortcutRecorder(c, &palette, mujica.ShortcutRecorderOptions{ - Label: "Command palette", - Taken: []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleKeyboardShortcutsList() { - query := "" - groups := []mujica.ShortcutGroup{{Title: "General", Bindings: []mujica.ShortcutBinding{ - {Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, - }}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.KeyboardShortcutsList(c, &query, groups, mujica.KeyboardShortcutsListOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLoginForm() { - var state mujica.LoginFormState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LoginForm(c, &state, mujica.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { - // Submit credentials through the caller-provided auth service. - state.EndSubmit("") - }}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOAuthButtons() { - providers := []mujica.OAuthProvider{{ID: "sso", Name: "Company SSO"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{}).Chosen(); ok { - fmt.Println("start sign-in with", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTwoFactorInput() { - code := "" - resendAt := time.Now().Add(30 * time.Second) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TwoFactorInput(c, &code, mujica.TwoFactorInputOptions{ResendAt: resendAt}) - if r.Completed() { - fmt.Println("verify the code") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleProfileEditor() { - saved := mujica.Profile{Name: "Sakiko", Bio: "Keyboard."} - profile := saved - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ProfileEditor(c, &profile, mujica.ProfileEditorOptions{Saved: saved}).Save() { - saved = profile - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAccountSwitcher() { - current := "a" - accounts := []mujica.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AccountSwitcher(c, ¤t, accounts, mujica.AccountSwitcherOptions{}).Add() { - fmt.Println("sign in another account") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWorkspaceSwitcher() { - current := "ave" - spaces := []mujica.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkspaceSwitcher(c, ¤t, spaces, mujica.WorkspaceSwitcherOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleUserMenu() { - status := mujica.UserStatusOnline - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.UserMenu(c, &status, mujica.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { - fmt.Println("sign out") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSessionList() { - sessions := []mujica.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.SessionList(c, sessions, mujica.SessionListOptions{}).Revoked(); ok { - fmt.Println("sign out", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleApiKeyManager() { - var state mujica.ApiKeyManagerState - keys := []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if name, ok := mujica.ApiKeyManager(c, &state, keys, mujica.ApiKeyManagerOptions{}).Create(); ok { - fmt.Println("create", name) - state.Reveal("secret from your key service") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleUsageQuota() { - items := []mujica.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UsageQuota(c, items, mujica.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOnboardingWizard() { - step := 0 - steps := []mujica.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.OnboardingWizard(c, &step, steps, mujica.OnboardingWizardOptions{}).Finished() { - fmt.Println("onboarded") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCoachmark() { - open, step := true, 0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - compose := mujica.Button(c, "Compose", mujica.ButtonOptions{}) - mujica.Coachmark(c, &open, &step, []mujica.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWhatsNewDialog() { - open, hide := true, false - releases := []mujica.Release{{Version: "1.1", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "500 components."}}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WhatsNewDialog(c, &open, &hide, releases, mujica.WhatsNewDialogOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFeedbackWidget() { - var state mujica.FeedbackWidgetState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if f, ok := mujica.FeedbackWidget(c, &state, mujica.FeedbackWidgetOptions{}).Submitted(); ok { - fmt.Println("send", f.Kind) - state.EndSubmit("") - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_agent_test.go b/example_agent_test.go deleted file mode 100644 index 0d17a41..0000000 --- a/example_agent_test.go +++ /dev/null @@ -1,243 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleToolCallCard() { - call := mujica.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", - Result: "{\"lines\":40}", State: mujica.AgentDone, Duration: 420 * time.Millisecond} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{OpenArgs: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToolCallGroup() { - open := false - calls := []mujica.ToolCall{ - {ID: "a", Name: "list_dir", State: mujica.AgentDone, Duration: 300 * time.Millisecond}, - {ID: "b", Name: "read_file", State: mujica.AgentFailed, Error: "not found"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallGroup(c, &open, calls, mujica.ToolCallGroupOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToolApprovalDialog() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolApprovalDialog(c, &open, mujica.ToolApprovalOptions{ - Tool: "delete_files", Args: "{\"glob\":\"*.tmp\"}", Reason: "Clean the build folder.", - }) - if r.Allowed { - fmt.Println("run the tool") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePermissionPrompt() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PermissionPrompt(c, &open, mujica.PermissionPromptOptions{ - Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger", - }) - if r.Choice == mujica.PermissionAllowAlways { - fmt.Println("remember the grant") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAgentStepList() { - steps := []mujica.AgentStep{ - {ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: time.Second}, - {ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Detail: "Table 4 is full."}, - {ID: "send", Title: "Send invitations", State: mujica.AgentPending}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.AgentStepList(c, steps, mujica.AgentStepListOptions{}); r.Retry != "" { - fmt.Println("retry", r.Retry) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAgentPlan() { - items := []mujica.PlanItem{ - {ID: "a", Text: "Read the guest list", State: mujica.AgentDone}, - {ID: "b", Text: "Assign the seats", State: mujica.AgentRunning}, - {ID: "c", Text: "Order the flowers"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentPlan(c, &items, mujica.AgentPlanOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAgentStatus() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentStatus(c, mujica.AgentWaiting, mujica.AgentStatusOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAgentProgress() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentProgress(c, 0.4, mujica.AgentProgressOptions{Action: "Reading ledger.md"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSubAgentTree() { - agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", State: mujica.AgentRunning, Children: []mujica.SubAgent{ - {ID: "cook", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting}, - }}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFileChangeCard() { - decision := mujica.FilePending - change := mujica.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FileChangeCard(c, &decision, change, mujica.FileChangeCardOptions{}).OpenDiff { - fmt.Println("open the full diff") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMultiFileDiffReview() { - files := []mujica.FileChange{ - {Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"}, - {Path: "menu.md", Old: "soup\n", New: "soup\ntart\n"}, - } - decisions := make([]mujica.FileDecision, len(files)) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiFileDiffReview(c, &decisions, files, mujica.MultiFileDiffReviewOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommandExecutionCard() { - run := mujica.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CommandExecutionCard(c, run, mujica.CommandExecutionCardOptions{}).Changed() { - fmt.Println("abort the process") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleScreenshotStream() { - var st mujica.ScreenshotStreamState - var frames []mujica.ScreenshotFrame // appended as the agent captures them - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenshotStream(c, &st, frames, mujica.ScreenshotStreamOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleArtifactPanel() { - art := mujica.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactPanel(c, art, mujica.ArtifactPanelOptions{}).Exported { - fmt.Println("save the file") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleArtifactVersionSwitcher() { - current := 1 - versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactVersionSwitcher(c, ¤t, versions, mujica.ArtifactVersionSwitcherOptions{}).Compare { - fmt.Println("diff v1 and v2") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSandboxStatus() { - info := mujica.SandboxInfo{Name: "sandbox-7", State: mujica.SandboxRunning, - Resources: []mujica.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SandboxStatus(c, info, mujica.SandboxStatusOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMCPServerList() { - servers := []mujica.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MCPServerList(c, servers, mujica.MCPServerListOptions{}); r.Action == mujica.MCPStop { - fmt.Println("stop", r.Server) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMemoryPanel() { - entries := []mujica.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemoryPanel(c, &entries, mujica.MemoryPanelOptions{}); r.Deleted != "" { - fmt.Println("forget", r.Deleted) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCheckpointList() { - current := "c2" - points := []mujica.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CheckpointList(c, ¤t, points, mujica.CheckpointListOptions{}).Changed() { - fmt.Println("restore", current) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHumanInputRequest() { - answer := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.HumanInputRequest(c, &answer, mujica.HumanInputRequestOptions{ - Question: "Which hall?", Options: []string{"East wing", "Great hall"}, AllowFree: true, - }) - if r.Submitted { - fmt.Println("reply", answer) - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_calendar_test.go b/example_calendar_test.go deleted file mode 100644 index badbc93..0000000 --- a/example_calendar_test.go +++ /dev/null @@ -1,262 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func exampleEvents(loc *time.Location) []mujica.CalendarEvent { - start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ - {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, - } -} - -func ExampleCalendarMonthView() { - loc := time.Local - events := exampleEvents(loc) - month := mujica.YearMonth{Year: 2026, Month: time.October} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarMonthView(c, &month, events, mujica.CalendarMonthViewOptions{Location: loc}) - if ch, ok := r.Changed(); ok { - fmt.Println("move", ch.ID, "to", ch.Start) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCalendarWeekView() { - loc := time.Local - events := exampleEvents(loc) - week := mujica.Date{Year: 2026, Month: time.October, Day: 14} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarWeekView(c, &week, events, mujica.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) - if ch, ok := r.Changed(); ok && ch.Kind == mujica.CalendarCreate { - fmt.Println("new event", ch.Start, ch.End) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCalendarDayView() { - loc := time.Local - events := exampleEvents(loc) - day := mujica.Date{Year: 2026, Month: time.October, Day: 14} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.CalendarDayView(c, &day, events, mujica.CalendarDayViewOptions{Location: loc}).Opened(); ok { - fmt.Println("open", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAgendaView() { - loc := time.Local - events := exampleEvents(loc) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.AgendaViewOptions{Location: loc, From: mujica.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} - if id, ok := mujica.AgendaView(c, events, opts).Opened(); ok { - fmt.Println("open", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleYearView() { - loc := time.Local - events := exampleEvents(loc) - year := 2026 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if day, ok := mujica.YearView(c, &year, events, mujica.YearViewOptions{Location: loc}).DayChosen(); ok { - fmt.Println("go to", day) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleEventChip() { - loc := time.Local - ev := exampleEvents(loc)[0] - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}).Clicked() { - fmt.Println("open", ev.ID) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleEventEditor() { - loc := time.Local - ev := exampleEvents(loc)[0] - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EventEditor(c, &ev, mujica.EventEditorOptions{Location: loc}) - if r.Saved() { - fmt.Println("saved", ev.Title, ev.Start) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleEventPopover() { - loc := time.Local - ev := exampleEvents(loc)[0] - open := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}) - if chip.Clicked() { - open = true - } - r := mujica.EventPopover(c, chip, &open, ev, mujica.EventPopoverOptions{Location: loc}) - if r.Delete() { - fmt.Println("delete", ev.ID) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCurrentTimeIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Box(c).Size(200, 240).Children(func() { - mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRecurrenceEditor() { - rule := mujica.Recurrence{Unit: mujica.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RecurrenceEditor(c, &rule, mujica.RecurrenceEditorOptions{Start: mujica.Date{Year: 2026, Month: time.October, Day: 5}}) - if r.Changed() && r.Problem() == "" { - fmt.Println(mujica.RecurrenceSummary(c, rule)) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAvailabilityPicker() { - days := []mujica.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} - mine := [][]bool{make([]bool, 18), make([]bool, 18)} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AvailabilityPicker(c, &mine, mujica.AvailabilityPickerOptions{Days: days}).Changed() { - fmt.Println("availability changed") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTimezoneSelect() { - zone := "Asia/Shanghai" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimezoneSelect(c, &zone, mujica.TimezoneSelectOptions{}).Changed() { - fmt.Println("zone", zone) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleReminderPicker() { - reminders := []time.Duration{15 * time.Minute} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ReminderPicker(c, &reminders, mujica.ReminderPickerOptions{Max: 3}).Changed() { - fmt.Println("reminders", reminders) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAttendeeList() { - people := []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if i, ok := mujica.AttendeeList(c, people, mujica.AttendeeListOptions{Removable: true}).Removed(); ok { - people = append(people[:i], people[i+1:]...) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWeekPicker() { - var week mujica.Date - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.WeekPicker(c, &week, mujica.WeekPickerOptions{WeekStart: mujica.WeekStartMonday, Clearable: true}).Changed() { - fmt.Println("week of", week) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDurationPicker() { - length := 90 * time.Minute - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} - if mujica.DurationPicker(c, &length, opts).Changed() { - fmt.Println("length", length) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCronEditor() { - expr := "0 9 * * MON-FRI" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CronEditor(c, &expr, mujica.CronEditorOptions{Location: time.Local}).Changed() { - if _, err := mujica.ParseCron(expr); err == nil { - fmt.Println("schedule", expr) - } - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRelativeTime() { - posted := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.Local) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RelativeTime(c, posted, mujica.RelativeTimeOptions{Location: time.Local}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCountdown() { - deadline := time.Date(2026, time.December, 31, 23, 59, 59, 0, time.Local) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Countdown(c, deadline, mujica.CountdownOptions{}).Finished() { - fmt.Println("happy new year") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleStopwatch() { - var sw mujica.StopwatchState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Stopwatch(c, &sw, mujica.StopwatchOptions{}).Changed() { - fmt.Println("laps", len(sw.Laps)) - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_charts1_test.go b/example_charts1_test.go deleted file mode 100644 index 90433bb..0000000 --- a/example_charts1_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package mujica_test - -import ( - "math" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleLineChart() { - series := []mujica.ChartSeries{ - {Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}}, - {Name: "Candles", Values: []float64{5, 2, 6, 7, 1}}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineChart(c, series, mujica.LineChartOptions{Frame: mujica.ChartFrame{Label: "Chapel"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAreaChart() { - series := []mujica.ChartSeries{ - {Name: "Matins", Values: []float64{3, 4, 6, 5}}, - {Name: "Vespers", Values: []float64{2, 3, 2, 4}}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: mujica.ChartFrame{Label: "Attendance"}, Stacked: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBarChart() { - categories := []string{"Nave", "Choir", "Crypt"} - series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarChart(c, categories, series, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Tithes"}, Labels: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePieChart() { - slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: mujica.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleScatterChart() { - var selected []mujica.ScatterKey - groups := []mujica.ScatterGroup{{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScatterChart(c, &selected, groups, mujica.ScatterChartOptions{Frame: mujica.ChartFrame{Label: "Choir"}, Bubbles: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRadarChart() { - axes := []string{"Voice", "Latin", "Chant", "Organ"} - series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RadarChart(c, axes, series, mujica.RadarChartOptions{Frame: mujica.ChartFrame{Label: "Skills"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHeatmapChart() { - rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"} - values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: mujica.ChartFrame{Label: "Hours"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCalendarHeatmap() { - year := 2026 - values := map[mujica.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CalendarHeatmap(c, &year, values, mujica.CalendarHeatmapOptions{Frame: mujica.ChartFrame{Label: "Prayers"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHistogram() { - values := []float64{1, 2, 2, 3, 4, 5, 9, 10} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Histogram(c, values, mujica.HistogramOptions{Frame: mujica.ChartFrame{Label: "Ages"}, BinWidth: 5}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBoxPlot() { - groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWaterfallChart() { - steps := []mujica.WaterfallStep{ - {Label: "Tithes", Value: 12}, - {Label: "Repairs", Value: -5}, - {Label: "Year", Kind: mujica.WaterfallTotal}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: mujica.ChartFrame{Label: "Ledger"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFunnelChart() { - stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: mujica.ChartFrame{Label: "Order"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBulletChart() { - target := 250.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BulletChart(c, 270, mujica.BulletChartOptions{Frame: mujica.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSparkline() { - values := []float64{3, 5, 4, 7, 6, 9} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: mujica.ChartFrame{Label: "Bells"}, Kind: mujica.SparklineArea}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleGauge() { - segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Gauge(c, 72, mujica.GaugeOptions{Frame: mujica.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTreemap() { - var path []string - root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, - {Name: "Kitchen", Value: 25}, - }} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Treemap(c, &path, root, mujica.TreemapOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSunburst() { - var path []string - root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, - {Name: "Kitchen", Value: 25}, - }} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sunburst(c, &path, root, mujica.SunburstOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSankeyChart() { - nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} - links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: mujica.ChartFrame{Label: "Money"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleNetworkGraph() { - var state mujica.NetworkGraphState - nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} - edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.NetworkGraph(c, &state, nodes, edges, mujica.NetworkGraphOptions{Frame: mujica.ChartFrame{Label: "House"}, Force: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRealtimeChart() { - buf := mujica.NewRealtimeBuffer(600, "Bell") - start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - for i := range 60 { - buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7)) - } - var state mujica.RealtimeChartState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RealtimeChart(c, &state, buf, mujica.RealtimeChartOptions{Frame: mujica.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_charts2_test.go b/example_charts2_test.go deleted file mode 100644 index b568723..0000000 --- a/example_charts2_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package mujica_test - -import ( - "github.com/egoist/mygo/ui" - mujica "mujicaui" -) - -func ExampleChartLegend() { - hidden := map[string]bool{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - items := []mujica.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} - mujica.ChartLegend(c, &hidden, items, mujica.ChartLegendOptions{MaxItems: 1}) - } - ui.Render(view, 400, 200, 1) -} - -func ExampleChartTooltip() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - mujica.ChartTooltip(c, 40, 40, []mujica.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) - } - ui.Render(view, 400, 200, 1) -} - -func ExampleChartCrosshair() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y}) - ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { - plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} - x.Draw(p, plot) - y.Draw(p, plot) - hair.Draw(p, plot, x.Map(plot, 5), y.Map(plot, 70)) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChartAxis() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - axis := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Kind: mujica.AxisLog, Min: 1, Max: 1000}) - ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { - axis.Draw(p, ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChartGrid() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - grid := mujica.ChartGrid(c, mujica.ChartGridOptions{X: &x, Y: &y, Minor: 2}) - ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { - plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} - grid.Draw(p, plot) - x.Draw(p, plot) - y.Draw(p, plot) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChartBrush() { - value := [2]float64{20, 60} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartBrush(c, &value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) - } - ui.Render(view, 400, 160, 1) -} - -func ExampleChartAnnotation() { - notes := []mujica.ChartNote{{Kind: mujica.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y}) - box := ui.Box(c).Fill().Focusable().Label("Annotations") - b := box.Bounds() - annotation.Drag(box, ui.Rect{X: 60, Y: 20, W: b.W - 80, H: b.H - 60}) - box.Draw(func(p *ui.Painter, r ui.Rect) { - plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} - x.Draw(p, plot) - y.Draw(p, plot) - annotation.Draw(p, plot) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChartExport() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - data, err := mujica.ChartExport(c, func(c *ui.Context) { - mujica.SparkBar(c, []float64{3, -2, 5}, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}}) - }, mujica.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) - if err != nil { - panic(err) - } - bitmap, err := ui.DecodeBitmap(data) - if err != nil { - panic(err) - } - ui.Image(c, bitmap).Size(300, 100) - } - ui.Render(view, 400, 160, 1) -} - -func ExampleSparkBar() { - values := []float64{3, -2, 4, 1, -3} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SparkBar(c, values, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}, BarWidth: 10}) - } - ui.Render(view, 400, 100, 1) -} - -func ExampleTrendIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TrendIndicator(c, 12.5, mujica.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) - } - ui.Render(view, 400, 100, 1) -} - -func ExampleChartEmptyState() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartEmptyState(c, mujica.DataFailed, mujica.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) - } - ui.Render(view, 400, 240, 1) -} - -func ExampleViolinPlot() { - groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleParallelCoordinates() { - filters := map[string][2]float64{} - axes := []string{"Age", "Hymns", "Vigils"} - rows := [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ParallelCoordinates(c, &filters, axes, rows, mujica.ParallelCoordinatesOptions{Frame: mujica.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChordDiagram() { - names := []string{"Nave", "Choir", "Crypt"} - matrix := [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: mujica.ChartFrame{Label: "Paths"}}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDensityPlot() { - groups := []mujica.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: mujica.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_chatinput_test.go b/example_chatinput_test.go deleted file mode 100644 index 36e691c..0000000 --- a/example_chatinput_test.go +++ /dev/null @@ -1,241 +0,0 @@ -package mujica_test - -import ( - "fmt" - "strings" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExamplePromptComposer() { - var prompt string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{Placeholder: "Ask the herald"}) - if r.Submitted() { - fmt.Println("send", prompt) - prompt = "" - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAttachmentChip() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AttachmentChip(c, mujica.Attachment{Name: "guests.csv", Size: 4096, Uploading: true, Progress: 0.4}) - if r.Removed() { - fmt.Println("cancel the upload") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePasteImagePreview() { - pic := ui.MustParseSVG([]byte(``)) - images := []mujica.PastedImage{{ID: "p1", Source: pic, Name: "screenshot"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.PasteImagePreview(c, images).Removed(); ok { - fmt.Println("remove", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDragDropOverlay() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - images := func(path string) bool { return strings.HasSuffix(path, ".png") } - r := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: images}, func() { - ui.Text(c, "Drag images into the chat") - }) - for _, path := range r.Accepted() { - fmt.Println("attach", path) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVoiceInputButton() { - var rec mujica.VoiceInputOptions - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.VoiceInputButton(c, rec).Changed() { - rec = mujica.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()} - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVoiceWaveform() { - levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSlashCommandMenu() { - var prompt string - commands := []mujica.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - composer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{}) - if cmd, ok := mujica.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok { - fmt.Println("run", cmd.Name) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleContextMentionMenu() { - var prompt string - var found []mujica.ContextItem - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - composer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{}) - r := mujica.ContextMentionMenu(c, composer, &prompt, found, mujica.ContextMentionOptions{}) - if r.QueryChanged() { - found = []mujica.ContextItem{{ID: "f1", Label: r.Query() + ".md", Kind: mujica.ContextFile}} - } - if item, ok := r.Chosen(); ok { - fmt.Println("attach", item.ID) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleContextChips() { - items := []mujica.ContextItem{{ID: "f1", Label: "guests.csv", Kind: mujica.ContextFile}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.ContextChips(c, items, mujica.ContextChipsOptions{}).Removed(); ok { - fmt.Println("detach", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleModelSelector() { - model := "herald" - models := []mujica.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision"}, ContextTokens: 200000}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ModelSelector(c, &model, models, mujica.ModelSelectorOptions{}).Changed() { - fmt.Println("model", model) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleModeSelector() { - mode := mujica.ModeChat - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ModeSelector(c, &mode, mujica.ModeSelectorOptions{}).Changed() { - fmt.Println("mode", mode) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToolToggleMenu() { - web := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - tools := []mujica.ToolItem{{ID: "web", Label: "Web search", On: &web}} - if id, ok := mujica.ToolToggleMenu(c, tools).Changed(); ok { - fmt.Println("toggled", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleParameterPanel() { - temperature := 1.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - params := []mujica.ModelParam{{Label: "Temperature", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}} - mujica.ParameterPanel(c, params) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTokenCounter() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TokenCounter(c, 1234, 8000) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleContextWindowMeter() { - segments := []mujica.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: 58000}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ContextWindowMeter(c, segments, 128000, mujica.ContextWindowMeterOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSendButton() { - streaming := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: streaming, Shortcut: "Enter"}) - if r.Sent() || r.Stopped() { - streaming = r.Sent() - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCitationBadge() { - var cites mujica.CitationState - src := mujica.Source{Title: "The Palace of Versailles", URL: "https://example.com/versailles"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CitationBadge(c, 1, src, &cites).Changed() { - fmt.Println("show source", cites.Active) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSourceCard() { - var cites mujica.CitationState - src := mujica.Source{Title: "Hall of Mirrors", URL: "https://example.org/hall", Snippet: "Seventeen arches."} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SourceCard(c, 1, src, &cites).Clicked() { - fmt.Println("open", src.URL) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSourcesPanel() { - var cites mujica.CitationState - sources := []mujica.Source{{Title: "Hall of Mirrors", URL: "https://example.org/hall"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if n, ok := mujica.SourcesPanel(c, sources, &cites).Opened(); ok { - fmt.Println("open", sources[n-1].URL) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSearchProgress() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - stages := []mujica.SearchStage{{Label: "Searching the web", Done: true}, {Label: "Reading sources"}} - mujica.SearchProgress(c, mujica.SearchProgressOptions{Query: "Versailles", Read: 3, Total: 8, Stages: stages}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_chatsession_test.go b/example_chatsession_test.go deleted file mode 100644 index 53a5849..0000000 --- a/example_chatsession_test.go +++ /dev/null @@ -1,231 +0,0 @@ -package mujica_test - -import ( - "log" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" - "mujicaui/icons" -) - -func ExampleConversationList() { - var state mujica.ConversationListState - convs := []mujica.ChatConversation{ - {ID: "1", Title: "Court etiquette", Updated: time.Now()}, - {ID: "2", Title: "Royal letters", Updated: time.Now().AddDate(0, 0, -30), Pinned: true}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConversationList(c, &state, convs, mujica.ConversationListOptions{Label: "History"}, nil).Changed() { - log.Println("open", state.Selected) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleConversationItem() { - conv := mujica.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{}) - if v.Deleted { - log.Println("delete", conv.ID) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleConversationSearch() { - query := "seat" - hits := []mujica.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationSearch(c, &query, hits, mujica.ConversationSearchOptions{}) - if v.QueryChanged { - log.Println("search", query) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleProjectList() { - var state mujica.ProjectListState - projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} - convs := func(id string) []mujica.ChatConversation { - return []mujica.ChatConversation{{ID: "1", Title: "Menu"}} - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ProjectList(c, &state, projects, convs, mujica.ProjectListOptions{}); v.Moved { - log.Println("move", v.MoveID, "to", v.MoveTo) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleProjectKnowledgePanel() { - files := []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ProjectKnowledgePanel(c, files, mujica.ProjectKnowledgePanelOptions{Capacity: 200000}) - if v.Add { - log.Println("pick files") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWelcomeScreen() { - cards := []mujica.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { - log.Println("ask", v.Example) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSuggestionChips() { - input := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePromptLibrary() { - var state mujica.PromptLibraryState - templates := []mujica.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.PromptLibrary(c, &state, templates, mujica.PromptLibraryOptions{}); v.Inserted != "" { - log.Println("insert", v.Inserted) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCapabilityCards() { - cards := []mujica.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.CapabilityCards(c, cards, mujica.CapabilityCardsOptions{}); v.Example != "" { - log.Println("ask", v.Example) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFeedbackForm() { - var value mujica.FeedbackValue - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FeedbackForm(c, &value, mujica.FeedbackFormOptions{Rating: mujica.FeedbackDown}).Submitted { - log.Println("reasons", value.Reasons) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDateSeparator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DateSeparator(c, mujica.DateOf(time.Now()), mujica.DateSeparatorOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleThinkingIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: "Searching the archives"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleStopGeneratingButton() { - generating := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: generating}).Changed() { - generating = false - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSystemPromptEditor() { - prompt, saved := "You are helpful.", "You are helpful." - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemPromptEditor(c, &prompt, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { - saved = prompt - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCostEstimator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSharedConversationView() { - msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}, {Role: mujica.SessionAssistant, Text: "Good day."}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SharedConversationView(c, msgs, mujica.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { - c.WriteClipboard("https://example.com/share/1") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleConversationExport() { - var state mujica.ConversationExportState - msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ConversationExport(c, &state, msgs, mujica.ConversationExportOptions{Title: "Greeting"}); v.Exported { - log.Println(len(v.Content), "bytes to save") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRegenerateMenu() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { - log.Println("retry", v.Choice.Kind, v.Choice.Model, v.Choice.Variant) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleArtifactCard() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactCard(c, mujica.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { - log.Println("open artifact panel") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChatEmptyState() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyOffline}).Action { - log.Println("reconnect") - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_choice_test.go b/example_choice_test.go deleted file mode 100644 index af5eae2..0000000 --- a/example_choice_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package mujica_test - -import ( - "log" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" - "mujicaui/icons" -) - -func ExampleSwitch() { - wifi := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Switch(c, &wifi, "Wi-Fi", mujica.SwitchOptions{}).Changed() { - log.Println("Wi-Fi:", wifi) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToggle() { - bold := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Toggle(c, &bold, "", mujica.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToggleGroup() { - align := []string{"left"} - items := []mujica.ToggleItem[string]{ - {Value: "left", Label: "Left"}, - {Value: "center", Label: "Center"}, - {Value: "right", Label: "Right"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToggleGroup(c, &align, items, mujica.ToggleGroupOptions{Label: "Align"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSlider() { - volume := 40.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Slider(c, &volume, mujica.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRangeSlider() { - low, high := 20.0, 80.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RangeSlider(c, &low, &high, mujica.RangeSliderOptions{Step: 10, Label: "Price"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSelect() { - plan := "" - options := []mujica.SelectOption[string]{ - {Value: "free", Label: "Free"}, - {Value: "pro", Label: "Pro"}, - {Value: "team", Label: "Team", Disabled: true}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Select(c, &plan, options, mujica.SelectOptions{Clearable: true, Label: "Plan"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMultiSelect() { - tags := []int{1} - options := []mujica.SelectOption[int]{ - {Value: 1, Label: "Rose"}, - {Value: 2, Label: "Lily"}, - {Value: 3, Label: "Iris"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiSelect(c, &tags, options, mujica.MultiSelectOptions{Max: 2, Label: "Flowers"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCombobox() { - font := "" - fonts := []string{"Garamond", "Georgia", "Didot"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Combobox(c, &font, fonts, mujica.ComboboxOptions{AllowCustom: true, Label: "Font"}) - if r.QueryChanged() { - log.Println("search:", r.Query()) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCascader() { - var path []string - level := func(p []string) mujica.CascaderLevel { - if len(p) == 0 { - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "fr", Label: "France"}}} - } - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Cascader(c, &path, mujica.CascaderOptions{Level: level, Label: "City"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTreeSelect() { - var roles []string - nodes := []mujica.TreeSelectNode{ - {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ - {ID: "king", Label: "King"}, - {ID: "queen", Label: "Queen"}, - }}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeSelect(c, &roles, nodes, mujica.TreeSelectOptions{Multiple: true, Label: "Roles"}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_code_test.go b/example_code_test.go deleted file mode 100644 index 10451d2..0000000 --- a/example_code_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package mujica_test - -import ( - "fmt" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleTerminal() { - var term mujica.TerminalState - term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{ - Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, - Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, - }).Size(380, 200) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTerminalTabs() { - tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} - active := "a" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { - ui.Text(c, "terminal "+id) - }) - if r.Closed != "" { - fmt.Println("close pty", r.Closed) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTerminalSearch() { - var term mujica.TerminalState - term.Write("build ok\r\nerror: missing seal\r\n") - var search mujica.TerminalSearchState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Column(c).Gap(8).Children(func() { - mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommandHistory() { - history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} - var state mujica.CommandHistoryState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) - r.Element.Height(240) - if r.Replayed != nil { - fmt.Println("run", r.Replayed.Command) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLogViewer() { - lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} - var state mujica.LogViewerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleProcessList() { - procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} - var state mujica.ProcessListState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) - r.Element.Height(240) - if r.End != nil { - fmt.Println("kill", r.End.PID) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCodeViewer() { - lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} - var state mujica.CodeViewerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMinimap() { - lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} - view := mujica.MinimapView{Top: 0, Visible: 2} - draw := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { - fmt.Println("scroll editor to", view.Top) - } - } - ui.Render(draw, 400, 300, 1) -} - -func ExampleFindWidget() { - text := "the seal and the seal" - state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { - fmt.Println(text) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSearchPanel() { - results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} - state := mujica.SearchPanelState{Query: "seal"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) - r.Element.Height(280) - if r.Opened != nil { - fmt.Println("open", r.Opened.Path, r.Opened.Line) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleProblemsPanel() { - problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} - var state mujica.ProblemsPanelState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) - r.Element.Height(240) - if r.Opened != nil { - fmt.Println("go to", r.Opened.File, r.Opened.Line) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOutputPanel() { - channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} - var state mujica.OutputPanelState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) - r.Element.Height(240) - if r.Cleared != "" { - channels[state.Channel].Lines = nil - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSymbolOutline() { - symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ - {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, - }}} - var state mujica.SymbolOutlineState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) - r.Element.Height(240) - if r.Jumped != nil { - fmt.Println("go to line", r.Jumped.Line) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCompletionMenu() { - text := "se" - open := true - items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} - var state mujica.CompletionMenuState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) - if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { - text = r.Accepted.Label - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDebugToolbar() { - state := mujica.DebugPaused - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { - case mujica.DebugContinue: - state = mujica.DebugRunning - case mujica.DebugStop: - state = mujica.DebugStopped - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBreakpointList() { - bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) - r.Element.Height(200) - if r.Toggled != "" || r.Edited != "" { - fmt.Println("sync breakpoints with the debugger") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCallStack() { - threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ - {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, - {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, - }}} - state := mujica.CallStackState{Current: "f1"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) - r.Element.Height(200) - if r.Changed() { - fmt.Println("show frame", state.Current) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVariablesPanel() { - vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} - var state mujica.VariablesPanelState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) - r.Element.Height(200) - if r.Edited != nil { - fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHexViewer() { - data := []byte("MUJICA court ledger\x00\x01") - var state mujica.HexViewerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFlamegraph() { - root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ - {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, - }} - var state mujica.FlamegraphState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_datetime_test.go b/example_datetime_test.go deleted file mode 100644 index be9afcc..0000000 --- a/example_datetime_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleCalendar() { - day := mujica.Date{Year: 2026, Month: time.October, Day: 5} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Calendar(c, &day, mujica.CalendarOptions{WeekStart: mujica.WeekStartMonday}).Changed() { - fmt.Println("chosen", day) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDatePicker() { - due := mujica.Date{Year: 2026, Month: time.October, Day: 5} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DatePickerOptions{Label: "Due date", Clearable: true, - Min: mujica.Date{Year: 2026, Month: time.January, Day: 1}} - if mujica.DatePicker(c, &due, opts).Changed() { - fmt.Println("due", due) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDateRangePicker() { - var stay mujica.DateRange - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DateRangePicker(c, &stay, mujica.DateRangePickerOptions{Label: "Stay"}) - if r.Changed() { - fmt.Println("stay", stay.Start, stay.End, stay.Days()) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTimePicker() { - alarm := mujica.TimeOfDay{Hour: 7, Minute: 30} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} - if mujica.TimePicker(c, &alarm, opts).Changed() { - fmt.Println("alarm", alarm) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTimeRangePicker() { - shift := mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 22}, End: mujica.TimeOfDay{Hour: 6}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} - if mujica.TimeRangePicker(c, &shift, opts).Changed() { - fmt.Println("shift", shift.Start, shift.End, shift.Overnight()) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDateTimePicker() { - tokyo, err := time.LoadLocation("Asia/Tokyo") - if err != nil { - panic(err) - } - var meeting time.Time - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} - if mujica.DateTimePicker(c, &meeting, opts).Changed() { - fmt.Println("meeting", meeting) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMonthPicker() { - var month mujica.YearMonth - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: true} - if mujica.MonthPicker(c, &month, opts).Changed() { - fmt.Println("month", month) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleYearPicker() { - year := 2026 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} - if mujica.YearPicker(c, &year, opts).Changed() { - fmt.Println("year", year) - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_devtools_test.go b/example_devtools_test.go deleted file mode 100644 index 4105351..0000000 --- a/example_devtools_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleStatCard() { - value := "12,480" - opts := mujica.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatCard(c, value, opts).Changed() { - fmt.Println("retry") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleAlertList() { - alerts := []mujica.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: mujica.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AlertList(c, alerts, mujica.AlertListOptions{}) - if r.Ack != "" { - fmt.Println("ack", r.Ack) - } - if r.Silence != "" { - fmt.Println("silence", r.Silence) - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleIncidentCard() { - incident := mujica.Incident{Title: "API latency", Status: mujica.IncidentMonitoring, Impact: mujica.ImpactMinor, Updates: []mujica.IncidentUpdate{{Status: mujica.IncidentIdentified, Text: "Slow query located"}, {Status: mujica.IncidentMonitoring, Text: "Recovery observed"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncidentCard(c, incident) - } - ui.Render(view, 720, 480, 1) -} - -func ExampleUptimeBar() { - days := []mujica.UptimeDay{{Date: mujica.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: mujica.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.UptimeBar(c, days, mujica.UptimeBarOptions{Frame: mujica.ChartFrame{Label: "API uptime"}}) - if r.Changed() { - fmt.Println("day", r.Point) - } - if r.Retried() { - fmt.Println("retry") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleServiceStatus() { - services := []mujica.ServiceItem{{Name: "API", Health: mujica.ServiceOperational}, {Name: "Search", Health: mujica.ServiceDegraded, Note: "Slow queries"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ServiceStatus(c, services, mujica.ServiceStatusOptions{}) - } - ui.Render(view, 720, 480, 1) -} - -func ExampleLogStream() { - lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "server ready"}, {Level: mujica.LogWarn, Text: "slow query"}} - var state mujica.LogStreamState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LogStream(c, lines, &state, mujica.LogStreamOptions{}).Changed() { - fmt.Println("filter changed") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleTraceWaterfall() { - spans := []mujica.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TraceWaterfall(c, spans, mujica.TraceWaterfallOptions{}).Changed() { - fmt.Println("span selected") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleResourceGauge() { - readings := []mujica.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResourceGauge(c, readings, mujica.ResourceGaugeOptions{}).Changed() { - fmt.Println("retry") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleSystemMonitor() { - times := []time.Time{time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC), time.Date(2026, 10, 5, 9, 0, 5, 0, time.UTC)} - metrics := []mujica.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemMonitor(c, times, metrics, mujica.SystemMonitorOptions{}).Changed() { - fmt.Println("retry") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleRefreshIntervalSelector() { - interval := 30 * time.Second - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RefreshIntervalSelector(c, &interval, mujica.RefreshIntervalSelectorOptions{}) - if r.Refresh { - fmt.Println("refresh") - } - if r.Changed { - fmt.Println("interval", interval) - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleDashboardFilterBar() { - var filters mujica.DashboardFilters - opts := mujica.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DashboardFilterBar(c, &filters, opts).Changed() { - fmt.Println("filters changed") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleJsonViewer() { - data := `{"server":{"port":8080},"ready":true}` - var state mujica.JsonViewerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonViewer(c, data, &state, mujica.JsonViewerOptions{}) - if r.Copied != "" { - fmt.Println("path copied") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleJsonEditor() { - text := `{"port":8080,"ready":true}` - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonEditor(c, &text, mujica.JsonEditorOptions{}) - if r.Changed { - fmt.Println("valid", r.Valid) - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleSchemaTree() { - roots := []mujica.SchemaNode{{Name: "app", Children: []mujica.SchemaNode{{Name: "users", Kind: mujica.SchemaTable, Children: []mujica.SchemaNode{{Name: "id", Kind: mujica.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: mujica.SchemaIndex, Detail: "(id)"}}}}}} - var state mujica.SchemaTreeState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SchemaTree(c, &state, roots, mujica.SchemaTreeOptions{}).Changed() { - path, _ := state.Selected() - fmt.Println("selected", path) - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleApiRequestBuilder() { - request := mujica.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ApiRequestBuilder(c, &request, mujica.ApiRequestBuilderOptions{}) - if r.Send { - fmt.Println("send", request.Method) - } - if r.Changed { - fmt.Println("request changed") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleResponseViewer() { - response := &mujica.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: `{"ready":true}`, Headers: []mujica.KeyValue{{Key: "Content-Type", Value: "application/json"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResponseViewer(c, response, mujica.ResponseViewerOptions{}).Changed() { - fmt.Println("retry") - } - } - ui.Render(view, 720, 480, 1) -} - -func ExampleKeyValueInput() { - rows := []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.KeyValueInput(c, &rows, mujica.KeyValueInputOptions{}).Changed() { - fmt.Println("rows", len(rows)) - } - } - ui.Render(view, 720, 480, 1) -} diff --git a/example_display_test.go b/example_display_test.go deleted file mode 100644 index 6708320..0000000 --- a/example_display_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package mujica_test - -import ( - "errors" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleText() { - name := "Coronation.md" - saveErr := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) - mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) - if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { - saveErr = "" // save name here; set saveErr on failure - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHeading() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) - mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleIcon() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Row(c).Gap(8).Children(func() { - mujica.Icon(c, "star", mujica.IconOptions{}) - mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleImage() { - var photo *ui.Bitmap // decode once with ui.DecodeBitmap, outside the view - loading, loadErr := false, errors.New("timeout") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - img := mujica.Image(c, photo, mujica.ImageOptions{ - Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", - Loading: loading, Err: loadErr, - }) - if img.Retry { - loading, loadErr = true, nil // start loading again here - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAvatar() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Row(c).Gap(8).Children(func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) - mujica.Avatar(c, "", mujica.AvatarOptions{}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAvatarGroup() { - people := []mujica.AvatarItem{ - {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, - {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBadge() { - unread := 120 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) - mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTag() { - tags := []string{"Baroque", "Gothic"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Row(c).Gap(8).Children(func() { - for i, t := range tags { - var v mujica.TagView - ui.Box(c).Key(t).Children(func() { - v = mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) - }) - if v.Closed { - tags = append(tags[:i:i], tags[i+1:]...) - } - } - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLink() { - details := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) - if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { - details = !details - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleKbd() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Row(c).Gap(6).Children(func() { - mujica.Text(c, "Command palette", mujica.TextOptions{}) - mujica.Kbd(c, "Mod", "Shift", "P") - }) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_essentials_test.go b/example_essentials_test.go deleted file mode 100644 index bd39af7..0000000 --- a/example_essentials_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package mujica_test - -import ( - "strings" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleSplitButton() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Save as…", mujica.PopupMenuItemOptions{}) { - // ask for a file name - } - }) - if r.Changed() { - // save - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCopyButton() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CopyButton(c, "https://example.com/invite/42", mujica.CopyButtonOptions{Label: "Copy link"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHoldToConfirm() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{}).Changed() { - // delete the project - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBulkActionBar() { - selected := 3 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BulkActionBar(c, selected, mujica.BulkActionBarOptions{}, func() { - mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) - }) - if r.Changed() { - selected = 0 - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleScrubInput() { - opacity := 0.6 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScrubInput(c, "Opacity", &opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.1}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCurrencyInput() { - cents := int64(123450) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CurrencyInput(c, ¢s, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMaskedInput() { - phone := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MaskedInput(c, &phone, mujica.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMentionInput() { - text, mentions := "", []mujica.Mention(nil) - people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MentionInput(c, &text, &mentions, mujica.MentionInputOptions{ - Label: "Comment", - Candidates: func(q string) []mujica.MentionCandidate { - var out []mujica.MentionCandidate - for _, p := range people { - if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { - out = append(out, p) - } - } - return out - }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChoiceChips() { - var status []string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChoiceChips(c, &status, []mujica.ToggleItem[string]{ - {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, - }, mujica.ChoiceChipsOptions{Multiple: true, Label: "Status"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleKnob() { - gain := 35.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Knob(c, &gain, mujica.KnobOptions{Label: "Gain", Ticks: 11}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVerticalSlider() { - level := 40.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VerticalSlider(c, &level, mujica.VerticalSliderOptions{Step: 10, Label: "Level"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleColorPalette() { - color := ui.Hex("#7D2034") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPalette(c, &color, []mujica.ColorSwatch{ - {Color: ui.Hex("#7D2034"), Name: "Wine"}, - {Color: ui.Hex("#AD9164"), Name: "Old gold"}, - {Color: ui.Hex("#355E80"), Name: "Slate blue"}, - }, mujica.ColorPaletteOptions{Custom: true, Label: "Tag color"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSignaturePad() { - var strokes [][]mujica.SignaturePoint - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SignaturePad(c, &strokes, mujica.SignaturePadOptions{}).Changed() { - _ = mujica.SignaturePath(strokes) // SVG path data to store - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBanner() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Banner(c, &open, "Maintenance tonight at 22:00.", mujica.BannerOptions{ - Severity: mujica.SeverityWarning, - Action: "Details", - }) - if r.Action { - // show the details - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleUndoToast() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UndoToast(c, &open, "Note deleted", mujica.UndoToastOptions{ - OnUndo: func() { - // restore the note - }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMasonry() { - titles := []string{"Sea", "Forest", "Desert", "Glacier"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Masonry(c, len(titles), mujica.MasonryOptions{ - Key: func(i int) string { return titles[i] }, - }, func(i int) { - ui.Text(c, titles[i]) - }).Size(380, 280) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleResizablePanelGroup() { - var sizes []float32 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ResizablePanelGroup(c, &sizes, []mujica.ResizablePanel{ - {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, - {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, - }, mujica.ResizablePanelGroupOptions{}) - if r.Committed { - // save sizes - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDockPanel() { - var layout mujica.DockLayout - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DockPanel(c, &layout, []mujica.DockItem{ - {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, - {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, - }, mujica.DockPanelOptions{}) - if r.Changed() { - // save layout - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleQRCode() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QRCode(c, "https://example.com/mujica", mujica.QRCodeOptions{Level: mujica.QRQuartile}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleActivityFeed() { - events := []mujica.Activity{ - {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ActivityFeed(c, events, mujica.ActivityFeedOptions{}).Size(380, 280) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_files_test.go b/example_files_test.go deleted file mode 100644 index e9a2047..0000000 --- a/example_files_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package mujica_test - -import ( - "github.com/egoist/mygo/ui" - mujica "mujicaui" - "time" -) - -func ExampleFileExplorer() { - var state mujica.FileExplorerState - opts := mujica.FileExplorerOptions{ - Roots: []string{"/court"}, - Folders: func(string) []string { return nil }, - Entries: func(string) []mujica.FileEntry { - return []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} - }, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileExplorer(c, &state, opts).Element.Height(240) - } - ui.Render(view, 600, 300, 1) -} - -func ExampleFileGrid() { - entries := []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} - var state mujica.FileGridState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileGrid(c, entries, &state, mujica.FileGridOptions{}).Element.Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFileIcon() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileIcon(c, "seal.go", mujica.FileIconOptions{Size: 32}) - } - ui.Render(view, 100, 80, 1) -} - -func ExampleFilePreview() { - file := mujica.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePreview(c, file, mujica.FilePreviewOptions{}).Element.Height(240) - } - ui.Render(view, 500, 300, 1) -} - -func ExamplePathBar() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PathBar(c, "/court/chronicles/seals", mujica.PathBarOptions{}) - } - ui.Render(view, 400, 100, 1) -} - -func ExampleRenameInline() { - name, editing := "seal.txt", true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RenameInline(c, &name, &editing, mujica.RenameInlineOptions{}) - } - ui.Render(view, 400, 100, 1) -} - -func ExampleFileOperationProgress() { - op := mujica.FileOperation{Kind: mujica.FileCopy, Done: 2, Total: 10, Current: "seal.go"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{}) - } - ui.Render(view, 500, 240, 1) -} - -func ExampleTransferQueue() { - items := []mujica.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 25, Total: 100}} - var state mujica.TransferQueueState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TransferQueue(c, items, &state, mujica.TransferQueueOptions{}).Element.Height(240) - } - ui.Render(view, 500, 300, 1) -} - -func ExampleStorageUsage() { - segments := []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.StorageUsage(c, segments, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) - } - ui.Render(view, 500, 240, 1) -} - -func ExampleRecentFiles() { - now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) - files := []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}} - var state mujica.RecentFilesState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecentFiles(c, files, &state, mujica.RecentFilesOptions{Now: now}).Element.Height(240) - } - ui.Render(view, 500, 300, 1) -} - -func ExampleArchiveViewer() { - entries := []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} - var state mujica.ArchiveViewerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ArchiveViewer(c, entries, &state, mujica.ArchiveViewerOptions{}).Element.Height(240) - } - ui.Render(view, 600, 300, 1) -} - -func ExamplePresenceAvatars() { - people := []mujica.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{}) - } - ui.Render(view, 300, 100, 1) -} - -func ExampleReactions() { - items := []mujica.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Reactions(c, items, mujica.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) - } - ui.Render(view, 400, 100, 1) -} - -func ExampleShareDialog() { - open := false - members := []mujica.ShareMember{{ID: "ada", Name: "Ada", Permission: mujica.PermissionEdit, Owner: true}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Share", mujica.ButtonOptions{}).Clicked() { - open = true - } - mujica.ShareDialog(c, &open, &members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) - } - ui.Render(view, 600, 400, 1) -} - -func ExamplePermissionSelect() { - value := mujica.PermissionView - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PermissionSelect(c, &value, mujica.PermissionSelectOptions{}) - } - ui.Render(view, 300, 100, 1) -} - -func ExampleLiveIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LiveIndicator(c, mujica.LiveOffline, mujica.LiveIndicatorOptions{}) - } - ui.Render(view, 500, 100, 1) -} - -func ExampleRemoteCursor() { - carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - ui.Box(c).Size(300, 100).Children(func() { - mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{ - Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} }, - }) - }) - } - ui.Render(view, 320, 120, 1) -} diff --git a/example_form_test.go b/example_form_test.go deleted file mode 100644 index d555109..0000000 --- a/example_form_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package mujica_test - -import ( - "fmt" - - "github.com/egoist/mygo" - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleForm() { - var state mujica.FormState - name := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { - return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input - }) - }) - if res.Submitted() { - state.EndSubmit("") // after saving name - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFormField() { - email := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FormField(c, "Email", mujica.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { - return mujica.InputGroup(c, &email, mujica.InputGroupOptions{}).Input - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleInputGroup() { - site := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - g := mujica.InputGroup(c, &site, mujica.InputGroupOptions{ - Prefix: "https://", Label: "Site", - Buttons: []mujica.InputGroupButton{{Label: "Open"}}, - }) - if g.Clicked == 0 { - c.OpenURL("https://" + site) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePinInput() { - code := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PinInput(c, &code, mujica.PinInputOptions{Length: 6}).Completed() { - fmt.Println("verify", code) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTagsInput() { - tags := []string{"gothic"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TagsInput(c, &tags, mujica.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRating() { - stars := 3.5 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Rating(c, &stars, mujica.RatingOptions{Step: 0.5, ShowValue: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleColorPicker() { - tint := ui.Hex("#7D2034") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPicker(c, &tint, mujica.ColorPickerOptions{Label: "Tint"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFilePicker() { - var paths []string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePicker(c, &paths, mujica.FilePickerOptions{ - Multiple: true, - Filters: []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}}, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFileDropZone() { - var files []mujica.DroppedFile - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileDropZone(c, &files, mujica.FileDropZoneOptions{ - Extensions: []string{"png", "jpg"}, MaxFiles: 3, MaxSize: 5 << 20, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTransfer() { - chosen := []string{"uika"} - items := []mujica.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Transfer(c, &chosen, items, mujica.TransferOptions{}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_git_test.go b/example_git_test.go deleted file mode 100644 index 51e60e0..0000000 --- a/example_git_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package mujica_test - -import ( - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleDiffViewer() { - var state mujica.DiffViewerState - from := "func main() {\n\tprintln(\"hi\")\n}\n" - to := "func main() {\n\tprintln(\"hello\")\n}\n" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffViewer(c, &state, from, to, mujica.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleInlineDiff() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.InlineDiff(c, "The court meets at noon", "The court meets at dusk", mujica.InlineDiffOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleThreeWayMerge() { - var state mujica.ThreeWayMergeState - base, local, remote := "a\nb\nc", "a\nB1\nc", "a\nB2\nc" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ThreeWayMerge(c, &state, base, local, remote, mujica.ThreeWayMergeOptions{}) - r.Element.Height(280) - if done, total := state.Resolved(); r.Changed() && done == total { - _ = state.Result() // write the merged file - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleConflictResolver() { - var state mujica.ConflictResolverState - text := "a\n<<<<<<< HEAD\nmine\n=======\ntheirs\n>>>>>>> feature\nb" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConflictResolver(c, &state, text, mujica.ConflictResolverOptions{}).Changed() { - _ = state.Result() // save the file once every block is resolved - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChangesList() { - var state mujica.ChangesListState - files := []mujica.ChangedFile{ - {Path: "main.go", Status: mujica.GitModified, Staged: true}, - {Path: "notes.txt", Status: mujica.GitUntracked}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ChangesList(c, &state, files, mujica.ChangesListOptions{}) - r.Element.Height(240) - if action, path, ok := r.Action(); ok { - _, _ = action, path // run git add or git restore --staged - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommitInput() { - msg := mujica.CommitMessage{} - busy := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitInput(c, &msg, mujica.CommitInputOptions{Busy: busy}) - if r.Committed() || r.Amended() { - busy = true // run git commit, then clear busy - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommitGraph() { - var state mujica.CommitGraphState - commits := []mujica.Commit{ - {Hash: "c3", Parents: []string{"c2", "b1"}, Subject: "Merge branch", Refs: []string{"main"}}, - {Hash: "b1", Parents: []string{"c1"}, Subject: "Feature"}, - {Hash: "c2", Parents: []string{"c1"}, Subject: "Fix"}, - {Hash: "c1", Subject: "Initial commit"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CommitGraph(c, &state, commits, mujica.CommitGraphOptions{}).Element.Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommitList() { - var state mujica.ListState[string] - commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitList(c, &state, commits, mujica.CommitListOptions{}) - r.Element.Height(240) - if hash, ok := state.Selected(); ok && r.Submitted() { - _ = hash // show the commit - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBranchSelector() { - branch := "main" - branches := []mujica.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchSelector(c, &branch, branches, mujica.BranchSelectorOptions{AllowCreate: true}) - if name, ok := r.Created(); ok { - _ = name // run git switch -c name - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBranchList() { - var state mujica.ListState[string] - branches := []mujica.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} - actions := []mujica.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchList(c, &state, branches, mujica.BranchListOptions{Actions: actions}) - r.Element.Height(200) - if action, name, ok := r.Action(); ok { - _, _ = action, name // run the action on the branch - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTagList() { - var state mujica.ListState[string] - tags := []mujica.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TagList(c, &state, tags, mujica.TagListOptions{}) - r.Element.Height(200) - if _, hash, ok := r.Action(); ok { - _ = hash // show the tagged commit - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleStashList() { - var state mujica.ListState[string] - stashes := []mujica.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.StashList(c, &state, stashes, mujica.StashListOptions{}) - r.Element.Height(200) - if action, ref, ok := r.Action(); ok { - _, _ = action, ref // run git stash apply, pop or drop - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBlameView() { - var state mujica.BlameViewState - lines := []mujica.BlameLine{ - {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "package main"}, - {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "func main() {}"}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlameView(c, &state, lines, mujica.BlameViewOptions{Language: "go"}).Height(240) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleFileHistory() { - var state mujica.ListState[string] - commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.FileHistory(c, &state, "main.go", commits, mujica.FileHistoryOptions{}) - r.Element.Height(240) - if _, hash, ok := r.Action(); ok { - _ = hash // open the diff of main.go at hash - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePullRequestCard() { - pr := mujica.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", - ChecksPassed: 3, Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{}).Changed() { - // open the pull request - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleReviewComment() { - thread := mujica.ReviewThread{File: "main.go", Line: 12, Messages: []mujica.ReviewMessage{ - {Author: "Uika", When: time.Now(), Body: "Can this return early?"}, - }} - draft := "" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ReviewComment(c, thread, &draft, mujica.ReviewCommentOptions{}) - if text, ok := r.Replied(); ok { - thread.Messages = append(thread.Messages, mujica.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) - } - if r.Resolved() || r.Reopened() { - thread.Resolved = !thread.Resolved - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDiffStat() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffStat(c, 120, 8, mujica.DiffStatOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleGitStatusBadge() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GitStatusBadge(c, mujica.GitModified, mujica.GitStatusBadgeOptions{ShowLabel: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommandBlock() { - open := true - run := mujica.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandBlock(c, run, &open, mujica.CommandBlockOptions{}) - if what, ok := r.Copied(); ok { - _ = what // "command" or "output" is on the clipboard - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAnsiText() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", mujica.AnsiTextOptions{}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_input_test.go b/example_input_test.go deleted file mode 100644 index 9828a07..0000000 --- a/example_input_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package mujica_test - -import ( - "fmt" - "strings" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" - "mujicaui/icons" -) - -func ExampleButton() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Save", mujica.ButtonOptions{Icon: icons.Must("check")}).Clicked() { - fmt.Println("saved") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleIconButton() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IconButton(c, icons.Must("settings"), "Settings", mujica.IconButtonOptions{Variant: mujica.Ghost}).Clicked() { - fmt.Println("open settings") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleButtonGroup() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} - if i := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { - fmt.Println("chose", items[i].Label) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTextInput() { - var name string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TextInput(c, &name, mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) - if r.Submitted() { - fmt.Println("hello", name) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTextArea() { - var notes string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TextArea(c, ¬es, mujica.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) - if r.Submitted() { - fmt.Println("send", notes) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleNumberInput() { - copies := 1.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.NumberInput(c, &copies, mujica.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { - fmt.Println("copies", copies) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePasswordInput() { - var password string - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PasswordInput(c, &password, mujica.PasswordInputOptions{Label: "Password"}).Submitted() { - fmt.Println("signing in") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSearchInput() { - var query string - current := 0 - text := "rose lys rose crown rose" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - matches := 0 - if query != "" { - matches = strings.Count(text, query) - } - r := mujica.SearchInput(c, &query, mujica.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) - if r.Moved() { - fmt.Println("show match", current) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCheckbox() { - var mail, calendar bool - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CheckboxGroup(c, "Notifications", []mujica.CheckboxItem{ - {Label: "Mail", Value: &mail}, - {Label: "Calendar", Value: &calendar}, - }, mujica.CheckboxGroupOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRadioGroup() { - size := "m" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} - if mujica.RadioGroup(c, &size, items, mujica.RadioGroupOptions{Label: "Size"}).Changed() { - fmt.Println("size", size) - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_markets_test.go b/example_markets_test.go deleted file mode 100644 index 0051779..0000000 --- a/example_markets_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package mujica_test - -import ( - "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" - "time" -) - -func ExampleOptionChain() { - var state mujica.OptionChainState - rows := []mujica.OptionStrike{{Strike: 100, Call: mujica.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: mujica.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, mujica.OptionChainOptions{Spot: 100.4}) - if r.Changed || r.Submitted || r.ExpiryChanged { - fmt.Println(state.Expiry, state.Strike, state.Side) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleGreeksTable() { - rows := []mujica.GreeksRow{{Label: "AAPL 100 C", Greeks: mujica.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GreeksTable(c, rows, mujica.GreeksTableOptions{Notes: true}) - } - ui.Render(view, 960, 440, 1) -} - -func ExamplePayoffDiagram() { - legs := []mujica.OptionLeg{{Kind: mujica.LegCall, Strike: 100, Premium: 3, Quantity: 1}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PayoffDiagram(c, legs, mujica.PayoffDiagramOptions{Frame: mujica.ChartFrame{Label: "Call payoff"}, Spot: 103}) - if r.Changed() || r.Retried() { - fmt.Println(r.Point) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleWatchlist() { - var state mujica.WatchlistState - groups := []mujica.WatchGroup{{Name: "Technology", Items: []mujica.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []mujica.WatchItem{{Symbol: "XOM", Price: 117.4}}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Watchlist(c, &state, groups, mujica.WatchlistOptions{}) - if r.Changed || r.Submitted || r.Moved { - fmt.Println(state.Selected, r.Rows, r.To) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleSymbolSearch() { - var state mujica.SymbolSearchState - rows := []mujica.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolSearch(c, &state, rows, mujica.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) - if r.Changed || r.Picked || r.Retry { - fmt.Println(state.Query, state.Market, r.Chosen.Code) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleSymbolBadge() { - symbol := mujica.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolBadge(c, symbol, mujica.SymbolBadgeOptions{Clickable: true}) - if r.Changed() { - fmt.Println(symbol.Code) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleMarketHeatmap() { - var path []string - root := mujica.HeatNode{Name: "Market", Children: []mujica.HeatNode{{Name: "Technology", Children: []mujica.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketHeatmap(c, &path, root, mujica.MarketHeatmapOptions{Frame: mujica.ChartFrame{Label: "Market"}}) - if r.Changed() || r.ValueChanged() || r.Retried() { - fmt.Println(path, r.Point) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleScreener() { - var state mujica.ScreenerState - fields := []mujica.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} - rows := []mujica.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Screener(c, &state, fields, rows, mujica.ScreenerOptions{}) - if r.Changed || r.Save || r.Table.Changed() || r.Table.Submitted() || r.Table.SortChanged() || r.Table.Retried() { - fmt.Println(state.Conditions) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleMarketOverview() { - indices := []mujica.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketOverview(c, indices, mujica.MarketOverviewOptions{Breadth: mujica.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) - if r.Chosen >= 0 { - fmt.Println(indices[r.Chosen].Name) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleIndexCard() { - quote := mujica.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.IndexCard(c, quote, mujica.IndexCardOptions{Clickable: true}) - if r.Changed() { - fmt.Println(quote.Name) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleMarketStatus() { - now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarketStatus(c, mujica.MarketOpen, mujica.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: mujica.MarketAfterHours}) - } - ui.Render(view, 960, 440, 1) -} - -func ExampleTradingSessionClock() { - now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) - markets := []mujica.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []mujica.TradingSession{{Phase: mujica.MarketOpen, Start: 570, End: 960}}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TradingSessionClock(c, markets, mujica.TradingSessionClockOptions{Now: now}) - } - ui.Render(view, 960, 440, 1) -} - -func ExampleEconomicCalendar() { - var state mujica.EconomicCalendarState - rows := []mujica.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EconomicCalendar(c, &state, rows, mujica.EconomicCalendarOptions{}) - if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { - fmt.Println(state.Table.Selected()) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleEarningsCalendar() { - var state mujica.DataTableState[string] - rows := []mujica.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: mujica.Date{Year: 2026, Month: 10, Day: 30}, Timing: mujica.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EarningsCalendar(c, &state, rows, mujica.EarningsCalendarOptions{}) - if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { - fmt.Println(state.Selected()) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleNewsFeed() { - var state mujica.NewsFeedState - rows := []mujica.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.NewsFeed(c, &state, rows, mujica.NewsFeedOptions{}) - if r.Opened != "" || r.Symbol != "" || r.Retry { - fmt.Println(r.Opened, r.Symbol) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleSentimentGauge() { - value := 72.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SentimentGauge(c, value, mujica.SentimentGaugeOptions{Label: "Fear & Greed"}) - } - ui.Render(view, 960, 440, 1) -} - -func ExamplePortfolioSummary() { - account := "" - accounts := []mujica.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PortfolioSummary(c, &account, accounts, mujica.PortfolioSummaryOptions{}) - if r.Changed() { - fmt.Println(account) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleAssetAllocationChart() { - items := []mujica.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AssetAllocationChart(c, items, mujica.AssetAllocationChartOptions{Frame: mujica.ChartFrame{Label: "Allocation"}}) - if r.Changed() || r.Retried() { - fmt.Println(r.Point) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExamplePerformanceChart() { - values := []float64{100, 110, 95, 120} - benchmark := []float64{100, 105, 108, 112} - times := []time.Time{time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PerformanceChart(c, values, mujica.PerformanceChartOptions{Frame: mujica.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) - if r.Changed() || r.Retried() { - fmt.Println(r.Point) - } - if name, ok := r.Toggled(); ok { - fmt.Println(name) - } - } - ui.Render(view, 960, 440, 1) -} - -func ExampleCurrencyConverter() { - state := mujica.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} - currencies := []mujica.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} - rate := 149.37 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CurrencyConverter(c, &state, currencies, mujica.CurrencyConverterOptions{Rate: rate}) - if r.PairChanged { - rate = 0 - fmt.Println(state.From, state.To) - } - } - ui.Render(view, 960, 440, 1) -} diff --git a/example_media_test.go b/example_media_test.go deleted file mode 100644 index 52b53f1..0000000 --- a/example_media_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package mujica_test - -import ( - "image" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -// examplePhoto stands in for a decoded picture. -var examplePhoto = image.NewRGBA(image.Rect(0, 0, 320, 180)) - -func ExampleImageCropper() { - crop := mujica.ImageCrop{} // empty: starts fitted to the image - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageCropper(c, examplePhoto, &crop, mujica.ImageCropperOptions{Aspect: 1}).Changed() { - _ = crop.Rect // in pixels of the photo turned by crop.Rotation - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleImageAnnotator() { - photo := ui.NewBitmap(examplePhoto) - var notes []mujica.Annotation - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageAnnotator(c, photo, ¬es, mujica.ImageAnnotatorOptions{}).Changed() { - data, _ := mujica.ExportAnnotations(notes) - _ = data // save the JSON - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleImageThumbnail() { - photo := ui.NewBitmap(examplePhoto) - selected := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageThumbnail(c, photo, mujica.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { - selected = !selected - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLightbox() { - images := []mujica.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} - open, index := false, 0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "View photos", mujica.ButtonOptions{}).Clicked() { - open = true - } - mujica.Lightbox(c, &open, &index, images) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVideoPlayer() { - st := mujica.VideoState{Duration: 4 * time.Minute, Rate: 1} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{ - OnPlay: func() { st.Playing = true }, // start your decoder - OnPause: func() { st.Playing = false }, // pause it - OnSeek: func(d time.Duration) { st.Position = d }, - OnRate: func(r float64) { st.Rate = r }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVideoScrubber() { - pos := 30 * time.Second - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoScrubber(c, &pos, mujica.VideoScrubberOptions{ - Duration: 2 * time.Minute, - Buffered: []mujica.MediaRange{{Start: 0, End: time.Minute}}, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVideoThumbnailStrip() { - frames := []mujica.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} - current := 0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.VideoThumbnailStrip(c, ¤t, frames, mujica.VideoThumbnailStripOptions{}).Changed() { - _ = frames[current].At // seek the player there - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAudioPlayer() { - st := mujica.AudioState{Title: "Overture", Duration: 3 * time.Minute} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioPlayer(c, st, mujica.AudioPlayerOptions{ - OnPlay: func() { st.Playing = true }, - OnPause: func() { st.Playing = false }, - OnSeek: func(d time.Duration) { st.Position = d }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAudioWaveform() { - samples := make([]float32, 48000) // from your decoder, -1 to 1 - pos, sel := time.Duration(0), mujica.MediaRange{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioWaveform(c, samples, mujica.AudioWaveformOptions{ - Duration: time.Second, Position: pos, Selection: &sel, - OnSeek: func(d time.Duration) { pos = d }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAudioSpectrum() { - bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate() - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVolumeControl() { - volume, muted := 0.8, false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeControl(c, &volume, &muted, mujica.VolumeControlOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePlaybackSpeedControl() { - rate := 1.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PlaybackSpeedControl(c, &rate, mujica.PlaybackSpeedControlOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMediaControls() { - st := mujica.MediaControlsState{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MediaControls(c, st, mujica.MediaControlsOptions{ - OnPrevious: func() {}, OnNext: func() {}, - OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, - OnLoop: func(m mujica.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePlaylist() { - items := []mujica.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} - current := "a" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.Playlist(c, &items, mujica.PlaylistOptions{Current: current}); r.Played != "" { - current = r.Played - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSubtitleEditor() { - cues := []mujica.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubtitleEditor(c, &cues, mujica.SubtitleEditorOptions{}) - if len(mujica.CheckSubtitles(cues)) > 0 { - _ = cues // block saving until the issues are fixed - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleScreenRecorderControls() { - st := mujica.RecorderState{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenRecorderControls(c, st, mujica.ScreenRecorderControlsOptions{ - OnStart: func() { st.Status = mujica.RecorderRecording }, - OnPause: func() { st.Status = mujica.RecorderPaused }, - OnResume: func() { st.Status = mujica.RecorderRecording }, - OnStop: func() { st = mujica.RecorderState{} }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCameraPreview() { - frame := mujica.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame - mirror := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CameraPreview(c, frame, &mirror, mujica.CameraPreviewOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMicLevelMeter() { - level := float32(0.4) // the input's peak, 0 to 1 of full scale - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDeviceSelector() { - devices := mujica.MediaDevices{ - Microphones: []mujica.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, - Speakers: []mujica.MediaDevice{{ID: "out-1", Name: "Headphones"}}, - } - sel := mujica.DeviceSelection{Microphone: "mic-1"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DeviceSelector(c, &sel, devices, mujica.DeviceSelectorOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleImageCompare() { - before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto) - split := 0.5 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ImageCompare(c, before, after, &split, mujica.ImageCompareOptions{}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_messaging_test.go b/example_messaging_test.go deleted file mode 100644 index 8f93b6a..0000000 --- a/example_messaging_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package mujica_test - -import ( - mujica "mujicaui" - "time" - - "github.com/egoist/mygo/ui" -) - -func ExampleChannelList() { - groups := []mujica.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []mujica.Channel{ - {ID: "general", Name: "general", Unread: 3}, - {ID: "rehearsal", Name: "rehearsal", Pinned: true}, - }}} - selected := "general" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChannelList(c, &selected, groups, mujica.ChannelListOptions{}).Changed() { - _ = selected // show that channel - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleChatMessage() { - prev := mujica.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} - m := mujica.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChatMessage(c, prev, mujica.ChatMessageOptions{}) - mujica.ChatMessage(c, m, mujica.ChatMessageOptions{ - Continued: mujica.ChatContinues(prev, m), - Reactions: func() { mujica.Tag(c, "👍 2", mujica.TagOptions{}) }, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleUnreadDivider() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UnreadDivider(c, 3) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleReadReceipt() { - readers := []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReadReceipt(c, mujica.ReceiptRead, mujica.ReadReceiptOptions{Readers: readers}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMemberList() { - var s mujica.MemberListState - members := []mujica.Member{ - {ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"}, - {ID: "2", Name: "Umiri", Presence: mujica.PresenceOffline}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemberList(c, &s, members, mujica.MemberListOptions{}); r.Opened != "" { - _ = r.Opened // show that member's profile - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleUserProfileCard() { - profile := mujica.UserProfile{Name: "Sakiko Togawa", Presence: mujica.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UserProfileCard(c, profile, mujica.UserProfileCardOptions{Actions: func() { - mujica.Button(c, "Message", mujica.ButtonOptions{}) - }}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOnlineStatus() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OnlineStatus(c, mujica.PresenceAway, mujica.OnlineStatusOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleStatusSetter() { - status := mujica.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSetter(c, &status, mujica.StatusSetterOptions{}).Changed() { - _ = status // publish it; clear it once status.Until passes - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCallControls() { - st := mujica.CallState{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CallControls(c, st, mujica.CallControlsOptions{ - OnMute: func(on bool) { st.Muted = on }, - OnCamera: func(off bool) { st.CameraOff = off }, - OnShare: func(on bool) { st.Sharing = on }, - OnHangUp: func() {}, // leave the call - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleVideoCallGrid() { - people := []mujica.CallParticipant{ - {ID: "1", Name: "Sakiko", Speaking: true}, // Frame: the latest decoded frame - {ID: "2", Name: "Uika", Muted: true}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleIncomingCallDialog() { - ringing := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncomingCallDialog(c, &ringing, mujica.IncomingCall{Caller: "Sakiko", Video: true}, mujica.IncomingCallOptions{ - OnAccept: func(video bool) {}, // join, with the camera on when video - OnDecline: func() {}, - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleEmojiPicker() { - var recent []string - tone := 0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { - _ = r.Picked // insert it - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePinnedMessages() { - pins := []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PinnedMessages(c, pins, mujica.PinnedMessagesOptions{}) - if r.Unpinned != "" { - pins = pins[:0] // unpin it - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleThreadPanel() { - open := true - root := mujica.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} - var replies []mujica.ChatEntry - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.ThreadPanel(c, &open, root, replies, mujica.ThreadPanelOptions{}); r.Sent != "" { - replies = append(replies, mujica.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMailList() { - var s mujica.ListState[string] - mails := []mujica.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailList(c, &s, mails, mujica.MailListOptions{}); r.Starred != "" { - mails[0].Starred = !mails[0].Starred - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMailReader() { - m := mujica.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", - Attachments: []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailReader(c, m); r.Link != "" { - c.OpenURL(r.Link) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMailComposer() { - draft := mujica.MailDraft{To: []string{"uika@example.com"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MailComposer(c, &draft, mujica.MailComposerOptions{ - OnSaveDraft: func(d mujica.MailDraft) {}, // store d - }) - if r.Sent { - draft = mujica.MailDraft{} - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRecipientInput() { - var to []string - contacts := []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecipientInput(c, &to, mujica.RecipientInputOptions{Contacts: contacts, Label: "To"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleQuotedText() { - open := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuotedText(c, "> Can you send the score?", &open) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSnoozePicker() { - var until time.Time - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SnoozePicker(c, &until, mujica.SnoozePickerOptions{}).Changed() { - _ = until // hide the mail until then - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_motion_test.go b/example_motion_test.go deleted file mode 100644 index e0e8184..0000000 --- a/example_motion_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package mujica_test - -import ( - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleInterruptButton() { - running, stopping := true, false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.InterruptButton(c, running, mujica.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) - if r.Changed() { - stopping = true // cancel the run, then clear running and stopping - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTokenUsageChart() { - usage := []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCostBreakdown() { - var state mujica.CostBreakdownState - items := []mujica.CostItem{ - {Model: "opus", Tool: "search", InputTokens: 1000, OutputTokens: 200, Cost: 0.30}, - {Model: "haiku", Tool: "search", InputTokens: 4000, OutputTokens: 500, Cost: 0.05}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostBreakdown(c, &state, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostByModel}).Element.Height(260) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTraceViewer() { - var state mujica.TraceViewerState - spans := []mujica.TraceSpan{ - {ID: "run", Name: "agent.run", End: 900 * time.Millisecond}, - {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, - Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}}}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TraceViewer(c, &state, spans, mujica.TraceViewerOptions{}).Element.Height(260) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleEvalResultTable() { - var state mujica.DataTableState[string] - cases := []mujica.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EvalResultTable(c, &state, cases, mujica.EvalResultTableOptions{}) - r.Table.Element.Height(260) - if id, ok := r.Diff(); ok { - _ = id // open expected and actual output side by side - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToolRegistryPanel() { - var state mujica.ListState[string] - tools := []mujica.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, - Params: []mujica.ToolParam{{Name: "query", Type: "string", Required: true}}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolRegistryPanel(c, &state, tools, mujica.ToolRegistryPanelOptions{}) - r.Element.Height(260) - if name, on, ok := r.Toggled(); ok && name == tools[0].Name { - tools[0].Enabled = on - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDotsLoader() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Thinking", ShowLabel: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBarsLoader() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarsLoader(c, mujica.LoaderOptions{Size: 20, Label: "Indexing"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePulseLoader() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PulseLoader(c, mujica.LoaderOptions{Label: "Listening", ShowLabel: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOrbitLoader() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OrbitLoader(c, mujica.LoaderOptions{Size: 24, Label: "Connecting"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleWaveLoader() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaveLoader(c, mujica.LoaderOptions{Label: "Speaking", ShowLabel: true}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleShimmer() { - loading := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Shimmer(c, loading, mujica.ShimmerOptions{Height: 48}, func() { - ui.Text(c, "Report ready") - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCountUp() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CountUp(c, 12840, mujica.CountUpOptions{Duration: 800 * time.Millisecond}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTypewriter() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Typewriter(c, "The court assembles at dusk.", mujica.TypewriterOptions{Speed: 30, Skippable: true}) - if r.Changed() { - // show the next message - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleShimmerText() { - thinking := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShimmerText(c, "Thinking…", mujica.ShimmerTextOptions{Active: thinking}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleStagger() { - guests := []string{"Sakiko", "Uika", "Mutsumi"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Stagger(c, len(guests), mujica.StaggerOptions{}, func(i int) { - ui.Text(c, guests[i]) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleReorderTransition() { - order := []string{"Sakiko", "Uika", "Mutsumi"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReorderTransition(c, order, mujica.ReorderTransitionOptions{Gap: 4}, func(name string) { - ui.Text(c, name) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLayoutTransition() { - open := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: true}, func() { - ui.Text(c, "Program") - if open { - ui.Text(c, "Overture · Masquerade · Finale") - } - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePresence() { - show := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Presence(c, show, mujica.PresenceOptions{Kind: mujica.PresenceSlide}, func() { - ui.Text(c, "Saved") - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBlinkHighlight() { - price := 101.5 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlinkHighlight(c, price, mujica.BlinkHighlightOptions{}) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_navigation_test.go b/example_navigation_test.go deleted file mode 100644 index 5b5a611..0000000 --- a/example_navigation_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package mujica_test - -import ( - "fmt" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" - "mujicaui/icons" -) - -func ExampleTabs() { - tab := 0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Tabs(c, &tab, []mujica.Tab{ - {Label: "General", Panel: func() { ui.Text(c, "General settings") }}, - {Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }}, - {Label: "Archive", Disabled: true}, - }, mujica.TabsOptions{Label: "Settings"}).Changed() { - fmt.Println("tab", tab) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSegmentedControl() { - view := 0 - build := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SegmentedControl(c, &view, []mujica.Segment{ - {Label: "List", Icon: icons.Must("list"), HideLabel: true}, - {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true}, - }, mujica.SegmentedControlOptions{Label: "View"}).Changed() { - fmt.Println("view", view) - } - } - ui.Render(build, 400, 300, 1) -} - -func ExampleBreadcrumb() { - path := []mujica.BreadcrumbItem{{Label: "Home", Icon: icons.Must("house")}, {Label: "Library"}, {Label: "Poems"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok { - path = path[:i+1] - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePagination() { - page, size := 1, 20 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { - fmt.Println("load page", page, "of size", size) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSteps() { - current := 1 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Steps(c, ¤t, []mujica.Step{ - {Title: "Account", Clickable: true}, - {Title: "Profile", Description: "Name and seal"}, - {Title: "Confirm"}, - }, mujica.StepsOptions{Label: "Sign-up"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSidebar() { - selected := "inbox" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sidebar(c, &selected, []mujica.SidebarSection{ - {Title: "Mail", Items: []mujica.SidebarItem{ - {ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: 3}, - {Label: "Folders", Items: []mujica.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, - }}, - }, mujica.SidebarOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleNavigationMenu() { - router := ui.NewRouter("/") - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.NavigationMenu(c, router, []mujica.NavigationLink{ - {Label: "Home", Path: "/"}, - {Label: "Library", Items: []mujica.NavigationLink{ - {Label: "Poems", Path: "/library/poems", Description: "Verse of the court"}, - }}, - }, mujica.NavigationMenuOptions{History: true}) - router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMenubar() { - wrap := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - res := mujica.Menubar(c, []mujica.MenubarMenu{ - {Label: "File", Items: []mujica.MenubarItem{ - {ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, - {Label: "Export", Items: []mujica.MenubarItem{{ID: "pdf", Label: "PDF"}}}, - }}, - {Label: "View", Items: []mujica.MenubarItem{ - {ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: wrap}, - }}, - }, mujica.MenubarOptions{}) - if id, ok := res.Chosen(); ok && id == "wrap" { - wrap = !wrap - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleToolbar() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() { - mujica.ToolbarGroup(c, func() { - mujica.ToolbarButton(c, "Bold", mujica.ToolbarButtonOptions{Icon: icons.Must("bold")}) - mujica.ToolbarButton(c, "Italic", mujica.ToolbarButtonOptions{Icon: icons.Must("italic")}) - }) - mujica.ToolbarSeparator(c) - if mujica.ToolbarButton(c, "Copy", mujica.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { - fmt.Println("copy") - } - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleCommandPalette() { - open := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - res := mujica.CommandPalette(c, &open, []mujica.Command{ - {ID: "new", Label: "New document", Group: "File"}, - {ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"night"}}, - }, mujica.CommandPaletteOptions{}) - if id, ok := res.Chosen(); ok { - fmt.Println("run", id) - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_overlay_test.go b/example_overlay_test.go deleted file mode 100644 index acbcf5b..0000000 --- a/example_overlay_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package mujica_test - -import ( - "fmt" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleDialog() { - open := true - name := "Ophelia" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() { - open = true - } - mujica.Dialog(c, &open, mujica.DialogOptions{ - Title: "Rename the chamber", - Description: "The new name shows in every hall.", - Decorated: true, - Actions: func() { - if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { - open = false - } - if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() { - open = false - } - }, - }, func() { - ui.TextInput(c, &name) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleAlertDialog() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { - open = true - } - r := mujica.AlertDialog(c, &open, mujica.AlertDialogOptions{ - Title: "Delete the archive?", - Description: "Twelve letters will be destroyed.", - ConfirmText: "archive", - }) - if r.Confirmed { - fmt.Println("deleted") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDrawer() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Filters", mujica.ButtonOptions{}).Clicked() { - open = true - } - mujica.Drawer(c, &open, mujica.DrawerOptions{Title: "Filters", Side: mujica.DrawerRight, Size: 320}, func() { - ui.Text(c, "Guests are placed by rank.") - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePopover() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary}) - if b.Clicked() { - open = !open - } - mujica.Popover(c, b, &open, mujica.PopoverOptions{Title: "Guest list"}, func() { - ui.Text(c, "Twelve guests confirmed.") - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTooltip() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) - mujica.Tooltip(c, b, "Seal the letter with wax", mujica.TooltipOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleHoverCard() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost}) - mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() { - ui.Text(c, "Keyboardist of the court.") - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDropdownMenu() { - pinned := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{}, func(m *mujica.PopupMenu) { - m.Group("Letter", func() { - if m.Item("Rename", mujica.PopupMenuItemOptions{}) { - fmt.Println("rename") - } - m.Check("Pinned", &pinned, mujica.PopupMenuItemOptions{KeepOpen: true}) - }) - m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) { - m.Item("Archive", mujica.PopupMenuItemOptions{}) - }) - m.Separator() - m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: true}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleContextMenu() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - row := ui.Row(c).Padding(12).Children(func() { - ui.Text(c, "Letter to the Countess") - }) - mujica.ContextMenu(c, row, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Reply", mujica.PopupMenuItemOptions{}) { - fmt.Println("reply") - } - m.Item("Delete", mujica.PopupMenuItemOptions{}) - }) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePopconfirm() { - open := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) - if b.Clicked() { - open = true - } - if mujica.Popconfirm(c, b, &open, mujica.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { - fmt.Println("archived") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOverlay() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Overlay(c, &open, mujica.OverlayOptions{Place: mujica.OverlayBottomRight, Label: "Now playing"}, func() { - ui.Text(c, "Symbol I: △") - }) - } - ui.Render(view, 400, 300, 1) -} diff --git a/example_projects_test.go b/example_projects_test.go deleted file mode 100644 index 7446e9c..0000000 --- a/example_projects_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package mujica_test - -import ( - "github.com/egoist/mygo/ui" - "log" - mujica "mujicaui" - "time" -) - -func ExampleKanbanBoard() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.KanbanBoard(c, &tasks, mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) - if move, ok := r.Moved(); ok { - log.Println("move", move.ID, move.To, move.Index) - } - if id, ok := r.Opened(); ok { - log.Println("open", id) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleTaskList() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} - filter := mujica.TaskShowAll - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskList(c, tasks, &filter, mujica.TaskListOptions{GroupBy: mujica.TaskGroupStatus, Statuses: statuses}) - if id, ok := r.Changed(); ok { - log.Println("edit", id) - } - if id, ok := r.Opened(); ok { - log.Println("open", id) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleTaskItem() { - task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskItem(c, &task, mujica.TaskItemOptions{}) - if id, ok := r.Changed(); ok { - log.Println("edit", id) - } - if id, ok := r.Opened(); ok { - log.Println("open", id) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleTaskDetailPanel() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskDetailPanel(c, &task, mujica.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) - if r.Changed() { - log.Println("task edited", task.ID) - } - if r.Closed() { - log.Println("close", task.ID) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleSubtaskList() { - subtasks := []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SubtaskList(c, &subtasks, mujica.SubtaskListOptions{}).Changed() { - log.Println("subtasks changed", len(subtasks)) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleSprintBoard() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} - sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SprintBoard(c, sprint, &tasks, mujica.SprintBoardOptions{Board: mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) - if move, ok := r.Moved(); ok { - log.Println("move", move.ID, move.To) - } - if id, ok := r.Opened(); ok { - log.Println("open", id) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleBurndownChart() { - remaining := []float64{40, 32, 25, 18} - opts := mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BurndownChart(c, remaining, opts) - if r.Changed() { - log.Println("point", r.Point) - } - if name, ok := r.Toggled(); ok { - log.Println("series", name) - } - if r.Retried() { - log.Println("retry") - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExamplePriorityIndicator() { - priority := mujica.PriorityHigh - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriorityIndicator(c, priority, mujica.PriorityIndicatorOptions{}) - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleStatusSelect() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - value := "todo" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSelect(c, &value, statuses, mujica.StatusSelectOptions{}).Changed() { - log.Println("status", value) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleAssigneePicker() { - members := []mujica.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} - ids := []string{"ann"} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AssigneePicker(c, &ids, members, mujica.AssigneePickerOptions{Max: 2}).Changed() { - log.Println("assignees", ids) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleDueDatePicker() { - var due mujica.Date - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DueDatePicker(c, &due, mujica.DueDatePickerOptions{}).Changed() { - log.Println("due", due) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleLabelManager() { - labels := []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}} - selected := []string{} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LabelManager(c, &labels, &selected, mujica.LabelManagerOptions{}).Changed() { - log.Println("labels changed", len(labels), selected) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleIssueCard() { - statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} - task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueCard(c, task, mujica.IssueCardOptions{Statuses: statuses}).Clicked() { - log.Println("open", task.ID) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleIssueIdBadge() { - id := "ENG-123" - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueIdBadge(c, id, mujica.IssueIdBadgeOptions{Copyable: true}).Changed() { - log.Println("copied", id) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleMilestoneProgress() { - milestone := mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MilestoneProgress(c, milestone, mujica.MilestoneProgressOptions{}) - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleWorkloadView() { - loads := []mujica.Workload{{Member: mujica.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkloadView(c, loads, mujica.WorkloadViewOptions{}) - } - ui.Render(view, 1000, 700, 1) -} - -func ExampleTimeTracker() { - var state mujica.TimeTrackerState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimeTracker(c, &state, mujica.TimeTrackerOptions{}).Changed() { - log.Println("tracked", state.Total(c.Now())) - } - } - ui.Render(view, 1000, 700, 1) -} - -func ExamplePomodoroTimer() { - var state mujica.PomodoroState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PomodoroTimer(c, &state, mujica.PomodoroTimerOptions{Focus: 25 * time.Minute}) - if r.Changed() { - log.Println("timer", state.Phase(), state.Running()) - } - if phase, ok := r.Finished(); ok { - log.Println("finished", phase) - } - } - ui.Render(view, 1000, 700, 1) -} diff --git a/example_trading_test.go b/example_trading_test.go deleted file mode 100644 index 6c32d4f..0000000 --- a/example_trading_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package mujica_test - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" - - mujica "mujicaui" -) - -func ExampleTickerTape() { - items := []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TickerTape(c, items, mujica.TickerTapeOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleQuoteCard() { - quote := mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuoteCard(c, quote, mujica.QuoteCardOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePriceText() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UseTrend(c, mujica.RedUp) - mujica.PriceText(c, 18234.5, mujica.PriceTextOptions{Compare: true, Reference: 18000}) - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePriceChangeBadge() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriceChangeBadge(c, -5.62, -1.35, mujica.PriceChangeBadgeOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOrderBook() { - var state mujica.OrderBookState - book := mujica.OrderBookData{ - Bids: []mujica.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, - Asks: []mujica.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.OrderBook(c, &state, book, mujica.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { - fmt.Println("order at", price, side) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLevel2Quotes() { - data := mujica.Level2Data{ - Bids: []mujica.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, - Asks: []mujica.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, - } - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTimeAndSales() { - var state mujica.TimeAndSalesState - trades := []mujica.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: mujica.SideBuy}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeAndSales(c, &state, trades, mujica.TimeAndSalesOptions{LargeSize: 1000}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleDepthLadder() { - var state mujica.DepthLadderState - book := mujica.OrderBookData{Bids: []mujica.BookLevel{{Price: 100, Size: 5}}, Asks: []mujica.BookLevel{{Price: 100.01, Size: 2}}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.DepthLadder(c, &state, 100, book, mujica.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { - fmt.Println("place", side, "at", price) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleSpreadIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{WarnPercent: 0.5}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleBidAskBar() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BidAskBar(c, 6234, 3820, mujica.BidAskBarOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOrderEntry() { - var state mujica.OrderEntryState - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if order, ok := mujica.OrderEntry(c, &state, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { - fmt.Println("place", order.Quantity) - state.EndSubmit("") - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleQuickTradeButtons() { - quantity := 10.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if side, q, ok := mujica.QuickTradeButtons(c, &quantity, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { - fmt.Println(side, q) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePositionTable() { - var state mujica.DataTableState[string] - rows := []mujica.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.PositionTable(c, &state, rows, mujica.PositionTableOptions{}).Acted(); ok { - fmt.Println("close", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOrderTable() { - var state mujica.OrderTableState - rows := []mujica.OrderRow{{ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OrderTable(c, &state, rows, mujica.OrderTableOptions{}).Acted(); ok { - fmt.Println("cancel", id) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleTradeHistoryTable() { - var state mujica.TradeHistoryTableState - rows := []mujica.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: mujica.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if fills, ok := mujica.TradeHistoryTable(c, &state, rows, mujica.TradeHistoryTableOptions{}).Exported(); ok { - fmt.Println("export", len(fills)) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExamplePnLDisplay() { - hidden := false - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, mujica.PnLDisplayOptions{Currency: "$"}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleLeverageSlider() { - leverage := 5.0 - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LeverageSlider(c, &leverage, mujica.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { - fmt.Println("leverage", leverage) - } - } - ui.Render(view, 400, 300, 1) -} - -func ExampleMarginIndicator() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarginIndicator(c, 0.86, mujica.MarginIndicatorOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleRiskMeter() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RiskMeter(c, 4, mujica.RiskMeterOptions{}) - } - ui.Render(view, 400, 300, 1) -} - -func ExampleOrderConfirmDialog() { - open := true - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OrderConfirmDialog(c, &open, mujica.OrderConfirmDialogOptions{ - Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, - Risk: "Leveraged products can lose more than the deposit.", - Acknowledge: "I understand the risk", - }) - if r.Confirmed { - fmt.Println("send order") - } - } - ui.Render(view, 400, 300, 1) -} diff --git a/examples/gallery/app.go b/examples/gallery/app.go index dba04b1..f80b245 100644 --- a/examples/gallery/app.go +++ b/examples/gallery/app.go @@ -7,9 +7,13 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // event is one line of the gallery's event log. @@ -65,16 +69,16 @@ func (g *gallery) logFor(id string) func(string) { } } -func (g *gallery) settings() mujica.Settings { - s := mujica.Settings{Mode: mujica.Mode(g.mode), Density: densities[g.density], Locale: mujica.EnUS} +func (g *gallery) settings() core.Settings { + s := core.Settings{Mode: core.Mode(g.mode), Density: densities[g.density], Locale: core.EnUS} if g.zh { - s.Locale = mujica.ZhCN + s.Locale = core.ZhCN } return s } func (g *gallery) view(c *ui.Context) { - mujica.Use(c, g.settings()) + core.Use(c, g.settings()) w, _ := c.Size() if narrow := w < 900; narrow != g.autoFold { g.autoFold, g.folded = narrow, narrow @@ -84,7 +88,7 @@ func (g *gallery) view(c *ui.Context) { scroll = &ui.ScrollState{} g.scrolls[g.selected] = scroll } - mujica.AppShell(c, &g.folded, mujica.AppShellOptions{ + layout.AppShell(c, &g.folded, layout.AppShellOptions{ TitleBar: func() { g.titleBar(c) }, Sidebar: func() { g.sidebar(c) }, Content: func() { g.content(c) }, @@ -92,33 +96,33 @@ func (g *gallery) view(c *ui.Context) { ContentScroll: scroll, }) g.commandPalette(c) - mujica.Toaster(c) + feedback.Toaster(c) } func (g *gallery) titleBar(c *ui.Context) { - mujica.TitleBar(c, "MujicaUI", mujica.TitleBarOptions{Trailing: func() { + layout.TitleBar(c, "MujicaUI", layout.TitleBarOptions{Trailing: func() { ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() { mode := g.mode - if mujica.SegmentedControl(c, &g.mode, []mujica.Segment{ + if navigation.SegmentedControl(c, &g.mode, []navigation.Segment{ {Label: g.t("System", "跟随系统"), Icon: icons.Must("settings"), HideLabel: true}, {Label: g.t("Light", "浅色"), Icon: icons.Must("sun"), HideLabel: true}, {Label: g.t("Dark", "深色"), Icon: icons.Must("moon"), HideLabel: true}, - }, mujica.SegmentedControlOptions{Label: g.t("Theme", "主题")}).Changed() && mode != g.mode { - g.logFor("shell")("theme " + mujica.Mode(g.mode).String()) + }, navigation.SegmentedControlOptions{Label: g.t("Theme", "主题")}).Changed() && mode != g.mode { + g.logFor("shell")("theme " + core.Mode(g.mode).String()) g.overture.switches++ } density := g.density - if mujica.SegmentedControl(c, &g.density, []mujica.Segment{ + if navigation.SegmentedControl(c, &g.density, []navigation.Segment{ {Label: g.t("Compact", "紧凑")}, {Label: g.t("Default", "默认")}, {Label: g.t("Comfortable", "宽松")}, - }, mujica.SegmentedControlOptions{Label: g.t("Density", "密度")}).Changed() && density != g.density { + }, navigation.SegmentedControlOptions{Label: g.t("Density", "密度")}).Changed() && density != g.density { g.logFor("shell")("density " + densities[g.density].String()) g.overture.switches++ } - if mujica.Button(c, g.t("中文", "English"), mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, g.t("中文", "English"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { g.zh = !g.zh g.logFor("shell")(fmt.Sprintf("locale %s", g.settings().Locale)) } - if mujica.IconButton(c, icons.Must("command"), g.t("Command palette", "命令面板"), mujica.IconButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.IconButton(c, icons.Must("command"), g.t("Command palette", "命令面板"), input.IconButtonOptions{Variant: input.Ghost}).Clicked() { g.palette = true } }) @@ -126,16 +130,16 @@ func (g *gallery) titleBar(c *ui.Context) { } func (g *gallery) statusBar(c *ui.Context) { - left := []mujica.StatusItem{ + left := []layout.StatusItem{ {ID: "count", Text: fmt.Sprintf(g.t("%d components", "%d 个组件"), len(g.pages)), Icon: icons.Must("layout-grid")}, - {ID: "look", Text: fmt.Sprintf("%s · %s", mujica.Mode(g.mode), densities[g.density]), Secondary: true}, + {ID: "look", Text: fmt.Sprintf("%s · %s", core.Mode(g.mode), densities[g.density]), Secondary: true}, } - var right []mujica.StatusItem + var right []layout.StatusItem if n := len(g.events); n > 0 { e := g.events[n-1] - right = append(right, mujica.StatusItem{ID: "last", Text: e.page + " · " + e.text, Icon: icons.Must("bell"), Secondary: true}) + right = append(right, layout.StatusItem{ID: "last", Text: e.page + " · " + e.text, Icon: icons.Must("bell"), Secondary: true}) } - mujica.StatusBar(c, mujica.StatusBarOptions{Left: left, Right: right}) + layout.StatusBar(c, layout.StatusBarOptions{Left: left, Right: right}) } func (g *gallery) content(c *ui.Context) { diff --git a/examples/gallery/catalog.go b/examples/gallery/catalog.go index 23405e9..8005a70 100644 --- a/examples/gallery/catalog.go +++ b/examples/gallery/catalog.go @@ -1,6 +1,8 @@ package main -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" +) // page is one component's gallery entry. type page struct { diff --git a/examples/gallery/composite_files.go b/examples/gallery/composite_files.go index cf669e8..fe5693f 100644 --- a/examples/gallery/composite_files.go +++ b/examples/gallery/composite_files.go @@ -11,9 +11,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const filesID = "c-files" @@ -30,8 +36,8 @@ type filesDemo struct { next int version int query string - table mujica.DataTableState[int] - dropped []mujica.DroppedFile + table data.DataTableState[int] + dropped []input.DroppedFile picked []string renaming bool renameID int @@ -68,14 +74,14 @@ func fileSize(n int64) string { } } -func (g *gallery) fileColumns(c *ui.Context) []mujica.DataColumn[fileEntry] { - return []mujica.DataColumn[fileEntry]{ +func (g *gallery) fileColumns(c *ui.Context) []data.DataColumn[fileEntry] { + return []data.DataColumn[fileEntry]{ {ID: "name", Title: g.t("Name", "名称"), MinWidth: 200, Sortable: true, Text: func(f fileEntry) string { return f.Name }, Compare: func(a, b fileEntry) int { return cmp.Compare(a.Name, b.Name) }, Cell: func(f fileEntry) { ui.Row(c).Gap(8).AlignItems(ui.Center).MinWidth(0).Children(func() { - ui.Icon(c, icons.Must("file")).Size(16, 16).TextColor(mujica.Tokens(c).TextMuted) + ui.Icon(c, icons.Must("file")).Size(16, 16).TextColor(core.Tokens(c).TextMuted) ui.Text(c, f.Name).SingleLine() }) }}, @@ -121,39 +127,39 @@ func (s *filesDemo) add(path string, size int64, at time.Time) bool { func (g *gallery) filesView(c *ui.Context, log func(string)) { s := &g.cFiles - k := mujica.Tokens(c) + k := core.Tokens(c) cols := g.fileColumns(c) targets := s.targets() ui.Row(c).FillWidth().Gap(12).AlignItems(ui.Center).Children(func() { - mujica.Toolbar(c, mujica.ToolbarOptions{Label: g.t("File actions", "文件操作")}, func() { - g.fileActions(c, log, targets, func(m *mujica.PopupMenu) { g.fileMenu(m, log, targets) }) + navigation.Toolbar(c, navigation.ToolbarOptions{Label: g.t("File actions", "文件操作")}, func() { + g.fileActions(c, log, targets, func(m *overlay.PopupMenu) { g.fileMenu(m, log, targets) }) }).Grow(1) before := s.query - mujica.SearchInput(c, &s.query, mujica.SearchInputOptions{Placeholder: g.t("Filter files", "筛选文件")}).Element.Width(240) + input.SearchInput(c, &s.query, input.SearchInputOptions{Placeholder: g.t("Filter files", "筛选文件")}).Element.Width(240) if s.query != before { log(fmt.Sprintf("filter %q", s.query)) } }) s.table.Filter = s.query - shown := len(mujica.DataTableRows(s.entries, cols, s.table.Sort, s.query)) + shown := len(data.DataTableRows(s.entries, cols, s.table.Sort, s.query)) switch { case len(s.entries) == 0: - if mujica.EmptyState(c, mujica.EmptyStateOptions{Kind: mujica.EmptyNoData, Title: g.t("No files", "没有文件"), + if feedback.EmptyState(c, feedback.EmptyStateOptions{Kind: feedback.EmptyNoData, Title: g.t("No files", "没有文件"), Description: g.t("Drop files below or choose some to add them.", "将文件拖到下方,或选取文件以添加。"), Action: g.t("Restore samples", "恢复示例")}).Action { g.restoreFiles(log) } case shown == 0: - if mujica.EmptyState(c, mujica.EmptyStateOptions{Kind: mujica.EmptyNoResults, Query: s.query, Action: g.t("Clear filter", "清除筛选")}).Action { + if feedback.EmptyState(c, feedback.EmptyStateOptions{Kind: feedback.EmptyNoResults, Query: s.query, Action: g.t("Clear filter", "清除筛选")}).Action { s.query = "" log("filter cleared") } default: - v := mujica.DataTable(c, &s.table, s.entries, mujica.DataTableOptions[fileEntry, int]{ + v := data.DataTable(c, &s.table, s.entries, data.DataTableOptions[fileEntry, int]{ Columns: cols, Key: func(f fileEntry) int { return f.ID }, Multiple: true, Version: s.version, }) v.Element.FillWidth().Height(430) - mujica.ContextMenu(c, v.Element, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) { g.fileMenu(m, log, targets) }) + overlay.ContextMenu(c, v.Element, overlay.ContextMenuOptions{}, func(m *overlay.PopupMenu) { g.fileMenu(m, log, targets) }) if v.Changed() { log(fmt.Sprintf("selection %d checked", s.table.Selection.Len())) } @@ -168,12 +174,12 @@ func (g *gallery) filesView(c *ui.Context, log func(string)) { for _, f := range s.entries { size += f.Size } - mujica.StatusBar(c, mujica.StatusBarOptions{ - Left: []mujica.StatusItem{ + layout.StatusBar(c, layout.StatusBarOptions{ + Left: []layout.StatusItem{ {ID: "count", Icon: icons.Must("folder"), Text: fmt.Sprintf(g.t("%d files · %s", "%d 个文件 · %s"), len(s.entries), fileSize(size))}, {ID: "shown", Text: fmt.Sprintf(g.t("%d shown", "显示 %d 个"), shown), Secondary: true}, }, - Right: []mujica.StatusItem{{ID: "selected", Text: fmt.Sprintf(g.t("%d selected", "已选 %d 个"), s.table.Selection.Len())}}, + Right: []layout.StatusItem{{ID: "selected", Text: fmt.Sprintf(g.t("%d selected", "已选 %d 个"), s.table.Selection.Len())}}, }).Element.Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border) g.fileAdders(c, log) g.renameDialog(c, log) @@ -181,39 +187,39 @@ func (g *gallery) filesView(c *ui.Context, log func(string)) { } // fileActions fills the toolbar; menu fills the More dropdown, shared with the context menu. -func (g *gallery) fileActions(c *ui.Context, log func(string), targets []int, menu func(m *mujica.PopupMenu)) { - mujica.ToolbarGroup(c, func() { - if mujica.ToolbarButton(c, g.t("Rename", "重命名"), mujica.ToolbarButtonOptions{Icon: icons.Must("file-text"), ShowLabel: true, Disabled: len(targets) != 1}).Clicked() { +func (g *gallery) fileActions(c *ui.Context, log func(string), targets []int, menu func(m *overlay.PopupMenu)) { + navigation.ToolbarGroup(c, func() { + if navigation.ToolbarButton(c, g.t("Rename", "重命名"), navigation.ToolbarButtonOptions{Icon: icons.Must("file-text"), ShowLabel: true, Disabled: len(targets) != 1}).Clicked() { g.startRename(log, targets[0]) } - if mujica.ToolbarButton(c, g.t("Delete", "删除"), mujica.ToolbarButtonOptions{Icon: icons.Must("trash-2"), ShowLabel: true, Disabled: len(targets) == 0}).Clicked() { + if navigation.ToolbarButton(c, g.t("Delete", "删除"), navigation.ToolbarButtonOptions{Icon: icons.Must("trash-2"), ShowLabel: true, Disabled: len(targets) == 0}).Clicked() { g.startDelete(log, targets) } }) - mujica.ToolbarSeparator(c) - mujica.DropdownMenu(c, g.t("Actions", "操作"), mujica.DropdownMenuOptions{Icon: icons.Must("ellipsis")}, menu) + navigation.ToolbarSeparator(c) + overlay.DropdownMenu(c, g.t("Actions", "操作"), overlay.DropdownMenuOptions{Icon: icons.Must("ellipsis")}, menu) } // fileMenu holds every file action; the dropdown and the context menu share it. -func (g *gallery) fileMenu(m *mujica.PopupMenu, log func(string), targets []int) { +func (g *gallery) fileMenu(m *overlay.PopupMenu, log func(string), targets []int) { s := &g.cFiles - if m.Item(g.t("Rename…", "重命名…"), mujica.PopupMenuItemOptions{Disabled: len(targets) != 1}) { + if m.Item(g.t("Rename…", "重命名…"), overlay.PopupMenuItemOptions{Disabled: len(targets) != 1}) { g.startRename(log, targets[0]) } - if m.Item(g.t("Delete…", "删除…"), mujica.PopupMenuItemOptions{Disabled: len(targets) == 0}) { + if m.Item(g.t("Delete…", "删除…"), overlay.PopupMenuItemOptions{Disabled: len(targets) == 0}) { g.startDelete(log, targets) } - if m.Item(g.t("Select all", "全选"), mujica.PopupMenuItemOptions{Disabled: len(s.entries) == 0}) { + if m.Item(g.t("Select all", "全选"), overlay.PopupMenuItemOptions{Disabled: len(s.entries) == 0}) { for _, f := range s.entries { s.table.Selection.Add(f.ID) } log(fmt.Sprintf("selected all %d", len(s.entries))) } - if m.Item(g.t("Clear selection", "清除选择"), mujica.PopupMenuItemOptions{Disabled: s.table.Selection.Len() == 0}) { + if m.Item(g.t("Clear selection", "清除选择"), overlay.PopupMenuItemOptions{Disabled: s.table.Selection.Len() == 0}) { s.table.Selection.Clear() log("selection cleared") } - if m.Item(g.t("Restore samples", "恢复示例"), mujica.PopupMenuItemOptions{}) { + if m.Item(g.t("Restore samples", "恢复示例"), overlay.PopupMenuItemOptions{}) { g.restoreFiles(log) } } @@ -229,16 +235,16 @@ func (g *gallery) restoreFiles(log func(string)) { // fileAdders are the drop zone and the picker; both only list files, nothing is uploaded. func (g *gallery) fileAdders(c *ui.Context, log func(string)) { s := &g.cFiles - mujica.Card(c, mujica.CardOptions{Title: g.t("Add files", "添加文件")}, func() { - ui.Text(c, g.t("Files are checked and listed, never read or uploaded.", "文件只做校验并列出,不会读取或上传。")).TextColor(mujica.Tokens(c).TextMuted) - if mujica.FileDropZone(c, &s.dropped, mujica.FileDropZoneOptions{MaxFiles: 8, MaxSize: 50 << 20, + data.Card(c, data.CardOptions{Title: g.t("Add files", "添加文件")}, func() { + ui.Text(c, g.t("Files are checked and listed, never read or uploaded.", "文件只做校验并列出,不会读取或上传。")).TextColor(core.Tokens(c).TextMuted) + if input.FileDropZone(c, &s.dropped, input.FileDropZoneOptions{MaxFiles: 8, MaxSize: 50 << 20, Hint: g.t("Drop files here to list them", "将文件拖到此处以列出")}).Changed() { for _, f := range s.dropped { g.addFile(c, log, f.Path, f.Size, time.Now()) } s.dropped = nil } - p := mujica.FilePicker(c, &s.picked, mujica.FilePickerOptions{Multiple: true, Label: g.t("Choose files…", "选取文件…")}) + p := input.FilePicker(c, &s.picked, input.FilePickerOptions{Multiple: true, Label: g.t("Choose files…", "选取文件…")}) if p.Canceled() { log("picker canceled") } @@ -246,7 +252,7 @@ func (g *gallery) fileAdders(c *ui.Context, log func(string)) { for _, path := range s.picked { info, err := os.Stat(path) if err != nil { - mujica.ShowToast(c, mujica.ToastMessage{Severity: mujica.SeverityError, Message: err.Error()}) + feedback.ShowToast(c, feedback.ToastMessage{Severity: core.SeverityError, Message: err.Error()}) log("stat failed: " + err.Error()) continue } @@ -263,7 +269,7 @@ func (g *gallery) addFile(c *ui.Context, log func(string), path string, size int return } log("skipped duplicate " + filepath.Base(path)) - mujica.ShowToast(c, mujica.ToastMessage{ID: "dup-" + path, Severity: mujica.SeverityWarning, + feedback.ShowToast(c, feedback.ToastMessage{ID: "dup-" + path, Severity: core.SeverityWarning, Message: fmt.Sprintf(g.t("%s is already listed", "%s 已在列表中"), filepath.Base(path))}) } @@ -304,17 +310,17 @@ func (g *gallery) renameDialog(c *ui.Context, log func(string)) { s.version++ s.renaming = false } - mujica.Dialog(c, &s.renaming, mujica.DialogOptions{Title: g.t("Rename file", "重命名文件"), Actions: func() { - if mujica.Button(c, g.t("Cancel", "取消"), mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + overlay.Dialog(c, &s.renaming, overlay.DialogOptions{Title: g.t("Rename file", "重命名文件"), Actions: func() { + if input.Button(c, g.t("Cancel", "取消"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.renaming = false log("rename canceled") } - if mujica.Button(c, g.t("Rename", "重命名"), mujica.ButtonOptions{Disabled: bad != ""}).Clicked() { + if input.Button(c, g.t("Rename", "重命名"), input.ButtonOptions{Disabled: bad != ""}).Clicked() { commit() } }}, func() { - mujica.FormField(c, g.t("New name", "新名称"), mujica.FormFieldOptions{Error: bad}, func() *ui.Element { - r := mujica.TextInput(c, &s.newName, mujica.TextInputOptions{Error: bad}) + input.FormField(c, g.t("New name", "新名称"), input.FormFieldOptions{Error: bad}, func() *ui.Element { + r := input.TextInput(c, &s.newName, input.TextInputOptions{Error: bad}) if r.Submitted() { commit() } @@ -338,7 +344,7 @@ func (g *gallery) deleteDialog(c *ui.Context, log func(string)) { if len(s.doomed) == 1 { desc = fmt.Sprintf(g.t("“%s” will be removed from the list.", "将从列表中移除“%s”。"), s.entry(s.doomed[0]).Name) } - r := mujica.AlertDialog(c, &s.deleting, mujica.AlertDialogOptions{Title: g.t("Delete files?", "删除文件?"), Description: desc, + r := overlay.AlertDialog(c, &s.deleting, overlay.AlertDialogOptions{Title: g.t("Delete files?", "删除文件?"), Description: desc, ConfirmLabel: g.t("Delete permanently", "永久删除")}) switch { case r.Confirmed: @@ -348,7 +354,7 @@ func (g *gallery) deleteDialog(c *ui.Context, log func(string)) { } s.version++ log(fmt.Sprintf("deleted %d", len(s.doomed))) - mujica.ShowToast(c, mujica.ToastMessage{ID: "files-deleted", Severity: mujica.SeveritySuccess, + feedback.ShowToast(c, feedback.ToastMessage{ID: "files-deleted", Severity: core.SeveritySuccess, Message: fmt.Sprintf(g.t("Deleted %d files", "已删除 %d 个文件"), len(s.doomed))}) s.doomed = nil case r.Canceled: diff --git a/examples/gallery/composite_form.go b/examples/gallery/composite_form.go index 7f71a4b..5a25f5f 100644 --- a/examples/gallery/composite_form.go +++ b/examples/gallery/composite_form.go @@ -7,7 +7,11 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const formID = "c-form" @@ -28,10 +32,10 @@ const ( ) type formSubmitDemo struct { - st mujica.FormState + st input.FormState name string password string - debut mujica.Date + debut core.Date tags []string terms bool failNext bool @@ -65,7 +69,7 @@ func (g *gallery) formView(c *ui.Context, log func(string)) { drained = true } } - mujica.Card(c, mujica.CardOptions{Title: g.t("Audition entry", "试演报名")}, func() { + data.Card(c, data.CardOptions{Title: g.t("Audition entry", "试演报名")}, func() { switch s.phase { case formSucceeded: g.formSuccess(c, log) @@ -79,11 +83,11 @@ func (g *gallery) formView(c *ui.Context, log func(string)) { func (g *gallery) formEdit(c *ui.Context, log func(string)) { s := &g.cForm - if mujica.Switch(c, &s.failNext, g.t("The server rejects the next submit", "服务器拒绝下一次提交"), mujica.SwitchOptions{Disabled: s.st.Submitting()}).Changed() { + if input.Switch(c, &s.failNext, g.t("The server rejects the next submit", "服务器拒绝下一次提交"), input.SwitchOptions{Disabled: s.st.Submitting()}).Changed() { log(fmt.Sprintf("fail next submit %v", s.failNext)) } - r := mujica.LoadingOverlay(c, s.st.Submitting(), mujica.LoadingOverlayOptions{Label: g.t("Submitting…", "正在提交…")}, func() { - res := mujica.Form(c, &s.st, mujica.FormOptions{Horizontal: true, SubmitLabel: g.t("Submit entry", "提交报名")}, func(f *mujica.FormScope) { + r := feedback.LoadingOverlay(c, s.st.Submitting(), feedback.LoadingOverlayOptions{Label: g.t("Submitting…", "正在提交…")}, func() { + res := input.Form(c, &s.st, input.FormOptions{Horizontal: true, SubmitLabel: g.t("Submit entry", "提交报名")}, func(f *input.FormScope) { g.formFields(c, f, log) }) res.Element.FillWidth() @@ -108,9 +112,9 @@ func (g *gallery) formEdit(c *ui.Context, log func(string)) { r.Element.FillWidth() } -func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string)) { +func (g *gallery) formFields(c *ui.Context, f *input.FormScope, log func(string)) { s := &g.cForm - mujica.FormItem(c, f, "name", &s.name, mujica.FormItemOptions[string]{Label: g.t("Stage name", "艺名"), Required: true, + input.FormItem(c, f, "name", &s.name, input.FormItemOptions[string]{Label: g.t("Stage name", "艺名"), Required: true, Description: g.t("Checked against the archive; “Oblivionis” is taken.", "将与档案核对;“Oblivionis”已被占用。"), Validate: func(v string) string { if len([]rune(strings.TrimSpace(v))) < 2 { @@ -132,44 +136,44 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string c.Invalidate() }() }}, func() *ui.Element { - return mujica.TextInput(c, &s.name, mujica.TextInputOptions{}).Input + return input.TextInput(c, &s.name, input.TextInputOptions{}).Input }) - mujica.FormItem(c, f, "password", &s.password, mujica.FormItemOptions[string]{Label: g.t("Password", "密码"), Required: true, + input.FormItem(c, f, "password", &s.password, input.FormItemOptions[string]{Label: g.t("Password", "密码"), Required: true, Validate: func(v string) string { if len([]rune(v)) < 8 { return g.t("Use at least 8 characters.", "至少 8 个字符。") } return "" }}, func() *ui.Element { - r := mujica.PasswordInput(c, &s.password, mujica.PasswordInputOptions{}) + r := input.PasswordInput(c, &s.password, input.PasswordInputOptions{}) if r.Submitted() { s.st.Submit() } return r.Input }) - mujica.FormItem(c, f, "debut", &s.debut, mujica.FormItemOptions[mujica.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() *ui.Element { - r := mujica.DatePicker(c, &s.debut, mujica.DatePickerOptions{Clearable: true}) + input.FormItem(c, f, "debut", &s.debut, input.FormItemOptions[core.Date]{Label: g.t("Debut date", "首演日期"), Required: true}, func() *ui.Element { + r := datetime.DatePicker(c, &s.debut, datetime.DatePickerOptions{Clearable: true}) if r.Changed() { log("debut " + s.debut.Format("2006-01-02")) } return r.Input }) - mujica.FormItem(c, f, "roles", &s.tags, mujica.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true, + input.FormItem(c, f, "roles", &s.tags, input.FormItemOptions[[]string]{Label: g.t("Roles", "担当"), Required: true, Description: g.t("Up to 4; press Enter after each.", "最多 4 个;每个后按 Enter。")}, func() *ui.Element { - r := mujica.TagsInput(c, &s.tags, mujica.TagsInputOptions{Max: 4, Invalid: s.st.Error("roles") != "", Placeholder: g.t("keyboard, bass…", "键盘、贝斯…")}) + r := input.TagsInput(c, &s.tags, input.TagsInputOptions{Max: 4, Invalid: s.st.Error("roles") != "", Placeholder: g.t("keyboard, bass…", "键盘、贝斯…")}) if r.Changed() { log(fmt.Sprintf("roles %q", s.tags)) } return r.Input }) - mujica.FormItem(c, f, "terms", &s.terms, mujica.FormItemOptions[bool]{Label: g.t("Terms", "条款"), + input.FormItem(c, f, "terms", &s.terms, input.FormItemOptions[bool]{Label: g.t("Terms", "条款"), Validate: func(v bool) string { if !v { return g.t("Accept the terms to continue.", "请先同意条款。") } return "" }}, func() *ui.Element { - e := mujica.Checkbox(c, &s.terms, g.t("I accept the covenant of the masquerade", "我接受假面舞会的盟约"), mujica.CheckboxOptions{}) + e := input.Checkbox(c, &s.terms, g.t("I accept the covenant of the masquerade", "我接受假面舞会的盟约"), input.CheckboxOptions{}) if e.Changed() { log(fmt.Sprintf("terms %v", s.terms)) } @@ -179,7 +183,7 @@ func (g *gallery) formFields(c *ui.Context, f *mujica.FormScope, log func(string func (g *gallery) formSuccess(c *ui.Context, log func(string)) { s := &g.cForm - r := mujica.Result(c, mujica.ResultOptions{Kind: mujica.ResultSuccess, Title: g.t("Entry received", "报名已收到"), + r := feedback.Result(c, feedback.ResultOptions{Kind: feedback.ResultSuccess, Title: g.t("Entry received", "报名已收到"), Description: fmt.Sprintf(g.t("%s, your debut on %s is on the ledger.", "%s,你在 %s 的首演已登记。"), s.name, s.debut.Format("2006-01-02")), Actions: []string{g.t("New entry", "新的报名")}}) if r.Action == 0 { @@ -190,7 +194,7 @@ func (g *gallery) formSuccess(c *ui.Context, log func(string)) { func (g *gallery) formFailure(c *ui.Context, log func(string)) { s := &g.cForm - r := mujica.Result(c, mujica.ResultOptions{Kind: mujica.ResultFailure, Title: g.t("Entry not sent", "报名未送达"), Description: s.failure, + r := feedback.Result(c, feedback.ResultOptions{Kind: feedback.ResultFailure, Title: g.t("Entry not sent", "报名未送达"), Description: s.failure, Actions: []string{g.t("Retry", "重试"), g.t("Back to the form", "返回表单")}}) switch r.Action { case 0: diff --git a/examples/gallery/composite_settings.go b/examples/gallery/composite_settings.go index 3be957e..558edd5 100644 --- a/examples/gallery/composite_settings.go +++ b/examples/gallery/composite_settings.go @@ -6,8 +6,12 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) const settingsID = "c-settings" @@ -45,13 +49,13 @@ func newSettingsDemo() settingsDemo { func (g *gallery) settingsView(c *ui.Context, log func(string)) { s := &g.cSettings - mujica.Card(c, mujica.CardOptions{Title: g.t("Preferences", "偏好设置"), Footer: func() { g.settingsFooter(c, log) }}, func() { - tabs := []mujica.Tab{ + data.Card(c, data.CardOptions{Title: g.t("Preferences", "偏好设置"), Footer: func() { g.settingsFooter(c, log) }}, func() { + tabs := []navigation.Tab{ {Label: g.t("General", "通用"), Icon: icons.Must("settings"), Panel: func() { g.settingsGeneral(c, log) }}, {Label: g.t("Appearance", "外观"), Icon: icons.Must("palette"), Panel: func() { g.settingsAppearance(c, log) }}, {Label: g.t("Privacy", "隐私"), Icon: icons.Must("lock"), Panel: func() { g.settingsPrivacy(c, log) }}, } - if mujica.Tabs(c, &s.tab, tabs, mujica.TabsOptions{Label: g.t("Preference groups", "设置分组")}).Changed() { + if navigation.Tabs(c, &s.tab, tabs, navigation.TabsOptions{Label: g.t("Preference groups", "设置分组")}).Changed() { log("tab " + tabs[s.tab].Label) } }) @@ -74,36 +78,36 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) { d := &g.cSettings.draft settingsPanel(c, func() { bad := g.workspaceError(d.workspace) - mujica.FormField(c, g.t("Workspace name", "工作区名称"), mujica.FormFieldOptions{Required: true, Error: bad, + input.FormField(c, g.t("Workspace name", "工作区名称"), input.FormFieldOptions{Required: true, Error: bad, Description: g.t("Shown in the title bar.", "显示在标题栏中。")}, func() *ui.Element { - r := mujica.TextInput(c, &d.workspace, mujica.TextInputOptions{Error: bad}) + r := input.TextInput(c, &d.workspace, input.TextInputOptions{Error: bad}) if r.Changed() { log(fmt.Sprintf("workspace %q", d.workspace)) } return r.Input }) - mujica.FormField(c, g.t("Language", "语言"), mujica.FormFieldOptions{}, func() *ui.Element { - r := mujica.Select(c, &d.language, []mujica.SelectOption[string]{ + input.FormField(c, g.t("Language", "语言"), input.FormFieldOptions{}, func() *ui.Element { + r := input.Select(c, &d.language, []input.SelectOption[string]{ {Value: "en", Label: "English"}, {Value: "zh", Label: "简体中文"}, {Value: "ja", Label: "日本語"}, - }, mujica.SelectOptions{}) + }, input.SelectOptions{}) if r.Changed() { log("language " + d.language) } return r.Element }) - mujica.FormField(c, g.t("On startup", "启动时"), mujica.FormFieldOptions{}, func() *ui.Element { - r := mujica.RadioGroup(c, &d.startPage, []mujica.RadioItem[string]{ + input.FormField(c, g.t("On startup", "启动时"), input.FormFieldOptions{}, func() *ui.Element { + r := input.RadioGroup(c, &d.startPage, []input.RadioItem[string]{ {Value: "blank", Label: g.t("Open a blank page", "打开空白页")}, {Value: "last", Label: g.t("Reopen the last session", "恢复上次会话")}, {Value: "home", Label: g.t("Open the home page", "打开主页")}, - }, mujica.RadioGroupOptions{}) + }, input.RadioGroupOptions{}) if r.Changed() { log("startup " + d.startPage) } return r.Element }) - mujica.FormField(c, g.t("Login item", "登录项"), mujica.FormFieldOptions{}, func() *ui.Element { - e := mujica.Switch(c, &d.launch, g.t("Launch at login", "登录时启动"), mujica.SwitchOptions{}) + input.FormField(c, g.t("Login item", "登录项"), input.FormFieldOptions{}, func() *ui.Element { + e := input.Switch(c, &d.launch, g.t("Launch at login", "登录时启动"), input.SwitchOptions{}) if e.Changed() { log(fmt.Sprintf("launch at login %v", d.launch)) } @@ -115,33 +119,33 @@ func (g *gallery) settingsGeneral(c *ui.Context, log func(string)) { func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) { d := &g.cSettings.draft settingsPanel(c, func() { - mujica.FormField(c, g.t("Theme", "主题"), mujica.FormFieldOptions{}, func() *ui.Element { - r := mujica.RadioGroup(c, &d.theme, []mujica.RadioItem[string]{ + input.FormField(c, g.t("Theme", "主题"), input.FormFieldOptions{}, func() *ui.Element { + r := input.RadioGroup(c, &d.theme, []input.RadioItem[string]{ {Value: "system", Label: g.t("System", "跟随系统")}, {Value: "light", Label: g.t("Light", "浅色")}, {Value: "dark", Label: g.t("Dark", "深色")}, - }, mujica.RadioGroupOptions{Horizontal: true}) + }, input.RadioGroupOptions{Horizontal: true}) if r.Changed() { log("theme " + d.theme) } return r.Element }) - mujica.FormField(c, g.t("Text size", "文字大小"), mujica.FormFieldOptions{}, func() *ui.Element { - r := mujica.Slider(c, &d.fontSize, mujica.SliderOptions{Min: 12, Max: 20, Step: 1, ShowValue: true, - Marks: []mujica.SliderMark{{Value: 12, Label: "12"}, {Value: 16, Label: "16"}, {Value: 20, Label: "20"}}, + input.FormField(c, g.t("Text size", "文字大小"), input.FormFieldOptions{}, func() *ui.Element { + r := input.Slider(c, &d.fontSize, input.SliderOptions{Min: 12, Max: 20, Step: 1, ShowValue: true, + Marks: []input.SliderMark{{Value: 12, Label: "12"}, {Value: 16, Label: "16"}, {Value: 20, Label: "20"}}, Format: func(v float64) string { return fmt.Sprintf("%.0f pt", v) }}) if r.Changed() { log(fmt.Sprintf("text size %.0f", d.fontSize)) } return r.Element }) - mujica.FormField(c, g.t("Sidebar width", "侧栏宽度"), mujica.FormFieldOptions{Description: g.t("In DIPs, from 180 to 360.", "单位 DIP,180 至 360。")}, func() *ui.Element { - r := mujica.NumberInput(c, &d.sidebar, mujica.NumberInputOptions{Min: 180, Max: 360, Step: 10}) + input.FormField(c, g.t("Sidebar width", "侧栏宽度"), input.FormFieldOptions{Description: g.t("In DIPs, from 180 to 360.", "单位 DIP,180 至 360。")}, func() *ui.Element { + r := input.NumberInput(c, &d.sidebar, input.NumberInputOptions{Min: 180, Max: 360, Step: 10}) if r.Changed() { log(fmt.Sprintf("sidebar width %.0f", d.sidebar)) } return r.Input }) - mujica.FormField(c, g.t("Motion", "动效"), mujica.FormFieldOptions{}, func() *ui.Element { - e := mujica.Switch(c, &d.reduce, g.t("Reduce motion", "减少动效"), mujica.SwitchOptions{}) + input.FormField(c, g.t("Motion", "动效"), input.FormFieldOptions{}, func() *ui.Element { + e := input.Switch(c, &d.reduce, g.t("Reduce motion", "减少动效"), input.SwitchOptions{}) if e.Changed() { log(fmt.Sprintf("reduce motion %v", d.reduce)) } @@ -153,32 +157,32 @@ func (g *gallery) settingsAppearance(c *ui.Context, log func(string)) { func (g *gallery) settingsPrivacy(c *ui.Context, log func(string)) { d := &g.cSettings.draft settingsPanel(c, func() { - mujica.FormField(c, g.t("Usage data", "使用数据"), mujica.FormFieldOptions{Description: g.t("Nothing leaves this demo.", "本示例不会发送任何数据。")}, func() *ui.Element { - e := mujica.Switch(c, &d.telemetry, g.t("Share anonymous usage data", "共享匿名使用数据"), mujica.SwitchOptions{}) + input.FormField(c, g.t("Usage data", "使用数据"), input.FormFieldOptions{Description: g.t("Nothing leaves this demo.", "本示例不会发送任何数据。")}, func() *ui.Element { + e := input.Switch(c, &d.telemetry, g.t("Share anonymous usage data", "共享匿名使用数据"), input.SwitchOptions{}) if e.Changed() { log(fmt.Sprintf("usage data %v", d.telemetry)) } return e }) - mujica.FormField(c, g.t("Crash reports", "崩溃报告"), mujica.FormFieldOptions{}, func() *ui.Element { - e := mujica.Switch(c, &d.crashes, g.t("Send crash reports", "发送崩溃报告"), mujica.SwitchOptions{}) + input.FormField(c, g.t("Crash reports", "崩溃报告"), input.FormFieldOptions{}, func() *ui.Element { + e := input.Switch(c, &d.crashes, g.t("Send crash reports", "发送崩溃报告"), input.SwitchOptions{}) if e.Changed() { log(fmt.Sprintf("crash reports %v", d.crashes)) } return e }) - mujica.FormField(c, g.t("Keep history", "保留历史"), mujica.FormFieldOptions{Description: g.t("Days, from 0 to 365; 0 keeps none.", "天数,0 至 365;0 表示不保留。")}, func() *ui.Element { - r := mujica.NumberInput(c, &d.history, mujica.NumberInputOptions{Min: 0, Max: 365, Step: 1}) + input.FormField(c, g.t("Keep history", "保留历史"), input.FormFieldOptions{Description: g.t("Days, from 0 to 365; 0 keeps none.", "天数,0 至 365;0 表示不保留。")}, func() *ui.Element { + r := input.NumberInput(c, &d.history, input.NumberInputOptions{Min: 0, Max: 365, Step: 1}) if r.Changed() { log(fmt.Sprintf("history %.0f days", d.history)) } return r.Input }) - mujica.FormField(c, g.t("Lock after", "自动锁定"), mujica.FormFieldOptions{}, func() *ui.Element { - r := mujica.Select(c, &d.lock, []mujica.SelectOption[string]{ + input.FormField(c, g.t("Lock after", "自动锁定"), input.FormFieldOptions{}, func() *ui.Element { + r := input.Select(c, &d.lock, []input.SelectOption[string]{ {Value: "never", Label: g.t("Never", "从不")}, {Value: "5", Label: g.t("5 minutes", "5 分钟")}, {Value: "15", Label: g.t("15 minutes", "15 分钟")}, {Value: "60", Label: g.t("1 hour", "1 小时")}, - }, mujica.SelectOptions{}) + }, input.SelectOptions{}) if r.Changed() { log("lock after " + d.lock) } @@ -194,19 +198,19 @@ func (g *gallery) settingsFooter(c *ui.Context, log func(string)) { invalid := g.workspaceError(s.draft.workspace) != "" ui.Row(c).FillWidth().AlignItems(ui.Center).Gap(12).Children(func() { if dirty { - mujica.StatusIndicator(c, mujica.StatusWarning, mujica.StatusIndicatorOptions{Label: g.t("Unsaved changes", "有未保存的更改")}) + feedback.StatusIndicator(c, feedback.StatusWarning, feedback.StatusIndicatorOptions{Label: g.t("Unsaved changes", "有未保存的更改")}) } else { - mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: g.t("All changes saved", "更改均已保存")}) + feedback.StatusIndicator(c, feedback.StatusSuccess, feedback.StatusIndicatorOptions{Label: g.t("All changes saved", "更改均已保存")}) } ui.Box(c).Grow(1) - if mujica.Button(c, g.t("Reset", "重置"), mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.draft == settingsInitial}).Clicked() { + if input.Button(c, g.t("Reset", "重置"), input.ButtonOptions{Variant: input.Secondary, Disabled: s.draft == settingsInitial}).Clicked() { s.draft = settingsInitial log("reset to initial values") } - if mujica.Button(c, g.t("Save", "保存"), mujica.ButtonOptions{Disabled: !dirty || invalid}).Clicked() { + if input.Button(c, g.t("Save", "保存"), input.ButtonOptions{Disabled: !dirty || invalid}).Clicked() { s.saved = s.draft log("saved") - mujica.ShowToast(c, mujica.ToastMessage{ID: "settings-saved", Severity: mujica.SeveritySuccess, Message: g.t("Settings saved", "设置已保存")}) + feedback.ShowToast(c, feedback.ToastMessage{ID: "settings-saved", Severity: core.SeveritySuccess, Message: g.t("Settings saved", "设置已保存")}) } }) } diff --git a/examples/gallery/nav.go b/examples/gallery/nav.go index e5301bf..0e0f1bc 100644 --- a/examples/gallery/nav.go +++ b/examples/gallery/nav.go @@ -5,8 +5,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) const overtureID = "overture" @@ -83,19 +84,19 @@ func (g *gallery) categoryName(cat string) string { func (g *gallery) pageName(p page) string { return g.t(p.Name, p.ZhName) } // sections groups the pages that match the query by category, in PRD order. -func (g *gallery) sections() []mujica.SidebarSection { - out := []mujica.SidebarSection{{Title: g.t("Overture", "序章"), Items: []mujica.SidebarItem{ +func (g *gallery) sections() []navigation.SidebarSection { + out := []navigation.SidebarSection{{Title: g.t("Overture", "序章"), Items: []navigation.SidebarItem{ {ID: overtureID, Label: g.t("Overture", "序章"), Icon: icons.Must("house")}, }}} - var comps []mujica.SidebarItem + var comps []navigation.SidebarItem q := strings.ToLower(strings.TrimSpace(g.query)) for _, cp := range compositions { if q == "" || strings.Contains(strings.ToLower(cp.ID+" "+cp.Name+" "+cp.ZhName), q) { - comps = append(comps, mujica.SidebarItem{ID: cp.ID, Label: g.t(cp.Name, cp.ZhName), Icon: icons.Must(cp.Icon)}) + comps = append(comps, navigation.SidebarItem{ID: cp.ID, Label: g.t(cp.Name, cp.ZhName), Icon: icons.Must(cp.Icon)}) } } if len(comps) > 0 { - out = append(out, mujica.SidebarSection{Title: g.t("Compositions", "组合示例"), Items: comps}) + out = append(out, navigation.SidebarSection{Title: g.t("Compositions", "组合示例"), Items: comps}) } index := map[string]int{} for _, p := range g.pages { @@ -106,9 +107,9 @@ func (g *gallery) sections() []mujica.SidebarSection { if !ok { i = len(out) index[p.Category] = i - out = append(out, mujica.SidebarSection{Title: g.categoryName(p.Category)}) + out = append(out, navigation.SidebarSection{Title: g.categoryName(p.Category)}) } - out[i].Items = append(out[i].Items, mujica.SidebarItem{ID: p.ID, Label: p.ID + " " + g.pageName(p)}) + out[i].Items = append(out[i].Items, navigation.SidebarItem{ID: p.ID, Label: p.ID + " " + g.pageName(p)}) } return out } @@ -116,7 +117,7 @@ func (g *gallery) sections() []mujica.SidebarSection { func (g *gallery) sidebar(c *ui.Context) { ui.Column(c).FillWidth().Gap(8).Padding(8, 8, 0, 8).Children(func() { before := g.query - mujica.SearchInput(c, &g.query, mujica.SearchInputOptions{Placeholder: g.t("Search · 搜索", "搜索 · Search")}) + input.SearchInput(c, &g.query, input.SearchInputOptions{Placeholder: g.t("Search · 搜索", "搜索 · Search")}) if g.query != before { g.logFor("shell")("search " + g.query) } @@ -125,27 +126,27 @@ func (g *gallery) sidebar(c *ui.Context) { if len(sections) == 1 && g.query != "" { ui.Text(c, g.t("No component matches.", "没有匹配的组件。")).Padding(8, 16) } - if mujica.Sidebar(c, &g.selected, sections, mujica.SidebarOptions{}).Changed() { + if navigation.Sidebar(c, &g.selected, sections, navigation.SidebarOptions{}).Changed() { g.logFor("shell")("open " + g.selected) } } func (g *gallery) commandPalette(c *ui.Context) { - cmds := []mujica.Command{ + cmds := []navigation.Command{ {ID: overtureID, Label: g.t("Overture", "序章"), Group: g.t("Go to", "前往")}, {ID: "theme", Label: g.t("Cycle theme", "切换主题"), Group: g.t("Window", "窗口"), Keywords: []string{"dark", "light", "深色", "浅色"}}, {ID: "density", Label: g.t("Cycle density", "切换密度"), Group: g.t("Window", "窗口"), Keywords: []string{"compact", "紧凑"}}, {ID: "locale", Label: g.t("Switch to Chinese", "切换到英文"), Group: g.t("Window", "窗口"), Keywords: []string{"language", "语言"}}, } for _, cp := range compositions { - cmds = append(cmds, mujica.Command{ID: "page:" + cp.ID, Label: g.t(cp.Name, cp.ZhName), Group: g.t("Compositions", "组合示例"), + cmds = append(cmds, navigation.Command{ID: "page:" + cp.ID, Label: g.t(cp.Name, cp.ZhName), Group: g.t("Compositions", "组合示例"), Keywords: []string{cp.Name, cp.ZhName, "composition", "组合"}}) } for _, p := range g.pages { - cmds = append(cmds, mujica.Command{ID: "page:" + p.ID, Label: p.ID + " " + g.pageName(p), Group: g.categoryName(p.Category), + cmds = append(cmds, navigation.Command{ID: "page:" + p.ID, Label: p.ID + " " + g.pageName(p), Group: g.categoryName(p.Category), Keywords: []string{p.Name, p.ZhName, p.Category, categoryZh[p.Category]}}) } - id, ok := mujica.CommandPalette(c, &g.palette, cmds, mujica.CommandPaletteOptions{}).Chosen() + id, ok := navigation.CommandPalette(c, &g.palette, cmds, navigation.CommandPaletteOptions{}).Chosen() if !ok { return } diff --git a/examples/gallery/overture.go b/examples/gallery/overture.go index d928c2a..64c8bbf 100644 --- a/examples/gallery/overture.go +++ b/examples/gallery/overture.go @@ -5,8 +5,12 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" ) // overtureState is the continuity proof: values that must survive theme changes. @@ -18,11 +22,11 @@ type overtureState struct { } func (g *gallery) overturePage(c *ui.Context) { - k := mujica.Tokens(c) + k := core.Tokens(c) w, _ := c.Size() - mujica.Container(c, mujica.ContainerOptions{MaxWidth: 1080, Center: true}, func() { + layout.Container(c, layout.ContainerOptions{MaxWidth: 1080, Center: true}, func() { ui.Column(c).FillWidth().Gap(32).Children(func() { - mujica.Heading(c, "MujicaUI", mujica.HeadingOptions{ + display.Heading(c, "MujicaUI", display.HeadingOptions{ Subtitle: g.t("One hundred court-gothic components for MyGo native UI.", "为 MyGo 原生界面打造的一百件宫廷哥特组件。"), Divider: true, }) @@ -30,10 +34,10 @@ func (g *gallery) overturePage(c *ui.Context) { "Every pixel here is drawn by Go. No HTML, no WebView. Ivory and charcoal, a wine accent, old gold on the edges.", "这里每一个像素都由 Go 绘制。没有 HTML,没有 WebView。象牙白与炭黑,酒红为主色,旧金只落在边缘。", )).TextColor(k.TextMuted).FillWidth() - mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fr(1), ui.Fr(1), ui.Fr(1)}}, func() { - mujica.Statistic(c, float64(len(g.pages)), mujica.StatisticOptions{Title: g.t("Components", "组件")}) - mujica.Statistic(c, 2, mujica.StatisticOptions{Title: g.t("Themes", "主题")}) - mujica.Statistic(c, 3, mujica.StatisticOptions{Title: g.t("Densities", "密度")}) + layout.Grid(c, layout.GridOptions{Columns: []ui.Track{ui.Fr(1), ui.Fr(1), ui.Fr(1)}}, func() { + data.Statistic(c, float64(len(g.pages)), data.StatisticOptions{Title: g.t("Components", "组件")}) + data.Statistic(c, 2, data.StatisticOptions{Title: g.t("Themes", "主题")}) + data.Statistic(c, 3, data.StatisticOptions{Title: g.t("Densities", "密度")}) }) g.continuity(c) g.categoryGrid(c, w) @@ -44,29 +48,29 @@ func (g *gallery) overturePage(c *ui.Context) { // continuity holds live state while the user flips theme and density. func (g *gallery) continuity(c *ui.Context) { s := &g.overture - k := mujica.Tokens(c) - mujica.Card(c, mujica.CardOptions{Title: g.t("Proof of continuity", "连续性证明"), Decorated: true}, func() { + k := core.Tokens(c) + data.Card(c, data.CardOptions{Title: g.t("Proof of continuity", "连续性证明"), Decorated: true}, func() { ui.Column(c).FillWidth().Gap(16).Children(func() { ui.Text(c, g.t( "Type a line, move the slider, then switch theme or density above, or press ⌘K. Nothing resets: not the text, not the caret, not the focus.", "写下一行字,拖动滑块,再切换上方的主题或密度,或按 ⌘K。什么都不会重置:文字、光标、焦点都留在原处。", )).FillWidth() - if mujica.TextInput(c, &s.note, mujica.TextInputOptions{Placeholder: g.t("A line for the court ledger", "写给宫廷账簿的一行"), Label: g.t("Ledger line", "账簿")}).Changed() { + if input.TextInput(c, &s.note, input.TextInputOptions{Placeholder: g.t("A line for the court ledger", "写给宫廷账簿的一行"), Label: g.t("Ledger line", "账簿")}).Changed() { g.logFor(overtureID)("ledger line edited") } - if mujica.Slider(c, &s.tempo, mujica.SliderOptions{Max: 100, Step: 1, ShowValue: true, Label: g.t("Tempo", "速度")}).Changed() { + if input.Slider(c, &s.tempo, input.SliderOptions{Max: 100, Step: 1, ShowValue: true, Label: g.t("Tempo", "速度")}).Changed() { g.logFor(overtureID)(fmt.Sprintf("tempo %.0f", s.tempo)) } - if mujica.Checkbox(c, &s.sealed, g.t("Seal with wax", "以火漆封缄"), mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.sealed, g.t("Seal with wax", "以火漆封缄"), input.CheckboxOptions{}).Changed() { g.logFor(overtureID)(fmt.Sprintf("sealed %v", s.sealed)) } ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { - if mujica.Button(c, g.t("Cycle theme", "切换主题"), mujica.ButtonOptions{Icon: icons.Must("moon")}).Clicked() { + if input.Button(c, g.t("Cycle theme", "切换主题"), input.ButtonOptions{Icon: icons.Must("moon")}).Clicked() { g.mode = (g.mode + 1) % 3 s.switches++ - g.logFor(overtureID)("theme " + mujica.Mode(g.mode).String()) + g.logFor(overtureID)("theme " + core.Mode(g.mode).String()) } - if mujica.Button(c, g.t("Cycle density", "切换密度"), mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, g.t("Cycle density", "切换密度"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { g.density = (g.density + 1) % len(densities) s.switches++ g.logFor(overtureID)("density " + densities[g.density].String()) @@ -98,17 +102,17 @@ func (g *gallery) categoryGrid(c *ui.Context, width float32) { } count[p.Category]++ } - mujica.Grid(c, mujica.GridOptions{Columns: tracks}, func() { + layout.Grid(c, layout.GridOptions{Columns: tracks}, func() { for _, cat := range cats { ui.Box(c).Key(cat).Children(func() { - mujica.Card(c, mujica.CardOptions{Title: g.categoryName(cat), Footer: func() { - if mujica.Button(c, g.t("Enter", "进入"), mujica.ButtonOptions{Variant: mujica.Outline}).Clicked() { + data.Card(c, data.CardOptions{Title: g.categoryName(cat), Footer: func() { + if input.Button(c, g.t("Enter", "进入"), input.ButtonOptions{Variant: input.Outline}).Clicked() { g.selected = first[cat].ID g.logFor(overtureID)("enter " + cat) } }}, func() { ui.Text(c, fmt.Sprintf(g.t("%d components, from %s", "%d 个组件,始于 %s"), count[cat], g.pageName(first[cat]))). - TextColor(mujica.Tokens(c).TextMuted) + TextColor(core.Tokens(c).TextMuted) }) }) } diff --git a/examples/gallery/page_view.go b/examples/gallery/page_view.go index 105cc96..7487881 100644 --- a/examples/gallery/page_view.go +++ b/examples/gallery/page_view.go @@ -5,32 +5,37 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // componentPage shows one component: demo, code, keys and its events. func (g *gallery) componentPage(c *ui.Context, p page) { - k := mujica.Tokens(c) - mujica.Container(c, mujica.ContainerOptions{MaxWidth: 1080, Center: true}, func() { + k := core.Tokens(c) + layout.Container(c, layout.ContainerOptions{MaxWidth: 1080, Center: true}, func() { ui.Column(c).FillWidth().Gap(24).Children(func() { - mujica.Heading(c, g.pageName(p), mujica.HeadingOptions{ + display.Heading(c, g.pageName(p), display.HeadingOptions{ Subtitle: fmt.Sprintf("%s · %s · %s", p.ID, g.t(p.ZhName, p.Name), g.categoryName(p.Category)), Divider: true, }) ui.Column(c).FillWidth().Gap(16).Children(func() { p.Demo(c, g.logFor(p.ID)) }) - mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fr(3), ui.Fr(2)}, Gap: mujica.SpaceL}, func() { - mujica.Card(c, mujica.CardOptions{Title: g.t("Code", "代码"), Footer: func() { - if mujica.Button(c, g.t("Copy", "复制"), mujica.ButtonOptions{Variant: mujica.Secondary, Icon: icons.Must("copy")}).Clicked() { + layout.Grid(c, layout.GridOptions{Columns: []ui.Track{ui.Fr(3), ui.Fr(2)}, Gap: layout.SpaceL}, func() { + data.Card(c, data.CardOptions{Title: g.t("Code", "代码"), Footer: func() { + if input.Button(c, g.t("Copy", "复制"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("copy")}).Clicked() { c.WriteClipboard(p.Code) g.logFor(p.ID)("code copied") - mujica.ShowToast(c, mujica.ToastMessage{ID: "copied", Severity: mujica.SeveritySuccess, Message: g.t("Code copied", "代码已复制")}) + feedback.ShowToast(c, feedback.ToastMessage{ID: "copied", Severity: core.SeveritySuccess, Message: g.t("Code copied", "代码已复制")}) } }}, func() { - ui.Text(c, p.Code).Font(theme.MonoFont).FontSize(mujica.FontSize(c, theme.CaptionSize)).Selectable().FillWidth() + ui.Text(c, p.Code).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).Selectable().FillWidth() }) - mujica.Card(c, mujica.CardOptions{Title: g.t("Keyboard", "键盘")}, func() { + data.Card(c, data.CardOptions{Title: g.t("Keyboard", "键盘")}, func() { ui.Text(c, p.Keys).TextColor(k.TextMuted).FillWidth() }) }) @@ -41,8 +46,8 @@ func (g *gallery) componentPage(c *ui.Context, p page) { // eventCard lists the newest events of one page. func (g *gallery) eventCard(c *ui.Context, id string) { - k := mujica.Tokens(c) - mujica.Card(c, mujica.CardOptions{Title: g.t("Events", "事件")}, func() { + k := core.Tokens(c) + data.Card(c, data.CardOptions{Title: g.t("Events", "事件")}, func() { shown := 0 for i := len(g.events) - 1; i >= 0 && shown < 8; i-- { e := g.events[i] @@ -51,7 +56,7 @@ func (g *gallery) eventCard(c *ui.Context, id string) { } shown++ ui.Row(c).Gap(12).Children(func() { - ui.Text(c, e.at.Format("15:04:05.000")).Font(theme.MonoFont).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, e.at.Format("15:04:05.000")).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) ui.Text(c, e.text).Grow(1) }) } @@ -64,9 +69,9 @@ func (g *gallery) eventCard(c *ui.Context, id string) { // compositePage shows one composition demo above its events. func (g *gallery) compositePage(c *ui.Context, id string, body func(c *ui.Context, log func(string))) { cp := compositionOf(id) - mujica.Container(c, mujica.ContainerOptions{MaxWidth: 1080, Center: true}, func() { + layout.Container(c, layout.ContainerOptions{MaxWidth: 1080, Center: true}, func() { ui.Column(c).FillWidth().Gap(24).Children(func() { - mujica.Heading(c, g.t(cp.Name, cp.ZhName), mujica.HeadingOptions{Subtitle: g.t(cp.About, cp.ZhAbout), Divider: true}) + display.Heading(c, g.t(cp.Name, cp.ZhName), display.HeadingOptions{Subtitle: g.t(cp.About, cp.ZhAbout), Divider: true}) ui.Column(c).FillWidth().Gap(16).Children(func() { body(c, g.logFor(id)) }) g.eventCard(c, id) }) diff --git a/examples/gallery/pages_account.go b/examples/gallery/pages_account.go index 7a27e3c..d5e1c18 100644 --- a/examples/gallery/pages_account.go +++ b/examples/gallery/pages_account.go @@ -5,7 +5,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/account" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const accountCategory = "Settings & Account" @@ -58,9 +60,9 @@ type accountSettingsState struct { sound, digest, report bool } -var accountSettingPages = []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} +var accountSettingPages = []account.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} -var accountSettingEntries = []mujica.SettingEntry{ +var accountSettingEntries = []account.SettingEntry{ {ID: "sound", Page: "general", Label: "Sound", Description: "Chime on new messages.", Keywords: []string{"audio", "notification"}}, {ID: "digest", Page: "general", Label: "Weekly digest", Description: "A summary every Monday.", Keywords: []string{"email"}}, {ID: "report", Page: "privacy", Label: "Usage reports", Description: "Send anonymous usage."}, @@ -69,15 +71,15 @@ var accountSettingEntries = []mujica.SettingEntry{ // accountSettingsPage builds the demo pages shared by 461 and 462. func accountSettingsPage(c *ui.Context, d *accountSettingsState, log func(string)) { ui.Box(c).Width(900).Height(320).Children(func() { - if mujica.SettingsLayout(c, &d.page, accountSettingPages, mujica.SettingsLayoutOptions{}, func(id string) { + if account.SettingsLayout(c, &d.page, accountSettingPages, account.SettingsLayoutOptions{}, func(id string) { switch id { case "general": - mujica.SettingsSection(c, "Notifications", func() { + account.SettingsSection(c, "Notifications", func() { accountSwitchRow(c, "sound", "Sound", "Chime on new messages.", &d.sound, log) accountSwitchRow(c, "digest", "Weekly digest", "A summary every Monday.", &d.digest, log) }) case "privacy": - mujica.SettingsSection(c, "Data", func() { + account.SettingsSection(c, "Data", func() { accountSwitchRow(c, "report", "Usage reports", "Send anonymous usage.", &d.report, log) }) } @@ -89,8 +91,8 @@ func accountSettingsPage(c *ui.Context, d *accountSettingsState, log func(string // accountSwitchRow is a settings row with a switch. func accountSwitchRow(c *ui.Context, id, label, desc string, v *bool, log func(string)) { - mujica.SettingsRow(c, label, mujica.SettingsRowOptions{ID: id, Description: desc}, func() *ui.Element { - sw := mujica.Switch(c, v, "", mujica.SwitchOptions{}) + account.SettingsRow(c, label, account.SettingsRowOptions{ID: id, Description: desc}, func() *ui.Element { + sw := input.Switch(c, v, "", input.SwitchOptions{}) if sw.Changed() { log(fmt.Sprintf("%s %v", label, *v)) } @@ -106,7 +108,7 @@ func accountSettingsDemo(c *ui.Context, log func(string)) { func accountSearchDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "462", func() accountSettingsState { return accountSettingsState{sound: true} }) ui.Box(c).Width(320).Children(func() { - if e, ok := mujica.SettingsSearch(c, &d.query, accountSettingEntries, mujica.SettingsSearchOptions{}).Chosen(); ok { + if e, ok := account.SettingsSearch(c, &d.query, accountSettingEntries, account.SettingsSearchOptions{}).Chosen(); ok { d.page = e.Page log("jump to " + e.ID) } @@ -115,14 +117,14 @@ func accountSearchDemo(c *ui.Context, log func(string)) { } type accountThemeState struct { - mode mujica.Mode + mode core.Mode disabled bool } func accountThemeDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "463", func() accountThemeState { return accountThemeState{} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - if mujica.ThemeSelector(c, &d.mode, mujica.ThemeSelectorOptions{Disabled: d.disabled}).Changed() { + if account.ThemeSelector(c, &d.mode, account.ThemeSelectorOptions{Disabled: d.disabled}).Changed() { log("theme " + d.mode.String()) } } @@ -135,10 +137,10 @@ type accountAccentState struct { func accountAccentDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "464", func() accountAccentState { return accountAccentState{color: ui.Hex("#7D2034")} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - if mujica.AccentColorPicker(c, &d.color, mujica.AccentColorPickerOptions{Disabled: d.disabled}).Changed() { + if account.AccentColorPicker(c, &d.color, account.AccentColorPickerOptions{Disabled: d.disabled}).Changed() { log(fmt.Sprintf("accent %v", d.color)) } - mujica.Button(c, "Preview button", mujica.ButtonOptions{}).Background(d.color) + input.Button(c, "Preview button", input.ButtonOptions{}).Background(d.color) } func accountToggles(c *ui.Context, log func(string), labels []string, values ...*bool) { diff --git a/examples/gallery/pages_account_auth.go b/examples/gallery/pages_account_auth.go index 47dc01b..821985c 100644 --- a/examples/gallery/pages_account_auth.go +++ b/examples/gallery/pages_account_auth.go @@ -6,27 +6,27 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/account" ) type accountRecorderState struct { - palette, zen mujica.Shortcut + palette, zen account.Shortcut disabled bool } -var accountTaken = []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}} +var accountTaken = []account.ShortcutBinding{{Action: "Save", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}} func accountRecorderDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "465", func() accountRecorderState { - return accountRecorderState{palette: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}} + return accountRecorderState{palette: account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) ui.Text(c, "Cmd/Ctrl+S is taken by Save.") for _, r := range []struct { name string - v *mujica.Shortcut + v *account.Shortcut }{{"Command palette", &d.palette}, {"Zen mode", &d.zen}} { - if mujica.ShortcutRecorder(c, r.v, mujica.ShortcutRecorderOptions{Label: r.name, Taken: accountTaken, Disabled: d.disabled}).Changed() { + if account.ShortcutRecorder(c, r.v, account.ShortcutRecorderOptions{Label: r.name, Taken: accountTaken, Disabled: d.disabled}).Changed() { log(fmt.Sprintf("%s = %s", r.name, r.v.String())) } } @@ -36,27 +36,27 @@ type accountShortcutsState struct{ query string } func accountShortcutsDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "466", func() accountShortcutsState { return accountShortcutsState{} }) - groups := []mujica.ShortcutGroup{ - {Title: "General", Bindings: []mujica.ShortcutBinding{ - {Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, - {Action: "Settings", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyComma}}, - {Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}, + groups := []account.ShortcutGroup{ + {Title: "General", Bindings: []account.ShortcutBinding{ + {Action: "Command palette", Shortcut: account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, + {Action: "Settings", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyComma}}, + {Action: "Save", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}, }}, - {Title: "Editing", Bindings: []mujica.ShortcutBinding{ - {Action: "Duplicate line", Shortcut: mujica.Shortcut{Mods: ui.Alt | ui.Shift, Key: ui.KeyDown}}, - {Action: "Toggle comment", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeySlash}}, + {Title: "Editing", Bindings: []account.ShortcutBinding{ + {Action: "Duplicate line", Shortcut: account.Shortcut{Mods: ui.Alt | ui.Shift, Key: ui.KeyDown}}, + {Action: "Toggle comment", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeySlash}}, {Action: "Zen mode"}, }}, } before := d.query - ui.Box(c).Width(480).Children(func() { mujica.KeyboardShortcutsList(c, &d.query, groups, mujica.KeyboardShortcutsListOptions{}) }) + ui.Box(c).Width(480).Children(func() { account.KeyboardShortcutsList(c, &d.query, groups, account.KeyboardShortcutsListOptions{}) }) if d.query != before { log(fmt.Sprintf("search %q", d.query)) } } type accountLoginState struct { - form mujica.LoginFormState + form account.LoginFormState disabled, reject bool attempts int } @@ -71,7 +71,7 @@ func accountLoginDemo(c *ui.Context, log func(string)) { default: } accountToggles(c, log, []string{"Disabled", "Server rejects"}, &d.disabled, &d.reject) - mujica.LoginForm(c, &d.form, mujica.LoginFormOptions{Disabled: d.disabled, OnSubmit: func(account, password string, remember bool) { + account.LoginForm(c, &d.form, account.LoginFormOptions{Disabled: d.disabled, OnSubmit: func(account, password string, remember bool) { // Credentials stay in the caller's sign-in request. d.attempts++ log(fmt.Sprintf("submit %q, remember %v", account, remember)) @@ -98,8 +98,8 @@ type accountOAuthState struct { func accountOAuthDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "468", func() accountOAuthState { return accountOAuthState{} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - providers := []mujica.OAuthProvider{{ID: "git", Name: "Git host"}, {ID: "sso", Name: "Company SSO"}, {ID: "mail", Name: "Email link"}} - if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{Pending: d.pending, Disabled: d.disabled}).Chosen(); ok { + providers := []account.OAuthProvider{{ID: "git", Name: "Git host"}, {ID: "sso", Name: "Company SSO"}, {ID: "mail", Name: "Email link"}} + if id, ok := account.OAuthButtons(c, providers, account.OAuthButtonsOptions{Pending: d.pending, Disabled: d.disabled}).Chosen(); ok { d.pending = id log("continue with " + id) } @@ -119,7 +119,7 @@ type accountTwoFactorState struct { func accountTwoFactorDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "469", func() accountTwoFactorState { return accountTwoFactorState{} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - r := mujica.TwoFactorInput(c, &d.code, mujica.TwoFactorInputOptions{ResendAt: d.resendAt, Error: d.err, Disabled: d.disabled}) + r := account.TwoFactorInput(c, &d.code, account.TwoFactorInputOptions{ResendAt: d.resendAt, Error: d.err, Disabled: d.disabled}) if r.Completed() { // The code is checked here and never logged. d.err = "" @@ -135,17 +135,17 @@ func accountTwoFactorDemo(c *ui.Context, log func(string)) { } type accountProfileState struct { - saved, edit mujica.Profile + saved, edit account.Profile saving, disabled bool } func accountProfileDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "470", func() accountProfileState { - p := mujica.Profile{Name: "Sakiko Togawa", Bio: "Keyboard, Ave Mujica."} + p := account.Profile{Name: "Sakiko Togawa", Bio: "Keyboard, Ave Mujica."} return accountProfileState{saved: p, edit: p} }) accountToggles(c, log, []string{"Saving", "Disabled"}, &d.saving, &d.disabled) - r := mujica.ProfileEditor(c, &d.edit, mujica.ProfileEditorOptions{Saved: d.saved, Saving: d.saving, Disabled: d.disabled}) + r := account.ProfileEditor(c, &d.edit, account.ProfileEditorOptions{Saved: d.saved, Saving: d.saving, Disabled: d.disabled}) if r.Save() { d.saved = d.edit log("saved") diff --git a/examples/gallery/pages_account_code.go b/examples/gallery/pages_account_code.go index 612f69c..03cd0bb 100644 --- a/examples/gallery/pages_account_code.go +++ b/examples/gallery/pages_account_code.go @@ -4,71 +4,71 @@ package main var accountCode = map[string]string{ "SettingsLayout": `page := "" sound := true -pages := []mujica.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} +pages := []account.SettingsPage{{ID: "general", Label: "General"}, {ID: "privacy", Label: "Privacy"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SettingsLayout(c, &page, pages, mujica.SettingsLayoutOptions{}, func(id string) { - mujica.SettingsSection(c, "Notifications", func() { - mujica.SettingsRow(c, "Sound", mujica.SettingsRowOptions{ID: "sound"}, func() *ui.Element { - return mujica.Switch(c, &sound, "", mujica.SwitchOptions{}) + core.Use(c, core.Settings{}) + account.SettingsLayout(c, &page, pages, account.SettingsLayoutOptions{}, func(id string) { + account.SettingsSection(c, "Notifications", func() { + account.SettingsRow(c, "Sound", account.SettingsRowOptions{ID: "sound"}, func() *ui.Element { + return input.Switch(c, &sound, "", input.SwitchOptions{}) }) }) }) } ui.Render(view, 400, 300, 1)`, "SettingsSearch": `query, page := "", "general" -entries := []mujica.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} +entries := []account.SettingEntry{{ID: "sound", Page: "general", Label: "Sound", Keywords: []string{"audio"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if e, ok := mujica.SettingsSearch(c, &query, entries, mujica.SettingsSearchOptions{}).Chosen(); ok { + core.Use(c, core.Settings{}) + if e, ok := account.SettingsSearch(c, &query, entries, account.SettingsSearchOptions{}).Chosen(); ok { page = e.Page } fmt.Println("page", page) } ui.Render(view, 400, 300, 1)`, - "ThemeSelector": `mode := mujica.System + "ThemeSelector": `mode := core.System view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) - mujica.ThemeSelector(c, &mode, mujica.ThemeSelectorOptions{}) + core.Use(c, core.Settings{Mode: mode}) + account.ThemeSelector(c, &mode, account.ThemeSelectorOptions{}) } ui.Render(view, 400, 300, 1)`, "AccentColorPicker": `accent := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AccentColorPicker(c, &accent, mujica.AccentColorPickerOptions{}) + core.Use(c, core.Settings{}) + account.AccentColorPicker(c, &accent, account.AccentColorPickerOptions{}) } ui.Render(view, 400, 300, 1)`, - "ShortcutRecorder": `palette := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} + "ShortcutRecorder": `palette := account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShortcutRecorder(c, &palette, mujica.ShortcutRecorderOptions{ + core.Use(c, core.Settings{}) + account.ShortcutRecorder(c, &palette, account.ShortcutRecorderOptions{ Label: "Command palette", - Taken: []mujica.ShortcutBinding{{Action: "Save", Shortcut: mujica.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, + Taken: []account.ShortcutBinding{{Action: "Save", Shortcut: account.Shortcut{Mods: ui.Cmd, Key: ui.KeyS}}}, }) } ui.Render(view, 400, 300, 1)`, "KeyboardShortcutsList": `query := "" -groups := []mujica.ShortcutGroup{{Title: "General", Bindings: []mujica.ShortcutBinding{ - {Action: "Command palette", Shortcut: mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, +groups := []account.ShortcutGroup{{Title: "General", Bindings: []account.ShortcutBinding{ + {Action: "Command palette", Shortcut: account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP}}, }}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.KeyboardShortcutsList(c, &query, groups, mujica.KeyboardShortcutsListOptions{}) + core.Use(c, core.Settings{}) + account.KeyboardShortcutsList(c, &query, groups, account.KeyboardShortcutsListOptions{}) } ui.Render(view, 400, 300, 1)`, - "LoginForm": `var state mujica.LoginFormState + "LoginForm": `var state account.LoginFormState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LoginForm(c, &state, mujica.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { + core.Use(c, core.Settings{}) + account.LoginForm(c, &state, account.LoginFormOptions{OnSubmit: func(account, password string, remember bool) { // Submit credentials through the caller-provided auth service. state.EndSubmit("") }}) } ui.Render(view, 400, 300, 1)`, - "OAuthButtons": `providers := []mujica.OAuthProvider{{ID: "sso", Name: "Company SSO"}} + "OAuthButtons": `providers := []account.OAuthProvider{{ID: "sso", Name: "Company SSO"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OAuthButtons(c, providers, mujica.OAuthButtonsOptions{}).Chosen(); ok { + core.Use(c, core.Settings{}) + if id, ok := account.OAuthButtons(c, providers, account.OAuthButtonsOptions{}).Chosen(); ok { fmt.Println("start sign-in with", id) } } @@ -76,97 +76,97 @@ ui.Render(view, 400, 300, 1)`, "TwoFactorInput": `code := "" resendAt := time.Now().Add(30 * time.Second) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TwoFactorInput(c, &code, mujica.TwoFactorInputOptions{ResendAt: resendAt}) + core.Use(c, core.Settings{}) + r := account.TwoFactorInput(c, &code, account.TwoFactorInputOptions{ResendAt: resendAt}) if r.Completed() { fmt.Println("verify the code") } } ui.Render(view, 400, 300, 1)`, - "ProfileEditor": `saved := mujica.Profile{Name: "Sakiko", Bio: "Keyboard."} + "ProfileEditor": `saved := account.Profile{Name: "Sakiko", Bio: "Keyboard."} profile := saved view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ProfileEditor(c, &profile, mujica.ProfileEditorOptions{Saved: saved}).Save() { + core.Use(c, core.Settings{}) + if account.ProfileEditor(c, &profile, account.ProfileEditorOptions{Saved: saved}).Save() { saved = profile } } ui.Render(view, 400, 300, 1)`, "AccountSwitcher": `current := "a" -accounts := []mujica.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} +accounts := []account.Account{{ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AccountSwitcher(c, ¤t, accounts, mujica.AccountSwitcherOptions{}).Add() { + core.Use(c, core.Settings{}) + if account.AccountSwitcher(c, ¤t, accounts, account.AccountSwitcherOptions{}).Add() { fmt.Println("sign in another account") } } ui.Render(view, 400, 300, 1)`, "WorkspaceSwitcher": `current := "ave" -spaces := []mujica.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} +spaces := []account.Workspace{{ID: "ave", Name: "Ave Mujica"}, {ID: "mygo", Name: "MyGO!!!!!"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkspaceSwitcher(c, ¤t, spaces, mujica.WorkspaceSwitcherOptions{}) + core.Use(c, core.Settings{}) + account.WorkspaceSwitcher(c, ¤t, spaces, account.WorkspaceSwitcherOptions{}) } ui.Render(view, 400, 300, 1)`, - "UserMenu": `status := mujica.UserStatusOnline + "UserMenu": `status := account.UserStatusOnline view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.UserMenu(c, &status, mujica.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { + core.Use(c, core.Settings{}) + if account.UserMenu(c, &status, account.UserMenuOptions{Name: "Sakiko Togawa"}).SignOut() { fmt.Println("sign out") } } ui.Render(view, 400, 300, 1)`, - "SessionList": `sessions := []mujica.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} + "SessionList": `sessions := []account.Session{{ID: "s1", Device: "MacBook Pro", Current: true}, {ID: "s2", Device: "iPhone", Location: "Osaka"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.SessionList(c, sessions, mujica.SessionListOptions{}).Revoked(); ok { + core.Use(c, core.Settings{}) + if id, ok := account.SessionList(c, sessions, account.SessionListOptions{}).Revoked(); ok { fmt.Println("sign out", id) } } ui.Render(view, 400, 300, 1)`, - "ApiKeyManager": `var state mujica.ApiKeyManagerState -keys := []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} + "ApiKeyManager": `var state account.ApiKeyManagerState +keys := []account.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if name, ok := mujica.ApiKeyManager(c, &state, keys, mujica.ApiKeyManagerOptions{}).Create(); ok { + core.Use(c, core.Settings{}) + if name, ok := account.ApiKeyManager(c, &state, keys, account.ApiKeyManagerOptions{}).Create(); ok { fmt.Println("create", name) state.Reveal("secret from your key service") } } ui.Render(view, 400, 300, 1)`, - "UsageQuota": `items := []mujica.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} + "UsageQuota": `items := []account.QuotaItem{{Label: "API calls", Used: 4200, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UsageQuota(c, items, mujica.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) + core.Use(c, core.Settings{}) + account.UsageQuota(c, items, account.UsageQuotaOptions{ResetAt: time.Now().Add(72 * time.Hour)}) } ui.Render(view, 400, 300, 1)`, "OnboardingWizard": `step := 0 -steps := []mujica.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} +steps := []account.WizardStep{{Title: "Welcome"}, {Title: "Pick a theme"}, {Title: "Done"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.OnboardingWizard(c, &step, steps, mujica.OnboardingWizardOptions{}).Finished() { + core.Use(c, core.Settings{}) + if account.OnboardingWizard(c, &step, steps, account.OnboardingWizardOptions{}).Finished() { fmt.Println("onboarded") } } ui.Render(view, 400, 300, 1)`, "Coachmark": `open, step := true, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - compose := mujica.Button(c, "Compose", mujica.ButtonOptions{}) - mujica.Coachmark(c, &open, &step, []mujica.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) + core.Use(c, core.Settings{}) + compose := input.Button(c, "Compose", input.ButtonOptions{}) + account.Coachmark(c, &open, &step, []account.CoachmarkStep{{Anchor: compose, Title: "Write here", Body: "Start a new letter."}}) } ui.Render(view, 400, 300, 1)`, "WhatsNewDialog": `open, hide := true, false -releases := []mujica.Release{{Version: "1.1", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "500 components."}}}} +releases := []account.Release{{Version: "1.1", Changes: []account.ReleaseChange{{Kind: account.ChangeNew, Text: "500 components."}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WhatsNewDialog(c, &open, &hide, releases, mujica.WhatsNewDialogOptions{}) + core.Use(c, core.Settings{}) + account.WhatsNewDialog(c, &open, &hide, releases, account.WhatsNewDialogOptions{}) } ui.Render(view, 400, 300, 1)`, - "FeedbackWidget": `var state mujica.FeedbackWidgetState + "FeedbackWidget": `var state account.FeedbackWidgetState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if f, ok := mujica.FeedbackWidget(c, &state, mujica.FeedbackWidgetOptions{}).Submitted(); ok { + core.Use(c, core.Settings{}) + if f, ok := account.FeedbackWidget(c, &state, account.FeedbackWidgetOptions{}).Submitted(); ok { fmt.Println("send", f.Kind) state.EndSubmit("") } diff --git a/examples/gallery/pages_account_more.go b/examples/gallery/pages_account_more.go index 210f07d..3959d97 100644 --- a/examples/gallery/pages_account_more.go +++ b/examples/gallery/pages_account_more.go @@ -6,33 +6,35 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/account" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type accountSwitchState struct { account, space string - accounts []mujica.Account + accounts []account.Account disabled bool } func accountSwitcherDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "471", func() accountSwitchState { - return accountSwitchState{account: "a", accounts: []mujica.Account{ + return accountSwitchState{account: "a", accounts: []account.Account{ {ID: "a", Name: "Sakiko Togawa", Email: "sakiko@court"}, {ID: "b", Name: "Oblivionis", Email: "ob@mask"}}} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - r := mujica.AccountSwitcher(c, &d.account, d.accounts, mujica.AccountSwitcherOptions{Disabled: d.disabled}) + r := account.AccountSwitcher(c, &d.account, d.accounts, account.AccountSwitcherOptions{Disabled: d.disabled}) if r.Changed() { log("switched to " + d.account) } if r.Add() { n := len(d.accounts) + 1 - d.accounts = append(d.accounts, mujica.Account{ID: fmt.Sprint(n), Name: fmt.Sprintf("Guest %d", n)}) + d.accounts = append(d.accounts, account.Account{ID: fmt.Sprint(n), Name: fmt.Sprintf("Guest %d", n)}) log("add account") } } -var accountSpaces = []mujica.Workspace{ +var accountSpaces = []account.Workspace{ {ID: "ave", Name: "Ave Mujica", Detail: "5 members"}, {ID: "mygo", Name: "MyGO!!!!!", Detail: "5 members"}, {ID: "crychic", Name: "CRYCHIC", Detail: "Archived"}, {ID: "sumimi", Name: "sumimi", Detail: "2 members"}, } @@ -40,20 +42,20 @@ var accountSpaces = []mujica.Workspace{ func accountWorkspaceDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "472", func() accountSwitchState { return accountSwitchState{space: "ave"} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - if mujica.WorkspaceSwitcher(c, &d.space, accountSpaces, mujica.WorkspaceSwitcherOptions{Disabled: d.disabled}).Changed() { + if account.WorkspaceSwitcher(c, &d.space, accountSpaces, account.WorkspaceSwitcherOptions{Disabled: d.disabled}).Changed() { log("workspace " + d.space) } } type accountUserState struct { - status mujica.UserStatus + status account.UserStatus disabled bool } func accountUserMenuDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "473", func() accountUserState { return accountUserState{} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - r := mujica.UserMenu(c, &d.status, mujica.UserMenuOptions{Name: "Sakiko Togawa", Email: "sakiko@court", Disabled: d.disabled}) + r := account.UserMenu(c, &d.status, account.UserMenuOptions{Name: "Sakiko Togawa", Email: "sakiko@court", Disabled: d.disabled}) if r.StatusChanged() { log(fmt.Sprintf("status %d", d.status)) } @@ -66,20 +68,20 @@ func accountUserMenuDemo(c *ui.Context, log func(string)) { } type accountSessionsState struct { - sessions []mujica.Session + sessions []account.Session disabled bool } func accountSessionsDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "474", func() accountSessionsState { - return accountSessionsState{sessions: []mujica.Session{ + return accountSessionsState{sessions: []account.Session{ {ID: "s1", Device: "MacBook Pro · Safari", Location: "Tokyo", LastActive: time.Now(), Current: true}, {ID: "s2", Device: "iPhone · App", Location: "Osaka", LastActive: time.Now().Add(-26 * time.Hour)}, {ID: "s3", Device: "Windows · Edge", Location: "Kyoto"}, }} }) accountToggles(c, log, []string{"Disabled"}, &d.disabled) - r := mujica.SessionList(c, d.sessions, mujica.SessionListOptions{Disabled: d.disabled}) + r := account.SessionList(c, d.sessions, account.SessionListOptions{Disabled: d.disabled}) if id, ok := r.Revoked(); ok { for i, s := range d.sessions { if s.ID == id { @@ -96,17 +98,17 @@ func accountSessionsDemo(c *ui.Context, log func(string)) { } type accountKeysState struct { - state mujica.ApiKeyManagerState - keys []mujica.APIKey + state account.ApiKeyManagerState + keys []account.APIKey disabled, reject bool } func accountKeysDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "475", func() accountKeysState { - return accountKeysState{keys: []mujica.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c", Created: time.Now().AddDate(0, -1, 0)}}} + return accountKeysState{keys: []account.APIKey{{ID: "k1", Name: "Deploy bot", Prefix: "mk_live_9f2c", Created: time.Now().AddDate(0, -1, 0)}}} }) accountToggles(c, log, []string{"Disabled", "Create fails"}, &d.disabled, &d.reject) - r := mujica.ApiKeyManager(c, &d.state, d.keys, mujica.ApiKeyManagerOptions{Disabled: d.disabled}) + r := account.ApiKeyManager(c, &d.state, d.keys, account.ApiKeyManagerOptions{Disabled: d.disabled}) if name, ok := r.Create(); ok { if d.reject { d.state.Fail("Key limit reached.") @@ -116,7 +118,7 @@ func accountKeysDemo(c *ui.Context, log func(string)) { // The caller supplies the generated key. n := len(d.keys) + 1 prefix := fmt.Sprintf("mk_live_%04x", n*7919) - d.keys = append(d.keys, mujica.APIKey{ID: fmt.Sprint("k", n), Name: name, Prefix: prefix, Created: time.Now()}) + d.keys = append(d.keys, account.APIKey{ID: fmt.Sprint("k", n), Name: name, Prefix: prefix, Created: time.Now()}) d.state.Reveal(fmt.Sprintf("%s_%x", prefix, time.Now().UnixNano())) log("created key " + name) } @@ -135,18 +137,18 @@ type accountQuotaState struct{ calls float64 } func accountQuotaDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "476", func() accountQuotaState { return accountQuotaState{calls: 4200} }) - if mujica.Slider(c, &d.calls, mujica.SliderOptions{Min: 0, Max: 12000, Step: 100, Label: "API calls used", ShowValue: true}).Changed() { + if input.Slider(c, &d.calls, input.SliderOptions{Min: 0, Max: 12000, Step: 100, Label: "API calls used", ShowValue: true}).Changed() { log(fmt.Sprintf("calls %.0f", d.calls)) } ui.Box(c).Width(440).Children(func() { - mujica.UsageQuota(c, []mujica.QuotaItem{{Label: "API calls", Used: d.calls, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}}, - mujica.UsageQuotaOptions{ResetAt: time.Now().Add(52 * time.Hour)}) + account.UsageQuota(c, []account.QuotaItem{{Label: "API calls", Used: d.calls, Limit: 10000}, {Label: "Storage", Used: 9, Limit: 10, Unit: "GB"}}, + account.UsageQuotaOptions{ResetAt: time.Now().Add(52 * time.Hour)}) }) } type accountWizardState struct { step int - theme mujica.Mode + theme core.Mode name string noSkip bool } @@ -154,21 +156,21 @@ type accountWizardState struct { func accountWizardDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "477", func() accountWizardState { return accountWizardState{} }) accountToggles(c, log, []string{"Hide Skip"}, &d.noSkip) - steps := []mujica.WizardStep{ + steps := []account.WizardStep{ {Title: "Welcome", Description: "Three short steps to set up your court."}, {Title: "Your name", Ready: func() bool { return d.name != "" }, Content: func() { - if mujica.TextInput(c, &d.name, mujica.TextInputOptions{Label: "Name", Placeholder: "Name"}).Changed() { + if input.TextInput(c, &d.name, input.TextInputOptions{Label: "Name", Placeholder: "Name"}).Changed() { log("name changed") } }}, {Title: "Pick a theme", Content: func() { - if mujica.ThemeSelector(c, &d.theme, mujica.ThemeSelectorOptions{}).Changed() { + if account.ThemeSelector(c, &d.theme, account.ThemeSelectorOptions{}).Changed() { log("theme " + d.theme.String()) } }}, } from := d.step - r := mujica.OnboardingWizard(c, &d.step, steps, mujica.OnboardingWizardOptions{NoSkip: d.noSkip}) + r := account.OnboardingWizard(c, &d.step, steps, account.OnboardingWizardOptions{NoSkip: d.noSkip}) if d.step != from { log(fmt.Sprintf("step %d", d.step+1)) } @@ -192,8 +194,8 @@ func accountCoachDemo(c *ui.Context, log func(string)) { } var compose, archive *ui.Element ui.Row(c).Gap(48).Padding(8).Children(func() { - compose = mujica.Button(c, "Compose", mujica.ButtonOptions{Disabled: d.disabled}) - archive = mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + compose = input.Button(c, "Compose", input.ButtonOptions{Disabled: d.disabled}) + archive = input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) }) if compose.Clicked() { log("compose") @@ -202,7 +204,7 @@ func accountCoachDemo(c *ui.Context, log func(string)) { log("archive") } from := d.step - r := mujica.Coachmark(c, &d.open, &d.step, []mujica.CoachmarkStep{ + r := account.Coachmark(c, &d.open, &d.step, []account.CoachmarkStep{ {Anchor: compose, Title: "Write here", Body: "Start a new letter."}, {Anchor: archive, Title: "Keep it tidy", Body: "Archive threads you have finished."}, }) @@ -225,12 +227,12 @@ func accountWhatsNewDemo(c *ui.Context, log func(string)) { d.open = true log("open") } - releases := []mujica.Release{ - {Version: "1.1", Date: "2026-10-05", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeNew, Text: "400 new components."}, {Kind: mujica.ChangeFixed, Text: "Snapshots no longer drift."}}}, - {Version: "1.0", Date: "2026-09-01", Changes: []mujica.ReleaseChange{{Kind: mujica.ChangeImproved, Text: "Court-gothic theme."}}}, + releases := []account.Release{ + {Version: "1.1", Date: "2026-10-05", Changes: []account.ReleaseChange{{Kind: account.ChangeNew, Text: "400 new components."}, {Kind: account.ChangeFixed, Text: "Snapshots no longer drift."}}}, + {Version: "1.0", Date: "2026-09-01", Changes: []account.ReleaseChange{{Kind: account.ChangeImproved, Text: "Court-gothic theme."}}}, } was := d.open - if mujica.WhatsNewDialog(c, &d.open, &d.hide, releases, mujica.WhatsNewDialogOptions{}).Changed() { + if account.WhatsNewDialog(c, &d.open, &d.hide, releases, account.WhatsNewDialogOptions{}).Changed() { log(fmt.Sprintf("don't show again %v", d.hide)) } if was && !d.open { @@ -239,7 +241,7 @@ func accountWhatsNewDemo(c *ui.Context, log func(string)) { } type accountFeedbackState struct { - state mujica.FeedbackWidgetState + state account.FeedbackWidgetState disabled bool } @@ -254,7 +256,7 @@ func accountFeedbackDemo(c *ui.Context, log func(string)) { } accountToggles(c, log, []string{"Disabled"}, &d.disabled) before := d.state.Draft - r := mujica.FeedbackWidget(c, &d.state, mujica.FeedbackWidgetOptions{Disabled: d.disabled, Attachments: func() { + r := account.FeedbackWidget(c, &d.state, account.FeedbackWidgetOptions{Disabled: d.disabled, Attachments: func() { if tradingAction(c, "Attach screenshot") { log("attach screenshot") } diff --git a/examples/gallery/pages_agent.go b/examples/gallery/pages_agent.go index ead48c3..4fe2c80 100644 --- a/examples/gallery/pages_agent.go +++ b/examples/gallery/pages_agent.go @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const agentCategory = "Agent" @@ -58,13 +59,13 @@ var agentPages = []page{ var agentStates = []string{"Pending", "Planning", "Running", "Waiting", "Done", "Failed", "Skipped"} // agentStatePick is a select over agentStates, logging changes. -func agentStatePick(c *ui.Context, log func(string), v *string) mujica.AgentState { +func agentStatePick(c *ui.Context, log func(string), v *string) agent.AgentState { if ui.Select(c, v, agentStates).Changed() { log("state = " + *v) } for i, s := range agentStates { if s == *v { - return mujica.AgentState(i) + return agent.AgentState(i) } } panic("gallery: unknown state " + *v) @@ -78,41 +79,41 @@ type toolCallDemoState struct { func toolCallCardDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "261", func() toolCallDemoState { return toolCallDemoState{state: "Done"} }) - var st mujica.AgentState + var st agent.AgentState overlayControls(c, func() { st = agentStatePick(c, log, &s.state) overlayCheck(c, log, &s.failed, "Error") overlayCheck(c, log, &s.still, "Still") }) - call := mujica.ToolCall{ID: "1", Name: "read_file", Args: `{"path":"court/ledger.md","limit":40}`, + call := agent.ToolCall{ID: "1", Name: "read_file", Args: `{"path":"court/ledger.md","limit":40}`, Result: `{"lines":40,"truncated":false}`, State: st, Duration: 420 * time.Millisecond} if s.failed { call.Error = "permission denied: court/ledger.md" } - if mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{Still: s.still}).Changed() { + if agent.ToolCallCard(c, call, agent.ToolCallCardOptions{Still: s.still}).Changed() { log("folded or unfolded a section") } } func toolCallGroupDemo(c *ui.Context, log func(string)) { open := ui.Local(c.Root(), "262", func() bool { return true }) - calls := []mujica.ToolCall{ - {ID: "a", Name: "list_dir", Args: `{"path":"court"}`, Result: `["ledger.md","menu.md"]`, State: mujica.AgentDone, Duration: 300 * time.Millisecond}, - {ID: "b", Name: "read_file", Args: `{"path":"court/wine.md"}`, Error: "not found", State: mujica.AgentFailed, Duration: 1200 * time.Millisecond}, - {ID: "c", Name: "grep", Args: `{"pattern":"Sakiko"}`, Result: `{"matches":3}`, State: mujica.AgentDone, Duration: 500 * time.Millisecond}, + calls := []agent.ToolCall{ + {ID: "a", Name: "list_dir", Args: `{"path":"court"}`, Result: `["ledger.md","menu.md"]`, State: agent.AgentDone, Duration: 300 * time.Millisecond}, + {ID: "b", Name: "read_file", Args: `{"path":"court/wine.md"}`, Error: "not found", State: agent.AgentFailed, Duration: 1200 * time.Millisecond}, + {ID: "c", Name: "grep", Args: `{"pattern":"Sakiko"}`, Result: `{"matches":3}`, State: agent.AgentDone, Duration: 500 * time.Millisecond}, } - if mujica.ToolCallGroup(c, open, calls, mujica.ToolCallGroupOptions{}).Changed() { + if agent.ToolCallGroup(c, open, calls, agent.ToolCallGroupOptions{}).Changed() { log(fmt.Sprintf("group open = %v", *open)) } } func toolApprovalDemo(c *ui.Context, log func(string)) { open := ui.Local(c.Root(), "263", func() bool { return false }) - if mujica.Button(c, "Run delete_files", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Run delete_files", input.ButtonOptions{}).Clicked() { *open = true log("asked for approval") } - r := mujica.ToolApprovalDialog(c, open, mujica.ToolApprovalOptions{Tool: "delete_files", + r := agent.ToolApprovalDialog(c, open, agent.ToolApprovalOptions{Tool: "delete_files", Args: `{"glob":"build/*.tmp","dry_run":false}`, Reason: "The agent wants to clean temporary files from the build folder."}) if r.Allowed { log("allowed") @@ -130,35 +131,35 @@ type permissionDemoState struct { func permissionDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "264", func() permissionDemoState { return permissionDemoState{grant: "none"} }) ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { - if mujica.Button(c, "Ask to write", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Ask to write", input.ButtonOptions{}).Clicked() { s.open = true log("asked for permission") } ui.Text(c, "Grant: "+s.grant) }) - r := mujica.PermissionPrompt(c, &s.open, mujica.PermissionPromptOptions{Title: "Write to the ledger?", + r := agent.PermissionPrompt(c, &s.open, agent.PermissionPromptOptions{Title: "Write to the ledger?", Description: "The agent wants to record tonight's guests.", Scope: "Write access to ~/court/ledger and its subfolders"}) - if r.Choice != mujica.PermissionNone { + if r.Choice != agent.PermissionNone { s.grant = [...]string{"none", "once", "always", "denied"}[r.Choice] log("choice = " + s.grant) } } type stepDemoState struct { - steps []mujica.AgentStep + steps []agent.AgentStep disabled bool } func agentStepListDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "265", func() stepDemoState { - return stepDemoState{steps: []mujica.AgentStep{ - {ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: 800 * time.Millisecond, Detail: "42 guests, 3 arrive late."}, - {ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Duration: 2 * time.Second, Detail: "Table 4 is over capacity."}, - {ID: "send", Title: "Send the invitations", State: mujica.AgentPending}, + return stepDemoState{steps: []agent.AgentStep{ + {ID: "read", Title: "Read the guest list", State: agent.AgentDone, Duration: 800 * time.Millisecond, Detail: "42 guests, 3 arrive late."}, + {ID: "seat", Title: "Assign the seats", State: agent.AgentFailed, Duration: 2 * time.Second, Detail: "Table 4 is over capacity."}, + {ID: "send", Title: "Send the invitations", State: agent.AgentPending}, }} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") }) - r := mujica.AgentStepList(c, s.steps, mujica.AgentStepListOptions{Disabled: s.disabled}) + r := agent.AgentStepList(c, s.steps, agent.AgentStepListOptions{Disabled: s.disabled}) if r.Toggled != "" { log("toggled " + r.Toggled) } @@ -166,38 +167,38 @@ func agentStepListDemo(c *ui.Context, log func(string)) { log("retry " + r.Retry) for i := range s.steps { if s.steps[i].ID == r.Retry { - s.steps[i].State, s.steps[i].Detail = mujica.AgentRunning, "Retrying with a larger table." + s.steps[i].State, s.steps[i].Detail = agent.AgentRunning, "Retrying with a larger table." } } } } type planDemoState struct { - items []mujica.PlanItem + items []agent.PlanItem disabled bool } func agentPlanDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "266", func() planDemoState { - return planDemoState{items: []mujica.PlanItem{ - {ID: "a", Text: "Read the guest list", State: mujica.AgentDone}, - {ID: "b", Text: "Assign the seats", State: mujica.AgentRunning}, + return planDemoState{items: []agent.PlanItem{ + {ID: "a", Text: "Read the guest list", State: agent.AgentDone}, + {ID: "b", Text: "Assign the seats", State: agent.AgentRunning}, {ID: "c", Text: "Order the flowers"}, }} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Agent finishes a step", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Agent finishes a step", input.ButtonOptions{Variant: input.Secondary}).Clicked() { for i := range s.items { - if s.items[i].State != mujica.AgentDone && s.items[i].State != mujica.AgentSkipped { - s.items[i].State = mujica.AgentDone + if s.items[i].State != agent.AgentDone && s.items[i].State != agent.AgentSkipped { + s.items[i].State = agent.AgentDone log("agent checked " + s.items[i].Text) break } } } }) - if mujica.AgentPlan(c, &s.items, mujica.AgentPlanOptions{Disabled: s.disabled}).Changed() { + if agent.AgentPlan(c, &s.items, agent.AgentPlanOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("plan changed (%d items)", len(s.items))) } } @@ -209,15 +210,15 @@ type statusDemoState struct { func agentStatusDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "267", func() statusDemoState { return statusDemoState{state: "Running"} }) - var st mujica.AgentState + var st agent.AgentState overlayControls(c, func() { st = agentStatePick(c, log, &s.state) overlayCheck(c, log, &s.still, "Still") }) ui.Row(c).Gap(8).Wrap().Children(func() { - mujica.AgentStatus(c, st, mujica.AgentStatusOptions{Still: s.still}) - for _, v := range []mujica.AgentState{mujica.AgentPlanning, mujica.AgentRunning, mujica.AgentWaiting, mujica.AgentDone, mujica.AgentFailed} { - mujica.AgentStatus(c, v, mujica.AgentStatusOptions{Still: s.still}) + agent.AgentStatus(c, st, agent.AgentStatusOptions{Still: s.still}) + for _, v := range []agent.AgentState{agent.AgentPlanning, agent.AgentRunning, agent.AgentWaiting, agent.AgentDone, agent.AgentFailed} { + agent.AgentStatus(c, v, agent.AgentStatusOptions{Still: s.still}) } }) } @@ -239,25 +240,25 @@ func agentProgressDemo(c *ui.Context, log func(string)) { if s.value > 0.5 { action = "Writing the seating chart" } - mujica.AgentProgress(c, s.value, mujica.AgentProgressOptions{Label: "Preparing the ball", Action: action, Indeterminate: s.indeterminate}) + agent.AgentProgress(c, s.value, agent.AgentProgressOptions{Label: "Preparing the ball", Action: action, Indeterminate: s.indeterminate}) } func subAgentTreeDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "269", func() bool { return false }) overlayControls(c, func() { overlayCheck(c, log, disabled, "Disabled") }) - agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", Task: "Prepare the ball", State: mujica.AgentRunning, Children: []mujica.SubAgent{ - {ID: "music", Name: "Musician", Task: "Hire the quartet", State: mujica.AgentDone}, - {ID: "kitchen", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting, Children: []mujica.SubAgent{ - {ID: "wine", Name: "Sommelier", Task: "Choose the wine", State: mujica.AgentFailed}, + agents := []agent.SubAgent{{ID: "lead", Name: "Steward", Task: "Prepare the ball", State: agent.AgentRunning, Children: []agent.SubAgent{ + {ID: "music", Name: "Musician", Task: "Hire the quartet", State: agent.AgentDone}, + {ID: "kitchen", Name: "Cook", Task: "Plan the menu", State: agent.AgentWaiting, Children: []agent.SubAgent{ + {ID: "wine", Name: "Sommelier", Task: "Choose the wine", State: agent.AgentFailed}, }}, }}} - if mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{Disabled: *disabled}).Changed() { + if agent.SubAgentTree(c, agents, agent.SubAgentTreeOptions{Disabled: *disabled}).Changed() { log("expanded or collapsed an agent") } } type fileChangeDemoState struct { - decision mujica.FileDecision + decision agent.FileDecision disabled bool preview float64 } @@ -268,7 +269,7 @@ func fileChangeCardDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.disabled, "Disabled") overlaySlider(c, log, &s.preview, "Preview", "lines", 2, 12, 1) }) - r := mujica.FileChangeCard(c, &s.decision, agentDemoChange, mujica.FileChangeCardOptions{Preview: int(s.preview), Disabled: s.disabled}) + r := agent.FileChangeCard(c, &s.decision, agentDemoChange, agent.FileChangeCardOptions{Preview: int(s.preview), Disabled: s.disabled}) if r.Decided { log(fmt.Sprintf("decision = %d", s.decision)) } @@ -277,6 +278,6 @@ func fileChangeCardDemo(c *ui.Context, log func(string)) { } } -var agentDemoChange = mujica.FileChange{Path: "court/ledger.md", +var agentDemoChange = agent.FileChange{Path: "court/ledger.md", Old: "# Ledger\nGuests: 40\nWine: claret\nMusic: none\nHall: east wing\n", New: "# Ledger\nGuests: 42\nWine: claret\nMusic: quartet\nFlowers: lilies\nHall: east wing\n"} diff --git a/examples/gallery/pages_agent_code.go b/examples/gallery/pages_agent_code.go index 1503640..cfe3b89 100644 --- a/examples/gallery/pages_agent_code.go +++ b/examples/gallery/pages_agent_code.go @@ -2,29 +2,29 @@ package main // Code snippets of the Agent pages, the same as their compiled Examples. -const toolCallCardCode = `call := mujica.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", - Result: "{\"lines\":40}", State: mujica.AgentDone, Duration: 420 * time.Millisecond} +const toolCallCardCode = `call := agent.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}", + Result: "{\"lines\":40}", State: agent.AgentDone, Duration: 420 * time.Millisecond} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{OpenArgs: true}) + core.Use(c, core.Settings{}) + agent.ToolCallCard(c, call, agent.ToolCallCardOptions{OpenArgs: true}) } ui.Render(view, 400, 300, 1)` const toolCallGroupCode = `open := false -calls := []mujica.ToolCall{ - {ID: "a", Name: "list_dir", State: mujica.AgentDone, Duration: 300 * time.Millisecond}, - {ID: "b", Name: "read_file", State: mujica.AgentFailed, Error: "not found"}, +calls := []agent.ToolCall{ + {ID: "a", Name: "list_dir", State: agent.AgentDone, Duration: 300 * time.Millisecond}, + {ID: "b", Name: "read_file", State: agent.AgentFailed, Error: "not found"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToolCallGroup(c, &open, calls, mujica.ToolCallGroupOptions{}) + core.Use(c, core.Settings{}) + agent.ToolCallGroup(c, &open, calls, agent.ToolCallGroupOptions{}) } ui.Render(view, 400, 300, 1)` const toolApprovalDialogCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolApprovalDialog(c, &open, mujica.ToolApprovalOptions{ + core.Use(c, core.Settings{}) + r := agent.ToolApprovalDialog(c, &open, agent.ToolApprovalOptions{ Tool: "delete_files", Args: "{\"glob\":\"*.tmp\"}", Reason: "Clean the build folder.", }) if r.Allowed { @@ -35,149 +35,149 @@ ui.Render(view, 400, 300, 1)` const permissionPromptCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PermissionPrompt(c, &open, mujica.PermissionPromptOptions{ + core.Use(c, core.Settings{}) + r := agent.PermissionPrompt(c, &open, agent.PermissionPromptOptions{ Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger", }) - if r.Choice == mujica.PermissionAllowAlways { + if r.Choice == agent.PermissionAllowAlways { fmt.Println("remember the grant") } } ui.Render(view, 400, 300, 1)` -const agentStepListCode = `steps := []mujica.AgentStep{ - {ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: time.Second}, - {ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Detail: "Table 4 is full."}, - {ID: "send", Title: "Send invitations", State: mujica.AgentPending}, +const agentStepListCode = `steps := []agent.AgentStep{ + {ID: "read", Title: "Read the guest list", State: agent.AgentDone, Duration: time.Second}, + {ID: "seat", Title: "Assign the seats", State: agent.AgentFailed, Detail: "Table 4 is full."}, + {ID: "send", Title: "Send invitations", State: agent.AgentPending}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.AgentStepList(c, steps, mujica.AgentStepListOptions{}); r.Retry != "" { + core.Use(c, core.Settings{}) + if r := agent.AgentStepList(c, steps, agent.AgentStepListOptions{}); r.Retry != "" { fmt.Println("retry", r.Retry) } } ui.Render(view, 400, 300, 1)` -const agentPlanCode = `items := []mujica.PlanItem{ - {ID: "a", Text: "Read the guest list", State: mujica.AgentDone}, - {ID: "b", Text: "Assign the seats", State: mujica.AgentRunning}, +const agentPlanCode = `items := []agent.PlanItem{ + {ID: "a", Text: "Read the guest list", State: agent.AgentDone}, + {ID: "b", Text: "Assign the seats", State: agent.AgentRunning}, {ID: "c", Text: "Order the flowers"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentPlan(c, &items, mujica.AgentPlanOptions{}) + core.Use(c, core.Settings{}) + agent.AgentPlan(c, &items, agent.AgentPlanOptions{}) } ui.Render(view, 400, 300, 1)` const agentStatusCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentStatus(c, mujica.AgentWaiting, mujica.AgentStatusOptions{}) + core.Use(c, core.Settings{}) + agent.AgentStatus(c, agent.AgentWaiting, agent.AgentStatusOptions{}) } ui.Render(view, 400, 300, 1)` const agentProgressCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AgentProgress(c, 0.4, mujica.AgentProgressOptions{Action: "Reading ledger.md"}) + core.Use(c, core.Settings{}) + agent.AgentProgress(c, 0.4, agent.AgentProgressOptions{Action: "Reading ledger.md"}) } ui.Render(view, 400, 300, 1)` -const subAgentTreeCode = `agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", State: mujica.AgentRunning, Children: []mujica.SubAgent{ - {ID: "cook", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting}, +const subAgentTreeCode = `agents := []agent.SubAgent{{ID: "lead", Name: "Steward", State: agent.AgentRunning, Children: []agent.SubAgent{ + {ID: "cook", Name: "Cook", Task: "Plan the menu", State: agent.AgentWaiting}, }}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{}) + core.Use(c, core.Settings{}) + agent.SubAgentTree(c, agents, agent.SubAgentTreeOptions{}) } ui.Render(view, 400, 300, 1)` -const fileChangeCardCode = `decision := mujica.FilePending -change := mujica.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} +const fileChangeCardCode = `decision := agent.FilePending +change := agent.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FileChangeCard(c, &decision, change, mujica.FileChangeCardOptions{}).OpenDiff { + core.Use(c, core.Settings{}) + if agent.FileChangeCard(c, &decision, change, agent.FileChangeCardOptions{}).OpenDiff { fmt.Println("open the full diff") } } ui.Render(view, 400, 300, 1)` -const multiFileDiffReviewCode = `files := []mujica.FileChange{ +const multiFileDiffReviewCode = `files := []agent.FileChange{ {Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"}, {Path: "menu.md", Old: "soup\n", New: "soup\ntart\n"}, } -decisions := make([]mujica.FileDecision, len(files)) +decisions := make([]agent.FileDecision, len(files)) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiFileDiffReview(c, &decisions, files, mujica.MultiFileDiffReviewOptions{}) + core.Use(c, core.Settings{}) + agent.MultiFileDiffReview(c, &decisions, files, agent.MultiFileDiffReviewOptions{}) } ui.Render(view, 400, 300, 1)` -const commandExecutionCardCode = `run := mujica.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} +const commandExecutionCardCode = `run := agent.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CommandExecutionCard(c, run, mujica.CommandExecutionCardOptions{}).Changed() { + core.Use(c, core.Settings{}) + if agent.CommandExecutionCard(c, run, agent.CommandExecutionCardOptions{}).Changed() { fmt.Println("abort the process") } } ui.Render(view, 400, 300, 1)` -const screenshotStreamCode = `var st mujica.ScreenshotStreamState -var frames []mujica.ScreenshotFrame // appended as the agent captures them +const screenshotStreamCode = `var st agent.ScreenshotStreamState +var frames []agent.ScreenshotFrame // appended as the agent captures them view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenshotStream(c, &st, frames, mujica.ScreenshotStreamOptions{}) + core.Use(c, core.Settings{}) + agent.ScreenshotStream(c, &st, frames, agent.ScreenshotStreamOptions{}) } ui.Render(view, 400, 300, 1)` -const artifactPanelCode = `art := mujica.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} +const artifactPanelCode = `art := agent.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactPanel(c, art, mujica.ArtifactPanelOptions{}).Exported { + core.Use(c, core.Settings{}) + if agent.ArtifactPanel(c, art, agent.ArtifactPanelOptions{}).Exported { fmt.Println("save the file") } } ui.Render(view, 400, 300, 1)` const artifactVersionSwitcherCode = `current := 1 -versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} +versions := []agent.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactVersionSwitcher(c, ¤t, versions, mujica.ArtifactVersionSwitcherOptions{}).Compare { + core.Use(c, core.Settings{}) + if agent.ArtifactVersionSwitcher(c, ¤t, versions, agent.ArtifactVersionSwitcherOptions{}).Compare { fmt.Println("diff v1 and v2") } } ui.Render(view, 400, 300, 1)` -const sandboxStatusCode = `info := mujica.SandboxInfo{Name: "sandbox-7", State: mujica.SandboxRunning, - Resources: []mujica.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} +const sandboxStatusCode = `info := agent.SandboxInfo{Name: "sandbox-7", State: agent.SandboxRunning, + Resources: []agent.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SandboxStatus(c, info, mujica.SandboxStatusOptions{}) + core.Use(c, core.Settings{}) + agent.SandboxStatus(c, info, agent.SandboxStatusOptions{}) } ui.Render(view, 400, 300, 1)` -const mcpServerListCode = `servers := []mujica.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12}} +const mcpServerListCode = `servers := []agent.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: agent.MCPConnected, Tools: 12}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MCPServerList(c, servers, mujica.MCPServerListOptions{}); r.Action == mujica.MCPStop { + core.Use(c, core.Settings{}) + if r := agent.MCPServerList(c, servers, agent.MCPServerListOptions{}); r.Action == agent.MCPStop { fmt.Println("stop", r.Server) } } ui.Render(view, 400, 300, 1)` -const memoryPanelCode = `entries := []mujica.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} +const memoryPanelCode = `entries := []agent.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemoryPanel(c, &entries, mujica.MemoryPanelOptions{}); r.Deleted != "" { + core.Use(c, core.Settings{}) + if r := agent.MemoryPanel(c, &entries, agent.MemoryPanelOptions{}); r.Deleted != "" { fmt.Println("forget", r.Deleted) } } ui.Render(view, 400, 300, 1)` const checkpointListCode = `current := "c2" -points := []mujica.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} +points := []agent.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CheckpointList(c, ¤t, points, mujica.CheckpointListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if agent.CheckpointList(c, ¤t, points, agent.CheckpointListOptions{}).Changed() { fmt.Println("restore", current) } } @@ -185,8 +185,8 @@ ui.Render(view, 400, 300, 1)` const humanInputRequestCode = `answer := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.HumanInputRequest(c, &answer, mujica.HumanInputRequestOptions{ + core.Use(c, core.Settings{}) + r := agent.HumanInputRequest(c, &answer, agent.HumanInputRequestOptions{ Question: "Which hall?", Options: []string{"East wing", "Great hall"}, AllowFree: true, }) if r.Submitted { diff --git a/examples/gallery/pages_agent_more.go b/examples/gallery/pages_agent_more.go index dbc4b9b..3c7b356 100644 --- a/examples/gallery/pages_agent_more.go +++ b/examples/gallery/pages_agent_more.go @@ -7,15 +7,17 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type multiFileDemoState struct { - decisions []mujica.FileDecision + decisions []agent.FileDecision disabled bool } -var agentDemoFiles = []mujica.FileChange{ +var agentDemoFiles = []agent.FileChange{ agentDemoChange, {Path: "court/menu.md", Old: "soup\nroast\n", New: "soup\nroast\ntart\n"}, {Path: "court/old-plan.md", Old: "Hold the ball in the cellar.\n"}, @@ -23,44 +25,44 @@ var agentDemoFiles = []mujica.FileChange{ func multiFileDiffDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "271", func() multiFileDemoState { - return multiFileDemoState{decisions: make([]mujica.FileDecision, len(agentDemoFiles))} + return multiFileDemoState{decisions: make([]agent.FileDecision, len(agentDemoFiles))} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Reset", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Reset", input.ButtonOptions{Variant: input.Secondary}).Clicked() { clear(s.decisions) log("reset") } }) - if mujica.MultiFileDiffReview(c, &s.decisions, agentDemoFiles, mujica.MultiFileDiffReviewOptions{Disabled: s.disabled}).Changed() { + if agent.MultiFileDiffReview(c, &s.decisions, agentDemoFiles, agent.MultiFileDiffReviewOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("decisions = %v", s.decisions)) } } type commandDemoState struct { - run mujica.CommandRun + run agent.CommandRun disabled bool lines int } func commandExecutionDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "272", func() commandDemoState { - return commandDemoState{run: mujica.CommandRun{Command: "go test ./court/...", Dir: "~/projects/court", Running: true}} + return commandDemoState{run: agent.CommandRun{Command: "go test ./court/...", Dir: "~/projects/court", Running: true}} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Print a line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Print a line", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.lines++ color := []string{"32mok ", "33mwarn", "31mFAIL"}[s.lines%3] s.run.Output += fmt.Sprintf("\x1b[%s\x1b[0m court/case%d 0.%02ds\n", color, s.lines, s.lines) log("printed a line") } - if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restart", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.run.Running, s.run.Output, s.lines = true, "", 0 log("restarted") } }) - if mujica.CommandExecutionCard(c, s.run, mujica.CommandExecutionCardOptions{Disabled: s.disabled}).Changed() { + if agent.CommandExecutionCard(c, s.run, agent.CommandExecutionCardOptions{Disabled: s.disabled}).Changed() { s.run.Running, s.run.ExitCode, s.run.Duration = false, 130, time.Duration(s.lines)*time.Second s.run.Output += "\x1b[31maborted by user\x1b[0m\n" log("abort requested") @@ -68,13 +70,13 @@ func commandExecutionDemo(c *ui.Context, log func(string)) { } type screenshotDemoState struct { - st mujica.ScreenshotStreamState - frames []mujica.ScreenshotFrame + st agent.ScreenshotStreamState + frames []agent.ScreenshotFrame disabled bool } // agentDemoFrame paints a frame with a moving block. -func agentDemoFrame(i int) mujica.ScreenshotFrame { +func agentDemoFrame(i int) agent.ScreenshotFrame { img := image.NewRGBA(image.Rect(0, 0, 320, 180)) for y := range 180 { for x := range 320 { @@ -86,21 +88,21 @@ func agentDemoFrame(i int) mujica.ScreenshotFrame { img.Pix[o], img.Pix[o+1], img.Pix[o+2], img.Pix[o+3] = v, v-10, v-20, 255 } } - return mujica.ScreenshotFrame{Image: ui.NewBitmap(img), Caption: fmt.Sprintf("Clicked button %d", i+1), At: time.Duration(i) * 2 * time.Second} + return agent.ScreenshotFrame{Image: ui.NewBitmap(img), Caption: fmt.Sprintf("Clicked button %d", i+1), At: time.Duration(i) * 2 * time.Second} } func screenshotStreamDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "273", func() screenshotDemoState { - return screenshotDemoState{frames: []mujica.ScreenshotFrame{agentDemoFrame(0), agentDemoFrame(1)}} + return screenshotDemoState{frames: []agent.ScreenshotFrame{agentDemoFrame(0), agentDemoFrame(1)}} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Capture a frame", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Capture a frame", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.frames = append(s.frames, agentDemoFrame(len(s.frames))) log(fmt.Sprintf("frame %d arrived", len(s.frames))) } }) - if mujica.ScreenshotStream(c, &s.st, s.frames, mujica.ScreenshotStreamOptions{Height: 220, Disabled: s.disabled}).Changed() { + if agent.ScreenshotStream(c, &s.st, s.frames, agent.ScreenshotStreamOptions{Height: 220, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("frame %d, paused = %v", s.st.Index+1, s.st.Paused)) } } @@ -118,17 +120,17 @@ func artifactPanelDemo(c *ui.Context, log func(string)) { } overlayCheck(c, log, &s.disabled, "Disabled") }) - art := mujica.Artifact{Title: "seating.go", Language: "go", + art := agent.Artifact{Title: "seating.go", Language: "go", Content: "package court\n\n// Seats maps guests to tables.\nfunc Seats() map[string]int {\n\treturn map[string]int{\"Sakiko\": 1, \"Uika\": 2}\n}\n"} - opts := mujica.ArtifactPanelOptions{Height: 200, Disabled: s.disabled} + opts := agent.ArtifactPanelOptions{Height: 200, Disabled: s.disabled} if s.kind == "Document" { - art = mujica.Artifact{Title: "Invitation", Kind: mujica.ArtifactDocument, Content: "# You are invited\n\nThe masquerade begins at nine.\n"} + art = agent.Artifact{Title: "Invitation", Kind: agent.ArtifactDocument, Content: "# You are invited\n\nThe masquerade begins at nine.\n"} opts.Preview = func() { - mujica.Heading(c, "You are invited", mujica.HeadingOptions{}) + display.Heading(c, "You are invited", display.HeadingOptions{}) ui.Text(c, "The masquerade begins at nine.") } } - r := mujica.ArtifactPanel(c, art, opts) + r := agent.ArtifactPanel(c, art, opts) if r.Copied { log("copied to the clipboard") } @@ -145,8 +147,8 @@ type versionDemoState struct { func artifactVersionDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "275", func() versionDemoState { return versionDemoState{current: 2} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") }) - versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added the quartet"}, {Label: "v3", Note: "Moved the wine table"}} - r := mujica.ArtifactVersionSwitcher(c, &s.current, versions, mujica.ArtifactVersionSwitcherOptions{Disabled: s.disabled}) + versions := []agent.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added the quartet"}, {Label: "v3", Note: "Moved the wine table"}} + r := agent.ArtifactVersionSwitcher(c, &s.current, versions, agent.ArtifactVersionSwitcherOptions{Disabled: s.disabled}) if r.Changed { log("version = " + versions[s.current].Label) } @@ -166,43 +168,43 @@ func sandboxStatusDemo(c *ui.Context, log func(string)) { return sandboxDemoState{state: "Running", cpu: 1.2, expires: time.Now().Add(10 * time.Minute)} }) names := []string{"Starting", "Running", "Stopped", "Failed"} - state := mujica.SandboxStarting + state := agent.SandboxStarting overlayControls(c, func() { if ui.Select(c, &s.state, names).Changed() { log("state = " + s.state) } overlaySlider(c, log, &s.cpu, "CPU", "cores", 0, 2, 0.1) - if mujica.Button(c, "Extend 5 min", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Extend 5 min", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.expires = s.expires.Add(5 * time.Minute) log("extended") } }) for i, n := range names { if n == s.state { - state = mujica.SandboxState(i) + state = agent.SandboxState(i) } } - mujica.SandboxStatus(c, mujica.SandboxInfo{Name: "sandbox-7", State: state, Expires: s.expires, Resources: []mujica.SandboxResource{ + agent.SandboxStatus(c, agent.SandboxInfo{Name: "sandbox-7", State: state, Expires: s.expires, Resources: []agent.SandboxResource{ {Name: "CPU", Used: s.cpu, Limit: 2, Unit: "cores"}, {Name: "Memory", Used: 3.1, Limit: 4, Unit: "GB"}, - }}, mujica.SandboxStatusOptions{}) + }}, agent.SandboxStatusOptions{}) } type mcpDemoState struct { - servers []mujica.MCPServer + servers []agent.MCPServer disabled bool } func mcpServerListDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "277", func() mcpDemoState { - return mcpDemoState{servers: []mujica.MCPServer{ - {ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12}, - {ID: "gh", Name: "github", Transport: "http", State: mujica.MCPError, Error: "401 Unauthorized"}, - {ID: "db", Name: "postgres", Transport: "stdio", State: mujica.MCPStopped}, + return mcpDemoState{servers: []agent.MCPServer{ + {ID: "fs", Name: "filesystem", Transport: "stdio", State: agent.MCPConnected, Tools: 12}, + {ID: "gh", Name: "github", Transport: "http", State: agent.MCPError, Error: "401 Unauthorized"}, + {ID: "db", Name: "postgres", Transport: "stdio", State: agent.MCPStopped}, }} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") }) - r := mujica.MCPServerList(c, s.servers, mujica.MCPServerListOptions{Disabled: s.disabled}) - if r.Action == mujica.MCPNone { + r := agent.MCPServerList(c, s.servers, agent.MCPServerListOptions{Disabled: s.disabled}) + if r.Action == agent.MCPNone { return } log(fmt.Sprintf("%s: %s", r.Server, [...]string{"", "start", "stop", "reconnect"}[r.Action])) @@ -211,29 +213,29 @@ func mcpServerListDemo(c *ui.Context, log func(string)) { continue } switch r.Action { - case mujica.MCPStop: - s.servers[i].State = mujica.MCPStopped + case agent.MCPStop: + s.servers[i].State = agent.MCPStopped default: - s.servers[i].State, s.servers[i].Tools, s.servers[i].Error = mujica.MCPConnected, 5, "" + s.servers[i].State, s.servers[i].Tools, s.servers[i].Error = agent.MCPConnected, 5, "" } } } type memoryDemoState struct { - entries []mujica.MemoryEntry + entries []agent.MemoryEntry disabled bool } func memoryPanelDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "278", func() memoryDemoState { - return memoryDemoState{entries: []mujica.MemoryEntry{ + return memoryDemoState{entries: []agent.MemoryEntry{ {ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat, 3 Oct"}, {ID: "2", Text: "The quartet needs two days' notice.", Source: "Plan, 1 Oct"}, {ID: "3", Text: "Claret is served after nine.", Source: "Ledger"}, }} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") }) - r := mujica.MemoryPanel(c, &s.entries, mujica.MemoryPanelOptions{Disabled: s.disabled}) + r := agent.MemoryPanel(c, &s.entries, agent.MemoryPanelOptions{Disabled: s.disabled}) if r.Edited != "" { log("edited " + r.Edited) } @@ -250,9 +252,9 @@ type checkpointDemoState struct { func checkpointListDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "279", func() checkpointDemoState { return checkpointDemoState{current: "c3"} }) overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") }) - points := []mujica.Checkpoint{{ID: "c3", Label: "Seats assigned", Note: "12 files changed"}, + points := []agent.Checkpoint{{ID: "c3", Label: "Seats assigned", Note: "12 files changed"}, {ID: "c2", Label: "Menu drafted", Note: "3 files changed"}, {ID: "c1", Label: "Start"}} - if mujica.CheckpointList(c, &s.current, points, mujica.CheckpointListOptions{Disabled: s.disabled}).Changed() { + if agent.CheckpointList(c, &s.current, points, agent.CheckpointListOptions{Disabled: s.disabled}).Changed() { log("rolled back to " + s.current) } } @@ -269,12 +271,12 @@ func humanInputDemo(c *ui.Context, log func(string)) { overlayControls(c, func() { overlayCheck(c, log, &s.free, "AllowFree") overlayCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Start a 30 s timer", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Start a 30 s timer", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.deadline = time.Now().Add(30 * time.Second) log("timer started") } }) - r := mujica.HumanInputRequest(c, &s.answer, mujica.HumanInputRequestOptions{Question: "Which hall should host the masquerade?", + r := agent.HumanInputRequest(c, &s.answer, agent.HumanInputRequestOptions{Question: "Which hall should host the masquerade?", Options: []string{"East wing", "Great hall"}, AllowFree: s.free, Deadline: s.deadline, Disabled: s.disabled}) if r.Submitted { log("answer sent: " + s.answer) diff --git a/examples/gallery/pages_calendar_code.go b/examples/gallery/pages_calendar_code.go index 949e14d..041f239 100644 --- a/examples/gallery/pages_calendar_code.go +++ b/examples/gallery/pages_calendar_code.go @@ -2,85 +2,85 @@ package main // Code snippets mirror example_calendar_test.go. const ( - calCalendarMonthViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calCalendarMonthViewCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local events := exampleEvents(loc) -month := mujica.YearMonth{Year: 2026, Month: time.October} +month := datetime.YearMonth{Year: 2026, Month: time.October} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarMonthView(c, &month, events, mujica.CalendarMonthViewOptions{Location: loc}) + core.Use(c, core.Settings{}) + r := datetime.CalendarMonthView(c, &month, events, datetime.CalendarMonthViewOptions{Location: loc}) if ch, ok := r.Changed(); ok { fmt.Println("move", ch.ID, "to", ch.Start) } } ui.Render(view, 400, 300, 1)` - calCalendarWeekViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calCalendarWeekViewCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local events := exampleEvents(loc) -week := mujica.Date{Year: 2026, Month: time.October, Day: 14} +week := core.Date{Year: 2026, Month: time.October, Day: 14} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CalendarWeekView(c, &week, events, mujica.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) - if ch, ok := r.Changed(); ok && ch.Kind == mujica.CalendarCreate { + core.Use(c, core.Settings{}) + r := datetime.CalendarWeekView(c, &week, events, datetime.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15}) + if ch, ok := r.Changed(); ok && ch.Kind == datetime.CalendarCreate { fmt.Println("new event", ch.Start, ch.End) } } ui.Render(view, 400, 300, 1)` - calCalendarDayViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calCalendarDayViewCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local events := exampleEvents(loc) -day := mujica.Date{Year: 2026, Month: time.October, Day: 14} +day := core.Date{Year: 2026, Month: time.October, Day: 14} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.CalendarDayView(c, &day, events, mujica.CalendarDayViewOptions{Location: loc}).Opened(); ok { + core.Use(c, core.Settings{}) + if id, ok := datetime.CalendarDayView(c, &day, events, datetime.CalendarDayViewOptions{Location: loc}).Opened(); ok { fmt.Println("open", id) } } ui.Render(view, 400, 300, 1)` - calAgendaViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calAgendaViewCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local events := exampleEvents(loc) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.AgendaViewOptions{Location: loc, From: mujica.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} - if id, ok := mujica.AgendaView(c, events, opts).Opened(); ok { + core.Use(c, core.Settings{}) + opts := datetime.AgendaViewOptions{Location: loc, From: core.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14} + if id, ok := datetime.AgendaView(c, events, opts).Opened(); ok { fmt.Println("open", id) } } ui.Render(view, 400, 300, 1)` - calYearViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calYearViewCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } @@ -88,52 +88,52 @@ loc := time.Local events := exampleEvents(loc) year := 2026 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if day, ok := mujica.YearView(c, &year, events, mujica.YearViewOptions{Location: loc}).DayChosen(); ok { + core.Use(c, core.Settings{}) + if day, ok := datetime.YearView(c, &year, events, datetime.YearViewOptions{Location: loc}).DayChosen(); ok { fmt.Println("go to", day) } } ui.Render(view, 400, 300, 1)` - calEventChipCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calEventChipCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local ev := exampleEvents(loc)[0] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}).Clicked() { + core.Use(c, core.Settings{}) + if datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}).Clicked() { fmt.Println("open", ev.ID) } } ui.Render(view, 400, 300, 1)` - calEventEditorCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calEventEditorCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } loc := time.Local ev := exampleEvents(loc)[0] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EventEditor(c, &ev, mujica.EventEditorOptions{Location: loc}) + core.Use(c, core.Settings{}) + r := datetime.EventEditor(c, &ev, datetime.EventEditorOptions{Location: loc}) if r.Saved() { fmt.Println("saved", ev.Title, ev.Start) } } ui.Render(view, 400, 300, 1)` - calEventPopoverCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent { + calEventPopoverCode = `func exampleEvents(loc *time.Location) []datetime.CalendarEvent { start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc) - return []mujica.CalendarEvent{ + return []datetime.CalendarEvent{ {ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"}, - {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo}, + {ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: datetime.EventInfo}, } } @@ -141,88 +141,88 @@ loc := time.Local ev := exampleEvents(loc)[0] open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}) + core.Use(c, core.Settings{}) + chip := datetime.EventChip(c, ev, datetime.EventChipOptions{Location: loc}) if chip.Clicked() { open = true } - r := mujica.EventPopover(c, chip, &open, ev, mujica.EventPopoverOptions{Location: loc}) + r := datetime.EventPopover(c, chip, &open, ev, datetime.EventPopoverOptions{Location: loc}) if r.Delete() { fmt.Println("delete", ev.ID) } } ui.Render(view, 400, 300, 1)` calCurrentTimeIndicatorCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Size(200, 240).Children(func() { - mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local}) + datetime.CurrentTimeIndicator(c, datetime.CurrentTimeIndicatorOptions{Location: time.Local}) }) } ui.Render(view, 400, 300, 1)` - calRecurrenceEditorCode = `rule := mujica.Recurrence{Unit: mujica.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} + calRecurrenceEditorCode = `rule := datetime.Recurrence{Unit: datetime.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RecurrenceEditor(c, &rule, mujica.RecurrenceEditorOptions{Start: mujica.Date{Year: 2026, Month: time.October, Day: 5}}) + core.Use(c, core.Settings{}) + r := datetime.RecurrenceEditor(c, &rule, datetime.RecurrenceEditorOptions{Start: core.Date{Year: 2026, Month: time.October, Day: 5}}) if r.Changed() && r.Problem() == "" { - fmt.Println(mujica.RecurrenceSummary(c, rule)) + fmt.Println(datetime.RecurrenceSummary(c, rule)) } } ui.Render(view, 400, 300, 1)` - calAvailabilityPickerCode = `days := []mujica.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} + calAvailabilityPickerCode = `days := []core.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}} mine := [][]bool{make([]bool, 18), make([]bool, 18)} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AvailabilityPicker(c, &mine, mujica.AvailabilityPickerOptions{Days: days}).Changed() { + core.Use(c, core.Settings{}) + if datetime.AvailabilityPicker(c, &mine, datetime.AvailabilityPickerOptions{Days: days}).Changed() { fmt.Println("availability changed") } } ui.Render(view, 400, 300, 1)` calTimezoneSelectCode = `zone := "Asia/Shanghai" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimezoneSelect(c, &zone, mujica.TimezoneSelectOptions{}).Changed() { + core.Use(c, core.Settings{}) + if datetime.TimezoneSelect(c, &zone, datetime.TimezoneSelectOptions{}).Changed() { fmt.Println("zone", zone) } } ui.Render(view, 400, 300, 1)` calReminderPickerCode = `reminders := []time.Duration{15 * time.Minute} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ReminderPicker(c, &reminders, mujica.ReminderPickerOptions{Max: 3}).Changed() { + core.Use(c, core.Settings{}) + if datetime.ReminderPicker(c, &reminders, datetime.ReminderPickerOptions{Max: 3}).Changed() { fmt.Println("reminders", reminders) } } ui.Render(view, 400, 300, 1)` - calAttendeeListCode = `people := []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} + calAttendeeListCode = `people := []datetime.Attendee{{Name: "Sakiko", Status: datetime.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if i, ok := mujica.AttendeeList(c, people, mujica.AttendeeListOptions{Removable: true}).Removed(); ok { + core.Use(c, core.Settings{}) + if i, ok := datetime.AttendeeList(c, people, datetime.AttendeeListOptions{Removable: true}).Removed(); ok { people = append(people[:i], people[i+1:]...) } } ui.Render(view, 400, 300, 1)` - calWeekPickerCode = `var week mujica.Date + calWeekPickerCode = `var week core.Date view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.WeekPicker(c, &week, mujica.WeekPickerOptions{WeekStart: mujica.WeekStartMonday, Clearable: true}).Changed() { + core.Use(c, core.Settings{}) + if datetime.WeekPicker(c, &week, datetime.WeekPickerOptions{WeekStart: datetime.WeekStartMonday, Clearable: true}).Changed() { fmt.Println("week of", week) } } ui.Render(view, 400, 300, 1)` calDurationPickerCode = `length := 90 * time.Minute view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} - if mujica.DurationPicker(c, &length, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour} + if datetime.DurationPicker(c, &length, opts).Changed() { fmt.Println("length", length) } } ui.Render(view, 400, 300, 1)` calCronEditorCode = `expr := "0 9 * * MON-FRI" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.CronEditor(c, &expr, mujica.CronEditorOptions{Location: time.Local}).Changed() { - if _, err := mujica.ParseCron(expr); err == nil { + core.Use(c, core.Settings{}) + if datetime.CronEditor(c, &expr, datetime.CronEditorOptions{Location: time.Local}).Changed() { + if _, err := datetime.ParseCron(expr); err == nil { fmt.Println("schedule", expr) } } @@ -230,22 +230,22 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` calRelativeTimeCode = `posted := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.Local) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RelativeTime(c, posted, mujica.RelativeTimeOptions{Location: time.Local}) + core.Use(c, core.Settings{}) + datetime.RelativeTime(c, posted, datetime.RelativeTimeOptions{Location: time.Local}) } ui.Render(view, 400, 300, 1)` calCountdownCode = `deadline := time.Date(2026, time.December, 31, 23, 59, 59, 0, time.Local) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Countdown(c, deadline, mujica.CountdownOptions{}).Finished() { + core.Use(c, core.Settings{}) + if datetime.Countdown(c, deadline, datetime.CountdownOptions{}).Finished() { fmt.Println("happy new year") } } ui.Render(view, 400, 300, 1)` - calStopwatchCode = `var sw mujica.StopwatchState + calStopwatchCode = `var sw datetime.StopwatchState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Stopwatch(c, &sw, mujica.StopwatchOptions{}).Changed() { + core.Use(c, core.Settings{}) + if datetime.Stopwatch(c, &sw, datetime.StopwatchOptions{}).Changed() { fmt.Println("laps", len(sw.Laps)) } } diff --git a/examples/gallery/pages_calendar_parts.go b/examples/gallery/pages_calendar_parts.go index fa08df4..5668f75 100644 --- a/examples/gallery/pages_calendar_parts.go +++ b/examples/gallery/pages_calendar_parts.go @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var calColors = []string{"Accent", "Info", "Success", "Warning", "Danger", "Neutral"} @@ -20,19 +22,19 @@ func calChipDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "426", func() state { return state{color: "Accent"} }) dtSection(c, "EventChip", func() { dtControls(c, func() { - mujica.Select(c, &s.color, calOptions(calColors), mujica.SelectOptions{Label: "Color"}) - mujica.Checkbox(c, &s.inline, "Inline", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.selected, "Selected", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Select(c, &s.color, calOptions(calColors), input.SelectOptions{Label: "Color"}) + input.Checkbox(c, &s.inline, "Inline", input.CheckboxOptions{}) + input.Checkbox(c, &s.selected, "Selected", input.CheckboxOptions{}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) ev := calSample()[0] for i, n := range calColors { if n == s.color { - ev.Color = mujica.EventColor(i) + ev.Color = datetime.EventColor(i) } } ui.Box(c).Width(240).Children(func() { - if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: time.Local, Inline: s.inline, Selected: s.selected, Disabled: s.disabled}).Clicked() { + if datetime.EventChip(c, ev, datetime.EventChipOptions{Location: time.Local, Inline: s.inline, Selected: s.selected, Disabled: s.disabled}).Clicked() { log("clicked " + ev.Title) } }) @@ -41,13 +43,13 @@ func calChipDemo(c *ui.Context, log func(string)) { func calEditorDemo(c *ui.Context, log func(string)) { type state struct { - ev mujica.CalendarEvent + ev datetime.CalendarEvent disabled bool } s := ui.Local(c.Root(), "427", func() state { return state{ev: calSample()[0]} }) dtSection(c, "EventEditor", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - r := mujica.EventEditor(c, &s.ev, mujica.EventEditorOptions{Location: time.Local, Disabled: s.disabled}) + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + r := datetime.EventEditor(c, &s.ev, datetime.EventEditorOptions{Location: time.Local, Disabled: s.disabled}) if r.Saved() { log(fmt.Sprintf("saved %q %s, %d guests", s.ev.Title, s.ev.Start.Format("Jan 2 15:04"), len(s.ev.Attendees))) } @@ -64,19 +66,19 @@ func calPopoverDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "428", func() state { return state{} }) dtSection(c, "EventPopover", func() { dtControls(c, func() { - mujica.Checkbox(c, &s.readOnly, "Read-only", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Checkbox(c, &s.readOnly, "Read-only", input.CheckboxOptions{}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) ev := calSample()[0] - ev.Attendees = []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} + ev.Attendees = []datetime.Attendee{{Name: "Sakiko", Status: datetime.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}} ev.Reminders = []time.Duration{15 * time.Minute} ui.Box(c).Width(240).Children(func() { - chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: time.Local, Disabled: s.disabled}) + chip := datetime.EventChip(c, ev, datetime.EventChipOptions{Location: time.Local, Disabled: s.disabled}) if chip.Clicked() { s.open = true log("opened popover") } - r := mujica.EventPopover(c, chip, &s.open, ev, mujica.EventPopoverOptions{Location: time.Local, ReadOnly: s.readOnly, Disabled: s.disabled}) + r := datetime.EventPopover(c, chip, &s.open, ev, datetime.EventPopoverOptions{Location: time.Local, ReadOnly: s.readOnly, Disabled: s.disabled}) if r.Edit() { log("edit " + ev.ID) } @@ -89,24 +91,24 @@ func calPopoverDemo(c *ui.Context, log func(string)) { func calNowDemo(c *ui.Context, log func(string)) { dtSection(c, "CurrentTimeIndicator", func() { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Box(c).Size(320, 240).Border(1, k.Border).Children(func() { - mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local}) + datetime.CurrentTimeIndicator(c, datetime.CurrentTimeIndicatorOptions{Location: time.Local}) }) }) } func calRecurrenceDemo(c *ui.Context, log func(string)) { type state struct { - rule mujica.Recurrence + rule datetime.Recurrence disabled bool } s := ui.Local(c.Root(), "430", func() state { return state{} }) dtSection(c, "RecurrenceEditor", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - r := mujica.RecurrenceEditor(c, &s.rule, mujica.RecurrenceEditorOptions{Start: mujica.DateOf(time.Now()), Disabled: s.disabled}) + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + r := datetime.RecurrenceEditor(c, &s.rule, datetime.RecurrenceEditorOptions{Start: core.DateOf(time.Now()), Disabled: s.disabled}) if r.Changed() { - log(mujica.RecurrenceSummary(c, s.rule) + strings.TrimSpace(" "+r.Problem())) + log(datetime.RecurrenceSummary(c, s.rule) + strings.TrimSpace(" "+r.Problem())) } }) } @@ -114,11 +116,11 @@ func calRecurrenceDemo(c *ui.Context, log func(string)) { func calAvailabilityDemo(c *ui.Context, log func(string)) { type state struct { mine, theirs [][]bool - days []mujica.Date + days []core.Date others, frozen bool } s := ui.Local(c.Root(), "431", func() state { - today := mujica.DateOf(time.Now()) + today := core.DateOf(time.Now()) grid := func(seed int) [][]bool { g := make([][]bool, 5) for i := range g { @@ -129,19 +131,19 @@ func calAvailabilityDemo(c *ui.Context, log func(string)) { } return g } - days := []mujica.Date{today, today.AddDays(1), today.AddDays(2), today.AddDays(3), today.AddDays(4)} + days := []core.Date{today, today.AddDays(1), today.AddDays(2), today.AddDays(3), today.AddDays(4)} return state{mine: grid(2), theirs: grid(1), days: days, others: true} }) dtSection(c, "AvailabilityPicker", func() { dtControls(c, func() { - mujica.Checkbox(c, &s.others, "Show Mutsumi", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.frozen, "Disabled", mujica.CheckboxOptions{}) + input.Checkbox(c, &s.others, "Show Mutsumi", input.CheckboxOptions{}) + input.Checkbox(c, &s.frozen, "Disabled", input.CheckboxOptions{}) }) - var others []mujica.AvailabilityPerson + var others []datetime.AvailabilityPerson if s.others { - others = []mujica.AvailabilityPerson{{Name: "Mutsumi", Free: s.theirs}} + others = []datetime.AvailabilityPerson{{Name: "Mutsumi", Free: s.theirs}} } - if mujica.AvailabilityPicker(c, &s.mine, mujica.AvailabilityPickerOptions{Days: s.days, Others: others, Disabled: s.frozen}).Changed() { + if datetime.AvailabilityPicker(c, &s.mine, datetime.AvailabilityPickerOptions{Days: s.days, Others: others, Disabled: s.frozen}).Changed() { n := 0 for _, d := range s.mine { for _, v := range d { @@ -162,8 +164,8 @@ func calTimezoneDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "432", func() state { return state{zone: "Asia/Shanghai"} }) dtSection(c, "TimezoneSelect", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - if mujica.TimezoneSelect(c, &s.zone, mujica.TimezoneSelectOptions{Disabled: s.disabled}).Changed() { + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + if datetime.TimezoneSelect(c, &s.zone, datetime.TimezoneSelectOptions{Disabled: s.disabled}).Changed() { log("zone " + s.zone) } }) @@ -178,13 +180,13 @@ func calReminderDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "433", func() state { return state{v: []time.Duration{15 * time.Minute}, max: "0"} }) dtSection(c, "ReminderPicker", func() { dtControls(c, func() { - mujica.Select(c, &s.max, calOptions([]string{"0", "2", "3"}), mujica.SelectOptions{Label: "Max (0 = any)"}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Select(c, &s.max, calOptions([]string{"0", "2", "3"}), input.SelectOptions{Label: "Max (0 = any)"}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) if limit := calAtoi(s.max); limit > 0 && len(s.v) > limit { s.v = s.v[:limit] } - if mujica.ReminderPicker(c, &s.v, mujica.ReminderPickerOptions{Max: calAtoi(s.max), Disabled: s.disabled}).Changed() { + if datetime.ReminderPicker(c, &s.v, datetime.ReminderPickerOptions{Max: calAtoi(s.max), Disabled: s.disabled}).Changed() { log(fmt.Sprint("reminders ", s.v)) } }) @@ -192,24 +194,24 @@ func calReminderDemo(c *ui.Context, log func(string)) { func calAttendeeDemo(c *ui.Context, log func(string)) { type state struct { - list []mujica.Attendee + list []datetime.Attendee removable bool } - people := func() []mujica.Attendee { - return []mujica.Attendee{{Name: "Sakiko Togawa", Status: mujica.AttendeeAccepted, Organizer: true}, - {Name: "Mutsumi Wakaba", Status: mujica.AttendeeTentative}, {Name: "Uika Misumi", Status: mujica.AttendeeDeclined}, + people := func() []datetime.Attendee { + return []datetime.Attendee{{Name: "Sakiko Togawa", Status: datetime.AttendeeAccepted, Organizer: true}, + {Name: "Mutsumi Wakaba", Status: datetime.AttendeeTentative}, {Name: "Uika Misumi", Status: datetime.AttendeeDeclined}, {Name: "Nyamu Yuutenji", Email: "nyamu@example.com"}} } s := ui.Local(c.Root(), "434", func() state { return state{list: people(), removable: true} }) dtSection(c, "AttendeeList", func() { dtControls(c, func() { - mujica.Checkbox(c, &s.removable, "Removable", mujica.CheckboxOptions{}) - if mujica.Button(c, "Reset", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + input.Checkbox(c, &s.removable, "Removable", input.CheckboxOptions{}) + if input.Button(c, "Reset", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.list = people() log("reset") } }) - if i, ok := mujica.AttendeeList(c, s.list, mujica.AttendeeListOptions{Removable: s.removable}).Removed(); ok { + if i, ok := datetime.AttendeeList(c, s.list, datetime.AttendeeListOptions{Removable: s.removable}).Removed(); ok { log("removed " + s.list[i].Name) s.list = append(s.list[:i:i], s.list[i+1:]...) } diff --git a/examples/gallery/pages_calendar_time.go b/examples/gallery/pages_calendar_time.go index 78c432a..5f28c8f 100644 --- a/examples/gallery/pages_calendar_time.go +++ b/examples/gallery/pages_calendar_time.go @@ -5,26 +5,28 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func calWeekPickerDemo(c *ui.Context, log func(string)) { type state struct { - week mujica.Date + week core.Date weekStart string disabled bool } s := ui.Local(c.Root(), "435", func() state { return state{weekStart: "Locale"} }) dtSection(c, "WeekPicker", func() { dtControls(c, func() { - if mujica.Select(c, &s.weekStart, calOptions(calWeekStarts), mujica.SelectOptions{Label: "Week start"}).Changed() { - s.week = mujica.Date{} + if input.Select(c, &s.weekStart, calOptions(calWeekStarts), input.SelectOptions{Label: "Week start"}).Changed() { + s.week = core.Date{} log("week start " + s.weekStart + ", value cleared") } - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) - opts := mujica.WeekPickerOptions{WeekStart: calWeekStart(s.weekStart), Clearable: true, Disabled: s.disabled} - if mujica.WeekPicker(c, &s.week, opts).Changed() { + opts := datetime.WeekPickerOptions{WeekStart: calWeekStart(s.weekStart), Clearable: true, Disabled: s.disabled} + if datetime.WeekPicker(c, &s.week, opts).Changed() { log("week of " + s.week.String()) } }) @@ -38,11 +40,11 @@ func calDurationDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "436", func() state { return state{v: 90 * time.Minute} }) dtSection(c, "DurationPicker", func() { dtControls(c, func() { - mujica.Checkbox(c, &s.seconds, "Seconds", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Checkbox(c, &s.seconds, "Seconds", input.CheckboxOptions{}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) - opts := mujica.DurationPickerOptions{Seconds: s.seconds, Min: 15 * time.Minute, Max: 8 * time.Hour, Disabled: s.disabled} - if mujica.DurationPicker(c, &s.v, opts).Changed() { + opts := datetime.DurationPickerOptions{Seconds: s.seconds, Min: 15 * time.Minute, Max: 8 * time.Hour, Disabled: s.disabled} + if datetime.DurationPicker(c, &s.v, opts).Changed() { log("duration " + s.v.String()) } }) @@ -55,9 +57,9 @@ func calCronDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "437", func() state { return state{expr: "*/15 9-17 * * MON-FRI"} }) dtSection(c, "CronEditor", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - if mujica.CronEditor(c, &s.expr, mujica.CronEditorOptions{Location: time.Local, Disabled: s.disabled}).Changed() { - if _, err := mujica.ParseCron(s.expr); err != nil { + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + if datetime.CronEditor(c, &s.expr, datetime.CronEditorOptions{Location: time.Local, Disabled: s.disabled}).Changed() { + if _, err := datetime.ParseCron(s.expr); err != nil { log("invalid: " + s.expr) } else { log("schedule " + s.expr) @@ -75,13 +77,13 @@ func calRelativeDemo(c *ui.Context, log func(string)) { label string d time.Duration }{{"Now", 0}, {"5 minutes ago", -5 * time.Minute}, {"In 3 hours", 3 * time.Hour}, {"40 days ago", -40 * 24 * time.Hour}} { - if mujica.Button(c, b.label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, b.label, input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.at = time.Now().Add(b.d) log("time set: " + b.label) } } }) - mujica.RelativeTime(c, s.at, mujica.RelativeTimeOptions{Location: time.Local}) + datetime.RelativeTime(c, s.at, datetime.RelativeTimeOptions{Location: time.Local}) }) } @@ -93,17 +95,17 @@ func calCountdownDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "439", func() state { return state{deadline: time.Now().Add(10 * time.Second)} }) dtSection(c, "Countdown", func() { dtControls(c, func() { - if mujica.Button(c, "Restart 10 s", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restart 10 s", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.deadline = time.Now().Add(10 * time.Second) log("restarted") } - mujica.Checkbox(c, &s.words, "Words", mujica.CheckboxOptions{}) + input.Checkbox(c, &s.words, "Words", input.CheckboxOptions{}) }) - format := mujica.CountdownClock + format := datetime.CountdownClock if s.words { - format = mujica.CountdownWords + format = datetime.CountdownWords } - if mujica.Countdown(c, s.deadline, mujica.CountdownOptions{Format: format}).Finished() { + if datetime.Countdown(c, s.deadline, datetime.CountdownOptions{Format: format}).Finished() { log("finished") } }) @@ -111,18 +113,18 @@ func calCountdownDemo(c *ui.Context, log func(string)) { func calStopwatchDemo(c *ui.Context, log func(string)) { type state struct { - sw mujica.StopwatchState + sw datetime.StopwatchState disabled bool } s := ui.Local(c.Root(), "440", func() state { return state{} }) dtSection(c, "Stopwatch", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - if mujica.Stopwatch(c, &s.sw, mujica.StopwatchOptions{Disabled: s.disabled}).Changed() { + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + if datetime.Stopwatch(c, &s.sw, datetime.StopwatchOptions{Disabled: s.disabled}).Changed() { state := "paused" if s.sw.Running() { state = "running" } - log(state + ", " + mujica.StopwatchText(s.sw.Elapsed(time.Now()))) + log(state + ", " + datetime.StopwatchText(s.sw.Elapsed(time.Now()))) } }) } diff --git a/examples/gallery/pages_calendar_views.go b/examples/gallery/pages_calendar_views.go index b7b627b..d76fcd4 100644 --- a/examples/gallery/pages_calendar_views.go +++ b/examples/gallery/pages_calendar_views.go @@ -7,39 +7,41 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // calSample is demo events around today in the local zone. -func calSample() []mujica.CalendarEvent { +func calSample() []datetime.CalendarEvent { now := time.Now() day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) at := func(d, h, m, mins int) (time.Time, time.Time) { s := day.AddDate(0, 0, d).Add(time.Duration(h)*time.Hour + time.Duration(m)*time.Minute) return s, s.Add(time.Duration(mins) * time.Minute) } - mk := func(id, title string, color mujica.EventColor, d, h, m, mins int) mujica.CalendarEvent { + mk := func(id, title string, color datetime.EventColor, d, h, m, mins int) datetime.CalendarEvent { s, e := at(d, h, m, mins) - return mujica.CalendarEvent{ID: id, Title: title, Color: color, Start: s, End: e, Location: "Rehearsal hall"} + return datetime.CalendarEvent{ID: id, Title: title, Color: color, Start: s, End: e, Location: "Rehearsal hall"} } - evs := []mujica.CalendarEvent{ - mk("1", "Rehearsal", mujica.EventAccent, 0, 9, 0, 120), mk("2", "Costume fitting", mujica.EventInfo, 0, 10, 0, 60), - mk("3", "Lunch", mujica.EventSuccess, 0, 12, 30, 60), mk("4", "Lyrics review", mujica.EventWarning, 1, 14, 0, 90), - mk("5", "Press", mujica.EventDanger, 2, 16, 0, 45), mk("6", "Sound check", mujica.EventNeutral, 3, 18, 0, 60), - mk("7", "Encore notes", mujica.EventInfo, 0, 15, 0, 30), mk("8", "Stage walk", mujica.EventAccent, 0, 17, 0, 30), + evs := []datetime.CalendarEvent{ + mk("1", "Rehearsal", datetime.EventAccent, 0, 9, 0, 120), mk("2", "Costume fitting", datetime.EventInfo, 0, 10, 0, 60), + mk("3", "Lunch", datetime.EventSuccess, 0, 12, 30, 60), mk("4", "Lyrics review", datetime.EventWarning, 1, 14, 0, 90), + mk("5", "Press", datetime.EventDanger, 2, 16, 0, 45), mk("6", "Sound check", datetime.EventNeutral, 3, 18, 0, 60), + mk("7", "Encore notes", datetime.EventInfo, 0, 15, 0, 30), mk("8", "Stage walk", datetime.EventAccent, 0, 17, 0, 30), } - holiday := mk("9", "Masquerade", mujica.EventAccent, 4, 0, 0, 24*60) + holiday := mk("9", "Masquerade", datetime.EventAccent, 4, 0, 0, 24*60) holiday.AllDay, holiday.Start = true, day.AddDate(0, 0, 4) holiday.End = holiday.Start.AddDate(0, 0, 1) return append(evs, holiday) } // calApply applies and logs a view change. -func calApply(evs *[]mujica.CalendarEvent, ch mujica.CalendarChange, log func(string)) { +func calApply(evs *[]datetime.CalendarEvent, ch datetime.CalendarChange, log func(string)) { switch ch.Kind { - case mujica.CalendarCreate: + case datetime.CalendarCreate: id := fmt.Sprint(len(*evs) + 100) - *evs = append(*evs, mujica.CalendarEvent{ID: id, Title: "New event", Start: ch.Start, End: ch.End}) + *evs = append(*evs, datetime.CalendarEvent{ID: id, Title: "New event", Start: ch.Start, End: ch.End}) log("created " + ch.Start.Format("Jan 2 15:04") + " – " + ch.End.Format("15:04")) return } @@ -49,14 +51,14 @@ func calApply(evs *[]mujica.CalendarEvent, ch mujica.CalendarChange, log func(st } } verb := "moved" - if ch.Kind == mujica.CalendarResize { + if ch.Kind == datetime.CalendarResize { verb = "resized" } log(fmt.Sprintf("%s %s to %s – %s", verb, ch.ID, ch.Start.Format("Jan 2 15:04"), ch.End.Format("15:04"))) } // calViewResult logs what a view reported and applies its change. -func calViewResult(r mujica.CalendarViewResult, evs *[]mujica.CalendarEvent, version *int, log func(string)) { +func calViewResult(r datetime.CalendarViewResult, evs *[]datetime.CalendarEvent, version *int, log func(string)) { if id, ok := r.Opened(); ok { log("opened " + id) } @@ -71,10 +73,10 @@ func calViewResult(r mujica.CalendarViewResult, evs *[]mujica.CalendarEvent, ver var calWeekStarts = []string{"Locale", "Sunday", "Monday", "Saturday"} -func calWeekStart(s string) mujica.WeekStart { +func calWeekStart(s string) datetime.WeekStart { for i, n := range calWeekStarts { if n == s { - return mujica.WeekStart(i) + return datetime.WeekStart(i) } } panic("gallery: unknown week start " + s) @@ -82,24 +84,24 @@ func calWeekStart(s string) mujica.WeekStart { func calMonthDemo(c *ui.Context, log func(string)) { type state struct { - month mujica.YearMonth - events []mujica.CalendarEvent + month datetime.YearMonth + events []datetime.CalendarEvent version, maxRows int weekStart, maxStr string disabled bool } s := ui.Local(c.Root(), "421", func() state { now := time.Now() - return state{month: mujica.YearMonth{Year: now.Year(), Month: now.Month()}, events: calSample(), weekStart: "Locale", maxStr: "3"} + return state{month: datetime.YearMonth{Year: now.Year(), Month: now.Month()}, events: calSample(), weekStart: "Locale", maxStr: "3"} }) dtSection(c, "CalendarMonthView", func() { dtControls(c, func() { - mujica.Select(c, &s.weekStart, calOptions(calWeekStarts), mujica.SelectOptions{Label: "Week start"}) - mujica.Select(c, &s.maxStr, calOptions([]string{"1", "2", "3", "4"}), mujica.SelectOptions{Label: "Rows per day"}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Select(c, &s.weekStart, calOptions(calWeekStarts), input.SelectOptions{Label: "Week start"}) + input.Select(c, &s.maxStr, calOptions([]string{"1", "2", "3", "4"}), input.SelectOptions{Label: "Rows per day"}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) s.maxRows = calAtoi(s.maxStr) - r := mujica.CalendarMonthView(c, &s.month, s.events, mujica.CalendarMonthViewOptions{Location: time.Local, Version: s.version, + r := datetime.CalendarMonthView(c, &s.month, s.events, datetime.CalendarMonthViewOptions{Location: time.Local, Version: s.version, WeekStart: calWeekStart(s.weekStart), MaxPerDay: s.maxRows, Disabled: s.disabled}) calViewResult(r, &s.events, &s.version, log) }) @@ -107,24 +109,24 @@ func calMonthDemo(c *ui.Context, log func(string)) { func calWeekDemo(c *ui.Context, log func(string)) { type state struct { - week mujica.Date - events []mujica.CalendarEvent + week core.Date + events []datetime.CalendarEvent version int slot string hour12 bool disabled bool } s := ui.Local(c.Root(), "422", func() state { - return state{week: mujica.DateOf(time.Now()), events: calSample(), slot: "30"} + return state{week: core.DateOf(time.Now()), events: calSample(), slot: "30"} }) dtSection(c, "CalendarWeekView", func() { dtControls(c, func() { - mujica.Select(c, &s.slot, calOptions([]string{"15", "30", "60"}), mujica.SelectOptions{Label: "Slot minutes"}) - mujica.Checkbox(c, &s.hour12, "12-hour", mujica.CheckboxOptions{}) - mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) + input.Select(c, &s.slot, calOptions([]string{"15", "30", "60"}), input.SelectOptions{Label: "Slot minutes"}) + input.Checkbox(c, &s.hour12, "12-hour", input.CheckboxOptions{}) + input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) slot := calAtoi(s.slot) - r := mujica.CalendarWeekView(c, &s.week, s.events, mujica.CalendarWeekViewOptions{Location: time.Local, Version: s.version, + r := datetime.CalendarWeekView(c, &s.week, s.events, datetime.CalendarWeekViewOptions{Location: time.Local, Version: s.version, SlotMinutes: slot, Hour12: s.hour12, Disabled: s.disabled}) calViewResult(r, &s.events, &s.version, log) }) @@ -132,32 +134,32 @@ func calWeekDemo(c *ui.Context, log func(string)) { func calDayDemo(c *ui.Context, log func(string)) { type state struct { - day mujica.Date - events []mujica.CalendarEvent + day core.Date + events []datetime.CalendarEvent version int disabled bool } - s := ui.Local(c.Root(), "423", func() state { return state{day: mujica.DateOf(time.Now()), events: calSample()} }) + s := ui.Local(c.Root(), "423", func() state { return state{day: core.DateOf(time.Now()), events: calSample()} }) dtSection(c, "CalendarDayView", func() { - dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) }) - r := mujica.CalendarDayView(c, &s.day, s.events, mujica.CalendarDayViewOptions{Location: time.Local, Version: s.version, Disabled: s.disabled}) + dtControls(c, func() { input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}) }) + r := datetime.CalendarDayView(c, &s.day, s.events, datetime.CalendarDayViewOptions{Location: time.Local, Version: s.version, Disabled: s.disabled}) calViewResult(r, &s.events, &s.version, log) }) } func calAgendaDemo(c *ui.Context, log func(string)) { type state struct { - events []mujica.CalendarEvent + events []datetime.CalendarEvent days string } s := ui.Local(c.Root(), "424", func() state { return state{events: calSample(), days: "7"} }) dtSection(c, "AgendaView", func() { dtControls(c, func() { - mujica.Select(c, &s.days, calOptions([]string{"1", "7", "30"}), mujica.SelectOptions{Label: "Days"}) + input.Select(c, &s.days, calOptions([]string{"1", "7", "30"}), input.SelectOptions{Label: "Days"}) }) days := calAtoi(s.days) ui.Column(c).Height(420).Children(func() { - if id, ok := mujica.AgendaView(c, s.events, mujica.AgendaViewOptions{Location: time.Local, Days: days}).Opened(); ok { + if id, ok := datetime.AgendaView(c, s.events, datetime.AgendaViewOptions{Location: time.Local, Days: days}).Opened(); ok { log("opened " + id) } }) @@ -167,11 +169,11 @@ func calAgendaDemo(c *ui.Context, log func(string)) { func calYearDemo(c *ui.Context, log func(string)) { type state struct { year int - events []mujica.CalendarEvent + events []datetime.CalendarEvent } s := ui.Local(c.Root(), "425", func() state { return state{year: time.Now().Year(), events: calSample()} }) dtSection(c, "YearView", func() { - if d, ok := mujica.YearView(c, &s.year, s.events, mujica.YearViewOptions{Location: time.Local}).DayChosen(); ok { + if d, ok := datetime.YearView(c, &s.year, s.events, datetime.YearViewOptions{Location: time.Local}).DayChosen(); ok { log("day " + d.String()) } }) @@ -185,10 +187,10 @@ func calAtoi(s string) int { return n } -func calOptions(values []string) []mujica.SelectOption[string] { - out := make([]mujica.SelectOption[string], len(values)) +func calOptions(values []string) []input.SelectOption[string] { + out := make([]input.SelectOption[string], len(values)) for i, v := range values { - out[i] = mujica.SelectOption[string]{Value: v, Label: v} + out[i] = input.SelectOption[string]{Value: v, Label: v} } return out } diff --git a/examples/gallery/pages_charts1.go b/examples/gallery/pages_charts1.go index f7cb1dd..f8e3f52 100644 --- a/examples/gallery/pages_charts1.go +++ b/examples/gallery/pages_charts1.go @@ -5,8 +5,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const c1Category = "Charts I" @@ -39,14 +41,14 @@ var charts1Pages = []page{ var c1Statuses = []string{"Ready", "Loading", "Failed", "Empty"} // c1Frame shows the status picker and returns the frame. -func c1Frame(c *ui.Context, label string, status *string) (mujica.ChartFrame, bool) { +func c1Frame(c *ui.Context, label string, status *string) (chart.ChartFrame, bool) { ui.Select(c, status, c1Statuses).Width(160).AlignSelf(ui.Start) - f := mujica.ChartFrame{Label: label} + f := chart.ChartFrame{Label: label} switch *status { case "Loading": - f.Status = mujica.DataLoading + f.Status = data.DataLoading case "Failed": - f.Status, f.Error = mujica.DataFailed, "The archive did not answer" + f.Status, f.Error = data.DataFailed, "The archive did not answer" } return f, *status != "Empty" } @@ -54,14 +56,14 @@ func c1Frame(c *ui.Context, label string, status *string) (mujica.ChartFrame, bo // c1Section lays out a title, controls, then the chart. func c1Section(c *ui.Context, title string, controls, body func()) { ui.Column(c).Gap(12).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)) ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(controls) ui.Column(c).Width(560).Children(body) }) } // c1Log records a chart's events in the gallery log. -func c1Log(log func(string), name string, r mujica.ChartResult) { +func c1Log(log func(string), name string, r chart.ChartResult) { if r.Changed() { log(fmt.Sprintf("%s: point %d", name, r.Point)) } diff --git a/examples/gallery/pages_charts1_code.go b/examples/gallery/pages_charts1_code.go index 8548a47..fd6c695 100644 --- a/examples/gallery/pages_charts1_code.go +++ b/examples/gallery/pages_charts1_code.go @@ -2,173 +2,173 @@ package main // Snippets are the bodies of the Examples in example_charts1_test.go. -const c1LineCode = `series := []mujica.ChartSeries{ +const c1LineCode = `series := []chart.ChartSeries{ {Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}}, {Name: "Candles", Values: []float64{5, 2, 6, 7, 1}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineChart(c, series, mujica.LineChartOptions{Frame: mujica.ChartFrame{Label: "Chapel"}}) + core.Use(c, core.Settings{}) + chart.LineChart(c, series, chart.LineChartOptions{Frame: chart.ChartFrame{Label: "Chapel"}}) } ui.Render(view, 400, 300, 1)` -const c1AreaCode = `series := []mujica.ChartSeries{ +const c1AreaCode = `series := []chart.ChartSeries{ {Name: "Matins", Values: []float64{3, 4, 6, 5}}, {Name: "Vespers", Values: []float64{2, 3, 2, 4}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: mujica.ChartFrame{Label: "Attendance"}, Stacked: true}) + core.Use(c, core.Settings{}) + chart.AreaChart(c, series, chart.AreaChartOptions{Frame: chart.ChartFrame{Label: "Attendance"}, Stacked: true}) } ui.Render(view, 400, 300, 1)` const c1BarCode = `categories := []string{"Nave", "Choir", "Crypt"} -series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} +series := []chart.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarChart(c, categories, series, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Tithes"}, Labels: true}) + core.Use(c, core.Settings{}) + chart.BarChart(c, categories, series, chart.BarChartOptions{Frame: chart.ChartFrame{Label: "Tithes"}, Labels: true}) } ui.Render(view, 400, 300, 1)` -const c1PieCode = `slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} +const c1PieCode = `slices := []chart.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: mujica.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) + core.Use(c, core.Settings{}) + chart.PieChart(c, slices, chart.PieChartOptions{Frame: chart.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20}) } ui.Render(view, 400, 300, 1)` -const c1ScatterCode = `var selected []mujica.ScatterKey -groups := []mujica.ScatterGroup{{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} +const c1ScatterCode = `var selected []chart.ScatterKey +groups := []chart.ScatterGroup{{Name: "Monks", Points: []chart.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScatterChart(c, &selected, groups, mujica.ScatterChartOptions{Frame: mujica.ChartFrame{Label: "Choir"}, Bubbles: true}) + core.Use(c, core.Settings{}) + chart.ScatterChart(c, &selected, groups, chart.ScatterChartOptions{Frame: chart.ChartFrame{Label: "Choir"}, Bubbles: true}) } ui.Render(view, 400, 300, 1)` const c1RadarCode = `axes := []string{"Voice", "Latin", "Chant", "Organ"} -series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} +series := []chart.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RadarChart(c, axes, series, mujica.RadarChartOptions{Frame: mujica.ChartFrame{Label: "Skills"}}) + core.Use(c, core.Settings{}) + chart.RadarChart(c, axes, series, chart.RadarChartOptions{Frame: chart.ChartFrame{Label: "Skills"}}) } ui.Render(view, 400, 300, 1)` const c1HeatCode = `rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"} values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: mujica.ChartFrame{Label: "Hours"}}) + core.Use(c, core.Settings{}) + chart.HeatmapChart(c, rows, cols, values, chart.HeatmapChartOptions{Frame: chart.ChartFrame{Label: "Hours"}}) } ui.Render(view, 400, 300, 1)` const c1CalendarCode = `year := 2026 -values := map[mujica.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} +values := map[core.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CalendarHeatmap(c, &year, values, mujica.CalendarHeatmapOptions{Frame: mujica.ChartFrame{Label: "Prayers"}}) + core.Use(c, core.Settings{}) + chart.CalendarHeatmap(c, &year, values, chart.CalendarHeatmapOptions{Frame: chart.ChartFrame{Label: "Prayers"}}) } ui.Render(view, 400, 300, 1)` const c1HistogramCode = `values := []float64{1, 2, 2, 3, 4, 5, 9, 10} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Histogram(c, values, mujica.HistogramOptions{Frame: mujica.ChartFrame{Label: "Ages"}, BinWidth: 5}) + core.Use(c, core.Settings{}) + chart.Histogram(c, values, chart.HistogramOptions{Frame: chart.ChartFrame{Label: "Ages"}, BinWidth: 5}) } ui.Render(view, 400, 300, 1)` -const c1BoxCode = `groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} +const c1BoxCode = `groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}}) + core.Use(c, core.Settings{}) + chart.BoxPlot(c, groups, chart.BoxPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}}) } ui.Render(view, 400, 300, 1)` -const c1WaterfallCode = `steps := []mujica.WaterfallStep{ +const c1WaterfallCode = `steps := []chart.WaterfallStep{ {Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, - {Label: "Year", Kind: mujica.WaterfallTotal}, + {Label: "Year", Kind: chart.WaterfallTotal}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: mujica.ChartFrame{Label: "Ledger"}}) + core.Use(c, core.Settings{}) + chart.WaterfallChart(c, steps, chart.WaterfallChartOptions{Frame: chart.ChartFrame{Label: "Ledger"}}) } ui.Render(view, 400, 300, 1)` -const c1FunnelCode = `stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} +const c1FunnelCode = `stages := []chart.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: mujica.ChartFrame{Label: "Order"}}) + core.Use(c, core.Settings{}) + chart.FunnelChart(c, stages, chart.FunnelChartOptions{Frame: chart.ChartFrame{Label: "Order"}}) } ui.Render(view, 400, 300, 1)` const c1BulletCode = `target := 250.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BulletChart(c, 270, mujica.BulletChartOptions{Frame: mujica.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) + core.Use(c, core.Settings{}) + chart.BulletChart(c, 270, chart.BulletChartOptions{Frame: chart.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}}) } ui.Render(view, 400, 300, 1)` const c1SparkCode = `values := []float64{3, 5, 4, 7, 6, 9} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: mujica.ChartFrame{Label: "Bells"}, Kind: mujica.SparklineArea}) + core.Use(c, core.Settings{}) + chart.Sparkline(c, values, chart.SparklineOptions{Frame: chart.ChartFrame{Label: "Bells"}, Kind: chart.SparklineArea}) } ui.Render(view, 400, 300, 1)` -const c1GaugeCode = `segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}} +const c1GaugeCode = `segments := []chart.GaugeSegment{{To: 60, Label: "Calm", Tone: chart.GaugeGood}, {To: 100, Label: "Crowded", Tone: chart.GaugeDanger}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Gauge(c, 72, mujica.GaugeOptions{Frame: mujica.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) + core.Use(c, core.Settings{}) + chart.Gauge(c, 72, chart.GaugeOptions{Frame: chart.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments}) } ui.Render(view, 400, 300, 1)` const c1TreemapCode = `var path []string -root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, +root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, {Name: "Kitchen", Value: 25}, }} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Treemap(c, &path, root, mujica.TreemapOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) + core.Use(c, core.Settings{}) + chart.Treemap(c, &path, root, chart.TreemapOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) } ui.Render(view, 400, 300, 1)` const c1SunburstCode = `var path []string -root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, +root := chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}}, {Name: "Kitchen", Value: 25}, }} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Sunburst(c, &path, root, mujica.SunburstOptions{Frame: mujica.ChartFrame{Label: "Rooms"}}) + core.Use(c, core.Settings{}) + chart.Sunburst(c, &path, root, chart.SunburstOptions{Frame: chart.ChartFrame{Label: "Rooms"}}) } ui.Render(view, 400, 300, 1)` -const c1SankeyCode = `nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} -links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} +const c1SankeyCode = `nodes := []chart.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}} +links := []chart.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: mujica.ChartFrame{Label: "Money"}}) + core.Use(c, core.Settings{}) + chart.SankeyChart(c, nodes, links, chart.SankeyChartOptions{Frame: chart.ChartFrame{Label: "Money"}}) } ui.Render(view, 400, 300, 1)` -const c1NetworkCode = `var state mujica.NetworkGraphState -nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} -edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} +const c1NetworkCode = `var state chart.NetworkGraphState +nodes := []chart.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}} +edges := []chart.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.NetworkGraph(c, &state, nodes, edges, mujica.NetworkGraphOptions{Frame: mujica.ChartFrame{Label: "House"}, Force: true}) + core.Use(c, core.Settings{}) + chart.NetworkGraph(c, &state, nodes, edges, chart.NetworkGraphOptions{Frame: chart.ChartFrame{Label: "House"}, Force: true}) } ui.Render(view, 400, 300, 1)` -const c1RealtimeCode = `buf := mujica.NewRealtimeBuffer(600, "Bell") +const c1RealtimeCode = `buf := chart.NewRealtimeBuffer(600, "Bell") start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) for i := range 60 { buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7)) } -var state mujica.RealtimeChartState +var state chart.RealtimeChartState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RealtimeChart(c, &state, buf, mujica.RealtimeChartOptions{Frame: mujica.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) + core.Use(c, core.Settings{}) + chart.RealtimeChart(c, &state, buf, chart.RealtimeChartOptions{Frame: chart.ChartFrame{Label: "Sound"}, Window: 30 * time.Second}) } ui.Render(view, 400, 300, 1)` diff --git a/examples/gallery/pages_charts1_flow.go b/examples/gallery/pages_charts1_flow.go index 02161ff..d2936ee 100644 --- a/examples/gallery/pages_charts1_flow.go +++ b/examples/gallery/pages_charts1_flow.go @@ -8,14 +8,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) -var c1Abbey = mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{ - {Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}, {Name: "Letters", Value: 8}}}, +var c1Abbey = chart.ChartNode{Name: "Abbey", Children: []chart.ChartNode{ + {Name: "Library", Children: []chart.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}, {Name: "Letters", Value: 8}}}, {Name: "Kitchen", Value: 25}, - {Name: "Garden", Children: []mujica.ChartNode{{Name: "Herbs", Value: 12}, {Name: "Vines", Value: 8}}}, - {Name: "Chapel", Children: []mujica.ChartNode{{Name: "Nave", Value: 18}, {Name: "Crypt", Value: 6}}}, + {Name: "Garden", Children: []chart.ChartNode{{Name: "Herbs", Value: 12}, {Name: "Vines", Value: 8}}}, + {Name: "Chapel", Children: []chart.ChartNode{{Name: "Nave", Value: 18}, {Name: "Crypt", Value: 6}}}, }} func c1TreemapDemo(c *ui.Context, log func(string)) { @@ -28,10 +29,10 @@ func c1TreemapDemo(c *ui.Context, log func(string)) { frame, show := c1Frame(c, "Rooms", &s.status) root := c1Abbey if !show { - root = mujica.ChartNode{Name: "Abbey"} + root = chart.ChartNode{Name: "Abbey"} s.path = nil } - r := mujica.Treemap(c, &s.path, root, mujica.TreemapOptions{Frame: frame}) + r := chart.Treemap(c, &s.path, root, chart.TreemapOptions{Frame: frame}) c1Log(log, "Treemap", r) if r.ValueChanged() { log("Treemap: path /" + strings.Join(s.path, "/")) @@ -54,10 +55,10 @@ func c1SunburstDemo(c *ui.Context, log func(string)) { frame.Height = 300 root := c1Abbey if !show { - root = mujica.ChartNode{Name: "Abbey"} + root = chart.ChartNode{Name: "Abbey"} s.path = nil } - r := mujica.Sunburst(c, &s.path, root, mujica.SunburstOptions{Frame: frame, Depth: int(s.depth)}) + r := chart.Sunburst(c, &s.path, root, chart.SunburstOptions{Frame: frame, Depth: int(s.depth)}) c1Log(log, "Sunburst", r) if r.ValueChanged() { log("Sunburst: path /" + strings.Join(s.path, "/")) @@ -70,12 +71,12 @@ func c1SankeyDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "118", func() state { return state{status: "Ready"} }) c1Section(c, "SankeyChart", func() {}, func() { frame, show := c1Frame(c, "Money", &s.status) - nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "alms", Label: "Alms"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}, {ID: "bread", Label: "Bread"}, {ID: "bells", Label: "Bells"}} - links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "alms", To: "fund", Value: 30}, {From: "alms", To: "bread", Value: 10}, {From: "fund", To: "roof", Value: 50}, {From: "fund", To: "bells", Value: 40}} + nodes := []chart.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "alms", Label: "Alms"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}, {ID: "bread", Label: "Bread"}, {ID: "bells", Label: "Bells"}} + links := []chart.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "alms", To: "fund", Value: 30}, {From: "alms", To: "bread", Value: 10}, {From: "fund", To: "roof", Value: 50}, {From: "fund", To: "bells", Value: 40}} if !show { links = nil } - c1Log(log, "SankeyChart", mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: frame})) + c1Log(log, "SankeyChart", chart.SankeyChart(c, nodes, links, chart.SankeyChartOptions{Frame: frame})) }) } @@ -83,23 +84,23 @@ func c1NetworkDemo(c *ui.Context, log func(string)) { type state struct { status string force bool - graph mujica.NetworkGraphState + graph chart.NetworkGraphState } s := ui.Local(c.Root(), "119", func() state { return state{status: "Ready", force: true} }) c1Section(c, "NetworkGraph", func() { ui.Checkbox(c, &s.force, "Force layout") - if mujica.Button(c, "Reset view", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Reset view", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.graph.ResetView() log("NetworkGraph: reset view") } }, func() { frame, show := c1Frame(c, "House", &s.status) - nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot", X: 0, Y: 0}, {ID: "b", Label: "Prior", X: 10, Y: 0}, {ID: "c", Label: "Cellarer", X: 10, Y: 10}, {ID: "d", Label: "Novice", X: 0, Y: 10}, {ID: "e", Label: "Cantor", X: 5, Y: 5}} - edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "a", To: "c"}, {From: "c", To: "d"}, {From: "b", To: "e"}, {From: "e", To: "d"}} + nodes := []chart.GraphNode{{ID: "a", Label: "Abbot", X: 0, Y: 0}, {ID: "b", Label: "Prior", X: 10, Y: 0}, {ID: "c", Label: "Cellarer", X: 10, Y: 10}, {ID: "d", Label: "Novice", X: 0, Y: 10}, {ID: "e", Label: "Cantor", X: 5, Y: 5}} + edges := []chart.GraphEdge{{From: "a", To: "b"}, {From: "a", To: "c"}, {From: "c", To: "d"}, {From: "b", To: "e"}, {From: "e", To: "d"}} if !show { nodes, edges = nil, nil } - c1Log(log, "NetworkGraph", mujica.NetworkGraph(c, &s.graph, nodes, edges, mujica.NetworkGraphOptions{Frame: frame, Force: s.force})) + c1Log(log, "NetworkGraph", chart.NetworkGraph(c, &s.graph, nodes, edges, chart.NetworkGraphOptions{Frame: frame, Force: s.force})) }) } @@ -107,13 +108,13 @@ func c1RealtimeDemo(c *ui.Context, log func(string)) { type state struct { status string feed bool - buf *mujica.RealtimeBuffer + buf *chart.RealtimeBuffer next time.Time tick int - view mujica.RealtimeChartState + view chart.RealtimeChartState } s := ui.Local(c.Root(), "120", func() state { - return state{status: "Ready", feed: true, buf: mujica.NewRealtimeBuffer(600, "Bell", "Organ")} + return state{status: "Ready", feed: true, buf: chart.NewRealtimeBuffer(600, "Bell", "Organ")} }) // The demo appends a sample every 500 ms while feeding. if s.feed && !c.Now().Before(s.next) { @@ -128,7 +129,7 @@ func c1RealtimeDemo(c *ui.Context, log func(string)) { } c1Section(c, "RealtimeChart", func() { ui.Checkbox(c, &s.feed, "Feed data") }, func() { frame, _ := c1Frame(c, "Sound", &s.status) - r := mujica.RealtimeChart(c, &s.view, s.buf, mujica.RealtimeChartOptions{Frame: frame, Window: 20 * time.Second}) + r := chart.RealtimeChart(c, &s.view, s.buf, chart.RealtimeChartOptions{Frame: frame, Window: 20 * time.Second}) c1Log(log, "RealtimeChart", r) if r.ValueChanged() { log(fmt.Sprintf("RealtimeChart: paused=%v", s.view.Paused)) diff --git a/examples/gallery/pages_charts1_series.go b/examples/gallery/pages_charts1_series.go index 6716f57..8e157d8 100644 --- a/examples/gallery/pages_charts1_series.go +++ b/examples/gallery/pages_charts1_series.go @@ -7,15 +7,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" ) -func c1Series(gap bool) []mujica.ChartSeries { +func c1Series(gap bool) []chart.ChartSeries { a := []float64{3, 5, 4, 6, 8, 7, 9, 11} if gap { a = []float64{3, 5, math.NaN(), 6, 8, 7, 9, 11} } - return []mujica.ChartSeries{ + return []chart.ChartSeries{ {Name: "Matins", Values: a}, {Name: "Vespers", Values: []float64{2, 3, 5, 4, 6, 9, 8, 7}}, {Name: "Compline", Values: []float64{1, 2, 2, 3, 2, 4, 5, 4}}, @@ -35,9 +35,9 @@ func c1LineDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.big, "100,000 points") }, func() { frame, show := c1Frame(c, "Attendance", &s.status) - series, opts := c1Series(s.gap), mujica.LineChartOptions{Frame: frame} + series, opts := c1Series(s.gap), chart.LineChartOptions{Frame: frame} if s.big { - series = []mujica.ChartSeries{{Name: "Wave", Values: c1Wave()}} + series = []chart.ChartSeries{{Name: "Wave", Values: c1Wave()}} } if s.time { t0 := time.Date(2026, 10, 5, 6, 0, 0, 0, time.UTC) @@ -50,7 +50,7 @@ func c1LineDemo(c *ui.Context, log func(string)) { series = nil opts.Times = nil } - c1Log(log, "LineChart", mujica.LineChart(c, series, opts)) + c1Log(log, "LineChart", chart.LineChart(c, series, opts)) }) } @@ -84,7 +84,7 @@ func c1AreaDemo(c *ui.Context, log func(string)) { if !show { series = nil } - c1Log(log, "AreaChart", mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: frame, Stacked: s.stacked, Baseline: s.base})) + c1Log(log, "AreaChart", chart.AreaChart(c, series, chart.AreaChartOptions{Frame: frame, Stacked: s.stacked, Baseline: s.base})) }) } @@ -101,11 +101,11 @@ func c1BarDemo(c *ui.Context, log func(string)) { }, func() { frame, show := c1Frame(c, "Tithes", &s.status) cats := []string{"Nave", "Choir", "Crypt", "Bells"} - series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3, 5}}, {Name: "Silver", Values: []float64{2, 5, 1, -1}}} + series := []chart.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3, 5}}, {Name: "Silver", Values: []float64{2, 5, 1, -1}}} if !show { series = nil } - c1Log(log, "BarChart", mujica.BarChart(c, cats, series, mujica.BarChartOptions{Frame: frame, Horizontal: s.horizontal, Stacked: s.stacked, Labels: s.labels})) + c1Log(log, "BarChart", chart.BarChart(c, cats, series, chart.BarChartOptions{Frame: frame, Horizontal: s.horizontal, Stacked: s.stacked, Labels: s.labels})) }) } @@ -122,11 +122,11 @@ func c1PieDemo(c *ui.Context, log func(string)) { ui.StepSlider(c, &s.min, 0, 40, 5).Width(140) }, func() { frame, show := c1Frame(c, "Stores", &s.status) - slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Myrrh", Value: 12}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 4}} + slices := []chart.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Myrrh", Value: 12}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 4}} if !show { slices = nil } - c1Log(log, "PieChart", mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: frame, Donut: s.donut, MinAngle: s.min})) + c1Log(log, "PieChart", chart.PieChart(c, slices, chart.PieChartOptions{Frame: frame, Donut: s.donut, MinAngle: s.min})) }) } @@ -134,7 +134,7 @@ func c1ScatterDemo(c *ui.Context, log func(string)) { type state struct { status string bubbles bool - selected []mujica.ScatterKey + selected []chart.ScatterKey } s := ui.Local(c.Root(), "105", func() state { return state{status: "Ready", bubbles: true} }) c1Section(c, "ScatterChart", func() { @@ -142,14 +142,14 @@ func c1ScatterDemo(c *ui.Context, log func(string)) { ui.Text(c, fmt.Sprintf("%d selected", len(s.selected))) }, func() { frame, show := c1Frame(c, "Choir", &s.status) - groups := []mujica.ScatterGroup{ - {Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}, {X: 6, Y: 4, Size: 1}, {X: 7, Y: 8, Size: 6}}}, - {Name: "Nuns", Points: []mujica.ScatterPoint{{X: 2, Y: 7, Size: 16}, {X: 8, Y: 9, Size: 2}, {X: 5, Y: 2, Size: 8}}}, + groups := []chart.ScatterGroup{ + {Name: "Monks", Points: []chart.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}, {X: 6, Y: 4, Size: 1}, {X: 7, Y: 8, Size: 6}}}, + {Name: "Nuns", Points: []chart.ScatterPoint{{X: 2, Y: 7, Size: 16}, {X: 8, Y: 9, Size: 2}, {X: 5, Y: 2, Size: 8}}}, } if !show { groups = nil } - r := mujica.ScatterChart(c, &s.selected, groups, mujica.ScatterChartOptions{Frame: frame, Bubbles: s.bubbles}) + r := chart.ScatterChart(c, &s.selected, groups, chart.ScatterChartOptions{Frame: frame, Bubbles: s.bubbles}) c1Log(log, "ScatterChart", r) if r.ValueChanged() { log(fmt.Sprintf("ScatterChart: %d selected", len(s.selected))) @@ -162,11 +162,11 @@ func c1RadarDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "106", func() state { return state{status: "Ready"} }) c1Section(c, "RadarChart", func() {}, func() { frame, show := c1Frame(c, "Skills", &s.status) - series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4, 7}}, {Name: "Clare", Values: []float64{5, 9, 6, 8, 3}}} + series := []chart.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4, 7}}, {Name: "Clare", Values: []float64{5, 9, 6, 8, 3}}} if !show { series = nil } - c1Log(log, "RadarChart", mujica.RadarChart(c, []string{"Voice", "Latin", "Chant", "Organ", "Bells"}, series, mujica.RadarChartOptions{Frame: frame})) + c1Log(log, "RadarChart", chart.RadarChart(c, []string{"Voice", "Latin", "Chant", "Organ", "Bells"}, series, chart.RadarChartOptions{Frame: frame})) }) } @@ -192,6 +192,6 @@ func c1HeatDemo(c *ui.Context, log func(string)) { if !show { rows, values = nil, nil } - c1Log(log, "HeatmapChart", mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: frame})) + c1Log(log, "HeatmapChart", chart.HeatmapChart(c, rows, cols, values, chart.HeatmapChartOptions{Frame: frame})) }) } diff --git a/examples/gallery/pages_charts1_stats.go b/examples/gallery/pages_charts1_stats.go index 3cb2c1e..ceb8292 100644 --- a/examples/gallery/pages_charts1_stats.go +++ b/examples/gallery/pages_charts1_stats.go @@ -8,7 +8,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) func c1CalendarDemo(c *ui.Context, log func(string)) { @@ -20,17 +22,17 @@ func c1CalendarDemo(c *ui.Context, log func(string)) { c1Section(c, "CalendarHeatmap", func() { ui.Select(c, &s.start, []string{"Locale", "Sunday", "Monday", "Saturday"}) }, func() { frame, show := c1Frame(c, "Prayers", &s.status) frame.Height = 160 - values := map[mujica.Date]float64{} + values := map[core.Date]float64{} if show { - d := mujica.Date{Year: s.year, Month: time.January, Day: 1} + d := core.Date{Year: s.year, Month: time.January, Day: 1} for i := range 366 { if (i*37)%5 != 0 { values[d.AddDays(i)] = float64((i * 13) % 9) } } } - start := map[string]mujica.WeekStart{"Locale": mujica.WeekStartLocale, "Sunday": mujica.WeekStartSunday, "Monday": mujica.WeekStartMonday, "Saturday": mujica.WeekStartSaturday}[s.start] - r := mujica.CalendarHeatmap(c, &s.year, values, mujica.CalendarHeatmapOptions{Frame: frame, WeekStart: start}) + start := map[string]datetime.WeekStart{"Locale": datetime.WeekStartLocale, "Sunday": datetime.WeekStartSunday, "Monday": datetime.WeekStartMonday, "Saturday": datetime.WeekStartSaturday}[s.start] + r := chart.CalendarHeatmap(c, &s.year, values, chart.CalendarHeatmapOptions{Frame: frame, WeekStart: start}) c1Log(log, "CalendarHeatmap", r) if r.ValueChanged() { log("CalendarHeatmap: year " + strconv.Itoa(s.year)) @@ -62,7 +64,7 @@ func c1HistogramDemo(c *ui.Context, log func(string)) { if !show { values = nil } - c1Log(log, "Histogram", mujica.Histogram(c, values, mujica.HistogramOptions{Frame: frame, Bins: int(s.bins), Cumulative: s.cumulative})) + c1Log(log, "Histogram", chart.Histogram(c, values, chart.HistogramOptions{Frame: frame, Bins: int(s.bins), Cumulative: s.cumulative})) }) } @@ -71,7 +73,7 @@ func c1BoxDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "110", func() state { return state{status: "Ready"} }) c1Section(c, "BoxPlot", func() {}, func() { frame, show := c1Frame(c, "Hymns", &s.status) - groups := []mujica.BoxGroup{ + groups := []chart.BoxGroup{ {Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 24}}, {Name: "Nave", Values: []float64{4, 5, 5, 6, 7, 8}}, {Name: "Crypt", Values: []float64{12, 14, 15, 18, 22, 25}}, @@ -79,7 +81,7 @@ func c1BoxDemo(c *ui.Context, log func(string)) { if !show { groups = nil } - c1Log(log, "BoxPlot", mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: frame})) + c1Log(log, "BoxPlot", chart.BoxPlot(c, groups, chart.BoxPlotOptions{Frame: frame})) }) } @@ -88,14 +90,14 @@ func c1WaterfallDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "111", func() state { return state{status: "Ready"} }) c1Section(c, "WaterfallChart", func() {}, func() { frame, show := c1Frame(c, "Ledger", &s.status) - steps := []mujica.WaterfallStep{ - {Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, {Label: "Q1", Kind: mujica.WaterfallSubtotal}, - {Label: "Alms", Value: 4}, {Label: "Bread", Value: -2}, {Label: "Year", Kind: mujica.WaterfallTotal}, + steps := []chart.WaterfallStep{ + {Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, {Label: "Q1", Kind: chart.WaterfallSubtotal}, + {Label: "Alms", Value: 4}, {Label: "Bread", Value: -2}, {Label: "Year", Kind: chart.WaterfallTotal}, } if !show { steps = nil } - c1Log(log, "WaterfallChart", mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: frame})) + c1Log(log, "WaterfallChart", chart.WaterfallChart(c, steps, chart.WaterfallChartOptions{Frame: frame})) }) } @@ -104,11 +106,11 @@ func c1FunnelDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "112", func() state { return state{status: "Ready"} }) c1Section(c, "FunnelChart", func() {}, func() { frame, show := c1Frame(c, "Order", &s.status) - stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}, {Label: "Priors", Value: 12}} + stages := []chart.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}, {Label: "Priors", Value: 12}} if !show { stages = nil } - c1Log(log, "FunnelChart", mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: frame})) + c1Log(log, "FunnelChart", chart.FunnelChart(c, stages, chart.FunnelChartOptions{Frame: frame})) }) } @@ -129,7 +131,7 @@ func c1BulletDemo(c *ui.Context, log func(string)) { frame.Height = 200 } target := s.target - c1Log(log, "BulletChart", mujica.BulletChart(c, s.value, mujica.BulletChartOptions{Frame: frame, Target: &target, Bands: []float64{150, 225, 300}, Vertical: s.vertical})) + c1Log(log, "BulletChart", chart.BulletChart(c, s.value, chart.BulletChartOptions{Frame: frame, Target: &target, Bands: []float64{150, 225, 300}, Vertical: s.vertical})) }) } @@ -143,10 +145,10 @@ func c1SparkDemo(c *ui.Context, log func(string)) { values = nil } ui.Row(c).Gap(24).Wrap().Children(func() { - for _, k := range []mujica.SparklineKind{mujica.SparklineLine, mujica.SparklineBar, mujica.SparklineArea} { + for _, k := range []chart.SparklineKind{chart.SparklineLine, chart.SparklineBar, chart.SparklineArea} { f := frame f.Label = fmt.Sprintf("Bells %d", k+1) - c1Log(log, f.Label, mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: f, Kind: k})) + c1Log(log, f.Label, chart.Sparkline(c, values, chart.SparklineOptions{Frame: f, Kind: k})) } }) }) @@ -165,7 +167,7 @@ func c1GaugeDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.full, "Full circle") }, func() { frame, _ := c1Frame(c, "Nave", &s.status) - segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 85, Label: "Busy", Tone: mujica.GaugeWarning}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}} - c1Log(log, "Gauge", mujica.Gauge(c, s.value, mujica.GaugeOptions{Frame: frame, Max: 100, Unit: "%", Segments: segments, Full: s.full})) + segments := []chart.GaugeSegment{{To: 60, Label: "Calm", Tone: chart.GaugeGood}, {To: 85, Label: "Busy", Tone: chart.GaugeWarning}, {To: 100, Label: "Crowded", Tone: chart.GaugeDanger}} + c1Log(log, "Gauge", chart.Gauge(c, s.value, chart.GaugeOptions{Frame: frame, Max: 100, Unit: "%", Segments: segments, Full: s.full})) }) } diff --git a/examples/gallery/pages_charts1_test.go b/examples/gallery/pages_charts1_test.go index af673ca..d584225 100644 --- a/examples/gallery/pages_charts1_test.go +++ b/examples/gallery/pages_charts1_test.go @@ -7,7 +7,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // c1Demo runs page id's demo with an event log. @@ -19,7 +19,7 @@ func c1Demo(t *testing.T, id string) (*ui.Tester, *[]string) { } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(24).Children(func() { charts1Pages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) diff --git a/examples/gallery/pages_charts2.go b/examples/gallery/pages_charts2.go index ef6aa5f..0e4c59e 100644 --- a/examples/gallery/pages_charts2.go +++ b/examples/gallery/pages_charts2.go @@ -5,7 +5,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const c2Category = "Charts II" @@ -29,7 +31,7 @@ var charts2Pages = []page{ } func c2Check(c *ui.Context, value *bool, label string, log func(string)) { - if mujica.Checkbox(c, value, label, mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, value, label, input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("%s = %v", label, *value)) } } @@ -37,7 +39,7 @@ func c2Check(c *ui.Context, value *bool, label string, log func(string)) { func c2Number(c *ui.Context, value *float64, label string, lo, hi, step float64, log func(string)) { ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() { ui.Text(c, label) - r := mujica.NumberInput(c, value, mujica.NumberInputOptions{Label: label, Min: lo, Max: hi, Step: step}) + r := input.NumberInput(c, value, input.NumberInputOptions{Label: label, Min: lo, Max: hi, Step: step}) r.Element.Width(150) if r.Changed() { log(fmt.Sprintf("%s = %g", label, *value)) @@ -46,25 +48,25 @@ func c2Number(c *ui.Context, value *float64, label string, lo, hi, step float64, } func c2Select(c *ui.Context, value *string, label string, choices []string, log func(string)) { - items := make([]mujica.SelectOption[string], len(choices)) + items := make([]input.SelectOption[string], len(choices)) for i, s := range choices { - items[i] = mujica.SelectOption[string]{Value: s, Label: s} + items[i] = input.SelectOption[string]{Value: s, Label: s} } - r := mujica.Select(c, value, items, mujica.SelectOptions{Label: label}) + r := input.Select(c, value, items, input.SelectOptions{Label: label}) r.Element.Width(160) if r.Changed() { log(label + " = " + *value) } } -func c2Frame(c *ui.Context, label string, status *string, log func(string)) (mujica.ChartFrame, bool) { +func c2Frame(c *ui.Context, label string, status *string, log func(string)) (chart.ChartFrame, bool) { c2Select(c, status, "Status", c1Statuses, log) - f := mujica.ChartFrame{Label: label} + f := chart.ChartFrame{Label: label} if *status == "Loading" { - f.Status = mujica.DataLoading + f.Status = data.DataLoading } if *status == "Failed" { - f.Status, f.Error = mujica.DataFailed, "The archive did not answer" + f.Status, f.Error = data.DataFailed, "The archive did not answer" } return f, *status != "Empty" } diff --git a/examples/gallery/pages_charts2_code.go b/examples/gallery/pages_charts2_code.go index e74b2a5..9e171ff 100644 --- a/examples/gallery/pages_charts2_code.go +++ b/examples/gallery/pages_charts2_code.go @@ -2,25 +2,25 @@ package main const c2ChartLegendCode = `hidden := map[string]bool{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - items := []mujica.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} - mujica.ChartLegend(c, &hidden, items, mujica.ChartLegendOptions{MaxItems: 1}) + core.Use(c, core.Settings{}) + k := core.Tokens(c) + items := []chart.ChartLegendItem{{Name: "Pews", Color: k.Info}, {Name: "Candles", Color: k.Success, Shape: 1}} + chart.ChartLegend(c, &hidden, items, chart.ChartLegendOptions{MaxItems: 1}) } ui.Render(view, 400, 200, 1)` const c2ChartTooltipCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - k := mujica.Tokens(c) - mujica.ChartTooltip(c, 40, 40, []mujica.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) + core.Use(c, core.Settings{}) + k := core.Tokens(c) + chart.ChartTooltip(c, 40, 40, []chart.ChartTooltipLine{{Text: "Lauds"}, {Text: "Pews 5", Color: k.Info}, {Text: "Candles 2", Color: k.Success, Shape: 1}}) } ui.Render(view, 400, 200, 1)` const c2ChartCrosshairCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + hair := chart.ChartCrosshair(c, chart.ChartCrosshairOptions{X: &x, Y: &y}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} x.Draw(p, plot) @@ -31,8 +31,8 @@ const c2ChartCrosshairCode = `view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const c2ChartAxisCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - axis := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Kind: mujica.AxisLog, Min: 1, Max: 1000}) + core.Use(c, core.Settings{}) + axis := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Kind: chart.AxisLog, Min: 1, Max: 1000}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { axis.Draw(p, ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60}) }) @@ -40,10 +40,10 @@ const c2ChartAxisCode = `view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const c2ChartGridCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - grid := mujica.ChartGrid(c, mujica.ChartGridOptions{X: &x, Y: &y, Minor: 2}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + grid := chart.ChartGrid(c, chart.ChartGridOptions{X: &x, Y: &y, Minor: 2}) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { plot := ui.Rect{X: r.X + 60, Y: r.Y + 20, W: r.W - 80, H: r.H - 60} grid.Draw(p, plot) @@ -55,17 +55,17 @@ ui.Render(view, 400, 300, 1)` const c2ChartBrushCode = `value := [2]float64{20, 60} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartBrush(c, &value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) + core.Use(c, core.Settings{}) + chart.ChartBrush(c, &value, chart.ChartBrushOptions{Label: "Months", Max: 100, Step: 1, Values: []float64{2, 4, 3, 8, 5}}) } ui.Render(view, 400, 160, 1)` -const c2ChartAnnotationCode = `notes := []mujica.ChartNote{{Kind: mujica.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"}} +const c2ChartAnnotationCode = `notes := []chart.ChartNote{{Kind: chart.NoteHLine, Value: 50, Text: "Goal", Draggable: true}, {Kind: chart.NoteBand, Value: 6, End: 8, Text: "Lent"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Max: 10}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100}) - annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y}) + core.Use(c, core.Settings{}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Max: 10}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100}) + annotation := chart.ChartAnnotation(c, notes, chart.ChartAnnotationOptions{X: &x, Y: &y}) box := ui.Box(c).Fill().Focusable().Label("Annotations") b := box.Bounds() annotation.Drag(box, ui.Rect{X: 60, Y: 20, W: b.W - 80, H: b.H - 60}) @@ -79,10 +79,10 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const c2ChartExportCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - data, err := mujica.ChartExport(c, func(c *ui.Context) { - mujica.SparkBar(c, []float64{3, -2, 5}, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}}) - }, mujica.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) + core.Use(c, core.Settings{}) + data, err := chart.ChartExport(c, func(c *ui.Context) { + chart.SparkBar(c, []float64{3, -2, 5}, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}}) + }, chart.ChartExportOptions{Width: 300, Height: 100, Scale: 2}) if err != nil { panic(err) } @@ -96,27 +96,27 @@ ui.Render(view, 400, 160, 1)` const c2SparkBarCode = `values := []float64{3, -2, 4, 1, -3} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SparkBar(c, values, mujica.SparkBarOptions{Frame: mujica.ChartFrame{Label: "Alms"}, BarWidth: 10}) + core.Use(c, core.Settings{}) + chart.SparkBar(c, values, chart.SparkBarOptions{Frame: chart.ChartFrame{Label: "Alms"}, BarWidth: 10}) } ui.Render(view, 400, 100, 1)` const c2TrendIndicatorCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TrendIndicator(c, 12.5, mujica.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) + core.Use(c, core.Settings{}) + chart.TrendIndicator(c, 12.5, chart.TrendIndicatorOptions{Precision: 1, Unit: "%", Note: "vs Lent"}) } ui.Render(view, 400, 100, 1)` const c2ChartEmptyStateCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartEmptyState(c, mujica.DataFailed, mujica.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) + core.Use(c, core.Settings{}) + chart.ChartEmptyState(c, data.DataFailed, chart.ChartEmptyStateOptions{Height: 180, Error: "The archive did not answer"}) } ui.Render(view, 400, 240, 1)` -const c2ViolinPlotCode = `groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} +const c2ViolinPlotCode = `groups := []chart.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 7, 9}}, {Name: "Nave", Values: []float64{2, 4, 5, 6, 8}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) + core.Use(c, core.Settings{}) + chart.ViolinPlot(c, groups, chart.ViolinPlotOptions{Frame: chart.ChartFrame{Label: "Hymns"}, Bandwidth: 1}) } ui.Render(view, 400, 300, 1)` @@ -124,22 +124,22 @@ const c2ParallelCoordinatesCode = `filters := map[string][2]float64{} axes := []string{"Age", "Hymns", "Vigils"} rows := [][]float64{{20, 3, 9}, {35, 8, 2}, {50, 5, 5}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ParallelCoordinates(c, &filters, axes, rows, mujica.ParallelCoordinatesOptions{Frame: mujica.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) + core.Use(c, core.Settings{}) + chart.ParallelCoordinates(c, &filters, axes, rows, chart.ParallelCoordinatesOptions{Frame: chart.ChartFrame{Label: "Monks"}, Names: []string{"Agnes", "Clare", "Hild"}}) } ui.Render(view, 400, 300, 1)` const c2ChordDiagramCode = `names := []string{"Nave", "Choir", "Crypt"} matrix := [][]float64{{0, 5, 2}, {4, 0, 1}, {1, 2, 0}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: mujica.ChartFrame{Label: "Paths"}}) + core.Use(c, core.Settings{}) + chart.ChordDiagram(c, names, matrix, chart.ChordDiagramOptions{Frame: chart.ChartFrame{Label: "Paths"}}) } ui.Render(view, 400, 300, 1)` -const c2DensityPlotCode = `groups := []mujica.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} +const c2DensityPlotCode = `groups := []chart.BoxGroup{{Name: "Matins", Values: []float64{1, 2, 2, 3, 7}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: mujica.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) + core.Use(c, core.Settings{}) + chart.DensityPlot(c, groups, chart.DensityPlotOptions{Frame: chart.ChartFrame{Label: "Ages"}, Bandwidth: 1, Points: 128}) } ui.Render(view, 400, 300, 1)` diff --git a/examples/gallery/pages_charts2_distribution.go b/examples/gallery/pages_charts2_distribution.go index 6dae3f8..1d1d70d 100644 --- a/examples/gallery/pages_charts2_distribution.go +++ b/examples/gallery/pages_charts2_distribution.go @@ -5,7 +5,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func c2ViolinDemo(c *ui.Context, log func(string)) { c2DistributionDemo(c, log, "129", "ViolinPlot") } @@ -16,10 +17,10 @@ func c2DistributionDemo(c *ui.Context, log func(string), id, name string) { status string bandwidth float64 disabled bool - groups []mujica.BoxGroup + groups []chart.BoxGroup } s := ui.Local(c.Root(), id, func() state { - return state{status: "Ready", groups: []mujica.BoxGroup{ + return state{status: "Ready", groups: []chart.BoxGroup{ {Name: "Matins", Values: []float64{1, 2, 2, 3, 4, 5, 8, 9}}, {Name: "Vespers", Values: []float64{3, 4, 5, 6, 6, 7, 8, 11}}, }} @@ -35,9 +36,9 @@ func c2DistributionDemo(c *ui.Context, log func(string), id, name string) { } ui.Column(c).Disabled(s.disabled).Children(func() { if id == "129" { - c1Log(log, name, mujica.ViolinPlot(c, groups, mujica.ViolinPlotOptions{Frame: f, Bandwidth: s.bandwidth})) + c1Log(log, name, chart.ViolinPlot(c, groups, chart.ViolinPlotOptions{Frame: f, Bandwidth: s.bandwidth})) } else { - c1Log(log, name, mujica.DensityPlot(c, groups, mujica.DensityPlotOptions{Frame: f, Bandwidth: s.bandwidth})) + c1Log(log, name, chart.DensityPlot(c, groups, chart.DensityPlotOptions{Frame: f, Bandwidth: s.bandwidth})) } }) }) @@ -55,7 +56,7 @@ func c2ParallelDemo(c *ui.Context, log func(string)) { }) c1Section(c, "ParallelCoordinates", func() { c2Check(c, &s.disabled, "Disabled", log) - if mujica.Button(c, "Clear filters", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Clear filters", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.filters = map[string][2]float64{} log("ParallelCoordinates: clear filters") } @@ -68,7 +69,7 @@ func c2ParallelDemo(c *ui.Context, log func(string)) { names = nil } ui.Column(c).Disabled(s.disabled).Children(func() { - c1Log(log, "ParallelCoordinates", mujica.ParallelCoordinates(c, &s.filters, []string{"Age", "Hymns", "Vigils"}, rows, mujica.ParallelCoordinatesOptions{Frame: f, Names: names})) + c1Log(log, "ParallelCoordinates", chart.ParallelCoordinates(c, &s.filters, []string{"Age", "Hymns", "Vigils"}, rows, chart.ParallelCoordinatesOptions{Frame: f, Names: names})) }) ui.Text(c, fmt.Sprintf("Filters: %v", s.filters)) }) @@ -99,7 +100,7 @@ func c2ChordDemo(c *ui.Context, log func(string)) { matrix = nil } ui.Column(c).Disabled(s.disabled).Children(func() { - c1Log(log, "ChordDiagram", mujica.ChordDiagram(c, names, matrix, mujica.ChordDiagramOptions{Frame: f})) + c1Log(log, "ChordDiagram", chart.ChordDiagram(c, names, matrix, chart.ChordDiagramOptions{Frame: f})) }) }) } diff --git a/examples/gallery/pages_charts2_parts.go b/examples/gallery/pages_charts2_parts.go index 1390f5d..e0903d6 100644 --- a/examples/gallery/pages_charts2_parts.go +++ b/examples/gallery/pages_charts2_parts.go @@ -6,15 +6,16 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) type c2PartsState struct { status, kind string disabled, crosshair, draggable, fixed bool minor float64 - notes []mujica.ChartNote + notes []chart.ChartNote } func c2TooltipDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "122", "ChartTooltip") } @@ -25,11 +26,11 @@ func c2AnnotationDemo(c *ui.Context, log func(string)) { c2PartsDemo(c, log, "12 func c2PartsDemo(c *ui.Context, log func(string), id, name string) { s := ui.Local(c.Root(), id, func() c2PartsState { - return c2PartsState{status: "Ready", kind: "Linear", crosshair: true, minor: 2, draggable: true, notes: []mujica.ChartNote{ - {Kind: mujica.NoteHLine, Value: 50, Text: "Goal"}, - {Kind: mujica.NoteVLine, Value: 3, Text: "Advent"}, - {Kind: mujica.NoteBand, Value: 6, End: 8, Text: "Lent"}, - {Kind: mujica.NoteText, Value: 1, End: 90, Text: "Feast"}, + return c2PartsState{status: "Ready", kind: "Linear", crosshair: true, minor: 2, draggable: true, notes: []chart.ChartNote{ + {Kind: chart.NoteHLine, Value: 50, Text: "Goal"}, + {Kind: chart.NoteVLine, Value: 3, Text: "Advent"}, + {Kind: chart.NoteBand, Value: 6, End: 8, Text: "Lent"}, + {Kind: chart.NoteText, Value: 1, End: 90, Text: "Feast"}, }} }) c1Section(c, name, func() { @@ -56,23 +57,23 @@ func c2PartsDemo(c *ui.Context, log func(string), id, name string) { }) } -func c2PartsChart(c *ui.Context, frame mujica.ChartFrame, show bool, s *c2PartsState, id string, log func(string)) mujica.ChartResult { - xo := mujica.ChartAxisOptions{Max: 10} - yo := mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 100} +func c2PartsChart(c *ui.Context, frame chart.ChartFrame, show bool, s *c2PartsState, id string, log func(string)) chart.ChartResult { + xo := chart.ChartAxisOptions{Max: 10} + yo := chart.ChartAxisOptions{Side: chart.SideLeft, Max: 100} pts := [][2]float64{{2, 30}, {5, 80}, {8, 55}} if id == "124" { switch s.kind { case "Log": - yo.Kind, yo.Min = mujica.AxisLog, 1 + yo.Kind, yo.Min = chart.AxisLog, 1 case "Time": - xo.Kind = mujica.AxisTime + xo.Kind = chart.AxisTime xo.From = time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) xo.To = xo.From.Add(10 * time.Hour) for i := range pts { pts[i][0] = float64(xo.From.Unix()) + pts[i][0]*3600 } case "Category": - xo.Kind, xo.Categories = mujica.AxisCategory, []string{"Nave", "Choir", "Crypt"} + xo.Kind, xo.Categories = chart.AxisCategory, []string{"Nave", "Choir", "Crypt"} for i := range pts { pts[i][0] = float64(i) } @@ -81,17 +82,17 @@ func c2PartsChart(c *ui.Context, frame mujica.ChartFrame, show bool, s *c2PartsS yo.Ticks = []float64{10, 50, 90} } } - x, y := mujica.ChartAxis(c, xo), mujica.ChartAxis(c, yo) - grid := mujica.ChartGrid(c, mujica.ChartGridOptions{Y: &y, Minor: int(s.minor)}) - hair := mujica.ChartCrosshair(c, mujica.ChartCrosshairOptions{X: &x, Y: &y}) - var notes []mujica.ChartNote + x, y := chart.ChartAxis(c, xo), chart.ChartAxis(c, yo) + grid := chart.ChartGrid(c, chart.ChartGridOptions{Y: &y, Minor: int(s.minor)}) + hair := chart.ChartCrosshair(c, chart.ChartCrosshairOptions{X: &x, Y: &y}) + var notes []chart.ChartNote if id == "127" { notes = s.notes for i := range notes { notes[i].Draggable = s.draggable } } - annotation := mujica.ChartAnnotation(c, notes, mujica.ChartAnnotationOptions{X: &x, Y: &y}) + annotation := chart.ChartAnnotation(c, notes, chart.ChartAnnotationOptions{X: &x, Y: &y}) plotOf := func(r ui.Rect) ui.Rect { return ui.Rect{X: r.X + 64, Y: r.Y + 16, W: max(1, r.W-100), H: max(1, r.H-56)} } @@ -103,16 +104,16 @@ func c2PartsChart(c *ui.Context, frame mujica.ChartFrame, show bool, s *c2PartsS if !show { marks = 0 } - colors := internal.Palette(mujica.Tokens(c)) - return mujica.Chart(c, frame, mujica.ChartSpec{ + colors := internal.Palette(core.Tokens(c)) + return chart.Chart(c, frame, chart.ChartSpec{ Marks: marks, Pick: func(r ui.Rect, px, py float32) int { return hair.Snap(len(pts), func(i int) (float32, float32) { return at(r, i) }, px, py) }, Anchor: at, Readout: func(i int) string { return fmt.Sprintf("x %s · y %s", x.Label(pts[i][0]), y.Label(pts[i][1])) }, - Tooltip: func(i int) []mujica.ChartTooltipLine { - return []mujica.ChartTooltipLine{{Text: x.Label(pts[i][0])}, {Text: "Pews " + y.Label(pts[i][1]), Color: colors[0]}, {Text: "Candles " + y.Label(pts[i][1]/2), Color: colors[1], Shape: 1}} + Tooltip: func(i int) []chart.ChartTooltipLine { + return []chart.ChartTooltipLine{{Text: x.Label(pts[i][0])}, {Text: "Pews " + y.Label(pts[i][1]), Color: colors[0]}, {Text: "Candles " + y.Label(pts[i][1]/2), Color: colors[1], Shape: 1}} }, Input: func(el *ui.Element, r ui.Rect) { if annotation.Drag(el, plotOf(r)) { diff --git a/examples/gallery/pages_charts2_stats.go b/examples/gallery/pages_charts2_stats.go index 95f8c7a..a13d1a4 100644 --- a/examples/gallery/pages_charts2_stats.go +++ b/examples/gallery/pages_charts2_stats.go @@ -5,8 +5,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) func c2LegendDemo(c *ui.Context, log func(string)) { @@ -21,14 +23,14 @@ func c2LegendDemo(c *ui.Context, log func(string)) { c2Check(c, &s.disabled, "Disabled", log) c2Number(c, &s.max, "Max items", 0, 6, 1, log) }, func() { - pal := internal.Palette(mujica.Tokens(c)) + pal := internal.Palette(core.Tokens(c)) names := []string{"Matins", "Lauds", "Prime", "Terce", "Sext", "None"} - items := make([]mujica.ChartLegendItem, len(names)) + items := make([]chart.ChartLegendItem, len(names)) for i, n := range names { - items[i] = mujica.ChartLegendItem{Name: n, Color: pal[i], Shape: i % 5} + items[i] = chart.ChartLegendItem{Name: n, Color: pal[i], Shape: i % 5} } ui.Column(c).Disabled(s.disabled).Children(func() { - r := mujica.ChartLegend(c, &s.hidden, items, mujica.ChartLegendOptions{Vertical: s.vertical, MaxItems: int(s.max)}) + r := chart.ChartLegend(c, &s.hidden, items, chart.ChartLegendOptions{Vertical: s.vertical, MaxItems: int(s.max)}) if expanded, ok := r.Expanded(); ok { log(fmt.Sprintf("ChartLegend: expanded %v", expanded)) } @@ -52,12 +54,12 @@ func c2BrushDemo(c *ui.Context, log func(string)) { c2Number(c, &s.step, "Step", 1, 10, 1, log) }, func() { vals := []float64{1, 4, 2, 7, 3, 8, 4, 9} - r := mujica.ChartBrush(c, &s.value, mujica.ChartBrushOptions{Label: "Months", Max: 100, Step: s.step, Values: vals, Disabled: s.disabled}) + r := chart.ChartBrush(c, &s.value, chart.ChartBrushOptions{Label: "Months", Max: 100, Step: s.step, Values: vals, Disabled: s.disabled}) if r.Changed() { log(fmt.Sprintf("ChartBrush: range %g–%g", s.value[0], s.value[1])) } - x := mujica.ChartAxis(c, mujica.ChartAxisOptions{Min: s.value[0], Max: s.value[1]}) - y := mujica.ChartAxis(c, mujica.ChartAxisOptions{Side: mujica.SideLeft, Max: 10}) + x := chart.ChartAxis(c, chart.ChartAxisOptions{Min: s.value[0], Max: s.value[1]}) + y := chart.ChartAxis(c, chart.ChartAxisOptions{Side: chart.SideLeft, Max: 10}) xs := []float64{0, 15, 30, 45, 60, 75, 90, 100} var visible []int for i, v := range xs { @@ -70,7 +72,7 @@ func c2BrushDemo(c *ui.Context, log func(string)) { plot := plotOf(r) return x.Map(plot, xs[visible[i]]), y.Map(plot, vals[visible[i]]) } - c1Log(log, "Zoom preview", mujica.Chart(c, mujica.ChartFrame{Label: "Zoom preview", Height: 140}, mujica.ChartSpec{ + c1Log(log, "Zoom preview", chart.Chart(c, chart.ChartFrame{Label: "Zoom preview", Height: 140}, chart.ChartSpec{ Marks: len(visible), Anchor: at, Pick: func(r ui.Rect, px, py float32) int { return internal.Nearest(len(visible), func(i int) (float32, float32) { return at(r, i) }, px, py, 24, false) @@ -82,7 +84,7 @@ func c2BrushDemo(c *ui.Context, log func(string)) { y.Draw(p, plot) for i := range visible { px, py := at(r, i) - internal.Marker(p, 0, px, py, 8, mujica.Tokens(c).AccentText) + internal.Marker(p, 0, px, py, 8, core.Tokens(c).AccentText) } }, })) @@ -104,10 +106,10 @@ func c2ExportDemo(c *ui.Context, log func(string)) { c2Number(c, &s.scale, "Scale", 1, 3, 0.5, log) c2Check(c, &s.disabled, "Disabled", log) }, func() { - if mujica.Button(c, "Export PNG", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { - data, err := mujica.ChartExport(c, func(c *ui.Context) { - mujica.BarChart(c, []string{"Nave", "Choir", "Crypt"}, []mujica.ChartSeries{{Name: "Pews", Values: []float64{3, 5, 2}}}, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Exported chart", Height: float32(s.height) - 80}}) - }, mujica.ChartExportOptions{Width: int(s.width), Height: int(s.height), Scale: float32(s.scale)}) + if input.Button(c, "Export PNG", input.ButtonOptions{Disabled: s.disabled}).Clicked() { + data, err := chart.ChartExport(c, func(c *ui.Context) { + chart.BarChart(c, []string{"Nave", "Choir", "Crypt"}, []chart.ChartSeries{{Name: "Pews", Values: []float64{3, 5, 2}}}, chart.BarChartOptions{Frame: chart.ChartFrame{Label: "Exported chart", Height: float32(s.height) - 80}}) + }, chart.ChartExportOptions{Width: int(s.width), Height: int(s.height), Scale: float32(s.scale)}) if err == nil { s.bitmap, err = ui.DecodeBitmap(data) } @@ -121,7 +123,7 @@ func c2ExportDemo(c *ui.Context, log func(string)) { } } if s.error != "" { - ui.Text(c, s.error).TextColor(mujica.Tokens(c).Danger).Role(ui.RoleStatus) + ui.Text(c, s.error).TextColor(core.Tokens(c).Danger).Role(ui.RoleStatus) } if s.bitmap != nil { ui.Image(c, s.bitmap).Size(float32(s.width), float32(s.height)) @@ -147,7 +149,7 @@ func c2SparkBarDemo(c *ui.Context, log func(string)) { vals = nil } ui.Column(c).Disabled(s.disabled).Children(func() { - c1Log(log, "SparkBar", mujica.SparkBar(c, vals, mujica.SparkBarOptions{Frame: f, BarWidth: float32(s.width)})) + c1Log(log, "SparkBar", chart.SparkBar(c, vals, chart.SparkBarOptions{Frame: f, BarWidth: float32(s.width)})) }) }) } @@ -163,11 +165,11 @@ func c2TrendDemo(c *ui.Context, log func(string)) { c2Number(c, &s.precision, "Precision", 0, 3, 1, log) c2Check(c, &s.invert, "Invert", log) }, func() { - mujica.TrendIndicator(c, s.change, mujica.TrendIndicatorOptions{Precision: int(s.precision), Unit: "%", Note: "vs Lent", Invert: s.invert}) + chart.TrendIndicator(c, s.change, chart.TrendIndicatorOptions{Precision: int(s.precision), Unit: "%", Note: "vs Lent", Invert: s.invert}) ui.Row(c).Gap(24).Children(func() { - mujica.TrendIndicator(c, 3, mujica.TrendIndicatorOptions{}) - mujica.TrendIndicator(c, -3, mujica.TrendIndicatorOptions{}) - mujica.TrendIndicator(c, 0, mujica.TrendIndicatorOptions{}) + chart.TrendIndicator(c, 3, chart.TrendIndicatorOptions{}) + chart.TrendIndicator(c, -3, chart.TrendIndicatorOptions{}) + chart.TrendIndicator(c, 0, chart.TrendIndicatorOptions{}) }) }) } @@ -184,8 +186,8 @@ func c2EmptyDemo(c *ui.Context, log func(string)) { c2Check(c, &s.disabled, "Disabled", log) }, func() { f, _ := c2Frame(c, "ChartEmptyState", &s.status, log) - ui.Column(c).Disabled(s.disabled).Border(1, mujica.Tokens(c).Border).Children(func() { - if mujica.ChartEmptyState(c, f.Status, mujica.ChartEmptyStateOptions{Height: float32(s.height), Error: f.Error}).Changed() { + ui.Column(c).Disabled(s.disabled).Border(1, core.Tokens(c).Border).Children(func() { + if chart.ChartEmptyState(c, f.Status, chart.ChartEmptyStateOptions{Height: float32(s.height), Error: f.Error}).Changed() { log("ChartEmptyState: retry") s.status = "Loading" } diff --git a/examples/gallery/pages_charts2_test.go b/examples/gallery/pages_charts2_test.go index bd9dc8d..4ee5ddb 100644 --- a/examples/gallery/pages_charts2_test.go +++ b/examples/gallery/pages_charts2_test.go @@ -7,7 +7,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func c2Demo(t *testing.T, id string) (*ui.Tester, *[]string) { @@ -18,7 +18,7 @@ func c2Demo(t *testing.T, id string) (*ui.Tester, *[]string) { } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(24).Children(func() { charts2Pages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) }, 900, 760) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/examples/gallery/pages_chatinput.go b/examples/gallery/pages_chatinput.go index 594ac10..7fe7dde 100644 --- a/examples/gallery/pages_chatinput.go +++ b/examples/gallery/pages_chatinput.go @@ -5,62 +5,62 @@ const chatinputCategory = "Chat Input" var chatinputPages = []page{ {ID: "221", Name: "PromptComposer", ZhName: "提示编辑器", Category: chatinputCategory, Demo: chatinputDemoPromptComposer, Keys: "Enter 发送,Shift+Enter 换行;CmdEnterSends 下使用 Command/Ctrl+Enter;组合保护期间 Enter 留给输入法。", - Code: "var prompt string\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{Placeholder: \"Ask the herald\"})\n\tif r.Submitted() {\n\t\tfmt.Println(\"send\", prompt)\n\t\tprompt = \"\"\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var prompt string\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tr := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{Placeholder: \"Ask the herald\"})\n\tif r.Submitted() {\n\t\tfmt.Println(\"send\", prompt)\n\t\tprompt = \"\"\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "222", Name: "AttachmentChip", ZhName: "附件标签", Category: chatinputCategory, Demo: chatinputDemoAttachmentChip, Keys: "Tab 到移除与重试;Enter、Space 激活。", - Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.AttachmentChip(c, mujica.Attachment{Name: \"guests.csv\", Size: 4096, Uploading: true, Progress: 0.4})\n\tif r.Removed() {\n\t\tfmt.Println(\"cancel the upload\")\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "view := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tr := chat.AttachmentChip(c, chat.Attachment{Name: \"guests.csv\", Size: 4096, Uploading: true, Progress: 0.4})\n\tif r.Removed() {\n\t\tfmt.Println(\"cancel the upload\")\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "223", Name: "PasteImagePreview", ZhName: "图片预览", Category: chatinputCategory, Demo: chatinputDemoPasteImagePreview, Keys: "Tab 到移除按钮;Enter、Space 移除。", - Code: "pic := ui.MustParseSVG([]byte(``))\nimages := []mujica.PastedImage{{ID: \"p1\", Source: pic, Name: \"screenshot\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif id, ok := mujica.PasteImagePreview(c, images).Removed(); ok {\n\t\tfmt.Println(\"remove\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "pic := ui.MustParseSVG([]byte(``))\nimages := []chat.PastedImage{{ID: \"p1\", Source: pic, Name: \"screenshot\"}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif id, ok := chat.PasteImagePreview(c, images).Removed(); ok {\n\t\tfmt.Println(\"remove\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "224", Name: "DragDropOverlay", ZhName: "拖放遮罩", Category: chatinputCategory, Demo: chatinputDemoDragDropOverlay, Keys: "内容中的操作按其自身键盘规则访问;文件拖入由操作系统驱动。", - Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\timages := func(path string) bool { return strings.HasSuffix(path, \".png\") }\n\tr := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: images}, func() {\n\t\tui.Text(c, \"Drag images into the chat\")\n\t})\n\tfor _, path := range r.Accepted() {\n\t\tfmt.Println(\"attach\", path)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "view := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\timages := func(path string) bool { return strings.HasSuffix(path, \".png\") }\n\tr := chat.DragDropOverlay(c, chat.DragDropOverlayOptions{Accept: images}, func() {\n\t\tui.Text(c, \"Drag images into the chat\")\n\t})\n\tfor _, path := range r.Accepted() {\n\t\tfmt.Println(\"attach\", path)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "225", Name: "VoiceInputButton", ZhName: "语音输入按钮", Category: chatinputCategory, Demo: chatinputDemoVoiceInputButton, Keys: "Tab 到按钮;Enter、Space 开始或停止。", - Code: "var rec mujica.VoiceInputOptions\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.VoiceInputButton(c, rec).Changed() {\n\t\trec = mujica.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()}\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var rec chat.VoiceInputOptions\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif chat.VoiceInputButton(c, rec).Changed() {\n\t\trec = chat.VoiceInputOptions{Recording: !rec.Recording, Started: time.Now()}\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "226", Name: "VoiceWaveform", ZhName: "输入波形", Category: chatinputCategory, Demo: chatinputDemoVoiceWaveform, Keys: "展示组件,沿用外层 Tab 顺序。", - Code: "levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{})\n}\nui.Render(view, 400, 300, 1)"}, + Code: "levels := []float32{0.2, 0.6, 0.9, 0.4, 0.1}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tchat.VoiceWaveform(c, levels, chat.VoiceWaveformOptions{})\n}\nui.Render(view, 400, 300, 1)"}, {ID: "227", Name: "SlashCommandMenu", ZhName: "斜杠命令菜单", Category: chatinputCategory, Demo: chatinputDemoSlashCommandMenu, Keys: "编辑器内 Up/Down 选择,Enter 选中,Tab 补全,Escape 关闭。", - Code: "var prompt string\ncommands := []mujica.SlashCommand{{Name: \"summarize\", Description: \"Summarize the thread\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tcomposer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{})\n\tif cmd, ok := mujica.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok {\n\t\tfmt.Println(\"run\", cmd.Name)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var prompt string\ncommands := []chat.SlashCommand{{Name: \"summarize\", Description: \"Summarize the thread\"}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tcomposer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{})\n\tif cmd, ok := chat.SlashCommandMenu(c, composer, &prompt, commands).Chosen(); ok {\n\t\tfmt.Println(\"run\", cmd.Name)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "228", Name: "ContextMentionMenu", ZhName: "上下文提及菜单", Category: chatinputCategory, Demo: chatinputDemoContextMentionMenu, Keys: "编辑器内 Up/Down 选择,Enter 或 Tab 附加,Escape 关闭。", - Code: "var prompt string\nvar found []mujica.ContextItem\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tcomposer := mujica.PromptComposer(c, &prompt, mujica.PromptComposerOptions{})\n\tr := mujica.ContextMentionMenu(c, composer, &prompt, found, mujica.ContextMentionOptions{})\n\tif r.QueryChanged() {\n\t\tfound = []mujica.ContextItem{{ID: \"f1\", Label: r.Query() + \".md\", Kind: mujica.ContextFile}}\n\t}\n\tif item, ok := r.Chosen(); ok {\n\t\tfmt.Println(\"attach\", item.ID)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var prompt string\nvar found []chat.ContextItem\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tcomposer := chat.PromptComposer(c, &prompt, chat.PromptComposerOptions{})\n\tr := chat.ContextMentionMenu(c, composer, &prompt, found, chat.ContextMentionOptions{})\n\tif r.QueryChanged() {\n\t\tfound = []chat.ContextItem{{ID: \"f1\", Label: r.Query() + \".md\", Kind: chat.ContextFile}}\n\t}\n\tif item, ok := r.Chosen(); ok {\n\t\tfmt.Println(\"attach\", item.ID)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "229", Name: "ContextChips", ZhName: "上下文标签", Category: chatinputCategory, Demo: chatinputDemoContextChips, Keys: "Tab 到移除与展开;Enter、Space 激活。", - Code: "items := []mujica.ContextItem{{ID: \"f1\", Label: \"guests.csv\", Kind: mujica.ContextFile}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif id, ok := mujica.ContextChips(c, items, mujica.ContextChipsOptions{}).Removed(); ok {\n\t\tfmt.Println(\"detach\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "items := []chat.ContextItem{{ID: \"f1\", Label: \"guests.csv\", Kind: chat.ContextFile}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif id, ok := chat.ContextChips(c, items, chat.ContextChipsOptions{}).Removed(); ok {\n\t\tfmt.Println(\"detach\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "230", Name: "ModelSelector", ZhName: "模型选择器", Category: chatinputCategory, Demo: chatinputDemoModelSelector, Keys: "Tab 到选择器;Up/Down、Home/End 选择,Enter/Space 确认,Escape 关闭。", - Code: "model := \"herald\"\nmodels := []mujica.ModelOption{{ID: \"herald\", Name: \"Herald 2\", Capabilities: []string{\"Vision\"}, ContextTokens: 200000}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.ModelSelector(c, &model, models, mujica.ModelSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"model\", model)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "model := \"herald\"\nmodels := []chat.ModelOption{{ID: \"herald\", Name: \"Herald 2\", Capabilities: []string{\"Vision\"}, ContextTokens: 200000}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif chat.ModelSelector(c, &model, models, chat.ModelSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"model\", model)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "231", Name: "ModeSelector", ZhName: "模式选择器", Category: chatinputCategory, Demo: chatinputDemoModeSelector, Keys: "Tab 进入模式组;Left/Right 切换模式。", - Code: "mode := mujica.ModeChat\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.ModeSelector(c, &mode, mujica.ModeSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"mode\", mode)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "mode := chat.ModeChat\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif chat.ModeSelector(c, &mode, chat.ModeSelectorOptions{}).Changed() {\n\t\tfmt.Println(\"mode\", mode)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "232", Name: "ToolToggleMenu", ZhName: "工具开关菜单", Category: chatinputCategory, Demo: chatinputDemoToolToggleMenu, Keys: "Tab 到菜单按钮;Enter/Space 打开;Tab 到开关与授权;Escape 关闭。", - Code: "web := true\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\ttools := []mujica.ToolItem{{ID: \"web\", Label: \"Web search\", On: &web}}\n\tif id, ok := mujica.ToolToggleMenu(c, tools).Changed(); ok {\n\t\tfmt.Println(\"toggled\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "web := true\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\ttools := []chat.ToolItem{{ID: \"web\", Label: \"Web search\", On: &web}}\n\tif id, ok := chat.ToolToggleMenu(c, tools).Changed(); ok {\n\t\tfmt.Println(\"toggled\", id)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "233", Name: "ParameterPanel", ZhName: "参数面板", Category: chatinputCategory, Demo: chatinputDemoParameterPanel, Keys: "Tab 到滑块与重置;方向键按 Step 调整,Home/End 到范围端点;Enter/Space 重置。", - Code: "temperature := 1.0\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tparams := []mujica.ModelParam{{Label: \"Temperature\", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}}\n\tmujica.ParameterPanel(c, params)\n}\nui.Render(view, 400, 300, 1)"}, + Code: "temperature := 1.0\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tparams := []chat.ModelParam{{Label: \"Temperature\", Value: &temperature, Min: 0, Max: 2, Step: 0.1, Default: 1}}\n\tchat.ParameterPanel(c, params)\n}\nui.Render(view, 400, 300, 1)"}, {ID: "234", Name: "TokenCounter", ZhName: "Token计数", Category: chatinputCategory, Demo: chatinputDemoTokenCounter, Keys: "展示组件,沿用外层 Tab 顺序。", - Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.TokenCounter(c, 1234, 8000)\n}\nui.Render(view, 400, 300, 1)"}, + Code: "view := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tchat.TokenCounter(c, 1234, 8000)\n}\nui.Render(view, 400, 300, 1)"}, {ID: "235", Name: "ContextWindowMeter", ZhName: "上下文容量", Category: chatinputCategory, Demo: chatinputDemoContextWindowMeter, Keys: "展示组件,沿用外层 Tab 顺序。", - Code: "segments := []mujica.ContextSegment{{Label: \"System\", Tokens: 1200}, {Label: \"History\", Tokens: 58000}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.ContextWindowMeter(c, segments, 128000, mujica.ContextWindowMeterOptions{})\n}\nui.Render(view, 400, 300, 1)"}, + Code: "segments := []chat.ContextSegment{{Label: \"System\", Tokens: 1200}, {Label: \"History\", Tokens: 58000}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tchat.ContextWindowMeter(c, segments, 128000, chat.ContextWindowMeterOptions{})\n}\nui.Render(view, 400, 300, 1)"}, {ID: "236", Name: "SendButton", ZhName: "发送按钮", Category: chatinputCategory, Demo: chatinputDemoSendButton, Keys: "Tab 到按钮;Enter、Space 激活;快捷键提示由调用方绑定。", - Code: "streaming := false\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tr := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: streaming, Shortcut: \"Enter\"})\n\tif r.Sent() || r.Stopped() {\n\t\tstreaming = r.Sent()\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "streaming := false\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tr := chat.SendButton(c, chat.SendButtonOptions{Streaming: streaming, Shortcut: \"Enter\"})\n\tif r.Sent() || r.Stopped() {\n\t\tstreaming = r.Sent()\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "237", Name: "CitationBadge", ZhName: "引用编号", Category: chatinputCategory, Demo: chatinputDemoCitationBadge, Keys: "Tab 到引用;Enter、Space 定位;悬停或键盘焦点 500 毫秒后预览;Escape 关闭预览。", - Code: "var cites mujica.CitationState\nsrc := mujica.Source{Title: \"The Palace of Versailles\", URL: \"https://example.com/versailles\"}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.CitationBadge(c, 1, src, &cites).Changed() {\n\t\tfmt.Println(\"show source\", cites.Active)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var cites chat.CitationState\nsrc := chat.Source{Title: \"The Palace of Versailles\", URL: \"https://example.com/versailles\"}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif chat.CitationBadge(c, 1, src, &cites).Changed() {\n\t\tfmt.Println(\"show source\", cites.Active)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "238", Name: "SourceCard", ZhName: "来源卡片", Category: chatinputCategory, Demo: chatinputDemoSourceCard, Keys: "Tab 到卡片;Enter、Space 请求打开。", - Code: "var cites mujica.CitationState\nsrc := mujica.Source{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\", Snippet: \"Seventeen arches.\"}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif mujica.SourceCard(c, 1, src, &cites).Clicked() {\n\t\tfmt.Println(\"open\", src.URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var cites chat.CitationState\nsrc := chat.Source{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\", Snippet: \"Seventeen arches.\"}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif chat.SourceCard(c, 1, src, &cites).Clicked() {\n\t\tfmt.Println(\"open\", src.URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "239", Name: "SourcesPanel", ZhName: "来源面板", Category: chatinputCategory, Demo: chatinputDemoSourcesPanel, Keys: "Tab 到可见来源;Enter、Space 请求打开;滚轮滚动;Active 变化定位来源。", - Code: "var cites mujica.CitationState\nsources := []mujica.Source{{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\"}}\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tif n, ok := mujica.SourcesPanel(c, sources, &cites).Opened(); ok {\n\t\tfmt.Println(\"open\", sources[n-1].URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, + Code: "var cites chat.CitationState\nsources := []chat.Source{{Title: \"Hall of Mirrors\", URL: \"https://example.org/hall\"}}\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tif n, ok := chat.SourcesPanel(c, sources, &cites).Opened(); ok {\n\t\tfmt.Println(\"open\", sources[n-1].URL)\n\t}\n}\nui.Render(view, 400, 300, 1)"}, {ID: "240", Name: "SearchProgress", ZhName: "搜索进度", Category: chatinputCategory, Demo: chatinputDemoSearchProgress, Keys: "Tab 到折叠标题;Enter、Space 展开或收起。", - Code: "view := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tstages := []mujica.SearchStage{{Label: \"Searching the web\", Done: true}, {Label: \"Reading sources\"}}\n\tmujica.SearchProgress(c, mujica.SearchProgressOptions{Query: \"Versailles\", Read: 3, Total: 8, Stages: stages})\n}\nui.Render(view, 400, 300, 1)"}, + Code: "view := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tstages := []chat.SearchStage{{Label: \"Searching the web\", Done: true}, {Label: \"Reading sources\"}}\n\tchat.SearchProgress(c, chat.SearchProgressOptions{Query: \"Versailles\", Read: 3, Total: 8, Stages: stages})\n}\nui.Render(view, 400, 300, 1)"}, } diff --git a/examples/gallery/pages_chatinput_compose.go b/examples/gallery/pages_chatinput_compose.go index a41b0b8..220f43e 100644 --- a/examples/gallery/pages_chatinput_compose.go +++ b/examples/gallery/pages_chatinput_compose.go @@ -7,7 +7,10 @@ import ( "strings" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func chatinputDemoPromptComposer(c *ui.Context, log func(string)) { @@ -23,20 +26,20 @@ func chatinputDemoPromptComposer(c *ui.Context, log func(string)) { choiceFlag(c, &s.attached, "Attachment", log) }, func() { ui.Column(c).Width(520).Children(func() { - r := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Prompt", Placeholder: "Ask the herald", Disabled: s.disabled, CmdEnterSends: s.cmd, Composing: s.composing, + r := chat.PromptComposer(c, &s.text, chat.PromptComposerOptions{Label: "Prompt", Placeholder: "Ask the herald", Disabled: s.disabled, CmdEnterSends: s.cmd, Composing: s.composing, Attachments: func() { - if s.attached && mujica.AttachmentChip(c, mujica.Attachment{Name: "guests.csv", Size: 4096}).Removed() { + if s.attached && chat.AttachmentChip(c, chat.Attachment{Name: "guests.csv", Size: 4096}).Removed() { s.attached = false log("remove attachment") } }, Tools: func() { - if _, ok := mujica.ToolToggleMenu(c, []mujica.ToolItem{{ID: "web", Label: "Web search", On: &s.web}}).Changed(); ok { + if _, ok := chat.ToolToggleMenu(c, []chat.ToolItem{{ID: "web", Label: "Web search", On: &s.web}}).Changed(); ok { log("toggle web search") } }, Actions: func() { - r := mujica.SendButton(c, mujica.SendButtonOptions{Streaming: s.streaming, Disabled: strings.TrimSpace(s.text) == "" || s.composing, Shortcut: "Enter"}) + r := chat.SendButton(c, chat.SendButtonOptions{Streaming: s.streaming, Disabled: strings.TrimSpace(s.text) == "" || s.composing, Shortcut: "Enter"}) if r.Sent() { s.streaming = true s.text = "" @@ -70,10 +73,10 @@ func chatinputDemoAttachmentChip(c *ui.Context, log func(string)) { choiceFlag(c, &s.upload, "Uploading", log) choiceFlag(c, &s.fail, "Failed", log) choiceFlag(c, &s.disabled, "Disabled", log) - if mujica.Slider(c, &s.progress, mujica.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Upload progress"}).Changed() { + if input.Slider(c, &s.progress, input.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Upload progress"}).Changed() { log(fmt.Sprint("progress ", s.progress)) } - if mujica.Button(c, "Restore attachment", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Restore attachment", input.ButtonOptions{}).Clicked() { s.removed = false log("restore") } @@ -83,11 +86,11 @@ func chatinputDemoAttachmentChip(c *ui.Context, log func(string)) { ui.Text(c, "Attachment removed") return } - a := mujica.Attachment{Name: "guests.csv", Size: 4096, Uploading: s.upload, Progress: s.progress} + a := chat.Attachment{Name: "guests.csv", Size: 4096, Uploading: s.upload, Progress: s.progress} if s.fail { a.Err = errors.New("upload rejected") } - r := mujica.AttachmentChip(c, a) + r := chat.AttachmentChip(c, a) if r.Removed() { s.removed = true log("remove") @@ -102,30 +105,30 @@ func chatinputDemoAttachmentChip(c *ui.Context, log func(string)) { }) } -func chatinputPictures() []mujica.PastedImage { - var out []mujica.PastedImage +func chatinputPictures() []chat.PastedImage { + var out []chat.PastedImage for i, col := range []string{"#7D2034", "#355E80"} { svg := fmt.Sprintf(``, col) - out = append(out, mujica.PastedImage{ID: fmt.Sprint(i), Name: fmt.Sprintf("Picture %d", i+1), Source: ui.MustParseSVG([]byte(svg))}) + out = append(out, chat.PastedImage{ID: fmt.Sprint(i), Name: fmt.Sprintf("Picture %d", i+1), Source: ui.MustParseSVG([]byte(svg))}) } return out } func chatinputDemoPasteImagePreview(c *ui.Context, log func(string)) { type state struct { - images []mujica.PastedImage + images []chat.PastedImage disabled bool } s := ui.Local(c.Root(), "223", func() state { return state{images: chatinputPictures()} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) - if mujica.Button(c, "Restore images", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Restore images", input.ButtonOptions{}).Clicked() { s.images = chatinputPictures() log("restore images") } }, func() { ui.Column(c).Padding(12).Disabled(s.disabled).Children(func() { - if id, ok := mujica.PasteImagePreview(c, s.images).Removed(); ok { + if id, ok := chat.PasteImagePreview(c, s.images).Removed(); ok { for i, im := range s.images { if im.ID == id { s.images = append(s.images[:i], s.images[i+1:]...) @@ -150,8 +153,8 @@ func chatinputDemoDragDropOverlay(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Width(520).Height(180).Disabled(s.disabled).Children(func() { - r := mujica.DragDropOverlay(c, mujica.DragDropOverlayOptions{Accept: func(p string) bool { return !s.imagesOnly || strings.EqualFold(filepath.Ext(p), ".png") }}, func() { - ui.Box(c).Fill().Border(1, mujica.Tokens(c).ControlBorder).Center().Children(func() { ui.Text(c, "Drop files here") }) + r := chat.DragDropOverlay(c, chat.DragDropOverlayOptions{Accept: func(p string) bool { return !s.imagesOnly || strings.EqualFold(filepath.Ext(p), ".png") }}, func() { + ui.Box(c).Fill().Border(1, core.Tokens(c).ControlBorder).Center().Children(func() { ui.Text(c, "Drop files here") }) }) r.Element.Fill() if n := len(r.Accepted()); n > 0 { @@ -168,16 +171,16 @@ func chatinputDemoDragDropOverlay(c *ui.Context, log func(string)) { } func chatinputDemoVoiceInputButton(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "225", func() mujica.VoiceInputOptions { return mujica.VoiceInputOptions{} }) + s := ui.Local(c.Root(), "225", func() chat.VoiceInputOptions { return chat.VoiceInputOptions{} }) choiceDemo(c, func() { choiceFlag(c, &s.Disabled, "Disabled", log) - if mujica.Button(c, "Permission failure", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Permission failure", input.ButtonOptions{}).Clicked() { s.Recording = false s.Denied = true log("permission denied") } }, func() { - if mujica.VoiceInputButton(c, *s).Changed() { + if chat.VoiceInputButton(c, *s).Changed() { s.Recording = !s.Recording s.Denied = false s.Started = c.Now() @@ -195,7 +198,7 @@ func chatinputDemoVoiceWaveform(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "226", func() state { return state{level: 0.6} }) choiceDemo(c, func() { choiceFlag(c, &s.silent, "Silent", log) - if mujica.Slider(c, &s.level, mujica.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Input level"}).Changed() { + if input.Slider(c, &s.level, input.SliderOptions{Min: 0, Max: 1, Step: 0.1, Label: "Input level"}).Changed() { log(fmt.Sprint("level ", s.level)) } }, func() { @@ -205,6 +208,6 @@ func chatinputDemoVoiceWaveform(c *ui.Context, log func(string)) { levels[i] = float32(s.level) * float32(i%7+1) / 7 } } - mujica.VoiceWaveform(c, levels, mujica.VoiceWaveformOptions{}) + chat.VoiceWaveform(c, levels, chat.VoiceWaveformOptions{}) }) } diff --git a/examples/gallery/pages_chatinput_context.go b/examples/gallery/pages_chatinput_context.go index 1724647..a9b9ab0 100644 --- a/examples/gallery/pages_chatinput_context.go +++ b/examples/gallery/pages_chatinput_context.go @@ -2,9 +2,12 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "strings" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func chatinputDemoSlashCommandMenu(c *ui.Context, log func(string)) { @@ -18,13 +21,13 @@ func chatinputDemoSlashCommandMenu(c *ui.Context, log func(string)) { choiceFlag(c, &s.empty, "Empty commands", log) }, func() { ui.Column(c).Width(520).Children(func() { - p := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Command prompt", Disabled: s.disabled}) - commands := []mujica.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}, {Name: "search", Description: "Search the web"}, {Name: "translate", Description: "Translate the message"}} + p := chat.PromptComposer(c, &s.text, chat.PromptComposerOptions{Label: "Command prompt", Disabled: s.disabled}) + commands := []chat.SlashCommand{{Name: "summarize", Description: "Summarize the thread"}, {Name: "search", Description: "Search the web"}, {Name: "translate", Description: "Translate the message"}} if s.empty { commands = nil } before := s.text - if cmd, ok := mujica.SlashCommandMenu(c, p, &s.text, commands).Chosen(); ok { + if cmd, ok := chat.SlashCommandMenu(c, p, &s.text, commands).Chosen(); ok { log("command " + cmd.Name) } else if before != s.text { log("complete command") @@ -39,14 +42,14 @@ func chatinputDemoSlashCommandMenu(c *ui.Context, log func(string)) { }) } -func chatinputContextItems() []mujica.ContextItem { - return []mujica.ContextItem{{ID: "file", Label: "guests.csv", Kind: mujica.ContextFile}, {ID: "doc", Label: "Court protocol", Kind: mujica.ContextDoc}, {ID: "web", Label: "Versailles", Detail: "example.com", Kind: mujica.ContextWeb}} +func chatinputContextItems() []chat.ContextItem { + return []chat.ContextItem{{ID: "file", Label: "guests.csv", Kind: chat.ContextFile}, {ID: "doc", Label: "Court protocol", Kind: chat.ContextDoc}, {ID: "web", Label: "Versailles", Detail: "example.com", Kind: chat.ContextWeb}} } func chatinputDemoContextMentionMenu(c *ui.Context, log func(string)) { type state struct { text, query string - items []mujica.ContextItem + items []chat.ContextItem loading, disabled bool } s := ui.Local(c.Root(), "228", func() state { return state{text: "@", items: chatinputContextItems()} }) @@ -55,12 +58,12 @@ func chatinputDemoContextMentionMenu(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Width(520).Children(func() { - p := mujica.PromptComposer(c, &s.text, mujica.PromptComposerOptions{Label: "Context prompt", Disabled: s.disabled}) + p := chat.PromptComposer(c, &s.text, chat.PromptComposerOptions{Label: "Context prompt", Disabled: s.disabled}) items := s.items if s.loading { items = nil } - r := mujica.ContextMentionMenu(c, p, &s.text, items, mujica.ContextMentionOptions{Loading: s.loading}) + r := chat.ContextMentionMenu(c, p, &s.text, items, chat.ContextMentionOptions{Loading: s.loading}) if r.QueryChanged() { s.query = r.Query() s.items = nil @@ -87,23 +90,23 @@ func chatinputDemoContextMentionMenu(c *ui.Context, log func(string)) { func chatinputDemoContextChips(c *ui.Context, log func(string)) { type state struct { - items []mujica.ContextItem + items []chat.ContextItem max float64 disabled bool } s := ui.Local(c.Root(), "229", func() state { return state{items: chatinputContextItems(), max: 2} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) - if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible chips"}).Changed() { + if input.NumberInput(c, &s.max, input.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible chips"}).Changed() { log(fmt.Sprint("max ", s.max)) } - if mujica.Button(c, "Restore context", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Restore context", input.ButtonOptions{}).Clicked() { s.items = chatinputContextItems() log("restore context") } }, func() { ui.Column(c).Width(520).Disabled(s.disabled).Children(func() { - if id, ok := mujica.ContextChips(c, s.items, mujica.ContextChipsOptions{Max: int(s.max)}).Removed(); ok { + if id, ok := chat.ContextChips(c, s.items, chat.ContextChipsOptions{Max: int(s.max)}).Removed(); ok { for i, it := range s.items { if it.ID == id { s.items = append(s.items[:i], s.items[i+1:]...) @@ -126,11 +129,11 @@ func chatinputDemoModelSelector(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) choiceFlag(c, &s.unavailable, "Restricted model", log) }, func() { - models := []mujica.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision", "Tools"}, ContextTokens: 200000}, {ID: "page", Name: "Page Mini", Capabilities: []string{"Fast"}, ContextTokens: 32000}, {ID: "oracle", Name: "Oracle", ContextTokens: 1000000}} + models := []chat.ModelOption{{ID: "herald", Name: "Herald 2", Capabilities: []string{"Vision", "Tools"}, ContextTokens: 200000}, {ID: "page", Name: "Page Mini", Capabilities: []string{"Fast"}, ContextTokens: 32000}, {ID: "oracle", Name: "Oracle", ContextTokens: 1000000}} if s.unavailable { models[2].Unavailable = "Needs the Court plan" } - if mujica.ModelSelector(c, &s.model, models, mujica.ModelSelectorOptions{Disabled: s.disabled}).Changed() { + if chat.ModelSelector(c, &s.model, models, chat.ModelSelectorOptions{Disabled: s.disabled}).Changed() { log("model " + s.model) } }) @@ -138,12 +141,12 @@ func chatinputDemoModelSelector(c *ui.Context, log func(string)) { func chatinputDemoModeSelector(c *ui.Context, log func(string)) { type state struct { - mode mujica.ChatMode + mode chat.ChatMode icons, disabled bool } s := ui.Local(c.Root(), "231", func() state { return state{} }) choiceDemo(c, func() { choiceFlag(c, &s.icons, "Icons only", log); choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - if mujica.ModeSelector(c, &s.mode, mujica.ModeSelectorOptions{IconsOnly: s.icons, Disabled: s.disabled}).Changed() { + if chat.ModeSelector(c, &s.mode, chat.ModeSelectorOptions{IconsOnly: s.icons, Disabled: s.disabled}).Changed() { log(fmt.Sprint("mode ", s.mode)) } }) @@ -154,7 +157,7 @@ func chatinputDemoToolToggleMenu(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "232", func() state { return state{web: true} }) choiceDemo(c, func() { choiceFlag(c, &s.authorized, "Authorized", log); choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Disabled(s.disabled).Children(func() { - r := mujica.ToolToggleMenu(c, []mujica.ToolItem{{ID: "web", Label: "Web search", On: &s.web}, {ID: "code", Label: "Code execution", On: &s.code}, {ID: "mail", Label: "Mail", On: &s.mail, NeedsAuth: true, Authorized: s.authorized}}) + r := chat.ToolToggleMenu(c, []chat.ToolItem{{ID: "web", Label: "Web search", On: &s.web}, {ID: "code", Label: "Code execution", On: &s.code}, {ID: "mail", Label: "Mail", On: &s.mail, NeedsAuth: true, Authorized: s.authorized}}) if id, ok := r.Changed(); ok { log("toggle " + id) } @@ -174,7 +177,7 @@ func chatinputDemoParameterPanel(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "233", func() state { return state{temp: 0.7, top: 1, length: 2048} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Width(420).Disabled(s.disabled).Children(func() { - if mujica.ParameterPanel(c, []mujica.ModelParam{{Label: "Temperature", Value: &s.temp, Min: 0, Max: 2, Step: 0.1, Default: 1}, {Label: "Top-p", Value: &s.top, Min: 0, Max: 1, Step: 0.05, Default: 1}, {Label: "Max tokens", Value: &s.length, Min: 256, Max: 8192, Step: 256, Default: 2048}}).Changed() { + if chat.ParameterPanel(c, []chat.ModelParam{{Label: "Temperature", Value: &s.temp, Min: 0, Max: 2, Step: 0.1, Default: 1}, {Label: "Top-p", Value: &s.top, Min: 0, Max: 1, Step: 0.05, Default: 1}, {Label: "Max tokens", Value: &s.length, Min: 256, Max: 8192, Step: 256, Default: 2048}}).Changed() { log(fmt.Sprintf("parameters %.1f / %.2f / %.0f", s.temp, s.top, s.length)) } }) diff --git a/examples/gallery/pages_chatinput_sources.go b/examples/gallery/pages_chatinput_sources.go index 1595e78..5a5de60 100644 --- a/examples/gallery/pages_chatinput_sources.go +++ b/examples/gallery/pages_chatinput_sources.go @@ -2,44 +2,47 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func chatinputDemoTokenCounter(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "234", func() float64 { return 1234 }) choiceDemo(c, func() { - if mujica.NumberInput(c, s, mujica.NumberInputOptions{Min: 0, Max: 12000, Step: 100, Label: "Estimated tokens"}).Changed() { + if input.NumberInput(c, s, input.NumberInputOptions{Min: 0, Max: 12000, Step: 100, Label: "Estimated tokens"}).Changed() { log(fmt.Sprint("estimate ", *s)) } }, func() { - mujica.TokenCounter(c, int(*s), 8000) - mujica.TokenCounter(c, 7500, 8000) - mujica.TokenCounter(c, 9100, 8000) + chat.TokenCounter(c, int(*s), 8000) + chat.TokenCounter(c, 7500, 8000) + chat.TokenCounter(c, 9100, 8000) }) } func chatinputDemoContextWindowMeter(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "235", func() float64 { return 58000 }) choiceDemo(c, func() { - if mujica.NumberInput(c, s, mujica.NumberInputOptions{Min: 0, Max: 150000, Step: 1000, Label: "History tokens"}).Changed() { + if input.NumberInput(c, s, input.NumberInputOptions{Min: 0, Max: 150000, Step: 1000, Label: "History tokens"}).Changed() { log(fmt.Sprint("history ", *s)) } }, func() { ui.Column(c).Width(520).Children(func() { - mujica.ContextWindowMeter(c, []mujica.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: int(*s)}, {Label: "Attachments", Tokens: 24000}}, 128000, mujica.ContextWindowMeterOptions{}) + chat.ContextWindowMeter(c, []chat.ContextSegment{{Label: "System", Tokens: 1200}, {Label: "History", Tokens: int(*s)}, {Label: "Attachments", Tokens: 24000}}, 128000, chat.ContextWindowMeterOptions{}) }) }) } func chatinputDemoSendButton(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "236", func() mujica.SendButtonOptions { return mujica.SendButtonOptions{Shortcut: "Enter"} }) + s := ui.Local(c.Root(), "236", func() chat.SendButtonOptions { return chat.SendButtonOptions{Shortcut: "Enter"} }) choiceDemo(c, func() { choiceFlag(c, &s.Streaming, "Streaming", log) choiceFlag(c, &s.Disabled, "Disabled send", log) }, func() { - r := mujica.SendButton(c, *s) + r := chat.SendButton(c, *s) if r.Sent() { s.Streaming = true log("send") @@ -51,13 +54,13 @@ func chatinputDemoSendButton(c *ui.Context, log func(string)) { }) } -func chatinputSources() []mujica.Source { - return []mujica.Source{{Title: "The Palace of Versailles", URL: "https://example.com/versailles", Site: "example.com", Snippet: "Seat of the court from 1682.", Published: time.Date(2025, 5, 4, 0, 0, 0, 0, time.UTC)}, {Title: "Hall of Mirrors", URL: "https://example.org/hall", Site: "example.org", Snippet: "Seventeen arches face the garden."}, {Title: "Court etiquette", URL: "https://example.net/etiquette", Snippet: "The ranks of the court."}} +func chatinputSources() []chat.Source { + return []chat.Source{{Title: "The Palace of Versailles", URL: "https://example.com/versailles", Site: "example.com", Snippet: "Seat of the court from 1682.", Published: time.Date(2025, 5, 4, 0, 0, 0, 0, time.UTC)}, {Title: "Hall of Mirrors", URL: "https://example.org/hall", Site: "example.org", Snippet: "Seventeen arches face the garden."}, {Title: "Court etiquette", URL: "https://example.net/etiquette", Snippet: "The ranks of the court."}} } func chatinputDemoCitationBadge(c *ui.Context, log func(string)) { type state struct { - cites mujica.CitationState + cites chat.CitationState disabled bool } s := ui.Local(c.Root(), "237", func() state { return state{} }) @@ -65,13 +68,13 @@ func chatinputDemoCitationBadge(c *ui.Context, log func(string)) { ui.Row(c).Disabled(s.disabled).Gap(8).AlignItems(ui.Center).Children(func() { ui.Text(c, "The court moved in 1682") for i, src := range chatinputSources() { - if mujica.CitationBadge(c, i+1, src, &s.cites).Changed() { + if chat.CitationBadge(c, i+1, src, &s.cites).Changed() { log(fmt.Sprint("locate source ", s.cites.Active)) } } }) ui.Column(c).Width(360).Height(260).Disabled(s.disabled).Children(func() { - if n, ok := mujica.SourcesPanel(c, chatinputSources(), &s.cites).Opened(); ok { + if n, ok := chat.SourcesPanel(c, chatinputSources(), &s.cites).Opened(); ok { log(fmt.Sprint("open source ", n)) } }) @@ -80,7 +83,7 @@ func chatinputDemoCitationBadge(c *ui.Context, log func(string)) { func chatinputDemoSourceCard(c *ui.Context, log func(string)) { type state struct { - cites mujica.CitationState + cites chat.CitationState selected, disabled bool } s := ui.Local(c.Root(), "238", func() state { return state{} }) @@ -90,7 +93,7 @@ func chatinputDemoSourceCard(c *ui.Context, log func(string)) { s.cites.Active = 1 } ui.Column(c).Width(400).Disabled(s.disabled).Children(func() { - if mujica.SourceCard(c, 1, chatinputSources()[0], &s.cites).Clicked() { + if chat.SourceCard(c, 1, chatinputSources()[0], &s.cites).Clicked() { log("open source 1") } }) @@ -99,26 +102,26 @@ func chatinputDemoSourceCard(c *ui.Context, log func(string)) { func chatinputDemoSourcesPanel(c *ui.Context, log func(string)) { type state struct { - cites mujica.CitationState - sources []mujica.Source + cites chat.CitationState + sources []chat.Source disabled, empty, many bool } s := ui.Local(c.Root(), "239", func() state { return state{sources: chatinputSources()} }) choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) choiceFlag(c, &s.empty, "Empty", log) - if mujica.Switch(c, &s.many, "100,000 sources", mujica.SwitchOptions{}).Changed() { + if input.Switch(c, &s.many, "100,000 sources", input.SwitchOptions{}).Changed() { s.sources = chatinputSources() s.cites.Active = 0 if s.many { - s.sources = make([]mujica.Source, 100000) + s.sources = make([]chat.Source, 100000) for i := range s.sources { - s.sources[i] = mujica.Source{Title: fmt.Sprintf("Archive %d", i+1), URL: "https://example.com/archive"} + s.sources[i] = chat.Source{Title: fmt.Sprintf("Archive %d", i+1), URL: "https://example.com/archive"} } } log(fmt.Sprint("sources ", len(s.sources))) } - if mujica.Button(c, "Locate last source", mujica.ButtonOptions{Disabled: s.empty}).Clicked() { + if input.Button(c, "Locate last source", input.ButtonOptions{Disabled: s.empty}).Clicked() { s.cites.Active = len(s.sources) log("locate last source") } @@ -129,7 +132,7 @@ func chatinputDemoSourcesPanel(c *ui.Context, log func(string)) { s.cites.Active = 0 } ui.Column(c).Width(400).Height(320).Disabled(s.disabled).Children(func() { - if n, ok := mujica.SourcesPanel(c, sources, &s.cites).Opened(); ok { + if n, ok := chat.SourcesPanel(c, sources, &s.cites).Opened(); ok { log(fmt.Sprint("open source ", n)) } }) @@ -145,12 +148,12 @@ func chatinputDemoSearchProgress(c *ui.Context, log func(string)) { choiceDemo(c, func() { choiceFlag(c, &s.done, "Done", log) choiceFlag(c, &s.disabled, "Disabled", log) - if mujica.NumberInput(c, &s.read, mujica.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Read sources"}).Changed() { + if input.NumberInput(c, &s.read, input.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Read sources"}).Changed() { log(fmt.Sprint("read ", s.read)) } }, func() { ui.Column(c).Width(440).Disabled(s.disabled).Children(func() { - if mujica.SearchProgress(c, mujica.SearchProgressOptions{Query: "Versailles 1682", Read: int(s.read), Total: 8, Done: s.done, Stages: []mujica.SearchStage{{Label: "Planning queries", Done: true}, {Label: "Searching the web", Done: true}, {Label: "Reading sources", Done: s.done}}}).Changed() { + if chat.SearchProgress(c, chat.SearchProgressOptions{Query: "Versailles 1682", Read: int(s.read), Total: 8, Done: s.done, Stages: []chat.SearchStage{{Label: "Planning queries", Done: true}, {Label: "Searching the web", Done: true}, {Label: "Reading sources", Done: s.done}}}).Changed() { log("toggle search details") } }) diff --git a/examples/gallery/pages_chatinput_test.go b/examples/gallery/pages_chatinput_test.go index ae99bb7..74fe750 100644 --- a/examples/gallery/pages_chatinput_test.go +++ b/examples/gallery/pages_chatinput_test.go @@ -8,10 +8,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func chatinputDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) { +func chatinputDemoTester(t *testing.T, id string, mode core.Mode) (*ui.Tester, *[]string) { t.Helper() var p *page for i := range chatinputPages { @@ -25,7 +25,7 @@ func chatinputDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, } events := new([]string) tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) ui.Scroll(c).Fill().Padding(24).Gap(16).Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) }) }, 900, 800) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -40,7 +40,7 @@ func TestChatinputDemosRender(t *testing.T) { if p.Category != "Chat Input" || p.Code == "" || p.Keys == "" { t.Errorf("incomplete page %s", p.ID) } - for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { tt, _ := chatinputDemoTester(t, p.ID, mode) if tt.HasText("panic") { t.Errorf("%s: panic shown", p.ID) @@ -69,7 +69,7 @@ func TestChatinputDemosEvents(t *testing.T) { {"237", "Source 1: The Palace of Versailles"}, {"238", "Open The Palace of Versailles"}, {"239", "Locate last source"}, {"240", "Searching for “Versailles 1682”"}, } { - tt, events := chatinputDemoTester(t, tc.id, mujica.Light) + tt, events := chatinputDemoTester(t, tc.id, core.Light) if err := tt.Click(tc.click); err != nil { t.Fatalf("%s: %v", tc.id, err) } diff --git a/examples/gallery/pages_chatmsg.go b/examples/gallery/pages_chatmsg.go index 10d6b85..a73debe 100644 --- a/examples/gallery/pages_chatmsg.go +++ b/examples/gallery/pages_chatmsg.go @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const chatmsgCategory = "Chat Messages" @@ -26,7 +29,7 @@ var chatmsgReplies = []string{ type chatmsgDemo struct { ids []string texts []string - list mujica.MessageListState + list chat.MessageListState next int sidebar bool dated bool @@ -54,27 +57,27 @@ func (d *chatmsgDemo) day(i int) time.Time { // row shows message i as a bubble, users and the assistant taking turns. func (d *chatmsgDemo) row(c *ui.Context, i int) { - role := mujica.MessageAssistant + role := chat.MessageAssistant if i%2 == 0 { - role = mujica.MessageUser + role = chat.MessageUser } - mujica.MessageBubble(c, role, mujica.MessageBubbleOptions{}, func() { mujica.MarkdownView(c, d.texts[i]) }) + chat.MessageBubble(c, role, chat.MessageBubbleOptions{}, func() { chat.MarkdownView(c, d.texts[i]) }) } // chatmsgControls adds messages to a demo chat and logs it. func chatmsgControls(c *ui.Context, log func(string), d *chatmsgDemo) { ui.Row(c).Gap(8).Children(func() { - if mujica.Button(c, "Add message", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Add message", input.ButtonOptions{Variant: input.Secondary}).Clicked() { d.add() log(fmt.Sprintf("message %s added", d.ids[len(d.ids)-1])) } - if mujica.Button(c, "Add 10", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Add 10", input.ButtonOptions{Variant: input.Secondary}).Clicked() { for range 10 { d.add() } log(fmt.Sprintf("10 messages added, %d in all", len(d.ids))) } - if mujica.Button(c, "Jump to latest", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Jump to latest", input.ButtonOptions{Variant: input.Ghost}).Clicked() { d.list.ScrollToEnd() log("scrolled to end") } @@ -84,8 +87,8 @@ func chatmsgControls(c *ui.Context, log func(string), d *chatmsgDemo) { var chatmsgListPages = []page{ {ID: "201", Name: "ChatContainer", ZhName: "对话容器", Category: chatmsgCategory, Keys: "Tab 在侧栏、消息与输入区之间移动;消息区滚轮与 PageUp/PageDown 滚动;上翻后出现回到最新按钮。", - Code: `mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{ - List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }}, + Code: `chat.ChatContainer(c, &thread, len(msgs), chat.ChatContainerOptions{ + List: chat.MessageListOptions{ID: func(i int) string { return msgs[i] }}, Input: func() { ui.Text(c, "Composer goes here") }, }, func(i int) { ui.Text(c, msgs[i]) })`, Demo: func(c *ui.Context, log func(string)) { @@ -96,11 +99,11 @@ var chatmsgListPages = []page{ chatmsgControls(c, log, d) }) inputSection(c, "Live", func() { - ui.Box(c).Height(420).Border(1, mujica.Tokens(c).Border).Clip().Children(func() { - opts := mujica.ChatContainerOptions{ - List: mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }}, + ui.Box(c).Height(420).Border(1, core.Tokens(c).Border).Clip().Children(func() { + opts := chat.ChatContainerOptions{ + List: chat.MessageListOptions{ID: func(i int) string { return d.ids[i] }}, Input: func() { - r := mujica.TextInput(c, draft, mujica.TextInputOptions{Label: "Message", Placeholder: "Write to the herald"}) + r := input.TextInput(c, draft, input.TextInputOptions{Label: "Message", Placeholder: "Write to the herald"}) if r.Submitted() && strings.TrimSpace(*draft) != "" { d.add() d.texts[len(d.texts)-1] = *draft @@ -118,7 +121,7 @@ var chatmsgListPages = []page{ }) } } - r := mujica.ChatContainer(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) + r := chat.ChatContainer(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) if r.Unread() > 0 { ui.Text(c, fmt.Sprintf("%d unread", r.Unread())).Absolute().Top(8).Right(12) } @@ -127,7 +130,7 @@ var chatmsgListPages = []page{ }}, {ID: "202", Name: "MessageList", ZhName: "消息列表", Category: chatmsgCategory, Keys: "滚轮与方向键滚动;上翻后停止跟随;Tab 到回到最新按钮,Enter 回到底部。", - Code: `r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{ + Code: `r := chat.MessageList(c, &list, len(ids), chat.MessageListOptions{ ID: func(i int) string { return ids[i] }, }, func(i int) { ui.Text(c, "Message "+ids[i]) }) fmt.Println("unread", r.Unread())`, @@ -137,7 +140,7 @@ fmt.Println("unread", r.Unread())`, inputFlags(c, log, map[string]*bool{"Date separators": &d.dated}, "Date separators") chatmsgControls(c, log, d) ui.Row(c).Children(func() { - if mujica.Button(c, "Load 100,000", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Load 100,000", input.ButtonOptions{Variant: input.Secondary}).Clicked() { for len(d.ids) < 100000 { d.add() } @@ -146,77 +149,77 @@ fmt.Println("unread", r.Unread())`, }) }) inputSection(c, "Live", func() { - opts := mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }, Label: func(i int) string { return d.texts[i] }} + opts := chat.MessageListOptions{ID: func(i int) string { return d.ids[i] }, Label: func(i int) string { return d.texts[i] }} if d.dated { opts.Date = d.day } - ui.Column(c).Height(360).Border(1, mujica.Tokens(c).Border).Children(func() { - r := mujica.MessageList(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) + ui.Column(c).Height(360).Border(1, core.Tokens(c).Border).Children(func() { + r := chat.MessageList(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) }) ui.Text(c, fmt.Sprintf("%d messages, %d unread, at end %v", len(d.ids), r.Unread(), d.list.AtEnd())).Padding(8) }) }) }}, {ID: "203", Name: "MessageBubble", ZhName: "消息气泡", Category: chatmsgCategory, Keys: "气泡不获取焦点;内容中的链接与按钮按 Tab 顺序访问。", - Code: `mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() { + Code: `chat.MessageBubble(c, chat.MessageAssistant, chat.MessageBubbleOptions{Name: "Herald"}, func() { ui.Text(c, "The ball begins at nine.") })`, Demo: func(c *ui.Context, log func(string)) { type state struct { - role mujica.MessageRole + role chat.MessageRole avatar, named bool width float64 } - s := ui.Local(c.Root(), "203", func() state { return state{role: mujica.MessageAssistant, avatar: true, named: true, width: 360} }) - roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"}, - {Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}} + s := ui.Local(c.Root(), "203", func() state { return state{role: chat.MessageAssistant, avatar: true, named: true, width: 360} }) + roles := []input.RadioItem[chat.MessageRole]{{Value: chat.MessageUser, Label: "User"}, {Value: chat.MessageAssistant, Label: "Assistant"}, + {Value: chat.MessageSystem, Label: "System"}, {Value: chat.MessageError, Label: "Error"}} inputSection(c, "Parameters", func() { - if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { + if input.RadioGroup(c, &s.role, roles, input.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { log(fmt.Sprintf("role %d", s.role)) } inputFlags(c, log, map[string]*bool{"Avatar": &s.avatar, "Name": &s.named}, "Avatar", "Name") - if mujica.NumberInput(c, &s.width, mujica.NumberInputOptions{Min: 160, Max: 640, Step: 40, Label: "MaxWidth"}).Changed() { + if input.NumberInput(c, &s.width, input.NumberInputOptions{Min: 160, Max: 640, Step: 40, Label: "MaxWidth"}).Changed() { log(fmt.Sprintf("MaxWidth %v", s.width)) } }) inputSection(c, "Live", func() { - opts := mujica.MessageBubbleOptions{MaxWidth: float32(s.width)} + opts := chat.MessageBubbleOptions{MaxWidth: float32(s.width)} if s.named { opts.Name = "Herald" } if s.avatar { - opts.Avatar = func() { mujica.Avatar(c, "Herald", mujica.AvatarOptions{}) } + opts.Avatar = func() { display.Avatar(c, "Herald", display.AvatarOptions{}) } } - mujica.MessageBubble(c, s.role, opts, func() { ui.Text(c, chatmsgReplies[0]+" "+chatmsgReplies[2]) }) + chat.MessageBubble(c, s.role, opts, func() { ui.Text(c, chatmsgReplies[0]+" "+chatmsgReplies[2]) }) }) inputSection(c, "States", func() { for _, r := range roles { - mujica.MessageBubble(c, r.Value, mujica.MessageBubbleOptions{Name: r.Label}, func() { ui.Text(c, r.Label+" message") }) + chat.MessageBubble(c, r.Value, chat.MessageBubbleOptions{Name: r.Label}, func() { ui.Text(c, r.Label+" message") }) } }) }}, {ID: "204", Name: "MessageHeader", ZhName: "消息头", Category: chatmsgCategory, Keys: "不获取焦点。", - Code: `mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent})`, + Code: `chat.MessageHeader(c, chat.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: chat.MessageSent})`, Demo: func(c *ui.Context, log func(string)) { - status := ui.Local(c.Root(), "204", func() mujica.MessageStatus { return mujica.MessageSent }) - items := []mujica.RadioItem[mujica.MessageStatus]{{Value: mujica.MessageNoStatus, Label: "None"}, {Value: mujica.MessageSending, Label: "Sending"}, - {Value: mujica.MessageStreaming, Label: "Streaming"}, {Value: mujica.MessageSent, Label: "Sent"}, {Value: mujica.MessageFailed, Label: "Failed"}} + status := ui.Local(c.Root(), "204", func() chat.MessageStatus { return chat.MessageSent }) + items := []input.RadioItem[chat.MessageStatus]{{Value: chat.MessageNoStatus, Label: "None"}, {Value: chat.MessageSending, Label: "Sending"}, + {Value: chat.MessageStreaming, Label: "Streaming"}, {Value: chat.MessageSent, Label: "Sent"}, {Value: chat.MessageFailed, Label: "Failed"}} inputSection(c, "Parameters", func() { - if mujica.RadioGroup(c, status, items, mujica.RadioGroupOptions{Horizontal: true, Label: "Status"}).Changed() { + if input.RadioGroup(c, status, items, input.RadioGroupOptions{Horizontal: true, Label: "Status"}).Changed() { log(fmt.Sprintf("status %d", *status)) } }) inputSection(c, "Live", func() { - mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: *status}) + chat.MessageHeader(c, chat.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: *status}) }) inputSection(c, "States", func() { for _, it := range items[1:] { - mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: it.Value}) + chat.MessageHeader(c, chat.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: it.Value}) } }) }}, {ID: "205", Name: "StreamingText", ZhName: "流式文本", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时直接显示全部文本。", - Code: `mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true})`, + Code: `chat.StreamingText(c, reply, chat.StreamingTextOptions{Streaming: true, Smooth: true})`, Demo: func(c *ui.Context, log func(string)) { type state struct { text string @@ -228,11 +231,11 @@ fmt.Println("unread", r.Unread())`, const full = "By order of the Queen, the ball begins at nine. Guests arrive by the east door, and carriages wait in the lower court until midnight." inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"Smooth": &s.smooth, "Streaming cursor": &s.streaming, "Stream": &s.auto}, "Smooth", "Streaming cursor", "Stream") - if mujica.NumberInput(c, &s.rate, mujica.NumberInputOptions{Min: 10, Max: 400, Step: 10, Label: "Rate"}).Changed() { + if input.NumberInput(c, &s.rate, input.NumberInputOptions{Min: 10, Max: 400, Step: 10, Label: "Rate"}).Changed() { log(fmt.Sprintf("Rate %v", s.rate)) } ui.Row(c).Children(func() { - if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restart", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.text = "" log("stream restarted") } @@ -255,7 +258,7 @@ fmt.Println("unread", r.Unread())`, c.After(150 * time.Millisecond) } inputSection(c, "Live", func() { - mujica.StreamingText(c, s.text, mujica.StreamingTextOptions{Streaming: s.streaming && len(s.text) < len(full), Smooth: s.smooth, Rate: s.rate}) + chat.StreamingText(c, s.text, chat.StreamingTextOptions{Streaming: s.streaming && len(s.text) < len(full), Smooth: s.smooth, Rate: s.rate}) }) }}, } diff --git a/examples/gallery/pages_chatmsg_actions.go b/examples/gallery/pages_chatmsg_actions.go index 4d84d48..7923903 100644 --- a/examples/gallery/pages_chatmsg_actions.go +++ b/examples/gallery/pages_chatmsg_actions.go @@ -6,12 +6,14 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var chatmsgActionPages = []page{ {ID: "211", Name: "AudioMessage", ZhName: "语音消息", Category: chatmsgCategory, Keys: "Tab 到播放按钮与波形;波形上 Left/Right 跳转,Home、End 到首尾。", - Code: `r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos}) + Code: `r := chat.AudioMessage(c, chat.AudioMessageOptions{Duration: time.Minute, Position: pos}) if d, ok := r.Seek(); ok { pos = d }`, @@ -34,7 +36,7 @@ if d, ok := r.Seek(); ok { } s.at = c.Now() inputSection(c, "Live", func() { - r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Position: s.pos, Playing: s.playing, Waveform: chatmsgWave}) + r := chat.AudioMessage(c, chat.AudioMessageOptions{Duration: length, Position: s.pos, Playing: s.playing, Waveform: chatmsgWave}) if r.Toggled() { s.playing = !s.playing if s.pos >= length { @@ -49,13 +51,13 @@ if d, ok := r.Seek(); ok { }) inputSection(c, "States", func() { ui.Column(c).Disabled(true).Children(func() { - mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Waveform: chatmsgWave}) + chat.AudioMessage(c, chat.AudioMessageOptions{Duration: length, Waveform: chatmsgWave}) }) }) }}, {ID: "212", Name: "LinkPreviewCard", ZhName: "链接预览卡片", Category: chatmsgCategory, Keys: "Tab 聚焦卡片,Enter 或 Space 打开。", - Code: `p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} -if mujica.LinkPreviewCard(c, p).Clicked() { + Code: `p := chat.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"} +if chat.LinkPreviewCard(c, p).Clicked() { fmt.Println("open", p.URL) }`, Demo: func(c *ui.Context, log func(string)) { @@ -63,66 +65,66 @@ if mujica.LinkPreviewCard(c, p).Clicked() { inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"Thumbnail": thumb}, "Thumbnail") }) - p := mujica.LinkPreview{URL: "https://example.com/versailles", Title: "The Palace of Versailles", Site: "example.com", + p := chat.LinkPreview{URL: "https://example.com/versailles", Title: "The Palace of Versailles", Site: "example.com", Description: "A royal château in the Île-de-France region, seat of the French court from 1682 until the Revolution."} if *thumb { p.Thumbnail = chatmsgPicture("#AD9164") } inputSection(c, "Live", func() { - if mujica.LinkPreviewCard(c, p).Clicked() { + if chat.LinkPreviewCard(c, p).Clicked() { log("open " + p.URL) } - if mujica.LinkPreviewCard(c, mujica.LinkPreview{URL: "https://example.com/hall", Title: "Hall of Mirrors"}).Clicked() { + if chat.LinkPreviewCard(c, chat.LinkPreview{URL: "https://example.com/hall", Title: "Hall of Mirrors"}).Clicked() { log("open https://example.com/hall") } }) }}, {ID: "213", Name: "QuoteReply", ZhName: "引用回复", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 跳转。", - Code: `if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { + Code: `if chat.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() { fmt.Println("jump to the quoted message") }`, Demo: func(c *ui.Context, log func(string)) { inputSection(c, "Live", func() { for _, q := range [][2]string{{"Marie", "Let them eat cake."}, {"Herald", chatmsgMarkdown}} { - if mujica.QuoteReply(c, q[0], q[1]).Clicked() { + if chat.QuoteReply(c, q[0], q[1]).Clicked() { log("jump to " + q[0] + ": " + chatmsgSnippet(q[1], 24)) } } }) }}, {ID: "214", Name: "MessageActions", ZhName: "消息操作", Category: chatmsgCategory, Keys: "Tab 进入工具栏,Left/Right 移动,Enter 或 Space 触发。", - Code: `if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok { + Code: `if a, ok := chat.MessageActions(c, chat.MessageAssistant, chat.MessageActionsOptions{Feedback: &rating}).Action(); ok { fmt.Println("action", a) }`, Demo: func(c *ui.Context, log func(string)) { type state struct { - role mujica.MessageRole - rating mujica.MessageFeedback + role chat.MessageRole + rating chat.MessageFeedback rated bool } - s := ui.Local(c.Root(), "214", func() state { return state{role: mujica.MessageAssistant, rated: true} }) - names := map[mujica.MessageAction]string{mujica.ActionCopy: "copy", mujica.ActionRegenerate: "regenerate", mujica.ActionEdit: "edit", - mujica.ActionLike: "like", mujica.ActionDislike: "dislike", mujica.ActionSpeak: "speak", mujica.ActionShare: "share"} + s := ui.Local(c.Root(), "214", func() state { return state{role: chat.MessageAssistant, rated: true} }) + names := map[chat.MessageAction]string{chat.ActionCopy: "copy", chat.ActionRegenerate: "regenerate", chat.ActionEdit: "edit", + chat.ActionLike: "like", chat.ActionDislike: "dislike", chat.ActionSpeak: "speak", chat.ActionShare: "share"} inputSection(c, "Parameters", func() { - roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"}, - {Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}} - if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { + roles := []input.RadioItem[chat.MessageRole]{{Value: chat.MessageUser, Label: "User"}, {Value: chat.MessageAssistant, Label: "Assistant"}, + {Value: chat.MessageSystem, Label: "System"}, {Value: chat.MessageError, Label: "Error"}} + if input.RadioGroup(c, &s.role, roles, input.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() { log(fmt.Sprintf("role %d", s.role)) } inputFlags(c, log, map[string]*bool{"Rating": &s.rated}, "Rating") }) inputSection(c, "Live", func() { - opts := mujica.MessageActionsOptions{} + opts := chat.MessageActionsOptions{} if s.rated { opts.Feedback = &s.rating } - if a, ok := mujica.MessageActions(c, s.role, opts).Action(); ok { + if a, ok := chat.MessageActions(c, s.role, opts).Action(); ok { log(fmt.Sprintf("action %s, rating %d", names[a], s.rating)) } }) }}, {ID: "215", Name: "MessageEditor", ZhName: "消息编辑器", Category: chatmsgCategory, Keys: "Cmd/Ctrl+Enter 保存,Escape 取消。", - Code: `if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() { + Code: `if chat.MessageEditor(c, &draft, chat.MessageEditorOptions{Original: "Hello"}).Saved() { fmt.Println("new branch from", draft) }`, Demo: func(c *ui.Context, log func(string)) { @@ -137,19 +139,19 @@ if mujica.LinkPreviewCard(c, p).Clicked() { }) inputSection(c, "Live", func() { if !s.editing { - mujica.MessageBubble(c, mujica.MessageUser, mujica.MessageBubbleOptions{}, func() { ui.Text(c, s.branches[s.index]) }) + chat.MessageBubble(c, chat.MessageUser, chat.MessageBubbleOptions{}, func() { ui.Text(c, s.branches[s.index]) }) ui.Row(c).Gap(8).Justify(ui.End).Children(func() { - if mujica.BranchNavigator(c, &s.index, len(s.branches)).Changed() { + if chat.BranchNavigator(c, &s.index, len(s.branches)).Changed() { log(fmt.Sprintf("branch %d", s.index+1)) } - if mujica.Button(c, "Edit", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Edit", input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.editing, s.draft = true, s.branches[s.index] log("editing") } }) return } - r := mujica.MessageEditor(c, &s.draft, mujica.MessageEditorOptions{Original: s.branches[s.index]}) + r := chat.MessageEditor(c, &s.draft, chat.MessageEditorOptions{Original: s.branches[s.index]}) if r.Saved() { s.branches = append(s.branches, s.draft) s.index, s.editing = len(s.branches)-1, false @@ -162,7 +164,7 @@ if mujica.LinkPreviewCard(c, p).Clicked() { }) }}, {ID: "216", Name: "BranchNavigator", ZhName: "分支切换", Category: chatmsgCategory, Keys: "Tab 聚焦,Left/Right 切换,Home、End 到首末。", - Code: `if mujica.BranchNavigator(c, &branch, 3).Changed() { + Code: `if chat.BranchNavigator(c, &branch, 3).Changed() { fmt.Println("show branch", branch) }`, Demo: func(c *ui.Context, log func(string)) { @@ -173,7 +175,7 @@ if mujica.LinkPreviewCard(c, p).Clicked() { } s := ui.Local(c.Root(), "216", func() state { return state{index: 1, count: 3} }) inputSection(c, "Parameters", func() { - if mujica.NumberInput(c, &s.count, mujica.NumberInputOptions{Min: 1, Max: 9, StepperOnly: true, Label: "Branches"}).Changed() { + if input.NumberInput(c, &s.count, input.NumberInputOptions{Min: 1, Max: 9, StepperOnly: true, Label: "Branches"}).Changed() { s.index = min(s.index, int(s.count)-1) log(fmt.Sprintf("%v branches", s.count)) } @@ -181,39 +183,39 @@ if mujica.LinkPreviewCard(c, p).Clicked() { }) inputSection(c, "Live", func() { ui.Row(c).Disabled(s.disabled).Children(func() { - if mujica.BranchNavigator(c, &s.index, int(s.count)).Changed() { + if chat.BranchNavigator(c, &s.index, int(s.count)).Changed() { log(fmt.Sprintf("branch %d of %v", s.index+1, s.count)) } }) }) }}, {ID: "217", Name: "ErrorMessage", ZhName: "错误消息", Category: chatmsgCategory, Keys: "Tab 到错误文本(可选择复制)与重试按钮。", - Code: `opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} -if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { + Code: `opts := chat.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)} +if chat.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { fmt.Println("send again") }`, Demo: func(c *ui.Context, log func(string)) { until := ui.Local(c.Root(), "217", func() time.Time { return time.Time{} }) inputSection(c, "Parameters", func() { ui.Row(c).Children(func() { - if mujica.Button(c, "Rate limit for 10 s", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Rate limit for 10 s", input.ButtonOptions{Variant: input.Secondary}).Clicked() { *until = c.Now().Add(10 * time.Second) log("rate limited for 10 s") } }) }) inputSection(c, "Live", func() { - opts := mujica.ErrorMessageOptions{RetryAt: *until} - if mujica.ErrorMessage(c, `429 Too Many Requests: {"type":"rate_limit_error","message":"Number of requests exceeded"}`, opts).Retried() { + opts := chat.ErrorMessageOptions{RetryAt: *until} + if chat.ErrorMessage(c, `429 Too Many Requests: {"type":"rate_limit_error","message":"Number of requests exceeded"}`, opts).Retried() { log("retry") } - if mujica.ErrorMessage(c, "dial tcp 10.0.0.7:443: connection refused", mujica.ErrorMessageOptions{Title: "Could not reach the server"}).Retried() { + if chat.ErrorMessage(c, "dial tcp 10.0.0.7:443: connection refused", chat.ErrorMessageOptions{Title: "Could not reach the server"}).Retried() { log("retry connection") } }) }}, {ID: "218", Name: "ThinkingBlock", ZhName: "思考过程", Category: chatmsgCategory, Keys: "Tab 聚焦标题,Enter 或 Space 展开与折叠。", - Code: `mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { + Code: `chat.ThinkingBlock(c, &open, chat.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() { ui.Text(c, "Weigh the guest list.") })`, Demo: func(c *ui.Context, log func(string)) { @@ -229,7 +231,7 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { label = "Finish thinking" } ui.Row(c).Children(func() { - if mujica.Button(c, label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, label, input.ButtonOptions{Variant: input.Secondary}).Clicked() { if s.thinking { s.elapsed = c.Now().Sub(s.started) } else { @@ -241,8 +243,8 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { }) }) inputSection(c, "Live", func() { - opts := mujica.ThinkingBlockOptions{Thinking: s.thinking, Started: s.started, Elapsed: s.elapsed} - if mujica.ThinkingBlock(c, &s.open, opts, func() { + opts := chat.ThinkingBlockOptions{Thinking: s.thinking, Started: s.started, Elapsed: s.elapsed} + if chat.ThinkingBlock(c, &s.open, opts, func() { ui.Text(c, "Forty guests, thirty-six seats. The envoys outrank the counts; four counts dine in the antechamber.") }).Changed() { log(fmt.Sprintf("open %v", s.open)) @@ -251,7 +253,7 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { }}, {ID: "219", Name: "ScrollToBottomButton", ZhName: "回到底部按钮", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 回到底部。", Code: `ui.Box(c).Fill().Children(func() { - if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { + if chat.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() { list.ScrollToEnd() } })`, @@ -263,13 +265,13 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { s := ui.Local(c.Root(), "219", func() state { return state{away: true, unread: 3} }) inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"Away from bottom": &s.away}, "Away from bottom") - if mujica.NumberInput(c, &s.unread, mujica.NumberInputOptions{Min: 0, Max: 99, Label: "Unread"}).Changed() { + if input.NumberInput(c, &s.unread, input.NumberInputOptions{Min: 0, Max: 99, Label: "Unread"}).Changed() { log(fmt.Sprintf("unread %v", s.unread)) } }) inputSection(c, "Live", func() { - ui.Box(c).Height(120).Background(mujica.Tokens(c).Surface).Border(1, mujica.Tokens(c).Border).Children(func() { - if mujica.ScrollToBottomButton(c, s.away, int(s.unread)).Clicked() { + ui.Box(c).Height(120).Background(core.Tokens(c).Surface).Border(1, core.Tokens(c).Border).Children(func() { + if chat.ScrollToBottomButton(c, s.away, int(s.unread)).Clicked() { s.away, s.unread = false, 0 log("back to the bottom") } @@ -277,7 +279,7 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { }) }}, {ID: "220", Name: "TypingIndicator", ZhName: "正在输入", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时显示静态文字。", - Code: `mujica.TypingIndicator(c, "Herald")`, + Code: `chat.TypingIndicator(c, "Herald")`, Demo: func(c *ui.Context, log func(string)) { typing := ui.Local(c.Root(), "220", func() bool { return true }) inputSection(c, "Parameters", func() { @@ -285,7 +287,7 @@ if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() { }) inputSection(c, "Live", func() { if *typing { - mujica.TypingIndicator(c, "Herald") + chat.TypingIndicator(c, "Herald") } }) }}, diff --git a/examples/gallery/pages_chatmsg_blocks.go b/examples/gallery/pages_chatmsg_blocks.go index 3356e64..d41923c 100644 --- a/examples/gallery/pages_chatmsg_blocks.go +++ b/examples/gallery/pages_chatmsg_blocks.go @@ -8,7 +8,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const chatmsgMarkdown = "## The Decree\n\nBy order of **the Queen**, *all* guests use `the east door`. See [the protocol](https://example.com/protocol).\n\n" + @@ -24,7 +25,7 @@ var chatmsgColors = []string{"#7D2034", "#AD9164", "#355E80", "#326247", "#80571 var chatmsgBlockPages = []page{ {ID: "206", Name: "MarkdownView", ZhName: "Markdown 视图", Category: chatmsgCategory, Keys: "Tab 到链接,Enter 激活;代码块与表格按钮按 Tab 顺序访问。", - Code: `if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { + Code: `if url, ok := chat.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok { fmt.Println("open", url) }`, Demo: func(c *ui.Context, log func(string)) { @@ -36,12 +37,12 @@ var chatmsgBlockPages = []page{ s := ui.Local(c.Root(), "206", func() state { return state{src: chatmsgMarkdown} }) inputSection(c, "Parameters", func() { ui.Row(c).Children(func() { - if mujica.Button(c, "Stream it again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Stream it again", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.src, s.stream = "", true log("streaming started") } }) - mujica.TextArea(c, &s.src, mujica.TextAreaOptions{Label: "Markdown source", MaxHeight: 160}) + input.TextArea(c, &s.src, input.TextAreaOptions{Label: "Markdown source", MaxHeight: 160}) }) if s.stream && len(s.src) < len(chatmsgMarkdown) && c.Now().Sub(s.at) > 40*time.Millisecond { s.src = chatmsgMarkdown[:min(len(chatmsgMarkdown), len(s.src)+6)] @@ -55,13 +56,13 @@ var chatmsgBlockPages = []page{ c.After(40 * time.Millisecond) } inputSection(c, "Live", func() { - if url, ok := mujica.MarkdownView(c, s.src).Link(); ok { + if url, ok := chat.MarkdownView(c, s.src).Link(); ok { log("link " + url) } }) }}, {ID: "207", Name: "CodeBlock", ZhName: "代码块", Category: chatmsgCategory, Keys: "Tab 到换行、复制、折叠按钮;Space 或 Enter 触发。", - Code: `mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true})`, + Code: `chat.CodeBlock(c, "func bow() {}", chat.CodeBlockOptions{Language: "go", LineNumbers: true})`, Demo: func(c *ui.Context, log func(string)) { type state struct { lang string @@ -74,25 +75,25 @@ var chatmsgBlockPages = []page{ "sql": "SELECT name, seat FROM guests WHERE rank > 2 ORDER BY seat LIMIT 10; -- the envoys", } inputSection(c, "Parameters", func() { - langs := []mujica.RadioItem[string]{{Value: "go", Label: "Go"}, {Value: "python", Label: "Python"}, {Value: "sql", Label: "SQL"}} - if mujica.RadioGroup(c, &s.lang, langs, mujica.RadioGroupOptions{Horizontal: true, Label: "Language"}).Changed() { + langs := []input.RadioItem[string]{{Value: "go", Label: "Go"}, {Value: "python", Label: "Python"}, {Value: "sql", Label: "SQL"}} + if input.RadioGroup(c, &s.lang, langs, input.RadioGroupOptions{Horizontal: true, Label: "Language"}).Changed() { log("language " + s.lang) } inputFlags(c, log, map[string]*bool{"LineNumbers": &s.numbers, "Wrap at first": &s.wrap}, "LineNumbers", "Wrap at first") }) inputSection(c, "Live", func() { ui.Column(c).Key(s.lang + fmt.Sprint(s.wrap)).Children(func() { - if mujica.CodeBlock(c, samples[s.lang], mujica.CodeBlockOptions{Language: s.lang, LineNumbers: s.numbers, Wrap: s.wrap}).Copied() { + if chat.CodeBlock(c, samples[s.lang], chat.CodeBlockOptions{Language: s.lang, LineNumbers: s.numbers, Wrap: s.wrap}).Copied() { log("code copied") } }) }) inputSection(c, "States", func() { - mujica.CodeBlock(c, samples["go"], mujica.CodeBlockOptions{Language: "go", Collapsed: true}) + chat.CodeBlock(c, samples["go"], chat.CodeBlockOptions{Language: "go", Collapsed: true}) }) }}, {ID: "208", Name: "TableBlock", ZhName: "表格块", Category: chatmsgCategory, Keys: "Tab 到复制按钮,Enter 或 Space 复制为 TSV。", - Code: `if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() { + Code: `if chat.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, chat.TableBlockOptions{}).Copied() { fmt.Println("copied as TSV") }`, Demo: func(c *ui.Context, log func(string)) { @@ -110,14 +111,14 @@ var chatmsgBlockPages = []page{ } inputSection(c, "Live", func() { ui.Column(c).MaxWidth(520).Children(func() { - if mujica.TableBlock(c, header, rows, mujica.TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}).Copied() { + if chat.TableBlock(c, header, rows, chat.TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}).Copied() { log("table copied as TSV") } }) }) }}, {ID: "209", Name: "ImageGrid", ZhName: "图片网格", Category: chatmsgCategory, Keys: "Tab 聚焦图片,Enter 打开查看器;查看器内 Left/Right 切换,Escape 关闭。", - Code: `if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { + Code: `if i, ok := chat.ImageGrid(c, []chat.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok { fmt.Println("reload", i) }`, Demo: func(c *ui.Context, log func(string)) { @@ -127,30 +128,30 @@ var chatmsgBlockPages = []page{ } s := ui.Local(c.Root(), "209", func() state { return state{n: 5} }) inputSection(c, "Parameters", func() { - if mujica.NumberInput(c, &s.n, mujica.NumberInputOptions{Min: 1, Max: 9, Label: "Images", StepperOnly: true}).Changed() { + if input.NumberInput(c, &s.n, input.NumberInputOptions{Min: 1, Max: 9, Label: "Images", StepperOnly: true}).Changed() { log(fmt.Sprintf("%v images", s.n)) } inputFlags(c, log, map[string]*bool{"Last loading": &s.loading, "Second failed": &s.fail}, "Last loading", "Second failed") }) - imgs := make([]mujica.GridImage, int(s.n)) + imgs := make([]chat.GridImage, int(s.n)) for i := range imgs { - imgs[i] = mujica.GridImage{Source: chatmsgPicture(chatmsgColors[i]), Alt: fmt.Sprintf("Portrait %d", i+1)} + imgs[i] = chat.GridImage{Source: chatmsgPicture(chatmsgColors[i]), Alt: fmt.Sprintf("Portrait %d", i+1)} } if s.loading { - imgs[len(imgs)-1] = mujica.GridImage{Alt: "Loading portrait", Loading: true} + imgs[len(imgs)-1] = chat.GridImage{Alt: "Loading portrait", Loading: true} } if s.fail && len(imgs) > 1 { - imgs[1] = mujica.GridImage{Alt: "Lost portrait", Err: errors.New("404")} + imgs[1] = chat.GridImage{Alt: "Lost portrait", Err: errors.New("404")} } inputSection(c, "Live", func() { - if i, ok := mujica.ImageGrid(c, imgs).Retried(); ok { + if i, ok := chat.ImageGrid(c, imgs).Retried(); ok { s.fail = false log(fmt.Sprintf("retry image %d", i)) } }) }}, {ID: "210", Name: "FileMessage", ZhName: "文件消息", Category: chatmsgCategory, Keys: "Tab 到文件与下载按钮,Enter 或 Space 触发。", - Code: `r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) + Code: `r := chat.FileMessage(c, chat.FileMessageOptions{Name: "treaty.pdf", Size: 2621440}) if r.Downloaded() { fmt.Println("save treaty.pdf") }`, @@ -161,9 +162,9 @@ if r.Downloaded() { }) inputSection(c, "Live", func() { ui.Column(c).Gap(8).Disabled(*disabled).Children(func() { - for _, f := range []mujica.FileMessageOptions{{Name: "treaty.pdf", Size: 2621440}, {Name: "portrait.png", Size: 812000}, + for _, f := range []chat.FileMessageOptions{{Name: "treaty.pdf", Size: 2621440}, {Name: "portrait.png", Size: 812000}, {Name: "ledger.zip", Size: 73400320}, {Name: "decree.go", Size: 1840}, {Name: "notes", Size: 12}} { - r := mujica.FileMessage(c, f) + r := chat.FileMessage(c, f) if r.Opened() { log("open " + f.Name) } diff --git a/examples/gallery/pages_chatmsg_test.go b/examples/gallery/pages_chatmsg_test.go index 7a2bf62..af06cf7 100644 --- a/examples/gallery/pages_chatmsg_test.go +++ b/examples/gallery/pages_chatmsg_test.go @@ -9,11 +9,11 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // chatmsgDemoTester runs the demo of page id and returns its tester and event log. -func chatmsgDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) { +func chatmsgDemoTester(t *testing.T, id string, mode core.Mode) (*ui.Tester, *[]string) { t.Helper() i := slices.IndexFunc(chatmsgPages, func(p page) bool { return p.ID == id }) if i < 0 { @@ -21,7 +21,7 @@ func chatmsgDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, * } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) ui.Scroll(c).Fill().Padding(24).Gap(16).Children(func() { chatmsgPages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) @@ -37,7 +37,7 @@ func TestChatmsgDemosRender(t *testing.T) { } dir := os.Getenv("CHATMSG_DEMO_DIR") for _, p := range chatmsgPages { - for _, m := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, m := range []core.Mode{core.Light, core.Dark} { tt, _ := chatmsgDemoTester(t, p.ID, m) if len(tt.Texts()) == 0 { t.Errorf("%s: demo shows nothing", p.ID) @@ -61,7 +61,7 @@ func TestChatmsgDemosLogEvents(t *testing.T) { for id, click := range map[string]string{ "202": "Add message", "207": "Copy code", "214": "Regenerate", "216": "Next branch", "217": "Retry", } { - tt, events := chatmsgDemoTester(t, id, mujica.Light) + tt, events := chatmsgDemoTester(t, id, core.Light) if err := tt.Click(click); err != nil { t.Fatalf("%s: %v", id, err) } diff --git a/examples/gallery/pages_chatsession.go b/examples/gallery/pages_chatsession.go index 7cd7b79..fabcc1e 100644 --- a/examples/gallery/pages_chatsession.go +++ b/examples/gallery/pages_chatsession.go @@ -4,23 +4,23 @@ const chatsessionCategory = "Chat Sessions" var chatsessionPages = []page{ {ID: "241", Name: "ConversationList", ZhName: "会话列表", Category: chatsessionCategory, Demo: chatDemoConversationList, - Keys: "Up/Down choose a conversation, skipping group headers; typing jumps; Enter opens.", Code: `var state mujica.ConversationListState -convs := []mujica.ChatConversation{ + Keys: "Up/Down choose a conversation, skipping group headers; typing jumps; Enter opens.", Code: `var state chat.ConversationListState +convs := []chat.ChatConversation{ {ID: "1", Title: "Court etiquette", Updated: time.Now()}, {ID: "2", Title: "Royal letters", Updated: time.Now().AddDate(0, 0, -30), Pinned: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConversationList(c, &state, convs, mujica.ConversationListOptions{Label: "History"}, nil).Changed() { + core.Use(c, core.Settings{}) + if chat.ConversationList(c, &state, convs, chat.ConversationListOptions{Label: "History"}, nil).Changed() { log.Println("open", state.Selected) } } ui.Render(view, 400, 300, 1)`}, {ID: "242", Name: "ConversationItem", ZhName: "会话条目", Category: chatsessionCategory, Demo: chatDemoConversationItem, - Keys: "Enter opens; F2 renames in place (Enter keeps, Escape cancels); Delete asks to delete; the ellipsis opens the menu.", Code: `conv := mujica.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} + Keys: "Enter opens; F2 renames in place (Enter keeps, Escape cancels); Delete asks to delete; the ellipsis opens the menu.", Code: `conv := chat.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{}) + core.Use(c, core.Settings{}) + v := chat.ConversationItem(c, &conv, chat.ConversationItemOptions{}) if v.Deleted { log.Println("delete", conv.ID) } @@ -28,43 +28,43 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)`}, {ID: "243", Name: "ConversationSearch", ZhName: "会话搜索", Category: chatsessionCategory, Demo: chatDemoConversationSearch, Keys: "Type to search; Down moves into the results; Up/Down move; Enter opens; Escape clears.", Code: `query := "seat" -hits := []mujica.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} +hits := []chat.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ConversationSearch(c, &query, hits, mujica.ConversationSearchOptions{}) + core.Use(c, core.Settings{}) + v := chat.ConversationSearch(c, &query, hits, chat.ConversationSearchOptions{}) if v.QueryChanged { log.Println("search", query) } } ui.Render(view, 400, 300, 1)`}, {ID: "244", Name: "ProjectList", ZhName: "项目列表", Category: chatsessionCategory, Demo: chatDemoProjectList, - Keys: "Up/Down move through folders, projects and conversations; Right/Left show or hide a project's conversations; the ellipsis menu moves a conversation.", Code: `var state mujica.ProjectListState -projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} -convs := func(id string) []mujica.ChatConversation { - return []mujica.ChatConversation{{ID: "1", Title: "Menu"}} + Keys: "Up/Down move through folders, projects and conversations; Right/Left show or hide a project's conversations; the ellipsis menu moves a conversation.", Code: `var state chat.ProjectListState +projects := []chat.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}} +convs := func(id string) []chat.ChatConversation { + return []chat.ChatConversation{{ID: "1", Title: "Menu"}} } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ProjectList(c, &state, projects, convs, mujica.ProjectListOptions{}); v.Moved { + core.Use(c, core.Settings{}) + if v := chat.ProjectList(c, &state, projects, convs, chat.ProjectListOptions{}); v.Moved { log.Println("move", v.MoveID, "to", v.MoveTo) } } ui.Render(view, 400, 300, 1)`}, {ID: "245", Name: "ProjectKnowledgePanel", ZhName: "项目知识面板", Category: chatsessionCategory, Demo: chatDemoProjectKnowledgePanel, - Keys: "Tab reaches Add files and the file list; Up/Down move between remove buttons; Enter removes.", Code: `files := []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} + Keys: "Tab reaches Add files and the file list; Up/Down move between remove buttons; Enter removes.", Code: `files := []chat.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.ProjectKnowledgePanel(c, files, mujica.ProjectKnowledgePanelOptions{Capacity: 200000}) + core.Use(c, core.Settings{}) + v := chat.ProjectKnowledgePanel(c, files, chat.ProjectKnowledgePanelOptions{Capacity: 200000}) if v.Add { log.Println("pick files") } } ui.Render(view, 400, 300, 1)`}, {ID: "246", Name: "WelcomeScreen", ZhName: "欢迎页", Category: chatsessionCategory, Demo: chatDemoWelcomeScreen, - Keys: "Tab moves through the cards' examples and the prompt slot; Enter picks.", Code: `cards := []mujica.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} + Keys: "Tab moves through the cards' examples and the prompt slot; Enter picks.", Code: `cards := []chat.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { + core.Use(c, core.Settings{}) + if v := chat.WelcomeScreen(c, chat.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" { log.Println("ask", v.Example) } } @@ -72,55 +72,55 @@ ui.Render(view, 400, 300, 1)`}, {ID: "247", Name: "SuggestionChips", ZhName: "建议标签", Category: chatsessionCategory, Demo: chatDemoSuggestionChips, Keys: "Tab enters the chips; Left/Right move; Enter or Space picks.", Code: `input := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{}) + core.Use(c, core.Settings{}) + chat.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, chat.SuggestionChipsOptions{}) } ui.Render(view, 400, 300, 1)`}, {ID: "248", Name: "PromptLibrary", ZhName: "提示模板库", Category: chatsessionCategory, Demo: chatDemoPromptLibrary, - Keys: "Tab moves between categories, search, templates and fields; Up/Down move in a column; Enter picks.", Code: `var state mujica.PromptLibraryState -templates := []mujica.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} + Keys: "Tab moves between categories, search, templates and fields; Up/Down move in a column; Enter picks.", Code: `var state chat.PromptLibraryState +templates := []chat.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.PromptLibrary(c, &state, templates, mujica.PromptLibraryOptions{}); v.Inserted != "" { + core.Use(c, core.Settings{}) + if v := chat.PromptLibrary(c, &state, templates, chat.PromptLibraryOptions{}); v.Inserted != "" { log.Println("insert", v.Inserted) } } ui.Render(view, 400, 300, 1)`}, {ID: "249", Name: "CapabilityCards", ZhName: "能力卡片", Category: chatsessionCategory, Demo: chatDemoCapabilityCards, - Keys: "Tab moves between cards; Up/Down move among a card's examples; Enter picks.", Code: `cards := []mujica.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} + Keys: "Tab moves between cards; Up/Down move among a card's examples; Enter picks.", Code: `cards := []chat.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.CapabilityCards(c, cards, mujica.CapabilityCardsOptions{}); v.Example != "" { + core.Use(c, core.Settings{}) + if v := chat.CapabilityCards(c, cards, chat.CapabilityCardsOptions{}); v.Example != "" { log.Println("ask", v.Example) } } ui.Render(view, 400, 300, 1)`}, {ID: "250", Name: "FeedbackForm", ZhName: "反馈表单", Category: chatsessionCategory, Demo: chatDemoFeedbackForm, - Keys: "Left/Right move among reasons, Space toggles; Escape cancels.", Code: `var value mujica.FeedbackValue + Keys: "Left/Right move among reasons, Space toggles; Escape cancels.", Code: `var value chat.FeedbackValue view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FeedbackForm(c, &value, mujica.FeedbackFormOptions{Rating: mujica.FeedbackDown}).Submitted { + core.Use(c, core.Settings{}) + if chat.FeedbackForm(c, &value, chat.FeedbackFormOptions{Rating: chat.FeedbackDown}).Submitted { log.Println("reasons", value.Reasons) } } ui.Render(view, 400, 300, 1)`}, {ID: "251", Name: "DateSeparator", ZhName: "日期分隔", Category: chatsessionCategory, Demo: chatDemoDateSeparator, Keys: "Not focusable; read as the date.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DateSeparator(c, mujica.DateOf(time.Now()), mujica.DateSeparatorOptions{}) + core.Use(c, core.Settings{}) + chat.DateSeparator(c, core.DateOf(time.Now()), chat.DateSeparatorOptions{}) } ui.Render(view, 400, 300, 1)`}, {ID: "252", Name: "ThinkingIndicator", ZhName: "思考指示", Category: chatsessionCategory, Demo: chatDemoThinkingIndicator, Keys: "Not focusable; read as a status.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: "Searching the archives"}) + core.Use(c, core.Settings{}) + chat.ThinkingIndicator(c, chat.ThinkingIndicatorOptions{Phase: "Searching the archives"}) } ui.Render(view, 400, 300, 1)`}, {ID: "253", Name: "StopGeneratingButton", ZhName: "停止生成按钮", Category: chatsessionCategory, Demo: chatDemoStopGeneratingButton, Keys: "Enter or Space stops; Cmd+. (Ctrl+. elsewhere) stops from anywhere.", Code: `generating := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: generating}).Changed() { + core.Use(c, core.Settings{}) + if chat.StopGeneratingButton(c, chat.StopGeneratingButtonOptions{Generating: generating}).Changed() { generating = false } } @@ -128,57 +128,57 @@ ui.Render(view, 400, 300, 1)`}, {ID: "254", Name: "SystemPromptEditor", ZhName: "系统提示编辑器", Category: chatsessionCategory, Demo: chatDemoSystemPromptEditor, Keys: "Cmd+S (Ctrl+S elsewhere) saves.", Code: `prompt, saved := "You are helpful.", "You are helpful." view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemPromptEditor(c, &prompt, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { + core.Use(c, core.Settings{}) + if chat.SystemPromptEditor(c, &prompt, chat.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save { saved = prompt } } ui.Render(view, 400, 300, 1)`}, {ID: "255", Name: "CostEstimator", ZhName: "费用估算", Category: chatsessionCategory, Demo: chatDemoCostEstimator, Keys: "Not focusable; read as the estimated cost.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) + core.Use(c, core.Settings{}) + chat.CostEstimator(c, chat.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15}) } ui.Render(view, 400, 300, 1)`}, {ID: "256", Name: "SharedConversationView", ZhName: "分享会话视图", Category: chatsessionCategory, Demo: chatDemoSharedConversationView, - Keys: "Tab reaches Copy link and the messages; message text is selectable.", Code: `msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}, {Role: mujica.SessionAssistant, Text: "Good day."}} + Keys: "Tab reaches Copy link and the messages; message text is selectable.", Code: `msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}, {Role: chat.SessionAssistant, Text: "Good day."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SharedConversationView(c, msgs, mujica.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { + core.Use(c, core.Settings{}) + if chat.SharedConversationView(c, msgs, chat.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() { c.WriteClipboard("https://example.com/share/1") } } ui.Render(view, 400, 300, 1)`}, {ID: "257", Name: "ConversationExport", ZhName: "会话导出", Category: chatsessionCategory, Demo: chatDemoConversationExport, - Keys: "Arrows choose a format and range; Tab reaches Export.", Code: `var state mujica.ConversationExportState -msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}} + Keys: "Arrows choose a format and range; Tab reaches Export.", Code: `var state chat.ConversationExportState +msgs := []chat.SessionMessage{{Role: chat.SessionUser, Text: "Hello"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.ConversationExport(c, &state, msgs, mujica.ConversationExportOptions{Title: "Greeting"}); v.Exported { + core.Use(c, core.Settings{}) + if v := chat.ConversationExport(c, &state, msgs, chat.ConversationExportOptions{Title: "Greeting"}); v.Exported { log.Println(len(v.Content), "bytes to save") } } ui.Render(view, 400, 300, 1)`}, {ID: "258", Name: "RegenerateMenu", ZhName: "重新生成菜单", Category: chatsessionCategory, Demo: chatDemoRegenerateMenu, Keys: "Enter, Space or Down open; Up/Down move; Right opens the model list; Escape closes.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { + core.Use(c, core.Settings{}) + if v := chat.RegenerateMenu(c, chat.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen { log.Println("retry", v.Choice.Kind, v.Choice.Model, v.Choice.Variant) } } ui.Render(view, 400, 300, 1)`}, {ID: "259", Name: "ArtifactCard", ZhName: "产物卡片", Category: chatsessionCategory, Demo: chatDemoArtifactCard, Keys: "Enter or Space opens the artifact panel.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ArtifactCard(c, mujica.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { + core.Use(c, core.Settings{}) + if chat.ArtifactCard(c, chat.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() { log.Println("open artifact panel") } } ui.Render(view, 400, 300, 1)`}, {ID: "260", Name: "ChatEmptyState", ZhName: "会话空状态", Category: chatsessionCategory, Demo: chatDemoChatEmptyState, Keys: "Tab reaches the action; Enter runs it.", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyOffline}).Action { + core.Use(c, core.Settings{}) + if chat.ChatEmptyState(c, chat.ChatEmptyStateOptions{Kind: chat.ChatEmptyOffline}).Action { log.Println("reconnect") } } diff --git a/examples/gallery/pages_chatsession_demos.go b/examples/gallery/pages_chatsession_demos.go index 8594e36..625268f 100644 --- a/examples/gallery/pages_chatsession_demos.go +++ b/examples/gallery/pages_chatsession_demos.go @@ -7,14 +7,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // chatHistory is the gallery's sample conversations, dated from now. -func chatHistory(now time.Time) []mujica.ChatConversation { +func chatHistory(now time.Time) []chat.ChatConversation { day := 24 * time.Hour - return []mujica.ChatConversation{ + return []chat.ChatConversation{ {ID: "a", Title: "Court etiquette notes", Updated: now.Add(-2 * time.Hour)}, {ID: "b", Title: "Ballroom seating plan", Updated: now.Add(-day)}, {ID: "c", Title: "Harpsichord repair", Updated: now.Add(-4 * day)}, @@ -24,17 +25,17 @@ func chatHistory(now time.Time) []mujica.ChatConversation { } } -func chatSampleMessages(now time.Time) []mujica.SessionMessage { - return []mujica.SessionMessage{ - {Role: mujica.SessionUser, Text: "How should guests be seated at the banquet?", Time: now.Add(-10 * time.Minute)}, - {Role: mujica.SessionAssistant, Text: "Seat by rank: the host at the head, honored guests to the right.", Time: now.Add(-9 * time.Minute)}, - {Role: mujica.SessionUser, Author: "Lady Ann", Text: "And the musicians?", Time: now.Add(-5 * time.Minute)}, - {Role: mujica.SessionAssistant, Text: "Place the quartet behind a screen near the gallery.", Time: now.Add(-4 * time.Minute)}, +func chatSampleMessages(now time.Time) []chat.SessionMessage { + return []chat.SessionMessage{ + {Role: chat.SessionUser, Text: "How should guests be seated at the banquet?", Time: now.Add(-10 * time.Minute)}, + {Role: chat.SessionAssistant, Text: "Seat by rank: the host at the head, honored guests to the right.", Time: now.Add(-9 * time.Minute)}, + {Role: chat.SessionUser, Author: "Lady Ann", Text: "And the musicians?", Time: now.Add(-5 * time.Minute)}, + {Role: chat.SessionAssistant, Text: "Place the quartet behind a screen near the gallery.", Time: now.Add(-4 * time.Minute)}, } } -func chatSampleCards() []mujica.CapabilityCard { - return []mujica.CapabilityCard{ +func chatSampleCards() []chat.CapabilityCard { + return []chat.CapabilityCard{ {Title: "Write", Description: "Letters, speeches and verse.", Icon: icons.Must("file-text"), Examples: []string{"Draft a royal decree", "Write a sonnet"}}, {Title: "Plan", Description: "Banquets, seating and schedules.", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball for 200 guests"}}, {Title: "Research", Description: "History, heraldry and music.", Icon: icons.Must("search"), Examples: []string{"Explain the minuet"}}, @@ -43,19 +44,19 @@ func chatSampleCards() []mujica.CapabilityCard { func chatDemoConversationList(c *ui.Context, log func(string)) { type state struct { - list mujica.ConversationListState - items []mujica.ChatConversation + list chat.ConversationListState + items []chat.ChatConversation many, rich bool disabled bool } s := *ui.Local(c.Root(), "241", func() *state { return &state{items: chatHistory(time.Now())} }) choiceDemo(c, func() { - if mujica.Switch(c, &s.many, "100,000 conversations", mujica.SwitchOptions{}).Changed() { + if input.Switch(c, &s.many, "100,000 conversations", input.SwitchOptions{}).Changed() { s.items = chatHistory(time.Now()) if s.many { now := time.Now() for i := range 100000 { - s.items = append(s.items, mujica.ChatConversation{ID: fmt.Sprint("n", i), Title: fmt.Sprintf("Archive %d", i), Updated: now.Add(-time.Duration(i) * 7 * time.Minute)}) + s.items = append(s.items, chat.ChatConversation{ID: fmt.Sprint("n", i), Title: fmt.Sprintf("Archive %d", i), Updated: now.Add(-time.Duration(i) * 7 * time.Minute)}) } } log(fmt.Sprintf("conversations = %d", len(s.items))) @@ -63,17 +64,17 @@ func chatDemoConversationList(c *ui.Context, log func(string)) { choiceFlag(c, &s.rich, "ConversationItem rows", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - var row func(conv mujica.ChatConversation) + var row func(conv chat.ChatConversation) if s.rich { - row = func(conv mujica.ChatConversation) { - v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{Selected: conv.ID == s.list.Selected}) + row = func(conv chat.ChatConversation) { + v := chat.ConversationItem(c, &conv, chat.ConversationItemOptions{Selected: conv.ID == s.list.Selected}) if v.Opened { s.list.Selected = conv.ID log("open " + conv.Title) } } } - v := mujica.ConversationList(c, &s.list, s.items, mujica.ConversationListOptions{Label: "History", Disabled: s.disabled}, row) + v := chat.ConversationList(c, &s.list, s.items, chat.ConversationListOptions{Label: "History", Disabled: s.disabled}, row) v.Element.Width(380).Height(380) if v.Changed() { log("select " + s.list.Selected) @@ -86,7 +87,7 @@ func chatDemoConversationList(c *ui.Context, log func(string)) { func chatDemoConversationItem(c *ui.Context, log func(string)) { type state struct { - convs []mujica.ChatConversation + convs []chat.ChatConversation selected, disabled bool } s := *ui.Local(c.Root(), "242", func() *state { return &state{convs: chatHistory(time.Now())[:3]} }) @@ -98,7 +99,7 @@ func chatDemoConversationItem(c *ui.Context, log func(string)) { for i := range s.convs { conv := &s.convs[i] ui.Row(c).Key(conv.ID).Children(func() { - v := mujica.ConversationItem(c, conv, mujica.ConversationItemOptions{Selected: s.selected && i == 0, Disabled: s.disabled}) + v := chat.ConversationItem(c, conv, chat.ConversationItemOptions{Selected: s.selected && i == 0, Disabled: s.disabled}) switch { case v.Opened: log("open " + conv.Title) @@ -122,7 +123,7 @@ func chatDemoConversationSearch(c *ui.Context, log func(string)) { type state struct { query, asked string at time.Time - hits []mujica.ConversationSearchHit + hits []chat.ConversationSearchHit fail, slow bool disabled bool } @@ -137,7 +138,7 @@ func chatDemoConversationSearch(c *ui.Context, log func(string)) { s.asked, s.hits, loading = s.query, nil, false for _, conv := range chatHistory(time.Now()) { if q := strings.ToLower(s.query); q != "" && strings.Contains(strings.ToLower(conv.Title), q) { - s.hits = append(s.hits, mujica.ConversationSearchHit{ID: conv.ID, Title: conv.Title, Snippet: "…mentions " + strings.ToLower(conv.Title) + "…"}) + s.hits = append(s.hits, chat.ConversationSearchHit{ID: conv.ID, Title: conv.Title, Snippet: "…mentions " + strings.ToLower(conv.Title) + "…"}) } } log(fmt.Sprintf("results for %q: %d", s.query, len(s.hits))) @@ -147,7 +148,7 @@ func chatDemoConversationSearch(c *ui.Context, log func(string)) { errText = "The archive did not answer." } ui.Column(c).Width(420).Children(func() { - v := mujica.ConversationSearch(c, &s.query, s.hits, mujica.ConversationSearchOptions{Loading: loading, Error: errText, Disabled: s.disabled, Label: "Search conversations"}) + v := chat.ConversationSearch(c, &s.query, s.hits, chat.ConversationSearchOptions{Loading: loading, Error: errText, Disabled: s.disabled, Label: "Search conversations"}) if v.QueryChanged { s.at = c.Now() log(fmt.Sprintf("query %q", s.query)) @@ -165,21 +166,21 @@ func chatDemoConversationSearch(c *ui.Context, log func(string)) { func chatDemoProjectList(c *ui.Context, log func(string)) { type state struct { - list mujica.ProjectListState + list chat.ProjectListState owner map[string]string disabled bool } s := *ui.Local(c.Root(), "244", func() *state { return &state{owner: map[string]string{"a": "p1", "b": "p1", "c": "p3", "f": "p2"}} }) - projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}, {ID: "p2", Name: "Gardens", Folder: "Court"}, {ID: "p3", Name: "Music"}} + projects := []chat.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}, {ID: "p2", Name: "Gardens", Folder: "Court"}, {ID: "p3", Name: "Music"}} choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Row(c).Gap(24).AlignItems(ui.Start).Children(func() { ui.Column(c).Width(380).Children(func() { - convs := func(id string) []mujica.ChatConversation { - var out []mujica.ChatConversation + convs := func(id string) []chat.ChatConversation { + var out []chat.ChatConversation for _, conv := range chatHistory(time.Now()) { if s.owner[conv.ID] == id { out = append(out, conv) @@ -187,7 +188,7 @@ func chatDemoProjectList(c *ui.Context, log func(string)) { } return out } - v := mujica.ProjectList(c, &s.list, projects, convs, mujica.ProjectListOptions{Label: "Projects", Disabled: s.disabled}) + v := chat.ProjectList(c, &s.list, projects, convs, chat.ProjectListOptions{Label: "Projects", Disabled: s.disabled}) switch { case v.Moved: s.owner[v.MoveID] = v.MoveTo @@ -200,7 +201,7 @@ func chatDemoProjectList(c *ui.Context, log func(string)) { }) choiceLabeled(c, "Drag onto a project", func() { conv := chatHistory(time.Now())[3] - v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{Draggable: true}) + v := chat.ConversationItem(c, &conv, chat.ConversationItemOptions{Draggable: true}) v.Element.Width(300) if v.Opened { log("open " + conv.Title) @@ -212,13 +213,13 @@ func chatDemoProjectList(c *ui.Context, log func(string)) { func chatDemoProjectKnowledgePanel(c *ui.Context, log func(string)) { type state struct { - files []mujica.KnowledgeFile + files []chat.KnowledgeFile next int small bool disabled bool } s := *ui.Local(c.Root(), "245", func() *state { - return &state{files: []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 120000}, {ID: "2", Name: "menu.md", Tokens: 30000}}} + return &state{files: []chat.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 120000}, {ID: "2", Name: "menu.md", Tokens: 30000}}} }) choiceDemo(c, func() { choiceFlag(c, &s.small, "Capacity 150,000", log) @@ -229,10 +230,10 @@ func chatDemoProjectKnowledgePanel(c *ui.Context, log func(string)) { capacity = 150000 } ui.Column(c).Width(440).Children(func() { - v := mujica.ProjectKnowledgePanel(c, s.files, mujica.ProjectKnowledgePanelOptions{Capacity: capacity, Disabled: s.disabled}) + v := chat.ProjectKnowledgePanel(c, s.files, chat.ProjectKnowledgePanelOptions{Capacity: capacity, Disabled: s.disabled}) if v.Add { s.next++ - s.files = append(s.files, mujica.KnowledgeFile{ID: fmt.Sprint("n", s.next), Name: fmt.Sprintf("notes-%d.txt", s.next), Tokens: 15000}) + s.files = append(s.files, chat.KnowledgeFile{ID: fmt.Sprint("n", s.next), Name: fmt.Sprintf("notes-%d.txt", s.next), Tokens: 15000}) log("add (the demo invents a file)") } if v.Removed != "" { diff --git a/examples/gallery/pages_chatsession_more.go b/examples/gallery/pages_chatsession_more.go index b6fd37d..2daa7e6 100644 --- a/examples/gallery/pages_chatsession_more.go +++ b/examples/gallery/pages_chatsession_more.go @@ -6,7 +6,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func chatDemoWelcomeScreen(c *ui.Context, log func(string)) { @@ -19,14 +22,14 @@ func chatDemoWelcomeScreen(c *ui.Context, log func(string)) { choiceFlag(c, &s.cards, "Capability cards", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - var cards []mujica.CapabilityCard + var cards []chat.CapabilityCard if s.cards { cards = chatSampleCards() } ui.Column(c).Width(860).Children(func() { - v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good afternoon", Subtitle: "How may I serve the court?", Cards: cards, Disabled: s.disabled, + v := chat.WelcomeScreen(c, chat.WelcomeScreenOptions{Greeting: "Good afternoon", Subtitle: "How may I serve the court?", Cards: cards, Disabled: s.disabled, Prompts: func() { - if mujica.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{Disabled: s.disabled}).Chosen != "" { + if chat.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet"}, chat.SuggestionChipsOptions{Disabled: s.disabled}).Chosen != "" { log(fmt.Sprintf("fill %q", s.input)) } }}) @@ -47,8 +50,8 @@ func chatDemoSuggestionChips(c *ui.Context, log func(string)) { choiceFlag(c, &s.send, "Send directly", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - v := mujica.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet", "Explain the minuet"}, - mujica.SuggestionChipsOptions{Send: s.send, Label: "Suggestions", Disabled: s.disabled}) + v := chat.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet", "Explain the minuet"}, + chat.SuggestionChipsOptions{Send: s.send, Label: "Suggestions", Disabled: s.disabled}) switch { case v.Sent: log(fmt.Sprintf("send %q", v.Chosen)) @@ -56,18 +59,18 @@ func chatDemoSuggestionChips(c *ui.Context, log func(string)) { log(fmt.Sprintf("fill %q", v.Chosen)) } ui.Column(c).Width(420).Children(func() { - mujica.TextInput(c, &s.input, mujica.TextInputOptions{Label: "Message", Placeholder: "Message"}) + input.TextInput(c, &s.input, input.TextInputOptions{Label: "Message", Placeholder: "Message"}) }) }) } func chatDemoPromptLibrary(c *ui.Context, log func(string)) { type state struct { - lib mujica.PromptLibraryState + lib chat.PromptLibraryState disabled bool } s := *ui.Local(c.Root(), "248", func() *state { return &state{} }) - templates := []mujica.PromptTemplate{ + templates := []chat.PromptTemplate{ {ID: "t1", Category: "Letters", Title: "Invitation", Body: "Write an invitation to {{guest}} for the {{event}}."}, {ID: "t2", Category: "Letters", Title: "Thank-you note", Body: "Thank {{host}} for the evening."}, {ID: "t3", Category: "Music", Title: "Recital program", Body: "List pieces for a {{instrument}} recital."}, @@ -77,7 +80,7 @@ func chatDemoPromptLibrary(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { before := s.lib.Selected - v := mujica.PromptLibrary(c, &s.lib, templates, mujica.PromptLibraryOptions{Label: "Prompt library", Disabled: s.disabled}) + v := chat.PromptLibrary(c, &s.lib, templates, chat.PromptLibraryOptions{Label: "Prompt library", Disabled: s.disabled}) if s.lib.Selected != before { log("preview " + s.lib.Selected) } @@ -101,7 +104,7 @@ func chatDemoCapabilityCards(c *ui.Context, log func(string)) { cols = 1 } ui.Column(c).Width(860).Children(func() { - if v := mujica.CapabilityCards(c, chatSampleCards(), mujica.CapabilityCardsOptions{Columns: cols, Disabled: s.disabled}); v.Example != "" { + if v := chat.CapabilityCards(c, chatSampleCards(), chat.CapabilityCardsOptions{Columns: cols, Disabled: s.disabled}); v.Example != "" { log(fmt.Sprintf("ask %q", v.Example)) } }) @@ -110,7 +113,7 @@ func chatDemoCapabilityCards(c *ui.Context, log func(string)) { func chatDemoFeedbackForm(c *ui.Context, log func(string)) { type state struct { - value mujica.FeedbackValue + value chat.FeedbackValue down, submitting bool disabled bool at time.Time @@ -120,22 +123,22 @@ func chatDemoFeedbackForm(c *ui.Context, log func(string)) { choiceFlag(c, &s.down, "Thumbs down", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - rating := mujica.FeedbackUp + rating := chat.FeedbackUp if s.down { - rating = mujica.FeedbackDown + rating = chat.FeedbackDown } if s.submitting && choiceDue(c, s.at) { - s.submitting, s.value = false, mujica.FeedbackValue{} + s.submitting, s.value = false, chat.FeedbackValue{} log("feedback sent") } ui.Column(c).Width(480).Children(func() { - v := mujica.FeedbackForm(c, &s.value, mujica.FeedbackFormOptions{Rating: rating, Submitting: s.submitting, Disabled: s.disabled}) + v := chat.FeedbackForm(c, &s.value, chat.FeedbackFormOptions{Rating: rating, Submitting: s.submitting, Disabled: s.disabled}) if v.Submitted { s.submitting, s.at = true, c.Now() log(fmt.Sprintf("submit reasons %q, %d characters", s.value.Reasons, len(s.value.Comment))) } if v.Cancelled { - s.value = mujica.FeedbackValue{} + s.value = chat.FeedbackValue{} log("cancel") } }) @@ -143,13 +146,13 @@ func chatDemoFeedbackForm(c *ui.Context, log func(string)) { } func chatDemoDateSeparator(c *ui.Context, log func(string)) { - today := mujica.DateOf(time.Now()) + today := core.DateOf(time.Now()) choiceDemo(c, func() { ui.Text(c, "No parameters; the language follows the window.") }, func() { ui.Column(c).Width(520).Children(func() { for _, d := range []int{0, -1, -12} { - mujica.DateSeparator(c, today.AddDays(d), mujica.DateSeparatorOptions{}) + chat.DateSeparator(c, today.AddDays(d), chat.DateSeparatorOptions{}) } }) }) @@ -160,12 +163,12 @@ func chatDemoThinkingIndicator(c *ui.Context, log func(string)) { s := *ui.Local(c.Root(), "252", func() *state { return &state{} }) phases := []string{"", "Searching the archives", "Reading 3 letters", "Composing a reply"} choiceDemo(c, func() { - if mujica.Button(c, "Next phase", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Next phase", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.phase = (s.phase + 1) % len(phases) log(fmt.Sprintf("phase %q", phases[s.phase])) } }, func() { - mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: phases[s.phase]}) + chat.ThinkingIndicator(c, chat.ThinkingIndicatorOptions{Phase: phases[s.phase]}) }) } @@ -176,7 +179,7 @@ func chatDemoStopGeneratingButton(c *ui.Context, log func(string)) { } s := *ui.Local(c.Root(), "253", func() *state { return &state{} }) choiceDemo(c, func() { - if mujica.Button(c, "Generate", mujica.ButtonOptions{Disabled: s.generating}).Clicked() { + if input.Button(c, "Generate", input.ButtonOptions{Disabled: s.generating}).Clicked() { s.generating, s.at = true, c.Now() log("generate") } @@ -188,12 +191,12 @@ func chatDemoStopGeneratingButton(c *ui.Context, log func(string)) { } else if s.generating { c.After(time.Second) } - if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: s.generating, Disabled: s.disabled}).Changed() { + if chat.StopGeneratingButton(c, chat.StopGeneratingButtonOptions{Generating: s.generating, Disabled: s.disabled}).Changed() { s.generating = false log("stop") } if !s.generating { - ui.Text(c, "Idle: the button is hidden.").TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, "Idle: the button is hidden.").TextColor(core.Tokens(c).TextMuted) } }) } @@ -221,7 +224,7 @@ func chatDemoSystemPromptEditor(c *ui.Context, log func(string)) { limit = 60 } ui.Column(c).Width(520).Children(func() { - v := mujica.SystemPromptEditor(c, &s.value, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: s.saved, + v := chat.SystemPromptEditor(c, &s.value, chat.SystemPromptEditorOptions{Default: "You are helpful.", Saved: s.saved, MaxLength: limit, Saving: s.saving, Disabled: s.disabled}) if v.Save { s.saving, s.at = true, c.Now() @@ -243,7 +246,7 @@ func chatDemoCostEstimator(c *ui.Context, log func(string)) { label string }{{&s.in, "Input tokens"}, {&s.out, "Output tokens"}, {&s.inPrice, "Input $/M"}, {&s.outPrice, "Output $/M"}} { choiceLabeled(c, f.label, func() { - r := mujica.NumberInput(c, f.v, mujica.NumberInputOptions{Min: 0, Max: 1e7, Step: 100, Label: f.label}) + r := input.NumberInput(c, f.v, input.NumberInputOptions{Min: 0, Max: 1e7, Step: 100, Label: f.label}) r.Element.Width(150) if r.Changed() { log(fmt.Sprintf("%s = %v", f.label, *f.v)) @@ -252,7 +255,7 @@ func chatDemoCostEstimator(c *ui.Context, log func(string)) { } }, func() { ui.Column(c).Width(420).Children(func() { - mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: int(s.in), OutputTokens: int(s.out), InputPrice: s.inPrice, OutputPrice: s.outPrice}) + chat.CostEstimator(c, chat.CostEstimatorOptions{InputTokens: int(s.in), OutputTokens: int(s.out), InputPrice: s.inPrice, OutputPrice: s.outPrice}) }) }) } @@ -264,7 +267,7 @@ func chatDemoSharedConversationView(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Width(600).Height(420).Children(func() { - if mujica.SharedConversationView(c, chatSampleMessages(time.Now()), mujica.SharedConversationViewOptions{Title: "Banquet seating", + if chat.SharedConversationView(c, chatSampleMessages(time.Now()), chat.SharedConversationViewOptions{Title: "Banquet seating", SharedBy: "Lady Ann", SharedAt: time.Now(), Disabled: s.disabled}).Changed() { c.WriteClipboard("https://example.com/share/banquet") log("copy link") @@ -275,7 +278,7 @@ func chatDemoSharedConversationView(c *ui.Context, log func(string)) { func chatDemoConversationExport(c *ui.Context, log func(string)) { type state struct { - export mujica.ConversationExportState + export chat.ConversationExportState disabled bool } s := *ui.Local(c.Root(), "257", func() *state { return &state{} }) @@ -283,7 +286,7 @@ func chatDemoConversationExport(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { ui.Column(c).Width(580).Children(func() { - v := mujica.ConversationExport(c, &s.export, chatSampleMessages(time.Now()), mujica.ConversationExportOptions{Title: "Banquet seating", + v := chat.ConversationExport(c, &s.export, chatSampleMessages(time.Now()), chat.ConversationExportOptions{Title: "Banquet seating", Selected: []int{1, 3}, Disabled: s.disabled}) if v.Exported { log(fmt.Sprintf("export %d bytes as format %d (the caller writes the file)", len(v.Content), v.Format)) @@ -298,7 +301,7 @@ func chatDemoRegenerateMenu(c *ui.Context, log func(string)) { choiceDemo(c, func() { choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet", "Haiku"}, Current: "Opus", Disabled: s.disabled}) + v := chat.RegenerateMenu(c, chat.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet", "Haiku"}, Current: "Opus", Disabled: s.disabled}) if v.Chosen { log(fmt.Sprintf("retry kind %d model %q variant %q", v.Choice.Kind, v.Choice.Model, v.Choice.Variant)) } @@ -312,13 +315,13 @@ func chatDemoArtifactCard(c *ui.Context, log func(string)) { choiceFlag(c, &s.streaming, "Streaming", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - for _, a := range []mujica.ArtifactCardOptions{ - {Kind: mujica.ArtifactCode, Title: "seating.go", Detail: "Go · 120 lines"}, - {Kind: mujica.ArtifactDocument, Title: "Royal decree", Detail: "2 pages"}, - {Kind: mujica.ArtifactWeb, Title: "Ball invitation"}, + for _, a := range []chat.ArtifactCardOptions{ + {Kind: agent.ArtifactCode, Title: "seating.go", Detail: "Go · 120 lines"}, + {Kind: agent.ArtifactDocument, Title: "Royal decree", Detail: "2 pages"}, + {Kind: agent.ArtifactWeb, Title: "Ball invitation"}, } { a.Streaming, a.Disabled = s.streaming, s.disabled - if mujica.ArtifactCard(c, a).Changed() { + if chat.ArtifactCard(c, a).Changed() { log("open " + a.Title) } } @@ -329,11 +332,11 @@ func chatDemoChatEmptyState(c *ui.Context, log func(string)) { type state struct{ kind int } s := *ui.Local(c.Root(), "260", func() *state { return &state{} }) choiceDemo(c, func() { - mujica.RadioGroup(c, &s.kind, []mujica.RadioItem[int]{{Value: 0, Label: "New"}, {Value: 1, Label: "No results"}, {Value: 2, Label: "Offline"}}, - mujica.RadioGroupOptions{Horizontal: true, Label: "Kind"}) + input.RadioGroup(c, &s.kind, []input.RadioItem[int]{{Value: 0, Label: "New"}, {Value: 1, Label: "No results"}, {Value: 2, Label: "Offline"}}, + input.RadioGroupOptions{Horizontal: true, Label: "Kind"}) }, func() { ui.Column(c).Width(420).Children(func() { - if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyKind(s.kind), Query: "minuet"}).Action { + if chat.ChatEmptyState(c, chat.ChatEmptyStateOptions{Kind: chat.ChatEmptyKind(s.kind), Query: "minuet"}).Action { log(fmt.Sprintf("action for kind %d", s.kind)) } }) diff --git a/examples/gallery/pages_choice.go b/examples/gallery/pages_choice.go index 2de7d31..ef0d5d9 100644 --- a/examples/gallery/pages_choice.go +++ b/examples/gallery/pages_choice.go @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const choiceCategory = "Choices" @@ -18,104 +19,104 @@ const choiceCategory = "Choices" var choicePages = []page{ {ID: "031", Name: "Switch", ZhName: "开关", Category: choiceCategory, Demo: switchDemo, Code: `wifi := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Switch(c, &wifi, "Wi-Fi", mujica.SwitchOptions{}).Changed() { + core.Use(c, core.Settings{}) + if input.Switch(c, &wifi, "Wi-Fi", input.SwitchOptions{}).Changed() { log.Println("Wi-Fi:", wifi) } }`, Keys: "Tab focuses the switch; Space turns it over."}, {ID: "032", Name: "Toggle", ZhName: "切换按钮", Category: choiceCategory, Demo: toggleDemo, Code: `bold := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Toggle(c, &bold, "", mujica.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) + core.Use(c, core.Settings{}) + input.Toggle(c, &bold, "", input.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) }`, Keys: "Tab focuses the toggle; Space or Enter presses or releases it."}, {ID: "033", Name: "ToggleGroup", ZhName: "切换按钮组", Category: choiceCategory, Demo: toggleGroupDemo, Code: `align := []string{"left"} -items := []mujica.ToggleItem[string]{ +items := []input.ToggleItem[string]{ {Value: "left", Label: "Left"}, {Value: "center", Label: "Center"}, {Value: "right", Label: "Right"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ToggleGroup(c, &align, items, mujica.ToggleGroupOptions{Label: "Align"}) + core.Use(c, core.Settings{}) + input.ToggleGroup(c, &align, items, input.ToggleGroupOptions{Label: "Align"}) }`, Keys: "Tab enters the group at the first enabled item, later at the last focused one; Left/Right (Up/Down when vertical), Home and End move; Space presses."}, {ID: "034", Name: "Slider", ZhName: "滑块", Category: choiceCategory, Demo: sliderDemo, Code: `volume := 40.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Slider(c, &volume, mujica.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) + core.Use(c, core.Settings{}) + input.Slider(c, &volume, input.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) }`, Keys: "Arrows move one step, Home and End go to the ends; the move applies when the key is released."}, {ID: "035", Name: "RangeSlider", ZhName: "范围滑块", Category: choiceCategory, Demo: rangeSliderDemo, Code: `low, high := 20.0, 80.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RangeSlider(c, &low, &high, mujica.RangeSliderOptions{Step: 10, Label: "Price"}) + core.Use(c, core.Settings{}) + input.RangeSlider(c, &low, &high, input.RangeSliderOptions{Step: 10, Label: "Price"}) }`, Keys: "Tab moves between the two thumbs; arrows move one step; Home and End go as far as the other thumb allows."}, {ID: "036", Name: "Select", ZhName: "选择器", Category: choiceCategory, Demo: selectDemo, Code: `plan := "" -options := []mujica.SelectOption[string]{ +options := []input.SelectOption[string]{ {Value: "free", Label: "Free"}, {Value: "pro", Label: "Pro"}, {Value: "team", Label: "Team", Disabled: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Select(c, &plan, options, mujica.SelectOptions{Clearable: true, Label: "Plan"}) + core.Use(c, core.Settings{}) + input.Select(c, &plan, options, input.SelectOptions{Clearable: true, Label: "Plan"}) }`, Keys: "Enter, Space, Up or Down open; arrows, Home, End move; Enter chooses; Escape closes; typing letters on the closed select jumps to a match; Backspace/Delete clear when Clearable."}, {ID: "037", Name: "MultiSelect", ZhName: "多选选择器", Category: choiceCategory, Demo: multiSelectDemo, Code: `tags := []int{1} -options := []mujica.SelectOption[int]{ +options := []input.SelectOption[int]{ {Value: 1, Label: "Rose"}, {Value: 2, Label: "Lily"}, {Value: 3, Label: "Iris"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiSelect(c, &tags, options, mujica.MultiSelectOptions{Max: 2, Label: "Flowers"}) + core.Use(c, core.Settings{}) + input.MultiSelect(c, &tags, options, input.MultiSelectOptions{Max: 2, Label: "Flowers"}) }`, Keys: "Enter, Space or Down open with the search focused; Down enters the list; Up/Down move; Space checks; Escape closes; Backspace on the field removes the last tag."}, {ID: "038", Name: "Combobox", ZhName: "组合框", Category: choiceCategory, Demo: comboboxDemo, Code: `font := "" fonts := []string{"Garamond", "Georgia", "Didot"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Combobox(c, &font, fonts, mujica.ComboboxOptions{AllowCustom: true, Label: "Font"}) + core.Use(c, core.Settings{}) + r := input.Combobox(c, &font, fonts, input.ComboboxOptions{AllowCustom: true, Label: "Font"}) if r.QueryChanged() { log.Println("search:", r.Query()) } }`, Keys: "Typing filters; Down opens or moves; Up moves; Enter chooses; Escape closes; Tab leaves (keeping typed text with AllowCustom)."}, {ID: "039", Name: "Cascader", ZhName: "级联选择", Category: choiceCategory, Demo: cascaderDemo, Code: `var path []string -level := func(p []string) mujica.CascaderLevel { +level := func(p []string) input.CascaderLevel { if len(p) == 0 { - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "fr", Label: "France"}}} + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "fr", Label: "France"}}} } - return mujica.CascaderLevel{Nodes: []mujica.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Cascader(c, &path, mujica.CascaderOptions{Level: level, Label: "City"}) + core.Use(c, core.Settings{}) + input.Cascader(c, &path, input.CascaderOptions{Level: level, Label: "City"}) }`, Keys: "Enter, Space or Down open; Up/Down move in a column; Right opens a branch; Left goes back; Enter chooses; Escape closes."}, {ID: "040", Name: "TreeSelect", ZhName: "树选择", Category: choiceCategory, Demo: treeSelectDemo, Code: `var roles []string -nodes := []mujica.TreeSelectNode{ - {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ +nodes := []input.TreeSelectNode{ + {ID: "court", Label: "Court", Children: []input.TreeSelectNode{ {ID: "king", Label: "King"}, {ID: "queen", Label: "Queen"}, }}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeSelect(c, &roles, nodes, mujica.TreeSelectOptions{Multiple: true, Label: "Roles"}) + core.Use(c, core.Settings{}) + input.TreeSelect(c, &roles, nodes, input.TreeSelectOptions{Multiple: true, Label: "Roles"}) }`, Keys: "Enter, Space or Down open with the search focused; Down enters the tree; Up/Down move; Right expands or enters; Left collapses or goes to the parent; Space checks; Escape closes."}, } // choiceDemo lays out a demo: parameter controls, then the samples. func choiceDemo(c *ui.Context, controls, samples func()) { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Column(c).Gap(20).AlignItems(ui.Start).Children(func() { - ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, "Parameters").Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Row(c).Gap(20).Wrap().AlignItems(ui.Center).Children(controls) ui.Box(c).Height(1).FillWidth().Background(k.Ornament) - ui.Text(c, "States").Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, "States").Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) ui.Column(c).Gap(16).AlignItems(ui.Start).Children(samples) }) } // choiceFlag is a parameter switch that logs its change. func choiceFlag(c *ui.Context, on *bool, label string, log func(string)) { - if mujica.Switch(c, on, label, mujica.SwitchOptions{}).Changed() { + if input.Switch(c, on, label, input.SwitchOptions{}).Changed() { log(fmt.Sprintf("%s = %v", label, *on)) } } @@ -123,7 +124,7 @@ func choiceFlag(c *ui.Context, on *bool, label string, log func(string)) { // choiceLabeled shows a caption above a sample. func choiceLabeled(c *ui.Context, caption string, fn func()) { ui.Column(c).Gap(6).Children(func() { - ui.Text(c, caption).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, caption).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted) fn() }) } @@ -136,12 +137,12 @@ func switchDemo(c *ui.Context, log func(string)) { choiceFlag(c, &s.disabled, "Disabled", log) choiceFlag(c, &s.loading, "Loading", log) }, func() { - if mujica.Switch(c, &s.wifi, "Wi-Fi", mujica.SwitchOptions{Disabled: s.disabled, Loading: s.loading}).Changed() { + if input.Switch(c, &s.wifi, "Wi-Fi", input.SwitchOptions{Disabled: s.disabled, Loading: s.loading}).Changed() { log(fmt.Sprintf("Wi-Fi changed: %v", s.wifi)) } // A caller applying the change: loading for a moment, then done. at := ui.Local(c.Root(), "031-at", func() time.Time { return time.Time{} }) - if mujica.Switch(c, &s.pending, "Backup (saves for 1 s)", mujica.SwitchOptions{Loading: !at.IsZero()}).Changed() { + if input.Switch(c, &s.pending, "Backup (saves for 1 s)", input.SwitchOptions{Loading: !at.IsZero()}).Changed() { *at = c.Now() log(fmt.Sprintf("Backup requested: %v", s.pending)) } @@ -155,8 +156,8 @@ func switchDemo(c *ui.Context, log func(string)) { } off, on := false, true ui.Row(c).Gap(24).Children(func() { - mujica.Switch(c, &off, "Disabled off", mujica.SwitchOptions{Disabled: true}) - mujica.Switch(c, &on, "Disabled on", mujica.SwitchOptions{Disabled: true}) + input.Switch(c, &off, "Disabled off", input.SwitchOptions{Disabled: true}) + input.Switch(c, &on, "Disabled on", input.SwitchOptions{Disabled: true}) }) }) } @@ -173,12 +174,12 @@ func toggleDemo(c *ui.Context, log func(string)) { on *bool label, ico string }{{&s.bold, "Bold", "bold"}, {&s.italic, "Italic", "italic"}, {&s.underline, "Underline", "underline"}} { - if mujica.Toggle(c, t.on, "", mujica.ToggleOptions{Icon: icons.Must(t.ico), Label: t.label, Disabled: s.disabled}).Changed() { + if input.Toggle(c, t.on, "", input.ToggleOptions{Icon: icons.Must(t.ico), Label: t.label, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("%s: %v", t.label, *t.on)) } } }) - if mujica.Toggle(c, &s.bold, "Bold text", mujica.ToggleOptions{Disabled: s.disabled}).Changed() { + if input.Toggle(c, &s.bold, "Bold text", input.ToggleOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("Bold: %v", s.bold)) } }) @@ -190,12 +191,12 @@ func toggleGroupDemo(c *ui.Context, log func(string)) { multiple, allowEmpty, vertical, disabled bool } s := *ui.Local(c.Root(), "033", func() *state { return &state{value: []string{"left"}} }) - items := []mujica.ToggleItem[string]{ + items := []input.ToggleItem[string]{ {Value: "left", Label: "Left"}, {Value: "center", Label: "Center"}, {Value: "right", Label: "Right"}, {Value: "justify", Label: "Justify", Disabled: true}, } choiceDemo(c, func() { - if mujica.Switch(c, &s.multiple, "Multiple", mujica.SwitchOptions{}).Changed() { + if input.Switch(c, &s.multiple, "Multiple", input.SwitchOptions{}).Changed() { s.value = s.value[:min(len(s.value), 1)] log(fmt.Sprintf("Multiple = %v", s.multiple)) } @@ -203,11 +204,11 @@ func toggleGroupDemo(c *ui.Context, log func(string)) { choiceFlag(c, &s.vertical, "Vertical", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - opts := mujica.ToggleGroupOptions{Multiple: s.multiple, AllowEmpty: s.allowEmpty, Vertical: s.vertical, Disabled: s.disabled, Label: "Align"} - if mujica.ToggleGroup(c, &s.value, items, opts).Changed() { + opts := input.ToggleGroupOptions{Multiple: s.multiple, AllowEmpty: s.allowEmpty, Vertical: s.vertical, Disabled: s.disabled, Label: "Align"} + if input.ToggleGroup(c, &s.value, items, opts).Changed() { log(fmt.Sprintf("value: %v", s.value)) } - ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(core.Tokens(c).TextMuted) }) } @@ -222,24 +223,24 @@ func sliderDemo(c *ui.Context, log func(string)) { choiceFlag(c, &s.showValue, "ShowValue", log) choiceFlag(c, &s.disabled, "Disabled", log) choiceLabeled(c, fmt.Sprintf("Step %v", s.step), func() { - if mujica.Slider(c, &s.step, mujica.SliderOptions{Max: 25, Step: 5, Label: "Step"}).Changed() { + if input.Slider(c, &s.step, input.SliderOptions{Max: 25, Step: 5, Label: "Step"}).Changed() { log(fmt.Sprintf("Step = %v", s.step)) } }) }, func() { - var marks []mujica.SliderMark + var marks []input.SliderMark if s.marks { for v := 0.0; v <= 100; v += 25 { - marks = append(marks, mujica.SliderMark{Value: v, Label: fmt.Sprint(v)}) + marks = append(marks, input.SliderMark{Value: v, Label: fmt.Sprint(v)}) } } ui.Column(c).Width(360).Children(func() { - opts := mujica.SliderOptions{Step: s.step, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Volume"} - if mujica.Slider(c, &s.value, opts).Changed() { + opts := input.SliderOptions{Step: s.step, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Volume"} + if input.Slider(c, &s.value, opts).Changed() { log(fmt.Sprintf("Volume: %v", s.value)) } }) - ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, fmt.Sprintf("Value: %v", s.value)).TextColor(core.Tokens(c).TextMuted) }) } @@ -254,18 +255,18 @@ func rangeSliderDemo(c *ui.Context, log func(string)) { choiceFlag(c, &s.showValue, "ShowValue", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - var marks []mujica.SliderMark + var marks []input.SliderMark if s.marks { - marks = []mujica.SliderMark{{Value: 0, Label: "£0"}, {Value: 500, Label: "£500"}, {Value: 1000, Label: "£1000"}} + marks = []input.SliderMark{{Value: 0, Label: "£0"}, {Value: 500, Label: "£500"}, {Value: 1000, Label: "£1000"}} } ui.Column(c).Width(360).Children(func() { - opts := mujica.RangeSliderOptions{Max: 1000, Step: 50, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Price", + opts := input.RangeSliderOptions{Max: 1000, Step: 50, Marks: marks, ShowValue: s.showValue, Disabled: s.disabled, Label: "Price", Format: func(v float64) string { return fmt.Sprintf("£%.0f", v) }} - if mujica.RangeSlider(c, &s.low, &s.high, opts).Changed() { + if input.RangeSlider(c, &s.low, &s.high, opts).Changed() { log(fmt.Sprintf("Price: %v–%v", s.low, s.high)) } }) - ui.Text(c, fmt.Sprintf("Range: £%.0f – £%.0f", s.low, s.high)).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, fmt.Sprintf("Range: £%.0f – £%.0f", s.low, s.high)).TextColor(core.Tokens(c).TextMuted) }) } @@ -281,7 +282,7 @@ func choiceDue(c *ui.Context, at time.Time) bool { return true } -var choiceFruits = []mujica.SelectOption[string]{ +var choiceFruits = []input.SelectOption[string]{ {Value: "apple", Label: "Apple"}, {Value: "banana", Label: "Banana"}, {Value: "blueberry", Label: "Blueberry"}, {Value: "cherry", Label: "Cherry", Disabled: true}, {Value: "date", Label: "Date"}, {Value: "fig", Label: "Fig"}, } @@ -302,12 +303,12 @@ func selectDemo(c *ui.Context, log func(string)) { options = nil } choiceLabeled(c, "Fruit", func() { - if mujica.Select(c, &s.value, options, mujica.SelectOptions{Clearable: s.clearable, Disabled: s.disabled, Label: "Fruit"}).Changed() { + if input.Select(c, &s.value, options, input.SelectOptions{Clearable: s.clearable, Disabled: s.disabled, Label: "Fruit"}).Changed() { log(fmt.Sprintf("Fruit: %q", s.value)) } }) choiceLabeled(c, "Placeholder", func() { - if mujica.Select(c, &s.picked, choiceFruits, mujica.SelectOptions{Placeholder: "Pick a fruit", Clearable: true}).Changed() { + if input.Select(c, &s.picked, choiceFruits, input.SelectOptions{Placeholder: "Pick a fruit", Clearable: true}).Changed() { log(fmt.Sprintf("Picked: %q", s.picked)) } }) @@ -329,7 +330,7 @@ func multiSelectDemo(c *ui.Context, log func(string)) { limit = 3 } choiceLabeled(c, "Fruits", func() { - r := mujica.MultiSelect(c, &s.values, choiceFruits, mujica.MultiSelectOptions{Max: limit, MaxTags: 2, Disabled: s.disable, Label: "Fruits"}) + r := input.MultiSelect(c, &s.values, choiceFruits, input.MultiSelectOptions{Max: limit, MaxTags: 2, Disabled: s.disable, Label: "Fruits"}) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("Fruits: %v", s.values)) @@ -368,7 +369,7 @@ func comboboxDemo(c *ui.Context, log func(string)) { options, loading = s.results, s.asked != s.query } choiceLabeled(c, "Typeface", func() { - r := mujica.Combobox(c, &s.value, options, mujica.ComboboxOptions{AllowCustom: s.custom, ExternalFilter: s.remote, Loading: loading, Label: "Typeface"}) + r := input.Combobox(c, &s.value, options, input.ComboboxOptions{AllowCustom: s.custom, ExternalFilter: s.remote, Loading: loading, Label: "Typeface"}) if r.QueryChanged() { s.query, s.at = r.Query(), c.Now() log(fmt.Sprintf("query: %q", s.query)) @@ -379,13 +380,13 @@ func comboboxDemo(c *ui.Context, log func(string)) { }) disabled := "Didot" choiceLabeled(c, "Disabled", func() { - mujica.Combobox(c, &disabled, choiceFonts, mujica.ComboboxOptions{Disabled: true}) + input.Combobox(c, &disabled, choiceFonts, input.ComboboxOptions{Disabled: true}) }) }) } // choiceAtlas is the cascader demo's pretend server data. -var choiceAtlas = map[string][]mujica.CascaderNode{ +var choiceAtlas = map[string][]input.CascaderNode{ "": {{Value: "eu", Label: "Europe"}, {Value: "as", Label: "Asia"}, {Value: "aq", Label: "Antarctica", Disabled: true}}, "eu": {{Value: "fr", Label: "France"}, {Value: "it", Label: "Italy"}}, "as": {{Value: "jp", Label: "Japan"}, {Value: "cn", Label: "China"}}, @@ -398,34 +399,34 @@ var choiceAtlas = map[string][]mujica.CascaderNode{ func cascaderDemo(c *ui.Context, log func(string)) { type state struct { path []string - levels map[string]mujica.CascaderLevel + levels map[string]input.CascaderLevel pending map[string]time.Time anyLevel, fail, ds bool } s := *ui.Local(c.Root(), "039", func() *state { - return &state{levels: map[string]mujica.CascaderLevel{"": {Nodes: choiceAtlas[""]}}, pending: map[string]time.Time{}} + return &state{levels: map[string]input.CascaderLevel{"": {Nodes: choiceAtlas[""]}}, pending: map[string]time.Time{}} }) for key, at := range s.pending { if choiceDue(c, at) { delete(s.pending, key) if s.fail { - s.levels[key] = mujica.CascaderLevel{Status: mujica.CascaderFailed, Err: errors.New("server busy")} + s.levels[key] = input.CascaderLevel{Status: input.CascaderFailed, Err: errors.New("server busy")} log(fmt.Sprintf("load %q failed", key)) continue } - s.levels[key] = mujica.CascaderLevel{Nodes: choiceAtlas[key]} + s.levels[key] = input.CascaderLevel{Nodes: choiceAtlas[key]} log(fmt.Sprintf("loaded %q", key)) } } - level := func(p []string) mujica.CascaderLevel { + level := func(p []string) input.CascaderLevel { if lv, ok := s.levels[strings.Join(p, "/")]; ok { return lv } - return mujica.CascaderLevel{Status: mujica.CascaderIdle} + return input.CascaderLevel{Status: input.CascaderIdle} } load := func(p []string) { key := strings.Join(p, "/") - s.levels[key] = mujica.CascaderLevel{Status: mujica.CascaderLoading} + s.levels[key] = input.CascaderLevel{Status: input.CascaderLoading} s.pending[key] = c.Now() log(fmt.Sprintf("load %q", key)) } @@ -435,8 +436,8 @@ func cascaderDemo(c *ui.Context, log func(string)) { choiceFlag(c, &s.ds, "Disabled", log) }, func() { choiceLabeled(c, "City (children load on demand)", func() { - opts := mujica.CascaderOptions{Level: level, Load: load, AnyLevel: s.anyLevel, Disabled: s.ds, Label: "City"} - r := mujica.Cascader(c, &s.path, opts) + opts := input.CascaderOptions{Level: level, Load: load, AnyLevel: s.anyLevel, Disabled: s.ds, Label: "City"} + r := input.Cascader(c, &s.path, opts) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("path: %v", s.path)) @@ -445,12 +446,12 @@ func cascaderDemo(c *ui.Context, log func(string)) { }) } -var choiceRoles = []mujica.TreeSelectNode{ - {ID: "court", Label: "Court", Children: []mujica.TreeSelectNode{ +var choiceRoles = []input.TreeSelectNode{ + {ID: "court", Label: "Court", Children: []input.TreeSelectNode{ {ID: "king", Label: "King"}, {ID: "queen", Label: "Queen"}, {ID: "jester", Label: "Jester", Disabled: true}, - {ID: "guard", Label: "Royal guard", Children: []mujica.TreeSelectNode{{ID: "captain", Label: "Captain"}, {ID: "archer", Label: "Archer"}}}, + {ID: "guard", Label: "Royal guard", Children: []input.TreeSelectNode{{ID: "captain", Label: "Captain"}, {ID: "archer", Label: "Archer"}}}, }}, - {ID: "church", Label: "Church", Children: []mujica.TreeSelectNode{{ID: "bishop", Label: "Bishop"}, {ID: "monk", Label: "Monk"}}}, + {ID: "church", Label: "Church", Children: []input.TreeSelectNode{{ID: "bishop", Label: "Bishop"}, {ID: "monk", Label: "Monk"}}}, {ID: "merchant", Label: "Merchant"}, } @@ -461,20 +462,20 @@ func treeSelectDemo(c *ui.Context, log func(string)) { } s := *ui.Local(c.Root(), "040", func() *state { return &state{sel: []string{"queen", "bishop"}, multiple: true} }) choiceDemo(c, func() { - if mujica.Switch(c, &s.multiple, "Multiple", mujica.SwitchOptions{}).Changed() { + if input.Switch(c, &s.multiple, "Multiple", input.SwitchOptions{}).Changed() { s.sel = s.sel[:min(len(s.sel), 1)] log(fmt.Sprintf("Multiple = %v", s.multiple)) } choiceFlag(c, &s.independent, "Independent rule", log) choiceFlag(c, &s.disabled, "Disabled", log) }, func() { - rule := mujica.TreeSelectCascade + rule := input.TreeSelectCascade if s.independent { - rule = mujica.TreeSelectIndependent + rule = input.TreeSelectIndependent } choiceLabeled(c, "Roles", func() { - opts := mujica.TreeSelectOptions{Multiple: s.multiple, Rule: rule, MaxTags: 2, Disabled: s.disabled, Label: "Roles"} - r := mujica.TreeSelect(c, &s.sel, choiceRoles, opts) + opts := input.TreeSelectOptions{Multiple: s.multiple, Rule: rule, MaxTags: 2, Disabled: s.disabled, Label: "Roles"} + r := input.TreeSelect(c, &s.sel, choiceRoles, opts) r.Element.Width(320) if r.Changed() { log(fmt.Sprintf("Roles: %v", s.sel)) diff --git a/examples/gallery/pages_choice_test.go b/examples/gallery/pages_choice_test.go index 9f22100..fd884cf 100644 --- a/examples/gallery/pages_choice_test.go +++ b/examples/gallery/pages_choice_test.go @@ -6,7 +6,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // choiceDemoTester runs the demo of page id at text scale and returns its tester and event log. @@ -18,7 +18,7 @@ func choiceDemoTester(t *testing.T, id string, scale float32) (*ui.Tester, *[]st } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mujica.Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Scroll(c).Fill().Padding(24).Children(func() { choicePages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) diff --git a/examples/gallery/pages_code.go b/examples/gallery/pages_code.go index 9f9be1e..57d6aa9 100644 --- a/examples/gallery/pages_code.go +++ b/examples/gallery/pages_code.go @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const codeCategory = "Terminal & Code" @@ -57,7 +59,7 @@ var codePages = []page{ // codeDemoCheck is a switch that logs its changes. func codeDemoCheck(c *ui.Context, v *bool, label string, log func(string)) { - if mujica.Switch(c, v, label, mujica.SwitchOptions{}).Changed() { + if input.Switch(c, v, label, input.SwitchOptions{}).Changed() { log(fmt.Sprintf("%s: %v", label, *v)) } } @@ -68,7 +70,7 @@ func codeDemoControls(c *ui.Context, fn func()) { func codeTerminalDemo(c *ui.Context, log func(string)) { type state struct { - term mujica.TerminalState + term code.TerminalState line string disabled bool } @@ -80,13 +82,13 @@ func codeTerminalDemo(c *ui.Context, log func(string)) { st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - if mujica.Button(c, "Clear screen", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Clear screen", input.ButtonOptions{Variant: input.Secondary}).Clicked() { st.term.Write("\x1b[2J\x1b[H$ ") log("clear screen") } }) // A tiny echo shell stands in for the PTY. - mujica.Terminal(c, &st.term, mujica.TerminalOptions{Disabled: st.disabled, + code.Terminal(c, &st.term, code.TerminalOptions{Disabled: st.disabled, Input: func(data string) { log(fmt.Sprintf("input %q", data)) for _, r := range data { @@ -128,27 +130,27 @@ func codeShell(line string) string { func codeTerminalTabsDemo(c *ui.Context, log func(string)) { type state struct { - tabs []mujica.TerminalTab + tabs []code.TerminalTab active string next int disabled bool } s := ui.Local(c.Root(), "322", func() *state { - return &state{tabs: []mujica.TerminalTab{{ID: "1", Title: "zsh"}, {ID: "2", Title: "build", Busy: true}, {ID: "3", Title: "logs"}}, active: "1", next: 4} + return &state{tabs: []code.TerminalTab{{ID: "1", Title: "zsh"}, {ID: "2", Title: "build", Busy: true}, {ID: "3", Title: "logs"}}, active: "1", next: 4} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - if mujica.Button(c, "New terminal", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "New terminal", input.ButtonOptions{Variant: input.Secondary}).Clicked() { id := fmt.Sprint(st.next) st.next++ - st.tabs = append(st.tabs, mujica.TerminalTab{ID: id, Title: "shell " + id}) + st.tabs = append(st.tabs, code.TerminalTab{ID: id, Title: "shell " + id}) st.active = id log("new " + id) } }) - r := mujica.TerminalTabs(c, &st.tabs, &st.active, mujica.TerminalTabsOptions{Disabled: st.disabled}, func(id string) { - ui.Text(c, "Terminal "+id+" content").TextColor(mujica.Tokens(c).TextMuted) + r := code.TerminalTabs(c, &st.tabs, &st.active, code.TerminalTabsOptions{Disabled: st.disabled}, func(id string) { + ui.Text(c, "Terminal "+id+" content").TextColor(core.Tokens(c).TextMuted) }) r.Element.Width(560) if r.Changed { @@ -164,8 +166,8 @@ func codeTerminalTabsDemo(c *ui.Context, log func(string)) { func codeTerminalSearchDemo(c *ui.Context, log func(string)) { type state struct { - term mujica.TerminalState - search mujica.TerminalSearchState + term code.TerminalState + search code.TerminalSearchState disabled bool } s := ui.Local(c.Root(), "323", func() *state { @@ -182,62 +184,62 @@ func codeTerminalSearchDemo(c *ui.Context, log func(string)) { st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) ui.Column(c).Width(600).Gap(8).Children(func() { - if mujica.TerminalSearch(c, &st.term, &st.search, mujica.TerminalSearchOptions{Disabled: st.disabled}).Changed() { + if code.TerminalSearch(c, &st.term, &st.search, code.TerminalSearchOptions{Disabled: st.disabled}).Changed() { log(fmt.Sprintf("query %q, match %d", st.search.Query, st.search.Current+1)) } - mujica.Terminal(c, &st.term, mujica.TerminalOptions{}).Height(220) + code.Terminal(c, &st.term, code.TerminalOptions{}).Height(220) }) } func codeCommandHistoryDemo(c *ui.Context, log func(string)) { type state struct { - entries []mujica.HistoryEntry - s mujica.CommandHistoryState + entries []code.HistoryEntry + s code.CommandHistoryState disabled bool } s := ui.Local(c.Root(), "324", func() *state { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - var e []mujica.HistoryEntry + var e []code.HistoryEntry for i, cmd := range []string{"git pull", "go build ./...", "go test ./...", "make seal", "ls -la", "git status"} { - e = append(e, mujica.HistoryEntry{Command: cmd, Time: at.Add(time.Duration(i) * time.Minute)}) + e = append(e, code.HistoryEntry{Command: cmd, Time: at.Add(time.Duration(i) * time.Minute)}) } e[3].ExitCode = 2 return &state{entries: e} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - r := mujica.CommandHistory(c, st.entries, &st.s, mujica.CommandHistoryOptions{Disabled: st.disabled}) + r := code.CommandHistory(c, st.entries, &st.s, code.CommandHistoryOptions{Disabled: st.disabled}) r.Element.Size(480, 300) if r.Replayed != nil { log("replay " + r.Replayed.Command) - st.entries = append(st.entries, mujica.HistoryEntry{Command: r.Replayed.Command, Time: time.Now()}) + st.entries = append(st.entries, code.HistoryEntry{Command: r.Replayed.Command, Time: time.Now()}) } } func codeLogViewerDemo(c *ui.Context, log func(string)) { type state struct { - lines []mujica.LogLine - s mujica.LogViewerState + lines []code.LogLine + s code.LogViewerState disabled bool } s := ui.Local(c.Root(), "325", func() *state { at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - lines := make([]mujica.LogLine, 100_000) + lines := make([]code.LogLine, 100_000) words := []string{"ledger synced", "seal applied", "herald dispatched", "archive rotated"} for i := range lines { - lines[i] = mujica.LogLine{Time: at.Add(time.Duration(i) * time.Second), Level: mujica.LogLevel(i % 7 % 4), Text: fmt.Sprintf("%s #%d", words[i%4], i)} + lines[i] = code.LogLine{Time: at.Add(time.Duration(i) * time.Second), Level: code.LogLevel(i % 7 % 4), Text: fmt.Sprintf("%s #%d", words[i%4], i)} } return &state{lines: lines} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - if mujica.Button(c, "Append line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { - st.lines = append(st.lines, mujica.LogLine{Time: time.Now(), Level: mujica.LogWarn, Text: fmt.Sprintf("late seal #%d", len(st.lines))}) + if input.Button(c, "Append line", input.ButtonOptions{Variant: input.Secondary}).Clicked() { + st.lines = append(st.lines, code.LogLine{Time: time.Now(), Level: code.LogWarn, Text: fmt.Sprintf("late seal #%d", len(st.lines))}) log("append") } }) - r := mujica.LogViewer(c, st.lines, &st.s, mujica.LogViewerOptions{Disabled: st.disabled}) + r := code.LogViewer(c, st.lines, &st.s, code.LogViewerOptions{Disabled: st.disabled}) r.Element.Size(720, 360) if r.Changed() { log(fmt.Sprintf("hidden %v, follow %v", st.s.Hidden, st.s.Follow)) diff --git a/examples/gallery/pages_code_debug.go b/examples/gallery/pages_code_debug.go index a6e91a2..4ac18be 100644 --- a/examples/gallery/pages_code_debug.go +++ b/examples/gallery/pages_code_debug.go @@ -6,12 +6,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func codeSymbolDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.SymbolOutlineState + s code.SymbolOutlineState cursor float64 disabled bool } @@ -20,19 +23,19 @@ func codeSymbolDemo(c *ui.Context, log func(string)) { codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) ui.Text(c, fmt.Sprintf("Cursor line %.0f", st.cursor)) - if mujica.Slider(c, &st.cursor, mujica.SliderOptions{Min: 1, Max: 34, Step: 1}).Changed() { + if input.Slider(c, &st.cursor, input.SliderOptions{Min: 1, Max: 34, Step: 1}).Changed() { log(fmt.Sprintf("cursor %.0f", st.cursor)) } }) - symbols := []mujica.Symbol{ - {Name: "Decree", Kind: mujica.SymbolType, Line: 6, EndLine: 9, Detail: "struct", Children: []mujica.Symbol{ - {Name: "ID", Kind: mujica.SymbolField, Line: 7, EndLine: 7, Detail: "int"}, - {Name: "Title", Kind: mujica.SymbolField, Line: 8, EndLine: 8, Detail: "string"}, + symbols := []code.Symbol{ + {Name: "Decree", Kind: code.SymbolType, Line: 6, EndLine: 9, Detail: "struct", Children: []code.Symbol{ + {Name: "ID", Kind: code.SymbolField, Line: 7, EndLine: 7, Detail: "int"}, + {Name: "Title", Kind: code.SymbolField, Line: 8, EndLine: 8, Detail: "string"}, }}, - {Name: "Seal", Kind: mujica.SymbolFunction, Line: 13, EndLine: 21, Detail: "(d Decree) (string, error)"}, - {Name: "Herald", Kind: mujica.SymbolFunction, Line: 23, EndLine: 31, Detail: "(ds []Decree) int"}, + {Name: "Seal", Kind: code.SymbolFunction, Line: 13, EndLine: 21, Detail: "(d Decree) (string, error)"}, + {Name: "Herald", Kind: code.SymbolFunction, Line: 23, EndLine: 31, Detail: "(ds []Decree) int"}, } - r := mujica.SymbolOutline(c, symbols, &st.s, mujica.SymbolOutlineOptions{CursorLine: int(st.cursor), Disabled: st.disabled}) + r := code.SymbolOutline(c, symbols, &st.s, code.SymbolOutlineOptions{CursorLine: int(st.cursor), Disabled: st.disabled}) r.Element.Size(420, 320) if r.Jumped != nil { log(fmt.Sprintf("jump to %s, line %d", r.Jumped.Name, r.Jumped.Line)) @@ -43,33 +46,33 @@ func codeCompletionDemo(c *ui.Context, log func(string)) { type state struct { text string open bool - s mujica.CompletionMenuState + s code.CompletionMenuState disabled bool } s := ui.Local(c.Root(), "334", func() *state { return &state{} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - all := []mujica.Completion{ - {Label: "Seal", Kind: mujica.SymbolFunction, Detail: "func(d Decree) (string, error)", Doc: "Seal stamps a decree with the royal wax."}, - {Label: "Search", Kind: mujica.SymbolFunction, Detail: "func(q string) []Decree"}, - {Label: "Season", Kind: mujica.SymbolConstant, Detail: "= 4"}, - {Label: "Herald", Kind: mujica.SymbolFunction, Detail: "func(ds []Decree) int"}, - {Label: "Decree", Kind: mujica.SymbolType, Detail: "struct", Doc: "Decree is an order of the court."}, + all := []code.Completion{ + {Label: "Seal", Kind: code.SymbolFunction, Detail: "func(d Decree) (string, error)", Doc: "Seal stamps a decree with the royal wax."}, + {Label: "Search", Kind: code.SymbolFunction, Detail: "func(q string) []Decree"}, + {Label: "Season", Kind: code.SymbolConstant, Detail: "= 4"}, + {Label: "Herald", Kind: code.SymbolFunction, Detail: "func(ds []Decree) int"}, + {Label: "Decree", Kind: code.SymbolType, Detail: "struct", Doc: "Decree is an order of the court."}, } word := st.text[strings.LastIndexAny(st.text, " .(")+1:] - var items []mujica.Completion + var items []code.Completion for _, it := range all { if word != "" && strings.HasPrefix(strings.ToLower(it.Label), strings.ToLower(word)) { items = append(items, it) } } ui.Column(c).Width(420).Gap(8).Children(func() { - ui.Text(c, "Type “se” or “de” to see suggestions.").TextColor(mujica.Tokens(c).TextMuted) - in := mujica.TextInput(c, &st.text, mujica.TextInputOptions{Label: "Code", Placeholder: "court.", Disabled: st.disabled}) + ui.Text(c, "Type “se” or “de” to see suggestions.").TextColor(core.Tokens(c).TextMuted) + in := input.TextInput(c, &st.text, input.TextInputOptions{Label: "Code", Placeholder: "court.", Disabled: st.disabled}) if in.Changed() { st.open = len(items) > 0 } - r := mujica.CompletionMenu(c, in.Input, &st.open, items, &st.s, mujica.CompletionMenuOptions{Disabled: st.disabled}) + r := code.CompletionMenu(c, in.Input, &st.open, items, &st.s, code.CompletionMenuOptions{Disabled: st.disabled}) if r.Accepted != nil { st.text = st.text[:len(st.text)-len(word)] + r.Accepted.Label log("accept " + r.Accepted.Label) @@ -79,37 +82,37 @@ func codeCompletionDemo(c *ui.Context, log func(string)) { func codeDebugDemo(c *ui.Context, log func(string)) { type state struct { - dbg mujica.DebugState + dbg code.DebugState disabled bool } - s := ui.Local(c.Root(), "335", func() *state { return &state{dbg: mujica.DebugPaused} }) + s := ui.Local(c.Root(), "335", func() *state { return &state{dbg: code.DebugPaused} }) st := *s names := []string{"Stopped", "Running", "Paused"} codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) ui.Text(c, "State: "+names[st.dbg]) }) - a := mujica.DebugToolbar(c, st.dbg, mujica.DebugToolbarOptions{Disabled: st.disabled}).Action + a := code.DebugToolbar(c, st.dbg, code.DebugToolbarOptions{Disabled: st.disabled}).Action switch a { - case mujica.DebugNone: + case code.DebugNone: return - case mujica.DebugContinue, mujica.DebugRestart: - st.dbg = mujica.DebugRunning - case mujica.DebugPause, mujica.DebugStepOver, mujica.DebugStepInto, mujica.DebugStepOut: - st.dbg = mujica.DebugPaused - case mujica.DebugStop: - st.dbg = mujica.DebugStopped + case code.DebugContinue, code.DebugRestart: + st.dbg = code.DebugRunning + case code.DebugPause, code.DebugStepOver, code.DebugStepInto, code.DebugStepOut: + st.dbg = code.DebugPaused + case code.DebugStop: + st.dbg = code.DebugStopped } log(fmt.Sprintf("action %d, now %s", a, names[st.dbg])) } func codeBreakpointDemo(c *ui.Context, log func(string)) { type state struct { - bps []mujica.Breakpoint + bps []code.Breakpoint disabled bool } s := ui.Local(c.Root(), "336", func() *state { - return &state{bps: []mujica.Breakpoint{ + return &state{bps: []code.Breakpoint{ {ID: "1", File: "court/seal.go", Line: 14, Enabled: true}, {ID: "2", File: "court/ledger.go", Line: 40, Condition: "n > 3"}, {ID: "3", File: "main.go", Line: 9, Enabled: true}, @@ -117,7 +120,7 @@ func codeBreakpointDemo(c *ui.Context, log func(string)) { }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - r := mujica.BreakpointList(c, &st.bps, mujica.BreakpointListOptions{Disabled: st.disabled}) + r := code.BreakpointList(c, &st.bps, code.BreakpointListOptions{Disabled: st.disabled}) r.Element.Size(480, 240) for _, e := range []struct{ what, id string }{{"toggled", r.Toggled}, {"condition", r.Edited}, {"open", r.Opened}} { if e.id != "" { @@ -128,21 +131,21 @@ func codeBreakpointDemo(c *ui.Context, log func(string)) { func codeCallStackDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.CallStackState + s code.CallStackState disabled bool } - s := ui.Local(c.Root(), "337", func() *state { return &state{s: mujica.CallStackState{Current: "f1"}} }) + s := ui.Local(c.Root(), "337", func() *state { return &state{s: code.CallStackState{Current: "f1"}} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - threads := []mujica.DebugThread{ - {ID: "t1", Name: "main", Frames: []mujica.StackFrame{ + threads := []code.DebugThread{ + {ID: "t1", Name: "main", Frames: []code.StackFrame{ {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, {ID: "f2", Name: "court.Herald", File: "court/seal.go", Line: 27}, {ID: "f3", Name: "main.main", File: "main.go", Line: 9}, }}, - {ID: "t2", Name: "ledger writer", Frames: []mujica.StackFrame{{ID: "f4", Name: "ledger.Flush", File: "ledger/write.go", Line: 51}}}, + {ID: "t2", Name: "ledger writer", Frames: []code.StackFrame{{ID: "f4", Name: "ledger.Flush", File: "ledger/write.go", Line: 51}}}, } - r := mujica.CallStack(c, threads, &st.s, mujica.CallStackOptions{Disabled: st.disabled}) + r := code.CallStack(c, threads, &st.s, code.CallStackOptions{Disabled: st.disabled}) r.Element.Size(440, 300) if r.Changed() { log("frame " + st.s.Current) @@ -151,13 +154,13 @@ func codeCallStackDemo(c *ui.Context, log func(string)) { func codeVariablesDemo(c *ui.Context, log func(string)) { type state struct { - vars []mujica.Variable - s mujica.VariablesPanelState + vars []code.Variable + s code.VariablesPanelState disabled bool } s := ui.Local(c.Root(), "338", func() *state { - return &state{vars: []mujica.Variable{ - {Name: "d", Type: "court.Decree", Value: "{…}", Children: []mujica.Variable{ + return &state{vars: []code.Variable{ + {Name: "d", Type: "court.Decree", Value: "{…}", Children: []code.Variable{ {Name: "ID", Value: "42", Type: "int", Changed: true}, {Name: "Title", Value: `"Of seals"`, Type: "string"}, }}, @@ -167,7 +170,7 @@ func codeVariablesDemo(c *ui.Context, log func(string)) { }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - r := mujica.VariablesPanel(c, st.vars, &st.s, mujica.VariablesPanelOptions{Disabled: st.disabled}) + r := code.VariablesPanel(c, st.vars, &st.s, code.VariablesPanelOptions{Disabled: st.disabled}) r.Element.Size(440, 260) if r.Edited != nil { log(fmt.Sprintf("set %s = %s", r.Edited.Path, r.Edited.Value)) @@ -176,7 +179,7 @@ func codeVariablesDemo(c *ui.Context, log func(string)) { } // codeDemoSet writes value at the dotted path. -func codeDemoSet(vars []mujica.Variable, path []string, value string) { +func codeDemoSet(vars []code.Variable, path []string, value string) { for i := range vars { if vars[i].Name != path[0] { continue @@ -191,14 +194,14 @@ func codeDemoSet(vars []mujica.Variable, path []string, value string) { func codeHexDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.HexViewerState + s code.HexViewerState disabled bool } s := ui.Local(c.Root(), "339", func() *state { return &state{} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) data := []byte(strings.Join(codeDemoSource, "\n")) - r := mujica.HexViewer(c, data, &st.s, mujica.HexViewerOptions{Disabled: st.disabled}) + r := code.HexViewer(c, data, &st.s, code.HexViewerOptions{Disabled: st.disabled}) r.Element.Size(720, 300) if r.Changed() { a, b := st.s.Selection() @@ -208,7 +211,7 @@ func codeHexDemo(c *ui.Context, log func(string)) { func codeFlamegraphDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.FlamegraphState + s code.FlamegraphState status string disabled bool } @@ -216,22 +219,22 @@ func codeFlamegraphDemo(c *ui.Context, log func(string)) { st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - opts := []mujica.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}} - if mujica.Select(c, &st.status, opts, mujica.SelectOptions{Label: "Status"}).Changed() { + opts := []input.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}} + if input.Select(c, &st.status, opts, input.SelectOptions{Label: "Status"}).Changed() { log("status " + st.status) } }) - root := &mujica.FlameNode{Name: "main", Value: 240, Children: []mujica.FlameNode{ - {Name: "court.Herald", Value: 150, Children: []mujica.FlameNode{ - {Name: "court.Seal", Value: 110, Children: []mujica.FlameNode{{Name: "wax.Melt", Value: 70}, {Name: "ink.Dry", Value: 25}}}, + root := &code.FlameNode{Name: "main", Value: 240, Children: []code.FlameNode{ + {Name: "court.Herald", Value: 150, Children: []code.FlameNode{ + {Name: "court.Seal", Value: 110, Children: []code.FlameNode{{Name: "wax.Melt", Value: 70}, {Name: "ink.Dry", Value: 25}}}, {Name: "ledger.Write", Value: 30}, }}, - {Name: "archive.Load", Value: 60, Children: []mujica.FlameNode{{Name: "json.Decode", Value: 45}}}, + {Name: "archive.Load", Value: 60, Children: []code.FlameNode{{Name: "json.Decode", Value: 45}}}, {Name: "gc", Value: 20}, }} - status := map[string]mujica.DataStatus{"Ready": mujica.DataReady, "Loading": mujica.DataLoading, "Failed": mujica.DataFailed}[st.status] + status := map[string]data.DataStatus{"Ready": data.DataReady, "Loading": data.DataLoading, "Failed": data.DataFailed}[st.status] ui.Column(c).Width(680).Children(func() { - if mujica.Flamegraph(c, root, &st.s, mujica.FlamegraphOptions{Unit: "ms", Status: status, Error: "Profile could not be read", Disabled: st.disabled}).Changed() { + if code.Flamegraph(c, root, &st.s, code.FlamegraphOptions{Unit: "ms", Status: status, Error: "Profile could not be read", Disabled: st.disabled}).Changed() { log(fmt.Sprintf("zoom %v focus %v", st.s.Zoom, st.s.Focus)) } }) diff --git a/examples/gallery/pages_code_snippets.go b/examples/gallery/pages_code_snippets.go index 3cff0a4..ce2c63c 100644 --- a/examples/gallery/pages_code_snippets.go +++ b/examples/gallery/pages_code_snippets.go @@ -2,22 +2,22 @@ package main // Snippets are the bodies of the Example functions in example_code_test.go. -const codeTerminalCode = `var term mujica.TerminalState +const codeTerminalCode = `var term code.TerminalState term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{ + core.Use(c, core.Settings{}) + code.Terminal(c, &term, code.TerminalOptions{ Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, }).Size(380, 200) } ui.Render(view, 400, 300, 1)` -const codeTerminalTabsCode = `tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} +const codeTerminalTabsCode = `tabs := []code.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} active := "a" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { + core.Use(c, core.Settings{}) + r := code.TerminalTabs(c, &tabs, &active, code.TerminalTabsOptions{}, func(id string) { ui.Text(c, "terminal "+id) }) if r.Closed != "" { @@ -26,23 +26,23 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeTerminalSearchCode = `var term mujica.TerminalState +const codeTerminalSearchCode = `var term code.TerminalState term.Write("build ok\r\nerror: missing seal\r\n") -var search mujica.TerminalSearchState +var search code.TerminalSearchState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(8).Children(func() { - mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) - mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) + code.TerminalSearch(c, &term, &search, code.TerminalSearchOptions{}) + code.Terminal(c, &term, code.TerminalOptions{}).Height(160) }) } ui.Render(view, 400, 300, 1)` -const codeCommandHistoryCode = `history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} -var state mujica.CommandHistoryState +const codeCommandHistoryCode = `history := []code.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} +var state code.CommandHistoryState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) + core.Use(c, core.Settings{}) + r := code.CommandHistory(c, history, &state, code.CommandHistoryOptions{}) r.Element.Height(240) if r.Replayed != nil { fmt.Println("run", r.Replayed.Command) @@ -50,19 +50,19 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeLogViewerCode = `lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} -var state mujica.LogViewerState +const codeLogViewerCode = `lines := []code.LogLine{{Level: code.LogInfo, Text: "court opened"}, {Level: code.LogError, Text: "seal missing"}} +var state code.LogViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + code.LogViewer(c, lines, &state, code.LogViewerOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1)` -const codeProcessListCode = `procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} -var state mujica.ProcessListState +const codeProcessListCode = `procs := []code.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} +var state code.ProcessListState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) + core.Use(c, core.Settings{}) + r := code.ProcessList(c, procs, &state, code.ProcessListOptions{}) r.Element.Height(240) if r.End != nil { fmt.Println("kill", r.End.PID) @@ -71,38 +71,38 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const codeCodeViewerCode = `lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} -var state mujica.CodeViewerState +var state code.CodeViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) + core.Use(c, core.Settings{}) + code.CodeViewer(c, lines, &state, code.CodeViewerOptions{Language: "go"}).Element.Height(240) } ui.Render(view, 400, 300, 1)` const codeMinimapCode = `lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} -view := mujica.MinimapView{Top: 0, Visible: 2} +view := code.MinimapView{Top: 0, Visible: 2} draw := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { + core.Use(c, core.Settings{}) + if code.Minimap(c, lines, &view, code.MinimapOptions{Language: "go"}).Changed() { fmt.Println("scroll editor to", view.Top) } } ui.Render(draw, 400, 300, 1)` const codeFindWidgetCode = `text := "the seal and the seal" -state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} +state := code.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { + core.Use(c, core.Settings{}) + if code.FindWidget(c, &text, &state, code.FindWidgetOptions{}).Replaced > 0 { fmt.Println(text) } } ui.Render(view, 400, 300, 1)` -const codeSearchPanelCode = `results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} -state := mujica.SearchPanelState{Query: "seal"} +const codeSearchPanelCode = `results := []code.SearchFileResult{{Path: "court/seal.go", Matches: []code.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} +state := code.SearchPanelState{Query: "seal"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.SearchPanel(c, results, &state, code.SearchPanelOptions{}) r.Element.Height(280) if r.Opened != nil { fmt.Println("open", r.Opened.Path, r.Opened.Line) @@ -110,11 +110,11 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeProblemsPanelCode = `problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} -var state mujica.ProblemsPanelState +const codeProblemsPanelCode = `problems := []code.Problem{{Severity: code.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} +var state code.ProblemsPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.ProblemsPanel(c, problems, &state, code.ProblemsPanelOptions{}) r.Element.Height(240) if r.Opened != nil { fmt.Println("go to", r.Opened.File, r.Opened.Line) @@ -122,11 +122,11 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeOutputPanelCode = `channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} -var state mujica.OutputPanelState +const codeOutputPanelCode = `channels := []code.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} +var state code.OutputPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.OutputPanel(c, channels, &state, code.OutputPanelOptions{}) r.Element.Height(240) if r.Cleared != "" { channels[state.Channel].Lines = nil @@ -134,13 +134,13 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeSymbolOutlineCode = `symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ - {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, +const codeSymbolOutlineCode = `symbols := []code.Symbol{{Name: "Court", Kind: code.SymbolType, Line: 3, EndLine: 40, Children: []code.Symbol{ + {Name: "Seal", Kind: code.SymbolMethod, Line: 10, EndLine: 20}, }}} -var state mujica.SymbolOutlineState +var state code.SymbolOutlineState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) + core.Use(c, core.Settings{}) + r := code.SymbolOutline(c, symbols, &state, code.SymbolOutlineOptions{CursorLine: 12}) r.Element.Height(240) if r.Jumped != nil { fmt.Println("go to line", r.Jumped.Line) @@ -150,33 +150,33 @@ ui.Render(view, 400, 300, 1)` const codeCompletionMenuCode = `text := "se" open := true -items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} -var state mujica.CompletionMenuState +items := []code.Completion{{Label: "Seal", Kind: code.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: code.SymbolFunction}} +var state code.CompletionMenuState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) - if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { + core.Use(c, core.Settings{}) + in := input.TextInput(c, &text, input.TextInputOptions{Label: "Code"}) + if r := code.CompletionMenu(c, in.Input, &open, items, &state, code.CompletionMenuOptions{}); r.Accepted != nil { text = r.Accepted.Label } } ui.Render(view, 400, 300, 1)` -const codeDebugToolbarCode = `state := mujica.DebugPaused +const codeDebugToolbarCode = `state := code.DebugPaused view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { - case mujica.DebugContinue: - state = mujica.DebugRunning - case mujica.DebugStop: - state = mujica.DebugStopped + core.Use(c, core.Settings{}) + switch code.DebugToolbar(c, state, code.DebugToolbarOptions{}).Action { + case code.DebugContinue: + state = code.DebugRunning + case code.DebugStop: + state = code.DebugStopped } } ui.Render(view, 400, 300, 1)` -const codeBreakpointListCode = `bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} +const codeBreakpointListCode = `bps := []code.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) + core.Use(c, core.Settings{}) + r := code.BreakpointList(c, &bps, code.BreakpointListOptions{}) r.Element.Height(200) if r.Toggled != "" || r.Edited != "" { fmt.Println("sync breakpoints with the debugger") @@ -184,14 +184,14 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeCallStackCode = `threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ +const codeCallStackCode = `threads := []code.DebugThread{{ID: "t1", Name: "main", Frames: []code.StackFrame{ {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, }}} -state := mujica.CallStackState{Current: "f1"} +state := code.CallStackState{Current: "f1"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) + core.Use(c, core.Settings{}) + r := code.CallStack(c, threads, &state, code.CallStackOptions{}) r.Element.Height(200) if r.Changed() { fmt.Println("show frame", state.Current) @@ -199,11 +199,11 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` -const codeVariablesPanelCode = `vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} -var state mujica.VariablesPanelState +const codeVariablesPanelCode = `vars := []code.Variable{{Name: "decree", Value: "{…}", Children: []code.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} +var state code.VariablesPanelState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) + core.Use(c, core.Settings{}) + r := code.VariablesPanel(c, vars, &state, code.VariablesPanelOptions{}) r.Element.Height(200) if r.Edited != nil { fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) @@ -212,19 +212,19 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const codeHexViewerCode = `data := []byte("MUJICA court ledger\x00\x01") -var state mujica.HexViewerState +var state code.HexViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) + core.Use(c, core.Settings{}) + code.HexViewer(c, data, &state, code.HexViewerOptions{}).Element.Height(160) } ui.Render(view, 400, 300, 1)` -const codeFlamegraphCode = `root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ +const codeFlamegraphCode = `root := &code.FlameNode{Name: "main", Value: 100, Children: []code.FlameNode{ {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, }} -var state mujica.FlamegraphState +var state code.FlamegraphState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) + core.Use(c, core.Settings{}) + code.Flamegraph(c, root, &state, code.FlamegraphOptions{Unit: "ms"}) } ui.Render(view, 400, 300, 1)` diff --git a/examples/gallery/pages_code_tools.go b/examples/gallery/pages_code_tools.go index 16357dc..9ff6ff5 100644 --- a/examples/gallery/pages_code_tools.go +++ b/examples/gallery/pages_code_tools.go @@ -6,28 +6,30 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func codeProcessListDemo(c *ui.Context, log func(string)) { type state struct { - procs []mujica.Process - s mujica.ProcessListState + procs []code.Process + s code.ProcessListState disabled bool } s := ui.Local(c.Root(), "326", func() *state { - return &state{procs: []mujica.Process{ + return &state{procs: []code.Process{ {PID: 101, Name: "chancellor", CPU: 3.5, Memory: 200 << 20}, {PID: 7, Name: "archivist", CPU: 42.1, Memory: 50 << 20}, {PID: 330, Name: "herald", CPU: 0.2, Memory: 2 << 30}, {PID: 412, Name: "scribe", CPU: 12.8, Memory: 96 << 20}, }} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - r := mujica.ProcessList(c, st.procs, &st.s, mujica.ProcessListOptions{Disabled: st.disabled}) + r := code.ProcessList(c, st.procs, &st.s, code.ProcessListOptions{Disabled: st.disabled}) r.Element.Size(560, 260) if r.End != nil { log(fmt.Sprintf("end %s (PID %d)", r.End.Name, r.End.PID)) - var kept []mujica.Process + var kept []code.Process for _, p := range st.procs { if p.PID != r.End.PID { kept = append(kept, p) @@ -71,7 +73,7 @@ func Herald(ds []Decree) int { func codeCodeViewerDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.CodeViewerState + s code.CodeViewerState lang string disabled bool } @@ -79,16 +81,16 @@ func codeCodeViewerDemo(c *ui.Context, log func(string)) { st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - langs := []mujica.SelectOption[string]{{Value: "go", Label: "Go"}, {Value: "", Label: "Plain"}} - if mujica.Select(c, &st.lang, langs, mujica.SelectOptions{Label: "Language"}).Changed() { + langs := []input.SelectOption[string]{{Value: "go", Label: "Go"}, {Value: "", Label: "Plain"}} + if input.Select(c, &st.lang, langs, input.SelectOptions{Label: "Language"}).Changed() { log("language " + st.lang) } - if mujica.Button(c, "Go to line 20", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Go to line 20", input.ButtonOptions{Variant: input.Secondary}).Clicked() { st.s.GoTo(19) log("go to 20") } }) - r := mujica.CodeViewer(c, codeDemoSource, &st.s, mujica.CodeViewerOptions{Language: st.lang, Label: "Source", Disabled: st.disabled}) + r := code.CodeViewer(c, codeDemoSource, &st.s, code.CodeViewerOptions{Language: st.lang, Label: "Source", Disabled: st.disabled}) r.Element.Size(600, 360) if r.Changed() { log(fmt.Sprintf("line %d", st.s.Line()+1)) @@ -97,8 +99,8 @@ func codeCodeViewerDemo(c *ui.Context, log func(string)) { func codeMinimapDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.CodeViewerState - view mujica.MinimapView + s code.CodeViewerState + view code.MinimapView lines []string disabled bool } @@ -112,10 +114,10 @@ func codeMinimapDemo(c *ui.Context, log func(string)) { st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) first, last := st.s.Visible() - st.view = mujica.MinimapView{Top: first, Visible: max(last-first+1, 1)} + st.view = code.MinimapView{Top: first, Visible: max(last-first+1, 1)} ui.Row(c).Gap(8).Height(360).AlignItems(ui.Stretch).Children(func() { - mujica.CodeViewer(c, st.lines, &st.s, mujica.CodeViewerOptions{Language: "go"}).Element.Width(520) - if mujica.Minimap(c, st.lines, &st.view, mujica.MinimapOptions{Language: "go", Disabled: st.disabled}).Changed() { + code.CodeViewer(c, st.lines, &st.s, code.CodeViewerOptions{Language: "go"}).Element.Width(520) + if code.Minimap(c, st.lines, &st.view, code.MinimapOptions{Language: "go", Disabled: st.disabled}).Changed() { st.s.ScrollTo(st.view.Top) log(fmt.Sprintf("jump to line %d", st.view.Top+1)) } @@ -125,11 +127,11 @@ func codeMinimapDemo(c *ui.Context, log func(string)) { func codeFindWidgetDemo(c *ui.Context, log func(string)) { type state struct { text string - s mujica.FindState + s code.FindState readOnly, disabled bool } s := ui.Local(c.Root(), "329", func() *state { - return &state{text: "The seal of the court; seals are kept sealed. Seal 42 and seal 7.", s: mujica.FindState{Query: "seal"}} + return &state{text: "The seal of the court; seals are kept sealed. Seal 42 and seal 7.", s: code.FindState{Query: "seal"}} }) st := *s codeDemoControls(c, func() { @@ -137,7 +139,7 @@ func codeFindWidgetDemo(c *ui.Context, log func(string)) { codeDemoCheck(c, &st.disabled, "Disabled", log) }) ui.Column(c).Width(620).Gap(12).Children(func() { - r := mujica.FindWidget(c, &st.text, &st.s, mujica.FindWidgetOptions{ReadOnly: st.readOnly, Disabled: st.disabled}) + r := code.FindWidget(c, &st.text, &st.s, code.FindWidgetOptions{ReadOnly: st.readOnly, Disabled: st.disabled}) if r.Moved { log(fmt.Sprintf("%d matches, at %d", len(st.s.Matches()), st.s.Current+1)) } @@ -148,7 +150,7 @@ func codeFindWidgetDemo(c *ui.Context, log func(string)) { at := 0 for i, m := range st.s.Matches() { spans = append(spans, ui.Span{Text: st.text[at:m[0]]}) - sp := ui.Span{Text: st.text[m[0]:m[1]], Background: mujica.Tokens(c).Selection, Underline: true} + sp := ui.Span{Text: st.text[m[0]:m[1]], Background: core.Tokens(c).Selection, Underline: true} if i == st.s.Current { sp.Weight = 700 } @@ -162,11 +164,11 @@ func codeFindWidgetDemo(c *ui.Context, log func(string)) { func codeSearchPanelDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.SearchPanelState - results []mujica.SearchFileResult + s code.SearchPanelState + results []code.SearchFileResult disabled bool } - s := ui.Local(c.Root(), "330", func() *state { return &state{s: mujica.SearchPanelState{Query: "seal"}} }) + s := ui.Local(c.Root(), "330", func() *state { return &state{s: code.SearchPanelState{Query: "seal"}} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) // The demo searches its own two files. @@ -174,7 +176,7 @@ func codeSearchPanelDemo(c *ui.Context, log func(string)) { if st.results == nil || st.s.Query != "" && len(st.results) == 0 { st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude) } - r := mujica.SearchPanel(c, st.results, &st.s, mujica.SearchPanelOptions{Disabled: st.disabled}) + r := code.SearchPanel(c, st.results, &st.s, code.SearchPanelOptions{Disabled: st.disabled}) r.Element.Size(560, 380) if r.Changed { st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude) @@ -186,8 +188,8 @@ func codeSearchPanelDemo(c *ui.Context, log func(string)) { } // codeDemoSearch finds q in the demo files. -func codeDemoSearch(files map[string][]string, q, include, exclude string) []mujica.SearchFileResult { - out := []mujica.SearchFileResult{} +func codeDemoSearch(files map[string][]string, q, include, exclude string) []code.SearchFileResult { + out := []code.SearchFileResult{} if q == "" { return out } @@ -195,10 +197,10 @@ func codeDemoSearch(files map[string][]string, q, include, exclude string) []muj if include != "" && !strings.Contains(path, strings.Trim(include, "*")) || exclude != "" && strings.Contains(path, strings.Trim(exclude, "*")) { continue } - f := mujica.SearchFileResult{Path: path} + f := code.SearchFileResult{Path: path} for i, line := range files[path] { if at := strings.Index(strings.ToLower(line), strings.ToLower(q)); at >= 0 { - f.Matches = append(f.Matches, mujica.SearchMatch{Line: i + 1, Text: line, Start: at, End: at + len(q)}) + f.Matches = append(f.Matches, code.SearchMatch{Line: i + 1, Text: line, Start: at, End: at + len(q)}) } } if len(f.Matches) > 0 { @@ -210,19 +212,19 @@ func codeDemoSearch(files map[string][]string, q, include, exclude string) []muj func codeProblemsDemo(c *ui.Context, log func(string)) { type state struct { - s mujica.ProblemsPanelState + s code.ProblemsPanelState disabled bool } s := ui.Local(c.Root(), "331", func() *state { return &state{} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) - problems := []mujica.Problem{ - {Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree", Source: "compiler"}, - {Severity: mujica.ProblemWarning, File: "court/seal.go", Line: 30, Col: 2, Message: "unused variable wax", Source: "vet"}, - {Severity: mujica.ProblemInfo, File: "docs/ledger.md", Line: 4, Col: 1, Message: "line is longer than 80", Source: "lint"}, - {Severity: mujica.ProblemError, File: "main.go", Line: 9, Col: 14, Message: "missing return", Source: "compiler"}, + problems := []code.Problem{ + {Severity: code.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree", Source: "compiler"}, + {Severity: code.ProblemWarning, File: "court/seal.go", Line: 30, Col: 2, Message: "unused variable wax", Source: "vet"}, + {Severity: code.ProblemInfo, File: "docs/ledger.md", Line: 4, Col: 1, Message: "line is longer than 80", Source: "lint"}, + {Severity: code.ProblemError, File: "main.go", Line: 9, Col: 14, Message: "missing return", Source: "compiler"}, } - r := mujica.ProblemsPanel(c, problems, &st.s, mujica.ProblemsPanelOptions{Disabled: st.disabled}) + r := code.ProblemsPanel(c, problems, &st.s, code.ProblemsPanelOptions{Disabled: st.disabled}) r.Element.Size(620, 320) if r.Opened != nil { log(fmt.Sprintf("go to %s:%d:%d", r.Opened.File, r.Opened.Line, r.Opened.Col)) @@ -231,8 +233,8 @@ func codeProblemsDemo(c *ui.Context, log func(string)) { func codeOutputDemo(c *ui.Context, log func(string)) { type state struct { - channels []mujica.OutputChannel - s mujica.OutputPanelState + channels []code.OutputChannel + s code.OutputPanelState disabled bool } s := ui.Local(c.Root(), "332", func() *state { @@ -240,19 +242,19 @@ func codeOutputDemo(c *ui.Context, log func(string)) { for i := range 300 { build = append(build, fmt.Sprintf("compiling unit %d of 300", i+1)) } - return &state{channels: []mujica.OutputChannel{{Name: "Build", Lines: build}, + return &state{channels: []code.OutputChannel{{Name: "Build", Lines: build}, {Name: "Tests", Lines: []string{"\x1b[32mPASS\x1b[0m court/seal", "\x1b[31mFAIL\x1b[0m court/ledger: expected 3 seals", "\x1b[1mok\x1b[0m 2 of 3 packages"}}}} }) st := *s codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) - if mujica.Button(c, "Write line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Write line", input.ButtonOptions{Variant: input.Secondary}).Clicked() { ch := &st.channels[st.s.Channel] ch.Lines = append(ch.Lines, fmt.Sprintf("line %d", len(ch.Lines)+1)) log("write to " + ch.Name) } }) - r := mujica.OutputPanel(c, st.channels, &st.s, mujica.OutputPanelOptions{Disabled: st.disabled}) + r := code.OutputPanel(c, st.channels, &st.s, code.OutputPanelOptions{Disabled: st.disabled}) r.Element.Size(560, 300) if r.Changed { log("channel " + st.channels[st.s.Channel].Name) diff --git a/examples/gallery/pages_data.go b/examples/gallery/pages_data.go index 310a332..71a1501 100644 --- a/examples/gallery/pages_data.go +++ b/examples/gallery/pages_data.go @@ -3,8 +3,9 @@ package main import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const dataCategory = "Data" @@ -39,18 +40,18 @@ var dataPages = []page{ // dataSection lays out a titled block of a demo. func dataSection(c *ui.Context, title string, fn func()) { ui.Column(c).Gap(12).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)) fn() }) } // dataStates lays labeled variants side by side. func dataStates(c *ui.Context, fn func(state func(name string, body func()))) { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Row(c).Gap(24).Wrap().AlignItems(ui.Start).Children(func() { fn(func(name string, body func()) { ui.Column(c).Gap(6).AlignItems(ui.Start).Children(func() { - ui.Text(c, name).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) body() }) }) @@ -64,18 +65,18 @@ func dataControls(c *ui.Context, fn func()) { var dataStatuses = []string{"Ready", "Loading", "Loading more", "Failed", "Empty"} // dataStatus maps a status choice to the status and whether rows show. -func dataStatus(s string) (mujica.DataStatus, bool) { +func dataStatus(s string) (data.DataStatus, bool) { switch s { case "Ready": - return mujica.DataReady, true + return data.DataReady, true case "Loading": - return mujica.DataLoading, false + return data.DataLoading, false case "Loading more": - return mujica.DataLoadingMore, true + return data.DataLoadingMore, true case "Failed": - return mujica.DataFailed, true + return data.DataFailed, true case "Empty": - return mujica.DataReady, false + return data.DataReady, false } panic("gallery: unknown status " + s) } diff --git a/examples/gallery/pages_data_code.go b/examples/gallery/pages_data_code.go index 2cde508..ea181fd 100644 --- a/examples/gallery/pages_data_code.go +++ b/examples/gallery/pages_data_code.go @@ -3,9 +3,9 @@ package main // Snippets are the bodies of the Example functions in example_data_test.go. const dataCardCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Card(c, mujica.CardOptions{Title: "Archive", Decorated: true, Footer: func() { - mujica.Button(c, "Open", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + data.Card(c, data.CardOptions{Title: "Archive", Decorated: true, Footer: func() { + input.Button(c, "Open", input.ButtonOptions{}) }}, func() { ui.Text(c, "Letters of the court, sealed in wax.") }).Width(280) @@ -13,10 +13,10 @@ const dataCardCode = `view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` const dataListCode = `names := []string{"Matins", "Lauds", "Prime", "Terce"} -var state mujica.ListState[string] +var state data.ListState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - v := mujica.List(c, &state, len(names), mujica.ListOptions[string]{ + core.Use(c, core.Settings{}) + v := data.List(c, &state, len(names), data.ListOptions[string]{ Key: func(i int) string { return names[i] }, Label: func(i int) string { return names[i] }, }, func(i int) { ui.Text(c, names[i]) }) @@ -34,10 +34,10 @@ const dataTableCode = `type relic struct { Age int } rows := []relic{{1, "Censer", 300}, {2, "Altar", 120}, {3, "Bell", 80}} -var state mujica.DataTableState[int] -opts := mujica.DataTableOptions[relic, int]{ +var state data.DataTableState[int] +opts := data.DataTableOptions[relic, int]{ Key: func(r relic) int { return r.ID }, - Columns: []mujica.DataColumn[relic]{ + Columns: []data.DataColumn[relic]{ {ID: "name", Title: "Name", Sortable: true, Text: func(r relic) string { return r.Name }, Compare: func(a, b relic) int { return cmp.Compare(a.Name, b.Name) }}, @@ -47,16 +47,16 @@ opts := mujica.DataTableOptions[relic, int]{ }, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DataTable(c, &state, rows, opts).Size(360, 240) + core.Use(c, core.Settings{}) + data.DataTable(c, &state, rows, opts).Size(360, 240) } ui.Render(view, 400, 300, 1)` const dataTreeCode = `kids := map[string][]string{"Nave": {"Pulpit", "Pews"}} -var state mujica.TreeState[string] +var state data.TreeState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Tree(c, &state, mujica.TreeOptions[string]{ + core.Use(c, core.Settings{}) + data.Tree(c, &state, data.TreeOptions[string]{ Roots: []string{"Nave", "Sacristy"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, @@ -66,14 +66,14 @@ ui.Render(view, 400, 300, 1)` const dataTreeTableCode = `kids := map[string][]string{"Vestry": {"Stole", "Alb"}} size := map[string]int{"Vestry": 9, "Stole": 2, "Alb": 5, "Organ": 7} -var state mujica.TreeTableState[string] +var state data.TreeTableState[string] view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TreeTable(c, &state, mujica.TreeTableOptions[string]{ + core.Use(c, core.Settings{}) + data.TreeTable(c, &state, data.TreeTableOptions[string]{ Roots: []string{"Vestry", "Organ"}, Children: func(n string) []string { return kids[n] }, Label: func(n string) string { return n }, - Columns: []mujica.TreeColumn[string]{ + Columns: []data.TreeColumn[string]{ {ID: "name", Title: "Name", Cell: func(n string) { ui.Text(c, n) }, Compare: cmp.Compare[string]}, {ID: "size", Title: "Size", Width: 80, Align: ui.End, @@ -85,59 +85,59 @@ ui.Render(view, 400, 300, 1)` const dataAccordionCode = `open := []string{"choir"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Accordion(c, &open, []mujica.AccordionItem{ + core.Use(c, core.Settings{}) + data.Accordion(c, &open, []data.AccordionItem{ {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices.") }}, {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Black oak pews.") }}, - }, mujica.AccordionOptions{}) + }, data.AccordionOptions{}) } ui.Render(view, 400, 300, 1)` const dataCollapsibleCode = `open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Collapsible(c, &open, mujica.CollapsibleOptions{Title: "Details"}, func() { + core.Use(c, core.Settings{}) + data.Collapsible(c, &open, data.CollapsibleOptions{Title: "Details"}, func() { ui.Text(c, "Candles are lit at the seventh bell.") }) } ui.Render(view, 400, 300, 1)` const dataTimelineCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Timeline(c, []mujica.TimelineItem{ - {Time: "06:00", Title: "Matins sung", Status: mujica.TimelineDone}, - {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: mujica.TimelineActive}, + core.Use(c, core.Settings{}) + data.Timeline(c, []data.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Status: data.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Content: "From the crypt to the altar.", Status: data.TimelineActive}, {Time: "18:00", Title: "Vespers"}, }) } ui.Render(view, 400, 300, 1)` const dataDescriptionCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DescriptionList(c, []mujica.DescriptionItem{ + core.Use(c, core.Settings{}) + data.DescriptionList(c, []data.DescriptionItem{ {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, {Label: "Shelf", Value: "North vault, row 7"}, {Label: "Seal", Value: "Wine-red wax", Span: -1}, - }, mujica.DescriptionListOptions{}) + }, data.DescriptionListOptions{}) } ui.Render(view, 400, 300, 1)` const dataStatisticCode = `change := 12.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Statistic(c, 12840, mujica.StatisticOptions{ + core.Use(c, core.Settings{}) + data.Statistic(c, 12840, data.StatisticOptions{ Title: "Pilgrims", Unit: "souls", Precision: 0, Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast", }) } ui.Render(view, 400, 300, 1)` -const dataViewerCode = "picture := ui.MustParseSVG([]byte(``))\nopen, index := true, 0\nview := func(c *ui.Context) {\n\tmujica.Use(c, mujica.Settings{})\n\tmujica.ImageViewer(c, &open, &index, []mujica.ViewerImage{{Source: picture, Alt: \"Rose window\"}})\n}\nui.Render(view, 400, 300, 1)" +const dataViewerCode = "picture := ui.MustParseSVG([]byte(``))\nopen, index := true, 0\nview := func(c *ui.Context) {\n\tcore.Use(c, core.Settings{})\n\tdata.ImageViewer(c, &open, &index, []data.ViewerImage{{Source: picture, Alt: \"Rose window\"}})\n}\nui.Render(view, 400, 300, 1)" const dataCarouselCode = `index := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Carousel(c, &index, 3, mujica.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { + core.Use(c, core.Settings{}) + data.Carousel(c, &index, 3, data.CarouselOptions{Label: "Windows", Autoplay: true}, func(i int) { ui.Text(c, fmt.Sprintf("Rose window %d", i+1)).Padding(24) }).Size(360, 220) } diff --git a/examples/gallery/pages_data_collections.go b/examples/gallery/pages_data_collections.go index 4d469d5..e0624f8 100644 --- a/examples/gallery/pages_data_collections.go +++ b/examples/gallery/pages_data_collections.go @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var dataOffices = []string{"Matins", "Lauds", "Prime", "Terce", "Sext", "None", "Vespers", "Compline"} @@ -18,7 +20,7 @@ func dataListDemo(c *ui.Context, log func(string)) { type state struct { big, grid, multiple bool status, query string - list mujica.ListState[int] + list data.ListState[int] } s := ui.Local(c.Root(), "070", func() state { return state{status: "Ready"} }) n := 24 @@ -39,7 +41,7 @@ func dataListDemo(c *ui.Context, log func(string)) { }) dataControls(c, func() { ui.TextInput(c, &s.query).Width(200).Placeholder("Find by label") - if mujica.Button(c, "Find", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Find", input.ButtonOptions{Variant: input.Secondary}).Clicked() { for i := range n { if strings.Contains(strings.ToLower(label(i)), strings.ToLower(s.query)) { s.list.ScrollTo(i) @@ -49,13 +51,13 @@ func dataListDemo(c *ui.Context, log func(string)) { } } }) - v := mujica.List(c, &s.list, n, mujica.ListOptions[int]{ + v := data.List(c, &s.list, n, data.ListOptions[int]{ Key: func(i int) int { return i }, Label: label, Grid: s.grid, Multiple: s.multiple, ItemWidth: 140, ItemHeight: 72, Status: st, }, func(i int) { ui.Text(c, label(i)).SingleLine() if s.grid { - ui.Text(c, "Office").TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, "Office").TextColor(core.Tokens(c).TextMuted) } }) v.Element.Size(480, 320) @@ -95,7 +97,7 @@ func dataTableDemo(c *ui.Context, log func(string)) { big, multiple bool status string small, large []dataRelic - table mujica.DataTableState[int] + table data.DataTableState[int] } s := ui.Local(c.Root(), "071", func() state { return state{status: "Ready", small: dataRelics(60)} }) rows := s.small @@ -109,8 +111,8 @@ func dataTableDemo(c *ui.Context, log func(string)) { if !show { rows = nil } - k := mujica.Tokens(c) - cols := []mujica.DataColumn[dataRelic]{ + k := core.Tokens(c) + cols := []data.DataColumn[dataRelic]{ {ID: "id", Title: "ID", Width: 80, Align: ui.End, Sortable: true, Text: func(r dataRelic) string { return strconv.Itoa(r.ID) }, Compare: func(a, b dataRelic) int { return cmp.Compare(a.ID, b.ID) }}, {ID: "name", Title: "Name", MinWidth: 120, Sortable: true, Text: func(r dataRelic) string { return r.Name }, @@ -135,7 +137,7 @@ func dataTableDemo(c *ui.Context, log func(string)) { ui.Select(c, &s.status, dataStatuses) ui.TextInput(c, &s.table.Filter).Width(180).Placeholder("Filter") }) - v := mujica.DataTable(c, &s.table, rows, mujica.DataTableOptions[dataRelic, int]{ + v := data.DataTable(c, &s.table, rows, data.DataTableOptions[dataRelic, int]{ Columns: cols, Key: func(r dataRelic) int { return r.ID }, Multiple: s.multiple, Status: st, }) v.Element.Size(620, 360) @@ -160,7 +162,7 @@ func dataTableDemo(c *ui.Context, log func(string)) { // dataLazy simulates children that arrive 600 ms after they are asked for. type dataLazy struct { kids map[string][]string - status map[string]mujica.DataStatus + status map[string]data.DataStatus due map[string]time.Time failed map[string]bool } @@ -169,14 +171,14 @@ func newDataLazy() *dataLazy { return &dataLazy{ kids: map[string][]string{"Nave": {"Pulpit", "Pews", "Font"}, "Crypt": {}, "Archive": {}, "Pulpit": nil, "Pews": nil, "Font": nil, "Sacristy": nil}, - status: map[string]mujica.DataStatus{"Crypt": mujica.DataUnloaded, "Archive": mujica.DataUnloaded}, + status: map[string]data.DataStatus{"Crypt": data.DataUnloaded, "Archive": data.DataUnloaded}, due: map[string]time.Time{}, failed: map[string]bool{}, } } func (d *dataLazy) load(c *ui.Context, log func(string)) func(string) { return func(n string) { - d.status[n] = mujica.DataLoading + d.status[n] = data.DataLoading d.due[n] = c.Now().Add(600 * time.Millisecond) log("load " + n) } @@ -191,10 +193,10 @@ func (d *dataLazy) tick(c *ui.Context) { } delete(d.due, n) if n == "Archive" && !d.failed[n] { - d.failed[n], d.status[n] = true, mujica.DataFailed + d.failed[n], d.status[n] = true, data.DataFailed continue } - d.status[n] = mujica.DataReady + d.status[n] = data.DataReady d.kids[n] = []string{n + " north", n + " south"} } } @@ -203,14 +205,14 @@ func dataTreeDemo(c *ui.Context, log func(string)) { type state struct { big, multiple bool lazy *dataLazy - tree mujica.TreeState[string] - bigTree mujica.TreeState[string] + tree data.TreeState[string] + bigTree data.TreeState[string] } s := ui.Local(c.Root(), "072", func() state { return state{lazy: newDataLazy()} }) s.lazy.tick(c) - opts := mujica.TreeOptions[string]{ + opts := data.TreeOptions[string]{ Roots: []string{"Nave", "Crypt", "Archive", "Sacristy"}, Children: func(n string) []string { return s.lazy.kids[n] }, - Label: func(n string) string { return n }, ItemStatus: func(n string) mujica.DataStatus { return s.lazy.status[n] }, + Label: func(n string) string { return n }, ItemStatus: func(n string) data.DataStatus { return s.lazy.status[n] }, Load: s.lazy.load(c, log), Multiple: s.multiple, } ts := &s.tree @@ -223,7 +225,7 @@ func dataTreeDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.big, "100,000 nodes") ui.Checkbox(c, &s.multiple, "Multiple") }) - v := mujica.Tree(c, ts, opts) + v := data.Tree(c, ts, opts) v.Element.Size(420, 340) if v.Changed() { n, _ := ts.Selected() @@ -237,12 +239,12 @@ func dataTreeDemo(c *ui.Context, log func(string)) { } // dataBigTree has 100 roots of 999 children: 100,000 nodes, built as they open. -func dataBigTree(multiple bool) mujica.TreeOptions[string] { +func dataBigTree(multiple bool) data.TreeOptions[string] { roots := make([]string, 100) for i := range roots { roots[i] = fmt.Sprintf("Wing %d", i+1) } - return mujica.TreeOptions[string]{ + return data.TreeOptions[string]{ Roots: roots, Label: func(n string) string { return n }, Multiple: multiple, Children: func(n string) []string { if !strings.HasPrefix(n, "Wing ") || strings.Contains(n, "·") { @@ -271,7 +273,7 @@ func dataTreeTableDemo(c *ui.Context, log func(string)) { type state struct { multiple bool lazy *dataLazy - table mujica.TreeTableState[string] + table data.TreeTableState[string] } s := ui.Local(c.Root(), "073", func() state { return state{lazy: newDataLazy()} }) s.lazy.tick(c) @@ -279,11 +281,11 @@ func dataTreeTableDemo(c *ui.Context, log func(string)) { text := func(n string) { ui.Text(c, n).SingleLine() } dataSection(c, "TreeTable", func() { dataControls(c, func() { ui.Checkbox(c, &s.multiple, "Multiple") }) - v := mujica.TreeTable(c, &s.table, mujica.TreeTableOptions[string]{ + v := data.TreeTable(c, &s.table, data.TreeTableOptions[string]{ Roots: []string{"Nave", "Crypt", "Archive", "Sacristy"}, Children: func(n string) []string { return s.lazy.kids[n] }, - Label: func(n string) string { return n }, ItemStatus: func(n string) mujica.DataStatus { return s.lazy.status[n] }, + Label: func(n string) string { return n }, ItemStatus: func(n string) data.DataStatus { return s.lazy.status[n] }, Load: s.lazy.load(c, log), Multiple: s.multiple, - Columns: []mujica.TreeColumn[string]{ + Columns: []data.TreeColumn[string]{ {ID: "name", Title: "Name", MinWidth: 160, Cell: text, Compare: cmp.Compare[string]}, {ID: "size", Title: "Size", Width: 90, Align: ui.End, Cell: func(n string) { ui.Text(c, fmt.Sprintf("%d m²", size(n))) }, Compare: func(a, b string) int { return cmp.Compare(size(a), size(b)) }}, diff --git a/examples/gallery/pages_data_content.go b/examples/gallery/pages_data_content.go index 6e93449..f159559 100644 --- a/examples/gallery/pages_data_content.go +++ b/examples/gallery/pages_data_content.go @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func dataCardDemo(c *ui.Context, log func(string)) { @@ -23,15 +25,15 @@ func dataCardDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.footer, "Footer") }) card := func(decorated bool) { - opts := mujica.CardOptions{Title: s.title, Decorated: decorated} + opts := data.CardOptions{Title: s.title, Decorated: decorated} if s.footer { opts.Footer = func() { - if mujica.Button(c, "Open", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Open", input.ButtonOptions{Variant: input.Secondary}).Clicked() { log(fmt.Sprintf("open clicked (decorated=%v)", decorated)) } } } - mujica.Card(c, opts, func() { + data.Card(c, opts, func() { ui.Text(c, "Letters of the court, sealed in wine-red wax and kept in the north vault.") }).Width(280) } @@ -53,21 +55,21 @@ func dataAccordionDemo(c *ui.Context, log func(string)) { dataControls(c, func() { ui.Checkbox(c, &s.multiple, "Multiple") ui.Checkbox(c, &s.keepOne, "Keep one open") - if mujica.Button(c, "Close all", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Close all", input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.open = nil log("closed all from outside") } }) - r := mujica.Accordion(c, &s.open, []mujica.AccordionItem{ + r := data.Accordion(c, &s.open, []data.AccordionItem{ {ID: "choir", Title: "Choir", Content: func() { ui.Text(c, "Twelve voices, two at the altar.") }}, {ID: "nave", Title: "Nave", Content: func() { ui.Text(c, "Pews of black oak, polished for the feast.") }}, {ID: "crypt", Title: "Crypt (disabled)", Disabled: true, Content: func() {}}, {ID: "bells", Title: "Bells", Content: func() { - if mujica.Button(c, "Ring", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Ring", input.ButtonOptions{}).Clicked() { log("ring the bells") } }}, - }, mujica.AccordionOptions{Multiple: s.multiple, KeepOne: s.keepOne}) + }, data.AccordionOptions{Multiple: s.multiple, KeepOne: s.keepOne}) r.Element.Width(420) if r.Changed() { log(fmt.Sprintf("open %v", s.open)) @@ -80,15 +82,15 @@ func dataCollapsibleDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "075", func() state { return state{} }) dataSection(c, "Collapsible", func() { dataControls(c, func() { - if mujica.Button(c, "Open from outside", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Open from outside", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.open = true log("opened from outside") } ui.Checkbox(c, &s.disabled, "Disabled") }) - if mujica.Collapsible(c, &s.open, mujica.CollapsibleOptions{Title: "Rites of the evening", Disabled: s.disabled}, func() { + if data.Collapsible(c, &s.open, data.CollapsibleOptions{Title: "Rites of the evening", Disabled: s.disabled}, func() { ui.Text(c, "Candles are lit at the seventh bell; the choir enters from the north door.") - if mujica.Button(c, "Light candles", mujica.ButtonOptions{Variant: mujica.Outline}).Clicked() { + if input.Button(c, "Light candles", input.ButtonOptions{Variant: input.Outline}).Clicked() { log("light candles") } }).Changed() { @@ -98,25 +100,25 @@ func dataCollapsibleDemo(c *ui.Context, log func(string)) { } func dataTimelineDemo(c *ui.Context, log func(string)) { - type state struct{ items []mujica.TimelineItem } + type state struct{ items []data.TimelineItem } s := ui.Local(c.Root(), "076", func() state { - return state{items: []mujica.TimelineItem{ - {Time: "06:00", Title: "Matins sung", Content: "The choir gathered before dawn.", Status: mujica.TimelineDone}, - {Time: "09:00", Title: "Relics moved", Status: mujica.TimelineActive, + return state{items: []data.TimelineItem{ + {Time: "06:00", Title: "Matins sung", Content: "The choir gathered before dawn.", Status: data.TimelineDone}, + {Time: "09:00", Title: "Relics moved", Status: data.TimelineActive, Content: "The reliquaries travel from the crypt to the high altar, carried in procession by four acolytes in black and wine."}, - {Time: "12:00", Title: "Bell cracked", Status: mujica.TimelineError}, - {Time: "15:00", Title: "Storm warning", Status: mujica.TimelineWarning}, + {Time: "12:00", Title: "Bell cracked", Status: data.TimelineError}, + {Time: "15:00", Title: "Storm warning", Status: data.TimelineWarning}, {Time: "18:00", Title: "Vespers"}, }} }) dataSection(c, "Timeline", func() { dataControls(c, func() { - if mujica.Button(c, "Add event", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { - s.items = append(s.items, mujica.TimelineItem{Time: fmt.Sprintf("%02d:00", 19+len(s.items)%5), Title: "Compline"}) + if input.Button(c, "Add event", input.ButtonOptions{Variant: input.Secondary}).Clicked() { + s.items = append(s.items, data.TimelineItem{Time: fmt.Sprintf("%02d:00", 19+len(s.items)%5), Title: "Compline"}) log(fmt.Sprintf("%d events", len(s.items))) } }) - ui.Column(c).Width(420).Children(func() { mujica.Timeline(c, s.items) }) + ui.Column(c).Width(420).Children(func() { data.Timeline(c, s.items) }) }) } @@ -134,12 +136,12 @@ func dataDescriptionDemo(c *ui.Context, log func(string)) { }) cols, _ := strconv.Atoi(s.columns) ui.Column(c).Width(float32(s.width)).Children(func() { - if i, ok := mujica.DescriptionList(c, []mujica.DescriptionItem{ + if i, ok := data.DescriptionList(c, []data.DescriptionItem{ {Label: "Archivist", Value: "Sister Agnès", Copyable: true}, {Label: "Shelf", Value: "North vault, row 7"}, {Label: "Catalogue", Value: "MS-1347-B", Copyable: true}, {Label: "Seal", Value: "Wine-red wax, cracked at the rim", Span: -1}, - }, mujica.DescriptionListOptions{Columns: cols}).Copied(); ok { + }, data.DescriptionListOptions{Columns: cols}).Copied(); ok { log(fmt.Sprintf("copied item %d", i)) } }) @@ -166,22 +168,22 @@ func dataStatisticDemo(c *ui.Context, log func(string)) { change := s.change dataStates(c, func(state func(string, func())) { state("With change", func() { - mujica.Statistic(c, s.value, mujica.StatisticOptions{Title: "Pilgrims", Unit: "souls", Precision: p, + data.Statistic(c, s.value, data.StatisticOptions{Title: "Pilgrims", Unit: "souls", Precision: p, Change: &change, ChangeUnit: "%", ChangeNote: "vs last feast"}) }) state("Prefix", func() { - mujica.Statistic(c, s.value/1000, mujica.StatisticOptions{Title: "Tithe", Prefix: "£", Suffix: "k", Precision: p}) + data.Statistic(c, s.value/1000, data.StatisticOptions{Title: "Tithe", Prefix: "£", Suffix: "k", Precision: p}) }) }) }) } -var dataPictures = func() []mujica.ViewerImage { +var dataPictures = func() []data.ViewerImage { art := func(bg, fg string, shape string) *ui.SVG { return ui.MustParseSVG([]byte(`` + ``)) } - return []mujica.ViewerImage{ + return []data.ViewerImage{ {Source: art("#AD9164", "#7D2034", "M240 40 L380 160 L240 280 L100 160 Z"), Alt: "Rose window"}, {Source: art("#211C1D", "#BDA172", "M80 280 L80 120 L240 40 L400 120 L400 280 Z"), Alt: "Chapel arch"}, {Source: art("#F2EBDD", "#651729", "M60 60 H420 V100 H60 Z M60 140 H420 V180 H60 Z M60 220 H420 V260 H60 Z"), Alt: "Choir stalls"}, @@ -195,7 +197,7 @@ func dataViewerDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "079", func() state { return state{} }) dataSection(c, "ImageViewer", func() { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Text(c, "Choose a picture to open the viewer.").TextColor(k.TextMuted) ui.Row(c).Gap(12).Children(func() { for i, p := range dataPictures { @@ -208,7 +210,7 @@ func dataViewerDemo(c *ui.Context, log func(string)) { } }) wasOpen := s.open - if mujica.ImageViewer(c, &s.open, &s.index, dataPictures).Changed() { + if data.ImageViewer(c, &s.open, &s.index, dataPictures).Changed() { log(fmt.Sprintf("image %d", s.index+1)) } if wasOpen && !s.open { @@ -234,7 +236,7 @@ func dataCarouselDemo(c *ui.Context, log func(string)) { if err != nil { panic(err) } - v := mujica.Carousel(c, &s.index, len(dataPictures), mujica.CarouselOptions{Label: "Windows", Autoplay: s.autoplay, Loop: s.loop, Interval: d}, func(i int) { + v := data.Carousel(c, &s.index, len(dataPictures), data.CarouselOptions{Label: "Windows", Autoplay: s.autoplay, Loop: s.loop, Interval: d}, func(i int) { ui.Image(c, dataPictures[i].Source).Grow(1).Label(dataPictures[i].Alt) }) v.Element.Size(520, 300) diff --git a/examples/gallery/pages_data_test.go b/examples/gallery/pages_data_test.go index d278852..b787b27 100644 --- a/examples/gallery/pages_data_test.go +++ b/examples/gallery/pages_data_test.go @@ -6,7 +6,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // dataDemo runs the demo of page id and returns its tester and event log. @@ -18,7 +18,7 @@ func dataDemo(t *testing.T, id string) (*ui.Tester, *[]string) { } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(24).Children(func() { dataPages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) diff --git a/examples/gallery/pages_datetime.go b/examples/gallery/pages_datetime.go index e09bf68..c9e60b1 100644 --- a/examples/gallery/pages_datetime.go +++ b/examples/gallery/pages_datetime.go @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const dtCategory = "Date & Time" @@ -33,20 +34,20 @@ var datetimePages = []page{ // dtSection lays out a captioned block of a demo. func dtSection(c *ui.Context, title string, fn func()) { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Column(c).Gap(8).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size)).TextColor(k.Text) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text) fn() }) } // dtStates lays labeled variants side by side. func dtStates(c *ui.Context, fn func(state func(name string, body func()))) { - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Row(c).Gap(24).Wrap().AlignItems(ui.Start).Children(func() { fn(func(name string, body func()) { ui.Column(c).Gap(6).AlignItems(ui.Start).Children(func() { - ui.Text(c, name).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) body() }) }) @@ -59,10 +60,10 @@ func dtControls(c *ui.Context, fn func()) { var dtWeekStarts = []string{"Locale", "Sunday", "Monday", "Saturday"} -func dtWeekStart(s string) mujica.WeekStart { +func dtWeekStart(s string) datetime.WeekStart { for i, n := range dtWeekStarts { if n == s { - return mujica.WeekStart(i) + return datetime.WeekStart(i) } } panic("gallery: unknown week start " + s) @@ -70,11 +71,11 @@ func dtWeekStart(s string) mujica.WeekStart { func dtCalendarDemo(c *ui.Context, log func(string)) { type state struct { - day mujica.Date + day core.Date weekStart string weekends, bound, frozen bool } - s := ui.Local(c.Root(), "051", func() state { return state{day: mujica.DateOf(time.Now()), weekStart: "Locale"} }) + s := ui.Local(c.Root(), "051", func() state { return state{day: core.DateOf(time.Now()), weekStart: "Locale"} }) dtSection(c, "Calendar", func() { dtControls(c, func() { ui.Select(c, &s.weekStart, dtWeekStarts) @@ -82,16 +83,16 @@ func dtCalendarDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.bound, "Only this month") ui.Checkbox(c, &s.frozen, "Disabled") }) - opts := mujica.CalendarOptions{WeekStart: dtWeekStart(s.weekStart), Disabled: s.frozen, Label: "Calendar demo"} + opts := datetime.CalendarOptions{WeekStart: dtWeekStart(s.weekStart), Disabled: s.frozen, Label: "Calendar demo"} if s.weekends { - opts.DisableDate = func(d mujica.Date) bool { return d.Weekday() == time.Saturday || d.Weekday() == time.Sunday } + opts.DisableDate = func(d core.Date) bool { return d.Weekday() == time.Saturday || d.Weekday() == time.Sunday } } if s.bound { - now := mujica.DateOf(time.Now()) - opts.Min = mujica.Date{Year: now.Year, Month: now.Month, Day: 1} + now := core.DateOf(time.Now()) + opts.Min = core.Date{Year: now.Year, Month: now.Month, Day: 1} opts.Max = opts.Min.AddMonths(1).AddDays(-1) } - if mujica.Calendar(c, &s.day, opts).Changed() { + if datetime.Calendar(c, &s.day, opts).Changed() { log("Calendar changed: " + s.day.String()) } }) @@ -99,11 +100,11 @@ func dtCalendarDemo(c *ui.Context, log func(string)) { func dtDatePickerDemo(c *ui.Context, log func(string)) { type state struct { - a, b, d mujica.Date + a, b, d core.Date clearable, slash bool } s := ui.Local(c.Root(), "052", func() state { - t := mujica.DateOf(time.Now()) + t := core.DateOf(time.Now()) return state{a: t, d: t, clearable: true} }) dtSection(c, "DatePicker", func() { @@ -115,24 +116,24 @@ func dtDatePickerDemo(c *ui.Context, log func(string)) { if s.slash { layout = "02/01/2006" } - now := mujica.DateOf(time.Now()) + now := core.DateOf(time.Now()) dtStates(c, func(state func(string, func())) { state("Default", func() { - if mujica.DatePicker(c, &s.a, mujica.DatePickerOptions{Label: "Due date", Layout: layout, Clearable: s.clearable}).Changed() { + if datetime.DatePicker(c, &s.a, datetime.DatePickerOptions{Label: "Due date", Layout: layout, Clearable: s.clearable}).Changed() { log("DatePicker changed: " + s.a.String()) } }) state("Next 30 days only", func() { - opts := mujica.DatePickerOptions{Label: "Delivery", Layout: layout, Clearable: s.clearable, Min: now, Max: now.AddDays(30)} - if mujica.DatePicker(c, &s.b, opts).Changed() { + opts := datetime.DatePickerOptions{Label: "Delivery", Layout: layout, Clearable: s.clearable, Min: now, Max: now.AddDays(30)} + if datetime.DatePicker(c, &s.b, opts).Changed() { log("DatePicker (bounded) changed: " + s.b.String()) } }) state("Read-only", func() { - mujica.DatePicker(c, &s.d, mujica.DatePickerOptions{Label: "Signed on", Layout: layout, ReadOnly: true}) + datetime.DatePicker(c, &s.d, datetime.DatePickerOptions{Label: "Signed on", Layout: layout, ReadOnly: true}) }) state("Disabled", func() { - mujica.DatePicker(c, &s.d, mujica.DatePickerOptions{Label: "Locked", Layout: layout, Disabled: true}) + datetime.DatePicker(c, &s.d, datetime.DatePickerOptions{Label: "Locked", Layout: layout, Disabled: true}) }) }) }) @@ -140,7 +141,7 @@ func dtDatePickerDemo(c *ui.Context, log func(string)) { func dtDateRangeDemo(c *ui.Context, log func(string)) { type state struct { - a mujica.DateRange + a datetime.DateRange noTuesdays bool frozen bool fromThisWeek bool @@ -152,14 +153,14 @@ func dtDateRangeDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.fromThisWeek, "Start from today") ui.Checkbox(c, &s.frozen, "Disabled") }) - opts := mujica.DateRangePickerOptions{Label: "Stay", Disabled: s.frozen} + opts := datetime.DateRangePickerOptions{Label: "Stay", Disabled: s.frozen} if s.noTuesdays { - opts.DisableDate = func(d mujica.Date) bool { return d.Weekday() == time.Tuesday } + opts.DisableDate = func(d core.Date) bool { return d.Weekday() == time.Tuesday } } if s.fromThisWeek { - opts.Min = mujica.DateOf(time.Now()) + opts.Min = core.DateOf(time.Now()) } - r := mujica.DateRangePicker(c, &s.a, opts) + r := datetime.DateRangePicker(c, &s.a, opts) if r.Changed() { log(fmt.Sprintf("DateRangePicker changed: %v – %v (%d days)", s.a.Start, s.a.End, s.a.Days())) } @@ -171,12 +172,12 @@ func dtDateRangeDemo(c *ui.Context, log func(string)) { func dtTimePickerDemo(c *ui.Context, log func(string)) { type state struct { - a, b mujica.TimeOfDay + a, b datetime.TimeOfDay seconds, hour12 bool step string } s := ui.Local(c.Root(), "054", func() state { - return state{a: mujica.TimeOfDay{Hour: 9, Minute: 30}, b: mujica.TimeOfDay{Hour: 10}, step: "1"} + return state{a: datetime.TimeOfDay{Hour: 9, Minute: 30}, b: datetime.TimeOfDay{Hour: 10}, step: "1"} }) dtSection(c, "TimePicker", func() { dtControls(c, func() { @@ -189,31 +190,31 @@ func dtTimePickerDemo(c *ui.Context, log func(string)) { if _, err := fmt.Sscan(s.step, &step); err != nil { panic(err) } - base := mujica.TimePickerOptions{Seconds: s.seconds, Hour12: s.hour12, MinuteStep: step} + base := datetime.TimePickerOptions{Seconds: s.seconds, Hour12: s.hour12, MinuteStep: step} dtStates(c, func(state func(string, func())) { state("Any time", func() { o := base o.Label = "Alarm" - if mujica.TimePicker(c, &s.a, o).Changed() { + if datetime.TimePicker(c, &s.a, o).Changed() { log("TimePicker changed: " + s.a.String()) } }) state("Office hours 09:00–18:00", func() { o := base - o.Label, o.Min, o.Max = "Meeting", mujica.TimeOfDay{Hour: 9}, mujica.TimeOfDay{Hour: 18} - if mujica.TimePicker(c, &s.b, o).Changed() { + o.Label, o.Min, o.Max = "Meeting", datetime.TimeOfDay{Hour: 9}, datetime.TimeOfDay{Hour: 18} + if datetime.TimePicker(c, &s.b, o).Changed() { log("TimePicker (bounded) changed: " + s.b.String()) } }) state("Read-only", func() { o := base o.Label, o.ReadOnly = "Logged at", true - mujica.TimePicker(c, &s.a, o) + datetime.TimePicker(c, &s.a, o) }) state("Disabled", func() { o := base o.Label, o.Disabled = "Locked", true - mujica.TimePicker(c, &s.a, o) + datetime.TimePicker(c, &s.a, o) }) }) }) @@ -221,21 +222,21 @@ func dtTimePickerDemo(c *ui.Context, log func(string)) { func dtTimeRangeDemo(c *ui.Context, log func(string)) { type state struct { - v mujica.TimeRange + v datetime.TimeRange overnight bool } s := ui.Local(c.Root(), "055", func() state { - return state{v: mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 9}, End: mujica.TimeOfDay{Hour: 17}}} + return state{v: datetime.TimeRange{Start: datetime.TimeOfDay{Hour: 9}, End: datetime.TimeOfDay{Hour: 17}}} }) dtSection(c, "TimeRangePicker", func() { dtControls(c, func() { if ui.Checkbox(c, &s.overnight, "Allow overnight").Changed() && !s.overnight && s.v.Overnight() { - s.v = mujica.TimeRange{Start: s.v.End, End: s.v.Start} + s.v = datetime.TimeRange{Start: s.v.End, End: s.v.Start} log("TimeRangePicker swapped ends: overnight turned off") } }) - opts := mujica.TimeRangePickerOptions{Label: "Opening hours", AllowOvernight: s.overnight, MinuteStep: 15} - if mujica.TimeRangePicker(c, &s.v, opts).Changed() { + opts := datetime.TimeRangePickerOptions{Label: "Opening hours", AllowOvernight: s.overnight, MinuteStep: 15} + if datetime.TimeRangePicker(c, &s.v, opts).Changed() { log(fmt.Sprintf("TimeRangePicker changed: %v – %v overnight=%v", s.v.Start, s.v.End, s.v.Overnight())) } }) @@ -260,22 +261,22 @@ func dtDateTimeDemo(c *ui.Context, log func(string)) { if err != nil { panic(err) } - r := mujica.DateTimePicker(c, &s.v, mujica.DateTimePickerOptions{Label: "Meeting", Location: loc, Hour12: s.hour12}) + r := datetime.DateTimePicker(c, &s.v, datetime.DateTimePickerOptions{Label: "Meeting", Location: loc, Hour12: s.hour12}) if r.Changed() { log("DateTimePicker changed: " + s.v.Format(time.RFC3339)) } if r.Canceled() { log("DateTimePicker canceled") } - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Text(c, "Try 2026-03-08 02:30 in America/New_York: the picker reports the DST gap."). - FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) } func dtMonthDemo(c *ui.Context, log func(string)) { type state struct { - v mujica.YearMonth + v datetime.YearMonth clearable, past bool } s := ui.Local(c.Root(), "057", func() state { return state{clearable: true} }) @@ -284,23 +285,23 @@ func dtMonthDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.clearable, "Clearable") ui.Checkbox(c, &s.past, "Past months only") }) - opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: s.clearable, - DisableMonth: func(m mujica.YearMonth) bool { return m.Month == time.February }} + opts := datetime.MonthPickerOptions{Label: "Billing month", Clearable: s.clearable, + DisableMonth: func(m datetime.YearMonth) bool { return m.Month == time.February }} if s.past { now := time.Now() - opts.Max = mujica.YearMonth{Year: now.Year(), Month: now.Month()} + opts.Max = datetime.YearMonth{Year: now.Year(), Month: now.Month()} } dtStates(c, func(state func(string, func())) { state("February unavailable", func() { - if mujica.MonthPicker(c, &s.v, opts).Changed() { + if datetime.MonthPicker(c, &s.v, opts).Changed() { log("MonthPicker changed: " + s.v.String()) } }) state("Read-only", func() { - mujica.MonthPicker(c, &s.v, mujica.MonthPickerOptions{Label: "Closed month", ReadOnly: true}) + datetime.MonthPicker(c, &s.v, datetime.MonthPickerOptions{Label: "Closed month", ReadOnly: true}) }) state("Disabled", func() { - mujica.MonthPicker(c, &s.v, mujica.MonthPickerOptions{Label: "Locked", Disabled: true}) + datetime.MonthPicker(c, &s.v, datetime.MonthPickerOptions{Label: "Locked", Disabled: true}) }) }) }) @@ -314,19 +315,19 @@ func dtYearDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "058", func() state { return state{v: time.Now().Year(), clearable: true} }) dtSection(c, "YearPicker", func() { dtControls(c, func() { ui.Checkbox(c, &s.clearable, "Clearable") }) - opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: time.Now().Year(), Clearable: s.clearable, + opts := datetime.YearPickerOptions{Label: "Vintage", Min: 1990, Max: time.Now().Year(), Clearable: s.clearable, DisableYear: func(y int) bool { return y == 2020 }} dtStates(c, func(state func(string, func())) { state("1990 to now, 2020 unavailable", func() { - if mujica.YearPicker(c, &s.v, opts).Changed() { + if datetime.YearPicker(c, &s.v, opts).Changed() { log(fmt.Sprintf("YearPicker changed: %d", s.v)) } }) state("Read-only", func() { - mujica.YearPicker(c, &s.v, mujica.YearPickerOptions{Label: "Founded", ReadOnly: true}) + datetime.YearPicker(c, &s.v, datetime.YearPickerOptions{Label: "Founded", ReadOnly: true}) }) state("Disabled", func() { - mujica.YearPicker(c, &s.v, mujica.YearPickerOptions{Label: "Locked", Disabled: true}) + datetime.YearPicker(c, &s.v, datetime.YearPickerOptions{Label: "Locked", Disabled: true}) }) }) }) @@ -334,47 +335,47 @@ func dtYearDemo(c *ui.Context, log func(string)) { // Code snippets mirror example_datetime_test.go. const ( - dtCalendarCode = `day := mujica.Date{Year: 2026, Month: time.October, Day: 5} + dtCalendarCode = `day := core.Date{Year: 2026, Month: time.October, Day: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Calendar(c, &day, mujica.CalendarOptions{WeekStart: mujica.WeekStartMonday}).Changed() { + core.Use(c, core.Settings{}) + if datetime.Calendar(c, &day, datetime.CalendarOptions{WeekStart: datetime.WeekStartMonday}).Changed() { fmt.Println("chosen", day) } } ui.Render(view, 400, 300, 1)` - dtDatePickerCode = `due := mujica.Date{Year: 2026, Month: time.October, Day: 5} + dtDatePickerCode = `due := core.Date{Year: 2026, Month: time.October, Day: 5} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DatePickerOptions{Label: "Due date", Clearable: true, - Min: mujica.Date{Year: 2026, Month: time.January, Day: 1}} - if mujica.DatePicker(c, &due, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DatePickerOptions{Label: "Due date", Clearable: true, + Min: core.Date{Year: 2026, Month: time.January, Day: 1}} + if datetime.DatePicker(c, &due, opts).Changed() { fmt.Println("due", due) } } ui.Render(view, 400, 300, 1)` - dtDateRangeCode = `var stay mujica.DateRange + dtDateRangeCode = `var stay datetime.DateRange view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DateRangePicker(c, &stay, mujica.DateRangePickerOptions{Label: "Stay"}) + core.Use(c, core.Settings{}) + r := datetime.DateRangePicker(c, &stay, datetime.DateRangePickerOptions{Label: "Stay"}) if r.Changed() { fmt.Println("stay", stay.Start, stay.End, stay.Days()) } } ui.Render(view, 400, 300, 1)` - dtTimePickerCode = `alarm := mujica.TimeOfDay{Hour: 7, Minute: 30} + dtTimePickerCode = `alarm := datetime.TimeOfDay{Hour: 7, Minute: 30} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} - if mujica.TimePicker(c, &alarm, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.TimePickerOptions{Label: "Alarm", Hour12: true, MinuteStep: 5} + if datetime.TimePicker(c, &alarm, opts).Changed() { fmt.Println("alarm", alarm) } } ui.Render(view, 400, 300, 1)` - dtTimeRangeCode = `shift := mujica.TimeRange{Start: mujica.TimeOfDay{Hour: 22}, End: mujica.TimeOfDay{Hour: 6}} + dtTimeRangeCode = `shift := datetime.TimeRange{Start: datetime.TimeOfDay{Hour: 22}, End: datetime.TimeOfDay{Hour: 6}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} - if mujica.TimeRangePicker(c, &shift, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.TimeRangePickerOptions{Label: "Night shift", AllowOvernight: true} + if datetime.TimeRangePicker(c, &shift, opts).Changed() { fmt.Println("shift", shift.Start, shift.End, shift.Overnight()) } } @@ -385,27 +386,27 @@ if err != nil { } var meeting time.Time view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} - if mujica.DateTimePicker(c, &meeting, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.DateTimePickerOptions{Label: "Meeting", Location: tokyo, MinuteStep: 15} + if datetime.DateTimePicker(c, &meeting, opts).Changed() { fmt.Println("meeting", meeting) } } ui.Render(view, 400, 300, 1)` - dtMonthCode = `var month mujica.YearMonth + dtMonthCode = `var month datetime.YearMonth view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.MonthPickerOptions{Label: "Billing month", Clearable: true} - if mujica.MonthPicker(c, &month, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.MonthPickerOptions{Label: "Billing month", Clearable: true} + if datetime.MonthPicker(c, &month, opts).Changed() { fmt.Println("month", month) } } ui.Render(view, 400, 300, 1)` dtYearCode = `year := 2026 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - opts := mujica.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} - if mujica.YearPicker(c, &year, opts).Changed() { + core.Use(c, core.Settings{}) + opts := datetime.YearPickerOptions{Label: "Vintage", Min: 1990, Max: 2030} + if datetime.YearPicker(c, &year, opts).Changed() { fmt.Println("year", year) } } diff --git a/examples/gallery/pages_devtools_code.go b/examples/gallery/pages_devtools_code.go index 6a733dd..e2040c7 100644 --- a/examples/gallery/pages_devtools_code.go +++ b/examples/gallery/pages_devtools_code.go @@ -1,19 +1,19 @@ package main const devStatCardCode = `value := "12,480" -opts := mujica.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} +opts := devtools.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, Trend: []float64{3, 5, 4, 8}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatCard(c, value, opts).Changed() { + core.Use(c, core.Settings{}) + if devtools.StatCard(c, value, opts).Changed() { fmt.Println("retry") } } ui.Render(view, 720, 480, 1)` -const devAlertListCode = `alerts := []mujica.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: mujica.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} +const devAlertListCode = `alerts := []devtools.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: devtools.AlertWarning, Source: "worker", At: time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AlertList(c, alerts, mujica.AlertListOptions{}) + core.Use(c, core.Settings{}) + r := devtools.AlertList(c, alerts, devtools.AlertListOptions{}) if r.Ack != "" { fmt.Println("ack", r.Ack) } @@ -23,17 +23,17 @@ view := func(c *ui.Context) { } ui.Render(view, 720, 480, 1)` -const devIncidentCardCode = `incident := mujica.Incident{Title: "API latency", Status: mujica.IncidentMonitoring, Impact: mujica.ImpactMinor, Updates: []mujica.IncidentUpdate{{Status: mujica.IncidentIdentified, Text: "Slow query located"}, {Status: mujica.IncidentMonitoring, Text: "Recovery observed"}}} +const devIncidentCardCode = `incident := devtools.Incident{Title: "API latency", Status: devtools.IncidentMonitoring, Impact: devtools.ImpactMinor, Updates: []devtools.IncidentUpdate{{Status: devtools.IncidentIdentified, Text: "Slow query located"}, {Status: devtools.IncidentMonitoring, Text: "Recovery observed"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncidentCard(c, incident) + core.Use(c, core.Settings{}) + devtools.IncidentCard(c, incident) } ui.Render(view, 720, 480, 1)` -const devUptimeBarCode = `days := []mujica.UptimeDay{{Date: mujica.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: mujica.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} +const devUptimeBarCode = `days := []devtools.UptimeDay{{Date: core.Date{Year: 2026, Month: 10, Day: 1}, Uptime: 1}, {Date: core.Date{Year: 2026, Month: 10, Day: 2}, Uptime: 0.91, Note: "Recovery"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.UptimeBar(c, days, mujica.UptimeBarOptions{Frame: mujica.ChartFrame{Label: "API uptime"}}) + core.Use(c, core.Settings{}) + r := devtools.UptimeBar(c, days, devtools.UptimeBarOptions{Frame: chart.ChartFrame{Label: "API uptime"}}) if r.Changed() { fmt.Println("day", r.Point) } @@ -43,46 +43,46 @@ view := func(c *ui.Context) { } ui.Render(view, 720, 480, 1)` -const devServiceStatusCode = `services := []mujica.ServiceItem{{Name: "API", Health: mujica.ServiceOperational}, {Name: "Search", Health: mujica.ServiceDegraded, Note: "Slow queries"}} +const devServiceStatusCode = `services := []devtools.ServiceItem{{Name: "API", Health: devtools.ServiceOperational}, {Name: "Search", Health: devtools.ServiceDegraded, Note: "Slow queries"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ServiceStatus(c, services, mujica.ServiceStatusOptions{}) + core.Use(c, core.Settings{}) + devtools.ServiceStatus(c, services, devtools.ServiceStatusOptions{}) } ui.Render(view, 720, 480, 1)` -const devLogStreamCode = `lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "server ready"}, {Level: mujica.LogWarn, Text: "slow query"}} -var state mujica.LogStreamState +const devLogStreamCode = `lines := []code.LogLine{{Level: code.LogInfo, Text: "server ready"}, {Level: code.LogWarn, Text: "slow query"}} +var state devtools.LogStreamState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LogStream(c, lines, &state, mujica.LogStreamOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.LogStream(c, lines, &state, devtools.LogStreamOptions{}).Changed() { fmt.Println("filter changed") } } ui.Render(view, 720, 480, 1)` -const devTraceWaterfallCode = `spans := []mujica.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} +const devTraceWaterfallCode = `spans := []devtools.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 200 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 20 * time.Millisecond, Duration: 80 * time.Millisecond}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TraceWaterfall(c, spans, mujica.TraceWaterfallOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.TraceWaterfall(c, spans, devtools.TraceWaterfallOptions{}).Changed() { fmt.Println("span selected") } } ui.Render(view, 720, 480, 1)` -const devResourceGaugeCode = `readings := []mujica.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} +const devResourceGaugeCode = `readings := []devtools.ResourceReading{{Name: "CPU", Used: 45, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResourceGauge(c, readings, mujica.ResourceGaugeOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.ResourceGauge(c, readings, devtools.ResourceGaugeOptions{}).Changed() { fmt.Println("retry") } } ui.Render(view, 720, 480, 1)` const devSystemMonitorCode = `times := []time.Time{time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC), time.Date(2026, 10, 5, 9, 0, 5, 0, time.UTC)} -metrics := []mujica.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} +metrics := []devtools.MonitorMetric{{Name: "CPU", Unit: "%", Samples: []float64{42, 51}}, {Name: "Memory", Unit: "GB", Samples: []float64{3.2, 3.4}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SystemMonitor(c, times, metrics, mujica.SystemMonitorOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.SystemMonitor(c, times, metrics, devtools.SystemMonitorOptions{}).Changed() { fmt.Println("retry") } } @@ -90,8 +90,8 @@ ui.Render(view, 720, 480, 1)` const devRefreshIntervalSelectorCode = `interval := 30 * time.Second view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.RefreshIntervalSelector(c, &interval, mujica.RefreshIntervalSelectorOptions{}) + core.Use(c, core.Settings{}) + r := devtools.RefreshIntervalSelector(c, &interval, devtools.RefreshIntervalSelectorOptions{}) if r.Refresh { fmt.Println("refresh") } @@ -101,21 +101,21 @@ view := func(c *ui.Context) { } ui.Render(view, 720, 480, 1)` -const devDashboardFilterBarCode = `var filters mujica.DashboardFilters -opts := mujica.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} +const devDashboardFilterBarCode = `var filters devtools.DashboardFilters +opts := devtools.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DashboardFilterBar(c, &filters, opts).Changed() { + core.Use(c, core.Settings{}) + if devtools.DashboardFilterBar(c, &filters, opts).Changed() { fmt.Println("filters changed") } } ui.Render(view, 720, 480, 1)` const devJsonViewerCode = `data := ` + "`" + `{"server":{"port":8080},"ready":true}` + "`" + ` -var state mujica.JsonViewerState +var state devtools.JsonViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonViewer(c, data, &state, mujica.JsonViewerOptions{}) + core.Use(c, core.Settings{}) + r := devtools.JsonViewer(c, data, &state, devtools.JsonViewerOptions{}) if r.Copied != "" { fmt.Println("path copied") } @@ -124,29 +124,29 @@ ui.Render(view, 720, 480, 1)` const devJsonEditorCode = `text := ` + "`" + `{"port":8080,"ready":true}` + "`" + ` view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.JsonEditor(c, &text, mujica.JsonEditorOptions{}) + core.Use(c, core.Settings{}) + r := devtools.JsonEditor(c, &text, devtools.JsonEditorOptions{}) if r.Changed { fmt.Println("valid", r.Valid) } } ui.Render(view, 720, 480, 1)` -const devSchemaTreeCode = `roots := []mujica.SchemaNode{{Name: "app", Children: []mujica.SchemaNode{{Name: "users", Kind: mujica.SchemaTable, Children: []mujica.SchemaNode{{Name: "id", Kind: mujica.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: mujica.SchemaIndex, Detail: "(id)"}}}}}} -var state mujica.SchemaTreeState +const devSchemaTreeCode = `roots := []devtools.SchemaNode{{Name: "app", Children: []devtools.SchemaNode{{Name: "users", Kind: devtools.SchemaTable, Children: []devtools.SchemaNode{{Name: "id", Kind: devtools.SchemaColumn, Detail: "bigint"}, {Name: "users_pk", Kind: devtools.SchemaIndex, Detail: "(id)"}}}}}} +var state devtools.SchemaTreeState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SchemaTree(c, &state, roots, mujica.SchemaTreeOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.SchemaTree(c, &state, roots, devtools.SchemaTreeOptions{}).Changed() { path, _ := state.Selected() fmt.Println("selected", path) } } ui.Render(view, 720, 480, 1)` -const devApiRequestBuilderCode = `request := mujica.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} +const devApiRequestBuilderCode = `request := devtools.ApiRequest{Method: "POST", URL: "https://example.com/api", Headers: []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ApiRequestBuilder(c, &request, mujica.ApiRequestBuilderOptions{}) + core.Use(c, core.Settings{}) + r := devtools.ApiRequestBuilder(c, &request, devtools.ApiRequestBuilderOptions{}) if r.Send { fmt.Println("send", request.Method) } @@ -156,19 +156,19 @@ view := func(c *ui.Context) { } ui.Render(view, 720, 480, 1)` -const devResponseViewerCode = `response := &mujica.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: ` + "`" + `{"ready":true}` + "`" + `, Headers: []mujica.KeyValue{{Key: "Content-Type", Value: "application/json"}}} +const devResponseViewerCode = `response := &devtools.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 32, Body: ` + "`" + `{"ready":true}` + "`" + `, Headers: []devtools.KeyValue{{Key: "Content-Type", Value: "application/json"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ResponseViewer(c, response, mujica.ResponseViewerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.ResponseViewer(c, response, devtools.ResponseViewerOptions{}).Changed() { fmt.Println("retry") } } ui.Render(view, 720, 480, 1)` -const devKeyValueInputCode = `rows := []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} +const devKeyValueInputCode = `rows := []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.KeyValueInput(c, &rows, mujica.KeyValueInputOptions{}).Changed() { + core.Use(c, core.Settings{}) + if devtools.KeyValueInput(c, &rows, devtools.KeyValueInputOptions{}).Changed() { fmt.Println("rows", len(rows)) } } diff --git a/examples/gallery/pages_devtools_editors.go b/examples/gallery/pages_devtools_editors.go index 7d3b74b..bd874f1 100644 --- a/examples/gallery/pages_devtools_editors.go +++ b/examples/gallery/pages_devtools_editors.go @@ -6,18 +6,19 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/devtools" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func devJsonViewerDemo(c *ui.Context, log func(string)) { dtSection(c, "JsonViewer", func() { s := devControls(c, "453", log, false) type state struct { - tree mujica.JsonViewerState + tree devtools.JsonViewerState invalid bool } st := ui.Local(c.Root(), "453", func() state { return state{} }) - if mujica.Checkbox(c, &st.invalid, "Invalid JSON", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &st.invalid, "Invalid JSON", input.CheckboxOptions{}).Changed() { log("JSON validity changed") } devPreview(c, s, func() { @@ -28,7 +29,7 @@ func devJsonViewerDemo(c *ui.Context, log func(string)) { if s.empty { data = `{}` } - if mujica.JsonViewer(c, data, &st.tree, mujica.JsonViewerOptions{}).Copied != "" { + if devtools.JsonViewer(c, data, &st.tree, devtools.JsonViewerOptions{}).Copied != "" { log("path copied") } }) @@ -39,12 +40,12 @@ func devJsonEditorDemo(c *ui.Context, log func(string)) { dtSection(c, "JsonEditor", func() { s := devControls(c, "454", log, false) text := ui.Local(c.Root(), "454", func() string { return `{"port":8080,"ready":true}` }) - if mujica.Button(c, "Invalid sample", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Invalid sample", input.ButtonOptions{}).Clicked() { *text = "{\n \"port\": [1,2,]\n}" log("invalid sample loaded") } devPreview(c, s, func() { - r := mujica.JsonEditor(c, text, mujica.JsonEditorOptions{}) + r := devtools.JsonEditor(c, text, devtools.JsonEditorOptions{}) if r.Changed { log(fmt.Sprintf("JSON edited; valid=%t", r.Valid)) } @@ -56,18 +57,18 @@ func devSchemaTreeDemo(c *ui.Context, log func(string)) { dtSection(c, "SchemaTree", func() { s := devControls(c, "455", log, false) type state struct { - tree mujica.SchemaTreeState - roots []mujica.SchemaNode + tree devtools.SchemaTreeState + roots []devtools.SchemaNode } st := ui.Local(c.Root(), "455", func() state { - return state{roots: []mujica.SchemaNode{{Name: "app", Children: []mujica.SchemaNode{{Name: "users", Kind: mujica.SchemaTable, Children: []mujica.SchemaNode{{Name: "id", Kind: mujica.SchemaColumn, Detail: "bigint"}, {Name: "name", Kind: mujica.SchemaColumn, Detail: "text"}, {Name: "users_pk", Kind: mujica.SchemaIndex, Detail: "(id)"}}}, {Name: "active_users", Kind: mujica.SchemaView}}}}} + return state{roots: []devtools.SchemaNode{{Name: "app", Children: []devtools.SchemaNode{{Name: "users", Kind: devtools.SchemaTable, Children: []devtools.SchemaNode{{Name: "id", Kind: devtools.SchemaColumn, Detail: "bigint"}, {Name: "name", Kind: devtools.SchemaColumn, Detail: "text"}, {Name: "users_pk", Kind: devtools.SchemaIndex, Detail: "(id)"}}}, {Name: "active_users", Kind: devtools.SchemaView}}}}} }) devPreview(c, s, func() { roots := st.roots if s.empty { roots = nil } - if mujica.SchemaTree(c, &st.tree, roots, mujica.SchemaTreeOptions{}).Changed() { + if devtools.SchemaTree(c, &st.tree, roots, devtools.SchemaTreeOptions{}).Changed() { log("schema node selected") } }) @@ -77,15 +78,15 @@ func devSchemaTreeDemo(c *ui.Context, log func(string)) { func devApiRequestBuilderDemo(c *ui.Context, log func(string)) { dtSection(c, "ApiRequestBuilder", func() { s := devControls(c, "456", log, false) - request := ui.Local(c.Root(), "456", func() mujica.ApiRequest { - return mujica.ApiRequest{URL: "https://example.com/api", Headers: []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} + request := ui.Local(c.Root(), "456", func() devtools.ApiRequest { + return devtools.ApiRequest{URL: "https://example.com/api", Headers: []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}}} }) sending := ui.Local(c.Root(), "456-sending", func() bool { return false }) - if mujica.Checkbox(c, sending, "Sending", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, sending, "Sending", input.CheckboxOptions{}).Changed() { log("send state changed") } devPreview(c, s, func() { - r := mujica.ApiRequestBuilder(c, request, mujica.ApiRequestBuilderOptions{Sending: *sending}) + r := devtools.ApiRequestBuilder(c, request, devtools.ApiRequestBuilderOptions{Sending: *sending}) if r.Send { log("send requested") } @@ -100,18 +101,18 @@ func devResponseViewerDemo(c *ui.Context, log func(string)) { dtSection(c, "ResponseViewer", func() { s := devControls(c, "457", log, true) text := ui.Local(c.Root(), "457", func() bool { return false }) - if mujica.Checkbox(c, text, "Text body", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, text, "Text body", input.CheckboxOptions{}).Changed() { log("response format changed") } devPreview(c, s, func() { - resp := &mujica.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 2048, Body: `{"ready":true,"requests":1248}`, Headers: []mujica.KeyValue{{Key: "Content-Type", Value: "application/json"}, {Key: "X-Trace", Value: "demo"}}} + resp := &devtools.ApiResponse{Status: 200, Duration: 182 * time.Millisecond, Size: 2048, Body: `{"ready":true,"requests":1248}`, Headers: []devtools.KeyValue{{Key: "Content-Type", Value: "application/json"}, {Key: "X-Trace", Value: "demo"}}} if *text { resp.Body = "Response received successfully." } if s.empty { resp = nil } - if mujica.ResponseViewer(c, resp, mujica.ResponseViewerOptions{Height: 260, Status: s.status, Error: "Connection refused"}).Changed() { + if devtools.ResponseViewer(c, resp, devtools.ResponseViewerOptions{Height: 260, Status: s.status, Error: "Connection refused"}).Changed() { log("retry requested") } }) @@ -121,11 +122,11 @@ func devResponseViewerDemo(c *ui.Context, log func(string)) { func devKeyValueInputDemo(c *ui.Context, log func(string)) { dtSection(c, "KeyValueInput", func() { s := devControls(c, "458", log, false) - rows := ui.Local(c.Root(), "458", func() []mujica.KeyValue { - return []mujica.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}, {Key: "debug", Value: "true"}} + rows := ui.Local(c.Root(), "458", func() []devtools.KeyValue { + return []devtools.KeyValue{{Key: "Accept", Value: "application/json", Enabled: true}, {Key: "debug", Value: "true"}} }) devPreview(c, s, func() { - if mujica.KeyValueInput(c, rows, mujica.KeyValueInputOptions{Label: "Headers"}).Changed() { + if devtools.KeyValueInput(c, rows, devtools.KeyValueInputOptions{Label: "Headers"}).Changed() { log("pairs edited") } }) diff --git a/examples/gallery/pages_devtools_status.go b/examples/gallery/pages_devtools_status.go index ee44e1c..11c6da4 100644 --- a/examples/gallery/pages_devtools_status.go +++ b/examples/gallery/pages_devtools_status.go @@ -6,31 +6,35 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/devtools" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type devDemoSettings struct { disabled bool - status mujica.DataStatus + status data.DataStatus empty bool } func devControls(c *ui.Context, id string, log func(string), states bool) *devDemoSettings { s := ui.Local(c.Root(), id+"-controls", func() devDemoSettings { return devDemoSettings{} }) dtControls(c, func() { - if mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}).Changed() { log("disabled changed") } switch id { case "451", "454", "456", "458": default: - if mujica.Checkbox(c, &s.empty, "Empty data", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.empty, "Empty data", input.CheckboxOptions{}).Changed() { log("data state changed") } } if states { - options := []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Failed"}} - if mujica.Select(c, &s.status, options, mujica.SelectOptions{Label: "Status"}).Changed() { + options := []input.SelectOption[data.DataStatus]{{Value: data.DataReady, Label: "Ready"}, {Value: data.DataLoading, Label: "Loading"}, {Value: data.DataFailed, Label: "Failed"}} + if input.Select(c, &s.status, options, input.SelectOptions{Label: "Status"}).Changed() { log("status changed") } } @@ -46,7 +50,7 @@ func devStatCardDemo(c *ui.Context, log func(string)) { dtSection(c, "StatCard", func() { s := devControls(c, "442", log, true) lower := ui.Local(c.Root(), "442-lower", func() bool { return false }) - if mujica.Checkbox(c, lower, "Lower is better", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, lower, "Lower is better", input.CheckboxOptions{}).Changed() { log("change direction updated") } devPreview(c, s, func() { @@ -55,7 +59,7 @@ func devStatCardDemo(c *ui.Context, log func(string)) { if s.empty { value, trend = "—", nil } - if mujica.StatCard(c, value, mujica.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, LowerIsBetter: *lower, Trend: trend, Status: s.status, Error: "Source unreachable"}).Changed() { + if devtools.StatCard(c, value, devtools.StatCardOptions{Label: "Requests", Change: 0.12, HasChange: true, LowerIsBetter: *lower, Trend: trend, Status: s.status, Error: "Source unreachable"}).Changed() { log("retry requested") } }) @@ -65,15 +69,15 @@ func devStatCardDemo(c *ui.Context, log func(string)) { func devAlertListDemo(c *ui.Context, log func(string)) { dtSection(c, "AlertList", func() { s := devControls(c, "443", log, false) - alerts := ui.Local(c.Root(), "443", func() []mujica.AlertItem { - return []mujica.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: mujica.AlertWarning, Source: "worker", At: time.Now()}, {ID: "api", Title: "API unavailable", Level: mujica.AlertCritical, Source: "gateway", At: time.Now()}} + alerts := ui.Local(c.Root(), "443", func() []devtools.AlertItem { + return []devtools.AlertItem{{ID: "cpu", Title: "CPU usage high", Level: devtools.AlertWarning, Source: "worker", At: time.Now()}, {ID: "api", Title: "API unavailable", Level: devtools.AlertCritical, Source: "gateway", At: time.Now()}} }) devPreview(c, s, func() { shown := *alerts if s.empty { shown = nil } - r := mujica.AlertList(c, shown, mujica.AlertListOptions{}) + r := devtools.AlertList(c, shown, devtools.AlertListOptions{}) for i := range *alerts { if r.Ack == (*alerts)[i].ID { (*alerts)[i].Acked = true @@ -91,17 +95,17 @@ func devAlertListDemo(c *ui.Context, log func(string)) { func devIncidentCardDemo(c *ui.Context, log func(string)) { dtSection(c, "IncidentCard", func() { s := devControls(c, "444", log, false) - status := ui.Local(c.Root(), "444", func() mujica.IncidentStatus { return mujica.IncidentMonitoring }) - choices := []mujica.SelectOption[mujica.IncidentStatus]{{Value: mujica.IncidentInvestigating, Label: "Investigating"}, {Value: mujica.IncidentIdentified, Label: "Identified"}, {Value: mujica.IncidentMonitoring, Label: "Monitoring"}, {Value: mujica.IncidentResolved, Label: "Resolved"}} - if mujica.Select(c, status, choices, mujica.SelectOptions{Label: "Incident status"}).Changed() { + status := ui.Local(c.Root(), "444", func() devtools.IncidentStatus { return devtools.IncidentMonitoring }) + choices := []input.SelectOption[devtools.IncidentStatus]{{Value: devtools.IncidentInvestigating, Label: "Investigating"}, {Value: devtools.IncidentIdentified, Label: "Identified"}, {Value: devtools.IncidentMonitoring, Label: "Monitoring"}, {Value: devtools.IncidentResolved, Label: "Resolved"}} + if input.Select(c, status, choices, input.SelectOptions{Label: "Incident status"}).Changed() { log("incident status changed") } devPreview(c, s, func() { - updates := []mujica.IncidentUpdate{{Status: mujica.IncidentIdentified, Text: "Slow query located"}, {Status: mujica.IncidentMonitoring, Text: "Recovery observed"}} + updates := []devtools.IncidentUpdate{{Status: devtools.IncidentIdentified, Text: "Slow query located"}, {Status: devtools.IncidentMonitoring, Text: "Recovery observed"}} if s.empty { updates = nil } - mujica.IncidentCard(c, mujica.Incident{Title: "API latency", Status: *status, Impact: mujica.ImpactMinor, Updates: updates}) + devtools.IncidentCard(c, devtools.Incident{Title: "API latency", Status: *status, Impact: devtools.ImpactMinor, Updates: updates}) }) }) } @@ -109,11 +113,11 @@ func devIncidentCardDemo(c *ui.Context, log func(string)) { func devUptimeBarDemo(c *ui.Context, log func(string)) { dtSection(c, "UptimeBar", func() { s := devControls(c, "445", log, true) - days := ui.Local(c.Root(), "445", func() []mujica.UptimeDay { - out := make([]mujica.UptimeDay, 30) - start := mujica.Date{Year: 2026, Month: 10, Day: 1} + days := ui.Local(c.Root(), "445", func() []devtools.UptimeDay { + out := make([]devtools.UptimeDay, 30) + start := core.Date{Year: 2026, Month: 10, Day: 1} for i := range out { - out[i] = mujica.UptimeDay{Date: start.AddDays(i), Uptime: 1} + out[i] = devtools.UptimeDay{Date: start.AddDays(i), Uptime: 1} } out[7].Uptime, out[7].Note = 0.91, "2 h outage" out[12].Uptime = 0.98 @@ -124,7 +128,7 @@ func devUptimeBarDemo(c *ui.Context, log func(string)) { if s.empty { data = nil } - r := mujica.UptimeBar(c, data, mujica.UptimeBarOptions{Frame: mujica.ChartFrame{Label: "API uptime", Status: s.status, Error: "Source unreachable"}}) + r := devtools.UptimeBar(c, data, devtools.UptimeBarOptions{Frame: chart.ChartFrame{Label: "API uptime", Status: s.status, Error: "Source unreachable"}}) if r.Changed() { log(fmt.Sprintf("day %d", r.Point)) } @@ -138,17 +142,17 @@ func devUptimeBarDemo(c *ui.Context, log func(string)) { func devServiceStatusDemo(c *ui.Context, log func(string)) { dtSection(c, "ServiceStatus", func() { s := devControls(c, "446", log, false) - health := ui.Local(c.Root(), "446", func() mujica.ServiceHealth { return mujica.ServiceDegraded }) - choices := []mujica.SelectOption[mujica.ServiceHealth]{{Value: mujica.ServiceOperational, Label: "Operational"}, {Value: mujica.ServiceDegraded, Label: "Degraded"}, {Value: mujica.ServiceOutage, Label: "Outage"}} - if mujica.Select(c, health, choices, mujica.SelectOptions{Label: "Search health"}).Changed() { + health := ui.Local(c.Root(), "446", func() devtools.ServiceHealth { return devtools.ServiceDegraded }) + choices := []input.SelectOption[devtools.ServiceHealth]{{Value: devtools.ServiceOperational, Label: "Operational"}, {Value: devtools.ServiceDegraded, Label: "Degraded"}, {Value: devtools.ServiceOutage, Label: "Outage"}} + if input.Select(c, health, choices, input.SelectOptions{Label: "Search health"}).Changed() { log("service health changed") } devPreview(c, s, func() { - data := []mujica.ServiceItem{{Name: "API", Health: mujica.ServiceOperational}, {Name: "Search", Health: *health, Note: "Search cluster"}} + data := []devtools.ServiceItem{{Name: "API", Health: devtools.ServiceOperational}, {Name: "Search", Health: *health, Note: "Search cluster"}} if s.empty { data = nil } - mujica.ServiceStatus(c, data, mujica.ServiceStatusOptions{}) + devtools.ServiceStatus(c, data, devtools.ServiceStatusOptions{}) }) }) } @@ -157,15 +161,15 @@ func devResourceGaugeDemo(c *ui.Context, log func(string)) { dtSection(c, "ResourceGauge", func() { s := devControls(c, "449", log, true) used := ui.Local(c.Root(), "449", func() float64 { return 45 }) - if mujica.NumberInput(c, used, mujica.NumberInputOptions{Min: 0, Max: 100, Step: 5, Label: "CPU usage"}).Changed() { + if input.NumberInput(c, used, input.NumberInputOptions{Min: 0, Max: 100, Step: 5, Label: "CPU usage"}).Changed() { log("resource reading changed") } devPreview(c, s, func() { - data := []mujica.ResourceReading{{Name: "CPU", Used: *used, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} + data := []devtools.ResourceReading{{Name: "CPU", Used: *used, Total: 100}, {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}} if s.empty { data = nil } - if mujica.ResourceGauge(c, data, mujica.ResourceGaugeOptions{Status: s.status, Error: "Source unreachable"}).Changed() { + if devtools.ResourceGauge(c, data, devtools.ResourceGaugeOptions{Status: s.status, Error: "Source unreachable"}).Changed() { log("retry requested") } }) diff --git a/examples/gallery/pages_devtools_streams.go b/examples/gallery/pages_devtools_streams.go index 0824879..dd9c06b 100644 --- a/examples/gallery/pages_devtools_streams.go +++ b/examples/gallery/pages_devtools_streams.go @@ -6,21 +6,23 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/devtools" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func devLogStreamDemo(c *ui.Context, log func(string)) { dtSection(c, "LogStream", func() { s := devControls(c, "447", log, false) type state struct { - lines []mujica.LogLine - stream mujica.LogStreamState + lines []code.LogLine + stream devtools.LogStreamState } st := ui.Local(c.Root(), "447", func() state { - return state{lines: []mujica.LogLine{{Level: mujica.LogInfo, Text: "server ready"}, {Level: mujica.LogWarn, Text: "slow query"}}} + return state{lines: []code.LogLine{{Level: code.LogInfo, Text: "server ready"}, {Level: code.LogWarn, Text: "slow query"}}} }) - if mujica.Button(c, "Append line", mujica.ButtonOptions{}).Clicked() { - st.lines = append(st.lines, mujica.LogLine{Time: c.Now(), Level: mujica.LogInfo, Text: fmt.Sprintf("request %d completed", len(st.lines))}) + if input.Button(c, "Append line", input.ButtonOptions{}).Clicked() { + st.lines = append(st.lines, code.LogLine{Time: c.Now(), Level: code.LogInfo, Text: fmt.Sprintf("request %d completed", len(st.lines))}) log("line appended") } devPreview(c, s, func() { @@ -28,7 +30,7 @@ func devLogStreamDemo(c *ui.Context, log func(string)) { if s.empty { data = nil } - if mujica.LogStream(c, data, &st.stream, mujica.LogStreamOptions{}).Changed() { + if devtools.LogStream(c, data, &st.stream, devtools.LogStreamOptions{}).Changed() { log("pause or level changed") } }) @@ -38,15 +40,15 @@ func devLogStreamDemo(c *ui.Context, log func(string)) { func devTraceWaterfallDemo(c *ui.Context, log func(string)) { dtSection(c, "TraceWaterfall", func() { s := devControls(c, "448", log, false) - spans := ui.Local(c.Root(), "448", func() []mujica.WaterfallSpan { - return []mujica.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 320 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 40 * time.Millisecond, Duration: 90 * time.Millisecond}, {ID: "pay", Parent: "root", Name: "Charge", Service: "payments", Start: 230 * time.Millisecond, Duration: 80 * time.Millisecond, Error: true}} + spans := ui.Local(c.Root(), "448", func() []devtools.WaterfallSpan { + return []devtools.WaterfallSpan{{ID: "root", Name: "GET /api", Service: "gateway", Duration: 320 * time.Millisecond}, {ID: "db", Parent: "root", Name: "Query", Service: "database", Start: 40 * time.Millisecond, Duration: 90 * time.Millisecond}, {ID: "pay", Parent: "root", Name: "Charge", Service: "payments", Start: 230 * time.Millisecond, Duration: 80 * time.Millisecond, Error: true}} }) devPreview(c, s, func() { data := *spans if s.empty { data = nil } - if mujica.TraceWaterfall(c, data, mujica.TraceWaterfallOptions{}).Changed() { + if devtools.TraceWaterfall(c, data, devtools.TraceWaterfallOptions{}).Changed() { log("span selected") } }) @@ -58,10 +60,10 @@ func devSystemMonitorDemo(c *ui.Context, log func(string)) { s := devControls(c, "450", log, true) type state struct { times []time.Time - metrics []mujica.MonitorMetric + metrics []devtools.MonitorMetric } st := ui.Local(c.Root(), "450", func() state { - out := state{metrics: []mujica.MonitorMetric{{Name: "CPU", Unit: "%"}, {Name: "Memory", Unit: "GB"}}} + out := state{metrics: []devtools.MonitorMetric{{Name: "CPU", Unit: "%"}, {Name: "Memory", Unit: "GB"}}} for i := range 20 { out.times = append(out.times, time.Date(2026, 10, 5, 9, 0, i*5, 0, time.UTC)) out.metrics[0].Samples = append(out.metrics[0].Samples, 40+float64(i%7)*3) @@ -69,7 +71,7 @@ func devSystemMonitorDemo(c *ui.Context, log func(string)) { } return out }) - if mujica.Button(c, "Append sample", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Append sample", input.ButtonOptions{}).Clicked() { st.times = append(append([]time.Time(nil), st.times...), st.times[len(st.times)-1].Add(5*time.Second)) for i := range st.metrics { last := st.metrics[i].Samples[len(st.metrics[i].Samples)-1] @@ -82,7 +84,7 @@ func devSystemMonitorDemo(c *ui.Context, log func(string)) { if s.empty { times, metrics = nil, nil } - if mujica.SystemMonitor(c, times, metrics, mujica.SystemMonitorOptions{Status: s.status, Error: "Agent offline"}).Changed() { + if devtools.SystemMonitor(c, times, metrics, devtools.SystemMonitorOptions{Status: s.status, Error: "Agent offline"}).Changed() { log("retry requested") } }) @@ -94,11 +96,11 @@ func devRefreshIntervalSelectorDemo(c *ui.Context, log func(string)) { s := devControls(c, "451", log, false) interval := ui.Local(c.Root(), "451", func() time.Duration { return 0 }) refreshing := ui.Local(c.Root(), "451-refreshing", func() bool { return false }) - if mujica.Checkbox(c, refreshing, "Refreshing", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, refreshing, "Refreshing", input.CheckboxOptions{}).Changed() { log("refresh state changed") } devPreview(c, s, func() { - r := mujica.RefreshIntervalSelector(c, interval, mujica.RefreshIntervalSelectorOptions{Refreshing: *refreshing}) + r := devtools.RefreshIntervalSelector(c, interval, devtools.RefreshIntervalSelectorOptions{Refreshing: *refreshing}) if r.Refresh { log("refresh requested") } @@ -112,14 +114,14 @@ func devRefreshIntervalSelectorDemo(c *ui.Context, log func(string)) { func devDashboardFilterBarDemo(c *ui.Context, log func(string)) { dtSection(c, "DashboardFilterBar", func() { s := devControls(c, "452", log, false) - filters := ui.Local(c.Root(), "452", func() mujica.DashboardFilters { return mujica.DashboardFilters{} }) + filters := ui.Local(c.Root(), "452", func() devtools.DashboardFilters { return devtools.DashboardFilters{} }) devPreview(c, s, func() { - opts := mujica.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db", "web"}} + opts := devtools.DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "db", "web"}} if s.empty { opts.Envs, opts.Tags = nil, nil - *filters = mujica.DashboardFilters{} + *filters = devtools.DashboardFilters{} } - if mujica.DashboardFilterBar(c, filters, opts).Changed() { + if devtools.DashboardFilterBar(c, filters, opts).Changed() { log("filters changed") } }) diff --git a/examples/gallery/pages_devtools_test.go b/examples/gallery/pages_devtools_test.go index c010b2b..5c4762f 100644 --- a/examples/gallery/pages_devtools_test.go +++ b/examples/gallery/pages_devtools_test.go @@ -5,12 +5,12 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func devPageTester(p page, mode mujica.Mode, events *[]string) *ui.Tester { +func devPageTester(p page, mode core.Mode, events *[]string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) ui.Column(c).Padding(16).Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) }) }, 1000, 800) } @@ -21,7 +21,7 @@ func TestDevtoolsPagesControlsAndThemes(t *testing.T) { } for _, p := range devtoolsPages { t.Run(p.Name, func(t *testing.T) { - for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { var events []string tt := devPageTester(p, mode, &events) if p.Category != "Dashboards & Dev Tools" || p.Code == "" || p.Keys == "" { @@ -70,7 +70,7 @@ func TestDevtoolsDemoEvents(t *testing.T) { continue } var events []string - tt := devPageTester(p, mujica.Light, &events) + tt := devPageTester(p, core.Light, &events) if err := tt.Click(tc.label); err != nil { t.Fatal(err) } diff --git a/examples/gallery/pages_display.go b/examples/gallery/pages_display.go index 13cee9e..469255b 100644 --- a/examples/gallery/pages_display.go +++ b/examples/gallery/pages_display.go @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var displayPages = []page{ @@ -43,7 +45,7 @@ type textState struct { func textDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "011", func() textState { return textState{maxLines: 0, selectable: true, name: "Coronation.md"} }) - mujica.Heading(c, "Text", mujica.HeadingOptions{Subtitle: "换行、省略、选择复制与可编辑模式", Divider: true}) + display.Heading(c, "Text", display.HeadingOptions{Subtitle: "换行、省略、选择复制与可编辑模式", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { ui.Text(c, fmt.Sprintf("MaxLines %d", int(s.maxLines))) @@ -55,12 +57,12 @@ func textDemo(c *ui.Context, log func(string)) { }) displaySection(c, "Display", func() { ui.Box(c).Width(360).Children(func() { - mujica.Text(c, "Velvet curtains fall over the long gallery. Every candle leans toward the open door, and the quartet plays on while the guests take their places beneath the chandeliers.", - mujica.TextOptions{MaxLines: int(s.maxLines), Selectable: s.selectable}) + display.Text(c, "Velvet curtains fall over the long gallery. Every candle leans toward the open door, and the quartet plays on while the guests take their places beneath the chandeliers.", + display.TextOptions{MaxLines: int(s.maxLines), Selectable: s.selectable}) }) }) displaySection(c, "Editable", func() { - e := mujica.EditableText(c, &s.name, mujica.EditableTextOptions{Saving: s.saving, Error: s.saveErr, Label: "File name"}) + e := display.EditableText(c, &s.name, display.EditableTextOptions{Saving: s.saving, Error: s.saveErr, Label: "File name"}) if e.Changed() { if s.failNext { s.saveErr = "Save failed: the name is already taken." @@ -76,9 +78,9 @@ func textDemo(c *ui.Context, log func(string)) { }) } -const textCode = `mujica.Text(c, "Order ID: 4821-AX", mujica.TextOptions{Selectable: true}) -mujica.Text(c, "A long note that stops after two lines.", mujica.TextOptions{MaxLines: 2}) -if mujica.EditableText(c, &name, mujica.EditableTextOptions{Error: saveErr}).Changed() { +const textCode = `display.Text(c, "Order ID: 4821-AX", display.TextOptions{Selectable: true}) +display.Text(c, "A long note that stops after two lines.", display.TextOptions{MaxLines: 2}) +if display.EditableText(c, &name, display.EditableTextOptions{Error: saveErr}).Changed() { saveErr = "" // save name here; set saveErr on failure }` @@ -89,7 +91,7 @@ type headingState struct { func headingDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "012", func() headingState { return headingState{level: 1, subtitle: true, divide: true} }) - mujica.Heading(c, "Heading", mujica.HeadingOptions{Subtitle: "三级衬线标题、副标题与装饰分隔线", Divider: true}) + display.Heading(c, "Heading", display.HeadingOptions{Subtitle: "三级衬线标题、副标题与装饰分隔线", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { ui.Text(c, fmt.Sprintf("Level %d", int(s.level))) @@ -105,17 +107,17 @@ func headingDemo(c *ui.Context, log func(string)) { if s.subtitle { sub = "Ceremonies of the winter season" } - mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Level: int(s.level), Subtitle: sub, Divider: s.divide}) + display.Heading(c, "The Grand Hall", display.HeadingOptions{Level: int(s.level), Subtitle: sub, Divider: s.divide}) }) displaySection(c, "Levels", func() { for l := 1; l <= 3; l++ { - mujica.Heading(c, fmt.Sprintf("Level %d · 宫廷", l), mujica.HeadingOptions{Level: l}) + display.Heading(c, fmt.Sprintf("Level %d · 宫廷", l), display.HeadingOptions{Level: l}) } }) } -const headingCode = `mujica.Heading(c, "The Grand Hall", mujica.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) -mujica.Heading(c, "Guest Ledger", mujica.HeadingOptions{Level: 2})` +const headingCode = `display.Heading(c, "The Grand Hall", display.HeadingOptions{Subtitle: "Winter ceremonies", Divider: true}) +display.Heading(c, "Guest Ledger", display.HeadingOptions{Level: 2})` type iconState struct { name string @@ -124,12 +126,12 @@ type iconState struct { label bool } -var iconTones = map[string]mujica.IconTone{"Inherit": mujica.IconInherit, "Muted": mujica.IconMuted, "Accent": mujica.IconAccent, - "Success": mujica.IconSuccess, "Warning": mujica.IconWarning, "Danger": mujica.IconDanger, "Info": mujica.IconInfo} +var iconTones = map[string]display.IconTone{"Inherit": display.IconInherit, "Muted": display.IconMuted, "Accent": display.IconAccent, + "Success": display.IconSuccess, "Warning": display.IconWarning, "Danger": display.IconDanger, "Info": display.IconInfo} func iconDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "013", func() iconState { return iconState{name: "star", size: "24", tone: "Accent"} }) - mujica.Heading(c, "Icon", mujica.HeadingOptions{Subtitle: "按名称取图标;尺寸与语义颜色;缺失时可见回退", Divider: true}) + display.Heading(c, "Icon", display.HeadingOptions{Subtitle: "按名称取图标;尺寸与语义颜色;缺失时可见回退", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { if ui.Select(c, &s.name, []string{"star", "bell", "settings", "moon", "sun", "no-such-icon"}).Changed() { @@ -151,24 +153,24 @@ func iconDemo(c *ui.Context, log func(string)) { if s.label { label = "Preview icon" } - mujica.Icon(c, s.name, mujica.IconOptions{Size: size, Tone: iconTones[s.tone], Label: label}) + display.Icon(c, s.name, display.IconOptions{Size: size, Tone: iconTones[s.tone], Label: label}) }) displaySection(c, "Tones and fallback", func() { ui.Row(c).Gap(14).Children(func() { for _, t := range []string{"Inherit", "Muted", "Accent", "Success", "Warning", "Danger", "Info"} { - mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: iconTones[t]}).Key(t) + display.Icon(c, "circle-check", display.IconOptions{Size: 20, Tone: iconTones[t]}).Key(t) } - mujica.Icon(c, "missing-name", mujica.IconOptions{Size: 20}) + display.Icon(c, "missing-name", display.IconOptions{Size: 20}) }) }) } const iconCode = `ui.Row(c).Gap(8).Children(func() { - mujica.Icon(c, "star", mujica.IconOptions{}) - mujica.Icon(c, "circle-check", mujica.IconOptions{Size: 20, Tone: mujica.IconSuccess, Label: "Done"}) + display.Icon(c, "star", display.IconOptions{}) + display.Icon(c, "circle-check", display.IconOptions{Size: 20, Tone: display.IconSuccess, Label: "Done"}) })` -var displayPeople = []mujica.AvatarItem{ +var displayPeople = []display.AvatarItem{ {Name: "Sakiko Togawa"}, {Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, {Name: "Umiri Yahata"}, {Name: "Nyamu Yutenji"}, {Name: "Tomori Takamatsu"}, {Name: "Anon Chihaya"}, {Name: "Rana Kaname"}, } @@ -177,7 +179,7 @@ type avatarGroupState struct{ max, count float64 } func avatarGroupDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "016", func() avatarGroupState { return avatarGroupState{max: 3, count: 6} }) - mujica.Heading(c, "AvatarGroup", mujica.HeadingOptions{Subtitle: "最大显示数量、剩余数量与完整名单", Divider: true}) + display.Heading(c, "AvatarGroup", display.HeadingOptions{Subtitle: "最大显示数量、剩余数量与完整名单", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { ui.Text(c, fmt.Sprintf("Max %d", int(s.max))) @@ -191,9 +193,9 @@ func avatarGroupDemo(c *ui.Context, log func(string)) { }) }) displaySection(c, "Preview", func() { - for i, g := range []mujica.AvatarGroupView{ - mujica.AvatarGroup(c, displayPeople[:int(s.count)], mujica.AvatarGroupOptions{Max: int(s.max)}), - mujica.AvatarGroup(c, displayPeople, mujica.AvatarGroupOptions{Max: 4, Size: 24}), + for i, g := range []display.AvatarGroupView{ + display.AvatarGroup(c, displayPeople[:int(s.count)], display.AvatarGroupOptions{Max: int(s.max)}), + display.AvatarGroup(c, displayPeople, display.AvatarGroupOptions{Max: 4, Size: 24}), } { if g.Toggled { log(fmt.Sprintf("group %d open=%v", i+1, g.Open)) @@ -202,7 +204,7 @@ func avatarGroupDemo(c *ui.Context, log func(string)) { }) } -const avatarGroupCode = `mujica.AvatarGroup(c, people, mujica.AvatarGroupOptions{Max: 3})` +const avatarGroupCode = `display.AvatarGroup(c, people, display.AvatarGroupOptions{Max: 3})` type badgeState struct { count, max float64 @@ -210,14 +212,14 @@ type badgeState struct { tone, pos string } -var badgeTones = map[string]mujica.BadgeTone{"Accent": mujica.BadgeAccent, "Neutral": mujica.BadgeNeutral, "Success": mujica.BadgeSuccess, - "Warning": mujica.BadgeWarning, "Danger": mujica.BadgeDanger, "Info": mujica.BadgeInfo} -var badgePositions = map[string]mujica.BadgePosition{"TopRight": mujica.BadgeTopRight, "TopLeft": mujica.BadgeTopLeft, - "BottomRight": mujica.BadgeBottomRight, "BottomLeft": mujica.BadgeBottomLeft} +var badgeTones = map[string]display.BadgeTone{"Accent": display.BadgeAccent, "Neutral": display.BadgeNeutral, "Success": display.BadgeSuccess, + "Warning": display.BadgeWarning, "Danger": display.BadgeDanger, "Info": display.BadgeInfo} +var badgePositions = map[string]display.BadgePosition{"TopRight": display.BadgeTopRight, "TopLeft": display.BadgeTopLeft, + "BottomRight": display.BadgeBottomRight, "BottomLeft": display.BadgeBottomLeft} func badgeDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "017", func() badgeState { return badgeState{count: 120, max: 99, tone: "Accent", pos: "TopRight"} }) - mujica.Heading(c, "Badge", mujica.HeadingOptions{Subtitle: "文字或计数、上限、位置与完整朗读", Divider: true}) + display.Heading(c, "Badge", display.HeadingOptions{Subtitle: "文字或计数、上限、位置与完整朗读", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { ui.Text(c, fmt.Sprintf("Count %d", int(s.count))) @@ -233,24 +235,24 @@ func badgeDemo(c *ui.Context, log func(string)) { } }) }) - opts := mujica.BadgeOptions{Text: s.text, Count: int(s.count), Max: int(s.max), Tone: badgeTones[s.tone], Label: "Unread"} + opts := display.BadgeOptions{Text: s.text, Count: int(s.count), Max: int(s.max), Tone: badgeTones[s.tone], Label: "Unread"} displaySection(c, "Inline", func() { ui.Row(c).Gap(8).AlignItems(ui.Center).Children(func() { ui.Text(c, "Inbox") - mujica.Badge(c, opts, nil) + display.Badge(c, opts, nil) }) }) displaySection(c, "Corner", func() { opts.Position = badgePositions[s.pos] ui.Box(c).Padding(10).Children(func() { - mujica.Badge(c, opts, func() { mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) }) + display.Badge(c, opts, func() { display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{Size: 40}) }) }) }) } -const badgeCode = `mujica.Badge(c, mujica.BadgeOptions{Text: "New", Tone: mujica.BadgeNeutral}, nil) -mujica.Badge(c, mujica.BadgeOptions{Count: unread, Label: "Unread", Position: mujica.BadgeTopRight}, func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{}) +const badgeCode = `display.Badge(c, display.BadgeOptions{Text: "New", Tone: display.BadgeNeutral}, nil) +display.Badge(c, display.BadgeOptions{Count: unread, Label: "Unread", Position: display.BadgeTopRight}, func() { + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{}) })` type tagState struct { @@ -262,14 +264,14 @@ func tagDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "018", func() tagState { return tagState{tags: []string{"Baroque", "Gothic", "Rococo", "Court"}, icon: true, clickable: true, closable: true} }) - mujica.Heading(c, "Tag", mujica.HeadingOptions{Subtitle: "标签文字、图标;点击与关闭分离", Divider: true}) + display.Heading(c, "Tag", display.HeadingOptions{Subtitle: "标签文字、图标;点击与关闭分离", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { displayLog(log, ui.Checkbox(c, &s.icon, "Icon"), "icon", &s.icon) displayLog(log, ui.Checkbox(c, &s.clickable, "Clickable"), "clickable", &s.clickable) displayLog(log, ui.Checkbox(c, &s.closable, "Closable"), "closable", &s.closable) displayLog(log, ui.Checkbox(c, &s.dis, "Disabled"), "dis", &s.dis) - if mujica.Button(c, "Reset tags", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Reset tags", input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.tags = []string{"Baroque", "Gothic", "Rococo", "Court"} log("reset") } @@ -283,9 +285,9 @@ func tagDemo(c *ui.Context, log func(string)) { ui.Row(c).Gap(8).Wrap().Children(func() { for i := 0; i < len(s.tags); i++ { t := s.tags[i] - var v mujica.TagView + var v display.TagView ui.Box(c).Key(t).Children(func() { - v = mujica.Tag(c, t, mujica.TagOptions{Icon: icon, Clickable: s.clickable, Closable: s.closable, Disabled: s.dis}) + v = display.Tag(c, t, display.TagOptions{Icon: icon, Clickable: s.clickable, Closable: s.closable, Disabled: s.dis}) }) if v.Clicked { log("clicked " + t) @@ -301,9 +303,9 @@ func tagDemo(c *ui.Context, log func(string)) { } const tagCode = `for i, t := range tags { - var v mujica.TagView + var v display.TagView ui.Box(c).Key(t).Children(func() { - v = mujica.Tag(c, t, mujica.TagOptions{Icon: "star", Clickable: true, Closable: true}) + v = display.Tag(c, t, display.TagOptions{Icon: "star", Clickable: true, Closable: true}) }) if v.Closed { tags = append(tags[:i:i], tags[i+1:]...) @@ -317,32 +319,32 @@ type linkState struct { func linkDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "019", func() linkState { return linkState{url: "https://example.com/charter"} }) - mujica.Heading(c, "Link", mujica.HeadingOptions{Subtitle: "外部地址或应用内动作;键盘激活;打开失败反馈", Divider: true}) + display.Heading(c, "Link", display.HeadingOptions{Subtitle: "外部地址或应用内动作;键盘激活;打开失败反馈", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { displayLog(log, ui.TextInput(c, &s.url).Placeholder("URL (empty = in-app action)").Width(320), "url", &s.url) - if mujica.Button(c, "Simulate open failure", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Simulate open failure", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.external = "No application can open this link." log("open failure set") } - if mujica.Button(c, "Clear error", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Clear error", input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.external = "" log("open failure cleared") } }) }) displaySection(c, "Preview", func() { - if mujica.Link(c, "Read the court charter", mujica.LinkOptions{URL: s.url, Error: s.external}).Clicked() { + if display.Link(c, "Read the court charter", display.LinkOptions{URL: s.url, Error: s.external}).Clicked() { log("activated " + s.url) } - if mujica.Link(c, "Show the guest list", mujica.LinkOptions{}).Clicked() { + if display.Link(c, "Show the guest list", display.LinkOptions{}).Clicked() { log("in-app action") } }) } -const linkCode = `mujica.Link(c, "Read the charter", mujica.LinkOptions{URL: "https://example.com/charter"}) -if mujica.Link(c, "Show details", mujica.LinkOptions{}).Clicked() { +const linkCode = `display.Link(c, "Read the charter", display.LinkOptions{URL: "https://example.com/charter"}) +if display.Link(c, "Show details", display.LinkOptions{}).Clicked() { details = !details }` @@ -350,7 +352,7 @@ type kbdState struct{ combo string } func kbdDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "020", func() kbdState { return kbdState{combo: "Mod+Shift+P"} }) - mujica.Heading(c, "Kbd", mujica.HeadingOptions{Subtitle: "单键与组合键;按平台显示 Command 或 Ctrl", Divider: true}) + display.Heading(c, "Kbd", display.HeadingOptions{Subtitle: "单键与组合键;按平台显示 Command 或 Ctrl", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { if ui.TextInput(c, &s.combo).Placeholder("Keys joined by +").Width(220).Changed() { @@ -366,20 +368,20 @@ func kbdDemo(c *ui.Context, log func(string)) { } } if len(keys) > 0 { - mujica.Kbd(c, keys...) + display.Kbd(c, keys...) } }) displaySection(c, "Common", func() { for _, row := range [][]string{{"Mod", "K"}, {"Mod", "Shift", "P"}, {"Alt", "Up"}, {"Esc"}, {"Ctrl", "Tab"}} { ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { - mujica.Kbd(c, row...) - ui.Text(c, strings.Join(row, " + ")).TextColor(mujica.Tokens(c).TextMuted) + display.Kbd(c, row...) + ui.Text(c, strings.Join(row, " + ")).TextColor(core.Tokens(c).TextMuted) }) } }) } const kbdCode = `ui.Row(c).Gap(6).Children(func() { - mujica.Text(c, "Command palette", mujica.TextOptions{}) - mujica.Kbd(c, "Mod", "Shift", "P") + display.Text(c, "Command palette", display.TextOptions{}) + display.Kbd(c, "Mod", "Shift", "P") })` diff --git a/examples/gallery/pages_display_media.go b/examples/gallery/pages_display_media.go index b22abc5..d3517e3 100644 --- a/examples/gallery/pages_display_media.go +++ b/examples/gallery/pages_display_media.go @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" ) type imageState struct { @@ -22,7 +23,7 @@ var displayPicture = func() *ui.SVG { func imageDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "014", func() imageState { return imageState{crop: true} }) - mujica.Heading(c, "Image", mujica.HeadingOptions{Subtitle: "位图与 SVG;包含或裁切;占位、加载、错误;替代说明", Divider: true}) + display.Heading(c, "Image", display.HeadingOptions{Subtitle: "位图与 SVG;包含或裁切;占位、加载、错误;替代说明", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { displayLog(log, ui.Checkbox(c, &s.crop, "Crop"), "crop", &s.crop) @@ -42,7 +43,7 @@ func imageDemo(c *ui.Context, log func(string)) { if s.empty { src = nil } - v := mujica.Image(c, src, mujica.ImageOptions{Width: 200, Height: 200, Crop: s.crop, Alt: "Gothic emblem", Loading: s.loading, Err: s.err}) + v := display.Image(c, src, display.ImageOptions{Width: 200, Height: 200, Crop: s.crop, Alt: "Gothic emblem", Loading: s.loading, Err: s.err}) if v.Retry { s.fail, s.err = false, nil log("retry") @@ -50,18 +51,18 @@ func imageDemo(c *ui.Context, log func(string)) { }) displaySection(c, "States", func() { ui.Row(c).Gap(12).Wrap().Children(func() { - mujica.Image(c, displayPicture, mujica.ImageOptions{Width: 120, Height: 120}) - mujica.Image(c, displayPicture, mujica.ImageOptions{Width: 120, Height: 120, Crop: true}) - mujica.Image(c, nil, mujica.ImageOptions{Width: 120, Height: 120}) - mujica.Image(c, nil, mujica.ImageOptions{Width: 120, Height: 120, Loading: true}) - if mujica.Image(c, nil, mujica.ImageOptions{Width: 160, Height: 140, Err: errors.New("404")}).Retry { + display.Image(c, displayPicture, display.ImageOptions{Width: 120, Height: 120}) + display.Image(c, displayPicture, display.ImageOptions{Width: 120, Height: 120, Crop: true}) + display.Image(c, nil, display.ImageOptions{Width: 120, Height: 120}) + display.Image(c, nil, display.ImageOptions{Width: 120, Height: 120, Loading: true}) + if display.Image(c, nil, display.ImageOptions{Width: 160, Height: 140, Err: errors.New("404")}).Retry { log("retry (static example)") } }) }) } -const imageCode = `img := mujica.Image(c, photo, mujica.ImageOptions{ +const imageCode = `img := display.Image(c, photo, display.ImageOptions{ Width: 160, Height: 120, Crop: true, Alt: "Hall of mirrors", Loading: loading, Err: loadErr, }) @@ -77,14 +78,14 @@ type avatarState struct { var displayBitmap = func() *ui.Bitmap { return ui.NewBitmap(ui.Render(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mujica.Dark}) + core.Use(c, core.Settings{Mode: core.Dark}) ui.Box(c).Fill().Background(ui.Hex("#7D2034")) }, 64, 64, 1)) }() func avatarDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "015", func() avatarState { return avatarState{name: "Sakiko Togawa", size: 48} }) - mujica.Heading(c, "Avatar", mujica.HeadingOptions{Subtitle: "图片、姓名首字母与默认图标回退", Divider: true}) + display.Heading(c, "Avatar", display.HeadingOptions{Subtitle: "图片、姓名首字母与默认图标回退", Divider: true}) displaySection(c, "Parameters", func() { displayControls(c, func() { if ui.TextInput(c, &s.name).Width(200).Changed() { @@ -100,19 +101,19 @@ func avatarDemo(c *ui.Context, log func(string)) { if s.img { img = displayBitmap } - mujica.Avatar(c, s.name, mujica.AvatarOptions{Size: float32(s.size), Image: img}) + display.Avatar(c, s.name, display.AvatarOptions{Size: float32(s.size), Image: img}) }) displaySection(c, "Fallbacks", func() { ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { - mujica.Avatar(c, "Photo", mujica.AvatarOptions{Image: displayBitmap, Size: 40}) - mujica.Avatar(c, "Uika Misumi", mujica.AvatarOptions{Size: 40}) - mujica.Avatar(c, "若叶睦", mujica.AvatarOptions{Size: 40}) - mujica.Avatar(c, "", mujica.AvatarOptions{Size: 40}) + display.Avatar(c, "Photo", display.AvatarOptions{Image: displayBitmap, Size: 40}) + display.Avatar(c, "Uika Misumi", display.AvatarOptions{Size: 40}) + display.Avatar(c, "若叶睦", display.AvatarOptions{Size: 40}) + display.Avatar(c, "", display.AvatarOptions{Size: 40}) }) }) } const avatarCode = `ui.Row(c).Gap(8).Children(func() { - mujica.Avatar(c, "Sakiko Togawa", mujica.AvatarOptions{Size: 40}) - mujica.Avatar(c, "", mujica.AvatarOptions{}) + display.Avatar(c, "Sakiko Togawa", display.AvatarOptions{Size: 40}) + display.Avatar(c, "", display.AvatarOptions{}) })` diff --git a/examples/gallery/pages_display_parts.go b/examples/gallery/pages_display_parts.go index e724d2d..cbf20dc 100644 --- a/examples/gallery/pages_display_parts.go +++ b/examples/gallery/pages_display_parts.go @@ -5,13 +5,13 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/display" ) // displaySection shows a titled block of a demo. func displaySection(c *ui.Context, title string, fn func()) { ui.Column(c).Gap(10).Margin(0, 0, 20, 0).Children(func() { - mujica.Heading(c, title, mujica.HeadingOptions{Level: 3}) + display.Heading(c, title, display.HeadingOptions{Level: 3}) fn() }) } diff --git a/examples/gallery/pages_essentials.go b/examples/gallery/pages_essentials.go index 6562f5f..247115f 100644 --- a/examples/gallery/pages_essentials.go +++ b/examples/gallery/pages_essentials.go @@ -5,9 +5,9 @@ var essentialsPages = []page{ ID: "481", Name: "SplitButton", ZhName: "拆分按钮 主操作 下拉", Category: "Essentials", Demo: splitButtonDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Save as…", mujica.PopupMenuItemOptions{}) { + core.Use(c, core.Settings{}) + r := overlay.SplitButton(c, "Save", overlay.SplitButtonOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Save as…", overlay.PopupMenuItemOptions{}) { // ask for a file name } }) @@ -21,8 +21,8 @@ var essentialsPages = []page{ ID: "482", Name: "CopyButton", ZhName: "复制按钮 剪贴板", Category: "Essentials", Demo: copyButtonDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CopyButton(c, "https://example.com/invite/42", mujica.CopyButtonOptions{Label: "Copy link"}) + core.Use(c, core.Settings{}) + input.CopyButton(c, "https://example.com/invite/42", input.CopyButtonOptions{Label: "Copy link"}) }`, Keys: "Tab focuses; Enter or Space copies. The copied state is announced.", }, @@ -30,8 +30,8 @@ var essentialsPages = []page{ ID: "483", Name: "HoldToConfirm", ZhName: "按住确认 危险操作", Category: "Essentials", Demo: holdToConfirmDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{}).Changed() { + core.Use(c, core.Settings{}) + if input.HoldToConfirm(c, "Delete project", input.HoldToConfirmOptions{}).Changed() { // delete the project } }`, @@ -42,9 +42,9 @@ var essentialsPages = []page{ Demo: bulkActionBarDemo, Code: `selected := 3 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BulkActionBar(c, selected, mujica.BulkActionBarOptions{}, func() { - mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + r := input.BulkActionBar(c, selected, input.BulkActionBarOptions{}, func() { + input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) }) if r.Changed() { selected = 0 @@ -57,8 +57,8 @@ view := func(c *ui.Context) { Demo: scrubInputDemo, Code: `opacity := 0.6 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScrubInput(c, "Opacity", &opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.1}) + core.Use(c, core.Settings{}) + input.ScrubInput(c, "Opacity", &opacity, input.ScrubInputOptions{Max: 1, Step: 0.1}) }`, Keys: "The label is pointer-only; in the field, Up and Down step and typing sets the value.", }, @@ -67,8 +67,8 @@ view := func(c *ui.Context) { Demo: currencyInputDemo, Code: `cents := int64(123450) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CurrencyInput(c, ¢s, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price"}) + core.Use(c, core.Settings{}) + input.CurrencyInput(c, ¢s, input.CurrencyInputOptions{Symbol: "$", Label: "Price"}) }`, Keys: "Tab focuses the field, which shows plain digits while focused and groups thousands when left.", }, @@ -77,22 +77,22 @@ view := func(c *ui.Context) { Demo: maskedInputDemo, Code: `phone := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MaskedInput(c, &phone, mujica.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) + core.Use(c, core.Settings{}) + input.MaskedInput(c, &phone, input.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) }`, Keys: "Arrows, Home and End move the caret; Backspace and Delete remove; paste keeps the characters that fit; Enter submits.", }, { ID: "488", Name: "MentionInput", ZhName: "提及输入 @ 候选", Category: "Essentials", Demo: mentionInputDemo, - Code: `text, mentions := "", []mujica.Mention(nil) -people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} + Code: `text, mentions := "", []input.Mention(nil) +people := []input.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MentionInput(c, &text, &mentions, mujica.MentionInputOptions{ + core.Use(c, core.Settings{}) + input.MentionInput(c, &text, &mentions, input.MentionInputOptions{ Label: "Comment", - Candidates: func(q string) []mujica.MentionCandidate { - var out []mujica.MentionCandidate + Candidates: func(q string) []input.MentionCandidate { + var out []input.MentionCandidate for _, p := range people { if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { out = append(out, p) @@ -109,10 +109,10 @@ view := func(c *ui.Context) { Demo: choiceChipsDemo, Code: `var status []string view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChoiceChips(c, &status, []mujica.ToggleItem[string]{ + core.Use(c, core.Settings{}) + input.ChoiceChips(c, &status, []input.ToggleItem[string]{ {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, - }, mujica.ChoiceChipsOptions{Multiple: true, Label: "Status"}) + }, input.ChoiceChipsOptions{Multiple: true, Label: "Status"}) }`, Keys: "The chips are one Tab stop; Left and Right move, skipping disabled chips; Space or Enter toggles.", }, @@ -121,8 +121,8 @@ view := func(c *ui.Context) { Demo: knobDemo, Code: `gain := 35.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Knob(c, &gain, mujica.KnobOptions{Label: "Gain", Ticks: 11}) + core.Use(c, core.Settings{}) + input.Knob(c, &gain, input.KnobOptions{Label: "Gain", Ticks: 11}) }`, Keys: "Arrows step the value; Home and End go to the ends; drag up or down with the pointer.", }, @@ -131,8 +131,8 @@ view := func(c *ui.Context) { Demo: verticalSliderDemo, Code: `level := 40.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VerticalSlider(c, &level, mujica.VerticalSliderOptions{Step: 10, Label: "Level"}) + core.Use(c, core.Settings{}) + input.VerticalSlider(c, &level, input.VerticalSliderOptions{Step: 10, Label: "Level"}) }`, Keys: "Up and Right raise the value, Down and Left lower it, Home and End go to the ends.", }, @@ -141,23 +141,23 @@ view := func(c *ui.Context) { Demo: colorPaletteDemo, Code: `color := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPalette(c, &color, []mujica.ColorSwatch{ + core.Use(c, core.Settings{}) + input.ColorPalette(c, &color, []input.ColorSwatch{ {Color: ui.Hex("#7D2034"), Name: "Wine"}, {Color: ui.Hex("#AD9164"), Name: "Old gold"}, {Color: ui.Hex("#355E80"), Name: "Slate blue"}, - }, mujica.ColorPaletteOptions{Custom: true, Label: "Tag color"}) + }, input.ColorPaletteOptions{Custom: true, Label: "Tag color"}) }`, Keys: "The palette is one Tab stop; Left and Right move by one swatch, Up and Down by a row; Enter or Space chooses.", }, { ID: "493", Name: "SignaturePad", ZhName: "签名板 手写", Category: "Essentials", Demo: signaturePadDemo, - Code: `var strokes [][]mujica.SignaturePoint + Code: `var strokes [][]input.SignaturePoint view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SignaturePad(c, &strokes, mujica.SignaturePadOptions{}).Changed() { - _ = mujica.SignaturePath(strokes) // SVG path data to store + core.Use(c, core.Settings{}) + if input.SignaturePad(c, &strokes, input.SignaturePadOptions{}).Changed() { + _ = input.SignaturePath(strokes) // SVG path data to store } }`, Keys: "Tab focuses the pad; Delete or Backspace clears it. Drawing needs the pointer.", @@ -167,9 +167,9 @@ view := func(c *ui.Context) { Demo: bannerDemo, Code: `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Banner(c, &open, "Maintenance tonight at 22:00.", mujica.BannerOptions{ - Severity: mujica.SeverityWarning, + core.Use(c, core.Settings{}) + r := input.Banner(c, &open, "Maintenance tonight at 22:00.", input.BannerOptions{ + Severity: core.SeverityWarning, Action: "Details", }) if r.Action { @@ -183,8 +183,8 @@ view := func(c *ui.Context) { Demo: undoToastDemo, Code: `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UndoToast(c, &open, "Note deleted", mujica.UndoToastOptions{ + core.Use(c, core.Settings{}) + feedback.UndoToast(c, &open, "Note deleted", feedback.UndoToastOptions{ OnUndo: func() { // restore the note }, @@ -197,8 +197,8 @@ view := func(c *ui.Context) { Demo: masonryDemo, Code: `titles := []string{"Sea", "Forest", "Desert", "Glacier"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Masonry(c, len(titles), mujica.MasonryOptions{ + core.Use(c, core.Settings{}) + input.Masonry(c, len(titles), input.MasonryOptions{ Key: func(i int) string { return titles[i] }, }, func(i int) { ui.Text(c, titles[i]) @@ -211,11 +211,11 @@ view := func(c *ui.Context) { Demo: resizablePanelGroupDemo, Code: `var sizes []float32 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ResizablePanelGroup(c, &sizes, []mujica.ResizablePanel{ + core.Use(c, core.Settings{}) + r := input.ResizablePanelGroup(c, &sizes, []input.ResizablePanel{ {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, - }, mujica.ResizablePanelGroupOptions{}) + }, input.ResizablePanelGroupOptions{}) if r.Committed { // save sizes } @@ -225,13 +225,13 @@ view := func(c *ui.Context) { { ID: "498", Name: "DockPanel", ZhName: "停靠面板 标签", Category: "Essentials", Demo: dockPanelDemo, - Code: `var layout mujica.DockLayout + Code: `var layout input.DockLayout view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.DockPanel(c, &layout, []mujica.DockItem{ + core.Use(c, core.Settings{}) + r := input.DockPanel(c, &layout, []input.DockItem{ {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, - }, mujica.DockPanelOptions{}) + }, input.DockPanelOptions{}) if r.Changed() { // save layout } @@ -242,20 +242,20 @@ view := func(c *ui.Context) { ID: "499", Name: "QRCode", ZhName: "二维码", Category: "Essentials", Demo: qrCodeDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QRCode(c, "https://example.com/mujica", mujica.QRCodeOptions{Level: mujica.QRQuartile}) + core.Use(c, core.Settings{}) + input.QRCode(c, "https://example.com/mujica", input.QRCodeOptions{Level: input.QRQuartile}) }`, Keys: "Not focusable; assistive technology reads its name.", }, { ID: "500", Name: "ActivityFeed", ZhName: "活动流 动态", Category: "Essentials", Demo: activityFeedDemo, - Code: `events := []mujica.Activity{ + Code: `events := []feedback.Activity{ {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ActivityFeed(c, events, mujica.ActivityFeedOptions{}).Size(380, 280) + core.Use(c, core.Settings{}) + feedback.ActivityFeed(c, events, feedback.ActivityFeedOptions{}).Size(380, 280) }`, Keys: "Scroll with the wheel or the scrollbar; only rows in view are built.", }, diff --git a/examples/gallery/pages_essentials_demos.go b/examples/gallery/pages_essentials_demos.go index 6e5ab05..166bfda 100644 --- a/examples/gallery/pages_essentials_demos.go +++ b/examples/gallery/pages_essentials_demos.go @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" ) func splitButtonDemo(c *ui.Context, log func(string)) { @@ -20,14 +21,14 @@ func splitButtonDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Live", func() { - v := mujica.Primary + v := input.Primary if s.variant { - v = mujica.Secondary + v = input.Secondary } ui.Row(c).Children(func() { - r := mujica.SplitButton(c, "Save", mujica.SplitButtonOptions{Variant: v, Disabled: s.disabled}, func(m *mujica.PopupMenu) { + r := overlay.SplitButton(c, "Save", overlay.SplitButtonOptions{Variant: v, Disabled: s.disabled}, func(m *overlay.PopupMenu) { for _, it := range []string{"Save as…", "Save a copy", "Save all"} { - if m.Item(it, mujica.PopupMenuItemOptions{}) { + if m.Item(it, overlay.PopupMenuItemOptions{}) { log("split button: menu " + it) } } @@ -53,10 +54,10 @@ func copyButtonDemo(c *ui.Context, log func(string)) { }) feedbackSection(c, "Live", func() { ui.Row(c).Gap(12).Children(func() { - if mujica.CopyButton(c, s.text, mujica.CopyButtonOptions{Disabled: s.disabled}).Changed() { + if input.CopyButton(c, s.text, input.CopyButtonOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("copy button: copied %d characters", len([]rune(s.text)))) } - if mujica.CopyButton(c, s.text, mujica.CopyButtonOptions{Label: "Copy link", Disabled: s.disabled}).Changed() { + if input.CopyButton(c, s.text, input.CopyButtonOptions{Label: "Copy link", Disabled: s.disabled}).Changed() { log("copy button: copied with label") } }) @@ -82,7 +83,7 @@ func holdToConfirmDemo(c *ui.Context, log func(string)) { feedbackSection(c, "Live (turn on reduced motion to see the countdown text)", func() { ui.Row(c).Children(func() { d := time.Duration(s.seconds * float64(time.Second)) - if mujica.HoldToConfirm(c, "Delete project", mujica.HoldToConfirmOptions{Duration: d, Disabled: s.disabled}).Changed() { + if input.HoldToConfirm(c, "Delete project", input.HoldToConfirmOptions{Duration: d, Disabled: s.disabled}).Changed() { s.deleted++ log("hold to confirm: confirmed") } @@ -113,11 +114,11 @@ func bulkActionBarDemo(c *ui.Context, log func(string)) { n++ } } - r := mujica.BulkActionBar(c, n, mujica.BulkActionBarOptions{Disabled: s.disabled}, func() { - if mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + r := input.BulkActionBar(c, n, input.BulkActionBarOptions{Disabled: s.disabled}, func() { + if input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}).Clicked() { log(fmt.Sprintf("bulk bar: archive %d", n)) } - if mujica.Button(c, "Delete", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + if input.Button(c, "Delete", input.ButtonOptions{Variant: input.Danger}).Clicked() { log(fmt.Sprintf("bulk bar: delete %d", n)) } }) @@ -137,10 +138,10 @@ func scrubInputDemo(c *ui.Context, log func(string)) { ui.Column(c).Gap(16).FillWidth().Children(func() { ui.Checkbox(c, &s.disabled, "Disabled") feedbackSection(c, "Drag a label sideways, or type", func() { - if mujica.ScrubInput(c, "Opacity", &s.opacity, mujica.ScrubInputOptions{Max: 1, Step: 0.05, Disabled: s.disabled}).Changed() { + if input.ScrubInput(c, "Opacity", &s.opacity, input.ScrubInputOptions{Max: 1, Step: 0.05, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("scrub: opacity %.2f", s.opacity)) } - if mujica.ScrubInput(c, "Size", &s.size, mujica.ScrubInputOptions{Min: 8, Max: 72, Disabled: s.disabled}).Changed() { + if input.ScrubInput(c, "Size", &s.size, input.ScrubInputOptions{Min: 8, Max: 72, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("scrub: size %.0f", s.size)) } }) @@ -157,14 +158,14 @@ func currencyInputDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.disabled, "Disabled") feedbackSection(c, "Live", func() { ui.Row(c).Gap(12).Children(func() { - if mujica.CurrencyInput(c, &s.cents, mujica.CurrencyInputOptions{Symbol: "$", Label: "Price", Disabled: s.disabled}).Changed() { + if input.CurrencyInput(c, &s.cents, input.CurrencyInputOptions{Symbol: "$", Label: "Price", Disabled: s.disabled}).Changed() { log(fmt.Sprintf("currency: %d cents", s.cents)) } - if mujica.CurrencyInput(c, &s.yen, mujica.CurrencyInputOptions{Symbol: "¥", NoFraction: true, Label: "Yen", Disabled: s.disabled}).Changed() { + if input.CurrencyInput(c, &s.yen, input.CurrencyInputOptions{Symbol: "¥", NoFraction: true, Label: "Yen", Disabled: s.disabled}).Changed() { log(fmt.Sprintf("currency: %d yen", s.yen)) } }) - ui.Text(c, "Stored as "+mujica.CurrencyFormat(s.cents, 2, true)+" and "+mujica.CurrencyFormat(s.yen, 0, true)) + ui.Text(c, "Stored as "+input.CurrencyFormat(s.cents, 2, true)+" and "+input.CurrencyFormat(s.yen, 0, true)) }) }) } @@ -186,7 +187,7 @@ func maskedInputDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.disabled, "Disabled") }) feedbackSection(c, "Live (try an input method: nothing commits until it does)", func() { - r := mujica.MaskedInput(c, &s.value, mujica.MaskedInputOptions{Mask: masks[s.mask], Label: "Masked field", Disabled: s.disabled}) + r := input.MaskedInput(c, &s.value, input.MaskedInputOptions{Mask: masks[s.mask], Label: "Masked field", Disabled: s.disabled}) r.Element.Width(260) if r.Changed() { log(fmt.Sprintf("masked: %d characters filled", len([]rune(s.value)))) @@ -204,7 +205,7 @@ func choiceChipsDemo(c *ui.Context, log func(string)) { multiple, disabled bool } s := ui.Local(c.Root(), "489", func() state { return state{multiple: true, value: []string{"open"}} }) - items := []mujica.ToggleItem[string]{{Value: "open", Label: "Open"}, {Value: "draft", Label: "Draft"}, {Value: "review", Label: "In review"}, {Value: "closed", Label: "Closed", Disabled: true}, {Value: "merged", Label: "Merged"}} + items := []input.ToggleItem[string]{{Value: "open", Label: "Open"}, {Value: "draft", Label: "Draft"}, {Value: "review", Label: "In review"}, {Value: "closed", Label: "Closed", Disabled: true}, {Value: "merged", Label: "Merged"}} ui.Column(c).Gap(16).FillWidth().Children(func() { feedbackSection(c, "Parameters", func() { ui.Row(c).Gap(16).Children(func() { @@ -215,7 +216,7 @@ func choiceChipsDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Live", func() { - if mujica.ChoiceChips(c, &s.value, items, mujica.ChoiceChipsOptions{Multiple: s.multiple, Disabled: s.disabled, Label: "Status"}).Changed() { + if input.ChoiceChips(c, &s.value, items, input.ChoiceChipsOptions{Multiple: s.multiple, Disabled: s.disabled, Label: "Status"}).Changed() { log(fmt.Sprintf("chips: %v", s.value)) } }) @@ -231,10 +232,10 @@ func knobDemo(c *ui.Context, log func(string)) { ui.Column(c).Gap(16).FillWidth().Children(func() { ui.Checkbox(c, &s.disabled, "Disabled") ui.Row(c).Gap(32).Children(func() { - if mujica.Knob(c, &s.gain, mujica.KnobOptions{Label: "Gain", Ticks: 11, Disabled: s.disabled}).Changed() { + if input.Knob(c, &s.gain, input.KnobOptions{Label: "Gain", Ticks: 11, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("knob: gain %.1f", s.gain)) } - if mujica.Knob(c, &s.mix, mujica.KnobOptions{Max: 1, Step: 0.1, Size: 72, Label: "Mix", Disabled: s.disabled}).Changed() { + if input.Knob(c, &s.mix, input.KnobOptions{Max: 1, Step: 0.1, Size: 72, Label: "Mix", Disabled: s.disabled}).Changed() { log(fmt.Sprintf("knob: mix %.1f", s.mix)) } }) @@ -254,11 +255,11 @@ func verticalSliderDemo(c *ui.Context, log func(string)) { }) ui.Row(c).Gap(16).Children(func() { for i := range s.levels { - opts := mujica.VerticalSliderOptions{Step: 10, ShowValue: s.showValue, Disabled: s.disable, Label: fmt.Sprintf("Band %d", i+1)} + opts := input.VerticalSliderOptions{Step: 10, ShowValue: s.showValue, Disabled: s.disable, Label: fmt.Sprintf("Band %d", i+1)} if i == 0 { - opts.Marks = []mujica.SliderMark{{Value: 0, Label: "0"}, {Value: 50, Label: "50"}, {Value: 100, Label: "100"}} + opts.Marks = []input.SliderMark{{Value: 0, Label: "0"}, {Value: 50, Label: "50"}, {Value: 100, Label: "100"}} } - if mujica.VerticalSlider(c, &s.levels[i], opts).Changed() { + if input.VerticalSlider(c, &s.levels[i], opts).Changed() { log(fmt.Sprintf("vertical slider: band %d = %.0f", i+1, s.levels[i])) } } diff --git a/examples/gallery/pages_essentials_more.go b/examples/gallery/pages_essentials_more.go index 0334815..26fc0ae 100644 --- a/examples/gallery/pages_essentials_more.go +++ b/examples/gallery/pages_essentials_more.go @@ -8,24 +8,26 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func mentionInputDemo(c *ui.Context, log func(string)) { type state struct { text string - mentions []mujica.Mention + mentions []input.Mention disabled bool } - people := []mujica.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Albert"}, {ID: "u3", Name: "Bob"}, {ID: "u4", Name: "Sakiko"}, {ID: "u5", Name: "Mutsumi"}} + people := []input.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Albert"}, {ID: "u3", Name: "Bob"}, {ID: "u4", Name: "Sakiko"}, {ID: "u5", Name: "Mutsumi"}} s := ui.Local(c.Root(), "488", func() state { return state{} }) ui.Column(c).Gap(16).FillWidth().Children(func() { ui.Checkbox(c, &s.disabled, "Disabled") feedbackSection(c, "Type @ to mention someone", func() { - r := mujica.MentionInput(c, &s.text, &s.mentions, mujica.MentionInputOptions{ + r := input.MentionInput(c, &s.text, &s.mentions, input.MentionInputOptions{ Placeholder: "Write a comment…", Label: "Comment", Disabled: s.disabled, - Candidates: func(q string) []mujica.MentionCandidate { - var out []mujica.MentionCandidate + Candidates: func(q string) []input.MentionCandidate { + var out []input.MentionCandidate for _, p := range people { if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { out = append(out, p) @@ -54,7 +56,7 @@ func colorPaletteDemo(c *ui.Context, log func(string)) { custom, disabled bool } s := ui.Local(c.Root(), "492", func() state { return state{color: ui.Hex("#7D2034"), custom: true} }) - swatches := []mujica.ColorSwatch{ + swatches := []input.ColorSwatch{ {Color: ui.Hex("#7D2034"), Name: "Wine"}, {Color: ui.Hex("#A12635"), Name: "Crimson"}, {Color: ui.Hex("#AD9164"), Name: "Old gold"}, {Color: ui.Hex("#805714"), Name: "Bronze"}, {Color: ui.Hex("#326247"), Name: "Ivy"}, {Color: ui.Hex("#355E80"), Name: "Slate blue"}, {Color: ui.Hex("#4B3A5A"), Name: "Plum"}, {Color: ui.Hex("#211C1D"), Name: "Ink"}, {Color: ui.Hex("#6A5B55"), Name: "Taupe"}, @@ -65,7 +67,7 @@ func colorPaletteDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.custom, "Custom entry") ui.Checkbox(c, &s.disabled, "Disabled") }) - if mujica.ColorPalette(c, &s.color, swatches, mujica.ColorPaletteOptions{Columns: 6, Custom: s.custom, Disabled: s.disabled, Label: "Tag color"}).Changed() { + if input.ColorPalette(c, &s.color, swatches, input.ColorPaletteOptions{Columns: 6, Custom: s.custom, Disabled: s.disabled, Label: "Tag color"}).Changed() { log(fmt.Sprintf("palette: %v", s.color)) } }) @@ -73,14 +75,14 @@ func colorPaletteDemo(c *ui.Context, log func(string)) { func signaturePadDemo(c *ui.Context, log func(string)) { type state struct { - strokes [][]mujica.SignaturePoint + strokes [][]input.SignaturePoint disabled bool } s := ui.Local(c.Root(), "493", func() state { return state{} }) ui.Column(c).Gap(16).Width(480).Children(func() { ui.Checkbox(c, &s.disabled, "Disabled") - if mujica.SignaturePad(c, &s.strokes, mujica.SignaturePadOptions{Disabled: s.disabled}).Changed() { - log(fmt.Sprintf("signature: %d strokes, %d bytes of path", len(s.strokes), len(mujica.SignaturePath(s.strokes)))) + if input.SignaturePad(c, &s.strokes, input.SignaturePadOptions{Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("signature: %d strokes, %d bytes of path", len(s.strokes), len(input.SignaturePath(s.strokes)))) } }) } @@ -99,7 +101,7 @@ func bannerDemo(c *ui.Context, log func(string)) { ui.Checkbox(c, &s.closable, "Closable") }) }) - opts := mujica.BannerOptions{Severity: feedbackSeverities[s.sev]} + opts := input.BannerOptions{Severity: feedbackSeverities[s.sev]} if s.action { opts.Action = "Details" } @@ -107,14 +109,14 @@ func bannerDemo(c *ui.Context, log func(string)) { if s.closable { open = &s.op } - r := mujica.Banner(c, open, "Scheduled maintenance tonight from 22:00 to 23:00.", opts) + r := input.Banner(c, open, "Scheduled maintenance tonight from 22:00 to 23:00.", opts) if r.Action { log("banner: action") } if r.Closed { log("banner: closed") } - if !s.op && mujica.Button(c, "Show again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if !s.op && input.Button(c, "Show again", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.op = true log("banner: reopened") } @@ -139,7 +141,7 @@ func undoToastDemo(c *ui.Context, log func(string)) { for i, n := range s.notes { ui.Row(c).Key(n).Gap(12).AlignItems(ui.Center).Children(func() { ui.Text(c, n).Grow(1) - if mujica.Button(c, "Delete", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Delete", input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.removed, s.open = n, true s.notes = append(s.notes[:i:i], s.notes[i+1:]...) log("undo toast: deleted " + n) @@ -151,7 +153,7 @@ func undoToastDemo(c *ui.Context, log func(string)) { return } removed := s.removed - r := mujica.UndoToast(c, &s.open, "Deleted “"+removed+"”", mujica.UndoToastOptions{ + r := feedback.UndoToast(c, &s.open, "Deleted “"+removed+"”", feedback.UndoToastOptions{ Duration: time.Duration(s.seconds * float64(time.Second)), OnUndo: func() { s.notes = append(s.notes, removed) }, }) @@ -168,7 +170,7 @@ func masonryDemo(c *ui.Context, log func(string)) { count, width float64 } s := ui.Local(c.Root(), "496", func() state { return state{count: 40, width: 180} }) - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Column(c).Gap(16).FillWidth().Children(func() { ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { ui.Text(c, fmt.Sprintf("Items %.0f", s.count)) @@ -178,7 +180,7 @@ func masonryDemo(c *ui.Context, log func(string)) { ui.Text(c, fmt.Sprintf("Column %.0f", s.width)) ui.Slider(c, &s.width, 120, 320).Width(160) }) - mujica.Masonry(c, int(s.count), mujica.MasonryOptions{Key: strconv.Itoa, ColumnWidth: float32(s.width)}, func(i int) { + input.Masonry(c, int(s.count), input.MasonryOptions{Key: strconv.Itoa, ColumnWidth: float32(s.width)}, func(i int) { card := ui.Column(c).Height(float32(60+i*37%90)).Padding(10).Background(k.Surface).Border(1, k.Border).Radius(4).Cursor(ui.CursorPointer) card.Children(func() { ui.Text(c, "Card "+strconv.Itoa(i+1)) }) if card.Clicked() { @@ -194,7 +196,7 @@ func resizablePanelGroupDemo(c *ui.Context, log func(string)) { vertical, disabled bool } s := ui.Local(c.Root(), "497", func() state { return state{} }) - k := mujica.Tokens(c) + k := core.Tokens(c) pane := func(name string) func() { return func() { ui.Column(c).Fill().Padding(12).Background(k.Surface).Children(func() { ui.Text(c, name) }) @@ -207,11 +209,11 @@ func resizablePanelGroupDemo(c *ui.Context, log func(string)) { } ui.Checkbox(c, &s.disabled, "Disabled") }) - r := mujica.ResizablePanelGroup(c, &s.sizes, []mujica.ResizablePanel{ + r := input.ResizablePanelGroup(c, &s.sizes, []input.ResizablePanel{ {Label: "Files", Collapsible: true, Build: pane("Files")}, {Label: "Editor", Min: 160, Build: pane("Editor")}, {Label: "Outline", Collapsible: true, Build: pane("Outline")}, - }, mujica.ResizablePanelGroupOptions{Vertical: s.vertical, Disabled: s.disabled}) + }, input.ResizablePanelGroupOptions{Vertical: s.vertical, Disabled: s.disabled}) r.Element.FillWidth().Height(320) if r.Committed { log(fmt.Sprintf("panels: save %.2f", s.sizes)) @@ -221,19 +223,19 @@ func resizablePanelGroupDemo(c *ui.Context, log func(string)) { func dockPanelDemo(c *ui.Context, log func(string)) { type state struct { - layout mujica.DockLayout + layout input.DockLayout disabled bool } s := ui.Local(c.Root(), "498", func() state { - return state{layout: mujica.DockLayout{Zones: [4][]string{{"editor"}, {"files"}, {"outline"}, {"terminal", "problems"}}}} + return state{layout: input.DockLayout{Zones: [4][]string{{"editor"}, {"files"}, {"outline"}, {"terminal", "problems"}}}} }) - var items []mujica.DockItem + var items []input.DockItem for _, id := range []string{"files", "editor", "outline", "terminal", "problems"} { - items = append(items, mujica.DockItem{ID: id, Title: strings.ToUpper(id[:1]) + id[1:], Build: func() { ui.Text(c, "Contents of "+id) }}) + items = append(items, input.DockItem{ID: id, Title: strings.ToUpper(id[:1]) + id[1:], Build: func() { ui.Text(c, "Contents of "+id) }}) } ui.Column(c).Gap(16).FillWidth().Children(func() { ui.Checkbox(c, &s.disabled, "Disabled") - r := mujica.DockPanel(c, &s.layout, items, mujica.DockPanelOptions{Disabled: s.disabled}) + r := input.DockPanel(c, &s.layout, items, input.DockPanelOptions{Disabled: s.disabled}) r.Element.FillWidth().Height(440) if r.Changed() { log(fmt.Sprintf("dock: save %v", s.layout.Zones)) @@ -248,7 +250,7 @@ func qrCodeDemo(c *ui.Context, log func(string)) { size float64 } s := ui.Local(c.Root(), "499", func() state { return state{text: "https://example.com/mujica", level: 1, size: 200} }) - levels := []mujica.QRLevel{mujica.QRLow, mujica.QRMedium, mujica.QRQuartile, mujica.QRHigh} + levels := []input.QRLevel{input.QRLow, input.QRMedium, input.QRQuartile, input.QRHigh} ui.Column(c).Gap(16).FillWidth().Children(func() { feedbackSection(c, "Parameters", func() { if ui.TextInput(c, &s.text).Width(360).Changed() { @@ -262,31 +264,31 @@ func qrCodeDemo(c *ui.Context, log func(string)) { ui.Slider(c, &s.size, 100, 320).Width(200) }) }) - mujica.QRCode(c, s.text, mujica.QRCodeOptions{Level: levels[s.level], Size: float32(s.size)}) - if m, err := mujica.EncodeQR([]byte(s.text), levels[s.level]); err == nil { + input.QRCode(c, s.text, input.QRCodeOptions{Level: levels[s.level], Size: float32(s.size)}) + if m, err := input.EncodeQR([]byte(s.text), levels[s.level]); err == nil { ui.Text(c, fmt.Sprintf("Version %d, mask %d, %d × %d modules", m.Version, m.Mask, m.Size(), m.Size())) } }) } func activityFeedDemo(c *ui.Context, log func(string)) { - type state struct{ events []mujica.Activity } + type state struct{ events []feedback.Activity } s := ui.Local(c.Root(), "500", func() state { now := time.Now() - var ev []mujica.Activity + var ev []feedback.Activity actors := []string{"Sakiko", "Mutsumi", "Uika", "Umiri", "Nyamu"} verbs := []string{"commented on", "edited", "approved", "closed", "opened"} for i := range 400 { - ev = append(ev, mujica.Activity{ID: strconv.Itoa(i), Actor: actors[i%5], Action: verbs[i*3%5], Object: fmt.Sprintf("Score #%d", 400-i), Time: now.Add(-time.Duration(i) * 97 * time.Minute)}) + ev = append(ev, feedback.Activity{ID: strconv.Itoa(i), Actor: actors[i%5], Action: verbs[i*3%5], Object: fmt.Sprintf("Score #%d", 400-i), Time: now.Add(-time.Duration(i) * 97 * time.Minute)}) } return state{events: ev} }) ui.Column(c).Gap(16).FillWidth().Children(func() { - if mujica.Button(c, "Add event", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() { - e := mujica.Activity{ID: fmt.Sprint("n", len(s.events)), Actor: "You", Action: "added", Object: "a new note", Time: time.Now()} - s.events = append([]mujica.Activity{e}, s.events...) + if input.Button(c, "Add event", input.ButtonOptions{Variant: input.Secondary}).AlignSelf(ui.Start).Clicked() { + e := feedback.Activity{ID: fmt.Sprint("n", len(s.events)), Actor: "You", Action: "added", Object: "a new note", Time: time.Now()} + s.events = append([]feedback.Activity{e}, s.events...) log("activity: added " + e.ID) } - mujica.ActivityFeed(c, s.events, mujica.ActivityFeedOptions{}).FillWidth().Height(420) + feedback.ActivityFeed(c, s.events, feedback.ActivityFeedOptions{}).FillWidth().Height(420) }) } diff --git a/examples/gallery/pages_feedback.go b/examples/gallery/pages_feedback.go index ea89ade..6eeb7cc 100644 --- a/examples/gallery/pages_feedback.go +++ b/examples/gallery/pages_feedback.go @@ -5,8 +5,8 @@ var feedbackPages = []page{ ID: "091", Name: "Alert", ZhName: "提示条 警告 通知横幅", Category: "Feedback", Demo: alertDemo, Code: `open := true -r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ - Severity: mujica.SeverityError, +r := feedback.Alert(c, &open, "The archive could not be read.", feedback.AlertOptions{ + Severity: core.SeverityError, Title: "Import failed", Action: "Retry", }) @@ -18,11 +18,11 @@ if r.Action { { ID: "092", Name: "Toast", ZhName: "轻提示 消息队列 吐司", Category: "Feedback", Demo: toastDemo, - Code: `if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { - mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) + Code: `if input.Button(c, "Delete note", input.ButtonOptions{Variant: input.Danger}).Clicked() { + feedback.ShowToast(c, feedback.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) } // Last in the view: -if r := mujica.Toaster(c); r.Action == "note-1" { +if r := feedback.Toaster(c); r.Action == "note-1" { // restore the note }`, Keys: "Toasts never take the focus. Tab reaches their buttons after the page; Escape closes the focused toast. Hover or focus pauses its timer.", @@ -30,9 +30,9 @@ if r := mujica.Toaster(c); r.Action == "note-1" { { ID: "093", Name: "Progress", ZhName: "进度条 环形进度 计量条", Category: "Feedback", Demo: progressDemo, - Code: `mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) -mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) -mujica.Meter(c, 93, mujica.MeterOptions{ + Code: `feedback.Progress(c, 0.62, feedback.ProgressOptions{Label: "Uploading", ShowValue: true}) +feedback.Progress(c, 0, feedback.ProgressOptions{Shape: feedback.ProgressRing, Indeterminate: true, Label: "Connecting"}) +feedback.Meter(c, 93, feedback.MeterOptions{ Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, })`, @@ -41,16 +41,16 @@ mujica.Meter(c, 93, mujica.MeterOptions{ { ID: "094", Name: "Spinner", ZhName: "加载标记 加载中 转圈", Category: "Feedback", Demo: spinnerDemo, - Code: `mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true})`, + Code: `core.Spinner(c, core.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true})`, Keys: "Not focusable; read as an indeterminate progress indicator named by Label.", }, { ID: "095", Name: "Skeleton", ZhName: "骨架屏 占位", Category: "Feedback", Demo: skeletonDemo, Code: `ui.Row(c).Gap(12).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + feedback.Skeleton(c, feedback.SkeletonOptions{Shape: feedback.SkeletonCircle}) ui.Column(c).Grow(1).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + feedback.Skeleton(c, feedback.SkeletonOptions{Lines: 2}) }) })`, Keys: "Decorative; not focusable and hidden from assistive technology.", @@ -58,8 +58,8 @@ mujica.Meter(c, 93, mujica.MeterOptions{ { ID: "096", Name: "EmptyState", ZhName: "空状态 无数据 无搜索结果", Category: "Feedback", Demo: emptyStateDemo, - Code: `r := mujica.EmptyState(c, mujica.EmptyStateOptions{ - Kind: mujica.EmptyNoResults, + Code: `r := feedback.EmptyState(c, feedback.EmptyStateOptions{ + Kind: feedback.EmptyNoResults, Query: query, Action: "Clear search", }) @@ -71,8 +71,8 @@ if r.Action { { ID: "097", Name: "Result", ZhName: "结果页 成功 失败 无权限", Category: "Feedback", Demo: resultDemo, - Code: `r := mujica.Result(c, mujica.ResultOptions{ - Kind: mujica.ResultForbidden, + Code: `r := feedback.Result(c, feedback.ResultOptions{ + Kind: feedback.ResultForbidden, Description: "You need the Editor role to change billing.", Actions: []string{"Request access", "Go back"}, }) @@ -85,14 +85,14 @@ case 1: // navigate back { ID: "098", Name: "StatusIndicator", ZhName: "状态标记 在线 离线", Category: "Feedback", Demo: statusDemo, - Code: `mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) -mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{})`, + Code: `feedback.StatusIndicator(c, feedback.StatusSuccess, feedback.StatusIndicatorOptions{Label: "API online"}) +feedback.StatusIndicator(c, feedback.StatusOffline, feedback.StatusIndicatorOptions{})`, Keys: "Not focusable; read as a status with its word.", }, { ID: "099", Name: "NotificationCenter", ZhName: "通知中心 消息列表", Category: "Feedback", Demo: notificationDemo, - Code: `r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) + Code: `r := feedback.NotificationCenter(c, &items, feedback.NotificationCenterOptions{}) if r.Action == "a" { // open the build log }`, @@ -101,9 +101,9 @@ if r.Action == "a" { { ID: "100", Name: "LoadingOverlay", ZhName: "加载遮罩 区域加载", Category: "Feedback", Demo: loadingOverlayDemo, - Code: `r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { + Code: `r := feedback.LoadingOverlay(c, loading, feedback.LoadingOverlayOptions{Cancel: "Cancel"}, func() { ui.Text(c, "Quarterly report") - mujica.Button(c, "Export", mujica.ButtonOptions{}) + input.Button(c, "Export", input.ButtonOptions{}) }) if r.Canceled { loading = false diff --git a/examples/gallery/pages_feedback_demos.go b/examples/gallery/pages_feedback_demos.go index f7c6cbd..a9a17c6 100644 --- a/examples/gallery/pages_feedback_demos.go +++ b/examples/gallery/pages_feedback_demos.go @@ -6,19 +6,21 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // feedbackSection titles a part of a demo. func feedbackSection(c *ui.Context, title string, fn func()) { ui.Column(c).Gap(8).FillWidth().Children(func() { - ui.Text(c, title).FontSize(theme.CaptionSize).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, title).FontSize(theme.CaptionSize).TextColor(core.Tokens(c).TextMuted) fn() }) } -var feedbackSeverities = []mujica.Severity{mujica.SeverityInfo, mujica.SeveritySuccess, mujica.SeverityWarning, mujica.SeverityError} +var feedbackSeverities = []core.Severity{core.SeverityInfo, core.SeveritySuccess, core.SeverityWarning, core.SeverityError} func alertDemo(c *ui.Context, log func(string)) { type state struct { @@ -38,7 +40,7 @@ func alertDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Live", func() { - opts := mujica.AlertOptions{Severity: feedbackSeverities[s.sev]} + opts := feedback.AlertOptions{Severity: feedbackSeverities[s.sev]} if s.title { opts.Title = "Import failed" } @@ -49,21 +51,21 @@ func alertDemo(c *ui.Context, log func(string)) { if s.closable { open = &s.open } - r := mujica.Alert(c, open, "The archive could not be read.", opts) + r := feedback.Alert(c, open, "The archive could not be read.", opts) if r.Action { log("alert: action Retry") } if r.Closed { log("alert: closed") } - if s.closable && !s.open && mujica.Button(c, "Show again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if s.closable && !s.open && input.Button(c, "Show again", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.open = true log("alert: reopened") } }) feedbackSection(c, "Severities", func() { for _, sev := range feedbackSeverities { - mujica.Alert(c, nil, "Severity "+sev.String()+" uses its own icon, bar and name.", mujica.AlertOptions{Severity: sev}).Element.Key(sev) + feedback.Alert(c, nil, "Severity "+sev.String()+" uses its own icon, bar and name.", feedback.AlertOptions{Severity: sev}).Element.Key(sev) } }) }) @@ -89,28 +91,28 @@ func toastDemo(c *ui.Context, log func(string)) { }) feedbackSection(c, "Actions", func() { ui.Row(c).Gap(12).Wrap().Children(func() { - if mujica.Button(c, "Show toast", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Show toast", input.ButtonOptions{}).Clicked() { s.n++ - m := mujica.ToastMessage{ID: fmt.Sprint(s.n), Severity: feedbackSeverities[s.sev], Message: fmt.Sprintf("Message %d", s.n)} + m := feedback.ToastMessage{ID: fmt.Sprint(s.n), Severity: feedbackSeverities[s.sev], Message: fmt.Sprintf("Message %d", s.n)} if s.action { m.Action = "Undo" } - if m.Severity != mujica.SeverityError { + if m.Severity != core.SeverityError { m.Duration = time.Duration(s.seconds * float64(time.Second)) } - mujica.ShowToast(c, m) + feedback.ShowToast(c, m) log("toast: show " + m.ID) } - if mujica.Button(c, "Show five at once", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Show five at once", input.ButtonOptions{Variant: input.Secondary}).Clicked() { for range 5 { s.n++ - mujica.ShowToast(c, mujica.ToastMessage{ID: fmt.Sprint(s.n), Message: fmt.Sprintf("Queued %d", s.n)}) + feedback.ShowToast(c, feedback.ToastMessage{ID: fmt.Sprint(s.n), Message: fmt.Sprintf("Queued %d", s.n)}) } log("toast: five queued, three visible") } - if mujica.Button(c, "Repeat same ID", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Repeat same ID", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.n++ - mujica.ShowToast(c, mujica.ToastMessage{ID: "sync", Message: fmt.Sprintf("Synced %d times", s.n)}) + feedback.ShowToast(c, feedback.ToastMessage{ID: "sync", Message: fmt.Sprintf("Synced %d times", s.n)}) log("toast: dedupe id sync") } }) @@ -118,7 +120,7 @@ func toastDemo(c *ui.Context, log func(string)) { ui.TextInput(c, &s.note).Width(320) }) }) - if r := mujica.Toaster(c); r.Action != "" { + if r := feedback.Toaster(c); r.Action != "" { log("toast: action " + r.Action) } else if r.Closed != "" { log("toast: closed " + r.Closed) @@ -149,8 +151,8 @@ func progressDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Progress", func() { - mujica.Progress(c, s.value, mujica.ProgressOptions{ - Shape: mujica.ProgressShape(s.ring), Indeterminate: s.indet, Label: "Uploading", ShowValue: s.showValue, + feedback.Progress(c, s.value, feedback.ProgressOptions{ + Shape: feedback.ProgressShape(s.ring), Indeterminate: s.indet, Label: "Uploading", ShowValue: s.showValue, }) }) feedbackSection(c, "Meter with thresholds 70 and 90", func() { @@ -161,8 +163,8 @@ func progressDemo(c *ui.Context, log func(string)) { } }) v := float64(int(s.meter)) - mujica.Meter(c, v, mujica.MeterOptions{Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}}) - mujica.Meter(c, 100-v, mujica.MeterOptions{Max: 100, Label: "Battery (low is bad)", ShowValue: true, Levels: &ui.MeterLevels{Warning: 30, Critical: 10}}) + feedback.Meter(c, v, feedback.MeterOptions{Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}}) + feedback.Meter(c, 100-v, feedback.MeterOptions{Max: 100, Label: "Battery (low is bad)", ShowValue: true, Levels: &ui.MeterLevels{Warning: 30, Critical: 10}}) }) }) } @@ -188,13 +190,13 @@ func spinnerDemo(c *ui.Context, log func(string)) { }) feedbackSection(c, "Live", func() { if s.active { - mujica.Spinner(c, mujica.SpinnerOptions{Size: float32(s.size), Label: "Syncing", ShowLabel: s.showLabel}) + core.Spinner(c, core.SpinnerOptions{Size: float32(s.size), Label: "Syncing", ShowLabel: s.showLabel}) } else { ui.Text(c, "Done") } }) feedbackSection(c, "In a button", func() { - mujica.Button(c, "Saving", mujica.ButtonOptions{Loading: true}) + input.Button(c, "Saving", input.ButtonOptions{Loading: true}) }) }) } @@ -216,15 +218,15 @@ func skeletonDemo(c *ui.Context, log func(string)) { } }) feedbackSection(c, "Card (same size loading and loaded)", func() { - ui.Row(c).Gap(12).Padding(16).Width(420).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { + ui.Row(c).Gap(12).Padding(16).Width(420).Background(core.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { if s.loading { - mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + feedback.Skeleton(c, feedback.SkeletonOptions{Shape: feedback.SkeletonCircle}) ui.Column(c).Grow(1).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Lines: int(s.lines)}) + feedback.Skeleton(c, feedback.SkeletonOptions{Lines: int(s.lines)}) }) return } - ui.Box(c).Size(40, 40).Radius(20).Background(mujica.Tokens(c).Selection) + ui.Box(c).Size(40, 40).Radius(20).Background(core.Tokens(c).Selection) ui.Column(c).Grow(1).Children(func() { for i := range int(s.lines) { ui.Text(c, fmt.Sprintf("Loaded line %d", i+1)).LineHeight(theme.LineHeight) @@ -233,7 +235,7 @@ func skeletonDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Rect", func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonRect, Width: 420, Height: 96}) + feedback.Skeleton(c, feedback.SkeletonOptions{Shape: feedback.SkeletonRect, Width: 420, Height: 96}) }) }) } @@ -257,14 +259,14 @@ func emptyStateDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Live", func() { - opts := mujica.EmptyStateOptions{Kind: mujica.EmptyKind(s.kind), NoIcon: !s.icon} + opts := feedback.EmptyStateOptions{Kind: feedback.EmptyKind(s.kind), NoIcon: !s.icon} if s.kind == 1 { opts.Query = s.query } if s.action { opts.Action = []string{"New project", "Clear search"}[s.kind] } - if mujica.EmptyState(c, opts).Action { + if feedback.EmptyState(c, opts).Action { log("empty state: action " + opts.Action) s.query = "" } @@ -281,7 +283,7 @@ func resultDemo(c *ui.Context, log func(string)) { feedbackSection(c, "Parameters", func() { ui.Segmented(c, &s.kind, "Success", "Failure", "Forbidden") }) - r := mujica.Result(c, mujica.ResultOptions{Kind: mujica.ResultKind(s.kind), Description: descs[s.kind], Actions: actions[s.kind]}) + r := feedback.Result(c, feedback.ResultOptions{Kind: feedback.ResultKind(s.kind), Description: descs[s.kind], Actions: actions[s.kind]}) if r.Action >= 0 { log("result: action " + actions[s.kind][r.Action]) } @@ -297,21 +299,21 @@ func statusDemo(c *ui.Context, log func(string)) { ui.Column(c).Gap(16).FillWidth().Children(func() { feedbackSection(c, "Parameters", func() { if ui.Segmented(c, &s.status, "Success", "Warning", "Error", "Offline", "Busy").Changed() { - log("status: " + mujica.Status(s.status).String()) + log("status: " + feedback.Status(s.status).String()) } ui.Checkbox(c, &s.custom, "Custom label") }) feedbackSection(c, "Live", func() { - opts := mujica.StatusIndicatorOptions{} + opts := feedback.StatusIndicatorOptions{} if s.custom { opts.Label = "Database replica" } - mujica.StatusIndicator(c, mujica.Status(s.status), opts) + feedback.StatusIndicator(c, feedback.Status(s.status), opts) }) feedbackSection(c, "All states (shape and word, not color alone)", func() { ui.Row(c).Gap(20).Wrap().Children(func() { - for st := mujica.StatusSuccess; st <= mujica.StatusBusy; st++ { - mujica.StatusIndicator(c, st, mujica.StatusIndicatorOptions{}).Key(st) + for st := feedback.StatusSuccess; st <= feedback.StatusBusy; st++ { + feedback.StatusIndicator(c, st, feedback.StatusIndicatorOptions{}).Key(st) } }) }) @@ -320,11 +322,11 @@ func statusDemo(c *ui.Context, log func(string)) { func notificationDemo(c *ui.Context, log func(string)) { type state struct { - items []mujica.Notification + items []feedback.Notification n int } s := ui.Local(c.Root(), "099", func() state { - return state{items: []mujica.Notification{ + return state{items: []feedback.Notification{ {ID: "a", Title: "Build failed", Body: "main · 3 tests failing", Time: "2 min ago", Action: "View log"}, {ID: "b", Title: "Invite accepted", Body: "Mira joined the workspace", Time: "1 h ago"}, {ID: "c", Title: "Backup complete", Time: "Yesterday", Read: true}, @@ -332,15 +334,15 @@ func notificationDemo(c *ui.Context, log func(string)) { }) ui.Column(c).Gap(16).FillWidth().Children(func() { feedbackSection(c, "Caller-owned list", func() { - if mujica.Button(c, "Add notification", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Add notification", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.n++ id := fmt.Sprintf("new-%d", s.n) - s.items = append([]mujica.Notification{{ID: id, Title: fmt.Sprintf("Comment %d on the design doc", s.n), Time: "now"}}, s.items...) + s.items = append([]feedback.Notification{{ID: id, Title: fmt.Sprintf("Comment %d on the design doc", s.n), Time: "now"}}, s.items...) log("notifications: added " + id) } }) ui.Column(c).Width(520).Height(420).Children(func() { - r := mujica.NotificationCenter(c, &s.items, mujica.NotificationCenterOptions{}) + r := feedback.NotificationCenter(c, &s.items, feedback.NotificationCenterOptions{}) for _, ev := range [][2]string{{"action", r.Action}, {"toggled", r.Toggled}, {"opened", r.Opened}, {"removed", r.Removed}} { if ev[1] != "" { log("notifications: " + ev[0] + " " + ev[1]) @@ -372,15 +374,15 @@ func loadingOverlayDemo(c *ui.Context, log func(string)) { }) }) feedbackSection(c, "Covered region", func() { - opts := mujica.LoadingOverlayOptions{Label: "Generating report…"} + opts := feedback.LoadingOverlayOptions{Label: "Generating report…"} if s.cancel { opts.Cancel = "Cancel" } - r := mujica.LoadingOverlay(c, s.loading, opts, func() { - ui.Column(c).Gap(8).Padding(16).FillWidth().AlignItems(ui.Start).Background(mujica.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { + r := feedback.LoadingOverlay(c, s.loading, opts, func() { + ui.Column(c).Gap(8).Padding(16).FillWidth().AlignItems(ui.Start).Background(core.Tokens(c).Surface).Radius(theme.CardRadius).Children(func() { ui.Text(c, "Quarterly report").Font(theme.SerifFont).FontSize(theme.H3Size) ui.Text(c, fmt.Sprintf("Exported %d times.", s.saves)) - if mujica.Button(c, "Export", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Export", input.ButtonOptions{}).Clicked() { s.saves++ log("overlay: export clicked") } @@ -393,7 +395,7 @@ func loadingOverlayDemo(c *ui.Context, log func(string)) { } }) feedbackSection(c, "Outside the region (always usable)", func() { - if mujica.Button(c, "Outside button", mujica.ButtonOptions{Variant: mujica.Secondary}).AlignSelf(ui.Start).Clicked() { + if input.Button(c, "Outside button", input.ButtonOptions{Variant: input.Secondary}).AlignSelf(ui.Start).Clicked() { log("overlay: outside clicked") } }) diff --git a/examples/gallery/pages_files_browser.go b/examples/gallery/pages_files_browser.go index 901957f..0075e23 100644 --- a/examples/gallery/pages_files_browser.go +++ b/examples/gallery/pages_files_browser.go @@ -7,7 +7,11 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/files" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var filesFolders = map[string][]string{ @@ -16,24 +20,24 @@ var filesFolders = map[string][]string{ "/court/decrees": {"42.pdf", "crest.png"}, } -func filesEntries(dir string) []mujica.FileEntry { - var entries []mujica.FileEntry +func filesEntries(dir string) []files.FileEntry { + var entries []files.FileEntry for _, n := range filesFolders[dir] { name := strings.TrimSuffix(n, "/") - entries = append(entries, mujica.FileEntry{Path: path.Join(dir, name), Name: name, Dir: name != n, Size: 2048}) + entries = append(entries, files.FileEntry{Path: path.Join(dir, name), Name: name, Dir: name != n, Size: 2048}) } return entries } func fileExplorerDemo(c *ui.Context, log func(string)) { type state struct { - explorer mujica.FileExplorerState + explorer files.FileExplorerState disabled, empty bool } s := ui.Local(c.Root(), "381", func() state { return state{} }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.empty, "Empty folder") - r := mujica.FileExplorer(c, &s.explorer, mujica.FileExplorerOptions{ + r := files.FileExplorer(c, &s.explorer, files.FileExplorerOptions{ Roots: []string{"/court"}, Disabled: s.disabled, Folders: func(dir string) []string { var dirs []string @@ -44,7 +48,7 @@ func fileExplorerDemo(c *ui.Context, log func(string)) { } return dirs }, - Entries: func(dir string) []mujica.FileEntry { + Entries: func(dir string) []files.FileEntry { if s.empty { return nil } @@ -65,8 +69,8 @@ func fileExplorerDemo(c *ui.Context, log func(string)) { func fileGridDemo(c *ui.Context, log func(string)) { type state struct { - grid mujica.FileGridState - entries []mujica.FileEntry + grid files.FileGridState + entries []files.FileEntry disabled, thumbs, empty bool } s := ui.Local(c.Root(), "382", func() state { @@ -79,11 +83,11 @@ func fileGridDemo(c *ui.Context, log func(string)) { if s.empty { entries = nil } - r := mujica.FileGrid(c, entries, &s.grid, mujica.FileGridOptions{Disabled: s.disabled, Thumbnail: func(e mujica.FileEntry) bool { - if !s.thumbs || mujica.FileKindOf(e.Name) != mujica.FileImage { + r := files.FileGrid(c, entries, &s.grid, files.FileGridOptions{Disabled: s.disabled, Thumbnail: func(e files.FileEntry) bool { + if !s.thumbs || files.FileKindOf(e.Name) != files.FileImage { return false } - mujica.Image(c, mediaBitmaps[0], mujica.ImageOptions{Alt: e.Name}).Element.Fill() + display.Image(c, mediaBitmaps[0], display.ImageOptions{Alt: e.Name}).Element.Fill() return true }}) r.Element.Height(320).FillWidth() @@ -102,20 +106,20 @@ func fileIconDemo(c *ui.Context, log func(string)) { size float64 } s := ui.Local(c.Root(), "383", func() state { return state{name: "seal.go", size: 32} }) - if mujica.TextInput(c, &s.name, mujica.TextInputOptions{Label: "File name"}).Changed() { + if input.TextInput(c, &s.name, input.TextInputOptions{Label: "File name"}).Changed() { log("File name changed") } mediaCheck(c, log, &s.folder, "Folder") if s.folder { mediaCheck(c, log, &s.open, "Open folder") } - if mujica.NumberInput(c, &s.size, mujica.NumberInputOptions{Min: 20, Max: 64, Step: 4, Label: "Icon size"}).Changed() { + if input.NumberInput(c, &s.size, input.NumberInputOptions{Min: 20, Max: 64, Step: 4, Label: "Icon size"}).Changed() { log(fmt.Sprintf("Size: %.0f", s.size)) } - mujica.FileIcon(c, s.name, mujica.FileIconOptions{Folder: s.folder, Open: s.folder && s.open, Size: float32(s.size)}) + files.FileIcon(c, s.name, files.FileIconOptions{Folder: s.folder, Open: s.folder && s.open, Size: float32(s.size)}) ui.Row(c).Gap(12).Children(func() { for _, name := range []string{"seal.go", "notes.md", "crest.png", "deed.pdf", "ledger.zip", "hymn.mp3", "rite.mov", "blob"} { - mujica.FileIcon(c, name, mujica.FileIconOptions{Size: 32}) + files.FileIcon(c, name, files.FileIconOptions{Size: 32}) } }) } @@ -123,24 +127,24 @@ func fileIconDemo(c *ui.Context, log func(string)) { func filePreviewDemo(c *ui.Context, log func(string)) { type state struct { kind int - status mujica.DataStatus + status data.DataStatus closed bool } s := ui.Local(c.Root(), "384", func() state { return state{} }) - if mujica.Select(c, &s.kind, []mujica.SelectOption[int]{{Value: 0, Label: "Code"}, {Value: 1, Label: "Text"}, {Value: 2, Label: "Image"}, {Value: 3, Label: "Unsupported"}}, mujica.SelectOptions{Label: "Preview type"}).Changed() { + if input.Select(c, &s.kind, []input.SelectOption[int]{{Value: 0, Label: "Code"}, {Value: 1, Label: "Text"}, {Value: 2, Label: "Image"}, {Value: 3, Label: "Unsupported"}}, input.SelectOptions{Label: "Preview type"}).Changed() { log("Preview type changed") s.closed = false } filesStatus(c, log, &s.status) if s.closed { - if mujica.Button(c, "Show preview", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Show preview", input.ButtonOptions{}).Clicked() { s.closed = false log("Preview reopened") } return } - f := []mujica.PreviewFile{{Name: "seal.go", Text: "package court\n\nfunc Seal() {}"}, {Name: "notes.md", Text: "The seal of the court."}, {Name: "crest.png", Image: mediaBitmaps[0]}, {Name: "deed.pdf", Size: 4096}}[s.kind] - r := mujica.FilePreview(c, f, mujica.FilePreviewOptions{Status: s.status, Error: "Reading failed"}) + f := []files.PreviewFile{{Name: "seal.go", Text: "package court\n\nfunc Seal() {}"}, {Name: "notes.md", Text: "The seal of the court."}, {Name: "crest.png", Image: mediaBitmaps[0]}, {Name: "deed.pdf", Size: 4096}}[s.kind] + r := files.FilePreview(c, f, files.FilePreviewOptions{Status: s.status, Error: "Reading failed"}) r.Element.Height(280).FillWidth() if r.Closed { s.closed = true @@ -150,13 +154,13 @@ func filePreviewDemo(c *ui.Context, log func(string)) { log("Open: " + f.Name) } if r.Retried { - s.status = mujica.DataReady + s.status = data.DataReady log("Read retried") } } -func filesStatus(c *ui.Context, log func(string), value *mujica.DataStatus) { - if mujica.Select(c, value, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Failed"}}, mujica.SelectOptions{Label: "Load status"}).Changed() { +func filesStatus(c *ui.Context, log func(string), value *data.DataStatus) { + if input.Select(c, value, []input.SelectOption[data.DataStatus]{{Value: data.DataReady, Label: "Ready"}, {Value: data.DataLoading, Label: "Loading"}, {Value: data.DataFailed, Label: "Failed"}}, input.SelectOptions{Label: "Load status"}).Changed() { log(fmt.Sprintf("Status: %d", *value)) } } @@ -168,14 +172,14 @@ func pathBarDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "385", func() state { return state{path: "/court/chronicles/royal/archives/seals"} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.TextInput(c, &s.path, mujica.TextInputOptions{Label: "Path"}).Changed() { + if input.TextInput(c, &s.path, input.TextInputOptions{Label: "Path"}).Changed() { log("Path changed") } if strings.Trim(s.path, "/") == "" { ui.Text(c, "Enter a path") return } - if r := mujica.PathBar(c, s.path, mujica.PathBarOptions{Disabled: s.disabled}); r.Navigated != "" { + if r := files.PathBar(c, s.path, files.PathBarOptions{Disabled: s.disabled}); r.Navigated != "" { s.path = r.Navigated log("Navigate: " + r.Navigated) } @@ -189,11 +193,11 @@ func renameInlineDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "386", func() state { return state{name: "seal.txt"} }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.folder, "Folder") - if mujica.Button(c, "Rename", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Rename", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.editing = true log("Rename started") } - r := mujica.RenameInline(c, &s.name, &s.editing, mujica.RenameInlineOptions{Disabled: s.disabled, Folder: s.folder, Validate: func(name string) string { + r := files.RenameInline(c, &s.name, &s.editing, files.RenameInlineOptions{Disabled: s.disabled, Folder: s.folder, Validate: func(name string) string { if strings.ContainsAny(name, "/\\") { return "Name contains a path separator" } @@ -209,23 +213,23 @@ func renameInlineDemo(c *ui.Context, log func(string)) { func fileOperationProgressDemo(c *ui.Context, log func(string)) { type state struct { - op mujica.FileOperation + op files.FileOperation disabled, conflict bool canceled bool } s := ui.Local(c.Root(), "387", func() state { - return state{op: mujica.FileOperation{Kind: mujica.FileCopy, Total: 10, Current: "seal.go"}} + return state{op: files.FileOperation{Kind: files.FileCopy, Total: 10, Current: "seal.go"}} }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.conflict, "Conflict") - if mujica.Select(c, &s.op.Kind, []mujica.SelectOption[mujica.FileOperationKind]{{Value: mujica.FileCopy, Label: "Copy"}, {Value: mujica.FileMove, Label: "Move"}, {Value: mujica.FileDelete, Label: "Delete"}}, mujica.SelectOptions{Label: "Operation"}).Changed() { + if input.Select(c, &s.op.Kind, []input.SelectOption[files.FileOperationKind]{{Value: files.FileCopy, Label: "Copy"}, {Value: files.FileMove, Label: "Move"}, {Value: files.FileDelete, Label: "Delete"}}, input.SelectOptions{Label: "Operation"}).Changed() { log("Operation changed") } - if mujica.Button(c, "Advance", mujica.ButtonOptions{Disabled: s.op.Done == s.op.Total}).Clicked() { + if input.Button(c, "Advance", input.ButtonOptions{Disabled: s.op.Done == s.op.Total}).Clicked() { s.op.Done++ log(fmt.Sprintf("Done: %d", s.op.Done)) } - if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Reset", input.ButtonOptions{}).Clicked() { s.op.Done = 0 s.canceled = false log("Operation reset") @@ -234,51 +238,51 @@ func fileOperationProgressDemo(c *ui.Context, log func(string)) { if s.conflict { op.Conflict = "seal.go" } - r := mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{Disabled: s.disabled || s.canceled}) + r := files.FileOperationProgress(c, op, files.FileOperationOptions{Disabled: s.disabled || s.canceled}) if r.Canceled { s.canceled = true log("Operation canceled") } - if r.Resolved != mujica.ConflictNone { + if r.Resolved != files.ConflictNone { s.conflict = false log(fmt.Sprintf("Conflict: %d; apply all: %v", r.Resolved, r.ApplyAll)) } } -func filesTransfers() []mujica.QueuedTransfer { - return []mujica.QueuedTransfer{ - {ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 30, Total: 120}, - {ID: "b", Name: "crest.png", Status: mujica.TransferPaused, Done: 1, Total: 4}, - {ID: "c", Name: "deed.pdf", Upload: true, Status: mujica.TransferFailed, Total: 2, Error: "Network lost"}, +func filesTransfers() []files.QueuedTransfer { + return []files.QueuedTransfer{ + {ID: "a", Name: "ledger.zip", Upload: true, Status: files.TransferActive, Done: 30, Total: 120}, + {ID: "b", Name: "crest.png", Status: files.TransferPaused, Done: 1, Total: 4}, + {ID: "c", Name: "deed.pdf", Upload: true, Status: files.TransferFailed, Total: 2, Error: "Network lost"}, } } func transferQueueDemo(c *ui.Context, log func(string)) { type state struct { - items []mujica.QueuedTransfer - queue mujica.TransferQueueState + items []files.QueuedTransfer + queue files.TransferQueueState disabled bool revision uint64 } s := ui.Local(c.Root(), "388", func() state { return state{items: filesTransfers()} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Reset", input.ButtonOptions{}).Clicked() { s.items = filesTransfers() log("Queue reset") } - r := mujica.TransferQueue(c, s.items, &s.queue, mujica.TransferQueueOptions{Disabled: s.disabled, Revision: s.revision}) + r := files.TransferQueue(c, s.items, &s.queue, files.TransferQueueOptions{Disabled: s.disabled, Revision: s.revision}) r.Element.Height(260).FillWidth() - if r.Action == mujica.TransferNone { + if r.Action == files.TransferNone { return } for i := range s.items { if s.items[i].ID == r.ID { switch r.Action { - case mujica.TransferPause: - s.items[i].Status = mujica.TransferPaused - case mujica.TransferResume, mujica.TransferRetry: - s.items[i].Status = mujica.TransferActive - case mujica.TransferRemove: + case files.TransferPause: + s.items[i].Status = files.TransferPaused + case files.TransferResume, files.TransferRetry: + s.items[i].Status = files.TransferActive + case files.TransferRemove: s.items = append(s.items[:i:i], s.items[i+1:]...) } break @@ -291,30 +295,30 @@ func transferQueueDemo(c *ui.Context, log func(string)) { func storageUsageDemo(c *ui.Context, log func(string)) { type state struct{ photos float64 } s := ui.Local(c.Root(), "389", func() state { return state{photos: 40} }) - if mujica.NumberInput(c, &s.photos, mujica.NumberInputOptions{Min: 0, Max: 92, Step: 1, Label: "Photos (GB)"}).Changed() { + if input.NumberInput(c, &s.photos, input.NumberInputOptions{Min: 0, Max: 92, Step: 1, Label: "Photos (GB)"}).Changed() { log(fmt.Sprintf("Photos: %.0f GB", s.photos)) } - mujica.StorageUsage(c, []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: int64(s.photos) * (1 << 30)}, {Label: "Apps", Bytes: 12 << 30}}, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}).FillWidth() + files.StorageUsage(c, []files.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: int64(s.photos) * (1 << 30)}, {Label: "Apps", Bytes: 12 << 30}}, files.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}).FillWidth() } -func filesRecent() []mujica.RecentFile { +func filesRecent() []files.RecentFile { now := time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC) - return []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}, {Path: "/court/notes.md", Opened: now.AddDate(0, 0, -1)}, {Path: "/court/crest.png", Opened: now.AddDate(0, 0, -4)}, {Path: "/court/charter.pdf", Opened: now.AddDate(0, -1, 0)}} + return []files.RecentFile{{Path: "/court/seal.go", Opened: now}, {Path: "/court/notes.md", Opened: now.AddDate(0, 0, -1)}, {Path: "/court/crest.png", Opened: now.AddDate(0, 0, -4)}, {Path: "/court/charter.pdf", Opened: now.AddDate(0, -1, 0)}} } func recentFilesDemo(c *ui.Context, log func(string)) { type state struct { - files []mujica.RecentFile - recent mujica.RecentFilesState + files []files.RecentFile + recent files.RecentFilesState disabled bool } s := ui.Local(c.Root(), "390", func() state { return state{files: filesRecent()} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Reset", input.ButtonOptions{}).Clicked() { s.files = filesRecent() log("Recent files reset") } - r := mujica.RecentFiles(c, s.files, &s.recent, mujica.RecentFilesOptions{Disabled: s.disabled, Now: time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)}) + r := files.RecentFiles(c, s.files, &s.recent, files.RecentFilesOptions{Disabled: s.disabled, Now: time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)}) r.Element.Height(320).FillWidth() if r.Opened != nil { log("Open: " + r.Opened.Path) @@ -336,13 +340,13 @@ func recentFilesDemo(c *ui.Context, log func(string)) { func archiveViewerDemo(c *ui.Context, log func(string)) { type state struct { - archive mujica.ArchiveViewerState - entries []mujica.ArchiveEntry - status mujica.DataStatus + archive files.ArchiveViewerState + entries []files.ArchiveEntry + status data.DataStatus empty bool } s := ui.Local(c.Root(), "391", func() state { - return state{entries: []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}, {Path: "court/decrees/42.pdf", Size: 6000, Packed: 5000}, {Path: "README.md", Size: 1000, Packed: 400}}} + return state{entries: []files.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}, {Path: "court/decrees/42.pdf", Size: 6000, Packed: 5000}, {Path: "README.md", Size: 1000, Packed: 400}}} }) filesStatus(c, log, &s.status) mediaCheck(c, log, &s.empty, "Empty") @@ -350,13 +354,13 @@ func archiveViewerDemo(c *ui.Context, log func(string)) { if s.empty { entries = nil } - r := mujica.ArchiveViewer(c, entries, &s.archive, mujica.ArchiveViewerOptions{Status: s.status, Error: "Invalid archive"}) + r := files.ArchiveViewer(c, entries, &s.archive, files.ArchiveViewerOptions{Status: s.status, Error: "Invalid archive"}) r.Element.Height(280).FillWidth() if r.Opened != nil { log("Open archive entry: " + r.Opened.Path) } if r.Retried { - s.status = mujica.DataReady + s.status = data.DataReady log("Archive read retried") } } diff --git a/examples/gallery/pages_files_code.go b/examples/gallery/pages_files_code.go index 7b1c108..ad75b73 100644 --- a/examples/gallery/pages_files_code.go +++ b/examples/gallery/pages_files_code.go @@ -1,135 +1,135 @@ package main -const filesFileExplorerCode = `var state mujica.FileExplorerState -opts := mujica.FileExplorerOptions{ +const filesFileExplorerCode = `var state files.FileExplorerState +opts := files.FileExplorerOptions{ Roots: []string{"/court"}, Folders: func(string) []string { return nil }, - Entries: func(string) []mujica.FileEntry { - return []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} + Entries: func(string) []files.FileEntry { + return []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} }, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileExplorer(c, &state, opts).Element.Height(240) + core.Use(c, core.Settings{}) + files.FileExplorer(c, &state, opts).Element.Height(240) } ui.Render(view, 600, 300, 1)` -const filesFileGridCode = `entries := []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} -var state mujica.FileGridState +const filesFileGridCode = `entries := []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} +var state files.FileGridState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileGrid(c, entries, &state, mujica.FileGridOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.FileGrid(c, entries, &state, files.FileGridOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1)` const filesFileIconCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileIcon(c, "seal.go", mujica.FileIconOptions{Size: 32}) + core.Use(c, core.Settings{}) + files.FileIcon(c, "seal.go", files.FileIconOptions{Size: 32}) } ui.Render(view, 100, 80, 1)` -const filesFilePreviewCode = `file := mujica.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} +const filesFilePreviewCode = `file := files.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePreview(c, file, mujica.FilePreviewOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.FilePreview(c, file, files.FilePreviewOptions{}).Element.Height(240) } ui.Render(view, 500, 300, 1)` const filesPathBarCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PathBar(c, "/court/chronicles/seals", mujica.PathBarOptions{}) + core.Use(c, core.Settings{}) + files.PathBar(c, "/court/chronicles/seals", files.PathBarOptions{}) } ui.Render(view, 400, 100, 1)` const filesRenameInlineCode = `name, editing := "seal.txt", true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RenameInline(c, &name, &editing, mujica.RenameInlineOptions{}) + core.Use(c, core.Settings{}) + files.RenameInline(c, &name, &editing, files.RenameInlineOptions{}) } ui.Render(view, 400, 100, 1)` -const filesFileOperationProgressCode = `op := mujica.FileOperation{Kind: mujica.FileCopy, Done: 2, Total: 10, Current: "seal.go"} +const filesFileOperationProgressCode = `op := files.FileOperation{Kind: files.FileCopy, Done: 2, Total: 10, Current: "seal.go"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{}) + core.Use(c, core.Settings{}) + files.FileOperationProgress(c, op, files.FileOperationOptions{}) } ui.Render(view, 500, 240, 1)` -const filesTransferQueueCode = `items := []mujica.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 25, Total: 100}} -var state mujica.TransferQueueState +const filesTransferQueueCode = `items := []files.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: files.TransferActive, Done: 25, Total: 100}} +var state files.TransferQueueState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TransferQueue(c, items, &state, mujica.TransferQueueOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.TransferQueue(c, items, &state, files.TransferQueueOptions{}).Element.Height(240) } ui.Render(view, 500, 300, 1)` -const filesStorageUsageCode = `segments := []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} +const filesStorageUsageCode = `segments := []files.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.StorageUsage(c, segments, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) + core.Use(c, core.Settings{}) + files.StorageUsage(c, segments, files.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) } ui.Render(view, 500, 240, 1)` const filesRecentFilesCode = `now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) -files := []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}} -var state mujica.RecentFilesState +recent := []files.RecentFile{{Path: "/court/seal.go", Opened: now}} +var state files.RecentFilesState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecentFiles(c, files, &state, mujica.RecentFilesOptions{Now: now}).Element.Height(240) + core.Use(c, core.Settings{}) + files.RecentFiles(c, recent, &state, files.RecentFilesOptions{Now: now}).Element.Height(240) } ui.Render(view, 500, 300, 1)` -const filesArchiveViewerCode = `entries := []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} -var state mujica.ArchiveViewerState +const filesArchiveViewerCode = `entries := []files.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} +var state files.ArchiveViewerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ArchiveViewer(c, entries, &state, mujica.ArchiveViewerOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + files.ArchiveViewer(c, entries, &state, files.ArchiveViewerOptions{}).Element.Height(240) } ui.Render(view, 600, 300, 1)` -const filesPresenceAvatarsCode = `people := []mujica.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} +const filesPresenceAvatarsCode = `people := []files.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{}) + core.Use(c, core.Settings{}) + files.PresenceAvatars(c, people, files.PresenceAvatarsOptions{}) } ui.Render(view, 300, 100, 1)` -const filesReactionsCode = `items := []mujica.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} +const filesReactionsCode = `items := []files.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Reactions(c, items, mujica.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) + core.Use(c, core.Settings{}) + files.Reactions(c, items, files.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) } ui.Render(view, 400, 100, 1)` const filesShareDialogCode = `open := false -members := []mujica.ShareMember{{ID: "ada", Name: "Ada", Permission: mujica.PermissionEdit, Owner: true}} +members := []files.ShareMember{{ID: "ada", Name: "Ada", Permission: files.PermissionEdit, Owner: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Share", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Share", input.ButtonOptions{}).Clicked() { open = true } - mujica.ShareDialog(c, &open, &members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) + files.ShareDialog(c, &open, &members, files.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) } ui.Render(view, 600, 400, 1)` -const filesPermissionSelectCode = `value := mujica.PermissionView +const filesPermissionSelectCode = `value := files.PermissionView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PermissionSelect(c, &value, mujica.PermissionSelectOptions{}) + core.Use(c, core.Settings{}) + files.PermissionSelect(c, &value, files.PermissionSelectOptions{}) } ui.Render(view, 300, 100, 1)` const filesLiveIndicatorCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LiveIndicator(c, mujica.LiveOffline, mujica.LiveIndicatorOptions{}) + core.Use(c, core.Settings{}) + files.LiveIndicator(c, files.LiveOffline, files.LiveIndicatorOptions{}) } ui.Render(view, 500, 100, 1)` -const filesRemoteCursorCode = `carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} +const filesRemoteCursorCode = `carets := []files.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Size(300, 100).Children(func() { - mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{ + files.RemoteCursor(c, carets, files.RemoteCursorOptions{ Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} }, }) }) diff --git a/examples/gallery/pages_files_collaboration.go b/examples/gallery/pages_files_collaboration.go index 07df5a7..03be2b6 100644 --- a/examples/gallery/pages_files_collaboration.go +++ b/examples/gallery/pages_files_collaboration.go @@ -5,11 +5,14 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/files" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) -var filesPeople = []mujica.Collaborator{ +var filesPeople = []files.Collaborator{ {ID: "a", Name: "Ada Lovelace"}, {ID: "b", Name: "Blaise Pascal", Away: true}, {ID: "c", Name: "Carl Gauss"}, {ID: "d", Name: "Dora Maar"}, {ID: "e", Name: "Emmy Noether"}, } @@ -23,14 +26,14 @@ func presenceAvatarsDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "392", func() state { return state{max: 3} }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.empty, "Empty") - if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible avatars"}).Changed() { + if input.NumberInput(c, &s.max, input.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible avatars"}).Changed() { log(fmt.Sprintf("Visible: %.0f", s.max)) } people := filesPeople if s.empty { people = nil } - r := mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{Following: s.following, Max: int(s.max), Disabled: s.disabled}) + r := files.PresenceAvatars(c, people, files.PresenceAvatarsOptions{Following: s.following, Max: int(s.max), Disabled: s.disabled}) if r.Follow != "" { s.following = r.Follow log("Follow: " + r.Follow) @@ -45,18 +48,18 @@ var filesReactionChoices = []string{"\U0001f44d", "\u2764\ufe0f", "\U0001f389", func reactionsDemo(c *ui.Context, log func(string)) { type state struct { - items []mujica.Reaction + items []files.Reaction disabled bool } s := ui.Local(c.Root(), "394", func() state { - return state{items: []mujica.Reaction{{Emoji: filesReactionChoices[0], Count: 3, Mine: true}, {Emoji: filesReactionChoices[2], Count: 1}}} + return state{items: []files.Reaction{{Emoji: filesReactionChoices[0], Count: 3, Mine: true}, {Emoji: filesReactionChoices[2], Count: 1}}} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Clear reactions", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Clear reactions", input.ButtonOptions{}).Clicked() { s.items = nil log("Reactions cleared") } - r := mujica.Reactions(c, s.items, mujica.ReactionsOptions{Choices: filesReactionChoices, Disabled: s.disabled}) + r := files.Reactions(c, s.items, files.ReactionsOptions{Choices: filesReactionChoices, Disabled: s.disabled}) emoji := r.Toggled if emoji == "" { emoji = r.Added @@ -80,32 +83,32 @@ func reactionsDemo(c *ui.Context, log func(string)) { return } } - s.items = append(s.items, mujica.Reaction{Emoji: emoji, Count: 1, Mine: true}) + s.items = append(s.items, files.Reaction{Emoji: emoji, Count: 1, Mine: true}) log("Reaction added") } func shareDialogDemo(c *ui.Context, log func(string)) { type state struct { - members []mujica.ShareMember + members []files.ShareMember open, disabled bool next int } s := ui.Local(c.Root(), "395", func() state { - return state{members: []mujica.ShareMember{ - {ID: "owner", Name: "Ada Lovelace", Email: "ada@court.org", Owner: true, Permission: mujica.PermissionEdit}, - {ID: "b", Name: "Blaise Pascal", Email: "blaise@court.org", Permission: mujica.PermissionView}, + return state{members: []files.ShareMember{ + {ID: "owner", Name: "Ada Lovelace", Email: "ada@court.org", Owner: true, Permission: files.PermissionEdit}, + {ID: "b", Name: "Blaise Pascal", Email: "blaise@court.org", Permission: files.PermissionView}, }} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Share Ledger", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Share Ledger", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.open = true log("Share dialog opened") } was := s.open - r := mujica.ShareDialog(c, &s.open, &s.members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", Disabled: s.disabled, OnCopy: func(link string) { c.WriteClipboard(link); log("Copy callback") }}) + r := files.ShareDialog(c, &s.open, &s.members, files.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", Disabled: s.disabled, OnCopy: func(link string) { c.WriteClipboard(link); log("Copy callback") }}) if r.Invited != nil { s.next++ - s.members = append(s.members, mujica.ShareMember{ID: fmt.Sprintf("invite-%d", s.next), Name: r.Invited.Email, Email: r.Invited.Email, Permission: r.Invited.Permission}) + s.members = append(s.members, files.ShareMember{ID: fmt.Sprintf("invite-%d", s.next), Name: r.Invited.Email, Email: r.Invited.Email, Permission: r.Invited.Permission}) log("Member invited") } if r.Changed != "" { @@ -124,33 +127,33 @@ func shareDialogDemo(c *ui.Context, log func(string)) { func permissionSelectDemo(c *ui.Context, log func(string)) { type state struct { - value mujica.Permission + value files.Permission disabled bool } s := ui.Local(c.Root(), "396", func() state { return state{} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.PermissionSelect(c, &s.value, mujica.PermissionSelectOptions{Disabled: s.disabled}).Changed() { + if files.PermissionSelect(c, &s.value, files.PermissionSelectOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("Permission: %d", s.value)) } } func liveIndicatorDemo(c *ui.Context, log func(string)) { type state struct { - status mujica.LiveStatus + status files.LiveStatus showTime bool } s := ui.Local(c.Root(), "399", func() state { return state{showTime: true} }) - if mujica.Select(c, &s.status, []mujica.SelectOption[mujica.LiveStatus]{{Value: mujica.LiveSynced, Label: "Synced"}, {Value: mujica.LiveSyncing, Label: "Syncing"}, {Value: mujica.LiveReconnecting, Label: "Reconnecting"}, {Value: mujica.LiveOffline, Label: "Offline"}, {Value: mujica.LiveError, Label: "Error"}}, mujica.SelectOptions{Label: "Sync status"}).Changed() { + if input.Select(c, &s.status, []input.SelectOption[files.LiveStatus]{{Value: files.LiveSynced, Label: "Synced"}, {Value: files.LiveSyncing, Label: "Syncing"}, {Value: files.LiveReconnecting, Label: "Reconnecting"}, {Value: files.LiveOffline, Label: "Offline"}, {Value: files.LiveError, Label: "Error"}}, input.SelectOptions{Label: "Sync status"}).Changed() { log(fmt.Sprintf("Sync status: %d", s.status)) } mediaCheck(c, log, &s.showTime, "Last sync time") now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) - opts := mujica.LiveIndicatorOptions{Error: "Server unavailable", Now: now} + opts := files.LiveIndicatorOptions{Error: "Server unavailable", Now: now} if s.showTime { opts.LastSynced = now.Add(-3 * time.Minute) } - if mujica.LiveIndicator(c, s.status, opts).Changed() { - s.status = mujica.LiveReconnecting + if files.LiveIndicator(c, s.status, opts).Changed() { + s.status = files.LiveReconnecting log("Sync retried") } } @@ -161,26 +164,26 @@ func remoteCursorDemo(c *ui.Context, log func(string)) { selection, second bool } s := ui.Local(c.Root(), "400", func() state { return state{offset: 48, selection: true, second: true} }) - if mujica.NumberInput(c, &s.offset, mujica.NumberInputOptions{Min: 0, Max: 79, Step: 1, Label: "Remote text offset"}).Changed() { + if input.NumberInput(c, &s.offset, input.NumberInputOptions{Min: 0, Max: 79, Step: 1, Label: "Remote text offset"}).Changed() { log(fmt.Sprintf("Offset: %.0f", s.offset)) } mediaCheck(c, log, &s.selection, "Selection") mediaCheck(c, log, &s.second, "Second collaborator") - carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: int(s.offset), Anchor: int(s.offset)}} + carets := []files.RemoteCaret{{ID: "ada", Name: "Ada", Offset: int(s.offset), Anchor: int(s.offset)}} if s.selection { carets[0].Anchor = max(0, carets[0].Offset-5) } if s.second { - carets = append(carets, mujica.RemoteCaret{ID: "blaise", Name: "Blaise", Offset: 8, Anchor: 8}) + carets = append(carets, files.RemoteCaret{ID: "blaise", Name: "Blaise", Offset: 8, Anchor: 8}) } - ui.Box(c).Height(140).FillWidth().Background(mujica.Tokens(c).Surface).Children(func() { + ui.Box(c).Height(140).FillWidth().Background(core.Tokens(c).Surface).Children(func() { ui.Column(c).Children(func() { for _, line := range []string{"The seal of the court is kept", "by the chancellor, who answers"} { - ui.Text(c, line).Font(theme.MonoFont).FontSize(mujica.FontSize(c, 13)).Height(20) + ui.Text(c, line).Font(theme.MonoFont).FontSize(core.FontSize(c, 13)).Height(20) } }) at := func(o int) (float32, float32) { return float32(o%40) * 8, float32(o/40) * 20 } - mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{ + files.RemoteCursor(c, carets, files.RemoteCursorOptions{ Caret: func(o int) ui.Rect { x, y := at(o); return ui.Rect{X: x, Y: y, H: 20} }, Selection: func(from, to int) []ui.Rect { var out []ui.Rect diff --git a/examples/gallery/pages_files_test.go b/examples/gallery/pages_files_test.go index 7d1b593..b733e17 100644 --- a/examples/gallery/pages_files_test.go +++ b/examples/gallery/pages_files_test.go @@ -6,16 +6,17 @@ import ( "testing" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func filesDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) { +func filesDemoTester(t *testing.T, id string, mode core.Mode) (*ui.Tester, *[]string) { t.Helper() for _, p := range filesPages { if p.ID == id { events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) ui.Scroll(c).Fill().Padding(24).Gap(12).Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) }) }, 900, 800) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -59,7 +60,7 @@ func TestFilesGalleryActions(t *testing.T) { {"400", "Selection", "Selection = false"}, } for _, tc := range cases { - for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { t.Run(fmt.Sprintf("%s-%s", tc.id, mode), func(t *testing.T) { tt, events := filesDemoTester(t, tc.id, mode) if err := tt.Click(tc.action); err != nil { @@ -88,7 +89,7 @@ func TestFilesGalleryActions(t *testing.T) { } func TestFilesGalleryStorageParameter(t *testing.T) { - tt, events := filesDemoTester(t, "389", mujica.Light) + tt, events := filesDemoTester(t, "389", core.Light) if err := tt.Click("Photos (GB)"); err != nil { t.Fatal(err) } @@ -101,7 +102,7 @@ func TestFilesGalleryStorageParameter(t *testing.T) { } func TestFilesGalleryGridRange(t *testing.T) { - tt, events := filesDemoTester(t, "382", mujica.Light) + tt, events := filesDemoTester(t, "382", core.Light) if err := tt.Click("notes.md"); err != nil { t.Fatal(err) } diff --git a/examples/gallery/pages_form.go b/examples/gallery/pages_form.go index 2d8aef9..08780d8 100644 --- a/examples/gallery/pages_form.go +++ b/examples/gallery/pages_form.go @@ -8,8 +8,8 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // formStates shows a component's states side by side. @@ -30,7 +30,7 @@ func formToggles(c *ui.Context, names []string, values ...*bool) { } type formDemo struct { - st mujica.FormState + st input.FormState name, email string tags []string horizontal, slowServer bool @@ -75,7 +75,7 @@ type pickerDemo struct { } type dropDemo struct { - files []mujica.DroppedFile + files []input.DroppedFile images bool disabled bool } @@ -85,7 +85,7 @@ type transferDemo struct { disabled bool } -var transferDemoItems = []mujica.TransferItem{ +var transferDemoItems = []input.TransferItem{ {ID: "sakiko", Label: "Sakiko Togawa"}, {ID: "uika", Label: "Uika Misumi"}, {ID: "mutsumi", Label: "Mutsumi Wakaba"}, {ID: "umiri", Label: "Umiri Yahata"}, {ID: "nyamu", Label: "Nyamu Yutenji"}, {ID: "tomori", Label: "Tomori Takamatsu", Disabled: true}, {ID: "anon", Label: "Anon Chihaya"}, {ID: "rana", Label: "Rana Kaname"}, {ID: "soyo", Label: "Soyo Nagasaki"}, @@ -117,8 +117,8 @@ var formPages = []page{ } } formToggles(c, []string{"Labels beside fields", "Server rejects the submit"}, &d.horizontal, &d.slowServer) - res := mujica.Form(c, &d.st, mujica.FormOptions{Horizontal: d.horizontal}, func(f *mujica.FormScope) { - mujica.FormItem(c, f, "name", &d.name, mujica.FormItemOptions[string]{Label: "Stage name", Required: true, Initial: "Sakiko", + res := input.Form(c, &d.st, input.FormOptions{Horizontal: d.horizontal}, func(f *input.FormScope) { + input.FormItem(c, f, "name", &d.name, input.FormItemOptions[string]{Label: "Stage name", Required: true, Initial: "Sakiko", Description: "Checked against the archive; \"Oblivionis\" is taken.", Async: func(v string, version uint64) { log(fmt.Sprintf("async check started v%d", version)) @@ -132,23 +132,23 @@ var formPages = []page{ c.Invalidate() }() }}, func() *ui.Element { - return mujica.InputGroup(c, &d.name, mujica.InputGroupOptions{Invalid: d.st.Error("name") != ""}).Input + return input.InputGroup(c, &d.name, input.InputGroupOptions{Invalid: d.st.Error("name") != ""}).Input }) - mujica.FormItem(c, f, "email", &d.email, mujica.FormItemOptions[string]{Label: "Email", Required: true, + input.FormItem(c, f, "email", &d.email, input.FormItemOptions[string]{Label: "Email", Required: true, Validate: func(v string) string { if !strings.Contains(v, "@") { return "Enter an email address." } return "" }}, func() *ui.Element { - g := mujica.InputGroup(c, &d.email, mujica.InputGroupOptions{Invalid: d.st.Error("email") != ""}) + g := input.InputGroup(c, &d.email, input.InputGroupOptions{Invalid: d.st.Error("email") != ""}) if g.Submitted() { d.st.Submit() } return g.Input }) - mujica.FormItem(c, f, "tags", &d.tags, mujica.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element { - return mujica.TagsInput(c, &d.tags, mujica.TagsInputOptions{Max: 4, Invalid: d.st.Error("tags") != ""}).Input + input.FormItem(c, f, "tags", &d.tags, input.FormItemOptions[[]string]{Label: "Roles", Required: true, Initial: []string{"keyboard"}}, func() *ui.Element { + return input.TagsInput(c, &d.tags, input.TagsInputOptions{Max: 4, Invalid: d.st.Error("tags") != ""}).Input }) }) if res.Submitted() { @@ -176,9 +176,9 @@ var formPages = []page{ if d.withError { e = "This name is already in the guest list." } - mujica.FormField(c, "Guest name", mujica.FormFieldOptions{Required: d.required, Horizontal: d.horizontal, + input.FormField(c, "Guest name", input.FormFieldOptions{Required: d.required, Horizontal: d.horizontal, Description: "As written on the invitation.", Error: e}, func() *ui.Element { - g := mujica.InputGroup(c, &d.value, mujica.InputGroupOptions{Invalid: d.withError}) + g := input.InputGroup(c, &d.value, input.InputGroupOptions{Invalid: d.withError}) if g.Changed() { log(fmt.Sprintf("value %q", d.value)) } @@ -190,9 +190,9 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "043", func() groupDemo { return groupDemo{value: "mujica"} }) formToggles(c, []string{"Disabled", "Read-only", "Invalid"}, &d.disabled, &d.readOnly, &d.invalid) - opts := mujica.InputGroupOptions{Prefix: "https://", Suffix: ".org", Label: "Site", Disabled: d.disabled, ReadOnly: d.readOnly, Invalid: d.invalid, - Buttons: []mujica.InputGroupButton{{Label: "Clear", Icon: icons.Must("x"), IconOnly: true}, {Label: "Copy", Icon: icons.Must("copy")}}} - g := mujica.InputGroup(c, &d.value, opts) + opts := input.InputGroupOptions{Prefix: "https://", Suffix: ".org", Label: "Site", Disabled: d.disabled, ReadOnly: d.readOnly, Invalid: d.invalid, + Buttons: []input.InputGroupButton{{Label: "Clear", Icon: icons.Must("x"), IconOnly: true}, {Label: "Copy", Icon: icons.Must("copy")}}} + g := input.InputGroup(c, &d.value, opts) switch g.Clicked { case 0: d.value = "" @@ -208,9 +208,9 @@ var formPages = []page{ log("submitted") } formStates(c, func() { - for _, o := range []mujica.InputGroupOptions{{Label: "Disabled", Disabled: true}, {Label: "Read-only", ReadOnly: true}, {Label: "Invalid", Invalid: true}} { + for _, o := range []input.InputGroupOptions{{Label: "Disabled", Disabled: true}, {Label: "Read-only", ReadOnly: true}, {Label: "Invalid", Invalid: true}} { v := o.Label - mujica.InputGroup(c, &v, o) + input.InputGroup(c, &v, o) } }) }}, @@ -226,7 +226,7 @@ var formPages = []page{ if len(d.code) > n { d.code = d.code[:n] } - p := mujica.PinInput(c, &d.code, mujica.PinInputOptions{Length: n, Disabled: d.disabled, Invalid: d.invalid}) + p := input.PinInput(c, &d.code, input.PinInputOptions{Length: n, Disabled: d.disabled, Invalid: d.invalid}) if p.Changed() { log(fmt.Sprintf("code %q", d.code)) } @@ -235,8 +235,8 @@ var formPages = []page{ } formStates(c, func() { a, b := "12", "0420" - mujica.PinInput(c, &a, mujica.PinInputOptions{Length: 4, Disabled: true, Label: "Disabled code"}) - mujica.PinInput(c, &b, mujica.PinInputOptions{Length: 4, Invalid: true, Label: "Invalid code"}) + input.PinInput(c, &a, input.PinInputOptions{Length: 4, Disabled: true, Label: "Disabled code"}) + input.PinInput(c, &b, input.PinInputOptions{Length: 4, Invalid: true, Label: "Invalid code"}) }) }}, {ID: "045", Name: "TagsInput", ZhName: "标签输入", Category: "Forms", Code: formCode["TagsInput"], @@ -244,7 +244,7 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "045", func() tagsDemo { return tagsDemo{tags: []string{"rose", "thorn"}} }) formToggles(c, []string{"At most 5", "Disabled"}, &d.limit, &d.disabled) - opts := mujica.TagsInputOptions{Placeholder: "Add a flower", Label: "Flowers", Disabled: d.disabled, + opts := input.TagsInputOptions{Placeholder: "Add a flower", Label: "Flowers", Disabled: d.disabled, Validate: func(s string) string { if len([]rune(s)) > 16 { return "Keep tags under 16 characters." @@ -254,7 +254,7 @@ var formPages = []page{ if d.limit { opts.Max = 5 } - if mujica.TagsInput(c, &d.tags, opts).Changed() { + if input.TagsInput(c, &d.tags, opts).Changed() { log(fmt.Sprintf("tags %q", d.tags)) } }}, @@ -270,13 +270,13 @@ var formPages = []page{ if !d.half { d.value = float64(int(d.value + 0.5)) } - if mujica.Rating(c, &d.value, mujica.RatingOptions{Step: step, ReadOnly: d.readOnly, Disabled: d.disabled, ShowValue: true}).Changed() { + if input.Rating(c, &d.value, input.RatingOptions{Step: step, ReadOnly: d.readOnly, Disabled: d.disabled, ShowValue: true}).Changed() { log(fmt.Sprintf("rating %v", d.value)) } formStates(c, func() { a, b := 4.5, 2.0 - mujica.Rating(c, &a, mujica.RatingOptions{Step: 0.5, ReadOnly: true, Label: "Read-only rating"}) - mujica.Rating(c, &b, mujica.RatingOptions{Disabled: true, Label: "Disabled rating"}) + input.Rating(c, &a, input.RatingOptions{Step: 0.5, ReadOnly: true, Label: "Read-only rating"}) + input.Rating(c, &b, input.RatingOptions{Disabled: true, Label: "Disabled rating"}) }) }}, {ID: "047", Name: "ColorPicker", ZhName: "颜色选择器", Category: "Forms", Code: formCode["ColorPicker"], @@ -284,7 +284,7 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "047", func() colorDemo { return colorDemo{color: ui.Hex("#7D2034")} }) formToggles(c, []string{"Disabled"}, &d.disabled) - if mujica.ColorPicker(c, &d.color, mujica.ColorPickerOptions{Label: "Banner color", Disabled: d.disabled}).Changed() { + if input.ColorPicker(c, &d.color, input.ColorPickerOptions{Label: "Banner color", Disabled: d.disabled}).Changed() { log(fmt.Sprintf("color %v", d.color)) } }}, @@ -293,11 +293,11 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "048", func() pickerDemo { return pickerDemo{multiple: true} }) formToggles(c, []string{"Folders", "Multiple", "PDF and MIDI only", "Disabled"}, &d.directory, &d.multiple, &d.filter, &d.disabled) - opts := mujica.FilePickerOptions{Directory: d.directory, Multiple: d.multiple, Disabled: d.disabled, Title: "Choose scores"} + opts := input.FilePickerOptions{Directory: d.directory, Multiple: d.multiple, Disabled: d.disabled, Title: "Choose scores"} if d.filter { opts.Filters = []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}} } - p := mujica.FilePicker(c, &d.paths, opts) + p := input.FilePicker(c, &d.paths, opts) if p.Changed() { log(fmt.Sprintf("picked %d: %q", len(d.paths), d.paths)) } @@ -310,11 +310,11 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "049", func() dropDemo { return dropDemo{} }) formToggles(c, []string{"Images only", "Disabled"}, &d.images, &d.disabled) - opts := mujica.FileDropZoneOptions{MaxFiles: 4, MaxSize: 10 << 20, Disabled: d.disabled} + opts := input.FileDropZoneOptions{MaxFiles: 4, MaxSize: 10 << 20, Disabled: d.disabled} if d.images { opts.Extensions = []string{"png", "jpg", "jpeg", "gif"} } - if mujica.FileDropZone(c, &d.files, opts).Changed() { + if input.FileDropZone(c, &d.files, opts).Changed() { log(fmt.Sprintf("files %d", len(d.files))) } }}, @@ -323,7 +323,7 @@ var formPages = []page{ Demo: func(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "050", func() transferDemo { return transferDemo{chosen: []string{"sakiko"}} }) formToggles(c, []string{"Disabled"}, &d.disabled) - if mujica.Transfer(c, &d.chosen, transferDemoItems, mujica.TransferOptions{SourceTitle: "Auditions", TargetTitle: "Band", Disabled: d.disabled}).Changed() { + if input.Transfer(c, &d.chosen, transferDemoItems, input.TransferOptions{SourceTitle: "Auditions", TargetTitle: "Band", Disabled: d.disabled}).Changed() { log(fmt.Sprintf("band %q", d.chosen)) } }}, @@ -331,13 +331,13 @@ var formPages = []page{ // formCode holds each page's copyable snippet, the same as its doc and Example. var formCode = map[string]string{ - "Form": `var state mujica.FormState + "Form": `var state input.FormState name := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - res := mujica.Form(c, &state, mujica.FormOptions{}, func(f *mujica.FormScope) { - mujica.FormItem(c, f, "name", &name, mujica.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { - return mujica.InputGroup(c, &name, mujica.InputGroupOptions{Invalid: state.Error("name") != ""}).Input + core.Use(c, core.Settings{}) + res := input.Form(c, &state, input.FormOptions{}, func(f *input.FormScope) { + input.FormItem(c, f, "name", &name, input.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + return input.InputGroup(c, &name, input.InputGroupOptions{Invalid: state.Error("name") != ""}).Input }) }) if res.Submitted() { @@ -347,18 +347,18 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)`, "FormField": `email := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FormField(c, "Email", mujica.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { - return mujica.InputGroup(c, &email, mujica.InputGroupOptions{}).Input + core.Use(c, core.Settings{}) + input.FormField(c, "Email", input.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { + return input.InputGroup(c, &email, input.InputGroupOptions{}).Input }) } ui.Render(view, 400, 300, 1)`, "InputGroup": `site := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - g := mujica.InputGroup(c, &site, mujica.InputGroupOptions{ + core.Use(c, core.Settings{}) + g := input.InputGroup(c, &site, input.InputGroupOptions{ Prefix: "https://", Label: "Site", - Buttons: []mujica.InputGroupButton{{Label: "Open"}}, + Buttons: []input.InputGroupButton{{Label: "Open"}}, }) if g.Clicked == 0 { c.OpenURL("https://" + site) @@ -367,52 +367,52 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)`, "PinInput": `code := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PinInput(c, &code, mujica.PinInputOptions{Length: 6}).Completed() { + core.Use(c, core.Settings{}) + if input.PinInput(c, &code, input.PinInputOptions{Length: 6}).Completed() { fmt.Println("verify", code) } } ui.Render(view, 400, 300, 1)`, "TagsInput": `tags := []string{"gothic"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TagsInput(c, &tags, mujica.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) + core.Use(c, core.Settings{}) + input.TagsInput(c, &tags, input.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) } ui.Render(view, 400, 300, 1)`, "Rating": `stars := 3.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Rating(c, &stars, mujica.RatingOptions{Step: 0.5, ShowValue: true}) + core.Use(c, core.Settings{}) + input.Rating(c, &stars, input.RatingOptions{Step: 0.5, ShowValue: true}) } ui.Render(view, 400, 300, 1)`, "ColorPicker": `tint := ui.Hex("#7D2034") view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ColorPicker(c, &tint, mujica.ColorPickerOptions{Label: "Tint"}) + core.Use(c, core.Settings{}) + input.ColorPicker(c, &tint, input.ColorPickerOptions{Label: "Tint"}) } ui.Render(view, 400, 300, 1)`, "FilePicker": `var paths []string view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FilePicker(c, &paths, mujica.FilePickerOptions{ + core.Use(c, core.Settings{}) + input.FilePicker(c, &paths, input.FilePickerOptions{ Multiple: true, Filters: []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}}, }) } ui.Render(view, 400, 300, 1)`, - "FileDropZone": `var files []mujica.DroppedFile + "FileDropZone": `var files []input.DroppedFile view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.FileDropZone(c, &files, mujica.FileDropZoneOptions{ + core.Use(c, core.Settings{}) + input.FileDropZone(c, &files, input.FileDropZoneOptions{ Extensions: []string{"png", "jpg"}, MaxFiles: 3, MaxSize: 5 << 20, }) } ui.Render(view, 400, 300, 1)`, "Transfer": `chosen := []string{"uika"} -items := []mujica.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} +items := []input.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Transfer(c, &chosen, items, mujica.TransferOptions{}) + core.Use(c, core.Settings{}) + input.Transfer(c, &chosen, items, input.TransferOptions{}) } ui.Render(view, 400, 300, 1)`, } diff --git a/examples/gallery/pages_git.go b/examples/gallery/pages_git.go index 4ac34b8..e0226c5 100644 --- a/examples/gallery/pages_git.go +++ b/examples/gallery/pages_git.go @@ -4,12 +4,12 @@ var gitPages = []page{ { ID: "301", Name: "DiffViewer", ZhName: "差异查看器 比较 diff", Category: "Git", Demo: gitDiffViewerDemo, - Code: `var state mujica.DiffViewerState + Code: `var state git.DiffViewerState from := "func main() {\n\tprintln(\"hi\")\n}\n" to := "func main() {\n\tprintln(\"hello\")\n}\n" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffViewer(c, &state, from, to, mujica.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) + core.Use(c, core.Settings{}) + git.DiffViewer(c, &state, from, to, git.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) } ui.Render(view, 400, 300, 1)`, Keys: "Tab focuses the list; arrows and Page Up/Down scroll. Fold buttons take Tab; Enter or Space shows the hidden lines.", @@ -18,8 +18,8 @@ ui.Render(view, 400, 300, 1)`, ID: "302", Name: "InlineDiff", ZhName: "行内差异 词级差异", Category: "Git", Demo: gitInlineDiffDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.InlineDiff(c, "The court meets at noon", "The court meets at dusk", mujica.InlineDiffOptions{}) + core.Use(c, core.Settings{}) + git.InlineDiff(c, "The court meets at noon", "The court meets at dusk", git.InlineDiffOptions{}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; each line is read with its full text.", @@ -27,11 +27,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "303", Name: "ThreeWayMerge", ZhName: "三方合并 合并冲突", Category: "Git", Demo: gitThreeWayMergeDemo, - Code: `var state mujica.ThreeWayMergeState + Code: `var state git.ThreeWayMergeState base, local, remote := "a\nb\nc", "a\nB1\nc", "a\nB2\nc" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ThreeWayMerge(c, &state, base, local, remote, mujica.ThreeWayMergeOptions{}) + core.Use(c, core.Settings{}) + r := git.ThreeWayMerge(c, &state, base, local, remote, git.ThreeWayMergeOptions{}) r.Element.Height(280) if done, total := state.Resolved(); r.Changed() && done == total { _ = state.Result() // write the merged file @@ -43,11 +43,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "304", Name: "ConflictResolver", ZhName: "冲突解决 冲突标记", Category: "Git", Demo: gitConflictResolverDemo, - Code: `var state mujica.ConflictResolverState + Code: `var state git.ConflictResolverState text := "a\n<<<<<<< HEAD\nmine\n=======\ntheirs\n>>>>>>> feature\nb" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ConflictResolver(c, &state, text, mujica.ConflictResolverOptions{}).Changed() { + core.Use(c, core.Settings{}) + if git.ConflictResolver(c, &state, text, git.ConflictResolverOptions{}).Changed() { _ = state.Result() // save the file once every block is resolved } } @@ -57,14 +57,14 @@ ui.Render(view, 400, 300, 1)`, { ID: "305", Name: "ChangesList", ZhName: "更改列表 暂存 未暂存", Category: "Git", Demo: gitChangesListDemo, - Code: `var state mujica.ChangesListState -files := []mujica.ChangedFile{ - {Path: "main.go", Status: mujica.GitModified, Staged: true}, - {Path: "notes.txt", Status: mujica.GitUntracked}, + Code: `var state git.ChangesListState +files := []git.ChangedFile{ + {Path: "main.go", Status: git.GitModified, Staged: true}, + {Path: "notes.txt", Status: git.GitUntracked}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ChangesList(c, &state, files, mujica.ChangesListOptions{}) + core.Use(c, core.Settings{}) + r := git.ChangesList(c, &state, files, git.ChangesListOptions{}) r.Element.Height(240) if action, path, ok := r.Action(); ok { _, _ = action, path // run git add or git restore --staged @@ -76,11 +76,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "306", Name: "CommitInput", ZhName: "提交输入 提交信息", Category: "Git", Demo: gitCommitInputDemo, - Code: `msg := mujica.CommitMessage{} + Code: `msg := git.CommitMessage{} busy := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitInput(c, &msg, mujica.CommitInputOptions{Busy: busy}) + core.Use(c, core.Settings{}) + r := git.CommitInput(c, &msg, git.CommitInputOptions{Busy: busy}) if r.Committed() || r.Amended() { busy = true // run git commit, then clear busy } @@ -91,16 +91,16 @@ ui.Render(view, 400, 300, 1)`, { ID: "307", Name: "CommitGraph", ZhName: "提交图谱 泳道 分支图", Category: "Git", Demo: gitCommitGraphDemo, - Code: `var state mujica.CommitGraphState -commits := []mujica.Commit{ + Code: `var state git.CommitGraphState +commits := []git.Commit{ {Hash: "c3", Parents: []string{"c2", "b1"}, Subject: "Merge branch", Refs: []string{"main"}}, {Hash: "b1", Parents: []string{"c1"}, Subject: "Feature"}, {Hash: "c2", Parents: []string{"c1"}, Subject: "Fix"}, {Hash: "c1", Subject: "Initial commit"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CommitGraph(c, &state, commits, mujica.CommitGraphOptions{}).Element.Height(240) + core.Use(c, core.Settings{}) + git.CommitGraph(c, &state, commits, git.CommitGraphOptions{}).Element.Height(240) } ui.Render(view, 400, 300, 1)`, Keys: "Up/Down/Home/End choose a commit; typing jumps by subject; Enter opens it.", @@ -108,11 +108,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "308", Name: "CommitList", ZhName: "提交列表 提交历史", Category: "Git", Demo: gitCommitListDemo, - Code: `var state mujica.ListState[string] -commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} + Code: `var state data.ListState[string] +commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommitList(c, &state, commits, mujica.CommitListOptions{}) + core.Use(c, core.Settings{}) + r := git.CommitList(c, &state, commits, git.CommitListOptions{}) r.Element.Height(240) if hash, ok := state.Selected(); ok && r.Submitted() { _ = hash // show the commit @@ -125,10 +125,10 @@ ui.Render(view, 400, 300, 1)`, ID: "309", Name: "BranchSelector", ZhName: "分支选择器 切换分支", Category: "Git", Demo: gitBranchSelectorDemo, Code: `branch := "main" -branches := []mujica.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} +branches := []git.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchSelector(c, &branch, branches, mujica.BranchSelectorOptions{AllowCreate: true}) + core.Use(c, core.Settings{}) + r := git.BranchSelector(c, &branch, branches, git.BranchSelectorOptions{AllowCreate: true}) if name, ok := r.Created(); ok { _ = name // run git switch -c name } @@ -139,12 +139,12 @@ ui.Render(view, 400, 300, 1)`, { ID: "310", Name: "BranchList", ZhName: "分支列表 上游 领先落后", Category: "Git", Demo: gitBranchListDemo, - Code: `var state mujica.ListState[string] -branches := []mujica.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} -actions := []mujica.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} + Code: `var state data.ListState[string] +branches := []git.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} +actions := []git.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BranchList(c, &state, branches, mujica.BranchListOptions{Actions: actions}) + core.Use(c, core.Settings{}) + r := git.BranchList(c, &state, branches, git.BranchListOptions{Actions: actions}) r.Element.Height(200) if action, name, ok := r.Action(); ok { _, _ = action, name // run the action on the branch @@ -156,11 +156,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "311", Name: "TagList", ZhName: "标签列表 tag", Category: "Git", Demo: gitTagListDemo, - Code: `var state mujica.ListState[string] -tags := []mujica.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} + Code: `var state data.ListState[string] +tags := []git.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TagList(c, &state, tags, mujica.TagListOptions{}) + core.Use(c, core.Settings{}) + r := git.TagList(c, &state, tags, git.TagListOptions{}) r.Element.Height(200) if _, hash, ok := r.Action(); ok { _ = hash // show the tagged commit @@ -172,11 +172,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "312", Name: "StashList", ZhName: "贮藏列表 stash", Category: "Git", Demo: gitStashListDemo, - Code: `var state mujica.ListState[string] -stashes := []mujica.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} + Code: `var state data.ListState[string] +stashes := []git.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.StashList(c, &state, stashes, mujica.StashListOptions{}) + core.Use(c, core.Settings{}) + r := git.StashList(c, &state, stashes, git.StashListOptions{}) r.Element.Height(200) if action, ref, ok := r.Action(); ok { _, _ = action, ref // run git stash apply, pop or drop @@ -188,14 +188,14 @@ ui.Render(view, 400, 300, 1)`, { ID: "313", Name: "BlameView", ZhName: "逐行追溯 blame 作者", Category: "Git", Demo: gitBlameViewDemo, - Code: `var state mujica.BlameViewState -lines := []mujica.BlameLine{ + Code: `var state git.BlameViewState +lines := []git.BlameLine{ {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "package main"}, {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "func main() {}"}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlameView(c, &state, lines, mujica.BlameViewOptions{Language: "go"}).Height(240) + core.Use(c, core.Settings{}) + git.BlameView(c, &state, lines, git.BlameViewOptions{Language: "go"}).Height(240) } ui.Render(view, 400, 300, 1)`, Keys: "Tab focuses the code; arrows scroll; typing jumps to a line starting with the text. Hover the left column for details.", @@ -203,11 +203,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "314", Name: "FileHistory", ZhName: "文件历史 单文件提交", Category: "Git", Demo: gitFileHistoryDemo, - Code: `var state mujica.ListState[string] -commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} + Code: `var state data.ListState[string] +commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.FileHistory(c, &state, "main.go", commits, mujica.FileHistoryOptions{}) + core.Use(c, core.Settings{}) + r := git.FileHistory(c, &state, "main.go", commits, git.FileHistoryOptions{}) r.Element.Height(240) if _, hash, ok := r.Action(); ok { _ = hash // open the diff of main.go at hash @@ -219,11 +219,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "315", Name: "PullRequestCard", ZhName: "拉取请求卡片 合并请求 PR", Category: "Git", Demo: gitPullRequestCardDemo, - Code: `pr := mujica.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", - ChecksPassed: 3, Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} + Code: `pr := git.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", + ChecksPassed: 3, Reviewers: []git.Reviewer{{Name: "Uika", State: git.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{}).Changed() { + core.Use(c, core.Settings{}) + if git.PullRequestCard(c, pr, git.PullRequestCardOptions{}).Changed() { // open the pull request } } @@ -233,15 +233,15 @@ ui.Render(view, 400, 300, 1)`, { ID: "316", Name: "ReviewComment", ZhName: "评审评论 代码评审", Category: "Git", Demo: gitReviewCommentDemo, - Code: `thread := mujica.ReviewThread{File: "main.go", Line: 12, Messages: []mujica.ReviewMessage{ + Code: `thread := git.ReviewThread{File: "main.go", Line: 12, Messages: []git.ReviewMessage{ {Author: "Uika", When: time.Now(), Body: "Can this return early?"}, }} draft := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ReviewComment(c, thread, &draft, mujica.ReviewCommentOptions{}) + core.Use(c, core.Settings{}) + r := git.ReviewComment(c, thread, &draft, git.ReviewCommentOptions{}) if text, ok := r.Replied(); ok { - thread.Messages = append(thread.Messages, mujica.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) + thread.Messages = append(thread.Messages, git.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) } if r.Resolved() || r.Reopened() { thread.Resolved = !thread.Resolved @@ -254,8 +254,8 @@ ui.Render(view, 400, 300, 1)`, ID: "317", Name: "DiffStat", ZhName: "差异统计 增删行数", Category: "Git", Demo: gitDiffStatDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DiffStat(c, 120, 8, mujica.DiffStatOptions{}) + core.Use(c, core.Settings{}) + git.DiffStat(c, 120, 8, git.DiffStatOptions{}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as additions and deletions.", @@ -264,8 +264,8 @@ ui.Render(view, 400, 300, 1)`, ID: "318", Name: "GitStatusBadge", ZhName: "Git 状态标记 文件状态", Category: "Git", Demo: gitStatusBadgeDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GitStatusBadge(c, mujica.GitModified, mujica.GitStatusBadgeOptions{ShowLabel: true}) + core.Use(c, core.Settings{}) + git.GitStatusBadge(c, git.GitModified, git.GitStatusBadgeOptions{ShowLabel: true}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; each badge is read by its status name.", @@ -274,10 +274,10 @@ ui.Render(view, 400, 300, 1)`, ID: "319", Name: "CommandBlock", ZhName: "命令块 终端输出 退出码", Category: "Git", Demo: gitCommandBlockDemo, Code: `open := true -run := mujica.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} +run := agent.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CommandBlock(c, run, &open, mujica.CommandBlockOptions{}) + core.Use(c, core.Settings{}) + r := git.CommandBlock(c, run, &open, git.CommandBlockOptions{}) if what, ok := r.Copied(); ok { _ = what // "command" or "output" is on the clipboard } @@ -289,8 +289,8 @@ ui.Render(view, 400, 300, 1)`, ID: "320", Name: "AnsiText", ZhName: "ANSI 文本 终端颜色", Category: "Git", Demo: gitAnsiTextDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", mujica.AnsiTextOptions{}) + core.Use(c, core.Settings{}) + git.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", git.AnsiTextOptions{}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; shows the text with its styles.", diff --git a/examples/gallery/pages_git_cards.go b/examples/gallery/pages_git_cards.go index 3f6dc03..6f28145 100644 --- a/examples/gallery/pages_git_cards.go +++ b/examples/gallery/pages_git_cards.go @@ -6,7 +6,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/git" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) func gitPullRequestCardDemo(c *ui.Context, log func(string)) { @@ -16,18 +19,18 @@ func gitPullRequestCardDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "315", func() state { return state{} }) navigationDemo(c, func() { - mujica.SegmentedControl(c, &s.st, []mujica.Segment{{Label: "Open"}, {Label: "Draft"}, {Label: "Merged"}, {Label: "Closed"}}, mujica.SegmentedControlOptions{Label: "State"}) + navigation.SegmentedControl(c, &s.st, []navigation.Segment{{Label: "Open"}, {Label: "Draft"}, {Label: "Merged"}, {Label: "Closed"}}, navigation.SegmentedControlOptions{Label: "State"}) navigationCheck(c, &s.failed, "Failing check", log) navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - pr := mujica.PullRequest{Number: 318, Title: "Git components 301–320", Author: "Sakiko", State: mujica.PullRequestState(s.st), + pr := git.PullRequest{Number: 318, Title: "Git components 301–320", Author: "Sakiko", State: git.PullRequestState(s.st), Head: "feature/git", Base: "main", ChecksPassed: 12, ChecksPending: 1, - Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}, {Name: "Mutsumi", State: mujica.ReviewChangesRequested}, {Name: "Umiri"}}, + Reviewers: []git.Reviewer{{Name: "Uika", State: git.ReviewApproved}, {Name: "Mutsumi", State: git.ReviewChangesRequested}, {Name: "Umiri"}}, Added: 2412, Deleted: 310, Files: 46} if s.failed { pr.ChecksFailed = 1 } - if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{Disabled: s.disabled}).Changed() { + if git.PullRequestCard(c, pr, git.PullRequestCardOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("Open #%d", pr.Number)) } }) @@ -35,12 +38,12 @@ func gitPullRequestCardDemo(c *ui.Context, log func(string)) { func gitReviewCommentDemo(c *ui.Context, log func(string)) { type state struct { - thread mujica.ReviewThread + thread git.ReviewThread draft string disabled bool } s := ui.Local(c.Root(), "316", func() state { - return state{thread: mujica.ReviewThread{File: "commit_graph.go", Line: 88, Messages: []mujica.ReviewMessage{ + return state{thread: git.ReviewThread{File: "commit_graph.go", Line: 88, Messages: []git.ReviewMessage{ {Author: "Uika", When: gitDemoTime, Body: "Should merge lanes reuse the parent's color?"}, {Author: "Sakiko", When: gitDemoTime.Add(time.Hour), Body: "Yes, the first parent keeps it."}, }}} @@ -48,10 +51,10 @@ func gitReviewCommentDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - r := mujica.ReviewComment(c, s.thread, &s.draft, mujica.ReviewCommentOptions{Disabled: s.disabled}) + r := git.ReviewComment(c, s.thread, &s.draft, git.ReviewCommentOptions{Disabled: s.disabled}) if text, ok := r.Replied(); ok { log("Reply: " + text) - s.thread.Messages = append(s.thread.Messages, mujica.ReviewMessage{Author: "You", When: c.Now(), Body: text}) + s.thread.Messages = append(s.thread.Messages, git.ReviewMessage{Author: "You", When: c.Now(), Body: text}) } if r.Resolved() { log("Resolved") @@ -72,7 +75,7 @@ func gitDiffStatDemo(c *ui.Context, log func(string)) { navigationSlider(c, &s.deleted, "Deleted", 0, 500, log) navigationSlider(c, &s.blocks, "Blocks", 1, 10, log) }, func() { - mujica.DiffStat(c, int(s.added), int(s.deleted), mujica.DiffStatOptions{Blocks: int(s.blocks)}) + git.DiffStat(c, int(s.added), int(s.deleted), git.DiffStatOptions{Blocks: int(s.blocks)}) }) } @@ -83,8 +86,8 @@ func gitStatusBadgeDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.labels, "Show labels", log) }, func() { ui.Row(c).Gap(16).Wrap().Children(func() { - for st := mujica.GitModified; st <= mujica.GitCopied; st++ { - mujica.GitStatusBadge(c, st, mujica.GitStatusBadgeOptions{ShowLabel: s.labels}) + for st := git.GitModified; st <= git.GitCopied; st++ { + git.GitStatusBadge(c, st, git.GitStatusBadgeOptions{ShowLabel: s.labels}) } }) }) @@ -99,14 +102,14 @@ func gitCommandBlockDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.failed, "Failed", log) navigationCheck(c, &s.running, "Running", log) }, func() { - run := mujica.CommandRun{Command: "go test -run Git ./...", Duration: 3214 * time.Millisecond, + run := agent.CommandRun{Command: "go test -run Git ./...", Duration: 3214 * time.Millisecond, Output: "\x1b[32mok\x1b[0m \tmujicaui\t3.214s\n\x1b[33mwarn\x1b[0m slow test: TestCommitGraphVirtualized"} if s.failed { run.ExitCode = 1 run.Output = "\x1b[31m--- FAIL\x1b[0m: TestDiffViewer (0.02s)\n\x1b[1mFAIL\x1b[0m\tmujicaui\t0.401s" } run.Running = s.running - r := mujica.CommandBlock(c, run, &s.open, mujica.CommandBlockOptions{}) + r := git.CommandBlock(c, run, &s.open, git.CommandBlockOptions{}) if r.Toggled() { log(fmt.Sprintf("Toggled: open=%v", s.open)) } @@ -122,13 +125,13 @@ func gitAnsiTextDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.wrap, "Wrap", log) }, func() { - ui.Column(c).Padding(12).Background(mujica.Tokens(c).Surface).Radius(4).Children(func() { - mujica.AnsiText(c, "\x1b[1mBuilding\x1b[0m mujica \x1b[2m(v1.1)\x1b[0m\n"+ + ui.Column(c).Padding(12).Background(core.Tokens(c).Surface).Radius(4).Children(func() { + git.AnsiText(c, "\x1b[1mBuilding\x1b[0m mujica \x1b[2m(v1.1)\x1b[0m\n"+ "\x1b[32m✓\x1b[0m 300 passed \x1b[31m✗\x1b[0m 2 failed \x1b[33m!\x1b[0m 1 skipped\n"+ "\x1b[30m0\x1b[31m1\x1b[32m2\x1b[33m3\x1b[34m4\x1b[35m5\x1b[36m6\x1b[37m7\x1b[90m8\x1b[91m9\x1b[92mA\x1b[93mB\x1b[94mC\x1b[95mD\x1b[96mE\x1b[97mF\x1b[0m\n"+ "\x1b[38;5;208m256-color\x1b[0m \x1b[38;2;125;32;52mtrue color\x1b[0m \x1b[4munderline\x1b[0m \x1b[3mitalic\x1b[0m \x1b[9mstruck\x1b[0m \x1b[7m inverse \x1b[0m\n"+ "A long line that only breaks when wrapping is on, so the terminal layout of columns stays intact as it was printed.\n"+ - "Unknown escapes stay as text: \x1b[2K(cleared line)", mujica.AnsiTextOptions{Wrap: s.wrap}) + "Unknown escapes stay as text: \x1b[2K(cleared line)", git.AnsiTextOptions{Wrap: s.wrap}) }) }) } diff --git a/examples/gallery/pages_git_demos.go b/examples/gallery/pages_git_demos.go index 8ad4b9b..00a0b58 100644 --- a/examples/gallery/pages_git_demos.go +++ b/examples/gallery/pages_git_demos.go @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/git" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var gitDemoTime = time.Date(2026, 10, 5, 9, 30, 0, 0, time.UTC) @@ -15,11 +18,11 @@ var gitDemoTime = time.Date(2026, 10, 5, 9, 30, 0, 0, time.UTC) const gitDemoOld = "package court\n\nimport \"fmt\"\n\n// Greet welcomes a guest.\nfunc Greet(name string) {\n\tfmt.Println(\"Welcome,\", name)\n}\n\nfunc Leave() {}\n\nfunc Dance() {}\n\nfunc Bow() {}\n" const gitDemoNew = "package court\n\nimport \"fmt\"\n\n// Greet welcomes a guest by title.\nfunc Greet(title, name string) {\n\tfmt.Println(\"Welcome,\", title, name)\n}\n\nfunc Leave() {}\n\nfunc Dance() {}\n\nfunc Bow() {}\n\nfunc Curtsy() {}\n" -func gitDemoCommits(n int) []mujica.Commit { - out := make([]mujica.Commit, n) +func gitDemoCommits(n int) []git.Commit { + out := make([]git.Commit, n) authors := []string{"Sakiko", "Mutsumi", "Uika", "Umiri", "Nyamu"} for i := range out { - out[i] = mujica.Commit{Hash: fmt.Sprintf("%016x%024x", uint64(n-i)*0x9E3779B97F4A7C15, n-i), Author: authors[i%5], + out[i] = git.Commit{Hash: fmt.Sprintf("%016x%024x", uint64(n-i)*0x9E3779B97F4A7C15, n-i), Author: authors[i%5], When: gitDemoTime.Add(-time.Duration(i) * 37 * time.Minute), Subject: fmt.Sprintf("Chapter %d of the chronicle", n-i)} if i+1 < n { out[i].Parents = []string{fmt.Sprintf("%016x%024x", uint64(n-i-1)*0x9E3779B97F4A7C15, n-i-1)} @@ -31,8 +34,8 @@ func gitDemoCommits(n int) []mujica.Commit { return out } -func gitDemoGraph() []mujica.Commit { - cs := []mujica.Commit{ +func gitDemoGraph() []git.Commit { + cs := []git.Commit{ {Hash: "e7", Parents: []string{"e5", "f3"}, Subject: "Merge branch 'feature/lanes'", Refs: []string{"main"}}, {Hash: "f3", Parents: []string{"f2"}, Subject: "Color lanes by branch"}, {Hash: "e5", Parents: []string{"e4", "h1"}, Subject: "Merge hotfix"}, @@ -51,8 +54,8 @@ func gitDemoGraph() []mujica.Commit { return cs } -func gitDemoBranches() []mujica.Branch { - return []mujica.Branch{ +func gitDemoBranches() []git.Branch { + return []git.Branch{ {Name: "main", Current: true, Upstream: "origin/main", Ahead: 2, Behind: 1}, {Name: "feature/lanes", Upstream: "origin/feature/lanes", Ahead: 5}, {Name: "fix/blame"}, @@ -64,7 +67,7 @@ func gitDemoBranches() []mujica.Branch { func gitDiffViewerDemo(c *ui.Context, log func(string)) { type state struct { - viewer mujica.DiffViewerState + viewer git.DiffViewerState split, plain bool context float64 } @@ -78,7 +81,7 @@ func gitDiffViewerDemo(c *ui.Context, log func(string)) { if s.plain { lang = "" } - mujica.DiffViewer(c, &s.viewer, gitDemoOld, gitDemoNew, mujica.DiffViewerOptions{SideBySide: s.split, Context: int(s.context), Language: lang}).Height(380) + git.DiffViewer(c, &s.viewer, gitDemoOld, gitDemoNew, git.DiffViewerOptions{SideBySide: s.split, Context: int(s.context), Language: lang}).Height(380) }) } @@ -93,19 +96,19 @@ func gitInlineDiffDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.mono, "Monospace", log) }, func() { - if mujica.TextInput(c, &s.from, mujica.TextInputOptions{Label: "Old line"}).Changed() { + if input.TextInput(c, &s.from, input.TextInputOptions{Label: "Old line"}).Changed() { log("Old: " + s.from) } - if mujica.TextInput(c, &s.to, mujica.TextInputOptions{Label: "New line"}).Changed() { + if input.TextInput(c, &s.to, input.TextInputOptions{Label: "New line"}).Changed() { log("New: " + s.to) } - mujica.InlineDiff(c, s.from, s.to, mujica.InlineDiffOptions{Mono: s.mono}) + git.InlineDiff(c, s.from, s.to, git.InlineDiffOptions{Mono: s.mono}) }) } func gitThreeWayMergeDemo(c *ui.Context, log func(string)) { type state struct { - merge mujica.ThreeWayMergeState + merge git.ThreeWayMergeState disabled bool } s := ui.Local(c.Root(), "303", func() state { return state{} }) @@ -115,19 +118,19 @@ func gitThreeWayMergeDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - r := mujica.ThreeWayMerge(c, &s.merge, base, ours, theirs, mujica.ThreeWayMergeOptions{Disabled: s.disabled}) + r := git.ThreeWayMerge(c, &s.merge, base, ours, theirs, git.ThreeWayMergeOptions{Disabled: s.disabled}) r.Element.Height(360) if r.Changed() { done, total := s.merge.Resolved() log(fmt.Sprintf("Changed: %d of %d resolved", done, total)) } - ui.Text(c, s.merge.Result()).Font("monospace").TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, s.merge.Result()).Font("monospace").TextColor(core.Tokens(c).TextMuted) }) } func gitConflictResolverDemo(c *ui.Context, log func(string)) { type state struct { - res mujica.ConflictResolverState + res git.ConflictResolverState disabled bool } s := ui.Local(c.Root(), "304", func() state { return state{} }) @@ -135,7 +138,7 @@ func gitConflictResolverDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - if mujica.ConflictResolver(c, &s.res, text, mujica.ConflictResolverOptions{Disabled: s.disabled}).Changed() { + if git.ConflictResolver(c, &s.res, text, git.ConflictResolverOptions{Disabled: s.disabled}).Changed() { done, total := s.res.Resolved() log(fmt.Sprintf("Changed: %d of %d resolved", done, total)) } @@ -144,23 +147,23 @@ func gitConflictResolverDemo(c *ui.Context, log func(string)) { func gitChangesListDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.ChangesListState - files []mujica.ChangedFile + list git.ChangesListState + files []git.ChangedFile disabled bool } s := ui.Local(c.Root(), "305", func() state { - return state{files: []mujica.ChangedFile{ - {Path: "commit_graph.go", Status: mujica.GitAdded, Staged: true}, - {Path: "internal/git.go", Status: mujica.GitModified}, - {Path: "docs/old.md", Status: mujica.GitDeleted}, - {Path: "notes.txt", Status: mujica.GitUntracked}, - {Path: "icons/svg/tag.svg", Status: mujica.GitRenamed}, + return state{files: []git.ChangedFile{ + {Path: "commit_graph.go", Status: git.GitAdded, Staged: true}, + {Path: "internal/git.go", Status: git.GitModified}, + {Path: "docs/old.md", Status: git.GitDeleted}, + {Path: "notes.txt", Status: git.GitUntracked}, + {Path: "icons/svg/tag.svg", Status: git.GitRenamed}, }} }) navigationDemo(c, func() { navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - r := mujica.ChangesList(c, &s.list, s.files, mujica.ChangesListOptions{Disabled: s.disabled}) + r := git.ChangesList(c, &s.list, s.files, git.ChangesListOptions{Disabled: s.disabled}) r.Element.Height(320) if a, path, ok := r.Action(); ok { log("Action: " + a + " " + path) @@ -173,8 +176,8 @@ func gitChangesListDemo(c *ui.Context, log func(string)) { } // gitStage applies a stage action to a copy of files. -func gitStage(files []mujica.ChangedFile, action, path string) []mujica.ChangedFile { - out := append([]mujica.ChangedFile(nil), files...) +func gitStage(files []git.ChangedFile, action, path string) []git.ChangedFile { + out := append([]git.ChangedFile(nil), files...) for i := range out { switch { case action == "stage_all": @@ -190,14 +193,14 @@ func gitStage(files []mujica.ChangedFile, action, path string) []mujica.ChangedF func gitCommitInputDemo(c *ui.Context, log func(string)) { type state struct { - msg mujica.CommitMessage + msg git.CommitMessage busy, disabled bool until time.Time } s := ui.Local(c.Root(), "306", func() state { return state{} }) if s.busy && c.Now().After(s.until) { s.busy = false - s.msg = mujica.CommitMessage{} + s.msg = git.CommitMessage{} log("Committed") } if s.busy { @@ -206,7 +209,7 @@ func gitCommitInputDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - r := mujica.CommitInput(c, &s.msg, mujica.CommitInputOptions{Busy: s.busy, Disabled: s.disabled}) + r := git.CommitInput(c, &s.msg, git.CommitInputOptions{Busy: s.busy, Disabled: s.disabled}) if r.Committed() || r.Amended() { log(fmt.Sprintf("Submit: amend=%v title=%q", r.Amended(), s.msg.Title)) s.busy, s.until = true, c.Now().Add(1200*time.Millisecond) @@ -216,7 +219,7 @@ func gitCommitInputDemo(c *ui.Context, log func(string)) { func gitCommitGraphDemo(c *ui.Context, log func(string)) { type state struct { - graph mujica.CommitGraphState + graph git.CommitGraphState big bool } s := ui.Local(c.Root(), "307", func() state { return state{} }) @@ -228,7 +231,7 @@ func gitCommitGraphDemo(c *ui.Context, log func(string)) { if s.big { commits = big } - r := mujica.CommitGraph(c, &s.graph, commits, mujica.CommitGraphOptions{}) + r := git.CommitGraph(c, &s.graph, commits, git.CommitGraphOptions{}) r.Element.Height(380) if hash, ok := s.graph.List.Selected(); ok && (r.Changed() || r.Submitted()) { log(fmt.Sprintf("Selected %s, open=%v", hash[:min(7, len(hash))], r.Submitted())) @@ -238,7 +241,7 @@ func gitCommitGraphDemo(c *ui.Context, log func(string)) { func gitCommitListDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.ListState[string] + list data.ListState[string] loading bool } s := ui.Local(c.Root(), "308", func() state { return state{} }) @@ -246,11 +249,11 @@ func gitCommitListDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.loading, "Loading more", log) }, func() { - st := mujica.DataReady + st := data.DataReady if s.loading { - st = mujica.DataLoadingMore + st = data.DataLoadingMore } - r := mujica.CommitList(c, &s.list, commits, mujica.CommitListOptions{Status: st}) + r := git.CommitList(c, &s.list, commits, git.CommitListOptions{Status: st}) r.Element.Height(360) if hash, ok := s.list.Selected(); ok && (r.Changed() || r.Submitted()) { log(fmt.Sprintf("Selected %s, open=%v", hash[:7], r.Submitted())) @@ -262,29 +265,29 @@ func gitBranchSelectorDemo(c *ui.Context, log func(string)) { type state struct { branch string create, disabled bool - branches []mujica.Branch + branches []git.Branch } s := ui.Local(c.Root(), "309", func() state { return state{branch: "main", create: true, branches: gitDemoBranches()} }) navigationDemo(c, func() { navigationCheck(c, &s.create, "Allow create", log) navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - r := mujica.BranchSelector(c, &s.branch, s.branches, mujica.BranchSelectorOptions{AllowCreate: s.create, Disabled: s.disabled}) + r := git.BranchSelector(c, &s.branch, s.branches, git.BranchSelectorOptions{AllowCreate: s.create, Disabled: s.disabled}) if r.Changed() { log("Changed: " + s.branch) } if name, ok := r.Created(); ok { log("Create: " + name) - s.branches = append(append([]mujica.Branch(nil), s.branches...), mujica.Branch{Name: name}) + s.branches = append(append([]git.Branch(nil), s.branches...), git.Branch{Name: name}) s.branch = name } }) } func gitBranchListDemo(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "310", func() mujica.ListState[string] { return mujica.ListState[string]{} }) - actions := []mujica.GitAction{{ID: "checkout", Label: "Check out", Icon: "git-branch"}, {ID: "delete", Label: "Delete", Icon: "trash-2"}} - r := mujica.BranchList(c, s, gitDemoBranches(), mujica.BranchListOptions{Actions: actions}) + s := ui.Local(c.Root(), "310", func() data.ListState[string] { return data.ListState[string]{} }) + actions := []git.GitAction{{ID: "checkout", Label: "Check out", Icon: "git-branch"}, {ID: "delete", Label: "Delete", Icon: "trash-2"}} + r := git.BranchList(c, s, gitDemoBranches(), git.BranchListOptions{Actions: actions}) r.Element.Height(300) if a, name, ok := r.Action(); ok { log("Action: " + a + " " + name) @@ -295,13 +298,13 @@ func gitBranchListDemo(c *ui.Context, log func(string)) { } func gitTagListDemo(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "311", func() mujica.ListState[string] { return mujica.ListState[string]{} }) - tags := []mujica.GitTag{ + s := ui.Local(c.Root(), "311", func() data.ListState[string] { return data.ListState[string]{} }) + tags := []git.GitTag{ {Name: "v1.1.0", Target: "9e3779b97f4a7c15", Annotation: "Five hundred components"}, {Name: "v1.0.0", Target: "3c6ef372fe94f82a", Annotation: "The first hundred"}, {Name: "nightly", Target: "daa66d2c7ddf743f"}, } - r := mujica.TagList(c, s, tags, mujica.TagListOptions{}) + r := git.TagList(c, s, tags, git.TagListOptions{}) r.Element.Height(220) if _, hash, ok := r.Action(); ok { log("Jump to " + hash[:7]) @@ -313,22 +316,22 @@ func gitTagListDemo(c *ui.Context, log func(string)) { func gitStashListDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.ListState[string] - stashes []mujica.Stash + list data.ListState[string] + stashes []git.Stash } s := ui.Local(c.Root(), "312", func() state { - return state{stashes: []mujica.Stash{ + return state{stashes: []git.Stash{ {Ref: "stash@{0}", Message: "WIP on main: lane colors", Branch: "main"}, {Ref: "stash@{1}", Message: "Try serif headings", Branch: "feature/type"}, {Ref: "stash@{2}", Message: "Blame hover experiment", Branch: "fix/blame"}, }} }) - r := mujica.StashList(c, &s.list, s.stashes, mujica.StashListOptions{}) + r := git.StashList(c, &s.list, s.stashes, git.StashListOptions{}) r.Element.Height(240) if a, ref, ok := r.Action(); ok { log("Action: " + a + " " + ref) if a != "apply" { - var keep []mujica.Stash + var keep []git.Stash for _, st := range s.stashes { if st.Ref != ref { keep = append(keep, st) @@ -340,23 +343,23 @@ func gitStashListDemo(c *ui.Context, log func(string)) { } func gitBlameViewDemo(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "313", func() mujica.BlameViewState { return mujica.BlameViewState{} }) - var lines []mujica.BlameLine + s := ui.Local(c.Root(), "313", func() git.BlameViewState { return git.BlameViewState{} }) + var lines []git.BlameLine for i, l := range strings.Split(strings.TrimSuffix(gitDemoNew, "\n"), "\n") { - who := []mujica.BlameLine{ + who := []git.BlameLine{ {Hash: "9e3779b97f4a7c15", Author: "Sakiko", When: gitDemoTime, Summary: "Create the court package"}, {Hash: "3c6ef372fe94f82a", Author: "Uika", When: gitDemoTime.Add(-48 * time.Hour), Summary: "Greet guests by title"}, }[min(i/4%2, 1)] who.Text = l lines = append(lines, who) } - mujica.BlameView(c, s, lines, mujica.BlameViewOptions{Language: "go"}).Height(380) - ui.Text(c, "Hover the left column for commit details.").TextColor(mujica.Tokens(c).TextMuted) + git.BlameView(c, s, lines, git.BlameViewOptions{Language: "go"}).Height(380) + ui.Text(c, "Hover the left column for commit details.").TextColor(core.Tokens(c).TextMuted) } func gitFileHistoryDemo(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "314", func() mujica.ListState[string] { return mujica.ListState[string]{} }) - r := mujica.FileHistory(c, s, "court/greet.go", gitDemoCommits(12), mujica.FileHistoryOptions{}) + s := ui.Local(c.Root(), "314", func() data.ListState[string] { return data.ListState[string]{} }) + r := git.FileHistory(c, s, "court/greet.go", gitDemoCommits(12), git.FileHistoryOptions{}) r.Element.Height(360) if _, hash, ok := r.Action(); ok { log("Diff at " + hash[:7]) diff --git a/examples/gallery/pages_input.go b/examples/gallery/pages_input.go index 19f4ea2..74da4d1 100644 --- a/examples/gallery/pages_input.go +++ b/examples/gallery/pages_input.go @@ -7,22 +7,23 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const inputCategory = "Inputs" // inputVariants lists the button variants for variant pickers. -var inputVariants = []mujica.RadioItem[mujica.ButtonVariant]{ - {Value: mujica.Primary, Label: "Primary"}, {Value: mujica.Secondary, Label: "Secondary"}, - {Value: mujica.Outline, Label: "Outline"}, {Value: mujica.Ghost, Label: "Ghost"}, {Value: mujica.Danger, Label: "Danger"}, +var inputVariants = []input.RadioItem[input.ButtonVariant]{ + {Value: input.Primary, Label: "Primary"}, {Value: input.Secondary, Label: "Secondary"}, + {Value: input.Outline, Label: "Outline"}, {Value: input.Ghost, Label: "Ghost"}, {Value: input.Danger, Label: "Danger"}, } // inputSection titles one part of a demo. func inputSection(c *ui.Context, title string, fn func()) { ui.Column(c).Gap(8).Children(func() { - ui.Text(c, title).FontWeight(600).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, title).FontWeight(600).TextColor(core.Tokens(c).TextMuted) fn() }) } @@ -31,7 +32,7 @@ func inputSection(c *ui.Context, title string, fn func()) { func inputFlags(c *ui.Context, log func(string), flags map[string]*bool, order ...string) { ui.Row(c).Gap(16).Wrap().Children(func() { for _, name := range order { - if mujica.Checkbox(c, flags[name], name, mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, flags[name], name, input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("option %s = %v", name, *flags[name])) } } @@ -39,7 +40,7 @@ func inputFlags(c *ui.Context, log func(string), flags map[string]*bool, order . } type inputButtonDemo struct { - variant mujica.ButtonVariant + variant input.ButtonVariant icon, loading, disabled bool } @@ -81,22 +82,22 @@ type inputRadioDemo struct { var inputPages = []page{ {ID: "021", Name: "Button", ZhName: "按钮", Category: inputCategory, Keys: "Tab 聚焦;Enter 或 Space 触发。", - Code: `if mujica.Button(c, "Save", mujica.ButtonOptions{Icon: icons.Must("check")}).Clicked() { + Code: `if input.Button(c, "Save", input.ButtonOptions{Icon: icons.Must("check")}).Clicked() { fmt.Println("saved") }`, Demo: func(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "021", func() inputButtonDemo { return inputButtonDemo{} }) inputSection(c, "Parameters", func() { - mujica.RadioGroup(c, &s.variant, inputVariants, mujica.RadioGroupOptions{Horizontal: true, Label: "Variant"}) + input.RadioGroup(c, &s.variant, inputVariants, input.RadioGroupOptions{Horizontal: true, Label: "Variant"}) inputFlags(c, log, map[string]*bool{"Icon": &s.icon, "Loading": &s.loading, "Disabled": &s.disabled}, "Icon", "Loading", "Disabled") }) inputSection(c, "Live", func() { - opts := mujica.ButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled} + opts := input.ButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled} if s.icon { opts.Icon = icons.Must("check") } ui.Row(c).Children(func() { - if mujica.Button(c, "Save changes", opts).Clicked() { + if input.Button(c, "Save changes", opts).Clicked() { log("Button clicked") } }) @@ -104,30 +105,30 @@ var inputPages = []page{ inputSection(c, "Variants", func() { ui.Row(c).Gap(12).Wrap().Children(func() { for _, v := range inputVariants { - if mujica.Button(c, v.Label, mujica.ButtonOptions{Variant: v.Value}).Clicked() { + if input.Button(c, v.Label, input.ButtonOptions{Variant: v.Value}).Clicked() { log(v.Label + " clicked") } } - mujica.Button(c, "Loading", mujica.ButtonOptions{Loading: true}) - mujica.Button(c, "Disabled", mujica.ButtonOptions{Disabled: true}) + input.Button(c, "Loading", input.ButtonOptions{Loading: true}) + input.Button(c, "Disabled", input.ButtonOptions{Disabled: true}) }) }) }}, {ID: "022", Name: "IconButton", ZhName: "图标按钮", Category: inputCategory, Keys: "Tab 聚焦;Enter 或 Space 触发;悬停显示提示。", - Code: `if mujica.IconButton(c, icons.Must("settings"), "Settings", mujica.IconButtonOptions{Variant: mujica.Ghost}).Clicked() { + Code: `if input.IconButton(c, icons.Must("settings"), "Settings", input.IconButtonOptions{Variant: input.Ghost}).Clicked() { fmt.Println("open settings") }`, Demo: func(c *ui.Context, log func(string)) { - s := ui.Local(c.Root(), "022", func() inputButtonDemo { return inputButtonDemo{variant: mujica.Ghost} }) + s := ui.Local(c.Root(), "022", func() inputButtonDemo { return inputButtonDemo{variant: input.Ghost} }) inputSection(c, "Parameters", func() { - mujica.RadioGroup(c, &s.variant, inputVariants, mujica.RadioGroupOptions{Horizontal: true, Label: "Variant"}) + input.RadioGroup(c, &s.variant, inputVariants, input.RadioGroupOptions{Horizontal: true, Label: "Variant"}) inputFlags(c, log, map[string]*bool{"Loading": &s.loading, "Disabled": &s.disabled}, "Loading", "Disabled") }) inputSection(c, "Live", func() { ui.Row(c).Gap(8).Children(func() { for _, n := range []string{"bold", "italic", "underline", "copy", "trash-2"} { - opts := mujica.IconButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled} - if mujica.IconButton(c, icons.Must(n), n, opts).Clicked() { + opts := input.IconButtonOptions{Variant: s.variant, Loading: s.loading, Disabled: s.disabled} + if input.IconButton(c, icons.Must(n), n, opts).Clicked() { log("IconButton " + n + " clicked") } } @@ -135,8 +136,8 @@ var inputPages = []page{ }) }}, {ID: "023", Name: "ButtonGroup", ZhName: "按钮组", Category: inputCategory, Keys: "Tab 进入组;方向键在组内移动并跳过禁用项;Enter 或 Space 触发。", - Code: `items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} -if i := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { + Code: `items := []input.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} +if i := input.ButtonGroup(c, items, input.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { fmt.Println("chose", items[i].Label) }`, Demo: func(c *ui.Context, log func(string)) { @@ -145,15 +146,15 @@ if i := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Label: "Range"}). inputFlags(c, log, map[string]*bool{"Vertical": &s.vertical, "Disable group": &s.disabled}, "Vertical", "Disable group") }) inputSection(c, "Live", func() { - items := []mujica.ButtonGroupItem{{Label: "Day"}, {Label: "Week", Disabled: true}, {Label: "Month"}, {Icon: icons.Must("calendar"), Name: "Pick a date"}} - r := mujica.ButtonGroup(c, items, mujica.ButtonGroupOptions{Vertical: s.vertical, Disabled: s.disabled, Label: "Range"}) + items := []input.ButtonGroupItem{{Label: "Day"}, {Label: "Week", Disabled: true}, {Label: "Month"}, {Icon: icons.Must("calendar"), Name: "Pick a date"}} + r := input.ButtonGroup(c, items, input.ButtonGroupOptions{Vertical: s.vertical, Disabled: s.disabled, Label: "Range"}) if i := r.ClickedItem(); i >= 0 { log(fmt.Sprintf("ButtonGroup item %d clicked", i)) } }) }}, {ID: "024", Name: "TextInput", ZhName: "文本输入框", Category: inputCategory, Keys: "Tab 聚焦;Enter 提交;Escape 清空(可清空时);Cmd/Ctrl+Z 撤销、+C/X/V 剪贴板;支持输入法组合。", - Code: `r := mujica.TextInput(c, &name, mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) + Code: `r := input.TextInput(c, &name, input.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) if r.Submitted() { fmt.Println("hello", name) }`, @@ -161,15 +162,15 @@ if r.Submitted() { s := ui.Local(c.Root(), "024", func() inputTextDemo { return inputTextDemo{clearable: true, sl: true} }) inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"Clearable": &s.clearable, "Slots": &s.sl, "ReadOnly": &s.readOnly, "Disabled": &s.disabled}, "Clearable", "Slots", "ReadOnly", "Disabled") - mujica.TextInput(c, &s.err, mujica.TextInputOptions{Label: "Error message", Placeholder: "Error message (empty for none)"}) + input.TextInput(c, &s.err, input.TextInputOptions{Label: "Error message", Placeholder: "Error message (empty for none)"}) }) inputSection(c, "Live", func() { - opts := mujica.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: s.clearable, ReadOnly: s.readOnly, Disabled: s.disabled, Error: s.err} + opts := input.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: s.clearable, ReadOnly: s.readOnly, Disabled: s.disabled, Error: s.err} if s.sl { - opts.Leading = func() { ui.Icon(c, icons.Must("user")).Size(16, 16).TextColor(mujica.Tokens(c).TextMuted) } - opts.Trailing = func() { ui.Text(c, fmt.Sprintf("%d", len([]rune(s.value)))).TextColor(mujica.Tokens(c).TextMuted) } + opts.Leading = func() { ui.Icon(c, icons.Must("user")).Size(16, 16).TextColor(core.Tokens(c).TextMuted) } + opts.Trailing = func() { ui.Text(c, fmt.Sprintf("%d", len([]rune(s.value)))).TextColor(core.Tokens(c).TextMuted) } } - r := mujica.TextInput(c, &s.value, opts) + r := input.TextInput(c, &s.value, opts) if r.Changed() { log(fmt.Sprintf("TextInput changed: %q", s.value)) } @@ -180,14 +181,14 @@ if r.Submitted() { inputSection(c, "States", func() { ui.Row(c).Gap(12).Children(func() { e, ro, off := "1789-13-01", "Read only", "Disabled" - mujica.TextInput(c, &e, mujica.TextInputOptions{Label: "Error state", Error: "No such date"}).Element.Grow(1) - mujica.TextInput(c, &ro, mujica.TextInputOptions{Label: "Read-only state", ReadOnly: true}).Element.Grow(1) - mujica.TextInput(c, &off, mujica.TextInputOptions{Label: "Disabled state", Disabled: true}).Element.Grow(1) + input.TextInput(c, &e, input.TextInputOptions{Label: "Error state", Error: "No such date"}).Element.Grow(1) + input.TextInput(c, &ro, input.TextInputOptions{Label: "Read-only state", ReadOnly: true}).Element.Grow(1) + input.TextInput(c, &off, input.TextInputOptions{Label: "Disabled state", Disabled: true}).Element.Grow(1) }) }) }}, {ID: "025", Name: "TextArea", ZhName: "多行文本框", Category: inputCategory, Keys: "Enter 换行;Cmd/Ctrl+Enter 提交(可配置);滚轮与方向键滚动;撤销、剪贴板与输入法同 TextInput。", - Code: `r := mujica.TextArea(c, ¬es, mujica.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) + Code: `r := input.TextArea(c, ¬es, input.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) if r.Submitted() { fmt.Println("send", notes) }`, @@ -195,12 +196,12 @@ if r.Submitted() { s := ui.Local(c.Root(), "025", func() inputAreaDemo { return inputAreaDemo{max: 120} }) inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"ReadOnly": &s.readOnly, "Disabled": &s.disabled}, "ReadOnly", "Disabled") - if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 0, Max: 1000, Step: 10, Label: "MaxLength"}).Changed() { + if input.NumberInput(c, &s.max, input.NumberInputOptions{Min: 0, Max: 1000, Step: 10, Label: "MaxLength"}).Changed() { log(fmt.Sprintf("option MaxLength = %v", s.max)) } }) inputSection(c, "Live", func() { - r := mujica.TextArea(c, &s.value, mujica.TextAreaOptions{Label: "Notes", Placeholder: "Write a dedication…", MinHeight: 80, MaxHeight: 160, + r := input.TextArea(c, &s.value, input.TextAreaOptions{Label: "Notes", Placeholder: "Write a dedication…", MinHeight: 80, MaxHeight: 160, MaxLength: int(s.max), ReadOnly: s.readOnly, Disabled: s.disabled}) if r.Changed() { log(fmt.Sprintf("TextArea changed: %d characters", len([]rune(s.value)))) @@ -211,15 +212,15 @@ if r.Submitted() { }) }}, {ID: "026", Name: "NumberInput", ZhName: "数字输入框", Category: inputCategory, Keys: "Up/Down 按步长增减;步进模式下 Up/Down/Left/Right、Home、End。", - Code: `if mujica.NumberInput(c, &copies, mujica.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { + Code: `if input.NumberInput(c, &copies, input.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { fmt.Println("copies", copies) }`, Demo: func(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "026", func() inputNumberDemo { return inputNumberDemo{value: 3, step: 1} }) inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"AllowEmpty": &s.empty, "StepperOnly": &s.stepper, "Disabled": &s.disabled, "Error": &s.fail}, "AllowEmpty", "StepperOnly", "Disabled", "Error") - steps := []mujica.RadioItem[float64]{{Value: 1, Label: "Step 1"}, {Value: 0.5, Label: "Step 0.5"}, {Value: 0.01, Label: "Step 0.01"}} - if mujica.RadioGroup(c, &s.step, steps, mujica.RadioGroupOptions{Horizontal: true, Label: "Step"}).Changed() { + steps := []input.RadioItem[float64]{{Value: 1, Label: "Step 1"}, {Value: 0.5, Label: "Step 0.5"}, {Value: 0.01, Label: "Step 0.01"}} + if input.RadioGroup(c, &s.step, steps, input.RadioGroupOptions{Horizontal: true, Label: "Step"}).Changed() { log(fmt.Sprintf("option Step = %v", s.step)) } }) @@ -230,12 +231,12 @@ if r.Submitted() { s.value = 0 } inputSection(c, "Live", func() { - opts := mujica.NumberInputOptions{Min: 0, Max: 10, Step: s.step, AllowEmpty: s.empty, StepperOnly: s.stepper, Disabled: s.disabled, Label: "Quantity", Placeholder: "Empty"} + opts := input.NumberInputOptions{Min: 0, Max: 10, Step: s.step, AllowEmpty: s.empty, StepperOnly: s.stepper, Disabled: s.disabled, Label: "Quantity", Placeholder: "Empty"} if s.fail { opts.Error = "Not in stock" } ui.Box(c).Width(240).Children(func() { - if mujica.NumberInput(c, &s.value, opts).Changed() { + if input.NumberInput(c, &s.value, opts).Changed() { log(fmt.Sprintf("NumberInput changed: %v", s.value)) } }) @@ -243,14 +244,14 @@ if r.Submitted() { inputSection(c, "States", func() { ui.Row(c).Gap(12).AlignItems(ui.Start).Children(func() { a, b, e := 4.0, 2.0, 7.5 - mujica.NumberInput(c, &a, mujica.NumberInputOptions{Min: 1, Max: 8, StepperOnly: true, Label: "Stepper state"}) - mujica.NumberInput(c, &b, mujica.NumberInputOptions{Label: "Disabled state", Disabled: true}).Element.Width(160) - mujica.NumberInput(c, &e, mujica.NumberInputOptions{Step: 0.5, Label: "Error state", Error: "Must be whole"}).Element.Width(160) + input.NumberInput(c, &a, input.NumberInputOptions{Min: 1, Max: 8, StepperOnly: true, Label: "Stepper state"}) + input.NumberInput(c, &b, input.NumberInputOptions{Label: "Disabled state", Disabled: true}).Element.Width(160) + input.NumberInput(c, &e, input.NumberInputOptions{Step: 0.5, Label: "Error state", Error: "Must be whole"}).Element.Width(160) }) }) }}, {ID: "027", Name: "PasswordInput", ZhName: "密码输入框", Category: inputCategory, Keys: "Tab 依次聚焦输入框与显隐按钮;Space 或 Enter 切换显隐;Enter 在输入框中提交;粘贴可用,遮蔽时不可复制。", - Code: `if mujica.PasswordInput(c, &password, mujica.PasswordInputOptions{Label: "Password"}).Submitted() { + Code: `if input.PasswordInput(c, &password, input.PasswordInputOptions{Label: "Password"}).Submitted() { fmt.Println("signing in") }`, Demo: func(c *ui.Context, log func(string)) { @@ -259,11 +260,11 @@ if r.Submitted() { inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled, "Error": &s.fail}, "Disabled", "Error") }) inputSection(c, "Live", func() { - opts := mujica.PasswordInputOptions{Label: "Password", Placeholder: "Password", Disabled: s.disabled} + opts := input.PasswordInputOptions{Label: "Password", Placeholder: "Password", Disabled: s.disabled} if s.fail { opts.Error = "At least 8 characters" } - r := mujica.PasswordInput(c, &s.value, opts) + r := input.PasswordInput(c, &s.value, opts) if r.Changed() { // The contents never reach the log. log(fmt.Sprintf("PasswordInput changed: %d characters", len([]rune(s.value)))) @@ -278,7 +279,7 @@ if r.Submitted() { if query != "" { matches = strings.Count(text, query) } -r := mujica.SearchInput(c, &query, mujica.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) +r := input.SearchInput(c, &query, input.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) if r.Moved() { fmt.Println("show match", current) }`, @@ -289,7 +290,7 @@ if r.Moved() { inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled}, "Disabled") }) inputSection(c, "Search mode", func() { - r := mujica.SearchInput(c, &s.query, mujica.SearchInputOptions{Disabled: s.disabled}) + r := input.SearchInput(c, &s.query, input.SearchInputOptions{Disabled: s.disabled}) if r.Changed() { log(fmt.Sprintf("SearchInput changed: %q", s.query)) } @@ -302,45 +303,45 @@ if r.Moved() { if s.find != "" { n = strings.Count(text, s.find) } - r := mujica.SearchInput(c, &s.find, mujica.SearchInputOptions{Find: true, Matches: n, Current: &s.current, Disabled: s.disabled}) + r := input.SearchInput(c, &s.find, input.SearchInputOptions{Find: true, Matches: n, Current: &s.current, Disabled: s.disabled}) if r.Changed() { log(fmt.Sprintf("find query: %q, %d matches", s.find, n)) } if r.Moved() { log(fmt.Sprintf("find moved to match %d", s.current+1)) } - ui.Text(c, text).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, text).TextColor(core.Tokens(c).TextMuted) }) }}, {ID: "029", Name: "Checkbox", ZhName: "复选框", Category: inputCategory, Keys: "Tab 聚焦;Space 切换;点击标签同样切换;全选框在组内勾选全部或清空。", - Code: `mujica.CheckboxGroup(c, "Notifications", []mujica.CheckboxItem{ + Code: `input.CheckboxGroup(c, "Notifications", []input.CheckboxItem{ {Label: "Mail", Value: &mail}, {Label: "Calendar", Value: &calendar}, -}, mujica.CheckboxGroupOptions{})`, +}, input.CheckboxGroupOptions{})`, Demo: func(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "029", func() inputCheckDemo { return inputCheckDemo{items: [4]bool{true, false, false, true}} }) inputSection(c, "Parameters", func() { inputFlags(c, log, map[string]*bool{"Indeterminate": &s.mixed, "Disabled": &s.disabled}, "Indeterminate", "Disabled") }) inputSection(c, "Live", func() { - if mujica.Checkbox(c, &s.single, "Accept the terms", mujica.CheckboxOptions{Indeterminate: s.mixed, Disabled: s.disabled}).Changed() { + if input.Checkbox(c, &s.single, "Accept the terms", input.CheckboxOptions{Indeterminate: s.mixed, Disabled: s.disabled}).Changed() { s.mixed = false log(fmt.Sprintf("Checkbox changed: %v", s.single)) } }) inputSection(c, "Group with select all", func() { - items := []mujica.CheckboxItem{ + items := []input.CheckboxItem{ {Label: "Mail", Value: &s.items[0]}, {Label: "Calendar", Value: &s.items[1]}, {Label: "Messages", Value: &s.items[2]}, {Label: "Security alerts (required)", Value: &s.items[3], Disabled: true}, } - if mujica.CheckboxGroup(c, "Notifications", items, mujica.CheckboxGroupOptions{Disabled: s.disabled}).Changed() { + if input.CheckboxGroup(c, "Notifications", items, input.CheckboxGroupOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("CheckboxGroup changed: %v", s.items)) } }) }}, {ID: "030", Name: "RadioGroup", ZhName: "单选组", Category: inputCategory, Keys: "Tab 进入组并落在已选项;方向键选择并跳过禁用项;Home、End 选择首末项。", - Code: `items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} -if mujica.RadioGroup(c, &size, items, mujica.RadioGroupOptions{Label: "Size"}).Changed() { + Code: `items := []input.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} +if input.RadioGroup(c, &size, items, input.RadioGroupOptions{Label: "Size"}).Changed() { fmt.Println("size", size) }`, Demo: func(c *ui.Context, log func(string)) { @@ -349,8 +350,8 @@ if mujica.RadioGroup(c, &size, items, mujica.RadioGroupOptions{Label: "Size"}).C inputFlags(c, log, map[string]*bool{"Horizontal": &s.horizontal, "Disabled": &s.disabledAll}, "Horizontal", "Disabled") }) inputSection(c, "Live", func() { - items := []mujica.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}, {Value: "xl", Label: "Sold out", Disabled: true}} - if mujica.RadioGroup(c, &s.value, items, mujica.RadioGroupOptions{Horizontal: s.horizontal, Disabled: s.disabledAll, Label: "Size"}).Changed() { + items := []input.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}, {Value: "xl", Label: "Sold out", Disabled: true}} + if input.RadioGroup(c, &s.value, items, input.RadioGroupOptions{Horizontal: s.horizontal, Disabled: s.disabledAll, Label: "Size"}).Changed() { log("RadioGroup chose " + s.value) } }) diff --git a/examples/gallery/pages_layout.go b/examples/gallery/pages_layout.go index 6c0eb71..c2c791c 100644 --- a/examples/gallery/pages_layout.go +++ b/examples/gallery/pages_layout.go @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" ) var layoutPages = []page{ @@ -67,16 +69,16 @@ func layoutSlider(c *ui.Context, v *float64, label string, lo, hi float64, log f func layoutControls(c *ui.Context, fn func()) { ui.Row(c).Wrap().Gap(12).Children(fn) } func layoutFrame(c *ui.Context, h float32, fn func()) *ui.Element { - return ui.Column(c).Height(h).Border(1, mujica.Tokens(c).Border).Radius(4).Clip().Children(fn) + return ui.Column(c).Height(h).Border(1, core.Tokens(c).Border).Radius(4).Clip().Children(fn) } var layoutSpaces = []string{"Default", "None", "XS", "S", "M", "L", "XL", "XXL"} -func layoutSpace(s string) mujica.Space { return mujica.Space(slices.Index(layoutSpaces, s)) } +func layoutSpace(s string) layout.Space { return layout.Space(slices.Index(layoutSpaces, s)) } const layoutAppShellCode = `var collapsed bool -mujica.AppShell(c, &collapsed, mujica.AppShellOptions{ - TitleBar: func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) }, +layout.AppShell(c, &collapsed, layout.AppShellOptions{ + TitleBar: func() { layout.TitleBar(c, "Court Records", layout.TitleBarOptions{}) }, Sidebar: func() { ui.Text(c, "Chronicles") }, Content: func() { ui.Text(c, "The Grand Hall") }, })` @@ -94,7 +96,7 @@ func layoutAppShellDemo(c *ui.Context, log func(string)) { layoutCheck(c, &s.collapsed, "Collapsed", log) layoutSlider(c, &s.width, "Sidebar width", 160, 320, log) }) - opts := mujica.AppShellOptions{ + opts := layout.AppShellOptions{ SidebarWidth: float32(s.width), ContentScroll: &s.scroll, Sidebar: func() { @@ -103,7 +105,7 @@ func layoutAppShellDemo(c *ui.Context, log func(string)) { } }, Content: func() { - mujica.Container(c, mujica.ContainerOptions{}, func() { + layout.Container(c, layout.ContainerOptions{}, func() { for i := range 30 { ui.Text(c, fmt.Sprintf("Entry %d of the court record", i+1)).Padding(4, 0) } @@ -111,24 +113,24 @@ func layoutAppShellDemo(c *ui.Context, log func(string)) { }, } if s.title { - opts.TitleBar = func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) } + opts.TitleBar = func() { layout.TitleBar(c, "Court Records", layout.TitleBarOptions{}) } } if s.status { opts.StatusBar = func() { - mujica.StatusBar(c, mujica.StatusBarOptions{Left: []mujica.StatusItem{{ID: "pos", Text: fmt.Sprintf("Scrolled %.0f", s.scroll.Y)}}}) + layout.StatusBar(c, layout.StatusBarOptions{Left: []layout.StatusItem{{ID: "pos", Text: fmt.Sprintf("Scrolled %.0f", s.scroll.Y)}}}) } } ui.Column(c).Margin(16, 0, 0, 0).Children(func() { layoutFrame(c, 360, func() { - if mujica.AppShell(c, &s.collapsed, opts).Toggled { + if layout.AppShell(c, &s.collapsed, opts).Toggled { log(fmt.Sprintf("sidebar collapsed: %v", s.collapsed)) } }) }) } -const layoutTitleBarCode = `mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{ - Trailing: func() { mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}) }, +const layoutTitleBarCode = `layout.TitleBar(c, "Court Records", layout.TitleBarOptions{ + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, })` func layoutTitleBarDemo(c *ui.Context, log func(string)) { @@ -142,32 +144,32 @@ func layoutTitleBarDemo(c *ui.Context, log func(string)) { layoutCheck(c, &s.trailing, "Trailing tool", log) layoutChoice(c, &s.title, "Title", []string{"Court Records", "宫廷档案", "A very long title of the royal chronicle of the court"}, log) }) - var opts mujica.TitleBarOptions + var opts layout.TitleBarOptions if s.leading { opts.Leading = func() { - if mujica.Button(c, "", mujica.ButtonOptions{Variant: mujica.Ghost, Icon: icons.Must("panel-left"), Label: "Sidebar"}).Clicked() { + if input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("panel-left"), Label: "Sidebar"}).Clicked() { log("leading tool clicked") } } } if s.trailing { opts.Trailing = func() { - if mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}).Clicked() { log("Share clicked") } } } ui.Column(c).Margin(16, 0, 0, 0).Width(520).Children(func() { - layoutFrame(c, 90, func() { mujica.TitleBar(c, s.title, opts) }) + layoutFrame(c, 90, func() { layout.TitleBar(c, s.title, opts) }) }) bar := c.TitleBar() ui.Text(c, fmt.Sprintf("Window controls reserve: left %.0f, right %.0f, height %.0f DIP", bar.Left, bar.Right, bar.Height)). - TextColor(mujica.Tokens(c).TextMuted).Margin(8, 0, 0, 0) + TextColor(core.Tokens(c).TextMuted).Margin(8, 0, 0, 0) } -const layoutStatusBarCode = `bar := mujica.StatusBar(c, mujica.StatusBarOptions{ - Left: []mujica.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, - Right: []mujica.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, +const layoutStatusBarCode = `bar := layout.StatusBar(c, layout.StatusBarOptions{ + Left: []layout.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, + Right: []layout.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, }) if bar.Clicked == "bell" { opened++ @@ -186,30 +188,30 @@ func layoutStatusBarDemo(c *ui.Context, log func(string)) { layoutSlider(c, &s.progress, "Percent", 0, 100, log) layoutChoice(c, &s.tone, "Tone", []string{"Neutral", "Success", "Warning", "Danger", "Info"}, log) }) - tones := map[string]mujica.StatusItemTone{"Neutral": mujica.StatusItemNeutral, "Success": mujica.StatusItemSuccess, - "Warning": mujica.StatusItemWarning, "Danger": mujica.StatusItemDanger, "Info": mujica.StatusItemInfo} - opts := mujica.StatusBarOptions{ - Left: []mujica.StatusItem{ + tones := map[string]layout.StatusItemTone{"Neutral": layout.StatusItemNeutral, "Success": layout.StatusItemSuccess, + "Warning": layout.StatusItemWarning, "Danger": layout.StatusItemDanger, "Info": layout.StatusItemInfo} + opts := layout.StatusBarOptions{ + Left: []layout.StatusItem{ {ID: "branch", Text: "main", Icon: icons.Must("folder")}, {ID: "sync", Text: "Syncing", ShowProgress: s.showProgress, Progress: float32(s.progress / 100)}, {ID: "lint", Text: "2 warnings", Tone: tones[s.tone], Secondary: true, Action: true}, }, - Right: []mujica.StatusItem{ + Right: []layout.StatusItem{ {ID: "enc", Text: "UTF-8", Secondary: true}, {ID: "pos", Text: "Ln 12, Col 4", Secondary: true}, {ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}, }, } ui.Column(c).Margin(16, 0, 0, 0).Width(float32(s.width)).Children(func() { - if id := mujica.StatusBar(c, opts).Clicked; id != "" { + if id := layout.StatusBar(c, opts).Clicked; id != "" { log("clicked " + id) } }) ui.Text(c, "Narrow the width: secondary items fold into a count, last first."). - TextColor(mujica.Tokens(c).TextMuted).Margin(8, 0, 0, 0) + TextColor(core.Tokens(c).TextMuted).Margin(8, 0, 0, 0) } -const layoutContainerCode = `mujica.Container(c, mujica.ContainerOptions{MaxWidth: 320, Center: true}, func() { +const layoutContainerCode = `layout.Container(c, layout.ContainerOptions{MaxWidth: 320, Center: true}, func() { ui.Text(c, "A centered column of prose.") })` @@ -228,10 +230,10 @@ func layoutContainerDemo(c *ui.Context, log func(string)) { layoutCheck(c, &s.clip, "Clip", log) layoutCheck(c, &s.wide, "Overflowing child", log) }) - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Column(c).Margin(16, 0, 0, 0).Children(func() { layoutFrame(c, 200, func() { - mujica.Container(c, mujica.ContainerOptions{MaxWidth: float32(s.maxWidth), Padding: layoutSpace(s.padding), + layout.Container(c, layout.ContainerOptions{MaxWidth: float32(s.maxWidth), Padding: layoutSpace(s.padding), Center: s.center, Fill: s.fill, Clip: s.clip}, func() { ui.Text(c, "Of the Court").FontSize(18) ui.Text(c, "Prose keeps a readable measure inside a container.").TextColor(k.TextMuted) @@ -243,12 +245,12 @@ func layoutContainerDemo(c *ui.Context, log func(string)) { }) } -const layoutStackCode = `mujica.Stack(c, mujica.StackOptions{Horizontal: true, Gap: mujica.SpaceM}, func() { - mujica.Button(c, "Save", mujica.ButtonOptions{}) +const layoutStackCode = `layout.Stack(c, layout.StackOptions{Horizontal: true, Gap: layout.SpaceM}, func() { + input.Button(c, "Save", input.ButtonOptions{}) if showDraft { - mujica.Button(c, "Draft", mujica.ButtonOptions{Variant: mujica.Secondary}) + input.Button(c, "Draft", input.ButtonOptions{Variant: input.Secondary}) } - mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Ghost}) + input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Ghost}) })` func layoutStackDemo(c *ui.Context, log func(string)) { @@ -272,13 +274,13 @@ func layoutStackDemo(c *ui.Context, log func(string)) { layoutChoice(c, &s.align, "Align", aligns, log) layoutChoice(c, &s.justify, "Justify", []string{"Start", "Center", "End", "SpaceBetween"}, log) }) - align := mujica.StackAlign(slices.Index(aligns, s.align)) + align := layout.StackAlign(slices.Index(aligns, s.align)) ui.Column(c).Margin(16, 0, 0, 0).Width(420).Children(func() { layoutFrame(c, 200, func() { - mujica.Stack(c, mujica.StackOptions{Horizontal: s.horizontal, Wrap: s.wrap, Gap: layoutSpace(s.gap), + layout.Stack(c, layout.StackOptions{Horizontal: s.horizontal, Wrap: s.wrap, Gap: layoutSpace(s.gap), Align: align, Justify: justifies[s.justify]}, func() { for _, n := range []string{"Save", "Draft", "Publish", "Archive", "Cancel"} { - if (!s.hide || n != "Draft") && mujica.Button(c, n, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if (!s.hide || n != "Draft") && input.Button(c, n, input.ButtonOptions{Variant: input.Secondary}).Clicked() { log(n + " clicked") } } @@ -287,8 +289,8 @@ func layoutStackDemo(c *ui.Context, log func(string)) { }) } -const layoutGridCode = `mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { - mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) +const layoutGridCode = `layout.Grid(c, layout.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { + layout.GridCell(c, layout.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) ui.Text(c, "Name") ui.Text(c, "Ada") })` @@ -309,22 +311,22 @@ func layoutGridDemo(c *ui.Context, log func(string)) { layoutCheck(c, &s.header, "Header spans all", log) layoutCheck(c, &s.tall, "First cell spans 2 rows", log) }) - k := mujica.Tokens(c) + k := core.Tokens(c) cell := func(text string) { ui.Box(c).Center().Padding(8).Background(k.Surface).Border(1, k.Border).Radius(4). Children(func() { ui.Text(c, text).TextColor(k.TextMuted) }) } ui.Column(c).Margin(16, 0, 0, 0).Width(520).Children(func() { - mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{tracks[s.first], ui.Fr(1), ui.Fr(1)}, + layout.Grid(c, layout.GridOptions{Columns: []ui.Track{tracks[s.first], ui.Fr(1), ui.Fr(1)}, Gap: layoutSpace(s.gap), RowGap: layoutSpace(s.rowGap)}, func() { if s.header { - mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { cell("Header, ColumnSpan -1") }) + layout.GridCell(c, layout.GridCellOptions{ColumnSpan: -1}, func() { cell("Header, ColumnSpan -1") }) } span := 1 if s.tall { span = 2 } - mujica.GridCell(c, mujica.GridCellOptions{RowSpan: span}, func() { cell(s.first) }) + layout.GridCell(c, layout.GridCellOptions{RowSpan: span}, func() { cell(s.first) }) for i := range 4 { cell(fmt.Sprintf("Cell %d", i+1)) } @@ -333,7 +335,7 @@ func layoutGridDemo(c *ui.Context, log func(string)) { } const layoutScrollAreaCode = `var offset ui.ScrollState -mujica.ScrollArea(c, &offset, mujica.ScrollAreaOptions{Label: "Log"}, func() { +layout.ScrollArea(c, &offset, layout.ScrollAreaOptions{Label: "Log"}, func() { for range 50 { ui.Text(c, "A line of the log") } @@ -346,25 +348,25 @@ func layoutScrollAreaDemo(c *ui.Context, log func(string)) { lastX, lastY float32 } s := ui.Local(c.Root(), "007", func() state { return state{direction: "Vertical"} }) - dirs := map[string]mujica.ScrollDirection{"Vertical": mujica.ScrollVertical, "Horizontal": mujica.ScrollHorizontal, "Both": mujica.ScrollBoth} + dirs := map[string]layout.ScrollDirection{"Vertical": layout.ScrollVertical, "Horizontal": layout.ScrollHorizontal, "Both": layout.ScrollBoth} layoutControls(c, func() { layoutChoice(c, &s.direction, "Direction", []string{"Vertical", "Horizontal", "Both"}, log) - if mujica.Button(c, "Top", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Top", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.offset.X, s.offset.Y = 0, 0 log("scroll to start") } - if mujica.Button(c, "Middle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Middle", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.offset.X, s.offset.Y = s.offset.MaxX/2, s.offset.MaxY/2 log("scroll to middle") } - if mujica.Button(c, "End", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "End", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.offset.X, s.offset.Y = math.MaxFloat32, math.MaxFloat32 log("scroll to end") } }) - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Column(c).Margin(16, 0, 0, 0).Width(420).Children(func() { - mujica.ScrollArea(c, &s.offset, mujica.ScrollAreaOptions{Direction: dirs[s.direction], Label: "Ledger"}, func() { + layout.ScrollArea(c, &s.offset, layout.ScrollAreaOptions{Direction: dirs[s.direction], Label: "Ledger"}, func() { for i := range 40 { ui.Text(c, fmt.Sprintf("%02d Ledger line of the treasury, folio %d, carried over from the last quarter", i+1, i+7)). SingleLine().Padding(4, 12) @@ -379,30 +381,30 @@ func layoutScrollAreaDemo(c *ui.Context, log func(string)) { TextColor(k.TextMuted).Margin(8, 0, 0, 0) } -const layoutSplitPaneCode = `split := mujica.SplitPaneState{Size: 160} -mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst}, +const layoutSplitPaneCode = `split := layout.SplitPaneState{Size: 160} +layout.SplitPane(c, &split, layout.SplitPaneOptions{MinFirst: 120, Collapsible: layout.SplitPaneFirst}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }, ).Fill()` func layoutSplitPaneDemo(c *ui.Context, log func(string)) { type state struct { - split mujica.SplitPaneState + split layout.SplitPaneState vertical bool collapsible string minFirst float64 } s := ui.Local(c.Root(), "008", func() state { - return state{split: mujica.SplitPaneState{Size: 180}, collapsible: "First", minFirst: 120} + return state{split: layout.SplitPaneState{Size: 180}, collapsible: "First", minFirst: 120} }) - sides := map[string]mujica.SplitPaneSide{"Neither": mujica.SplitPaneNeither, "First": mujica.SplitPaneFirst, "Second": mujica.SplitPaneSecond} + sides := map[string]layout.SplitPaneSide{"Neither": layout.SplitPaneNeither, "First": layout.SplitPaneFirst, "Second": layout.SplitPaneSecond} layoutControls(c, func() { layoutCheck(c, &s.vertical, "Vertical", log) layoutChoice(c, &s.collapsible, "Collapsible", []string{"Neither", "First", "Second"}, log) layoutSlider(c, &s.minFirst, "Min first", 40, 200, log) }) s.split.Collapsed = s.split.Collapsed && s.collapsible != "Neither" - k := mujica.Tokens(c) + k := core.Tokens(c) pane := func(title, body string, bg ui.Color) func() { return func() { ui.Column(c).Fill().Padding(12).Gap(6).Background(bg).Children(func() { @@ -413,7 +415,7 @@ func layoutSplitPaneDemo(c *ui.Context, log func(string)) { } ui.Column(c).Margin(16, 0, 0, 0).Width(560).Children(func() { layoutFrame(c, 280, func() { - r := mujica.SplitPane(c, &s.split, mujica.SplitPaneOptions{Vertical: s.vertical, MinFirst: float32(s.minFirst), + r := layout.SplitPane(c, &s.split, layout.SplitPaneOptions{Vertical: s.vertical, MinFirst: float32(s.minFirst), Collapsible: sides[s.collapsible]}, pane("Files", "Drag the divider, or focus it and use the arrows.", k.Surface), pane("Editor", "Enter on the divider folds the collapsible pane.", k.Background)) @@ -429,7 +431,7 @@ func layoutSplitPaneDemo(c *ui.Context, log func(string)) { } const layoutAspectRatioCode = `ui.Box(c).Size(320, 240).Children(func() { - mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: 16.0 / 9}, func() { + layout.AspectRatio(c, layout.AspectRatioOptions{Ratio: 16.0 / 9}, func() { ui.Text(c, "16 : 9") }) })` @@ -446,9 +448,9 @@ func layoutAspectRatioDemo(c *ui.Context, log func(string)) { layoutSlider(c, &s.width, "Parent width", 120, 560, log) layoutSlider(c, &s.height, "Parent height", 80, 320, log) }) - k := mujica.Tokens(c) + k := core.Tokens(c) ui.Box(c).Margin(16, 0, 0, 0).Size(float32(s.width), float32(s.height)).Border(1, k.Border).Children(func() { - mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: ratios[s.ratio]}, func() { + layout.AspectRatio(c, layout.AspectRatioOptions{Ratio: ratios[s.ratio]}, func() { ui.Box(c).Fill().Center().Background(k.Surface).Border(1, k.Ornament).Children(func() { ui.Text(c, s.ratio).TextColor(k.TextMuted) }) @@ -457,8 +459,8 @@ func layoutAspectRatioDemo(c *ui.Context, log func(string)) { } const layoutDividerCode = `ui.Column(c).Padding(16).Gap(12).Children(func() { - mujica.Divider(c, mujica.DividerOptions{Ornament: true}) - mujica.Divider(c, mujica.DividerOptions{Label: "Chapter II"}) + layout.Divider(c, layout.DividerOptions{Ornament: true}) + layout.Divider(c, layout.DividerOptions{Label: "Chapter II"}) })` func layoutDividerDemo(c *ui.Context, log func(string)) { @@ -478,19 +480,19 @@ func layoutDividerDemo(c *ui.Context, log func(string)) { if label == "None" || s.vertical { label = "" } - opts := mujica.DividerOptions{Vertical: s.vertical, Ornament: s.ornament, Label: label} + opts := layout.DividerOptions{Vertical: s.vertical, Ornament: s.ornament, Label: label} ui.Column(c).Margin(16, 0, 0, 0).Width(480).Children(func() { if s.vertical { ui.Row(c).Height(80).Gap(16).Children(func() { ui.Text(c, "Overture") - mujica.Divider(c, opts) + layout.Divider(c, opts) ui.Text(c, "Aria") }) return } ui.Column(c).Gap(16).Children(func() { ui.Text(c, "Overture") - mujica.Divider(c, opts) + layout.Divider(c, opts) ui.Text(c, "Aria") }) }) diff --git a/examples/gallery/pages_layout_test.go b/examples/gallery/pages_layout_test.go index 3e3a569..ef43de6 100644 --- a/examples/gallery/pages_layout_test.go +++ b/examples/gallery/pages_layout_test.go @@ -5,7 +5,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func layoutPageTester(t *testing.T, id string, events *[]string) *ui.Tester { @@ -13,7 +13,7 @@ func layoutPageTester(t *testing.T, id string, events *[]string) *ui.Tester { for _, p := range layoutPages { if p.ID == id { return ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) }) diff --git a/examples/gallery/pages_markets.go b/examples/gallery/pages_markets.go index efe5759..ae9b418 100644 --- a/examples/gallery/pages_markets.go +++ b/examples/gallery/pages_markets.go @@ -2,65 +2,68 @@ package main import ( "fmt" + "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) var marketsPages = []page{ - {ID: "181", Name: "OptionChain", ZhName: "期权链", Category: "Markets", Demo: marketsOptionChainDemo, Code: `var state mujica.OptionChainState -rows := []mujica.OptionStrike{{Strike: 100, Call: mujica.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: mujica.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} + {ID: "181", Name: "OptionChain", ZhName: "期权链", Category: "Markets", Demo: marketsOptionChainDemo, Code: `var state finance.OptionChainState +rows := []finance.OptionStrike{{Strike: 100, Call: finance.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: finance.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, mujica.OptionChainOptions{Spot: 100.4}) + core.Use(c, core.Settings{}) + r := finance.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, finance.OptionChainOptions{Spot: 100.4}) if r.Changed || r.Submitted || r.ExpiryChanged { fmt.Println(state.Expiry, state.Strike, state.Side) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 切换到期日或列表;上下、Home/End 选择行;左右切换认购认沽;Enter 确认。"}, - {ID: "182", Name: "GreeksTable", ZhName: "希腊值表", Category: "Markets", Demo: marketsGreeksTableDemo, Code: `rows := []mujica.GreeksRow{{Label: "AAPL 100 C", Greeks: mujica.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} + {ID: "182", Name: "GreeksTable", ZhName: "希腊值表", Category: "Markets", Demo: marketsGreeksTableDemo, Code: `rows := []finance.GreeksRow{{Label: "AAPL 100 C", Greeks: finance.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.GreeksTable(c, rows, mujica.GreeksTableOptions{Notes: true}) + core.Use(c, core.Settings{}) + finance.GreeksTable(c, rows, finance.GreeksTableOptions{Notes: true}) } ui.Render(view, 960, 440, 1) `, Keys: "展示组件;表头提供说明和悬停提示。"}, - {ID: "183", Name: "PayoffDiagram", ZhName: "到期损益图", Category: "Markets", Demo: marketsPayoffDiagramDemo, Code: `legs := []mujica.OptionLeg{{Kind: mujica.LegCall, Strike: 100, Premium: 3, Quantity: 1}} + {ID: "183", Name: "PayoffDiagram", ZhName: "到期损益图", Category: "Markets", Demo: marketsPayoffDiagramDemo, Code: `legs := []finance.OptionLeg{{Kind: finance.LegCall, Strike: 100, Premium: 3, Quantity: 1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PayoffDiagram(c, legs, mujica.PayoffDiagramOptions{Frame: mujica.ChartFrame{Label: "Call payoff"}, Spot: 103}) + core.Use(c, core.Settings{}) + r := finance.PayoffDiagram(c, legs, finance.PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Call payoff"}, Spot: 103}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦;方向键、Home/End 逐点读数;Escape 清除。"}, - {ID: "184", Name: "Watchlist", ZhName: "自选列表", Category: "Markets", Demo: marketsWatchlistDemo, Code: `var state mujica.WatchlistState -groups := []mujica.WatchGroup{{Name: "Technology", Items: []mujica.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []mujica.WatchItem{{Symbol: "XOM", Price: 117.4}}}} + {ID: "184", Name: "Watchlist", ZhName: "自选列表", Category: "Markets", Demo: marketsWatchlistDemo, Code: `var state finance.WatchlistState +groups := []finance.WatchGroup{{Name: "Technology", Items: []finance.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []finance.WatchItem{{Symbol: "XOM", Price: 117.4}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Watchlist(c, &state, groups, mujica.WatchlistOptions{}) + core.Use(c, core.Settings{}) + r := finance.Watchlist(c, &state, groups, finance.WatchlistOptions{}) if r.Changed || r.Submitted || r.Moved { fmt.Println(state.Selected, r.Rows, r.To) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 切换分组或列菜单;上下、Home/End 选择;Enter 确认;菜单 Space 开关列。"}, - {ID: "185", Name: "SymbolSearch", ZhName: "代码搜索", Category: "Markets", Demo: marketsSymbolSearchDemo, Code: `var state mujica.SymbolSearchState -rows := []mujica.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} + {ID: "185", Name: "SymbolSearch", ZhName: "代码搜索", Category: "Markets", Demo: marketsSymbolSearchDemo, Code: `var state finance.SymbolSearchState +rows := []finance.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolSearch(c, &state, rows, mujica.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) + core.Use(c, core.Settings{}) + r := finance.SymbolSearch(c, &state, rows, finance.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) if r.Changed || r.Picked || r.Retry { fmt.Println(state.Query, state.Market, r.Chosen.Code) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦输入与市场;Down 进入结果;Tab 到达结果按钮,Enter 选择。"}, - {ID: "186", Name: "SymbolBadge", ZhName: "代码徽标", Category: "Markets", Demo: marketsSymbolBadgeDemo, Code: `symbol := mujica.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} + {ID: "186", Name: "SymbolBadge", ZhName: "代码徽标", Category: "Markets", Demo: marketsSymbolBadgeDemo, Code: `symbol := finance.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SymbolBadge(c, symbol, mujica.SymbolBadgeOptions{Clickable: true}) + core.Use(c, core.Settings{}) + r := finance.SymbolBadge(c, symbol, finance.SymbolBadgeOptions{Clickable: true}) if r.Changed() { fmt.Println(symbol.Code) } @@ -68,42 +71,42 @@ view := func(c *ui.Context) { ui.Render(view, 960, 440, 1) `, Keys: "Clickable 开启时 Tab 聚焦,Enter/Space 激活。"}, {ID: "187", Name: "MarketHeatmap", ZhName: "市场热力图", Category: "Markets", Demo: marketsMarketHeatmapDemo, Code: `var path []string -root := mujica.HeatNode{Name: "Market", Children: []mujica.HeatNode{{Name: "Technology", Children: []mujica.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} +root := chart.HeatNode{Name: "Market", Children: []chart.HeatNode{{Name: "Technology", Children: []chart.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketHeatmap(c, &path, root, mujica.MarketHeatmapOptions{Frame: mujica.ChartFrame{Label: "Market"}}) + core.Use(c, core.Settings{}) + r := chart.MarketHeatmap(c, &path, root, chart.MarketHeatmapOptions{Frame: chart.ChartFrame{Label: "Market"}}) if r.Changed() || r.ValueChanged() || r.Retried() { fmt.Println(path, r.Point) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦图;方向键逐块读数;Enter 下钻;Backspace 返回;路径按钮可用 Tab。"}, - {ID: "188", Name: "Screener", ZhName: "选股器", Category: "Markets", Demo: marketsScreenerDemo, Code: `var state mujica.ScreenerState -fields := []mujica.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} -rows := []mujica.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} + {ID: "188", Name: "Screener", ZhName: "选股器", Category: "Markets", Demo: marketsScreenerDemo, Code: `var state finance.ScreenerState +fields := []finance.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} +rows := []finance.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Screener(c, &state, fields, rows, mujica.ScreenerOptions{}) + core.Use(c, core.Settings{}) + r := finance.Screener(c, &state, fields, rows, finance.ScreenerOptions{}) if r.Changed || r.Save || r.Table.Changed() || r.Table.Submitted() || r.Table.SortChanged() || r.Table.Retried() { fmt.Println(state.Conditions) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 到达字段、比较、数值和保存;Select 方向键选择;NumberInput 方向键步进;表格方向键选择。"}, - {ID: "189", Name: "MarketOverview", ZhName: "市场概览", Category: "Markets", Demo: marketsMarketOverviewDemo, Code: `indices := []mujica.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} + {ID: "189", Name: "MarketOverview", ZhName: "市场概览", Category: "Markets", Demo: marketsMarketOverviewDemo, Code: `indices := []finance.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MarketOverview(c, indices, mujica.MarketOverviewOptions{Breadth: mujica.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) + core.Use(c, core.Settings{}) + r := finance.MarketOverview(c, indices, finance.MarketOverviewOptions{Breadth: finance.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) if r.Chosen >= 0 { fmt.Println(indices[r.Chosen].Name) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 到达指数卡片;Enter/Space 选择。"}, - {ID: "190", Name: "IndexCard", ZhName: "指数卡片", Category: "Markets", Demo: marketsIndexCardDemo, Code: `quote := mujica.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} + {ID: "190", Name: "IndexCard", ZhName: "指数卡片", Category: "Markets", Demo: marketsIndexCardDemo, Code: `quote := finance.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.IndexCard(c, quote, mujica.IndexCardOptions{Clickable: true}) + core.Use(c, core.Settings{}) + r := finance.IndexCard(c, quote, finance.IndexCardOptions{Clickable: true}) if r.Changed() { fmt.Println(quote.Name) } @@ -112,46 +115,46 @@ ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦卡片或迷你图;Enter/Space 激活卡片,图表方向键读数。"}, {ID: "191", Name: "MarketStatus", ZhName: "市场状态", Category: "Markets", Demo: marketsMarketStatusDemo, Code: `now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarketStatus(c, mujica.MarketOpen, mujica.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: mujica.MarketAfterHours}) + core.Use(c, core.Settings{}) + finance.MarketStatus(c, finance.MarketOpen, finance.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: finance.MarketAfterHours}) } ui.Render(view, 960, 440, 1) `, Keys: "展示组件。"}, {ID: "192", Name: "TradingSessionClock", ZhName: "交易时段时钟", Category: "Markets", Demo: marketsTradingSessionClockDemo, Code: `now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) -markets := []mujica.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []mujica.TradingSession{{Phase: mujica.MarketOpen, Start: 570, End: 960}}}} +markets := []finance.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []finance.TradingSession{{Phase: finance.MarketOpen, Start: 570, End: 960}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TradingSessionClock(c, markets, mujica.TradingSessionClockOptions{Now: now}) + core.Use(c, core.Settings{}) + finance.TradingSessionClock(c, markets, finance.TradingSessionClockOptions{Now: now}) } ui.Render(view, 960, 440, 1) `, Keys: "展示组件。"}, - {ID: "193", Name: "EconomicCalendar", ZhName: "经济日历", Category: "Markets", Demo: marketsEconomicCalendarDemo, Code: `var state mujica.EconomicCalendarState -rows := []mujica.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} + {ID: "193", Name: "EconomicCalendar", ZhName: "经济日历", Category: "Markets", Demo: marketsEconomicCalendarDemo, Code: `var state finance.EconomicCalendarState +rows := []finance.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EconomicCalendar(c, &state, rows, mujica.EconomicCalendarOptions{}) + core.Use(c, core.Settings{}) + r := finance.EconomicCalendar(c, &state, rows, finance.EconomicCalendarOptions{}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { fmt.Println(state.Table.Selected()) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 到达国家过滤、重要性和表格;方向键选择;Enter 提交行。"}, - {ID: "194", Name: "EarningsCalendar", ZhName: "财报日历", Category: "Markets", Demo: marketsEarningsCalendarDemo, Code: `var state mujica.DataTableState[string] -rows := []mujica.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: mujica.Date{Year: 2026, Month: 10, Day: 30}, Timing: mujica.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} + {ID: "194", Name: "EarningsCalendar", ZhName: "财报日历", Category: "Markets", Demo: marketsEarningsCalendarDemo, Code: `var state data.DataTableState[string] +rows := []finance.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: finance.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EarningsCalendar(c, &state, rows, mujica.EarningsCalendarOptions{}) + core.Use(c, core.Settings{}) + r := finance.EarningsCalendar(c, &state, rows, finance.EarningsCalendarOptions{}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { fmt.Println(state.Selected()) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦表格;方向键及 Home/End 选择;Enter 提交;表头支持键盘排序。"}, - {ID: "195", Name: "NewsFeed", ZhName: "新闻流", Category: "Markets", Demo: marketsNewsFeedDemo, Code: `var state mujica.NewsFeedState -rows := []mujica.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} + {ID: "195", Name: "NewsFeed", ZhName: "新闻流", Category: "Markets", Demo: marketsNewsFeedDemo, Code: `var state finance.NewsFeedState +rows := []finance.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.NewsFeed(c, &state, rows, mujica.NewsFeedOptions{}) + core.Use(c, core.Settings{}) + r := finance.NewsFeed(c, &state, rows, finance.NewsFeedOptions{}) if r.Opened != "" || r.Symbol != "" || r.Retry { fmt.Println(r.Opened, r.Symbol) } @@ -160,26 +163,26 @@ ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦列表或相关代码;上下选择;Enter 打开并标记已读。"}, {ID: "196", Name: "SentimentGauge", ZhName: "情绪仪表", Category: "Markets", Demo: marketsSentimentGaugeDemo, Code: `value := 72.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SentimentGauge(c, value, mujica.SentimentGaugeOptions{Label: "Fear & Greed"}) + core.Use(c, core.Settings{}) + finance.SentimentGauge(c, value, finance.SentimentGaugeOptions{Label: "Fear & Greed"}) } ui.Render(view, 960, 440, 1) `, Keys: "展示组件,数值与文字同时呈现。"}, {ID: "197", Name: "PortfolioSummary", ZhName: "资产总览", Category: "Markets", Demo: marketsPortfolioSummaryDemo, Code: `account := "" -accounts := []mujica.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} +accounts := []finance.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PortfolioSummary(c, &account, accounts, mujica.PortfolioSummaryOptions{}) + core.Use(c, core.Settings{}) + r := finance.PortfolioSummary(c, &account, accounts, finance.PortfolioSummaryOptions{}) if r.Changed() { fmt.Println(account) } } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦账户组;方向键切换账户。"}, - {ID: "198", Name: "AssetAllocationChart", ZhName: "资产配置图", Category: "Markets", Demo: marketsAssetAllocationChartDemo, Code: `items := []mujica.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} + {ID: "198", Name: "AssetAllocationChart", ZhName: "资产配置图", Category: "Markets", Demo: marketsAssetAllocationChartDemo, Code: `items := []finance.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.AssetAllocationChart(c, items, mujica.AssetAllocationChartOptions{Frame: mujica.ChartFrame{Label: "Allocation"}}) + core.Use(c, core.Settings{}) + r := finance.AssetAllocationChart(c, items, finance.AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } @@ -190,8 +193,8 @@ ui.Render(view, 960, 440, 1) benchmark := []float64{100, 105, 108, 112} times := []time.Time{time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PerformanceChart(c, values, mujica.PerformanceChartOptions{Frame: mujica.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) + core.Use(c, core.Settings{}) + r := finance.PerformanceChart(c, values, finance.PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) if r.Changed() || r.Retried() { fmt.Println(r.Point) } @@ -201,12 +204,12 @@ view := func(c *ui.Context) { } ui.Render(view, 960, 440, 1) `, Keys: "Tab 聚焦图和图例;方向键读数;Space 切换序列。"}, - {ID: "200", Name: "CurrencyConverter", ZhName: "货币换算", Category: "Markets", Demo: marketsCurrencyConverterDemo, Code: `state := mujica.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} -currencies := []mujica.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} + {ID: "200", Name: "CurrencyConverter", ZhName: "货币换算", Category: "Markets", Demo: marketsCurrencyConverterDemo, Code: `state := finance.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} +currencies := []finance.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} rate := 149.37 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.CurrencyConverter(c, &state, currencies, mujica.CurrencyConverterOptions{Rate: rate}) + core.Use(c, core.Settings{}) + r := finance.CurrencyConverter(c, &state, currencies, finance.CurrencyConverterOptions{Rate: rate}) if r.PairChanged { rate = 0 fmt.Println(state.From, state.To) @@ -216,18 +219,18 @@ ui.Render(view, 960, 440, 1) `, Keys: "Tab 到达金额、币种和互换;方向键步进、选择;Enter/Space 互换。"}, } -func marketsDataState(c *ui.Context, id string, log func(string)) (mujica.DataStatus, bool, string) { +func marketsDataState(c *ui.Context, id string, log func(string)) (data.DataStatus, bool, string) { state := ui.Local(c.Root(), id+".status", func() int { return 0 }) - if mujica.RadioGroup(c, state, []mujica.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, state, []input.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint("status ", *state)) } switch *state { case 1: - return mujica.DataLoading, false, "" + return data.DataLoading, false, "" case 2: - return mujica.DataReady, true, "" + return data.DataReady, true, "" case 3: - return mujica.DataFailed, false, "Sample failed" + return data.DataFailed, false, "Sample failed" } - return mujica.DataReady, false, "" + return data.DataReady, false, "" } diff --git a/examples/gallery/pages_markets_1.go b/examples/gallery/pages_markets_1.go index 0e0311a..dd6fab4 100644 --- a/examples/gallery/pages_markets_1.go +++ b/examples/gallery/pages_markets_1.go @@ -2,32 +2,37 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "slices" "strings" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func marketsOptionChainDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "181.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "181.state", func() mujica.OptionChainState { return mujica.OptionChainState{} }) - rows := *ui.Local(c.Root(), "markets.OptionChain.rows.pages_markets_1", func() []mujica.OptionStrike { - return []mujica.OptionStrike{{Strike: 100, Call: mujica.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: mujica.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} + state := ui.Local(c.Root(), "181.state", func() finance.OptionChainState { return finance.OptionChainState{} }) + rows := *ui.Local(c.Root(), "markets.OptionChain.rows.pages_markets_1", func() []finance.OptionStrike { + return []finance.OptionStrike{{Strike: 100, Call: finance.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: finance.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} }) precision := ui.Local(c.Root(), "markets.OptionChain.precision", func() float64 { return 2 }) - if mujica.Slider(c, precision, mujica.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { + if input.Slider(c, precision, input.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { log(fmt.Sprint("decimals ", *precision)) } spot := ui.Local(c.Root(), "181.spot", func() float64 { return 100.4 }) - if mujica.Slider(c, spot, mujica.SliderOptions{Min: 95, Max: 105, Step: 0.1, Label: "Spot", ShowValue: true}).Changed() { + if input.Slider(c, spot, input.SliderOptions{Min: 95, Max: 105, Step: 0.1, Label: "Spot", ShowValue: true}).Changed() { log(fmt.Sprint("spot ", *spot)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.OptionChain(c, state, []string{"Oct 17", "Nov 21"}, rows, mujica.OptionChainOptions{Decimals: int(*precision), Spot: *spot}) + r := finance.OptionChain(c, state, []string{"Oct 17", "Nov 21"}, rows, finance.OptionChainOptions{Decimals: int(*precision), Spot: *spot}) if r.Changed || r.Submitted || r.ExpiryChanged { log(fmt.Sprint(state.Expiry, state.Strike, state.Side)) } @@ -36,52 +41,52 @@ func marketsOptionChainDemo(c *ui.Context, log func(string)) { func marketsGreeksTableDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "182.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - rows := *ui.Local(c.Root(), "markets.GreeksTable.rows.pages_markets_1", func() []mujica.GreeksRow { - return []mujica.GreeksRow{{Label: "AAPL 100 C", Greeks: mujica.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} + rows := *ui.Local(c.Root(), "markets.GreeksTable.rows.pages_markets_1", func() []finance.GreeksRow { + return []finance.GreeksRow{{Label: "AAPL 100 C", Greeks: finance.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} }) precision := ui.Local(c.Root(), "markets.GreeksTable.precision", func() float64 { return 2 }) - if mujica.Slider(c, precision, mujica.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { + if input.Slider(c, precision, input.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { log(fmt.Sprint("decimals ", *precision)) } notes := ui.Local(c.Root(), "182.notes", func() bool { return true }) - if mujica.Checkbox(c, notes, "Notes", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, notes, "Notes", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("notes ", *notes)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - mujica.GreeksTable(c, rows, mujica.GreeksTableOptions{Decimals: int(*precision), Notes: *notes}) + finance.GreeksTable(c, rows, finance.GreeksTableOptions{Decimals: int(*precision), Notes: *notes}) }) } func marketsPayoffDiagramDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "183.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - legs := *ui.Local(c.Root(), "markets.PayoffDiagram.legs.pages_markets_1", func() []mujica.OptionLeg { - return []mujica.OptionLeg{{Kind: mujica.LegCall, Strike: 100, Premium: 3, Quantity: 1}} + legs := *ui.Local(c.Root(), "markets.PayoffDiagram.legs.pages_markets_1", func() []finance.OptionLeg { + return []finance.OptionLeg{{Kind: finance.LegCall, Strike: 100, Premium: 3, Quantity: 1}} }) status := ui.Local(c.Root(), "183.status", func() int { return 0 }) - if mujica.RadioGroup(c, status, []mujica.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, status, []input.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint("status ", *status)) } - frame := mujica.ChartFrame{Label: "PayoffDiagram"} + frame := chart.ChartFrame{Label: "PayoffDiagram"} if *status == 1 { - frame.Status = mujica.DataLoading + frame.Status = data.DataLoading } if *status == 3 { - frame.Status = mujica.DataFailed + frame.Status = data.DataFailed frame.Error = "Sample failed" } if *status == 2 { legs = nil } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.PayoffDiagram(c, legs, mujica.PayoffDiagramOptions{Frame: frame, Spot: 103}) + r := finance.PayoffDiagram(c, legs, finance.PayoffDiagramOptions{Frame: frame, Spot: 103}) if r.Changed() || r.Retried() { log(fmt.Sprint(r.Point)) } @@ -90,33 +95,33 @@ func marketsPayoffDiagramDemo(c *ui.Context, log func(string)) { func marketsWatchlistDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "184.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "184.state", func() mujica.WatchlistState { return mujica.WatchlistState{} }) - groups := *ui.Local(c.Root(), "markets.Watchlist.groups.pages_markets_1", func() []mujica.WatchGroup { - return []mujica.WatchGroup{{Name: "Technology", Items: []mujica.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}, {Symbol: "MSFT", Name: "Microsoft", Price: 418.2, Change: -2.5, ChangePercent: -0.6}}}, {Name: "Energy", Items: []mujica.WatchItem{{Symbol: "XOM", Price: 117.4}}}} + state := ui.Local(c.Root(), "184.state", func() finance.WatchlistState { return finance.WatchlistState{} }) + groups := *ui.Local(c.Root(), "markets.Watchlist.groups.pages_markets_1", func() []finance.WatchGroup { + return []finance.WatchGroup{{Name: "Technology", Items: []finance.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}, {Symbol: "MSFT", Name: "Microsoft", Price: 418.2, Change: -2.5, ChangePercent: -0.6}}}, {Name: "Energy", Items: []finance.WatchItem{{Symbol: "XOM", Price: 117.4}}}} }) precision := ui.Local(c.Root(), "markets.Watchlist.precision", func() float64 { return 2 }) - if mujica.Slider(c, precision, mujica.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { + if input.Slider(c, precision, input.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { log(fmt.Sprint("decimals ", *precision)) } - if mujica.Button(c, "Refresh quotes", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Refresh quotes", input.ButtonOptions{Variant: input.Secondary}).Clicked() { groups[0].Items = slices.Clone(groups[0].Items) groups[0].Items[0].Price += 0.1 log("quotes refreshed") } beforeGroup, beforeColumns := state.Group, fmt.Sprint(state.Hidden) ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.Watchlist(c, state, groups, mujica.WatchlistOptions{Decimals: int(*precision)}) + r := finance.Watchlist(c, state, groups, finance.WatchlistOptions{Decimals: int(*precision)}) if r.Changed || r.Submitted || r.Moved { log(fmt.Sprint(state.Selected, r.Rows, r.To)) } if r.Moved { items := groups[state.Group].Items - var moving []mujica.WatchItem - var rest []mujica.WatchItem + var moving []finance.WatchItem + var rest []finance.WatchItem at := r.To for j, item := range items { if slices.Contains(r.Rows, j) { @@ -138,26 +143,26 @@ func marketsWatchlistDemo(c *ui.Context, log func(string)) { func marketsSymbolSearchDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "185.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "185.state", func() mujica.SymbolSearchState { return mujica.SymbolSearchState{} }) - rows := *ui.Local(c.Root(), "markets.SymbolSearch.rows.pages_markets_1", func() []mujica.MarketSymbol { - return []mujica.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} + state := ui.Local(c.Root(), "185.state", func() finance.SymbolSearchState { return finance.SymbolSearchState{} }) + rows := *ui.Local(c.Root(), "markets.SymbolSearch.rows.pages_markets_1", func() []finance.MarketSymbol { + return []finance.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} }) status, empty, err := marketsDataState(c, "185", log) - if empty || status != mujica.DataReady { + if empty || status != data.DataReady { rows = nil } - results := []mujica.MarketSymbol{} + results := []finance.MarketSymbol{} for _, row := range rows { if (state.Market == "" || state.Market == row.Market) && strings.Contains(strings.ToUpper(row.Code+" "+row.Name), strings.ToUpper(state.Query)) { results = append(results, row) } } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.SymbolSearch(c, state, results, mujica.SymbolSearchOptions{Loading: status == mujica.DataLoading, Error: err, Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) + r := finance.SymbolSearch(c, state, results, finance.SymbolSearchOptions{Loading: status == data.DataLoading, Error: err, Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) if r.Changed || r.Picked || r.Retry { log(fmt.Sprint(state.Query, state.Market, r.Chosen.Code)) } diff --git a/examples/gallery/pages_markets_2.go b/examples/gallery/pages_markets_2.go index 0a3efbc..57098fe 100644 --- a/examples/gallery/pages_markets_2.go +++ b/examples/gallery/pages_markets_2.go @@ -2,26 +2,31 @@ package main import ( "fmt" + "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func marketsSymbolBadgeDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "186.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - symbol := *ui.Local(c.Root(), "markets.SymbolBadge.symbol.pages_markets_2", func() mujica.MarketSymbol { - return mujica.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} + symbol := *ui.Local(c.Root(), "markets.SymbolBadge.symbol.pages_markets_2", func() finance.MarketSymbol { + return finance.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} }) kind := ui.Local(c.Root(), "186.kind", func() string { return "Stock" }) - if mujica.RadioGroup(c, kind, []mujica.RadioItem[string]{{Value: "Stock", Label: "Stock"}, {Value: "ETF", Label: "ETF"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, kind, []input.RadioItem[string]{{Value: "Stock", Label: "Stock"}, {Value: "ETF", Label: "ETF"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(*kind) } symbol.Type = *kind ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.SymbolBadge(c, symbol, mujica.SymbolBadgeOptions{Clickable: true}) + r := finance.SymbolBadge(c, symbol, finance.SymbolBadgeOptions{Clickable: true}) if r.Changed() { log(fmt.Sprint(symbol.Code)) } @@ -30,24 +35,24 @@ func marketsSymbolBadgeDemo(c *ui.Context, log func(string)) { func marketsMarketHeatmapDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "187.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } path := ui.Local(c.Root(), "187.path", func() []string { return []string{} }) - root := *ui.Local(c.Root(), "markets.MarketHeatmap.root.pages_markets_2", func() mujica.HeatNode { - return mujica.HeatNode{Name: "Market", Children: []mujica.HeatNode{{Name: "Technology", Children: []mujica.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} + root := *ui.Local(c.Root(), "markets.MarketHeatmap.root.pages_markets_2", func() chart.HeatNode { + return chart.HeatNode{Name: "Market", Children: []chart.HeatNode{{Name: "Technology", Children: []chart.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}}}, {Name: "Energy", Cap: 450, Change: -2.1}}} }) status := ui.Local(c.Root(), "187.status", func() int { return 0 }) - if mujica.RadioGroup(c, status, []mujica.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, status, []input.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint("status ", *status)) } - frame := mujica.ChartFrame{Label: "MarketHeatmap"} + frame := chart.ChartFrame{Label: "MarketHeatmap"} if *status == 1 { - frame.Status = mujica.DataLoading + frame.Status = data.DataLoading } if *status == 3 { - frame.Status = mujica.DataFailed + frame.Status = data.DataFailed frame.Error = "Sample failed" } if *status == 2 { @@ -58,11 +63,11 @@ func marketsMarketHeatmapDemo(c *ui.Context, log func(string)) { } } scale := ui.Local(c.Root(), "187.scale", func() float64 { return 3 }) - if mujica.Slider(c, scale, mujica.SliderOptions{Min: 1, Max: 10, Step: 1, Label: "Color scale", ShowValue: true}).Changed() { + if input.Slider(c, scale, input.SliderOptions{Min: 1, Max: 10, Step: 1, Label: "Color scale", ShowValue: true}).Changed() { log(fmt.Sprint(*scale)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.MarketHeatmap(c, path, root, mujica.MarketHeatmapOptions{Frame: frame, Scale: *scale}) + r := chart.MarketHeatmap(c, path, root, chart.MarketHeatmapOptions{Frame: frame, Scale: *scale}) if r.Changed() || r.ValueChanged() || r.Retried() { log(fmt.Sprint(path, r.Point)) } @@ -71,21 +76,21 @@ func marketsMarketHeatmapDemo(c *ui.Context, log func(string)) { func marketsScreenerDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "188.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "188.state", func() mujica.ScreenerState { return mujica.ScreenerState{} }) - fields := *ui.Local(c.Root(), "markets.Screener.fields.pages_markets_2", func() []mujica.ScreenerField { return []mujica.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} }) - rows := *ui.Local(c.Root(), "markets.Screener.rows.pages_markets_2", func() []mujica.ScreenerRow { - return []mujica.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} + state := ui.Local(c.Root(), "188.state", func() finance.ScreenerState { return finance.ScreenerState{} }) + fields := *ui.Local(c.Root(), "markets.Screener.fields.pages_markets_2", func() []finance.ScreenerField { return []finance.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} }) + rows := *ui.Local(c.Root(), "markets.Screener.rows.pages_markets_2", func() []finance.ScreenerRow { + return []finance.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} }) status, empty, err := marketsDataState(c, "188", log) - if empty || status != mujica.DataReady { + if empty || status != data.DataReady { rows = nil } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.Screener(c, state, fields, rows, mujica.ScreenerOptions{Status: status, Error: err}) + r := finance.Screener(c, state, fields, rows, finance.ScreenerOptions{Status: status, Error: err}) if r.Changed || r.Save || r.Table.Changed() || r.Table.Submitted() || r.Table.SortChanged() || r.Table.Retried() { log(fmt.Sprint(state.Conditions)) } @@ -94,19 +99,19 @@ func marketsScreenerDemo(c *ui.Context, log func(string)) { func marketsMarketOverviewDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "189.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - indices := *ui.Local(c.Root(), "markets.MarketOverview.indices.pages_markets_2", func() []mujica.IndexQuote { - return []mujica.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} + indices := *ui.Local(c.Root(), "markets.MarketOverview.indices.pages_markets_2", func() []finance.IndexQuote { + return []finance.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} }) advancing := ui.Local(c.Root(), "189.advancing", func() float64 { return 2100 }) - if mujica.Slider(c, advancing, mujica.SliderOptions{Min: 0, Max: 4000, Step: 100, Label: "Advancing", ShowValue: true}).Changed() { + if input.Slider(c, advancing, input.SliderOptions{Min: 0, Max: 4000, Step: 100, Label: "Advancing", ShowValue: true}).Changed() { log(fmt.Sprint(*advancing)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.MarketOverview(c, indices, mujica.MarketOverviewOptions{Breadth: mujica.MarketBreadth{Advancing: int(*advancing), Declining: 1400, Unchanged: 300}}) + r := finance.MarketOverview(c, indices, finance.MarketOverviewOptions{Breadth: finance.MarketBreadth{Advancing: int(*advancing), Declining: 1400, Unchanged: 300}}) if r.Chosen >= 0 { log(fmt.Sprint(indices[r.Chosen].Name)) } @@ -115,24 +120,24 @@ func marketsMarketOverviewDemo(c *ui.Context, log func(string)) { func marketsIndexCardDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "190.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - quote := *ui.Local(c.Root(), "markets.IndexCard.quote.pages_markets_2", func() mujica.IndexQuote { - return mujica.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} + quote := *ui.Local(c.Root(), "markets.IndexCard.quote.pages_markets_2", func() finance.IndexQuote { + return finance.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} }) precision := ui.Local(c.Root(), "markets.IndexCard.precision", func() float64 { return 2 }) - if mujica.Slider(c, precision, mujica.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { + if input.Slider(c, precision, input.SliderOptions{Min: 1, Max: 8, Step: 1, Label: "Decimals", ShowValue: true}).Changed() { log(fmt.Sprint("decimals ", *precision)) } price := ui.Local(c.Root(), "190.price", func() float64 { return 5751.07 }) - if mujica.NumberInput(c, price, mujica.NumberInputOptions{Min: 1, Max: 10000, Step: 0.01, Precision: 2, Label: "Price"}).Changed() { + if input.NumberInput(c, price, input.NumberInputOptions{Min: 1, Max: 10000, Step: 0.01, Precision: 2, Label: "Price"}).Changed() { log(fmt.Sprint(*price)) } quote.Price = *price ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.IndexCard(c, quote, mujica.IndexCardOptions{Decimals: int(*precision), Clickable: true}) + r := finance.IndexCard(c, quote, finance.IndexCardOptions{Decimals: int(*precision), Clickable: true}) if r.Changed() { log(fmt.Sprint(quote.Name)) } diff --git a/examples/gallery/pages_markets_3.go b/examples/gallery/pages_markets_3.go index 8bf7b00..6daf009 100644 --- a/examples/gallery/pages_markets_3.go +++ b/examples/gallery/pages_markets_3.go @@ -2,69 +2,74 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func marketsMarketStatusDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "191.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) - phase := ui.Local(c.Root(), "191.phase", func() mujica.MarketPhase { return mujica.MarketOpen }) - if mujica.RadioGroup(c, phase, []mujica.RadioItem[mujica.MarketPhase]{{Value: mujica.MarketClosed, Label: "Closed"}, {Value: mujica.MarketPreMarket, Label: "Pre-market"}, {Value: mujica.MarketOpen, Label: "Open"}, {Value: mujica.MarketAfterHours, Label: "After hours"}, {Value: mujica.MarketHoliday, Label: "Holiday"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + phase := ui.Local(c.Root(), "191.phase", func() finance.MarketPhase { return finance.MarketOpen }) + if input.RadioGroup(c, phase, []input.RadioItem[finance.MarketPhase]{{Value: finance.MarketClosed, Label: "Closed"}, {Value: finance.MarketPreMarket, Label: "Pre-market"}, {Value: finance.MarketOpen, Label: "Open"}, {Value: finance.MarketAfterHours, Label: "After hours"}, {Value: finance.MarketHoliday, Label: "Holiday"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint(*phase)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - mujica.MarketStatus(c, *phase, mujica.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: mujica.MarketAfterHours}) + finance.MarketStatus(c, *phase, finance.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: finance.MarketAfterHours}) }) } func marketsTradingSessionClockDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "192.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) - markets := *ui.Local(c.Root(), "markets.TradingSessionClock.markets.pages_markets_3", func() []mujica.SessionMarket { - return []mujica.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []mujica.TradingSession{{Phase: mujica.MarketOpen, Start: 570, End: 960}}}} + markets := *ui.Local(c.Root(), "markets.TradingSessionClock.markets.pages_markets_3", func() []finance.SessionMarket { + return []finance.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []finance.TradingSession{{Phase: finance.MarketOpen, Start: 570, End: 960}}}} }) holiday := ui.Local(c.Root(), "192.holiday", func() bool { return false }) - if mujica.Checkbox(c, holiday, "Holiday", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, holiday, "Holiday", input.CheckboxOptions{}).Changed() { log(fmt.Sprint(*holiday)) } markets[0].Holiday = *holiday minute := ui.Local(c.Root(), "192.minute", func() float64 { return 870 }) - if mujica.Slider(c, minute, mujica.SliderOptions{Min: 0, Max: 1439, Step: 1, Label: "UTC minute", ShowValue: true}).Changed() { + if input.Slider(c, minute, input.SliderOptions{Min: 0, Max: 1439, Step: 1, Label: "UTC minute", ShowValue: true}).Changed() { log(fmt.Sprint(*minute)) } now = now.Truncate(24 * time.Hour).Add(time.Duration(*minute) * time.Minute) ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - mujica.TradingSessionClock(c, markets, mujica.TradingSessionClockOptions{Now: now}) + finance.TradingSessionClock(c, markets, finance.TradingSessionClockOptions{Now: now}) }) } func marketsEconomicCalendarDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "193.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "193.state", func() mujica.EconomicCalendarState { return mujica.EconomicCalendarState{} }) - rows := *ui.Local(c.Root(), "markets.EconomicCalendar.rows.pages_markets_3", func() []mujica.EconomicEvent { - return []mujica.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} + state := ui.Local(c.Root(), "193.state", func() finance.EconomicCalendarState { return finance.EconomicCalendarState{} }) + rows := *ui.Local(c.Root(), "markets.EconomicCalendar.rows.pages_markets_3", func() []finance.EconomicEvent { + return []finance.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} }) status, empty, err := marketsDataState(c, "193", log) - if empty || status != mujica.DataReady { + if empty || status != data.DataReady { rows = nil } beforeFilter := fmt.Sprint(state.Countries, state.MinImportance) ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.EconomicCalendar(c, state, rows, mujica.EconomicCalendarOptions{Status: status, Error: err}) + r := finance.EconomicCalendar(c, state, rows, finance.EconomicCalendarOptions{Status: status, Error: err}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { log(fmt.Sprint(state.Table.Selected())) } @@ -76,20 +81,20 @@ func marketsEconomicCalendarDemo(c *ui.Context, log func(string)) { func marketsEarningsCalendarDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "194.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "194.state", func() mujica.DataTableState[string] { return mujica.DataTableState[string]{} }) - rows := *ui.Local(c.Root(), "markets.EarningsCalendar.rows.pages_markets_3", func() []mujica.EarningsEvent { - return []mujica.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: mujica.Date{Year: 2026, Month: 10, Day: 30}, Timing: mujica.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} + state := ui.Local(c.Root(), "194.state", func() data.DataTableState[string] { return data.DataTableState[string]{} }) + rows := *ui.Local(c.Root(), "markets.EarningsCalendar.rows.pages_markets_3", func() []finance.EarningsEvent { + return []finance.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: finance.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} }) status, empty, err := marketsDataState(c, "194", log) - if empty || status != mujica.DataReady { + if empty || status != data.DataReady { rows = nil } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.EarningsCalendar(c, state, rows, mujica.EarningsCalendarOptions{Status: status, Error: err}) + r := finance.EarningsCalendar(c, state, rows, finance.EarningsCalendarOptions{Status: status, Error: err}) if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { log(fmt.Sprint(state.Selected())) } @@ -98,20 +103,20 @@ func marketsEarningsCalendarDemo(c *ui.Context, log func(string)) { func marketsNewsFeedDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "195.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "195.state", func() mujica.NewsFeedState { return mujica.NewsFeedState{} }) - rows := *ui.Local(c.Root(), "markets.NewsFeed.rows.pages_markets_3", func() []mujica.NewsItem { - return []mujica.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} + state := ui.Local(c.Root(), "195.state", func() finance.NewsFeedState { return finance.NewsFeedState{} }) + rows := *ui.Local(c.Root(), "markets.NewsFeed.rows.pages_markets_3", func() []finance.NewsItem { + return []finance.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} }) status, empty, err := marketsDataState(c, "195", log) - if empty || status != mujica.DataReady { + if empty || status != data.DataReady { rows = nil } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.NewsFeed(c, state, rows, mujica.NewsFeedOptions{Status: status, Error: err}) + r := finance.NewsFeed(c, state, rows, finance.NewsFeedOptions{Status: status, Error: err}) if r.Opened != "" || r.Symbol != "" || r.Retry || r.ReadChanged { log(fmt.Sprint(r.Opened, r.Symbol)) } diff --git a/examples/gallery/pages_markets_4.go b/examples/gallery/pages_markets_4.go index a29ccfa..04de4ed 100644 --- a/examples/gallery/pages_markets_4.go +++ b/examples/gallery/pages_markets_4.go @@ -2,46 +2,51 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func marketsSentimentGaugeDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "196.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } value := 72.0 level := ui.Local(c.Root(), "196.value", func() float64 { return 72 }) - if mujica.Slider(c, level, mujica.SliderOptions{Min: 0, Max: 100, Step: 1, Label: "Sentiment", ShowValue: true}).Changed() { + if input.Slider(c, level, input.SliderOptions{Min: 0, Max: 100, Step: 1, Label: "Sentiment", ShowValue: true}).Changed() { log(fmt.Sprint(*level)) } value = *level status, empty, err := marketsDataState(c, "196", log) ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - mujica.SentimentGauge(c, value, mujica.SentimentGaugeOptions{Label: "Fear & Greed", Status: status, Empty: empty, Error: err, Retry: func() { log("retry requested") }}) + finance.SentimentGauge(c, value, finance.SentimentGaugeOptions{Label: "Fear & Greed", Status: status, Empty: empty, Error: err, Retry: func() { log("retry requested") }}) }) } func marketsPortfolioSummaryDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "197.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } account := ui.Local(c.Root(), "197.account", func() string { return "" }) - accounts := *ui.Local(c.Root(), "markets.PortfolioSummary.accounts.pages_markets_4", func() []mujica.PortfolioAccount { - return []mujica.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} + accounts := *ui.Local(c.Root(), "markets.PortfolioSummary.accounts.pages_markets_4", func() []finance.PortfolioAccount { + return []finance.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} }) assets := ui.Local(c.Root(), "197.assets", func() float64 { return 120000 }) - if mujica.NumberInput(c, assets, mujica.NumberInputOptions{Min: 0, Max: 1000000, Step: 100, Precision: 2, Label: "Brokerage assets"}).Changed() { + if input.NumberInput(c, assets, input.NumberInputOptions{Min: 0, Max: 1000000, Step: 100, Precision: 2, Label: "Brokerage assets"}).Changed() { log(fmt.Sprint(*assets)) } accounts[0].Total = *assets ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.PortfolioSummary(c, account, accounts, mujica.PortfolioSummaryOptions{}) + r := finance.PortfolioSummary(c, account, accounts, finance.PortfolioSummaryOptions{}) if r.Changed() { log(fmt.Sprint(account)) } @@ -50,34 +55,34 @@ func marketsPortfolioSummaryDemo(c *ui.Context, log func(string)) { func marketsAssetAllocationChartDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "198.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - items := *ui.Local(c.Root(), "markets.AssetAllocationChart.items.pages_markets_4", func() []mujica.AllocationItem { - return []mujica.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} + items := *ui.Local(c.Root(), "markets.AssetAllocationChart.items.pages_markets_4", func() []finance.AllocationItem { + return []finance.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} }) status := ui.Local(c.Root(), "198.status", func() int { return 0 }) - if mujica.RadioGroup(c, status, []mujica.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, status, []input.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint("status ", *status)) } - frame := mujica.ChartFrame{Label: "AssetAllocationChart"} + frame := chart.ChartFrame{Label: "AssetAllocationChart"} if *status == 1 { - frame.Status = mujica.DataLoading + frame.Status = data.DataLoading } if *status == 3 { - frame.Status = mujica.DataFailed + frame.Status = data.DataFailed frame.Error = "Sample failed" } if *status == 2 { items = nil } tolerance := ui.Local(c.Root(), "198.tolerance", func() float64 { return 5 }) - if mujica.Slider(c, tolerance, mujica.SliderOptions{Min: 1, Max: 15, Step: 1, Label: "Drift tolerance", ShowValue: true}).Changed() { + if input.Slider(c, tolerance, input.SliderOptions{Min: 1, Max: 15, Step: 1, Label: "Drift tolerance", ShowValue: true}).Changed() { log(fmt.Sprint(*tolerance)) } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.AssetAllocationChart(c, items, mujica.AssetAllocationChartOptions{Frame: frame, Tolerance: *tolerance}) + r := finance.AssetAllocationChart(c, items, finance.AssetAllocationChartOptions{Frame: frame, Tolerance: *tolerance}) if r.Changed() || r.Retried() { log(fmt.Sprint(r.Point)) } @@ -86,7 +91,7 @@ func marketsAssetAllocationChartDemo(c *ui.Context, log func(string)) { func marketsPerformanceChartDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "199.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } values := *ui.Local(c.Root(), "markets.PerformanceChart.values.pages_markets_4", func() []float64 { return []float64{100, 110, 95, 120} }) @@ -96,15 +101,15 @@ func marketsPerformanceChartDemo(c *ui.Context, log func(string)) { }) status := ui.Local(c.Root(), "199.status", func() int { return 0 }) - if mujica.RadioGroup(c, status, []mujica.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, mujica.RadioGroupOptions{Horizontal: true}).Changed() { + if input.RadioGroup(c, status, []input.RadioItem[int]{{Value: 0, Label: "Ready"}, {Value: 1, Label: "Loading"}, {Value: 2, Label: "Empty"}, {Value: 3, Label: "Error"}}, input.RadioGroupOptions{Horizontal: true}).Changed() { log(fmt.Sprint("status ", *status)) } - frame := mujica.ChartFrame{Label: "PerformanceChart"} + frame := chart.ChartFrame{Label: "PerformanceChart"} if *status == 1 { - frame.Status = mujica.DataLoading + frame.Status = data.DataLoading } if *status == 3 { - frame.Status = mujica.DataFailed + frame.Status = data.DataFailed frame.Error = "Sample failed" } if *status == 2 { @@ -113,7 +118,7 @@ func marketsPerformanceChartDemo(c *ui.Context, log func(string)) { times = nil } ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.PerformanceChart(c, values, mujica.PerformanceChartOptions{Frame: frame, Times: times, Benchmark: benchmark}) + r := finance.PerformanceChart(c, values, finance.PerformanceChartOptions{Frame: frame, Times: times, Benchmark: benchmark}) if r.Changed() || r.Retried() { log(fmt.Sprint(r.Point)) } @@ -125,21 +130,21 @@ func marketsPerformanceChartDemo(c *ui.Context, log func(string)) { func marketsCurrencyConverterDemo(c *ui.Context, log func(string)) { disabled := ui.Local(c.Root(), "200.disabled", func() bool { return false }) - if mujica.Checkbox(c, disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprint("disabled ", *disabled)) } - state := ui.Local(c.Root(), "200.state", func() mujica.CurrencyConverterState { - return mujica.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} + state := ui.Local(c.Root(), "200.state", func() finance.CurrencyConverterState { + return finance.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} }) - currencies := *ui.Local(c.Root(), "markets.CurrencyConverter.currencies.pages_markets_4", func() []mujica.Currency { - return []mujica.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} + currencies := *ui.Local(c.Root(), "markets.CurrencyConverter.currencies.pages_markets_4", func() []finance.Currency { + return []finance.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} }) rate := ui.Local(c.Root(), "200.rate", func() float64 { return 149.37 }) - if mujica.NumberInput(c, rate, mujica.NumberInputOptions{Min: 0, Max: 1000, Step: 0.0001, Precision: 4, Label: "Caller rate"}).Changed() { + if input.NumberInput(c, rate, input.NumberInputOptions{Min: 0, Max: 1000, Step: 0.0001, Precision: 4, Label: "Caller rate"}).Changed() { log(fmt.Sprint("rate ", *rate)) } - if mujica.Button(c, "Load pair rate", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Load pair rate", input.ButtonOptions{Variant: input.Secondary}).Clicked() { *rate = 149.37 if state.From == "JPY" { *rate = 1 / 149.37 @@ -148,7 +153,7 @@ func marketsCurrencyConverterDemo(c *ui.Context, log func(string)) { } beforeAmount := state.Amount ui.Column(c).Disabled(*disabled).MinHeight(0).Height(400).Children(func() { - r := mujica.CurrencyConverter(c, state, currencies, mujica.CurrencyConverterOptions{Rate: *rate, RateFrom: state.From, RateTo: state.To}) + r := finance.CurrencyConverter(c, state, currencies, finance.CurrencyConverterOptions{Rate: *rate, RateFrom: state.From, RateTo: state.To}) if r.PairChanged { *rate = 0 log(fmt.Sprint(state.From, state.To)) diff --git a/examples/gallery/pages_markets_test.go b/examples/gallery/pages_markets_test.go index 2d48730..94103b5 100644 --- a/examples/gallery/pages_markets_test.go +++ b/examples/gallery/pages_markets_test.go @@ -1,11 +1,13 @@ package main import ( - "github.com/egoist/mygo/ui" - mujica "mujicaui" "slices" "strings" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func marketsDemo(t *testing.T, id string) (*ui.Tester, *[]string) { @@ -16,7 +18,7 @@ func marketsDemo(t *testing.T, id string) (*ui.Tester, *[]string) { } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(24).Children(func() { marketsPages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) }, 1000, 800) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/examples/gallery/pages_media.go b/examples/gallery/pages_media.go index dd1d945..6f4a887 100644 --- a/examples/gallery/pages_media.go +++ b/examples/gallery/pages_media.go @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) const mediaCategory = "Media" @@ -61,7 +62,7 @@ var mediaPages = []page{ // mediaSection titles a part of a demo. func mediaSection(c *ui.Context, title string, fn func()) { ui.Column(c).Gap(8).FillWidth().Children(func() { - ui.Text(c, title).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted) fn() }) } @@ -69,7 +70,7 @@ func mediaSection(c *ui.Context, title string, fn func()) { // mediaCheck is a parameter checkbox that logs its changes. func mediaCheck(c *ui.Context, log func(string), v *bool, label string) { before := *v - mujica.Checkbox(c, v, label, mujica.CheckboxOptions{}) + input.Checkbox(c, v, label, input.CheckboxOptions{}) if *v != before { log(fmt.Sprintf("%s = %v", label, *v)) } diff --git a/examples/gallery/pages_media_av.go b/examples/gallery/pages_media_av.go index 394b94a..70b1320 100644 --- a/examples/gallery/pages_media_av.go +++ b/examples/gallery/pages_media_av.go @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/media" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) // mediaClockState simulates the caller's playback: position advances while playing. @@ -55,12 +57,12 @@ func videoPlayerDemo(c *ui.Context, log func(string)) { if s.noFrame { frame = nil } - st := mujica.VideoState{Frame: frame, Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing, - Rate: s.clock.rate, Buffered: []mujica.MediaRange{{Start: 0, End: s.clock.pos + 40*time.Second}}, + st := media.VideoState{Frame: frame, Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing, + Rate: s.clock.rate, Buffered: []media.MediaRange{{Start: 0, End: s.clock.pos + 40*time.Second}}, Subtitle: cues[int(s.clock.pos/(10*time.Second))%3], SubtitlesOn: s.subs, Fullscreen: s.full, PiP: s.pip} mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(640).Children(func() { - mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{ + media.VideoPlayer(c, st, media.VideoPlayerOptions{ OnPlay: func() { s.clock.playing = true; log("play") }, OnPause: func() { s.clock.playing = false; log("pause") }, OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) }, @@ -86,8 +88,8 @@ func videoScrubberDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.disabled, "Disabled") }) }) - opts := mujica.VideoScrubberOptions{Duration: 3 * time.Minute, Disabled: s.disabled, - Buffered: []mujica.MediaRange{{Start: 0, End: 90 * time.Second}, {Start: 120 * time.Second, End: 140 * time.Second}}} + opts := media.VideoScrubberOptions{Duration: 3 * time.Minute, Disabled: s.disabled, + Buffered: []media.MediaRange{{Start: 0, End: 90 * time.Second}, {Start: 120 * time.Second, End: 140 * time.Second}}} if s.preview { opts.Preview = func(at time.Duration) { ui.Image(c, mediaBitmaps[int(at/(time.Minute))%3]).Fit(ui.Cover).Fill() @@ -95,7 +97,7 @@ func videoScrubberDemo(c *ui.Context, log func(string)) { } mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(560).Padding(110, 0, 0, 0).Children(func() { - if mujica.VideoScrubber(c, &s.pos, opts).Changed() { + if media.VideoScrubber(c, &s.pos, opts).Changed() { log("seek " + mediaClockLabel(s.pos)) } }) @@ -112,8 +114,8 @@ func audioPlayerDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(560).Children(func() { - mujica.AudioPlayer(c, mujica.AudioState{Title: "Ave Mujica — Overture", Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing}, - mujica.AudioPlayerOptions{ + media.AudioPlayer(c, media.AudioState{Title: "Ave Mujica — Overture", Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing}, + media.AudioPlayerOptions{ OnPlay: func() { s.clock.playing = true; log("play") }, OnPause: func() { s.clock.playing = false; log("pause") }, OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) }, @@ -136,7 +138,7 @@ var mediaSamples = func() []float32 { func audioWaveformDemo(c *ui.Context, log func(string)) { type state struct { pos time.Duration - sel mujica.MediaRange + sel media.MediaRange status int selecting bool } @@ -144,28 +146,28 @@ func audioWaveformDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() { before := s.status - mujica.SegmentedControl(c, &s.status, []mujica.Segment{{Label: "Ready"}, {Label: "Loading"}, {Label: "Failed"}, {Label: "Empty"}}, mujica.SegmentedControlOptions{Label: "Status"}) + navigation.SegmentedControl(c, &s.status, []navigation.Segment{{Label: "Ready"}, {Label: "Loading"}, {Label: "Failed"}, {Label: "Empty"}}, navigation.SegmentedControlOptions{Label: "Status"}) if s.status != before { log(fmt.Sprintf("status %d", s.status)) } mediaCheck(c, log, &s.selecting, "Selectable") }) }) - opts := mujica.AudioWaveformOptions{Duration: time.Minute, Position: s.pos, OnSeek: func(d time.Duration) { s.pos = d; log("seek " + mediaClockLabel(d)) }} + opts := media.AudioWaveformOptions{Duration: time.Minute, Position: s.pos, OnSeek: func(d time.Duration) { s.pos = d; log("seek " + mediaClockLabel(d)) }} if s.selecting { opts.Selection = &s.sel } - data := mediaSamples + samples := mediaSamples switch s.status { case 1: - opts.Status, data = mujica.DataLoading, nil + opts.Status, samples = data.DataLoading, nil case 2: - opts.Status, opts.Error, data = mujica.DataFailed, "The recording could not be decoded.", nil + opts.Status, opts.Error, samples = data.DataFailed, "The recording could not be decoded.", nil case 3: - data = nil + samples = nil } mediaSection(c, "Live", func() { - if mujica.AudioWaveform(c, data, opts).Changed() { + if media.AudioWaveform(c, samples, opts).Changed() { log(fmt.Sprintf("selection %s – %s", mediaClockLabel(s.sel.Start), mediaClockLabel(s.sel.End))) } }) @@ -186,6 +188,6 @@ func audioSpectrumDemo(c *ui.Context, log func(string)) { c.AnimationFrame() } mediaSection(c, "Live", func() { - mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{BandLabel: func(i int) string { return freqs[i] }}) + media.AudioSpectrum(c, bands, media.AudioSpectrumOptions{BandLabel: func(i int) string { return freqs[i] }}) }) } diff --git a/examples/gallery/pages_media_code.go b/examples/gallery/pages_media_code.go index 7a2528d..f6886ad 100644 --- a/examples/gallery/pages_media_code.go +++ b/examples/gallery/pages_media_code.go @@ -2,20 +2,20 @@ package main // Media page snippets, each its compiled Example's body. const ( - imageCropperCode = `crop := mujica.ImageCrop{} // empty: starts fitted to the image + imageCropperCode = `crop := media.ImageCrop{} // empty: starts fitted to the image view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageCropper(c, examplePhoto, &crop, mujica.ImageCropperOptions{Aspect: 1}).Changed() { + core.Use(c, core.Settings{}) + if media.ImageCropper(c, examplePhoto, &crop, media.ImageCropperOptions{Aspect: 1}).Changed() { _ = crop.Rect // in pixels of the photo turned by crop.Rotation } } ui.Render(view, 400, 300, 1)` imageAnnotatorCode = `photo := ui.NewBitmap(examplePhoto) -var notes []mujica.Annotation +var notes []media.Annotation view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageAnnotator(c, photo, ¬es, mujica.ImageAnnotatorOptions{}).Changed() { - data, _ := mujica.ExportAnnotations(notes) + core.Use(c, core.Settings{}) + if media.ImageAnnotator(c, photo, ¬es, media.ImageAnnotatorOptions{}).Changed() { + data, _ := media.ExportAnnotations(notes) _ = data // save the JSON } } @@ -23,26 +23,26 @@ ui.Render(view, 400, 300, 1)` imageThumbnailCode = `photo := ui.NewBitmap(examplePhoto) selected := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ImageThumbnail(c, photo, mujica.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { + core.Use(c, core.Settings{}) + if media.ImageThumbnail(c, photo, media.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { selected = !selected } } ui.Render(view, 400, 300, 1)` - lightboxCode = `images := []mujica.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} + lightboxCode = `images := []data.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} open, index := false, 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "View photos", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "View photos", input.ButtonOptions{}).Clicked() { open = true } - mujica.Lightbox(c, &open, &index, images) + media.Lightbox(c, &open, &index, images) } ui.Render(view, 400, 300, 1)` - videoPlayerCode = `st := mujica.VideoState{Duration: 4 * time.Minute, Rate: 1} + videoPlayerCode = `st := media.VideoState{Duration: 4 * time.Minute, Rate: 1} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{ + core.Use(c, core.Settings{}) + media.VideoPlayer(c, st, media.VideoPlayerOptions{ OnPlay: func() { st.Playing = true }, // start your decoder OnPause: func() { st.Playing = false }, // pause it OnSeek: func(d time.Duration) { st.Position = d }, @@ -52,26 +52,26 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` videoScrubberCode = `pos := 30 * time.Second view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoScrubber(c, &pos, mujica.VideoScrubberOptions{ + core.Use(c, core.Settings{}) + media.VideoScrubber(c, &pos, media.VideoScrubberOptions{ Duration: 2 * time.Minute, - Buffered: []mujica.MediaRange{{Start: 0, End: time.Minute}}, + Buffered: []media.MediaRange{{Start: 0, End: time.Minute}}, }) } ui.Render(view, 400, 300, 1)` - videoStripCode = `frames := []mujica.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} + videoStripCode = `frames := []media.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} current := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.VideoThumbnailStrip(c, ¤t, frames, mujica.VideoThumbnailStripOptions{}).Changed() { + core.Use(c, core.Settings{}) + if media.VideoThumbnailStrip(c, ¤t, frames, media.VideoThumbnailStripOptions{}).Changed() { _ = frames[current].At // seek the player there } } ui.Render(view, 400, 300, 1)` - audioPlayerCode = `st := mujica.AudioState{Title: "Overture", Duration: 3 * time.Minute} + audioPlayerCode = `st := media.AudioState{Title: "Overture", Duration: 3 * time.Minute} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioPlayer(c, st, mujica.AudioPlayerOptions{ + core.Use(c, core.Settings{}) + media.AudioPlayer(c, st, media.AudioPlayerOptions{ OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, OnSeek: func(d time.Duration) { st.Position = d }, @@ -79,10 +79,10 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)` audioWaveformCode = `samples := make([]float32, 48000) // from your decoder, -1 to 1 -pos, sel := time.Duration(0), mujica.MediaRange{} +pos, sel := time.Duration(0), media.MediaRange{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioWaveform(c, samples, mujica.AudioWaveformOptions{ + core.Use(c, core.Settings{}) + media.AudioWaveform(c, samples, media.AudioWaveformOptions{ Duration: time.Second, Position: pos, Selection: &sel, OnSeek: func(d time.Duration) { pos = d }, }) @@ -90,89 +90,89 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)` audioSpectrumCode = `bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate() view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{}) + core.Use(c, core.Settings{}) + media.AudioSpectrum(c, bands, media.AudioSpectrumOptions{}) } ui.Render(view, 400, 300, 1)` volumeCode = `volume, muted := 0.8, false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeControl(c, &volume, &muted, mujica.VolumeControlOptions{}) + core.Use(c, core.Settings{}) + media.VolumeControl(c, &volume, &muted, media.VolumeControlOptions{}) } ui.Render(view, 400, 300, 1)` speedCode = `rate := 1.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PlaybackSpeedControl(c, &rate, mujica.PlaybackSpeedControlOptions{}) + core.Use(c, core.Settings{}) + media.PlaybackSpeedControl(c, &rate, media.PlaybackSpeedControlOptions{}) } ui.Render(view, 400, 300, 1)` - mediaControlsCode = `st := mujica.MediaControlsState{} + mediaControlsCode = `st := media.MediaControlsState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MediaControls(c, st, mujica.MediaControlsOptions{ + core.Use(c, core.Settings{}) + media.MediaControls(c, st, media.MediaControlsOptions{ OnPrevious: func() {}, OnNext: func() {}, OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, - OnLoop: func(m mujica.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, + OnLoop: func(m media.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, }) } ui.Render(view, 400, 300, 1)` - playlistCode = `items := []mujica.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} + playlistCode = `items := []media.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} current := "a" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.Playlist(c, &items, mujica.PlaylistOptions{Current: current}); r.Played != "" { + core.Use(c, core.Settings{}) + if r := media.Playlist(c, &items, media.PlaylistOptions{Current: current}); r.Played != "" { current = r.Played } } ui.Render(view, 400, 300, 1)` - subtitleCode = `cues := []mujica.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} + subtitleCode = `cues := []media.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SubtitleEditor(c, &cues, mujica.SubtitleEditorOptions{}) - if len(mujica.CheckSubtitles(cues)) > 0 { + core.Use(c, core.Settings{}) + media.SubtitleEditor(c, &cues, media.SubtitleEditorOptions{}) + if len(media.CheckSubtitles(cues)) > 0 { _ = cues // block saving until the issues are fixed } } ui.Render(view, 400, 300, 1)` - recorderCode = `st := mujica.RecorderState{} + recorderCode = `st := media.RecorderState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScreenRecorderControls(c, st, mujica.ScreenRecorderControlsOptions{ - OnStart: func() { st.Status = mujica.RecorderRecording }, - OnPause: func() { st.Status = mujica.RecorderPaused }, - OnResume: func() { st.Status = mujica.RecorderRecording }, - OnStop: func() { st = mujica.RecorderState{} }, + core.Use(c, core.Settings{}) + media.ScreenRecorderControls(c, st, media.ScreenRecorderControlsOptions{ + OnStart: func() { st.Status = media.RecorderRecording }, + OnPause: func() { st.Status = media.RecorderPaused }, + OnResume: func() { st.Status = media.RecorderRecording }, + OnStop: func() { st = media.RecorderState{} }, }) } ui.Render(view, 400, 300, 1)` - cameraCode = `frame := mujica.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame + cameraCode = `frame := media.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame mirror := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CameraPreview(c, frame, &mirror, mujica.CameraPreviewOptions{}) + core.Use(c, core.Settings{}) + media.CameraPreview(c, frame, &mirror, media.CameraPreviewOptions{}) } ui.Render(view, 400, 300, 1)` micCode = `level := float32(0.4) // the input's peak, 0 to 1 of full scale view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) + core.Use(c, core.Settings{}) + media.MicLevelMeter(c, level, media.MicLevelMeterOptions{}) } ui.Render(view, 400, 300, 1)` - deviceCode = `devices := mujica.MediaDevices{ - Microphones: []mujica.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, - Speakers: []mujica.MediaDevice{{ID: "out-1", Name: "Headphones"}}, + deviceCode = `devices := media.MediaDevices{ + Microphones: []media.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, + Speakers: []media.MediaDevice{{ID: "out-1", Name: "Headphones"}}, } -sel := mujica.DeviceSelection{Microphone: "mic-1"} +sel := media.DeviceSelection{Microphone: "mic-1"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DeviceSelector(c, &sel, devices, mujica.DeviceSelectorOptions{}) + core.Use(c, core.Settings{}) + media.DeviceSelector(c, &sel, devices, media.DeviceSelectorOptions{}) } ui.Render(view, 400, 300, 1)` compareCode = `before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto) split := 0.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ImageCompare(c, before, after, &split, mujica.ImageCompareOptions{}) + core.Use(c, core.Settings{}) + media.ImageCompare(c, before, after, &split, media.ImageCompareOptions{}) } ui.Render(view, 400, 300, 1)` ) diff --git a/examples/gallery/pages_media_devices.go b/examples/gallery/pages_media_devices.go index 19527d7..a1182c9 100644 --- a/examples/gallery/pages_media_devices.go +++ b/examples/gallery/pages_media_devices.go @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/media" ) func volumeDemo(c *ui.Context, log func(string)) { @@ -18,7 +19,7 @@ func volumeDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "351", func() state { return state{volume: 0.7} }) mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { - if mujica.VolumeControl(c, &s.volume, &s.muted, mujica.VolumeControlOptions{Disabled: s.disabled}).Changed() { + if media.VolumeControl(c, &s.volume, &s.muted, media.VolumeControlOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("volume %.0f%%, muted %v", s.volume*100, s.muted)) } }) @@ -32,7 +33,7 @@ func speedDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "352", func() state { return state{rate: 1} }) mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { - if mujica.PlaybackSpeedControl(c, &s.rate, mujica.PlaybackSpeedControlOptions{Disabled: s.disabled}).Changed() { + if media.PlaybackSpeedControl(c, &s.rate, media.PlaybackSpeedControlOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("rate %v×", s.rate)) } }) @@ -40,7 +41,7 @@ func speedDemo(c *ui.Context, log func(string)) { func mediaControlsDemo(c *ui.Context, log func(string)) { type state struct { - st mujica.MediaControlsState + st media.MediaControlsState track int disabled bool } @@ -50,12 +51,12 @@ func mediaControlsDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { ui.Text(c, fmt.Sprintf("Now: %s (%d of %d)", tracks[s.track], s.track+1, len(tracks))) - mujica.MediaControls(c, s.st, mujica.MediaControlsOptions{ + media.MediaControls(c, s.st, media.MediaControlsOptions{ OnPrevious: func() { s.track--; log("previous") }, OnNext: func() { s.track++; log("next") }, OnPlay: func() { s.st.Playing = true; log("play") }, OnPause: func() { s.st.Playing = false; log("pause") }, - OnLoop: func(m mujica.LoopMode) { s.st.Loop = m; log(fmt.Sprintf("loop mode %d", m)) }, + OnLoop: func(m media.LoopMode) { s.st.Loop = m; log(fmt.Sprintf("loop mode %d", m)) }, OnShuffle: func(on bool) { s.st.Shuffle = on; log(fmt.Sprintf("shuffle %v", on)) }, Disabled: s.disabled, }) @@ -64,31 +65,31 @@ func mediaControlsDemo(c *ui.Context, log func(string)) { func playlistDemo(c *ui.Context, log func(string)) { type state struct { - items []mujica.PlaylistItem + items []media.PlaylistItem current string disabled bool } s := ui.Local(c.Root(), "354", func() state { titles := []string{"Ave Mujica", "Imprisoned XII", "KiLLKiSS", "Symbol I: △", "Choir 'S' Choir", "Georgette Me, Georgette You"} - items := make([]mujica.PlaylistItem, len(titles)) + items := make([]media.PlaylistItem, len(titles)) for i, t := range titles { - items[i] = mujica.PlaylistItem{ID: fmt.Sprint("t", i), Title: t, Subtitle: fmt.Sprintf("Act %d", i+1), Duration: time.Duration(200+i*17) * time.Second} + items[i] = media.PlaylistItem{ID: fmt.Sprint("t", i), Title: t, Subtitle: fmt.Sprintf("Act %d", i+1), Duration: time.Duration(200+i*17) * time.Second} } return state{items: items, current: "t1"} }) mediaSection(c, "Parameters", func() { ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() { mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Add track", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Add track", input.ButtonOptions{Variant: input.Secondary}).Clicked() { id := fmt.Sprint("t", len(s.items)+int(time.Now().UnixNano()%1000000)) - s.items = append(s.items, mujica.PlaylistItem{ID: id, Title: "Encore " + id, Duration: 3 * time.Minute}) + s.items = append(s.items, media.PlaylistItem{ID: id, Title: "Encore " + id, Duration: 3 * time.Minute}) log("added " + id) } }) }) mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(520).Children(func() { - r := mujica.Playlist(c, &s.items, mujica.PlaylistOptions{Current: s.current, Disabled: s.disabled}) + r := media.Playlist(c, &s.items, media.PlaylistOptions{Current: s.current, Disabled: s.disabled}) switch { case r.Played != "": s.current = r.Played @@ -104,11 +105,11 @@ func playlistDemo(c *ui.Context, log func(string)) { func subtitleDemo(c *ui.Context, log func(string)) { type state struct { - cues []mujica.SubtitleCue + cues []media.SubtitleCue disabled bool } s := ui.Local(c.Root(), "355", func() state { - return state{cues: []mujica.SubtitleCue{ + return state{cues: []media.SubtitleCue{ {Start: 0, End: 2500 * time.Millisecond, Text: "The curtain rises."}, {Start: 2 * time.Second, End: 5 * time.Second, Text: "Welcome to the masquerade."}, {Start: 6 * time.Second, End: 9 * time.Second, Text: "Ave Mujica."}, @@ -117,8 +118,8 @@ func subtitleDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(720).Children(func() { - if mujica.SubtitleEditor(c, &s.cues, mujica.SubtitleEditorOptions{Height: 260, Disabled: s.disabled}).Changed() { - log(fmt.Sprintf("%d cues, %d issues", len(s.cues), len(mujica.CheckSubtitles(s.cues)))) + if media.SubtitleEditor(c, &s.cues, media.SubtitleEditorOptions{Height: 260, Disabled: s.disabled}).Changed() { + log(fmt.Sprintf("%d cues, %d issues", len(s.cues), len(media.CheckSubtitles(s.cues)))) } }) }) @@ -126,27 +127,27 @@ func subtitleDemo(c *ui.Context, log func(string)) { func recorderDemo(c *ui.Context, log func(string)) { type state struct { - st mujica.RecorderState + st media.RecorderState since time.Time disabled bool accumulated time.Duration } s := ui.Local(c.Root(), "356", func() state { return state{} }) - if s.st.Status == mujica.RecorderRecording { + if s.st.Status == media.RecorderRecording { s.st.Elapsed = s.accumulated + c.Now().Sub(s.since) // The clock changes each second; ask for that frame only. c.After(time.Second - s.st.Elapsed%time.Second) } mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { - mujica.ScreenRecorderControls(c, s.st, mujica.ScreenRecorderControlsOptions{ + media.ScreenRecorderControls(c, s.st, media.ScreenRecorderControlsOptions{ OnStart: func() { - s.st, s.since, s.accumulated = mujica.RecorderState{Status: mujica.RecorderRecording}, c.Now(), 0 + s.st, s.since, s.accumulated = media.RecorderState{Status: media.RecorderRecording}, c.Now(), 0 log("start") }, - OnPause: func() { s.st.Status, s.accumulated = mujica.RecorderPaused, s.st.Elapsed; log("pause") }, - OnResume: func() { s.st.Status, s.since = mujica.RecorderRecording, c.Now(); log("resume") }, - OnStop: func() { log("stop at " + mediaClockLabel(s.st.Elapsed)); s.st = mujica.RecorderState{} }, + OnPause: func() { s.st.Status, s.accumulated = media.RecorderPaused, s.st.Elapsed; log("pause") }, + OnResume: func() { s.st.Status, s.since = media.RecorderRecording, c.Now(); log("resume") }, + OnStop: func() { log("stop at " + mediaClockLabel(s.st.Elapsed)); s.st = media.RecorderState{} }, Disabled: s.disabled, }) }) @@ -163,13 +164,13 @@ func cameraDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.disabled, "Disabled") }) }) - frame := mujica.CameraFrame{} + frame := media.CameraFrame{} if s.signal { - frame = mujica.CameraFrame{Image: mediaPhotos[2], Seq: 1} + frame = media.CameraFrame{Image: mediaPhotos[2], Seq: 1} } mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(480).Children(func() { - if mujica.CameraPreview(c, frame, &s.mirror, mujica.CameraPreviewOptions{Disabled: s.disabled}).Changed() { + if media.CameraPreview(c, frame, &s.mirror, media.CameraPreviewOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("mirror %v", s.mirror)) } }) @@ -187,20 +188,20 @@ func micDemo(c *ui.Context, log func(string)) { c.AnimationFrame() } mediaSection(c, "Live", func() { - ui.Column(c).MaxWidth(420).Children(func() { mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) }) + ui.Column(c).MaxWidth(420).Children(func() { media.MicLevelMeter(c, level, media.MicLevelMeterOptions{}) }) }) } func deviceDemo(c *ui.Context, log func(string)) { type state struct { - sel mujica.DeviceSelection + sel media.DeviceSelection noCamera, disabled bool } - s := ui.Local(c.Root(), "359", func() state { return state{sel: mujica.DeviceSelection{Microphone: "m1", Speaker: "s2"}} }) - devices := mujica.MediaDevices{ - Microphones: []mujica.MediaDevice{{ID: "m1", Name: "Built-in Microphone"}, {ID: "m2", Name: "Studio Condenser"}}, - Cameras: []mujica.MediaDevice{{ID: "c1", Name: "FaceTime HD Camera"}}, - Speakers: []mujica.MediaDevice{{ID: "s1", Name: "Built-in Speakers"}, {ID: "s2", Name: "Headphones"}}, + s := ui.Local(c.Root(), "359", func() state { return state{sel: media.DeviceSelection{Microphone: "m1", Speaker: "s2"}} }) + devices := media.MediaDevices{ + Microphones: []media.MediaDevice{{ID: "m1", Name: "Built-in Microphone"}, {ID: "m2", Name: "Studio Condenser"}}, + Cameras: []media.MediaDevice{{ID: "c1", Name: "FaceTime HD Camera"}}, + Speakers: []media.MediaDevice{{ID: "s1", Name: "Built-in Speakers"}, {ID: "s2", Name: "Headphones"}}, } mediaSection(c, "Parameters", func() { ui.Row(c).Gap(16).Children(func() { @@ -213,11 +214,11 @@ func deviceDemo(c *ui.Context, log func(string)) { }) }) if s.noCamera { - devices.Cameras = []mujica.MediaDevice{} + devices.Cameras = []media.MediaDevice{} } mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(480).Children(func() { - if mujica.DeviceSelector(c, &s.sel, devices, mujica.DeviceSelectorOptions{Disabled: s.disabled}).Changed() { + if media.DeviceSelector(c, &s.sel, devices, media.DeviceSelectorOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("microphone %q, camera %q, speaker %q", s.sel.Microphone, s.sel.Camera, s.sel.Speaker)) } }) diff --git a/examples/gallery/pages_media_image.go b/examples/gallery/pages_media_image.go index f62ff53..597726f 100644 --- a/examples/gallery/pages_media_image.go +++ b/examples/gallery/pages_media_image.go @@ -8,12 +8,15 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/media" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) func imageCropperDemo(c *ui.Context, log func(string)) { type state struct { - crop mujica.ImageCrop + crop media.ImageCrop aspect int disabled bool } @@ -22,7 +25,7 @@ func imageCropperDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() { before := s.aspect - mujica.SegmentedControl(c, &s.aspect, []mujica.Segment{{Label: "Free"}, {Label: "1:1"}, {Label: "16:9"}, {Label: "4:3"}}, mujica.SegmentedControlOptions{Label: "Aspect"}) + navigation.SegmentedControl(c, &s.aspect, []navigation.Segment{{Label: "Free"}, {Label: "1:1"}, {Label: "16:9"}, {Label: "4:3"}}, navigation.SegmentedControlOptions{Label: "Aspect"}) if s.aspect != before { s.crop.Rect = image.Rectangle{} // refit to the new ratio log(fmt.Sprintf("aspect = %v", aspects[s.aspect])) @@ -31,7 +34,7 @@ func imageCropperDemo(c *ui.Context, log func(string)) { }) }) mediaSection(c, "Live", func() { - if mujica.ImageCropper(c, mediaPhotos[0], &s.crop, mujica.ImageCropperOptions{Aspect: aspects[s.aspect], Disabled: s.disabled}).Changed() { + if media.ImageCropper(c, mediaPhotos[0], &s.crop, media.ImageCropperOptions{Aspect: aspects[s.aspect], Disabled: s.disabled}).Changed() { log(fmt.Sprintf("crop %v rotated %d°", s.crop.Rect, s.crop.Rotation)) } }) @@ -39,17 +42,17 @@ func imageCropperDemo(c *ui.Context, log func(string)) { func imageAnnotatorDemo(c *ui.Context, log func(string)) { type state struct { - notes []mujica.Annotation + notes []media.Annotation disabled bool } s := ui.Local(c.Root(), "342", func() state { - return state{notes: []mujica.Annotation{{Kind: mujica.AnnotateRect, From: image.Pt(150, 60), To: image.Pt(330, 230)}}} + return state{notes: []media.Annotation{{Kind: media.AnnotateRect, From: image.Pt(150, 60), To: image.Pt(330, 230)}}} }) mediaSection(c, "Parameters", func() { ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() { mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Export JSON", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { - data, err := mujica.ExportAnnotations(s.notes) + if input.Button(c, "Export JSON", input.ButtonOptions{Variant: input.Secondary}).Clicked() { + data, err := media.ExportAnnotations(s.notes) if err != nil { log("export failed: " + err.Error()) } else { @@ -60,7 +63,7 @@ func imageAnnotatorDemo(c *ui.Context, log func(string)) { }) }) mediaSection(c, "Live", func() { - if mujica.ImageAnnotator(c, mediaBitmaps[1], &s.notes, mujica.ImageAnnotatorOptions{Disabled: s.disabled}).Changed() { + if media.ImageAnnotator(c, mediaBitmaps[1], &s.notes, media.ImageAnnotatorOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("annotations: %d", len(s.notes))) } }) @@ -86,9 +89,9 @@ func imageThumbnailDemo(c *ui.Context, log func(string)) { if s.failed && i == 2 { err = errors.New("the file could not be read") } - o := mujica.ImageThumbnailOptions{Alt: fmt.Sprintf("Scene %d", i+1), Aspect: 4.0 / 3, Width: 120, + o := media.ImageThumbnailOptions{Alt: fmt.Sprintf("Scene %d", i+1), Aspect: 4.0 / 3, Width: 120, Selected: s.selected == i, Loading: s.loading && i == 1, Err: err, Disabled: s.disabled} - if mujica.ImageThumbnail(c, b, o).Changed() { + if media.ImageThumbnail(c, b, o).Changed() { s.selected = i log(fmt.Sprintf("selected scene %d", i+1)) } @@ -103,7 +106,7 @@ func lightboxDemo(c *ui.Context, log func(string)) { index int } s := ui.Local(c.Root(), "344", func() state { return state{} }) - images := []mujica.ViewerImage{ + images := []data.ViewerImage{ {Source: mediaBitmaps[0], Alt: "The grand hall at dusk"}, {Source: mediaBitmaps[1], Alt: "The ivory gallery"}, {Source: mediaBitmaps[2], Alt: "The blue chapel"}, @@ -111,14 +114,14 @@ func lightboxDemo(c *ui.Context, log func(string)) { mediaSection(c, "Live", func() { ui.Row(c).Gap(12).Children(func() { for i, im := range images { - if mujica.ImageThumbnail(c, im.Source, mujica.ImageThumbnailOptions{Alt: im.Alt, Aspect: 4.0 / 3}).Changed() { + if media.ImageThumbnail(c, im.Source, media.ImageThumbnailOptions{Alt: im.Alt, Aspect: 4.0 / 3}).Changed() { s.open, s.index = true, i log(fmt.Sprintf("open at %d", i+1)) } } }) was := s.open - if mujica.Lightbox(c, &s.open, &s.index, images).Changed() { + if media.Lightbox(c, &s.open, &s.index, images).Changed() { log(fmt.Sprintf("image %d", s.index+1)) } if was && !s.open { @@ -133,13 +136,13 @@ func videoStripDemo(c *ui.Context, log func(string)) { disabled bool } s := ui.Local(c.Root(), "347", func() state { return state{} }) - frames := make([]mujica.StripFrame, 240) + frames := make([]media.StripFrame, 240) for i := range frames { - frames[i] = mujica.StripFrame{Image: mediaBitmaps[i%3], At: time.Duration(i) * 5 * time.Second} + frames[i] = media.StripFrame{Image: mediaBitmaps[i%3], At: time.Duration(i) * 5 * time.Second} } mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { - if mujica.VideoThumbnailStrip(c, &s.current, frames, mujica.VideoThumbnailStripOptions{Disabled: s.disabled}).Changed() { + if media.VideoThumbnailStrip(c, &s.current, frames, media.VideoThumbnailStripOptions{Disabled: s.disabled}).Changed() { log("jump to " + mediaClockLabel(frames[s.current].At)) } }) @@ -154,7 +157,7 @@ func compareDemo(c *ui.Context, log func(string)) { mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") }) mediaSection(c, "Live", func() { ui.Column(c).MaxWidth(560).Children(func() { - if mujica.ImageCompare(c, mediaBitmaps[0], mediaBitmaps[1], &s.split, mujica.ImageCompareOptions{Disabled: s.disabled}).Changed() { + if media.ImageCompare(c, mediaBitmaps[0], mediaBitmaps[1], &s.split, media.ImageCompareOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("split %.0f%%", s.split*100)) } }) diff --git a/examples/gallery/pages_messaging.go b/examples/gallery/pages_messaging.go index c4c902a..9bbed3e 100644 --- a/examples/gallery/pages_messaging.go +++ b/examples/gallery/pages_messaging.go @@ -4,85 +4,85 @@ const messagingCategory = "Messaging & Mail" var messagingPages = []page{ {ID: "401", Name: "ChannelList", ZhName: "频道列表", Category: messagingCategory, Demo: channelListDemo, - Keys: "Tab 进入;上下方向键切换行;Enter / Space 选择或折叠;分组标题上的左、右方向键折叠、展开。", Code: `groups := []mujica.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []mujica.Channel{ + Keys: "Tab 进入;上下方向键切换行;Enter / Space 选择或折叠;分组标题上的左、右方向键折叠、展开。", Code: `groups := []messaging.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []messaging.Channel{ {ID: "general", Name: "general", Unread: 3}, {ID: "rehearsal", Name: "rehearsal", Pinned: true}, }}} selected := "general" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.ChannelList(c, &selected, groups, mujica.ChannelListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.ChannelList(c, &selected, groups, messaging.ChannelListOptions{}).Changed() { _ = selected // show that channel } } ui.Render(view, 400, 300, 1)`}, {ID: "402", Name: "ChatMessage", ZhName: "聊天消息", Category: messagingCategory, Demo: chatMessageDemo, - Keys: "可选文字支持选择复制;插槽控件按 Tab 访问。", Code: `prev := mujica.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} -m := mujica.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} + Keys: "可选文字支持选择复制;插槽控件按 Tab 访问。", Code: `prev := messaging.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} +m := messaging.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChatMessage(c, prev, mujica.ChatMessageOptions{}) - mujica.ChatMessage(c, m, mujica.ChatMessageOptions{ - Continued: mujica.ChatContinues(prev, m), - Reactions: func() { mujica.Tag(c, "👍 2", mujica.TagOptions{}) }, + core.Use(c, core.Settings{}) + messaging.ChatMessage(c, prev, messaging.ChatMessageOptions{}) + messaging.ChatMessage(c, m, messaging.ChatMessageOptions{ + Continued: messaging.ChatContinues(prev, m), + Reactions: func() { display.Tag(c, "👍 2", display.TagOptions{}) }, }) } ui.Render(view, 400, 300, 1)`}, {ID: "403", Name: "UnreadDivider", ZhName: "未读分隔线", Category: messagingCategory, Demo: unreadDividerDemo, Keys: "展示组件保持自然焦点序列。", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UnreadDivider(c, 3) + core.Use(c, core.Settings{}) + messaging.UnreadDivider(c, 3) } ui.Render(view, 400, 300, 1)`}, {ID: "404", Name: "ReadReceipt", ZhName: "已读回执", Category: messagingCategory, Demo: readReceiptDemo, - Keys: "展示组件保持自然焦点序列。", Code: `readers := []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} + Keys: "展示组件保持自然焦点序列。", Code: `readers := []display.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReadReceipt(c, mujica.ReceiptRead, mujica.ReadReceiptOptions{Readers: readers}) + core.Use(c, core.Settings{}) + messaging.ReadReceipt(c, messaging.ReceiptRead, messaging.ReadReceiptOptions{Readers: readers}) } ui.Render(view, 400, 300, 1)`}, {ID: "405", Name: "MemberList", ZhName: "成员列表", Category: messagingCategory, Demo: memberListDemo, - Keys: "Tab 聚焦列表;上下方向键选择;Home / End 首尾;Enter 或双击打开成员。", Code: `var s mujica.MemberListState -members := []mujica.Member{ - {ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"}, - {ID: "2", Name: "Umiri", Presence: mujica.PresenceOffline}, + Keys: "Tab 聚焦列表;上下方向键选择;Home / End 首尾;Enter 或双击打开成员。", Code: `var s messaging.MemberListState +members := []messaging.Member{ + {ID: "1", Name: "Sakiko", Presence: messaging.PresenceOnline, Role: "Leader"}, + {ID: "2", Name: "Umiri", Presence: messaging.PresenceOffline}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MemberList(c, &s, members, mujica.MemberListOptions{}); r.Opened != "" { + core.Use(c, core.Settings{}) + if r := messaging.MemberList(c, &s, members, messaging.MemberListOptions{}); r.Opened != "" { _ = r.Opened // show that member's profile } } ui.Render(view, 400, 300, 1)`}, {ID: "406", Name: "UserProfileCard", ZhName: "用户资料卡", Category: messagingCategory, Demo: userProfileCardDemo, - Keys: "Tab 访问操作插槽;Enter / Space 激活按钮。", Code: `profile := mujica.UserProfile{Name: "Sakiko Togawa", Presence: mujica.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} + Keys: "Tab 访问操作插槽;Enter / Space 激活按钮。", Code: `profile := messaging.UserProfile{Name: "Sakiko Togawa", Presence: messaging.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UserProfileCard(c, profile, mujica.UserProfileCardOptions{Actions: func() { - mujica.Button(c, "Message", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + messaging.UserProfileCard(c, profile, messaging.UserProfileCardOptions{Actions: func() { + input.Button(c, "Message", input.ButtonOptions{}) }}) } ui.Render(view, 400, 300, 1)`}, {ID: "407", Name: "OnlineStatus", ZhName: "在线状态", Category: messagingCategory, Demo: onlineStatusDemo, Keys: "展示组件保持自然焦点序列。", Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OnlineStatus(c, mujica.PresenceAway, mujica.OnlineStatusOptions{}) + core.Use(c, core.Settings{}) + messaging.OnlineStatus(c, messaging.PresenceAway, messaging.OnlineStatusOptions{}) } ui.Render(view, 400, 300, 1)`}, {ID: "408", Name: "StatusSetter", ZhName: "状态设置", Category: messagingCategory, Demo: statusSetterDemo, - Keys: "Tab 访问表情、文字、期限和清除按钮;选择器用方向键移动、Enter 确认、Escape 关闭。", Code: `status := mujica.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} + Keys: "Tab 访问表情、文字、期限和清除按钮;选择器用方向键移动、Enter 确认、Escape 关闭。", Code: `status := messaging.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSetter(c, &status, mujica.StatusSetterOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.StatusSetter(c, &status, messaging.StatusSetterOptions{}).Changed() { _ = status // publish it; clear it once status.Until passes } } ui.Render(view, 400, 300, 1)`}, {ID: "409", Name: "CallControls", ZhName: "通话控制", Category: messagingCategory, Demo: callControlsDemo, - Keys: "Tab 进入工具栏;左右方向键移动;Enter / Space 激活。Cmd/Ctrl+D 静音,Cmd/Ctrl+E 摄像头,Cmd/Ctrl+Shift+S 共享,Cmd/Ctrl+Shift+H 挂断。", Code: `st := mujica.CallState{} + Keys: "Tab 进入工具栏;左右方向键移动;Enter / Space 激活。Cmd/Ctrl+D 静音,Cmd/Ctrl+E 摄像头,Cmd/Ctrl+Shift+S 共享,Cmd/Ctrl+Shift+H 挂断。", Code: `st := messaging.CallState{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CallControls(c, st, mujica.CallControlsOptions{ + core.Use(c, core.Settings{}) + messaging.CallControls(c, st, messaging.CallControlsOptions{ OnMute: func(on bool) { st.Muted = on }, OnCamera: func(off bool) { st.CameraOff = off }, OnShare: func(on bool) { st.Sharing = on }, @@ -91,20 +91,20 @@ view := func(c *ui.Context) { } ui.Render(view, 400, 300, 1)`}, {ID: "410", Name: "VideoCallGrid", ZhName: "视频通话宫格", Category: messagingCategory, Demo: videoCallGridDemo, - Keys: "Tab 进入宫格;方向键移动参与者焦点并读取状态。", Code: `people := []mujica.CallParticipant{ + Keys: "Tab 进入宫格;方向键移动参与者焦点并读取状态。", Code: `people := []messaging.CallParticipant{ {ID: "1", Name: "Sakiko", Speaking: true}, // Frame: the latest decoded frame {ID: "2", Name: "Uika", Muted: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{}) + core.Use(c, core.Settings{}) + messaging.VideoCallGrid(c, people, messaging.VideoCallGridOptions{}) } ui.Render(view, 400, 300, 1)`}, {ID: "411", Name: "IncomingCallDialog", ZhName: "来电对话框", Category: messagingCategory, Demo: incomingCallDialogDemo, Keys: "默认焦点为拒绝;Tab 循环访问操作;Enter / Space 激活;Escape 拒绝;关闭后恢复焦点。", Code: `ringing := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IncomingCallDialog(c, &ringing, mujica.IncomingCall{Caller: "Sakiko", Video: true}, mujica.IncomingCallOptions{ + core.Use(c, core.Settings{}) + messaging.IncomingCallDialog(c, &ringing, messaging.IncomingCall{Caller: "Sakiko", Video: true}, messaging.IncomingCallOptions{ OnAccept: func(video bool) {}, // join, with the camera on when video OnDecline: func() {}, }) @@ -114,17 +114,17 @@ ui.Render(view, 400, 300, 1)`}, Keys: "Tab 访问搜索、肤色、分类、表情;方向键在组内移动;Enter / Space 选择。", Code: `var recent []string tone := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { + core.Use(c, core.Settings{}) + if r := messaging.EmojiPicker(c, messaging.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { _ = r.Picked // insert it } } ui.Render(view, 400, 300, 1)`}, {ID: "413", Name: "PinnedMessages", ZhName: "置顶消息", Category: messagingCategory, Demo: pinnedMessagesDemo, - Keys: "Tab 访问跳转与取消置顶;方向键移动;Enter / Space 激活。", Code: `pins := []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} + Keys: "Tab 访问跳转与取消置顶;方向键移动;Enter / Space 激活。", Code: `pins := []messaging.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PinnedMessages(c, pins, mujica.PinnedMessagesOptions{}) + core.Use(c, core.Settings{}) + r := messaging.PinnedMessages(c, pins, messaging.PinnedMessagesOptions{}) if r.Unpinned != "" { pins = pins[:0] // unpin it } @@ -132,67 +132,67 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)`}, {ID: "414", Name: "ThreadPanel", ZhName: "话题侧栏", Category: messagingCategory, Demo: threadPanelDemo, Keys: "Tab 访问关闭、回复和发送;Enter 发送回复;Escape 关闭面板。", Code: `open := true -root := mujica.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} -var replies []mujica.ChatEntry +root := messaging.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} +var replies []messaging.ChatEntry view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.ThreadPanel(c, &open, root, replies, mujica.ThreadPanelOptions{}); r.Sent != "" { - replies = append(replies, mujica.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) + core.Use(c, core.Settings{}) + if r := messaging.ThreadPanel(c, &open, root, replies, messaging.ThreadPanelOptions{}); r.Sent != "" { + replies = append(replies, messaging.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) } } ui.Render(view, 400, 300, 1)`}, {ID: "415", Name: "MailList", ZhName: "邮件列表", Category: messagingCategory, Demo: mailListDemo, - Keys: "上下方向键、Home / End 移动选择;Enter 或双击打开;S 请求星标;Cmd/Ctrl+A 全选;Cmd/Ctrl 点击切换单项,Shift 点击选择范围。", Code: `var s mujica.ListState[string] -mails := []mujica.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} + Keys: "上下方向键、Home / End 移动选择;Enter 或双击打开;S 请求星标;Cmd/Ctrl+A 全选;Cmd/Ctrl 点击切换单项,Shift 点击选择范围。", Code: `var s data.ListState[string] +mails := []messaging.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailList(c, &s, mails, mujica.MailListOptions{}); r.Starred != "" { + core.Use(c, core.Settings{}) + if r := messaging.MailList(c, &s, mails, messaging.MailListOptions{}); r.Starred != "" { mails[0].Starred = !mails[0].Starred } } ui.Render(view, 400, 300, 1)`}, {ID: "416", Name: "MailReader", ZhName: "邮件阅读器", Category: messagingCategory, Demo: mailReaderDemo, - Keys: "Tab 访问链接、引用开关和附件;Enter / Space 激活;正文支持选择复制。", Code: `m := mujica.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", - Attachments: []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}}} + Keys: "Tab 访问链接、引用开关和附件;Enter / Space 激活;正文支持选择复制。", Code: `m := messaging.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", + Attachments: []messaging.MailAttachment{{Name: "score.pdf", Size: 2048}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if r := mujica.MailReader(c, m); r.Link != "" { + core.Use(c, core.Settings{}) + if r := messaging.MailReader(c, m); r.Link != "" { c.OpenURL(r.Link) } } ui.Render(view, 400, 300, 1)`}, {ID: "417", Name: "MailComposer", ZhName: "邮件编辑器", Category: messagingCategory, Demo: mailComposerDemo, - Keys: "Tab 访问字段与操作;Cmd/Ctrl+S 保存草稿;组件内 Cmd/Ctrl+Enter 发送;正文 Enter 换行。", Code: `draft := mujica.MailDraft{To: []string{"uika@example.com"}} + Keys: "Tab 访问字段与操作;Cmd/Ctrl+S 保存草稿;组件内 Cmd/Ctrl+Enter 发送;正文 Enter 换行。", Code: `draft := messaging.MailDraft{To: []string{"uika@example.com"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.MailComposer(c, &draft, mujica.MailComposerOptions{ - OnSaveDraft: func(d mujica.MailDraft) {}, // store d + core.Use(c, core.Settings{}) + r := messaging.MailComposer(c, &draft, messaging.MailComposerOptions{ + OnSaveDraft: func(d messaging.MailDraft) {}, // store d }) if r.Sent { - draft = mujica.MailDraft{} + draft = messaging.MailDraft{} } } ui.Render(view, 400, 300, 1)`}, {ID: "418", Name: "RecipientInput", ZhName: "收件人输入", Category: messagingCategory, Demo: recipientInputDemo, Keys: "Enter 或分隔符提交标签;空输入 Backspace 删除末项;上下方向键移动联系人建议,Enter 选择,Escape 收起建议。", Code: `var to []string -contacts := []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} +contacts := []messaging.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RecipientInput(c, &to, mujica.RecipientInputOptions{Contacts: contacts, Label: "To"}) + core.Use(c, core.Settings{}) + messaging.RecipientInput(c, &to, messaging.RecipientInputOptions{Contacts: contacts, Label: "To"}) } ui.Render(view, 400, 300, 1)`}, {ID: "419", Name: "QuotedText", ZhName: "引用文字", Category: messagingCategory, Demo: quotedTextDemo, Keys: "Tab 聚焦开关;Enter / Space 展开或折叠;展开内容支持选择复制。", Code: `open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuotedText(c, "> Can you send the score?", &open) + core.Use(c, core.Settings{}) + messaging.QuotedText(c, "> Can you send the score?", &open) } ui.Render(view, 400, 300, 1)`}, {ID: "420", Name: "SnoozePicker", ZhName: "稍后提醒", Category: messagingCategory, Demo: snoozePickerDemo, Keys: "Tab 进入;上下方向键选择快捷项;Enter / Space 激活;自定义时间沿用 DateTimePicker 的输入、确认和取消操作。", Code: `var until time.Time view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SnoozePicker(c, &until, mujica.SnoozePickerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if messaging.SnoozePicker(c, &until, messaging.SnoozePickerOptions{}).Changed() { _ = until // hide the mail until then } } diff --git a/examples/gallery/pages_messaging_calls.go b/examples/gallery/pages_messaging_calls.go index f5fa3e5..a601c03 100644 --- a/examples/gallery/pages_messaging_calls.go +++ b/examples/gallery/pages_messaging_calls.go @@ -4,22 +4,24 @@ import ( "fmt" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/messaging" ) func callControlsDemo(c *ui.Context, log func(string)) { type state struct { - call mujica.CallState + call messaging.CallState disabled bool ended bool } s := ui.Local(c.Root(), "409", func() state { return state{} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Reconnect", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Reconnect", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.ended = false log("call joined") } - mujica.CallControls(c, s.call, mujica.CallControlsOptions{ + messaging.CallControls(c, s.call, messaging.CallControlsOptions{ Disabled: s.disabled || s.ended, OnMute: func(on bool) { s.call.Muted = on; log(fmt.Sprintf("mute %v", on)) }, OnCamera: func(off bool) { s.call.CameraOff = off; log(fmt.Sprintf("camera off %v", off)) }, @@ -39,10 +41,10 @@ func videoCallGridDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.muted, "Muted") mediaCheck(c, log, &s.cameraOff, "Camera off") mediaCheck(c, log, &s.frame, "Caller-provided frame") - if mujica.NumberInput(c, &s.columns, mujica.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Columns (0 = auto)"}).Changed() { + if input.NumberInput(c, &s.columns, input.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Columns (0 = auto)"}).Changed() { log(fmt.Sprintf("columns %.0f", s.columns)) } - people := []mujica.CallParticipant{ + people := []messaging.CallParticipant{ {ID: "1", Name: "Sakiko", Speaking: s.speaking, Muted: s.muted, CameraOff: s.cameraOff}, {ID: "2", Name: "Uika", Muted: true}, {ID: "3", Name: "Mutsumi", CameraOff: true}, {ID: "4", Name: "Umiri"}, } @@ -50,7 +52,7 @@ func videoCallGridDemo(c *ui.Context, log func(string)) { people[0].Frame = mediaBitmaps[0] people[3].Frame = mediaBitmaps[1] } - mujica.VideoCallGrid(c, people, mujica.VideoCallGridOptions{Columns: int(s.columns)}) + messaging.VideoCallGrid(c, people, messaging.VideoCallGridOptions{Columns: int(s.columns)}) } func incomingCallDialogDemo(c *ui.Context, log func(string)) { @@ -62,11 +64,11 @@ func incomingCallDialogDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.video, "Video call") mediaCheck(c, log, &s.focusAccept, "Focus Accept") mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Incoming call", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Incoming call", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.open = true log("incoming call") } - mujica.IncomingCallDialog(c, &s.open, mujica.IncomingCall{Caller: "Sakiko Togawa", Video: s.video}, mujica.IncomingCallOptions{ + messaging.IncomingCallDialog(c, &s.open, messaging.IncomingCall{Caller: "Sakiko Togawa", Video: s.video}, messaging.IncomingCallOptions{ Disabled: s.disabled, FocusAccept: s.focusAccept, OnAccept: func(video bool) { s.result = fmt.Sprintf("Accepted, video: %v", video); log(s.result) }, OnDecline: func() { s.result = "Declined"; log(s.result) }, diff --git a/examples/gallery/pages_messaging_chat.go b/examples/gallery/pages_messaging_chat.go index 586cf95..7aa64ea 100644 --- a/examples/gallery/pages_messaging_chat.go +++ b/examples/gallery/pages_messaging_chat.go @@ -6,7 +6,11 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/messaging" ) func chatMessageDemo(c *ui.Context, log func(string)) { @@ -21,54 +25,54 @@ func chatMessageDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.reply, "Reply slot") mediaCheck(c, log, &s.reactions, "Reaction slot") at := time.Date(2026, 10, 5, 21, 7, 0, 0, time.Local) - mujica.ChatMessage(c, mujica.ChatEntry{Author: "Uika", Time: at, Text: "Rehearsal at nine.", Own: s.own}, mujica.ChatMessageOptions{}) - opts := mujica.ChatMessageOptions{Continued: s.continued} + messaging.ChatMessage(c, messaging.ChatEntry{Author: "Uika", Time: at, Text: "Rehearsal at nine.", Own: s.own}, messaging.ChatMessageOptions{}) + opts := messaging.ChatMessageOptions{Continued: s.continued} if s.reply { opts.Reply = func() { - if mujica.QuoteReply(c, "Sakiko", "When do we start?").Clicked() { + if chat.QuoteReply(c, "Sakiko", "When do we start?").Clicked() { log("jump to quoted message") } } } if s.reactions { opts.Reactions = func() { - if mujica.Toggle(c, &s.reacted, "Like", mujica.ToggleOptions{}).Changed() { + if input.Toggle(c, &s.reacted, "Like", input.ToggleOptions{}).Changed() { log(fmt.Sprintf("reaction %v", s.reacted)) } } } - mujica.ChatMessage(c, mujica.ChatEntry{Author: "Uika", Time: at.Add(time.Minute), Text: "Bring the masks.", Own: s.own, Edited: s.edited}, opts) + messaging.ChatMessage(c, messaging.ChatEntry{Author: "Uika", Time: at.Add(time.Minute), Text: "Bring the masks.", Own: s.own, Edited: s.edited}, opts) } func unreadDividerDemo(c *ui.Context, log func(string)) { count := ui.Local(c.Root(), "403", func() float64 { return 3 }) - if mujica.NumberInput(c, count, mujica.NumberInputOptions{Min: 0, Max: 999, Step: 1, Label: "Unread count"}).Changed() { + if input.NumberInput(c, count, input.NumberInputOptions{Min: 0, Max: 999, Step: 1, Label: "Unread count"}).Changed() { log(fmt.Sprintf("unread %.0f", *count)) } - mujica.UnreadDivider(c, int(*count)) - inputSection(c, "Zero count", func() { mujica.UnreadDivider(c, 0) }) + messaging.UnreadDivider(c, int(*count)) + inputSection(c, "Zero count", func() { messaging.UnreadDivider(c, 0) }) } func readReceiptDemo(c *ui.Context, log func(string)) { type state struct { - receipt mujica.ReceiptState + receipt messaging.ReceiptState readers bool max float64 } - s := ui.Local(c.Root(), "404", func() state { return state{receipt: mujica.ReceiptRead, readers: true, max: 3} }) - items := []mujica.SelectOption[mujica.ReceiptState]{{Value: mujica.ReceiptSent, Label: "Sent"}, {Value: mujica.ReceiptDelivered, Label: "Delivered"}, {Value: mujica.ReceiptRead, Label: "Read"}} - if mujica.Select(c, &s.receipt, items, mujica.SelectOptions{Label: "Receipt"}).Changed() { + s := ui.Local(c.Root(), "404", func() state { return state{receipt: messaging.ReceiptRead, readers: true, max: 3} }) + items := []input.SelectOption[messaging.ReceiptState]{{Value: messaging.ReceiptSent, Label: "Sent"}, {Value: messaging.ReceiptDelivered, Label: "Delivered"}, {Value: messaging.ReceiptRead, Label: "Read"}} + if input.Select(c, &s.receipt, items, input.SelectOptions{Label: "Receipt"}).Changed() { log(fmt.Sprintf("receipt %d", s.receipt)) } mediaCheck(c, log, &s.readers, "Reader avatars") - if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 4, Step: 1, Label: "Max avatars"}).Changed() { + if input.NumberInput(c, &s.max, input.NumberInputOptions{Min: 1, Max: 4, Step: 1, Label: "Max avatars"}).Changed() { log(fmt.Sprintf("max %.0f", s.max)) } - opts := mujica.ReadReceiptOptions{Max: int(s.max)} + opts := messaging.ReadReceiptOptions{Max: int(s.max)} if s.readers { - opts.Readers = []mujica.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}, {Name: "Umiri"}, {Name: "Nyamu"}} + opts.Readers = []display.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}, {Name: "Umiri"}, {Name: "Nyamu"}} } - mujica.ReadReceipt(c, s.receipt, opts) + messaging.ReadReceipt(c, s.receipt, opts) } func emojiPickerDemo(c *ui.Context, log func(string)) { @@ -80,7 +84,7 @@ func emojiPickerDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "412", func() state { return state{} }) mediaCheck(c, log, &s.disabled, "Disabled") - if r := mujica.EmojiPicker(c, mujica.EmojiPickerOptions{Recent: &s.recent, Tone: &s.tone, Disabled: s.disabled}); r.Picked != "" { + if r := messaging.EmojiPicker(c, messaging.EmojiPickerOptions{Recent: &s.recent, Tone: &s.tone, Disabled: s.disabled}); r.Picked != "" { s.picked = r.Picked log("emoji " + r.Picked) } @@ -89,26 +93,26 @@ func emojiPickerDemo(c *ui.Context, log func(string)) { func pinnedMessagesDemo(c *ui.Context, log func(string)) { type state struct { - pins []mujica.ChatEntry + pins []messaging.ChatEntry disabled bool jump string } - newPins := func() []mujica.ChatEntry { - return []mujica.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}, {ID: "b", Author: "Uika", Text: "Masks are in the hall."}} + newPins := func() []messaging.ChatEntry { + return []messaging.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}, {ID: "b", Author: "Uika", Text: "Masks are in the hall."}} } s := ui.Local(c.Root(), "413", func() state { return state{pins: newPins()} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Restore pins", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restore pins", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.pins = newPins() log("pins restored") } - r := mujica.PinnedMessages(c, s.pins, mujica.PinnedMessagesOptions{Disabled: s.disabled, Height: 180}) + r := messaging.PinnedMessages(c, s.pins, messaging.PinnedMessagesOptions{Disabled: s.disabled, Height: 180}) if r.Jump != "" { s.jump = r.Jump log("jump " + r.Jump) } if r.Unpinned != "" { - s.pins = slices.DeleteFunc(slices.Clone(s.pins), func(m mujica.ChatEntry) bool { return m.ID == r.Unpinned }) + s.pins = slices.DeleteFunc(slices.Clone(s.pins), func(m messaging.ChatEntry) bool { return m.ID == r.Unpinned }) log("unpin " + r.Unpinned) } ui.Text(c, "Jump target: "+s.jump) @@ -117,21 +121,21 @@ func pinnedMessagesDemo(c *ui.Context, log func(string)) { func threadPanelDemo(c *ui.Context, log func(string)) { type state struct { open, disabled bool - replies []mujica.ChatEntry + replies []messaging.ChatEntry } s := ui.Local(c.Root(), "414", func() state { - return state{open: true, replies: []mujica.ChatEntry{{ID: "1", Author: "Uika", Text: "Overture first."}}} + return state{open: true, replies: []messaging.ChatEntry{{ID: "1", Author: "Uika", Text: "Overture first."}}} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Open thread", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Open thread", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.open = true log("thread opened") } before := s.open ui.Row(c).Height(420).Children(func() { - r := mujica.ThreadPanel(c, &s.open, mujica.ChatEntry{ID: "root", Author: "Sakiko", Text: "Set list for Friday?"}, s.replies, mujica.ThreadPanelOptions{Disabled: s.disabled}) + r := messaging.ThreadPanel(c, &s.open, messaging.ChatEntry{ID: "root", Author: "Sakiko", Text: "Set list for Friday?"}, s.replies, messaging.ThreadPanelOptions{Disabled: s.disabled}) if r.Sent != "" { - s.replies = append(s.replies, mujica.ChatEntry{ID: fmt.Sprint(len(s.replies) + 1), Author: "Me", Text: r.Sent, Own: true}) + s.replies = append(s.replies, messaging.ChatEntry{ID: fmt.Sprint(len(s.replies) + 1), Author: "Me", Text: r.Sent, Own: true}) log("reply sent") } }) diff --git a/examples/gallery/pages_messaging_mail.go b/examples/gallery/pages_messaging_mail.go index 0e35654..5afc8ce 100644 --- a/examples/gallery/pages_messaging_mail.go +++ b/examples/gallery/pages_messaging_mail.go @@ -5,18 +5,21 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/messaging" ) func mailListDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.ListState[string] - mails []mujica.MailItem + list data.ListState[string] + mails []messaging.MailItem disabled, empty bool opened string } s := ui.Local(c.Root(), "415", func() state { - return state{mails: []mujica.MailItem{ + return state{mails: []messaging.MailItem{ {ID: "1", From: "Sakiko", Subject: "Second act", Snippet: "The score is attached.", Unread: true, Attachments: 2}, {ID: "2", From: "Uika", Subject: "Costumes", Snippet: "Fittings on Thursday.", Starred: true}, {ID: "3", From: "Umiri", Subject: "Venue", Snippet: "The hall is booked."}, @@ -24,10 +27,10 @@ func mailListDemo(c *ui.Context, log func(string)) { }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.empty, "Empty") - if mujica.Button(c, "Load 100,000 mails", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { - s.mails = make([]mujica.MailItem, 100000) + if input.Button(c, "Load 100,000 mails", input.ButtonOptions{Variant: input.Secondary}).Clicked() { + s.mails = make([]messaging.MailItem, 100000) for i := range s.mails { - s.mails[i] = mujica.MailItem{ID: fmt.Sprint(i), From: "Sender", Subject: fmt.Sprintf("Mail %d", i), Unread: i%2 == 0} + s.mails[i] = messaging.MailItem{ID: fmt.Sprint(i), From: "Sender", Subject: fmt.Sprintf("Mail %d", i), Unread: i%2 == 0} } log("loaded 100,000 mails") } @@ -35,7 +38,7 @@ func mailListDemo(c *ui.Context, log func(string)) { if s.empty { mails = nil } - r := mujica.MailList(c, &s.list, mails, mujica.MailListOptions{Disabled: s.disabled, Height: 240}) + r := messaging.MailList(c, &s.list, mails, messaging.MailListOptions{Disabled: s.disabled, Height: 240}) if r.Changed() { log(fmt.Sprintf("selected %d mails", s.list.Selection.Len())) } @@ -67,16 +70,16 @@ func mailReaderDemo(c *ui.Context, log func(string)) { mediaCheck(c, log, &s.markdown, "Safe Markdown") mediaCheck(c, log, &s.attachments, "Attachments") mediaCheck(c, log, &s.disabled, "Disabled actions") - m := mujica.Mail{From: "Sakiko Togawa", To: []string{"Uika Misumi"}, Cc: []string{"Umiri Yahata"}, Subject: "Second act", Date: time.Date(2026, 10, 5, 21, 7, 0, 0, time.Local), Body: "The score is attached.\n> Can you send it tonight?"} + m := messaging.Mail{From: "Sakiko Togawa", To: []string{"Uika Misumi"}, Cc: []string{"Umiri Yahata"}, Subject: "Second act", Date: time.Date(2026, 10, 5, 21, 7, 0, 0, time.Local), Body: "The score is attached.\n> Can you send it tonight?"} if s.markdown { - m.Format = mujica.MailMarkdown + m.Format = messaging.MailMarkdown m.Body = "**Rehearsal** at nine. [Venue](https://example.com)\n\n" } if s.attachments { - m.Attachments = []mujica.MailAttachment{{Name: "score.pdf", Size: 2048}} + m.Attachments = []messaging.MailAttachment{{Name: "score.pdf", Size: 2048}} } ui.Column(c).Disabled(s.disabled).Children(func() { - r := mujica.MailReader(c, m) + r := messaging.MailReader(c, m) if r.Link != "" { log("link " + r.Link) } @@ -91,32 +94,32 @@ func mailReaderDemo(c *ui.Context, log func(string)) { func mailComposerDemo(c *ui.Context, log func(string)) { type state struct { - draft mujica.MailDraft + draft messaging.MailDraft disabled bool saved string } - s := ui.Local(c.Root(), "417", func() state { return state{draft: mujica.MailDraft{To: []string{"uika@example.com"}}} }) + s := ui.Local(c.Root(), "417", func() state { return state{draft: messaging.MailDraft{To: []string{"uika@example.com"}}} }) mediaCheck(c, log, &s.disabled, "Disabled") - r := mujica.MailComposer(c, &s.draft, mujica.MailComposerOptions{ + r := messaging.MailComposer(c, &s.draft, messaging.MailComposerOptions{ Disabled: s.disabled, Contacts: messagingContacts, - OnSaveDraft: func(d mujica.MailDraft) { s.saved = d.Subject; log("draft saved") }, + OnSaveDraft: func(d messaging.MailDraft) { s.saved = d.Subject; log("draft saved") }, OnAttach: func() { - s.draft.Attachments = append(s.draft.Attachments, mujica.MailAttachment{Name: fmt.Sprintf("score-%d.pdf", len(s.draft.Attachments)+1), Size: 2048}) + s.draft.Attachments = append(s.draft.Attachments, messaging.MailAttachment{Name: fmt.Sprintf("score-%d.pdf", len(s.draft.Attachments)+1), Size: 2048}) log("attachment added") }, }) if r.Sent { log("mail sent") - s.draft = mujica.MailDraft{} + s.draft = messaging.MailDraft{} } if r.Discarded { log("draft discarded") - s.draft = mujica.MailDraft{} + s.draft = messaging.MailDraft{} } ui.Text(c, "Saved subject: "+s.saved) } -var messagingContacts = []mujica.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}, {Name: "Umiri Yahata", Email: "umiri@example.com"}, {Name: "Sakiko Togawa", Email: "sakiko@example.com"}} +var messagingContacts = []messaging.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}, {Name: "Umiri Yahata", Email: "umiri@example.com"}, {Name: "Sakiko Togawa", Email: "sakiko@example.com"}} func recipientInputDemo(c *ui.Context, log func(string)) { type state struct { @@ -130,10 +133,10 @@ func recipientInputDemo(c *ui.Context, log func(string)) { if !s.contacts { contacts = nil } - if mujica.RecipientInput(c, &s.to, mujica.RecipientInputOptions{Contacts: contacts, Label: "Recipients", Disabled: s.disabled}).Changed() { + if messaging.RecipientInput(c, &s.to, messaging.RecipientInputOptions{Contacts: contacts, Label: "Recipients", Disabled: s.disabled}).Changed() { log(fmt.Sprintf("recipients %d", len(s.to))) } - if mujica.Button(c, "Clear recipients", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.disabled}).Clicked() { + if input.Button(c, "Clear recipients", input.ButtonOptions{Variant: input.Secondary, Disabled: s.disabled}).Clicked() { s.to = nil log("recipients cleared") } @@ -147,11 +150,11 @@ func quotedTextDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "419", func() state { return state{text: "> Can you send the score?\n> Thanks."} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.TextArea(c, &s.text, mujica.TextAreaOptions{Label: "Quoted source", MinHeight: 80}).Changed() { + if input.TextArea(c, &s.text, input.TextAreaOptions{Label: "Quoted source", MinHeight: 80}).Changed() { log("quote source edited") } ui.Column(c).Disabled(s.disabled).Children(func() { - if mujica.QuotedText(c, s.text, &s.open).Changed() { + if messaging.QuotedText(c, s.text, &s.open).Changed() { log(fmt.Sprintf("quote expanded %v", s.open)) } }) @@ -165,13 +168,13 @@ func snoozePickerDemo(c *ui.Context, log func(string)) { } s := ui.Local(c.Root(), "420", func() state { return state{morning: 9} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.NumberInput(c, &s.morning, mujica.NumberInputOptions{Min: 1, Max: 23, Step: 1, Label: "Morning hour"}).Changed() { + if input.NumberInput(c, &s.morning, input.NumberInputOptions{Min: 1, Max: 23, Step: 1, Label: "Morning hour"}).Changed() { log(fmt.Sprintf("morning %.0f", s.morning)) } - if mujica.SnoozePicker(c, &s.until, mujica.SnoozePickerOptions{Disabled: s.disabled, MorningHour: int(s.morning)}).Changed() { + if messaging.SnoozePicker(c, &s.until, messaging.SnoozePickerOptions{Disabled: s.disabled, MorningHour: int(s.morning)}).Changed() { log("snooze " + s.until.Format(time.RFC3339)) } - if mujica.Button(c, "Clear snooze", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.disabled}).Clicked() { + if input.Button(c, "Clear snooze", input.ButtonOptions{Variant: input.Secondary, Disabled: s.disabled}).Clicked() { s.until = time.Time{} log("snooze cleared") } diff --git a/examples/gallery/pages_messaging_people.go b/examples/gallery/pages_messaging_people.go index 355f338..11c64af 100644 --- a/examples/gallery/pages_messaging_people.go +++ b/examples/gallery/pages_messaging_people.go @@ -5,7 +5,9 @@ import ( "slices" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/messaging" ) func channelListDemo(c *ui.Context, log func(string)) { @@ -13,15 +15,15 @@ func channelListDemo(c *ui.Context, log func(string)) { selected string disabled bool revision int - groups []mujica.ChannelGroup + groups []messaging.ChannelGroup } s := ui.Local(c.Root(), "401", func() state { - return state{groups: []mujica.ChannelGroup{ - {ID: "channels", Title: "Channels", Channels: []mujica.Channel{ + return state{groups: []messaging.ChannelGroup{ + {ID: "channels", Title: "Channels", Channels: []messaging.Channel{ {ID: "general", Name: "general", Unread: 3}, {ID: "rehearsal", Name: "rehearsal", Pinned: true}, {ID: "random", Name: "random", Unread: 120, Muted: true}, }}, - {ID: "direct", Title: "Direct messages", Channels: []mujica.Channel{{ID: "uika", Name: "Uika", Direct: true, Presence: mujica.PresenceOnline}}}, + {ID: "direct", Title: "Direct messages", Channels: []messaging.Channel{{ID: "uika", Name: "Uika", Direct: true, Presence: messaging.PresenceOnline}}}, }} }) mediaCheck(c, log, &s.disabled, "Disabled") @@ -31,7 +33,7 @@ func channelListDemo(c *ui.Context, log func(string)) { if before != s.groups[0].Channels[0] { s.revision++ } - if mujica.ChannelList(c, &s.selected, s.groups, mujica.ChannelListOptions{Disabled: s.disabled, Revision: s.revision}).Changed() { + if messaging.ChannelList(c, &s.selected, s.groups, messaging.ChannelListOptions{Disabled: s.disabled, Revision: s.revision}).Changed() { log("channel " + s.selected) } ui.Text(c, "Selected: "+s.selected) @@ -39,25 +41,25 @@ func channelListDemo(c *ui.Context, log func(string)) { func memberListDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.MemberListState + list messaging.MemberListState disabled, empty bool - members []mujica.Member + members []messaging.Member } s := ui.Local(c.Root(), "405", func() state { - return state{members: []mujica.Member{ - {ID: "1", Name: "Sakiko", Presence: mujica.PresenceOnline, Role: "Leader"}, - {ID: "2", Name: "Uika", Presence: mujica.PresenceBusy, Role: "Vocal"}, - {ID: "3", Name: "Mutsumi", Presence: mujica.PresenceAway}, {ID: "4", Name: "Umiri", Presence: mujica.PresenceOffline}, + return state{members: []messaging.Member{ + {ID: "1", Name: "Sakiko", Presence: messaging.PresenceOnline, Role: "Leader"}, + {ID: "2", Name: "Uika", Presence: messaging.PresenceBusy, Role: "Vocal"}, + {ID: "3", Name: "Mutsumi", Presence: messaging.PresenceAway}, {ID: "4", Name: "Umiri", Presence: messaging.PresenceOffline}, }} }) mediaCheck(c, log, &s.disabled, "Disabled") mediaCheck(c, log, &s.empty, "Empty") - if mujica.Button(c, "Toggle Uika presence", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Toggle Uika presence", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.members = slices.Clone(s.members) - if s.members[1].Presence == mujica.PresenceOffline { - s.members[1].Presence = mujica.PresenceOnline + if s.members[1].Presence == messaging.PresenceOffline { + s.members[1].Presence = messaging.PresenceOnline } else { - s.members[1].Presence = mujica.PresenceOffline + s.members[1].Presence = messaging.PresenceOffline } log("Uika presence changed") } @@ -65,7 +67,7 @@ func memberListDemo(c *ui.Context, log func(string)) { if s.empty { members = nil } - r := mujica.MemberList(c, &s.list, members, mujica.MemberListOptions{Disabled: s.disabled, Height: 240}) + r := messaging.MemberList(c, &s.list, members, messaging.MemberListOptions{Disabled: s.disabled, Height: 240}) if r.Changed() { log("member " + s.list.Selected) } @@ -76,61 +78,61 @@ func memberListDemo(c *ui.Context, log func(string)) { func userProfileCardDemo(c *ui.Context, log func(string)) { type state struct { - presence mujica.PresenceStatus + presence messaging.PresenceStatus actions, disabled bool } - s := ui.Local(c.Root(), "406", func() state { return state{actions: true, presence: mujica.PresenceOnline} }) + s := ui.Local(c.Root(), "406", func() state { return state{actions: true, presence: messaging.PresenceOnline} }) mediaCheck(c, log, &s.actions, "Actions") mediaCheck(c, log, &s.disabled, "Disable actions") messagingPresence(c, log, &s.presence) - opts := mujica.UserProfileCardOptions{} + opts := messaging.UserProfileCardOptions{} if s.actions { opts.Actions = func() { for _, action := range []string{"Message", "Call"} { - if mujica.Button(c, action, mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, action, input.ButtonOptions{Disabled: s.disabled}).Clicked() { log(action) } } } } - mujica.UserProfileCard(c, mujica.UserProfile{Name: "Sakiko Togawa", Presence: s.presence, Role: "Keyboard", Status: "In rehearsal", About: "Founder of the masquerade."}, opts) + messaging.UserProfileCard(c, messaging.UserProfile{Name: "Sakiko Togawa", Presence: s.presence, Role: "Keyboard", Status: "In rehearsal", About: "Founder of the masquerade."}, opts) } func onlineStatusDemo(c *ui.Context, log func(string)) { type state struct { - presence mujica.PresenceStatus + presence messaging.PresenceStatus hidden bool } - s := ui.Local(c.Root(), "407", func() state { return state{presence: mujica.PresenceOnline} }) + s := ui.Local(c.Root(), "407", func() state { return state{presence: messaging.PresenceOnline} }) mediaCheck(c, log, &s.hidden, "Hide text") messagingPresence(c, log, &s.presence) - mujica.OnlineStatus(c, s.presence, mujica.OnlineStatusOptions{HideText: s.hidden}) + messaging.OnlineStatus(c, s.presence, messaging.OnlineStatusOptions{HideText: s.hidden}) inputSection(c, "States", func() { - for p := mujica.PresenceOffline; p <= mujica.PresenceBusy; p++ { - mujica.OnlineStatus(c, p, mujica.OnlineStatusOptions{}) + for p := messaging.PresenceOffline; p <= messaging.PresenceBusy; p++ { + messaging.OnlineStatus(c, p, messaging.OnlineStatusOptions{}) } }) } func statusSetterDemo(c *ui.Context, log func(string)) { type state struct { - status mujica.CustomStatus + status messaging.CustomStatus disabled bool } s := ui.Local(c.Root(), "408", func() state { return state{} }) mediaCheck(c, log, &s.disabled, "Disabled") - if mujica.StatusSetter(c, &s.status, mujica.StatusSetterOptions{Disabled: s.disabled}).Changed() { + if messaging.StatusSetter(c, &s.status, messaging.StatusSetterOptions{Disabled: s.disabled}).Changed() { log("status changed") } ui.Text(c, fmt.Sprintf("Text: %s; expires: %v", s.status.Text, s.status.Until)) } -func messagingPresence(c *ui.Context, log func(string), value *mujica.PresenceStatus) { - items := []mujica.SelectOption[mujica.PresenceStatus]{ - {Value: mujica.PresenceOnline, Label: "Online"}, {Value: mujica.PresenceAway, Label: "Away"}, - {Value: mujica.PresenceBusy, Label: "Busy"}, {Value: mujica.PresenceOffline, Label: "Offline"}, +func messagingPresence(c *ui.Context, log func(string), value *messaging.PresenceStatus) { + items := []input.SelectOption[messaging.PresenceStatus]{ + {Value: messaging.PresenceOnline, Label: "Online"}, {Value: messaging.PresenceAway, Label: "Away"}, + {Value: messaging.PresenceBusy, Label: "Busy"}, {Value: messaging.PresenceOffline, Label: "Offline"}, } - if mujica.Select(c, value, items, mujica.SelectOptions{Label: "Presence"}).Changed() { + if input.Select(c, value, items, input.SelectOptions{Label: "Presence"}).Changed() { log(fmt.Sprintf("presence %d", *value)) } } diff --git a/examples/gallery/pages_messaging_test.go b/examples/gallery/pages_messaging_test.go index e55faa0..a7a3011 100644 --- a/examples/gallery/pages_messaging_test.go +++ b/examples/gallery/pages_messaging_test.go @@ -10,10 +10,11 @@ import ( "testing" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func messagingDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) { +func messagingDemoTester(t *testing.T, id string, mode core.Mode) (*ui.Tester, *[]string) { t.Helper() i := slices.IndexFunc(messagingPages, func(p page) bool { return p.ID == id }) if i < 0 { @@ -21,7 +22,7 @@ func messagingDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, } events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{Mode: mode}) + core.Use(c, core.Settings{Mode: mode}) ui.Scroll(c).Fill().Padding(24).Gap(12).Children(func() { messagingPages[i].Demo(c, func(s string) { *events = append(*events, s) }) }) @@ -58,7 +59,7 @@ func TestMessagingDemosInteractAndLog(t *testing.T) { t.Fatalf("pages=%d, cases=%d", len(messagingPages), len(cases)) } for _, tc := range cases { - for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { t.Run(tc.id+mode.String(), func(t *testing.T) { tt, events := messagingDemoTester(t, tc.id, mode) if err := tt.Click(tc.click); err != nil { @@ -105,7 +106,7 @@ func TestMessagingDemoCaptures(t *testing.T) { if p.Category != "Messaging & Mail" || p.Code == "" || p.Keys == "" { t.Fatalf("incomplete page %s", p.ID) } - for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} { + for _, mode := range []core.Mode{core.Light, core.Dark} { tt, _ := messagingDemoTester(t, p.ID, mode) f, err := os.Create(filepath.Join(dir, fmt.Sprintf("%s-%s.png", p.ID, mode))) if err != nil { diff --git a/examples/gallery/pages_mktchart.go b/examples/gallery/pages_mktchart.go index e495179..65573b0 100644 --- a/examples/gallery/pages_mktchart.go +++ b/examples/gallery/pages_mktchart.go @@ -1,6 +1,8 @@ package main -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" +) var mktchartPages = []page{ {ID: "141", Name: "CandlestickChart", ZhName: "K 线", Category: "Market Charts", Demo: func(c *ui.Context, log func(string)) { mktDemo(c, log, "141") }, Code: mktCode141, Keys: mktKeys}, diff --git a/examples/gallery/pages_mktchart_code.go b/examples/gallery/pages_mktchart_code.go index c81078d..60aa44b 100644 --- a/examples/gallery/pages_mktchart_code.go +++ b/examples/gallery/pages_mktchart_code.go @@ -1,211 +1,211 @@ package main const mktCode141 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1)` const mktCode142 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OHLCChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.OHLCChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1)` const mktCode143 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView raw := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.HeikinAshiChart(c, candles, &window, &raw, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.HeikinAshiChart(c, candles, &window, &raw, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1)` const mktCode144 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - points := []mujica.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} + points := []chart.QuotePoint{{Time: at, Price: 101, Volume: 100}, {Time: at.Add(time.Hour), Price: 103, Volume: 200}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LineQuoteChart(c, points, mujica.LineQuoteChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) + core.Use(c, core.Settings{}) + chart.LineQuoteChart(c, points, chart.LineQuoteChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, PrevClose: 100, RegularOpen: at.Add(30 * time.Minute), RegularClose: at.Add(45 * time.Minute)}) } ui.Render(view, 640, 400, 1)` const mktCode145 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView + var window chart.MarketView view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeChart(c, candles, &window, mujica.VolumeChartOptions{Frame: mujica.ChartFrame{Label: "Volume"}}) + core.Use(c, core.Settings{}) + chart.VolumeChart(c, candles, &window, chart.VolumeChartOptions{Frame: chart.ChartFrame{Label: "Volume"}}) } ui.Render(view, 640, 400, 1)` -const mktCode146 = ` bids := []mujica.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} - asks := []mujica.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} +const mktCode146 = ` bids := []chart.DepthLevel{{Price: 99, Size: 10}, {Price: 98, Size: 20}} + asks := []chart.DepthLevel{{Price: 101, Size: 15}, {Price: 102, Size: 25}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.DepthChart(c, bids, asks, chart.DepthChartOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1)` -const mktCode147 = ` bins := []mujica.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} +const mktCode147 = ` bins := []chart.ProfileBin{{Low: 99, High: 100, Volume: 30}, {Low: 100, High: 101, Volume: 80}, {Low: 101, High: 102, Volume: 40}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}}) + core.Use(c, core.Settings{}) + chart.VolumeProfile(c, bins, chart.VolumeProfileOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}}) } ui.Render(view, 640, 400, 1)` -const mktCode148 = ` state := mujica.MultiChartState{Count: 2} +const mktCode148 = ` state := chart.MultiChartState{Count: 2} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MultiChartLayout(c, &state, mujica.MultiChartLayoutOptions{Height: 200}, func(i int, cell *mujica.ChartCell) { - mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: "Symbol"}) + core.Use(c, core.Settings{}) + chart.MultiChartLayout(c, &state, chart.MultiChartLayoutOptions{Height: 200}, func(i int, cell *chart.ChartCell) { + input.TextInput(c, &cell.Symbol, input.TextInputOptions{Label: "Symbol"}) }) } ui.Render(view, 640, 400, 1)` const mktCode149 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - var sync mujica.ChartSync + var window chart.MarketView + var sync chart.ChartSync view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) sync.Link(c, "price") - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Sync: &sync, SyncID: "price"}) } ui.Render(view, 640, 400, 1)` -const mktCode150 = ` value := mujica.MarketRange{Preset: mujica.Range1M} +const mktCode150 = ` value := chart.MarketRange{Preset: chart.Range1M} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeRangeSelector(c, &value, mujica.TimeRangeSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.TimeRangeSelector(c, &value, chart.TimeRangeSelectorOptions{}) } ui.Render(view, 640, 400, 1)` -const mktCode151 = ` value := mujica.Interval("1h") - favorites := []mujica.Interval{"1m", "1h", "1D"} +const mktCode151 = ` value := chart.Interval("1h") + favorites := []chart.Interval{"1m", "1h", "1D"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IntervalSelector(c, &value, &favorites, mujica.IntervalSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IntervalSelector(c, &value, &favorites, chart.IntervalSelectorOptions{}) } ui.Render(view, 640, 400, 1)` -const mktCode152 = ` value := mujica.ChartCandles +const mktCode152 = ` value := chart.ChartCandles view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ChartTypeSwitcher(c, &value, mujica.ChartTypeSwitcherOptions{}) + core.Use(c, core.Settings{}) + chart.ChartTypeSwitcher(c, &value, chart.ChartTypeSwitcherOptions{}) } ui.Render(view, 640, 400, 1)` const mktCode153 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - overlays := []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{2}}} + var window chart.MarketView + overlays := []chart.IndicatorOverlay{{Kind: chart.IndicatorMA, Params: []float64{2}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Overlays: overlays}) } ui.Render(view, 640, 400, 1)` const mktCode154 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - panes := mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorRSI, Params: []float64{2}}}} + var window chart.MarketView + panes := chart.IndicatorPanes{Panes: []chart.IndicatorPaneSpec{{Kind: chart.IndicatorRSI, Params: []float64{2}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorPane(c, candles, &window, &panes, mujica.IndicatorPaneOptions{Height: 200, Label: "BTC"}) + core.Use(c, core.Settings{}) + chart.IndicatorPane(c, candles, &window, &panes, chart.IndicatorPaneOptions{Height: 200, Label: "BTC"}) } ui.Render(view, 640, 400, 1)` -const mktCode155 = ` var overlays []mujica.IndicatorOverlay - var panes mujica.IndicatorPanes +const mktCode155 = ` var overlays []chart.IndicatorOverlay + var panes chart.IndicatorPanes view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.IndicatorSelector(c, &overlays, &panes, mujica.IndicatorSelectorOptions{}) + core.Use(c, core.Settings{}) + chart.IndicatorSelector(c, &overlays, &panes, chart.IndicatorSelectorOptions{}) } ui.Render(view, 640, 400, 1)` const mktCode156 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - drawings := mujica.DrawingTools{Tool: mujica.ToolTrendLine} + var window chart.MarketView + drawings := chart.DrawingTools{Tool: chart.ToolTrendLine} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Drawings: &drawings}) } ui.Render(view, 640, 400, 1)` -const mktCode157 = ` var drawings mujica.DrawingTools +const mktCode157 = ` var drawings chart.DrawingTools view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DrawingToolbar(c, &drawings, mujica.DrawingToolbarOptions{Horizontal: true}) + core.Use(c, core.Settings{}) + chart.DrawingToolbar(c, &drawings, chart.DrawingToolbarOptions{Horizontal: true}) } ui.Render(view, 640, 400, 1)` const mktCode158 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - symbols := []mujica.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} + var window chart.MarketView + symbols := []chart.CompareSymbol{{Name: "ETH", Closes: []float64{20, 21, 23}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CompareLegend(c, &symbols) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) + core.Use(c, core.Settings{}) + chart.CompareLegend(c, &symbols) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Compare: &symbols}) } ui.Render(view, 640, 400, 1)` const mktCode159 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) - candles := []mujica.Candle{ + candles := []chart.Candle{ {Time: at, Open: 100, High: 103, Low: 99, Close: 102, Volume: 1200}, {Time: at.Add(time.Hour), Open: 102, High: 104, Low: 100, Close: 101, Volume: 1600}, {Time: at.Add(2 * time.Hour), Open: 101, High: 105, Low: 100, Close: 104, Volume: 900}, } - var window mujica.MarketView - alerts := []mujica.PriceAlertLine{{ID: "take-profit", Price: 105}} + var window chart.MarketView + alerts := []chart.PriceAlertLine{{ID: "take-profit", Price: 105}} triggered := "" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandlestickChart(c, candles, &window, mujica.CandlestickOptions{Frame: mujica.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert mujica.PriceAlertLine, last mujica.Candle) { + core.Use(c, core.Settings{}) + chart.CandlestickChart(c, candles, &window, chart.CandlestickOptions{Frame: chart.ChartFrame{Label: "BTC", Height: 200}, Alerts: &alerts, OnAlert: func(alert chart.PriceAlertLine, last chart.Candle) { triggered = alert.ID }}) ui.Text(c, triggered) @@ -213,7 +213,7 @@ const mktCode159 = ` at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) ui.Render(view, 640, 400, 1)` const mktCode160 = ` view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CandleCountdown(c, "1h", mujica.CandleCountdownOptions{Location: time.UTC}) + core.Use(c, core.Settings{}) + chart.CandleCountdown(c, "1h", chart.CandleCountdownOptions{Location: time.UTC}) } ui.Render(view, 640, 400, 1)` diff --git a/examples/gallery/pages_mktchart_demo.go b/examples/gallery/pages_mktchart_demo.go index f0fc07b..e51a93c 100644 --- a/examples/gallery/pages_mktchart_demo.go +++ b/examples/gallery/pages_mktchart_demo.go @@ -5,7 +5,9 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func mktDemo(c *ui.Context, log func(string), id string) { @@ -21,65 +23,65 @@ func mktDemo(c *ui.Context, log func(string), id string) { bins = nil } ui.Column(c).Disabled(s.disabled).Gap(12).FillWidth().Children(func() { - opts := mujica.CandlestickOptions{Frame: frame, Type: s.chartType} - chart := func() { mktLog(log, id, mujica.CandlestickChart(c, candles, &s.window, opts)) } + opts := chart.CandlestickOptions{Frame: frame, Type: s.chartType} + draw := func() { mktLog(log, id, chart.CandlestickChart(c, candles, &s.window, opts)) } switch id { case "141": - if mujica.ChartTypeSwitcher(c, &s.chartType, mujica.ChartTypeSwitcherOptions{}).Changed() { + if chart.ChartTypeSwitcher(c, &s.chartType, chart.ChartTypeSwitcherOptions{}).Changed() { log(fmt.Sprintf("Chart type: %d", s.chartType)) } opts.Type = s.chartType - chart() + draw() case "142": - mktLog(log, id, mujica.OHLCChart(c, candles, &s.window, opts)) + mktLog(log, id, chart.OHLCChart(c, candles, &s.window, opts)) case "143": before := s.raw - mktLog(log, id, mujica.HeikinAshiChart(c, candles, &s.window, &s.raw, opts)) + mktLog(log, id, chart.HeikinAshiChart(c, candles, &s.window, &s.raw, opts)) if before != s.raw { log(fmt.Sprintf("Raw: %v", s.raw)) } case "144": - if mujica.NumberInput(c, &s.previous, mujica.NumberInputOptions{Min: 80, Max: 120, Step: 0.5, Label: "Previous close"}).Changed() { + if input.NumberInput(c, &s.previous, input.NumberInputOptions{Min: 80, Max: 120, Step: 0.5, Label: "Previous close"}).Changed() { log(fmt.Sprintf("Previous close: %.2f", s.previous)) } - if mujica.Checkbox(c, &s.extended, "Extended sessions", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.extended, "Extended sessions", input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("Extended sessions: %v", s.extended)) } - o := mujica.LineQuoteChartOptions{Frame: frame, PrevClose: s.previous} + o := chart.LineQuoteChartOptions{Frame: frame, PrevClose: s.previous} if s.extended { o.RegularOpen = s.points[0].Time.Add(90 * time.Minute) o.RegularClose = s.points[0].Time.Add(8 * time.Hour) } - mktLog(log, id, mujica.LineQuoteChart(c, points, o)) + mktLog(log, id, chart.LineQuoteChart(c, points, o)) case "145": opts.Frame.Height = 150 - chart() + draw() frame.Label = "Volume" frame.Height = 100 - mktLog(log, id, mujica.VolumeChart(c, candles, &s.window, mujica.VolumeChartOptions{Frame: frame})) + mktLog(log, id, chart.VolumeChart(c, candles, &s.window, chart.VolumeChartOptions{Frame: frame})) case "146": - mktLog(log, id, mujica.DepthChart(c, bids, asks, mujica.DepthChartOptions{Frame: frame})) + mktLog(log, id, chart.DepthChart(c, bids, asks, chart.DepthChartOptions{Frame: frame})) case "147": - if mujica.NumberInput(c, &s.fraction, mujica.NumberInputOptions{Min: 0.1, Max: 1, Step: 0.1, Label: "Value area fraction"}).Changed() { + if input.NumberInput(c, &s.fraction, input.NumberInputOptions{Min: 0.1, Max: 1, Step: 0.1, Label: "Value area fraction"}).Changed() { log(fmt.Sprintf("Value area: %.1f", s.fraction)) } - mktLog(log, id, mujica.VolumeProfile(c, bins, mujica.VolumeProfileOptions{Frame: frame, ValueArea: s.fraction})) + mktLog(log, id, chart.VolumeProfile(c, bins, chart.VolumeProfileOptions{Frame: frame, ValueArea: s.fraction})) case "148": - if mujica.MultiChartLayout(c, &s.layout, mujica.MultiChartLayoutOptions{Height: 420}, func(i int, cell *mujica.ChartCell) { - if mujica.TextInput(c, &cell.Symbol, mujica.TextInputOptions{Label: fmt.Sprintf("Symbol %d", i+1)}).Changed() { + if chart.MultiChartLayout(c, &s.layout, chart.MultiChartLayoutOptions{Height: 420}, func(i int, cell *chart.ChartCell) { + if input.TextInput(c, &cell.Symbol, input.TextInputOptions{Label: fmt.Sprintf("Symbol %d", i+1)}).Changed() { log(fmt.Sprintf("Cell %d: %s", i+1, cell.Symbol)) } - items := make([]mujica.SelectOption[mujica.Interval], len(mujica.Intervals)) - for j, iv := range mujica.Intervals { - items[j] = mujica.SelectOption[mujica.Interval]{Value: iv, Label: string(iv)} + items := make([]input.SelectOption[chart.Interval], len(chart.Intervals)) + for j, iv := range chart.Intervals { + items[j] = input.SelectOption[chart.Interval]{Value: iv, Label: string(iv)} } - if mujica.Select(c, &cell.Interval, items, mujica.SelectOptions{Label: fmt.Sprintf("Interval %d", i+1)}).Changed() { + if input.Select(c, &cell.Interval, items, input.SelectOptions{Label: fmt.Sprintf("Interval %d", i+1)}).Changed() { log(fmt.Sprintf("Cell %d: %s", i+1, cell.Interval)) } f := frame f.Label = cell.Symbol f.Height = 70 - mktLog(log, fmt.Sprint(i), mujica.CandlestickChart(c, candles, &cell.View, mujica.CandlestickOptions{Frame: f})) + mktLog(log, fmt.Sprint(i), chart.CandlestickChart(c, candles, &cell.View, chart.CandlestickOptions{Frame: f})) }).Changed() { log(fmt.Sprintf("Layout: %d, active %d", s.layout.Count, s.layout.Active)) } @@ -98,41 +100,41 @@ func mktDemo(c *ui.Context, log func(string), id string) { o.Frame.Label = chartID o.Sync = &s.sync o.SyncID = chartID - mktLog(log, chartID, mujica.CandlestickChart(c, candles, v, o)) + mktLog(log, chartID, chart.CandlestickChart(c, candles, v, o)) }) } case "150": - if mujica.TimeRangeSelector(c, &s.rangeValue, mujica.TimeRangeSelectorOptions{}).Changed() { + if chart.TimeRangeSelector(c, &s.rangeValue, chart.TimeRangeSelectorOptions{}).Changed() { log(fmt.Sprintf("Range: %d", s.rangeValue.Preset)) } from, to := s.rangeValue.Bounds(c.Now(), time.UTC) ui.Text(c, from.Format("2006-01-02")+" — "+to.Format("2006-01-02")) case "151": before := fmt.Sprint(s.favorites) - if mujica.IntervalSelector(c, &s.interval, &s.favorites, mujica.IntervalSelectorOptions{}).Changed() { + if chart.IntervalSelector(c, &s.interval, &s.favorites, chart.IntervalSelectorOptions{}).Changed() { log("Interval: " + string(s.interval)) } if before != fmt.Sprint(s.favorites) { log("Favorites: " + fmt.Sprint(s.favorites)) } - mujica.CandleCountdown(c, s.interval, mujica.CandleCountdownOptions{}) + chart.CandleCountdown(c, s.interval, chart.CandleCountdownOptions{}) case "152": - if mujica.ChartTypeSwitcher(c, &s.chartType, mujica.ChartTypeSwitcherOptions{}).Changed() { + if chart.ChartTypeSwitcher(c, &s.chartType, chart.ChartTypeSwitcherOptions{}).Changed() { log(fmt.Sprintf("Chart type: %d", s.chartType)) } opts.Type = s.chartType - chart() + draw() case "153": - if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() { + if chart.IndicatorSelector(c, &s.overlays, &s.panes, chart.IndicatorSelectorOptions{}).Changed() { log("Overlays changed") } opts.Overlays = s.overlays - chart() + draw() case "154": - if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() { + if chart.IndicatorSelector(c, &s.overlays, &s.panes, chart.IndicatorSelectorOptions{}).Changed() { log("Indicators changed") } - r := mujica.IndicatorPane(c, candles, &s.window, &s.panes, mujica.IndicatorPaneOptions{Label: "BTC", Height: 320, Status: frame.Status, Error: frame.Error}) + r := chart.IndicatorPane(c, candles, &s.window, &s.panes, chart.IndicatorPaneOptions{Label: "BTC", Height: 320, Status: frame.Status, Error: frame.Error}) if r.PointChanged { log(fmt.Sprintf("Pane %d: point %d", r.Pane, r.Point)) } @@ -143,43 +145,43 @@ func mktDemo(c *ui.Context, log func(string), id string) { log("Panes resized") } case "155": - if mujica.IndicatorSelector(c, &s.overlays, &s.panes, mujica.IndicatorSelectorOptions{}).Changed() { + if chart.IndicatorSelector(c, &s.overlays, &s.panes, chart.IndicatorSelectorOptions{}).Changed() { log("Indicators changed") } opts.Overlays = s.overlays - chart() + draw() case "156", "157": - if mujica.TextInput(c, &s.drawings.Text, mujica.TextInputOptions{Label: "Annotation text"}).Changed() { + if input.TextInput(c, &s.drawings.Text, input.TextInputOptions{Label: "Annotation text"}).Changed() { log("Annotation text changed") } - if mujica.DrawingToolbar(c, &s.drawings, mujica.DrawingToolbarOptions{Horizontal: true}).Changed() { + if chart.DrawingToolbar(c, &s.drawings, chart.DrawingToolbarOptions{Horizontal: true}).Changed() { log(fmt.Sprintf("Tool: %d, locked: %v", s.drawings.Tool, s.drawings.Locked)) } before := fmt.Sprint(s.drawings.Items) opts.Drawings = &s.drawings - chart() + draw() if before != fmt.Sprint(s.drawings.Items) { log(fmt.Sprintf("Drawings: %d", len(s.drawings.Items))) } case "158": - if mujica.CompareLegend(c, &s.compare).Changed() { + if chart.CompareLegend(c, &s.compare).Changed() { log("Compared symbol removed") } - if mujica.Button(c, "Restore ETH", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restore ETH", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.compare = mktInitial().compare log("Restored ETH") } if len(candles) > 0 { opts.Compare = &s.compare } - chart() + draw() case "159": before := s.alerts[0].Price opts.Alerts = &s.alerts - opts.OnAlert = func(a mujica.PriceAlertLine, last mujica.Candle) { + opts.OnAlert = func(a chart.PriceAlertLine, last chart.Candle) { log(fmt.Sprintf("Alert %s crossed %.2f", a.ID, last.Close)) } - if mujica.Button(c, "Append crossing candle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Append crossing candle", input.ButtonOptions{Variant: input.Secondary}).Clicked() { last := s.candles[len(s.candles)-1] next := last next.Time = last.Time.Add(time.Hour) @@ -194,19 +196,19 @@ func mktDemo(c *ui.Context, log func(string), id string) { candles = s.candles log("Appended candle") } - chart() + draw() if before != s.alerts[0].Price { log(fmt.Sprintf("Alert price: %.2f", s.alerts[0].Price)) } case "160": - if mujica.IntervalSelector(c, &s.interval, &s.favorites, mujica.IntervalSelectorOptions{}).Changed() { + if chart.IntervalSelector(c, &s.interval, &s.favorites, chart.IntervalSelectorOptions{}).Changed() { log("Interval: " + string(s.interval)) } - if mujica.Checkbox(c, &s.hidden, "Hide countdown", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.hidden, "Hide countdown", input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("Hidden: %v", s.hidden)) } if !s.hidden { - mujica.CandleCountdown(c, s.interval, mujica.CandleCountdownOptions{}) + chart.CandleCountdown(c, s.interval, chart.CandleCountdownOptions{}) } } }) diff --git a/examples/gallery/pages_mktchart_state.go b/examples/gallery/pages_mktchart_state.go index 8cd822d..08d069a 100644 --- a/examples/gallery/pages_mktchart_state.go +++ b/examples/gallery/pages_mktchart_state.go @@ -6,89 +6,93 @@ import ( "time" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type mktDemoState struct { - candles []mujica.Candle - points []mujica.QuotePoint - bids, asks []mujica.DepthLevel - bins []mujica.ProfileBin - window mujica.MarketView - second mujica.MarketView - sync mujica.ChartSync - layout mujica.MultiChartState - interval mujica.Interval - favorites []mujica.Interval - rangeValue mujica.MarketRange - chartType mujica.ChartType - overlays []mujica.IndicatorOverlay - panes mujica.IndicatorPanes - drawings mujica.DrawingTools - compare []mujica.CompareSymbol - alerts []mujica.PriceAlertLine + candles []chart.Candle + points []chart.QuotePoint + bids, asks []chart.DepthLevel + bins []chart.ProfileBin + window chart.MarketView + second chart.MarketView + sync chart.ChartSync + layout chart.MultiChartState + interval chart.Interval + favorites []chart.Interval + rangeValue chart.MarketRange + chartType chart.ChartType + overlays []chart.IndicatorOverlay + panes chart.IndicatorPanes + drawings chart.DrawingTools + compare []chart.CompareSymbol + alerts []chart.PriceAlertLine status string disabled, raw, redUp, hidden, extended bool previous, fraction float64 } func mktInitial() mktDemoState { - s := mktDemoState{status: "Ready", interval: "1h", favorites: []mujica.Interval{"1m", "1h", "1D"}, previous: 100, fraction: 0.7, extended: true} + s := mktDemoState{status: "Ready", interval: "1h", favorites: []chart.Interval{"1m", "1h", "1D"}, previous: 100, fraction: 0.7, extended: true} start := time.Date(2026, 10, 5, 8, 0, 0, 0, time.UTC) price := 100.0 compare := make([]float64, 180) for i := range 180 { closeAt := price + math.Sin(float64(i)*0.7)*1.5 + math.Cos(float64(i)*0.13)*0.8 - s.candles = append(s.candles, mujica.Candle{Time: start.Add(time.Duration(i) * time.Hour), Open: price, High: max(price, closeAt) + 0.8, Low: min(price, closeAt) - 0.7, Close: closeAt, Volume: 1000 + float64(i%17)*130}) - s.points = append(s.points, mujica.QuotePoint{Time: start.Add(time.Duration(i) * 5 * time.Minute), Price: 100 + 2*math.Sin(float64(i)/10), Volume: 100 + float64(i%7)*20}) + s.candles = append(s.candles, chart.Candle{Time: start.Add(time.Duration(i) * time.Hour), Open: price, High: max(price, closeAt) + 0.8, Low: min(price, closeAt) - 0.7, Close: closeAt, Volume: 1000 + float64(i%17)*130}) + s.points = append(s.points, chart.QuotePoint{Time: start.Add(time.Duration(i) * 5 * time.Minute), Price: 100 + 2*math.Sin(float64(i)/10), Volume: 100 + float64(i%7)*20}) compare[i] = 20 + float64(i)*0.08 price = closeAt } for i := range 40 { - s.bids = append(s.bids, mujica.DepthLevel{Price: 99.9 - float64(i)*0.1, Size: float64(10 + i*3)}) - s.asks = append(s.asks, mujica.DepthLevel{Price: 100.1 + float64(i)*0.1, Size: float64(8 + i*4)}) + s.bids = append(s.bids, chart.DepthLevel{Price: 99.9 - float64(i)*0.1, Size: float64(10 + i*3)}) + s.asks = append(s.asks, chart.DepthLevel{Price: 100.1 + float64(i)*0.1, Size: float64(8 + i*4)}) } - s.bins = mujica.VolumeProfileOf(s.candles, 24) - s.compare = []mujica.CompareSymbol{{Name: "ETH", Closes: compare}} - s.alerts = []mujica.PriceAlertLine{{ID: "target", Price: 104, Label: "Target"}} - s.overlays = []mujica.IndicatorOverlay{{Kind: mujica.IndicatorMA, Params: []float64{10}}} - s.panes = mujica.IndicatorPanes{Panes: []mujica.IndicatorPaneSpec{{Kind: mujica.IndicatorMACD}, {Kind: mujica.IndicatorRSI}}} + s.bins = chart.VolumeProfileOf(s.candles, 24) + s.compare = []chart.CompareSymbol{{Name: "ETH", Closes: compare}} + s.alerts = []chart.PriceAlertLine{{ID: "target", Price: 104, Label: "Target"}} + s.overlays = []chart.IndicatorOverlay{{Kind: chart.IndicatorMA, Params: []float64{10}}} + s.panes = chart.IndicatorPanes{Panes: []chart.IndicatorPaneSpec{{Kind: chart.IndicatorMACD}, {Kind: chart.IndicatorRSI}}} s.layout.Count = 2 for i := range s.layout.Cells { - s.layout.Cells[i] = mujica.ChartCell{Symbol: fmt.Sprintf("Symbol %d", i+1), Interval: "1h"} + s.layout.Cells[i] = chart.ChartCell{Symbol: fmt.Sprintf("Symbol %d", i+1), Interval: "1h"} } return s } -func mktControls(c *ui.Context, s *mktDemoState, log func(string)) mujica.ChartFrame { +func mktControls(c *ui.Context, s *mktDemoState, log func(string)) chart.ChartFrame { ui.Row(c).Gap(12).Wrap().Children(func() { - if mujica.Select(c, &s.status, []mujica.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}, {Value: "Empty", Label: "Empty"}}, mujica.SelectOptions{Label: "Data state"}).Changed() { + if input.Select(c, &s.status, []input.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}, {Value: "Empty", Label: "Empty"}}, input.SelectOptions{Label: "Data state"}).Changed() { log("Data state: " + s.status) } - if mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.disabled, "Disabled", input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("Disabled: %v", s.disabled)) } - if mujica.Checkbox(c, &s.redUp, "Red up", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, &s.redUp, "Red up", input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("Red up: %v", s.redUp)) } }) - trend := mujica.GreenUp + trend := core.GreenUp if s.redUp { - trend = mujica.RedUp + trend = core.RedUp } - mujica.UseTrend(c, trend) - f := mujica.ChartFrame{Label: "BTC", Height: 220} + core.UseTrend(c, trend) + f := chart.ChartFrame{Label: "BTC", Height: 220} switch s.status { case "Loading": - f.Status = mujica.DataLoading + f.Status = data.DataLoading case "Failed": - f.Status = mujica.DataFailed + f.Status = data.DataFailed f.Error = "Archive unavailable" } return f } -func mktLog(log func(string), name string, r mujica.ChartResult) { +func mktLog(log func(string), name string, r chart.ChartResult) { if r.Changed() { log(fmt.Sprintf("%s: point %d", name, r.Point)) } diff --git a/examples/gallery/pages_mktchart_test.go b/examples/gallery/pages_mktchart_test.go index af5ec52..c31fa94 100644 --- a/examples/gallery/pages_mktchart_test.go +++ b/examples/gallery/pages_mktchart_test.go @@ -5,14 +5,15 @@ import ( "testing" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func mktDemoTester(t *testing.T, id string) (*ui.Tester, *[]string) { t.Helper() events := &[]string{} tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Padding(12).Children(func() { mktDemo(c, func(s string) { *events = append(*events, s) }, id) }) }, 900, 800) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/examples/gallery/pages_motion.go b/examples/gallery/pages_motion.go index 7a0764f..47ef053 100644 --- a/examples/gallery/pages_motion.go +++ b/examples/gallery/pages_motion.go @@ -1,6 +1,8 @@ package main -import mujica "mujicaui" +import ( + "github.com/ZacharyZhang-NY/MujicaUI/feedback" +) var motionPages = []page{ { @@ -8,8 +10,8 @@ var motionPages = []page{ Demo: motionInterruptDemo, Code: `running, stopping := true, false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.InterruptButton(c, running, mujica.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) + core.Use(c, core.Settings{}) + r := feedback.InterruptButton(c, running, feedback.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) if r.Changed() { stopping = true // cancel the run, then clear running and stopping } @@ -20,10 +22,10 @@ ui.Render(view, 400, 300, 1)`, { ID: "282", Name: "TokenUsageChart", ZhName: "Token 用量图 输入输出", Category: "Agent Ops & Motion", Demo: motionTokenUsageDemo, - Code: `usage := []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} + Code: `usage := []feedback.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{}) + core.Use(c, core.Settings{}) + feedback.TokenUsageChart(c, usage, feedback.TokenUsageChartOptions{}) } ui.Render(view, 400, 300, 1)`, Keys: "Tab focuses the chart; Left/Right move between bars or points and the readout is announced.", @@ -31,14 +33,14 @@ ui.Render(view, 400, 300, 1)`, { ID: "283", Name: "CostBreakdown", ZhName: "费用分解 成本 合计", Category: "Agent Ops & Motion", Demo: motionCostDemo, - Code: `var state mujica.CostBreakdownState -items := []mujica.CostItem{ + Code: `var state feedback.CostBreakdownState +items := []feedback.CostItem{ {Model: "opus", Tool: "search", InputTokens: 1000, OutputTokens: 200, Cost: 0.30}, {Model: "haiku", Tool: "search", InputTokens: 4000, OutputTokens: 500, Cost: 0.05}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CostBreakdown(c, &state, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostByModel}).Element.Height(260) + core.Use(c, core.Settings{}) + feedback.CostBreakdown(c, &state, items, feedback.CostBreakdownOptions{GroupBy: feedback.CostByModel}).Element.Height(260) } ui.Render(view, 400, 300, 1)`, Keys: "Up/Down choose a group; column headers sort with Enter; the column menu takes Tab.", @@ -46,15 +48,15 @@ ui.Render(view, 400, 300, 1)`, { ID: "284", Name: "TraceViewer", ZhName: "调用链 追踪 跨度 时间轴", Category: "Agent Ops & Motion", Demo: motionTraceDemo, - Code: `var state mujica.TraceViewerState -spans := []mujica.TraceSpan{ + Code: `var state feedback.TraceViewerState +spans := []feedback.TraceSpan{ {ID: "run", Name: "agent.run", End: 900 * time.Millisecond}, {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, - Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}}}, + Attrs: []feedback.TraceAttr{{Key: "model", Value: "opus"}}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TraceViewer(c, &state, spans, mujica.TraceViewerOptions{}).Element.Height(260) + core.Use(c, core.Settings{}) + feedback.TraceViewer(c, &state, spans, feedback.TraceViewerOptions{}).Element.Height(260) } ui.Render(view, 400, 300, 1)`, Keys: "Up/Down choose a span, Right opens and Left closes it; typing jumps by name.", @@ -62,11 +64,11 @@ ui.Render(view, 400, 300, 1)`, { ID: "285", Name: "EvalResultTable", ZhName: "评测结果 得分 通过失败", Category: "Agent Ops & Motion", Demo: motionEvalDemo, - Code: `var state mujica.DataTableState[string] -cases := []mujica.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} + Code: `var state data.DataTableState[string] +cases := []feedback.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EvalResultTable(c, &state, cases, mujica.EvalResultTableOptions{}) + core.Use(c, core.Settings{}) + r := feedback.EvalResultTable(c, &state, cases, feedback.EvalResultTableOptions{}) r.Table.Element.Height(260) if id, ok := r.Diff(); ok { _ = id // open expected and actual output side by side @@ -78,12 +80,12 @@ ui.Render(view, 400, 300, 1)`, { ID: "286", Name: "ToolRegistryPanel", ZhName: "工具注册 工具列表 启停", Category: "Agent Ops & Motion", Demo: motionToolsDemo, - Code: `var state mujica.ListState[string] -tools := []mujica.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, - Params: []mujica.ToolParam{{Name: "query", Type: "string", Required: true}}}} + Code: `var state data.ListState[string] +tools := []feedback.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, + Params: []feedback.ToolParam{{Name: "query", Type: "string", Required: true}}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.ToolRegistryPanel(c, &state, tools, mujica.ToolRegistryPanelOptions{}) + core.Use(c, core.Settings{}) + r := feedback.ToolRegistryPanel(c, &state, tools, feedback.ToolRegistryPanelOptions{}) r.Element.Height(260) if name, on, ok := r.Toggled(); ok && name == tools[0].Name { tools[0].Enabled = on @@ -94,50 +96,50 @@ ui.Render(view, 400, 300, 1)`, }, { ID: "287", Name: "DotsLoader", ZhName: "三点加载 加载中", Category: "Agent Ops & Motion", - Demo: motionLoaderDemo("287", mujica.DotsLoader), + Demo: motionLoaderDemo("287", feedback.DotsLoader), Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Thinking", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.DotsLoader(c, feedback.LoaderOptions{Label: "Thinking", ShowLabel: true}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as progress.", }, { ID: "288", Name: "BarsLoader", ZhName: "条形加载", Category: "Agent Ops & Motion", - Demo: motionLoaderDemo("288", mujica.BarsLoader), + Demo: motionLoaderDemo("288", feedback.BarsLoader), Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BarsLoader(c, mujica.LoaderOptions{Size: 20, Label: "Indexing"}) + core.Use(c, core.Settings{}) + feedback.BarsLoader(c, feedback.LoaderOptions{Size: 20, Label: "Indexing"}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as progress.", }, { ID: "289", Name: "PulseLoader", ZhName: "呼吸加载 脉冲", Category: "Agent Ops & Motion", - Demo: motionLoaderDemo("289", mujica.PulseLoader), + Demo: motionLoaderDemo("289", feedback.PulseLoader), Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PulseLoader(c, mujica.LoaderOptions{Label: "Listening", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.PulseLoader(c, feedback.LoaderOptions{Label: "Listening", ShowLabel: true}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as progress.", }, { ID: "290", Name: "OrbitLoader", ZhName: "环绕加载 菱形", Category: "Agent Ops & Motion", - Demo: motionLoaderDemo("290", mujica.OrbitLoader), + Demo: motionLoaderDemo("290", feedback.OrbitLoader), Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.OrbitLoader(c, mujica.LoaderOptions{Size: 24, Label: "Connecting"}) + core.Use(c, core.Settings{}) + feedback.OrbitLoader(c, feedback.LoaderOptions{Size: 24, Label: "Connecting"}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as progress.", }, { ID: "291", Name: "WaveLoader", ZhName: "波形加载", Category: "Agent Ops & Motion", - Demo: motionLoaderDemo("291", mujica.WaveLoader), + Demo: motionLoaderDemo("291", feedback.WaveLoader), Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WaveLoader(c, mujica.LoaderOptions{Label: "Speaking", ShowLabel: true}) + core.Use(c, core.Settings{}) + feedback.WaveLoader(c, feedback.LoaderOptions{Label: "Speaking", ShowLabel: true}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as progress.", @@ -147,8 +149,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionShimmerDemo, Code: `loading := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Shimmer(c, loading, mujica.ShimmerOptions{Height: 48}, func() { + core.Use(c, core.Settings{}) + feedback.Shimmer(c, loading, feedback.ShimmerOptions{Height: 48}, func() { ui.Text(c, "Report ready") }) } @@ -159,8 +161,8 @@ ui.Render(view, 400, 300, 1)`, ID: "293", Name: "CountUp", ZhName: "数字过渡 计数", Category: "Agent Ops & Motion", Demo: motionCountUpDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.CountUp(c, 12840, mujica.CountUpOptions{Duration: 800 * time.Millisecond}) + core.Use(c, core.Settings{}) + feedback.CountUp(c, 12840, feedback.CountUpOptions{Duration: 800 * time.Millisecond}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; the final value is read out.", @@ -169,8 +171,8 @@ ui.Render(view, 400, 300, 1)`, ID: "294", Name: "Typewriter", ZhName: "打字机 逐字显示", Category: "Agent Ops & Motion", Demo: motionTypewriterDemo, Code: `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Typewriter(c, "The court assembles at dusk.", mujica.TypewriterOptions{Speed: 30, Skippable: true}) + core.Use(c, core.Settings{}) + r := feedback.Typewriter(c, "The court assembles at dusk.", feedback.TypewriterOptions{Speed: 30, Skippable: true}) if r.Changed() { // show the next message } @@ -183,8 +185,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionShimmerTextDemo, Code: `thinking := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ShimmerText(c, "Thinking…", mujica.ShimmerTextOptions{Active: thinking}) + core.Use(c, core.Settings{}) + feedback.ShimmerText(c, "Thinking…", feedback.ShimmerTextOptions{Active: thinking}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; read as a status.", @@ -194,8 +196,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionStaggerDemo, Code: `guests := []string{"Sakiko", "Uika", "Mutsumi"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Stagger(c, len(guests), mujica.StaggerOptions{}, func(i int) { + core.Use(c, core.Settings{}) + feedback.Stagger(c, len(guests), feedback.StaggerOptions{}, func(i int) { ui.Text(c, guests[i]) }) } @@ -207,8 +209,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionReorderDemo, Code: `order := []string{"Sakiko", "Uika", "Mutsumi"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ReorderTransition(c, order, mujica.ReorderTransitionOptions{Gap: 4}, func(name string) { + core.Use(c, core.Settings{}) + feedback.ReorderTransition(c, order, feedback.ReorderTransitionOptions{Gap: 4}, func(name string) { ui.Text(c, name) }) } @@ -220,8 +222,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionLayoutDemo, Code: `open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: true}, func() { + core.Use(c, core.Settings{}) + feedback.LayoutTransition(c, feedback.LayoutTransitionOptions{Clip: true}, func() { ui.Text(c, "Program") if open { ui.Text(c, "Overture · Masquerade · Finale") @@ -236,8 +238,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionPresenceDemo, Code: `show := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Presence(c, show, mujica.PresenceOptions{Kind: mujica.PresenceSlide}, func() { + core.Use(c, core.Settings{}) + feedback.Presence(c, show, feedback.PresenceOptions{Kind: feedback.PresenceSlide}, func() { ui.Text(c, "Saved") }) } @@ -249,8 +251,8 @@ ui.Render(view, 400, 300, 1)`, Demo: motionBlinkDemo, Code: `price := 101.5 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BlinkHighlight(c, price, mujica.BlinkHighlightOptions{}) + core.Use(c, core.Settings{}) + feedback.BlinkHighlight(c, price, feedback.BlinkHighlightOptions{}) } ui.Render(view, 400, 300, 1)`, Keys: "Not focusable; the value and direction are read out.", diff --git a/examples/gallery/pages_motion_fx.go b/examples/gallery/pages_motion_fx.go index 43348ed..e8653df 100644 --- a/examples/gallery/pages_motion_fx.go +++ b/examples/gallery/pages_motion_fx.go @@ -7,11 +7,14 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) // motionLoaderDemo shows a loader while loading. -func motionLoaderDemo(id string, loader func(c *ui.Context, o mujica.LoaderOptions) *ui.Element) func(c *ui.Context, log func(string)) { +func motionLoaderDemo(id string, loader func(c *ui.Context, o feedback.LoaderOptions) *ui.Element) func(c *ui.Context, log func(string)) { return func(c *ui.Context, log func(string)) { type state struct { loading, label bool @@ -28,8 +31,8 @@ func motionLoaderDemo(id string, loader func(c *ui.Context, o mujica.LoaderOptio ui.Text(c, "Done.") return } - loader(c, mujica.LoaderOptions{Size: float32(s.size), Label: "Thinking", ShowLabel: s.label}) - loader(c, mujica.LoaderOptions{Size: float32(s.size), Label: "Indexing", Color: mujica.Tokens(c).Info}) + loader(c, feedback.LoaderOptions{Size: float32(s.size), Label: "Thinking", ShowLabel: s.label}) + loader(c, feedback.LoaderOptions{Size: float32(s.size), Label: "Indexing", Color: core.Tokens(c).Info}) }) }) } @@ -41,11 +44,11 @@ func motionShimmerDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.loading, "Loading", log) }, func() { - mujica.Shimmer(c, s.loading, mujica.ShimmerOptions{Height: 56, Label: "Loading report"}, func() { + feedback.Shimmer(c, s.loading, feedback.ShimmerOptions{Height: 56, Label: "Loading report"}, func() { ui.Text(c, "Quarterly report: 12 chapters, 340 pages.") }) - mujica.Shimmer(c, s.loading, mujica.ShimmerOptions{Width: 240, Height: 20}, func() { - ui.Text(c, "Updated today").TextColor(mujica.Tokens(c).TextMuted) + feedback.Shimmer(c, s.loading, feedback.ShimmerOptions{Width: 240, Height: 20}, func() { + ui.Text(c, "Updated today").TextColor(core.Tokens(c).TextMuted) }) }) } @@ -56,17 +59,17 @@ func motionCountUpDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationSlider(c, &s.duration, "Duration (ms)", 120, 1200, log) navigationSlider(c, &s.decimals, "Decimals", 0, 4, log) - if mujica.Button(c, "Add 500", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Add 500", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.value += 500 log(fmt.Sprintf("Value %.0f", s.value)) } - if mujica.Button(c, "Random", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Random", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.value = float64(rand.Intn(100000)) log(fmt.Sprintf("Value %.0f", s.value)) } }, func() { - mujica.CountUp(c, s.value, mujica.CountUpOptions{Duration: time.Duration(s.duration) * time.Millisecond, Decimals: int(s.decimals)}).FontSize(mujica.FontSize(c, 32)) - mujica.CountUp(c, s.value/1000, mujica.CountUpOptions{Decimals: 2, Duration: time.Second}) + feedback.CountUp(c, s.value, feedback.CountUpOptions{Duration: time.Duration(s.duration) * time.Millisecond, Decimals: int(s.decimals)}).FontSize(core.FontSize(c, 32)) + feedback.CountUp(c, s.value/1000, feedback.CountUpOptions{Decimals: 2, Duration: time.Second}) }) } @@ -79,18 +82,18 @@ func motionTypewriterDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "294", func() state { return state{text: "The court assembles at dusk.", speed: 30} }) navigationDemo(c, func() { navigationSlider(c, &s.speed, "Speed", 5, 120, log) - if mujica.Button(c, "Append", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Append", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.text += " The music begins." log("Appended") } - if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Restart", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.text = "A new letter arrives from the duchess." s.round++ log("Restarted") } }, func() { ui.Column(c).Key(s.round).Children(func() { - if mujica.Typewriter(c, s.text, mujica.TypewriterOptions{Speed: s.speed, Skippable: true}).Changed() { + if feedback.Typewriter(c, s.text, feedback.TypewriterOptions{Speed: s.speed, Skippable: true}).Changed() { log("Finished") } }) @@ -103,8 +106,8 @@ func motionShimmerTextDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.active, "Thinking", log) }, func() { - mujica.ShimmerText(c, "Thinking about the program…", mujica.ShimmerTextOptions{Active: s.active}) - mujica.ShimmerText(c, "思考中…", mujica.ShimmerTextOptions{Active: s.active}) + feedback.ShimmerText(c, "Thinking about the program…", feedback.ShimmerTextOptions{Active: s.active}) + feedback.ShimmerText(c, "思考中…", feedback.ShimmerTextOptions{Active: s.active}) }) } @@ -119,14 +122,14 @@ func motionStaggerDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationSlider(c, &s.interval, "Interval (ms)", 20, 300, log) navigationCheck(c, &s.horizontal, "Horizontal", log) - if mujica.Button(c, "Replay", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Replay", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.round++ log(fmt.Sprintf("Replay %d", s.round)) } }, func() { ui.Column(c).Key(s.round).Children(func() { - mujica.Stagger(c, len(guests), mujica.StaggerOptions{Interval: time.Duration(s.interval) * time.Millisecond, Horizontal: s.horizontal}, func(i int) { - ui.Text(c, guests[i]+" arrives").Padding(6, 10).Background(mujica.Tokens(c).Surface).Radius(4).Margin(0, 0, 4, 0) + feedback.Stagger(c, len(guests), feedback.StaggerOptions{Interval: time.Duration(s.interval) * time.Millisecond, Horizontal: s.horizontal}, func(i int) { + ui.Text(c, guests[i]+" arrives").Padding(6, 10).Background(core.Tokens(c).Surface).Radius(4).Margin(0, 0, 4, 0) }) }) }) @@ -140,13 +143,13 @@ func motionReorderDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "297", func() state { return state{order: []string{"Overture", "Masquerade", "Interlude", "Finale"}, gap: 4} }) navigationDemo(c, func() { navigationSlider(c, &s.gap, "Gap", 0, 24, log) - if mujica.Button(c, "Shuffle", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Shuffle", input.ButtonOptions{Variant: input.Secondary}).Clicked() { next := append([]string(nil), s.order...) rand.Shuffle(len(next), func(i, j int) { next[i], next[j] = next[j], next[i] }) s.order = next log(fmt.Sprintf("Order %v", s.order)) } - if mujica.Button(c, "Reverse", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Reverse", input.ButtonOptions{Variant: input.Secondary}).Clicked() { next := make([]string, len(s.order)) for i, v := range s.order { next[len(next)-1-i] = v @@ -156,8 +159,8 @@ func motionReorderDemo(c *ui.Context, log func(string)) { } }, func() { ui.Column(c).Width(280).Children(func() { - mujica.ReorderTransition(c, s.order, mujica.ReorderTransitionOptions{Gap: float32(s.gap)}, func(name string) { - ui.Text(c, name).Padding(8, 12).Background(mujica.Tokens(c).Surface).Radius(4) + feedback.ReorderTransition(c, s.order, feedback.ReorderTransitionOptions{Gap: float32(s.gap)}, func(name string) { + ui.Text(c, name).Padding(8, 12).Background(core.Tokens(c).Surface).Radius(4) }) }) }) @@ -171,8 +174,8 @@ func motionLayoutDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.clip, "Clip", log) }, func() { ui.Column(c).AlignItems(ui.Start).Children(func() { - mujica.LayoutTransition(c, mujica.LayoutTransitionOptions{Clip: s.clip}, func() { - ui.Column(c).Padding(12).Gap(6).Background(mujica.Tokens(c).Surface).Radius(4).Children(func() { + feedback.LayoutTransition(c, feedback.LayoutTransitionOptions{Clip: s.clip}, func() { + ui.Column(c).Padding(12).Gap(6).Background(core.Tokens(c).Surface).Radius(4).Children(func() { ui.Text(c, "Program").FontWeight(600) if s.open { for _, p := range []string{"Overture", "Masquerade", "Interlude", "Finale"} { @@ -193,15 +196,15 @@ func motionPresenceDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "299", func() state { return state{show: true} }) navigationDemo(c, func() { navigationCheck(c, &s.show, "Show", log) - if mujica.SegmentedControl(c, &s.kind, []mujica.Segment{{Label: "Fade"}, {Label: "Slide"}, {Label: "Collapse"}}, mujica.SegmentedControlOptions{Label: "Kind"}).Changed() { + if navigation.SegmentedControl(c, &s.kind, []navigation.Segment{{Label: "Fade"}, {Label: "Slide"}, {Label: "Collapse"}}, navigation.SegmentedControlOptions{Label: "Kind"}).Changed() { log(fmt.Sprintf("Kind %d", s.kind)) } }, func() { ui.Column(c).Width(320).Children(func() { - mujica.Presence(c, s.show, mujica.PresenceOptions{Kind: mujica.PresenceKind(s.kind)}, func() { - ui.Text(c, "Your changes were saved.").Padding(10, 12).Background(mujica.Tokens(c).Surface).Radius(4) + feedback.Presence(c, s.show, feedback.PresenceOptions{Kind: feedback.PresenceKind(s.kind)}, func() { + ui.Text(c, "Your changes were saved.").Padding(10, 12).Background(core.Tokens(c).Surface).Radius(4) }) - ui.Text(c, "Content below moves with it.").TextColor(mujica.Tokens(c).TextMuted).Padding(8, 0) + ui.Text(c, "Content below moves with it.").TextColor(core.Tokens(c).TextMuted).Padding(8, 0) }) }) } @@ -210,11 +213,11 @@ func motionBlinkDemo(c *ui.Context, log func(string)) { type state struct{ price float64 } s := ui.Local(c.Root(), "300", func() state { return state{price: 101.5} }) navigationDemo(c, func() { - if mujica.Button(c, "Tick", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Tick", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.price += float64(rand.Intn(200)-100) / 50 log(fmt.Sprintf("Price %.2f", s.price)) } }, func() { - mujica.BlinkHighlight(c, s.price, mujica.BlinkHighlightOptions{}) + feedback.BlinkHighlight(c, s.price, feedback.BlinkHighlightOptions{}) }) } diff --git a/examples/gallery/pages_motion_ops.go b/examples/gallery/pages_motion_ops.go index 35ae608..d96dfee 100644 --- a/examples/gallery/pages_motion_ops.go +++ b/examples/gallery/pages_motion_ops.go @@ -6,7 +6,11 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) func motionInterruptDemo(c *ui.Context, log func(string)) { @@ -24,16 +28,16 @@ func motionInterruptDemo(c *ui.Context, log func(string)) { } navigationDemo(c, func() { navigationCheck(c, &s.confirm, "Confirm", log) - if mujica.Button(c, "Start run", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.running}).Clicked() { + if input.Button(c, "Start run", input.ButtonOptions{Variant: input.Secondary, Disabled: s.running}).Clicked() { s.running = true log("Run started") } }, func() { ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() { if s.running && !s.stopping { - mujica.DotsLoader(c, mujica.LoaderOptions{Label: "Agent running", ShowLabel: true}) + feedback.DotsLoader(c, feedback.LoaderOptions{Label: "Agent running", ShowLabel: true}) } - if mujica.InterruptButton(c, s.running, mujica.InterruptButtonOptions{Confirm: s.confirm, Interrupting: s.stopping}).Changed() { + if feedback.InterruptButton(c, s.running, feedback.InterruptButtonOptions{Confirm: s.confirm, Interrupting: s.stopping}).Changed() { log("Interrupt requested") s.stopping, s.until = true, c.Now().Add(1500*time.Millisecond) } @@ -44,8 +48,8 @@ func motionInterruptDemo(c *ui.Context, log func(string)) { func motionTokenUsageDemo(c *ui.Context, log func(string)) { type state struct{ line, loading, empty, failed bool } s := ui.Local(c.Root(), "282", func() state { return state{} }) - usage := *ui.Local(c.Root(), "282.data", func() []mujica.TokenUsage { - return []mujica.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Search", Input: 4800, Output: 650}, + usage := *ui.Local(c.Root(), "282.data", func() []feedback.TokenUsage { + return []feedback.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Search", Input: 4800, Output: 650}, {Label: "Read", Input: 6100, Output: 420}, {Label: "Write", Input: 2100, Output: 2900}, {Label: "Review", Input: 3300, Output: 800}} }) navigationDemo(c, func() { @@ -54,17 +58,17 @@ func motionTokenUsageDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.empty, "Empty", log) navigationCheck(c, &s.failed, "Error", log) }, func() { - st := mujica.DataReady + st := data.DataReady if s.loading { - st = mujica.DataLoading + st = data.DataLoading } if s.failed { - st = mujica.DataFailed + st = data.DataFailed } if s.empty { usage = nil } - r := mujica.TokenUsageChart(c, usage, mujica.TokenUsageChartOptions{Line: s.line, Frame: mujica.ChartFrame{Status: st, Error: "Usage unavailable"}}) + r := feedback.TokenUsageChart(c, usage, feedback.TokenUsageChartOptions{Line: s.line, Frame: chart.ChartFrame{Status: st, Error: "Usage unavailable"}}) if r.Changed() && r.Point >= 0 { log(fmt.Sprintf("Readout: mark %d", r.Point)) } @@ -76,12 +80,12 @@ func motionTokenUsageDemo(c *ui.Context, log func(string)) { func motionCostDemo(c *ui.Context, log func(string)) { type state struct { - table mujica.CostBreakdownState + table feedback.CostBreakdownState by int } s := ui.Local(c.Root(), "283", func() state { return state{} }) - items := *ui.Local(c.Root(), "283.data", func() []mujica.CostItem { - return []mujica.CostItem{ + items := *ui.Local(c.Root(), "283.data", func() []feedback.CostItem { + return []feedback.CostItem{ {Model: "opus", Tool: "search", Step: "plan", InputTokens: 1000, OutputTokens: 200, Cost: 0.30}, {Model: "haiku", Tool: "search", Step: "gather", InputTokens: 4000, OutputTokens: 500, Cost: 0.05}, {Model: "opus", Tool: "write", Step: "write", InputTokens: 2000, OutputTokens: 3000, Cost: 1.15}, @@ -89,11 +93,11 @@ func motionCostDemo(c *ui.Context, log func(string)) { } }) navigationDemo(c, func() { - if mujica.SegmentedControl(c, &s.by, []mujica.Segment{{Label: "Model"}, {Label: "Tool"}, {Label: "Step"}}, mujica.SegmentedControlOptions{Label: "Group by"}).Changed() { + if navigation.SegmentedControl(c, &s.by, []navigation.Segment{{Label: "Model"}, {Label: "Tool"}, {Label: "Step"}}, navigation.SegmentedControlOptions{Label: "Group by"}).Changed() { log(fmt.Sprintf("Group by %d", s.by)) } }, func() { - v := mujica.CostBreakdown(c, &s.table, items, mujica.CostBreakdownOptions{GroupBy: mujica.CostGroup(s.by)}) + v := feedback.CostBreakdown(c, &s.table, items, feedback.CostBreakdownOptions{GroupBy: feedback.CostGroup(s.by)}) v.Element.Height(320) if v.SortChanged() { log("Sorted by " + s.table.Table.Sort.Column) @@ -106,15 +110,15 @@ func motionCostDemo(c *ui.Context, log func(string)) { func motionTraceDemo(c *ui.Context, log func(string)) { type state struct { - viewer mujica.TraceViewerState + viewer feedback.TraceViewerState empty, loading, failed, disabled bool } s := ui.Local(c.Root(), "284", func() state { return state{} }) - spans := *ui.Local(c.Root(), "284.data", func() []mujica.TraceSpan { - return []mujica.TraceSpan{ - {ID: "run", Name: "agent.run", End: 900 * time.Millisecond, Attrs: []mujica.TraceAttr{{Key: "model", Value: "opus"}, {Key: "session", Value: "s-42"}}}, - {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, Attrs: []mujica.TraceAttr{{Key: "tokens", Value: "1200"}}}, - {ID: "search", Parent: "run", Name: "tool.search", Start: 220 * time.Millisecond, End: 520 * time.Millisecond, Failed: true, Attrs: []mujica.TraceAttr{{Key: "error", Value: "timeout"}}}, + spans := *ui.Local(c.Root(), "284.data", func() []feedback.TraceSpan { + return []feedback.TraceSpan{ + {ID: "run", Name: "agent.run", End: 900 * time.Millisecond, Attrs: []feedback.TraceAttr{{Key: "model", Value: "opus"}, {Key: "session", Value: "s-42"}}}, + {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, Attrs: []feedback.TraceAttr{{Key: "tokens", Value: "1200"}}}, + {ID: "search", Parent: "run", Name: "tool.search", Start: 220 * time.Millisecond, End: 520 * time.Millisecond, Failed: true, Attrs: []feedback.TraceAttr{{Key: "error", Value: "timeout"}}}, {ID: "fetch", Parent: "search", Name: "http.get", Start: 230 * time.Millisecond, End: 500 * time.Millisecond}, {ID: "write", Parent: "run", Name: "llm.write", Start: 530 * time.Millisecond, End: 880 * time.Millisecond}, } @@ -125,18 +129,18 @@ func motionTraceDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.failed, "Error", log) navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - status := mujica.DataReady + status := data.DataReady if s.empty || s.loading || s.failed { spans = nil } if s.loading { - status = mujica.DataLoading + status = data.DataLoading } if s.failed { - status = mujica.DataFailed + status = data.DataFailed } ui.Column(c).Disabled(s.disabled).Children(func() { - v := mujica.TraceViewer(c, &s.viewer, spans, mujica.TraceViewerOptions{Status: status, Error: "Trace unavailable"}) + v := feedback.TraceViewer(c, &s.viewer, spans, feedback.TraceViewerOptions{Status: status, Error: "Trace unavailable"}) v.Element.Height(440) if id, ok := s.viewer.Tree.Selected(); ok && v.Changed() { log("Selected " + id) @@ -153,12 +157,12 @@ func motionTraceDemo(c *ui.Context, log func(string)) { func motionEvalDemo(c *ui.Context, log func(string)) { type state struct { - table mujica.DataTableState[string] + table data.DataTableState[string] empty, loading, failed, disabled bool } s := ui.Local(c.Root(), "285", func() state { return state{} }) - cases := *ui.Local(c.Root(), "285.data", func() []mujica.EvalCase { - return []mujica.EvalCase{ + cases := *ui.Local(c.Root(), "285.data", func() []feedback.EvalCase { + return []feedback.EvalCase{ {ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}, {ID: "c3", Name: "Summarizes letter", Score: 0.87, Passed: true}, @@ -171,18 +175,18 @@ func motionEvalDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.failed, "Error", log) navigationCheck(c, &s.disabled, "Disabled", log) }, func() { - status := mujica.DataReady + status := data.DataReady if s.empty || s.loading || s.failed { cases = nil } if s.loading { - status = mujica.DataLoading + status = data.DataLoading } if s.failed { - status = mujica.DataFailed + status = data.DataFailed } ui.Column(c).Disabled(s.disabled).Children(func() { - r := mujica.EvalResultTable(c, &s.table, cases, mujica.EvalResultTableOptions{Status: status, Error: "Evaluation unavailable"}) + r := feedback.EvalResultTable(c, &s.table, cases, feedback.EvalResultTableOptions{Status: status, Error: "Evaluation unavailable"}) r.Element.Height(320) if id, ok := r.Diff(); ok { log("Diff " + id) @@ -203,16 +207,16 @@ func motionEvalDemo(c *ui.Context, log func(string)) { func motionToolsDemo(c *ui.Context, log func(string)) { type state struct { - list mujica.ListState[string] - tools []mujica.AgentTool + list data.ListState[string] + tools []feedback.AgentTool disabled, empty bool } s := ui.Local(c.Root(), "286", func() state { - return state{tools: []mujica.AgentTool{ - {Name: "web_search", Description: "Search the web", Enabled: true, Params: []mujica.ToolParam{ + return state{tools: []feedback.AgentTool{ + {Name: "web_search", Description: "Search the web", Enabled: true, Params: []feedback.ToolParam{ {Name: "query", Type: "string", Required: true, Description: "What to look for"}, {Name: "limit", Type: "integer"}}}, - {Name: "read_file", Description: "Read a local file", Params: []mujica.ToolParam{{Name: "path", Type: "string", Required: true}}}, - {Name: "run_shell", Description: "Run a shell command", Params: []mujica.ToolParam{{Name: "command", Type: "string", Required: true}, {Name: "timeout", Type: "number"}}}, + {Name: "read_file", Description: "Read a local file", Params: []feedback.ToolParam{{Name: "path", Type: "string", Required: true}}}, + {Name: "run_shell", Description: "Run a shell command", Params: []feedback.ToolParam{{Name: "command", Type: "string", Required: true}, {Name: "timeout", Type: "number"}}}, }} }) navigationDemo(c, func() { @@ -223,7 +227,7 @@ func motionToolsDemo(c *ui.Context, log func(string)) { if s.empty { tools = nil } - r := mujica.ToolRegistryPanel(c, &s.list, tools, mujica.ToolRegistryPanelOptions{Disabled: s.disabled}) + r := feedback.ToolRegistryPanel(c, &s.list, tools, feedback.ToolRegistryPanelOptions{Disabled: s.disabled}) r.Element.Height(320) if r.List.Changed() { name, _ := s.list.Selected() diff --git a/examples/gallery/pages_motion_test.go b/examples/gallery/pages_motion_test.go index 4d3172f..62fe813 100644 --- a/examples/gallery/pages_motion_test.go +++ b/examples/gallery/pages_motion_test.go @@ -6,7 +6,7 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMotionPagesControlsAndEvents(t *testing.T) { @@ -21,7 +21,7 @@ func TestMotionPagesControlsAndEvents(t *testing.T) { } var events []string tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) }) }, 1000, 700) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -43,7 +43,7 @@ func TestMotionPagesControlsAndEvents(t *testing.T) { func TestMotionTypewriterRestartReplays(t *testing.T) { finished := 0 tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) motionTypewriterDemo(c, func(s string) { if s == "Finished" { finished++ diff --git a/examples/gallery/pages_navigation.go b/examples/gallery/pages_navigation.go index 3809106..807684a 100644 --- a/examples/gallery/pages_navigation.go +++ b/examples/gallery/pages_navigation.go @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) var navigationPages = []page{ @@ -63,11 +65,11 @@ func navigationDemo(c *ui.Context, controls, body func()) { }) } -const navigationTabsCode = `mujica.Tabs(c, &tab, []mujica.Tab{ +const navigationTabsCode = `navigation.Tabs(c, &tab, []navigation.Tab{ {Label: "General", Panel: func() { ui.Text(c, "General settings") }}, {Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }}, {Label: "Archive", Disabled: true}, -}, mujica.TabsOptions{Label: "Settings"})` +}, navigation.TabsOptions{Label: "Settings"})` func navigationTabsDemo(c *ui.Context, log func(string)) { type state struct { @@ -84,9 +86,9 @@ func navigationTabsDemo(c *ui.Context, log func(string)) { if s.many { names = append(names, "Seals", "Heraldry", "Ledgers", "Envoys", "Banquets", "Chapel") } - var tabs []mujica.Tab + var tabs []navigation.Tab for i, n := range names { - tabs = append(tabs, mujica.Tab{Label: n, Disabled: n == "Archive" && s.archive, Panel: func() { + tabs = append(tabs, navigation.Tab{Label: n, Disabled: n == "Archive" && s.archive, Panel: func() { ui.Text(c, fmt.Sprintf("The %s panel (tab %d).", n, i+1)) }}) } @@ -96,17 +98,17 @@ func navigationTabsDemo(c *ui.Context, log func(string)) { s.tab = 0 } ui.Column(c).Width(420).Children(func() { - if mujica.Tabs(c, &s.tab, tabs, mujica.TabsOptions{Vertical: s.vertical, Label: "Settings"}).Changed() { + if navigation.Tabs(c, &s.tab, tabs, navigation.TabsOptions{Vertical: s.vertical, Label: "Settings"}).Changed() { log("Changed: tab " + names[s.tab]) } }) }) } -const navigationSegmentedCode = `mujica.SegmentedControl(c, &view, []mujica.Segment{ +const navigationSegmentedCode = `navigation.SegmentedControl(c, &view, []navigation.Segment{ {Label: "List", Icon: icons.Must("list"), HideLabel: true}, {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true}, -}, mujica.SegmentedControlOptions{Label: "View"})` +}, navigation.SegmentedControlOptions{Label: "View"})` func navigationSegmentedDemo(c *ui.Context, log func(string)) { type state struct { @@ -119,13 +121,13 @@ func navigationSegmentedDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.iconOnly, "Icons only", log) navigationCheck(c, &s.disab, "Disabled", log) }, func() { - opts := mujica.SegmentedControlOptions{Equal: s.equal, Disabled: s.disab, Label: "Range"} + opts := navigation.SegmentedControlOptions{Equal: s.equal, Disabled: s.disab, Label: "Range"} ui.Row(c).Gap(24).AlignItems(ui.Center).Children(func() { - if mujica.SegmentedControl(c, &s.text, []mujica.Segment{{Label: "Day"}, {Label: "Week"}, {Label: "Fortnight"}}, opts).Changed() { + if navigation.SegmentedControl(c, &s.text, []navigation.Segment{{Label: "Day"}, {Label: "Week"}, {Label: "Fortnight"}}, opts).Changed() { log(fmt.Sprintf("Changed: range %d", s.text)) } opts.Label = "View" - if mujica.SegmentedControl(c, &s.icon, []mujica.Segment{ + if navigation.SegmentedControl(c, &s.icon, []navigation.Segment{ {Label: "List", Icon: icons.Must("list"), HideLabel: s.iconOnly}, {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: s.iconOnly}, }, opts).Changed() { @@ -135,7 +137,7 @@ func navigationSegmentedDemo(c *ui.Context, log func(string)) { }) } -const navigationBreadcrumbCode = `if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{}).Chosen(); ok { +const navigationBreadcrumbCode = `if i, ok := navigation.Breadcrumb(c, path, navigation.BreadcrumbOptions{}).Chosen(); ok { path = path[:i+1] }` @@ -148,22 +150,22 @@ func navigationBreadcrumbDemo(c *ui.Context, log func(string)) { navigationSlider(c, &s.depth, "Depth", 1, 7, log) navigationSlider(c, &s.max, "MaxItems", 3, 7, log) }, func() { - var path []mujica.BreadcrumbItem + var path []navigation.BreadcrumbItem for i, n := range navigationBreadcrumbNames[:int(s.depth)] { - it := mujica.BreadcrumbItem{Label: n} + it := navigation.BreadcrumbItem{Label: n} if i == 0 { it.Icon = icons.Must("house") } path = append(path, it) } - if i, ok := mujica.Breadcrumb(c, path, mujica.BreadcrumbOptions{MaxItems: int(s.max)}).Chosen(); ok { + if i, ok := navigation.Breadcrumb(c, path, navigation.BreadcrumbOptions{MaxItems: int(s.max)}).Chosen(); ok { log("Chosen: " + path[i].Label) s.depth = float64(i + 1) } }) } -const navigationPaginationCode = `if mujica.Pagination(c, &page, &size, mujica.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { +const navigationPaginationCode = `if navigation.Pagination(c, &page, &size, navigation.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { load(page, size) }` @@ -180,23 +182,23 @@ func navigationPaginationDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.jump, "Jump", log) navigationCheck(c, &s.disab, "Disabled", log) }, func() { - opts := mujica.PaginationOptions{Total: int(s.total), Jump: s.jump, Disabled: s.disab} + opts := navigation.PaginationOptions{Total: int(s.total), Jump: s.jump, Disabled: s.disab} if s.sizes { opts.PageSizes = []int{10, 20, 50} } else { s.size = 20 } - if mujica.Pagination(c, &s.page, &s.size, opts).Changed() { + if navigation.Pagination(c, &s.page, &s.size, opts).Changed() { log(fmt.Sprintf("Changed: page %d, size %d", s.page, s.size)) } }) } -const navigationStepsCode = `mujica.Steps(c, ¤t, []mujica.Step{ +const navigationStepsCode = `navigation.Steps(c, ¤t, []navigation.Step{ {Title: "Account", Clickable: true}, {Title: "Profile", Description: "Name and seal"}, {Title: "Confirm"}, -}, mujica.StepsOptions{Label: "Sign-up"})` +}, navigation.StepsOptions{Label: "Sign-up"})` func navigationStepsDemo(c *ui.Context, log func(string)) { type state struct { @@ -204,7 +206,7 @@ func navigationStepsDemo(c *ui.Context, log func(string)) { vertical, failed, clickOK bool } s := ui.Local(c.Root(), "063", func() state { return state{current: 1, clickOK: true} }) - steps := []mujica.Step{ + steps := []navigation.Step{ {Title: "Account", Description: "Name and seal"}, {Title: "Profile", Description: "Titles and house"}, {Title: "Payment", Description: "Treasury"}, @@ -214,11 +216,11 @@ func navigationStepsDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.vertical, "Vertical", log) navigationCheck(c, &s.failed, "Payment failed", log) navigationCheck(c, &s.clickOK, "Done steps clickable", log) - if mujica.Button(c, "Back", mujica.ButtonOptions{Variant: mujica.Secondary, Disabled: s.current == 0}).Clicked() { + if input.Button(c, "Back", input.ButtonOptions{Variant: input.Secondary, Disabled: s.current == 0}).Clicked() { s.current-- log(fmt.Sprintf("Back: step %d", s.current+1)) } - if mujica.Button(c, "Next", mujica.ButtonOptions{Disabled: s.current == len(steps)}).Clicked() { + if input.Button(c, "Next", input.ButtonOptions{Disabled: s.current == len(steps)}).Clicked() { s.current++ log(fmt.Sprintf("Next: step %d", s.current+1)) } @@ -227,18 +229,18 @@ func navigationStepsDemo(c *ui.Context, log func(string)) { steps[i].Clickable = s.clickOK && i < s.current } steps[2].Error = s.failed - if mujica.Steps(c, &s.current, steps, mujica.StepsOptions{Vertical: s.vertical, Label: "Sign-up"}).Changed() { + if navigation.Steps(c, &s.current, steps, navigation.StepsOptions{Vertical: s.vertical, Label: "Sign-up"}).Changed() { log("Changed: step " + steps[s.current].Title) } }) } -const navigationSidebarCode = `mujica.Sidebar(c, &selected, []mujica.SidebarSection{ - {Title: "Mail", Items: []mujica.SidebarItem{ +const navigationSidebarCode = `navigation.Sidebar(c, &selected, []navigation.SidebarSection{ + {Title: "Mail", Items: []navigation.SidebarItem{ {ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: 3}, - {Label: "Folders", Items: []mujica.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, + {Label: "Folders", Items: []navigation.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, }}, -}, mujica.SidebarOptions{})` +}, navigation.SidebarOptions{})` func navigationSidebarDemo(c *ui.Context, log func(string)) { type state struct { @@ -251,22 +253,22 @@ func navigationSidebarDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.compact, "Compact", log) navigationSlider(c, &s.unread, "Unread", 0, 1200, log) }, func() { - sections := []mujica.SidebarSection{ - {Title: "Library", Items: []mujica.SidebarItem{ + sections := []navigation.SidebarSection{ + {Title: "Library", Items: []navigation.SidebarItem{ {ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: int(s.unread)}, {ID: "starred", Label: "Starred", Icon: icons.Must("star")}, - {Label: "Projects", Items: []mujica.SidebarItem{ + {Label: "Projects", Items: []navigation.SidebarItem{ {ID: "opera", Label: "Opera", Icon: icons.Must("folder")}, {ID: "ballet", Label: "Ballet", Icon: icons.Must("folder")}, }}, }}, - {Title: "System", Items: []mujica.SidebarItem{ + {Title: "System", Items: []navigation.SidebarItem{ {ID: "settings", Label: "Settings", Icon: icons.Must("settings")}, {ID: "trash", Label: "Trash", Icon: icons.Must("trash-2")}, }}, } - ui.Row(c).Height(360).Border(1, mujica.Tokens(c).Border).Radius(4).Clip().Children(func() { - if mujica.Sidebar(c, &s.selected, sections, mujica.SidebarOptions{Compact: s.compact}).Changed() { + ui.Row(c).Height(360).Border(1, core.Tokens(c).Border).Radius(4).Clip().Children(func() { + if navigation.Sidebar(c, &s.selected, sections, navigation.SidebarOptions{Compact: s.compact}).Changed() { log("Changed: " + s.selected) } ui.Text(c, "Showing "+s.selected).Padding(24) @@ -274,12 +276,12 @@ func navigationSidebarDemo(c *ui.Context, log func(string)) { }) } -const navigationMenuCode = `mujica.NavigationMenu(c, router, []mujica.NavigationLink{ +const navigationMenuCode = `navigation.NavigationMenu(c, router, []navigation.NavigationLink{ {Label: "Home", Path: "/"}, - {Label: "Library", Items: []mujica.NavigationLink{ + {Label: "Library", Items: []navigation.NavigationLink{ {Label: "Poems", Path: "/library/poems", Description: "Verse of the court"}, }}, -}, mujica.NavigationMenuOptions{History: true}) +}, navigation.NavigationMenuOptions{History: true}) router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) })` func navigationMenuDemo(c *ui.Context, log func(string)) { @@ -292,17 +294,17 @@ func navigationMenuDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.history, "Back and forward", log) }, func() { - mujica.NavigationMenu(c, s.router, []mujica.NavigationLink{ + navigation.NavigationMenu(c, s.router, []navigation.NavigationLink{ {Label: "Home", Path: "/", Icon: icons.Must("house")}, - {Label: "Library", Items: []mujica.NavigationLink{ + {Label: "Library", Items: []navigation.NavigationLink{ {Label: "Poems", Path: "/library/poems", Description: "Verse of the court"}, {Label: "Letters", Path: "/library/letters", Description: "Correspondence"}, - {Label: "Archive", Items: []mujica.NavigationLink{ + {Label: "Archive", Items: []navigation.NavigationLink{ {Label: "Older letters", Path: "/archive/letters"}, }}, }}, {Label: "About", Path: "/about"}, - }, mujica.NavigationMenuOptions{History: s.history}) + }, navigation.NavigationMenuOptions{History: s.history}) if p := s.router.Path(); p != s.last { log(fmt.Sprintf("Path: %s → %s", s.last, p)) s.last = p @@ -316,15 +318,15 @@ func navigationMenuDemo(c *ui.Context, log func(string)) { }) } -const navigationMenubarCode = `res := mujica.Menubar(c, []mujica.MenubarMenu{ - {Label: "File", Items: []mujica.MenubarItem{ - {ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, - {Label: "Export", Items: []mujica.MenubarItem{{ID: "pdf", Label: "PDF"}}}, +const navigationMenubarCode = `res := navigation.Menubar(c, []navigation.MenubarMenu{ + {Label: "File", Items: []navigation.MenubarItem{ + {ID: "new", Label: "New", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {Label: "Export", Items: []navigation.MenubarItem{{ID: "pdf", Label: "PDF"}}}, }}, - {Label: "View", Items: []mujica.MenubarItem{ - {ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: wrap}, + {Label: "View", Items: []navigation.MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: navigation.MenubarCheck, Checked: wrap}, }}, -}, mujica.MenubarOptions{}) +}, navigation.MenubarOptions{}) if id, ok := res.Chosen(); ok && id == "wrap" { wrap = !wrap }` @@ -338,32 +340,32 @@ func navigationMenubarDemo(c *ui.Context, log func(string)) { navigationDemo(c, func() { navigationCheck(c, &s.locked, "Lock editing (disables Edit items)", log) }, func() { - menus := []mujica.MenubarMenu{ - {Label: "File", Items: []mujica.MenubarItem{ - {ID: "new", Label: "New", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, - {ID: "open", Label: "Open…", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyO}}, - {Label: "Export", Items: []mujica.MenubarItem{ + menus := []navigation.MenubarMenu{ + {Label: "File", Items: []navigation.MenubarItem{ + {ID: "new", Label: "New", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {ID: "open", Label: "Open…", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyO}}, + {Label: "Export", Items: []navigation.MenubarItem{ {ID: "pdf", Label: "PDF"}, {ID: "png", Label: "Image"}, - {Label: "Archive", Items: []mujica.MenubarItem{{ID: "zip", Label: "Zip"}, {ID: "tar", Label: "Tarball"}}}, + {Label: "Archive", Items: []navigation.MenubarItem{{ID: "zip", Label: "Zip"}, {ID: "tar", Label: "Tarball"}}}, }}, - {Kind: mujica.MenubarSeparator}, - {ID: "quit", Label: "Quit", Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyQ}}, + {Kind: navigation.MenubarSeparator}, + {ID: "quit", Label: "Quit", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyQ}}, }}, - {Label: "Edit", Items: []mujica.MenubarItem{ - {ID: "undo", Label: "Undo", Disabled: s.locked, Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyZ}}, - {ID: "redo", Label: "Redo", Disabled: s.locked, Shortcut: mujica.MenubarShortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyZ}}, + {Label: "Edit", Items: []navigation.MenubarItem{ + {ID: "undo", Label: "Undo", Disabled: s.locked, Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyZ}}, + {ID: "redo", Label: "Redo", Disabled: s.locked, Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyZ}}, }}, - {Label: "View", Items: []mujica.MenubarItem{ - {ID: "wrap", Label: "Word wrap", Kind: mujica.MenubarCheck, Checked: s.wrap}, - {ID: "ruler", Label: "Ruler", Kind: mujica.MenubarCheck, Checked: s.ruler}, - {Kind: mujica.MenubarSeparator}, - {ID: "fit", Label: "Fit to window", Kind: mujica.MenubarRadio, Checked: s.zoom == "fit"}, - {ID: "actual", Label: "Actual size", Kind: mujica.MenubarRadio, Checked: s.zoom == "actual"}, + {Label: "View", Items: []navigation.MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: navigation.MenubarCheck, Checked: s.wrap}, + {ID: "ruler", Label: "Ruler", Kind: navigation.MenubarCheck, Checked: s.ruler}, + {Kind: navigation.MenubarSeparator}, + {ID: "fit", Label: "Fit to window", Kind: navigation.MenubarRadio, Checked: s.zoom == "fit"}, + {ID: "actual", Label: "Actual size", Kind: navigation.MenubarRadio, Checked: s.zoom == "actual"}, }}, } - ui.Column(c).Height(260).Border(1, mujica.Tokens(c).Border).Radius(4).Clip().Children(func() { - if id, ok := mujica.Menubar(c, menus, mujica.MenubarOptions{}).Chosen(); ok { + ui.Column(c).Height(260).Border(1, core.Tokens(c).Border).Radius(4).Clip().Children(func() { + if id, ok := navigation.Menubar(c, menus, navigation.MenubarOptions{}).Chosen(); ok { switch id { case "wrap": s.wrap = !s.wrap @@ -379,13 +381,13 @@ func navigationMenubarDemo(c *ui.Context, log func(string)) { }) } -const navigationToolbarCode = `mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() { - mujica.ToolbarGroup(c, func() { - mujica.ToolbarButton(c, "Bold", mujica.ToolbarButtonOptions{Icon: icons.Must("bold")}) - mujica.ToolbarButton(c, "Italic", mujica.ToolbarButtonOptions{Icon: icons.Must("italic")}) +const navigationToolbarCode = `navigation.Toolbar(c, navigation.ToolbarOptions{Label: "Format"}, func() { + navigation.ToolbarGroup(c, func() { + navigation.ToolbarButton(c, "Bold", navigation.ToolbarButtonOptions{Icon: icons.Must("bold")}) + navigation.ToolbarButton(c, "Italic", navigation.ToolbarButtonOptions{Icon: icons.Must("italic")}) }) - mujica.ToolbarSeparator(c) - if mujica.ToolbarButton(c, "Copy", mujica.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { + navigation.ToolbarSeparator(c) + if navigation.ToolbarButton(c, "Copy", navigation.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { copySelection() } })` @@ -403,32 +405,32 @@ func navigationToolbarDemo(c *ui.Context, log func(string)) { navigationCheck(c, &s.noDeletes, "Disable Delete", log) }, func() { ui.Column(c).Width(float32(s.width)).Children(func() { - mujica.Toolbar(c, mujica.ToolbarOptions{Label: "Format"}, func() { + navigation.Toolbar(c, navigation.ToolbarOptions{Label: "Format"}, func() { button := func(label, icon string, disabled bool) { - if mujica.ToolbarButton(c, label, mujica.ToolbarButtonOptions{Icon: icons.Must(icon), ShowLabel: s.labels, Disabled: disabled}).Clicked() { + if navigation.ToolbarButton(c, label, navigation.ToolbarButtonOptions{Icon: icons.Must(icon), ShowLabel: s.labels, Disabled: disabled}).Clicked() { log("Clicked: " + label) } } - mujica.ToolbarGroup(c, func() { + navigation.ToolbarGroup(c, func() { button("Bold", "bold", false) button("Italic", "italic", false) button("Underline", "underline", false) }) - mujica.ToolbarSeparator(c) + navigation.ToolbarSeparator(c) button("Copy", "copy", false) button("Image", "image", false) button("Delete", "trash-2", s.noDeletes) - mujica.ToolbarSeparator(c) + navigation.ToolbarSeparator(c) button("Settings", "settings", false) }) }) }) } -const navigationPaletteCode = `res := mujica.CommandPalette(c, &open, []mujica.Command{ +const navigationPaletteCode = `res := navigation.CommandPalette(c, &open, []navigation.Command{ {ID: "new", Label: "New document", Group: "File"}, {ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"night"}}, -}, mujica.CommandPaletteOptions{}) +}, navigation.CommandPaletteOptions{}) if id, ok := res.Chosen(); ok { run(id) }` @@ -437,12 +439,12 @@ func navigationPaletteDemo(c *ui.Context, log func(string)) { type state struct{ open, wasOpen bool } s := ui.Local(c.Root(), "068", func() state { return state{} }) navigationDemo(c, func() { - if mujica.Button(c, "Open command palette", mujica.ButtonOptions{Icon: icons.Must("command")}).Clicked() { + if input.Button(c, "Open command palette", input.ButtonOptions{Icon: icons.Must("command")}).Clicked() { s.open = true } - ui.Text(c, "or press Cmd+K / Ctrl+K").TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, "or press Cmd+K / Ctrl+K").TextColor(core.Tokens(c).TextMuted) }, func() { - res := mujica.CommandPalette(c, &s.open, []mujica.Command{ + res := navigation.CommandPalette(c, &s.open, []navigation.Command{ {ID: "new", Label: "New document", Group: "File", Icon: icons.Must("file")}, {ID: "open", Label: "Open recent", Group: "File", Icon: icons.Must("folder")}, {ID: "save", Label: "Save all", Group: "File", Icon: icons.Must("file-text")}, @@ -450,7 +452,7 @@ func navigationPaletteDemo(c *ui.Context, log func(string)) { {ID: "zoom-in", Label: "Zoom in", Group: "View", Icon: icons.Must("zoom-in")}, {ID: "zoom-out", Label: "Zoom out", Group: "View", Icon: icons.Must("zoom-out")}, {ID: "settings", Label: "Open settings", Group: "Application", Keywords: []string{"preferences"}, Icon: icons.Must("settings")}, - }, mujica.CommandPaletteOptions{}) + }, navigation.CommandPaletteOptions{}) if s.open != s.wasOpen { log(fmt.Sprintf("Open: %v", s.open)) s.wasOpen = s.open diff --git a/examples/gallery/pages_overlay.go b/examples/gallery/pages_overlay.go index 2e65b9a..f4dfa15 100644 --- a/examples/gallery/pages_overlay.go +++ b/examples/gallery/pages_overlay.go @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" ) const overlayCategory = "Overlays" @@ -48,19 +50,19 @@ func overlayCheck(c *ui.Context, log func(string), v *bool, label string) { const dialogCode = `open := true name := "Ophelia" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Rename…", input.ButtonOptions{}).Clicked() { open = true } - mujica.Dialog(c, &open, mujica.DialogOptions{ + overlay.Dialog(c, &open, overlay.DialogOptions{ Title: "Rename the chamber", Description: "The new name shows in every hall.", Decorated: true, Actions: func() { - if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}).Clicked() { open = false } - if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Rename", input.ButtonOptions{}).Clicked() { open = false } }, @@ -83,19 +85,19 @@ func dialogDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.keepEscape, "KeepOnEscape") overlayCheck(c, log, &s.hideClose, "HideClose") }) - if mujica.Button(c, "Rename…", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Rename…", input.ButtonOptions{}).Clicked() { s.open = true log("open") } was := s.open - mujica.Dialog(c, &s.open, mujica.DialogOptions{ + overlay.Dialog(c, &s.open, overlay.DialogOptions{ Title: "Rename the chamber", Description: "The new name shows in every hall.", Decorated: s.decorated, KeepOnBackdrop: s.keepBackdrop, KeepOnEscape: s.keepEscape, HideClose: s.hideClose, Actions: func() { - if mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.open = false } - if mujica.Button(c, "Rename", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Rename", input.ButtonOptions{}).Clicked() { log("renamed to " + s.name) s.open = false } @@ -108,11 +110,11 @@ func dialogDemo(c *ui.Context, log func(string)) { const alertDialogCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Delete archive", input.ButtonOptions{Variant: input.Danger}).Clicked() { open = true } - r := mujica.AlertDialog(c, &open, mujica.AlertDialogOptions{ + r := overlay.AlertDialog(c, &open, overlay.AlertDialogOptions{ Title: "Delete the archive?", Description: "Twelve letters will be destroyed.", ConfirmText: "archive", @@ -128,15 +130,15 @@ type alertDialogDemoState struct{ open, typed bool } func alertDialogDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "082", func() alertDialogDemoState { return alertDialogDemoState{} }) overlayControls(c, func() { overlayCheck(c, log, &s.typed, "ConfirmText") }) - if mujica.Button(c, "Delete archive", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { + if input.Button(c, "Delete archive", input.ButtonOptions{Variant: input.Danger}).Clicked() { s.open = true log("open") } - opts := mujica.AlertDialogOptions{Title: "Delete the archive?", Description: "Twelve letters will be destroyed. This cannot be undone."} + opts := overlay.AlertDialogOptions{Title: "Delete the archive?", Description: "Twelve letters will be destroyed. This cannot be undone."} if s.typed { opts.ConfirmText = "archive" } - r := mujica.AlertDialog(c, &s.open, opts) + r := overlay.AlertDialog(c, &s.open, opts) if r.Confirmed { log("confirmed") } @@ -147,11 +149,11 @@ func alertDialogDemo(c *ui.Context, log func(string)) { const drawerCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Filters", mujica.ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Filters", input.ButtonOptions{}).Clicked() { open = true } - mujica.Drawer(c, &open, mujica.DrawerOptions{Title: "Filters", Side: mujica.DrawerRight, Size: 320}, func() { + overlay.Drawer(c, &open, overlay.DrawerOptions{Title: "Filters", Side: overlay.DrawerRight, Size: 320}, func() { ui.Text(c, "Guests are placed by rank.") }) } @@ -165,7 +167,7 @@ type drawerDemoState struct { func drawerDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "083", func() drawerDemoState { return drawerDemoState{side: "Right", size: 320} }) - sides := map[string]mujica.DrawerSide{"Right": mujica.DrawerRight, "Left": mujica.DrawerLeft, "Top": mujica.DrawerTop, "Bottom": mujica.DrawerBottom} + sides := map[string]overlay.DrawerSide{"Right": overlay.DrawerRight, "Left": overlay.DrawerLeft, "Top": overlay.DrawerTop, "Bottom": overlay.DrawerBottom} overlayControls(c, func() { if ui.Select(c, &s.side, []string{"Right", "Left", "Top", "Bottom"}).Changed() { log("side = " + s.side) @@ -173,19 +175,19 @@ func drawerDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.nonModal, "NonModal") overlaySlider(c, log, &s.size, "Size", "DIP", 120, 900, 20) }) - if mujica.Button(c, "Open drawer", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Open drawer", input.ButtonOptions{}).Clicked() { s.open = true log("open") } was := s.open - mujica.Drawer(c, &s.open, mujica.DrawerOptions{Title: "Seating chart", Side: sides[s.side], Size: float32(s.size), NonModal: s.nonModal, + overlay.Drawer(c, &s.open, overlay.DrawerOptions{Title: "Seating chart", Side: sides[s.side], Size: float32(s.size), NonModal: s.nonModal, Actions: func() { - if mujica.Button(c, "Apply", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Apply", input.ButtonOptions{}).Clicked() { log("applied") s.open = false } }}, func() { - ui.Text(c, "Guests are placed by rank, then by arrival.").TextColor(mujica.Tokens(c).TextMuted) + ui.Text(c, "Guests are placed by rank, then by arrival.").TextColor(core.Tokens(c).TextMuted) }) if was && !s.open { log("closed") @@ -194,12 +196,12 @@ func drawerDemo(c *ui.Context, log func(string)) { const popoverCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Guests", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { open = !open } - mujica.Popover(c, b, &open, mujica.PopoverOptions{Title: "Guest list"}, func() { + overlay.Popover(c, b, &open, overlay.PopoverOptions{Title: "Guest list"}, func() { ui.Text(c, "Twelve guests confirmed.") }) } @@ -213,17 +215,17 @@ type popoverDemoState struct { func popoverDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "084", func() popoverDemoState { return popoverDemoState{titled: true} }) overlayControls(c, func() { overlayCheck(c, log, &s.titled, "Title") }) - b := mujica.Button(c, "Guests", mujica.ButtonOptions{Variant: mujica.Secondary}) + b := input.Button(c, "Guests", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { s.open = !s.open log(fmt.Sprintf("open = %v", s.open)) } - opts := mujica.PopoverOptions{Label: "Guest list"} + opts := overlay.PopoverOptions{Label: "Guest list"} if s.titled { opts.Title = "Guest list" } was := s.open - mujica.Popover(c, b, &s.open, opts, func() { + overlay.Popover(c, b, &s.open, opts, func() { ui.Text(c, "Twelve guests confirmed; three await a reply.") if ui.Checkbox(c, &s.rsvp, "Remind the others").Changed() { log(fmt.Sprintf("remind = %v", s.rsvp)) @@ -235,9 +237,9 @@ func popoverDemo(c *ui.Context, log func(string)) { } const tooltipCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) - mujica.Tooltip(c, b, "Seal the letter with wax", mujica.TooltipOptions{}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) + overlay.Tooltip(c, b, "Seal the letter with wax", overlay.TooltipOptions{}) } ui.Render(view, 400, 300, 1)` @@ -246,25 +248,25 @@ func tooltipDemo(c *ui.Context, log func(string)) { overlayControls(c, func() { overlaySlider(c, log, delay, "Delay", "ms", 100, 1500, 100) }) - opts := mujica.TooltipOptions{Delay: time.Duration(*delay) * time.Millisecond} + opts := overlay.TooltipOptions{Delay: time.Duration(*delay) * time.Millisecond} ui.Row(c).Gap(12).Children(func() { - b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) if b.Clicked() { log("seal clicked") } - mujica.Tooltip(c, b, "Seal the letter with wax", opts) - d := mujica.Button(c, "Burn", mujica.ButtonOptions{Variant: mujica.Danger}) + overlay.Tooltip(c, b, "Seal the letter with wax", opts) + d := input.Button(c, "Burn", input.ButtonOptions{Variant: input.Danger}) if d.Clicked() { log("burn clicked") } - mujica.Tooltip(c, d, "Burn every copy of the letter; nothing is kept", opts) + overlay.Tooltip(c, d, "Burn every copy of the letter; nothing is kept", opts) }) } const hoverCardCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost}) - mujica.HoverCard(c, b, mujica.HoverCardOptions{}, func() { + core.Use(c, core.Settings{}) + b := input.Button(c, "Lady Sakiko", input.ButtonOptions{Variant: input.Ghost}) + overlay.HoverCard(c, b, overlay.HoverCardOptions{}, func() { ui.Text(c, "Keyboardist of the court.") }) } @@ -284,15 +286,15 @@ func hoverCardDemo(c *ui.Context, log func(string)) { overlaySlider(c, log, &s.closeDelay, "CloseDelay", "ms", 100, 1500, 100) overlaySlider(c, log, &s.width, "Width", "DIP", 200, 420, 20) }) - b := mujica.Button(c, "Lady Sakiko", mujica.ButtonOptions{Variant: mujica.Ghost}) - card := mujica.HoverCard(c, b, mujica.HoverCardOptions{ + b := input.Button(c, "Lady Sakiko", input.ButtonOptions{Variant: input.Ghost}) + card := overlay.HoverCard(c, b, overlay.HoverCardOptions{ OpenDelay: time.Duration(s.openDelay) * time.Millisecond, CloseDelay: time.Duration(s.closeDelay) * time.Millisecond, Width: float32(s.width), }, func() { ui.Text(c, "Lady Sakiko").FontWeight(600) - ui.Text(c, "Keyboardist of the court; writes the masquerade's score.").TextColor(mujica.Tokens(c).TextMuted) - if mujica.Button(c, "Follow", mujica.ButtonOptions{Variant: mujica.Outline}).Clicked() { + ui.Text(c, "Keyboardist of the court; writes the masquerade's score.").TextColor(core.Tokens(c).TextMuted) + if input.Button(c, "Follow", input.ButtonOptions{Variant: input.Outline}).Clicked() { log("follow") } }) @@ -312,19 +314,19 @@ func overlaySlider(c *ui.Context, log func(string), v *float64, name, unit strin const dropdownMenuCode = `pinned := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{}, func(m *mujica.PopupMenu) { + core.Use(c, core.Settings{}) + overlay.DropdownMenu(c, "Actions", overlay.DropdownMenuOptions{}, func(m *overlay.PopupMenu) { m.Group("Letter", func() { - if m.Item("Rename", mujica.PopupMenuItemOptions{}) { + if m.Item("Rename", overlay.PopupMenuItemOptions{}) { fmt.Println("rename") } - m.Check("Pinned", &pinned, mujica.PopupMenuItemOptions{KeepOpen: true}) + m.Check("Pinned", &pinned, overlay.PopupMenuItemOptions{KeepOpen: true}) }) - m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) { - m.Item("Archive", mujica.PopupMenuItemOptions{}) + m.Submenu("Move to", overlay.PopupMenuItemOptions{}, func(m *overlay.PopupMenu) { + m.Item("Archive", overlay.PopupMenuItemOptions{}) }) m.Separator() - m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: true}) + m.Item("Burn", overlay.PopupMenuItemOptions{Disabled: true}) }) } ui.Render(view, 400, 300, 1)` @@ -332,30 +334,30 @@ ui.Render(view, 400, 300, 1)` type menuDemoState struct{ pinned, sealed, locked, disabled bool } // overlayMenu fills the shared demo menu, logging every choice. -func overlayMenu(s *menuDemoState, log func(string)) func(m *mujica.PopupMenu) { - return func(m *mujica.PopupMenu) { +func overlayMenu(s *menuDemoState, log func(string)) func(m *overlay.PopupMenu) { + return func(m *overlay.PopupMenu) { m.Group("Letter", func() { - if m.Item("Rename", mujica.PopupMenuItemOptions{}) { + if m.Item("Rename", overlay.PopupMenuItemOptions{}) { log("rename") } - if m.Check("Pinned", &s.pinned, mujica.PopupMenuItemOptions{KeepOpen: true}) { + if m.Check("Pinned", &s.pinned, overlay.PopupMenuItemOptions{KeepOpen: true}) { log(fmt.Sprintf("pinned = %v", s.pinned)) } - if m.Check("Sealed", &s.sealed, mujica.PopupMenuItemOptions{}) { + if m.Check("Sealed", &s.sealed, overlay.PopupMenuItemOptions{}) { log(fmt.Sprintf("sealed = %v", s.sealed)) } }) m.Group("Move", func() { - m.Submenu("Move to", mujica.PopupMenuItemOptions{}, func(m *mujica.PopupMenu) { + m.Submenu("Move to", overlay.PopupMenuItemOptions{}, func(m *overlay.PopupMenu) { for _, f := range []string{"Archive", "Drafts", "Vault"} { - if m.Item(f, mujica.PopupMenuItemOptions{}) { + if m.Item(f, overlay.PopupMenuItemOptions{}) { log("move to " + f) } } }) }) m.Separator() - if m.Item("Burn", mujica.PopupMenuItemOptions{Disabled: s.locked}) { + if m.Item("Burn", overlay.PopupMenuItemOptions{Disabled: s.locked}) { log("burn") } } @@ -367,19 +369,19 @@ func dropdownMenuDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.locked, "Burn disabled") overlayCheck(c, log, &s.disabled, "Disabled") }) - mujica.DropdownMenu(c, "Actions", mujica.DropdownMenuOptions{Disabled: s.disabled}, overlayMenu(s, log)) + overlay.DropdownMenu(c, "Actions", overlay.DropdownMenuOptions{Disabled: s.disabled}, overlayMenu(s, log)) } const contextMenuCode = `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) row := ui.Row(c).Padding(12).Children(func() { ui.Text(c, "Letter to the Countess") }) - mujica.ContextMenu(c, row, mujica.ContextMenuOptions{}, func(m *mujica.PopupMenu) { - if m.Item("Reply", mujica.PopupMenuItemOptions{}) { + overlay.ContextMenu(c, row, overlay.ContextMenuOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Reply", overlay.PopupMenuItemOptions{}) { fmt.Println("reply") } - m.Item("Delete", mujica.PopupMenuItemOptions{}) + m.Item("Delete", overlay.PopupMenuItemOptions{}) }) } ui.Render(view, 400, 300, 1)` @@ -390,25 +392,25 @@ func contextMenuDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.locked, "Burn disabled") overlayCheck(c, log, &s.disabled, "Disabled") }) - k := mujica.Tokens(c) + k := core.Tokens(c) row := ui.Column(c).Height(160).FillWidth().Padding(16).Gap(8).Background(k.Surface).Border(1, k.Border).Radius(4) row.Children(func() { ui.Text(c, "Right-click anywhere here, or focus the button and press Shift+F10.").TextColor(k.TextMuted) - if mujica.Button(c, "Letter to the Countess", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() { + if input.Button(c, "Letter to the Countess", input.ButtonOptions{Variant: input.Ghost}).Clicked() { log("letter clicked") } }) - mujica.ContextMenu(c, row, mujica.ContextMenuOptions{Disabled: s.disabled}, overlayMenu(s, log)) + overlay.ContextMenu(c, row, overlay.ContextMenuOptions{Disabled: s.disabled}, overlayMenu(s, log)) } const popconfirmCode = `open := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { open = true } - if mujica.Popconfirm(c, b, &open, mujica.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { + if overlay.Popconfirm(c, b, &open, overlay.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { fmt.Println("archived") } } @@ -419,12 +421,12 @@ type popconfirmDemoState struct{ open, danger bool } func popconfirmDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "089", func() popconfirmDemoState { return popconfirmDemoState{} }) overlayControls(c, func() { overlayCheck(c, log, &s.danger, "Danger") }) - b := mujica.Button(c, "Archive", mujica.ButtonOptions{Variant: mujica.Secondary}) + b := input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { s.open = true log("open") } - r := mujica.Popconfirm(c, b, &s.open, mujica.PopconfirmOptions{Title: "Archive this letter?", Description: "It leaves the inbox.", Danger: s.danger}) + r := overlay.Popconfirm(c, b, &s.open, overlay.PopconfirmOptions{Title: "Archive this letter?", Description: "It leaves the inbox.", Danger: s.danger}) if r.Confirmed { log("confirmed") } @@ -435,8 +437,8 @@ func popconfirmDemo(c *ui.Context, log func(string)) { const overlayCode = `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Overlay(c, &open, mujica.OverlayOptions{Place: mujica.OverlayBottomRight, Label: "Now playing"}, func() { + core.Use(c, core.Settings{}) + overlay.Overlay(c, &open, overlay.OverlayOptions{Place: overlay.OverlayBottomRight, Label: "Now playing"}, func() { ui.Text(c, "Symbol I: △") }) } @@ -450,10 +452,10 @@ type overlayDemoState struct { func overlayDemo(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "090", func() overlayDemoState { return overlayDemoState{place: "BottomRight"} }) names := []string{"Center", "Top", "Bottom", "Left", "Right", "TopLeft", "TopRight", "BottomLeft", "BottomRight"} - place := mujica.OverlayCenter + place := overlay.OverlayCenter for i, n := range names { if n == s.place { - place = mujica.OverlayPlace(i) + place = overlay.OverlayPlace(i) } } overlayControls(c, func() { @@ -465,15 +467,15 @@ func overlayDemo(c *ui.Context, log func(string)) { overlayCheck(c, log, &s.dim, "Dim") } }) - if mujica.Button(c, "Show overlay", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Show overlay", input.ButtonOptions{}).Clicked() { s.open = true log("open") } was := s.open - mujica.Overlay(c, &s.open, mujica.OverlayOptions{Place: place, Modal: s.modal, Dim: s.modal && s.dim, Label: "Now playing"}, func() { + overlay.Overlay(c, &s.open, overlay.OverlayOptions{Place: place, Modal: s.modal, Dim: s.modal && s.dim, Label: "Now playing"}, func() { ui.Text(c, "Now playing").FontWeight(600) - ui.Text(c, "Symbol I: △").TextColor(mujica.Tokens(c).TextMuted) - if mujica.Button(c, "Close", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + ui.Text(c, "Symbol I: △").TextColor(core.Tokens(c).TextMuted) + if input.Button(c, "Close", input.ButtonOptions{Variant: input.Secondary}).Clicked() { s.open = false } }) diff --git a/examples/gallery/pages_projects.go b/examples/gallery/pages_projects.go index bdbae6a..e328509 100644 --- a/examples/gallery/pages_projects.go +++ b/examples/gallery/pages_projects.go @@ -5,11 +5,11 @@ const projectsCategory = "Projects" var projectsPages = append(append(projectsTaskPages, projectsFieldPages...), projectsTimePages...) var projectsTaskPages = []page{ - {ID: "361", Name: "KanbanBoard", ZhName: "任务看板", Category: projectsCategory, Demo: projectsDemoKanbanBoard, Keys: "Tab 聚焦卡片;Enter/Space 打开;Alt+左右跨列;Alt+上下排序;可拖放到卡片之前或列末。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} + {ID: "361", Name: "KanbanBoard", ZhName: "任务看板", Category: projectsCategory, Demo: projectsDemoKanbanBoard, Keys: "Tab 聚焦卡片;Enter/Space 打开;Alt+左右跨列;Alt+上下排序;可拖放到卡片之前或列末。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.KanbanBoard(c, &tasks, mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) + core.Use(c, core.Settings{}) + r := project.KanbanBoard(c, &tasks, project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) if move, ok := r.Moved(); ok { log.Println("move", move.ID, move.To, move.Index) } @@ -18,12 +18,12 @@ view := func(c *ui.Context) { } } ui.Render(view, 1000, 700, 1)`}, - {ID: "362", Name: "TaskList", ZhName: "任务列表", Category: projectsCategory, Demo: projectsDemoTaskList, Keys: "Tab 在筛选器和任务控件间移动;空格切换完成;Enter 打开;F2 编辑标题。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} -filter := mujica.TaskShowAll + {ID: "362", Name: "TaskList", ZhName: "任务列表", Category: projectsCategory, Demo: projectsDemoTaskList, Keys: "Tab 在筛选器和任务控件间移动;空格切换完成;Enter 打开;F2 编辑标题。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} +filter := project.TaskShowAll view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskList(c, tasks, &filter, mujica.TaskListOptions{GroupBy: mujica.TaskGroupStatus, Statuses: statuses}) + core.Use(c, core.Settings{}) + r := project.TaskList(c, tasks, &filter, project.TaskListOptions{GroupBy: project.TaskGroupStatus, Statuses: statuses}) if id, ok := r.Changed(); ok { log.Println("edit", id) } @@ -32,10 +32,10 @@ view := func(c *ui.Context) { } } ui.Render(view, 1000, 700, 1)`}, - {ID: "363", Name: "TaskItem", ZhName: "任务行", Category: projectsCategory, Demo: projectsDemoTaskItem, Keys: "Tab 在复选框和标题间移动;空格切换完成;Enter 打开;F2 或双击开始编辑;Enter 保存;Escape 取消。", Code: `task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} + {ID: "363", Name: "TaskItem", ZhName: "任务行", Category: projectsCategory, Demo: projectsDemoTaskItem, Keys: "Tab 在复选框和标题间移动;空格切换完成;Enter 打开;F2 或双击开始编辑;Enter 保存;Escape 取消。", Code: `task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskItem(c, &task, mujica.TaskItemOptions{}) + core.Use(c, core.Settings{}) + r := project.TaskItem(c, &task, project.TaskItemOptions{}) if id, ok := r.Changed(); ok { log.Println("edit", id) } @@ -44,11 +44,11 @@ view := func(c *ui.Context) { } } ui.Render(view, 1000, 700, 1)`}, - {ID: "364", Name: "TaskDetailPanel", ZhName: "任务详情侧栏", Category: projectsCategory, Demo: projectsDemoTaskDetailPanel, Keys: "Tab 在字段间移动;选择框使用方向键和 Enter;Escape 返回关闭事件;子任务 Enter 新增。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} + {ID: "364", Name: "TaskDetailPanel", ZhName: "任务详情侧栏", Category: projectsCategory, Demo: projectsDemoTaskDetailPanel, Keys: "Tab 在字段间移动;选择框使用方向键和 Enter;Escape 返回关闭事件;子任务 Enter 新增。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.TaskDetailPanel(c, &task, mujica.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) + core.Use(c, core.Settings{}) + r := project.TaskDetailPanel(c, &task, project.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) if r.Changed() { log.Println("task edited", task.ID) } @@ -57,20 +57,20 @@ view := func(c *ui.Context) { } } ui.Render(view, 1000, 700, 1)`}, - {ID: "365", Name: "SubtaskList", ZhName: "子任务列表", Category: projectsCategory, Demo: projectsDemoSubtaskList, Keys: "Tab 移动;Space 切换完成;新增输入框 Enter 提交;Alt+上下排序;拖动握柄排序。", Code: `subtasks := []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} + {ID: "365", Name: "SubtaskList", ZhName: "子任务列表", Category: projectsCategory, Demo: projectsDemoSubtaskList, Keys: "Tab 移动;Space 切换完成;新增输入框 Enter 提交;Alt+上下排序;拖动握柄排序。", Code: `subtasks := []project.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.SubtaskList(c, &subtasks, mujica.SubtaskListOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.SubtaskList(c, &subtasks, project.SubtaskListOptions{}).Changed() { log.Println("subtasks changed", len(subtasks)) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "368", Name: "SprintBoard", ZhName: "迭代看板", Category: projectsCategory, Demo: projectsDemoSprintBoard, Keys: "看板卡片支持 Tab、Enter/Space、Alt+方向键与拖放。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} -sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}} + {ID: "368", Name: "SprintBoard", ZhName: "迭代看板", Category: projectsCategory, Demo: projectsDemoSprintBoard, Keys: "看板卡片支持 Tab、Enter/Space、Alt+方向键与拖放。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} +sprint := project.Sprint{Name: "Release sprint", Goal: "Ship", Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.SprintBoard(c, sprint, &tasks, mujica.SprintBoardOptions{Board: mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) + core.Use(c, core.Settings{}) + r := project.SprintBoard(c, sprint, &tasks, project.SprintBoardOptions{Board: project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) if move, ok := r.Moved(); ok { log.Println("move", move.ID, move.To) } diff --git a/examples/gallery/pages_projects_demos.go b/examples/gallery/pages_projects_demos.go index b3a5b03..a852956 100644 --- a/examples/gallery/pages_projects_demos.go +++ b/examples/gallery/pages_projects_demos.go @@ -5,54 +5,58 @@ import ( "slices" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/project" ) -var projectsStatuses = []mujica.WorkflowStatus{ - {ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, - {ID: "doing", Name: "In progress", Group: mujica.StatusStarted}, - {ID: "done", Name: "Done", Group: mujica.StatusCompleted}, +var projectsStatuses = []project.WorkflowStatus{ + {ID: "todo", Name: "To do", Group: project.StatusUnstarted}, + {ID: "doing", Name: "In progress", Group: project.StatusStarted}, + {ID: "done", Name: "Done", Group: project.StatusCompleted}, } -var projectsMembers = []mujica.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}, {ID: "cy", Name: "Cy"}} -var projectsLabels = []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}, {ID: "release", Name: "Release", Color: mujica.EventSuccess}} +var projectsMembers = []project.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}, {ID: "cy", Name: "Cy"}} +var projectsLabels = []project.TaskLabel{{ID: "ui", Name: "UI", Color: datetime.EventInfo}, {ID: "release", Name: "Release", Color: datetime.EventSuccess}} func projectsCheck(c *ui.Context, log func(string), value *bool, label string) { - if mujica.Checkbox(c, value, label, mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, value, label, input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("%s = %v", label, *value)) } } -func projectsChoice[T comparable](c *ui.Context, log func(string), value *T, options []mujica.SelectOption[T], label string) { - if mujica.Select(c, value, options, mujica.SelectOptions{Label: label}).Changed() { +func projectsChoice[T comparable](c *ui.Context, log func(string), value *T, options []input.SelectOption[T], label string) { + if input.Select(c, value, options, input.SelectOptions{Label: label}).Changed() { log(fmt.Sprintf("%s = %v", label, *value)) } } type projectsTaskState struct { - tasks []mujica.Task + tasks []project.Task disabled bool version int - group mujica.TaskGrouping - filter mujica.TaskFilter + group project.TaskGrouping + filter project.TaskFilter wip float64 closed bool } func projectsTasks(c *ui.Context, key string) *projectsTaskState { return *ui.Local(c.Root(), key, func() *projectsTaskState { - return &projectsTaskState{wip: 2, tasks: []mujica.Task{ - {ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh, Due: mujica.Date{Year: 2026, Month: 10, Day: 3}, Assignees: []string{"ann"}, Labels: []string{"ui"}, Subtasks: []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}}, - {ID: "ENG-2", Title: "Check keyboard paths", Status: "doing", Priority: mujica.PriorityUrgent, Assignees: []string{"ben"}, Labels: []string{"release"}}, - {ID: "ENG-3", Title: "Publish docs", Status: "done", Done: true, Priority: mujica.PriorityLow}, + return &projectsTaskState{wip: 2, tasks: []project.Task{ + {ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh, Due: core.Date{Year: 2026, Month: 10, Day: 3}, Assignees: []string{"ann"}, Labels: []string{"ui"}, Subtasks: []project.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}}, + {ID: "ENG-2", Title: "Check keyboard paths", Status: "doing", Priority: project.PriorityUrgent, Assignees: []string{"ben"}, Labels: []string{"release"}}, + {ID: "ENG-3", Title: "Publish docs", Status: "done", Done: true, Priority: project.PriorityLow}, }} }) } -func projectsBoardOptions(s *projectsTaskState) mujica.KanbanBoardOptions { - return mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: int(s.wip)}, {Status: "done"}}, Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Version: s.version, Disabled: s.disabled} +func projectsBoardOptions(s *projectsTaskState) project.KanbanBoardOptions { + return project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: int(s.wip)}, {Status: "done"}}, Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Version: s.version, Disabled: s.disabled} } -func projectsLogBoard(s *projectsTaskState, r mujica.KanbanResult, log func(string)) { +func projectsLogBoard(s *projectsTaskState, r project.KanbanResult, log func(string)) { if move, ok := r.Moved(); ok { s.version++ log(fmt.Sprintf("move %s: %s -> %s at %d", move.ID, move.From, move.To, move.Index)) @@ -65,21 +69,21 @@ func projectsLogBoard(s *projectsTaskState, r mujica.KanbanResult, log func(stri func projectsDemoKanbanBoard(c *ui.Context, log func(string)) { s := projectsTasks(c, "361") projectsCheck(c, log, &s.disabled, "Disabled") - if mujica.NumberInput(c, &s.wip, mujica.NumberInputOptions{Min: 0, Max: 10, Step: 1, Label: "WIP limit"}).Changed() { + if input.NumberInput(c, &s.wip, input.NumberInputOptions{Min: 0, Max: 10, Step: 1, Label: "WIP limit"}).Changed() { log(fmt.Sprintf("WIP = %g", s.wip)) } ui.ScrollHorizontal(c).Height(420).FillWidth().Children(func() { - ui.Box(c).Width(880).Height(420).Children(func() { projectsLogBoard(s, mujica.KanbanBoard(c, &s.tasks, projectsBoardOptions(s)), log) }) + ui.Box(c).Width(880).Height(420).Children(func() { projectsLogBoard(s, project.KanbanBoard(c, &s.tasks, projectsBoardOptions(s)), log) }) }) } func projectsDemoTaskList(c *ui.Context, log func(string)) { s := projectsTasks(c, "362") projectsCheck(c, log, &s.disabled, "Disabled") - projectsChoice(c, log, &s.group, []mujica.SelectOption[mujica.TaskGrouping]{{Value: mujica.TaskGroupNone, Label: "Ungrouped"}, {Value: mujica.TaskGroupStatus, Label: "By status"}, {Value: mujica.TaskGroupPriority, Label: "By priority"}}, "Grouping") + projectsChoice(c, log, &s.group, []input.SelectOption[project.TaskGrouping]{{Value: project.TaskGroupNone, Label: "Ungrouped"}, {Value: project.TaskGroupStatus, Label: "By status"}, {Value: project.TaskGroupPriority, Label: "By priority"}}, "Grouping") ui.Box(c).Height(360).FillWidth().Children(func() { before := s.filter - r := mujica.TaskList(c, s.tasks, &s.filter, mujica.TaskListOptions{Statuses: projectsStatuses, Members: projectsMembers, GroupBy: s.group, Version: s.version, Disabled: s.disabled}) + r := project.TaskList(c, s.tasks, &s.filter, project.TaskListOptions{Statuses: projectsStatuses, Members: projectsMembers, GroupBy: s.group, Version: s.version, Disabled: s.disabled}) if s.filter != before { log(fmt.Sprintf("filter = %d", s.filter)) } @@ -96,8 +100,8 @@ func projectsDemoTaskList(c *ui.Context, log func(string)) { func projectsDemoTaskItem(c *ui.Context, log func(string)) { s := projectsTasks(c, "363") projectsCheck(c, log, &s.disabled, "Disabled") - projectsChoice(c, log, &s.tasks[0].Priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority") - r := mujica.TaskItem(c, &s.tasks[0], mujica.TaskItemOptions{Members: projectsMembers, Disabled: s.disabled}) + projectsChoice(c, log, &s.tasks[0].Priority, []input.SelectOption[project.Priority]{{Value: project.PriorityLow, Label: "Low"}, {Value: project.PriorityHigh, Label: "High"}, {Value: project.PriorityUrgent, Label: "Urgent"}}, "Priority") + r := project.TaskItem(c, &s.tasks[0], project.TaskItemOptions{Members: projectsMembers, Disabled: s.disabled}) if id, ok := r.Changed(); ok { log("edit " + id) } @@ -110,7 +114,7 @@ func projectsDemoTaskDetailPanel(c *ui.Context, log func(string)) { s := projectsTasks(c, "364") projectsCheck(c, log, &s.disabled, "Disabled") if s.closed { - if mujica.Button(c, "Open details", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Open details", input.ButtonOptions{}).Clicked() { s.closed = false log("open details") } @@ -118,7 +122,7 @@ func projectsDemoTaskDetailPanel(c *ui.Context, log func(string)) { } ui.Scroll(c).Height(620).FillWidth().Children(func() { ui.Box(c).MaxWidth(500).FillWidth().Children(func() { - r := mujica.TaskDetailPanel(c, &s.tasks[0], mujica.TaskDetailPanelOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Disabled: s.disabled, Activity: func() { ui.Text(c, "Ann created this task") }}) + r := project.TaskDetailPanel(c, &s.tasks[0], project.TaskDetailPanelOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Disabled: s.disabled, Activity: func() { ui.Text(c, "Ann created this task") }}) if r.Changed() { log("task fields changed") } @@ -133,11 +137,11 @@ func projectsDemoTaskDetailPanel(c *ui.Context, log func(string)) { func projectsDemoSubtaskList(c *ui.Context, log func(string)) { s := projectsTasks(c, "365") projectsCheck(c, log, &s.disabled, "Disabled") - if mujica.Button(c, "Clear subtasks", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() { + if input.Button(c, "Clear subtasks", input.ButtonOptions{Disabled: s.disabled}).Clicked() { s.tasks[0].Subtasks = nil log("clear subtasks") } - if mujica.SubtaskList(c, &s.tasks[0].Subtasks, mujica.SubtaskListOptions{Disabled: s.disabled}).Changed() { + if project.SubtaskList(c, &s.tasks[0].Subtasks, project.SubtaskListOptions{Disabled: s.disabled}).Changed() { log("subtasks changed") } } @@ -145,28 +149,28 @@ func projectsDemoSubtaskList(c *ui.Context, log func(string)) { func projectsDemoSprintBoard(c *ui.Context, log func(string)) { s := projectsTasks(c, "368") projectsCheck(c, log, &s.disabled, "Disabled") - sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship the first release", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}} + sprint := project.Sprint{Name: "Release sprint", Goal: "Ship the first release", Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}} ui.ScrollHorizontal(c).Height(540).FillWidth().Children(func() { ui.Box(c).Width(880).Height(540).Children(func() { - projectsLogBoard(s, mujica.SprintBoard(c, sprint, &s.tasks, mujica.SprintBoardOptions{Board: projectsBoardOptions(s)}), log) + projectsLogBoard(s, project.SprintBoard(c, sprint, &s.tasks, project.SprintBoardOptions{Board: projectsBoardOptions(s)}), log) }) }) } func projectsDemoLabelManager(c *ui.Context, log func(string)) { s := *ui.Local(c.Root(), "374", func() *struct { - labels []mujica.TaskLabel + labels []project.TaskLabel selected []string disabled bool } { return &struct { - labels []mujica.TaskLabel + labels []project.TaskLabel selected []string disabled bool }{labels: slices.Clone(projectsLabels)} }) projectsCheck(c, log, &s.disabled, "Disabled") - if mujica.LabelManager(c, &s.labels, &s.selected, mujica.LabelManagerOptions{Disabled: s.disabled}).Changed() { + if project.LabelManager(c, &s.labels, &s.selected, project.LabelManagerOptions{Disabled: s.disabled}).Changed() { log(fmt.Sprintf("labels changed: %d, selected %v", len(s.labels), s.selected)) } } diff --git a/examples/gallery/pages_projects_fields.go b/examples/gallery/pages_projects_fields.go index 7489909..6616fe6 100644 --- a/examples/gallery/pages_projects_fields.go +++ b/examples/gallery/pages_projects_fields.go @@ -2,10 +2,10 @@ package main var projectsFieldPages = []page{ {ID: "369", Name: "BurndownChart", ZhName: "燃尽图", Category: projectsCategory, Demo: projectsDemoBurndownChart, Keys: "Tab 聚焦图例与图表;Space 切换图例;方向键逐日读取;Home/End 首尾;Escape 清除读数。", Code: `remaining := []float64{40, 32, 25, 18} -opts := mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} +opts := project.BurndownChartOptions{Frame: chart.ChartFrame{Label: "Release burndown"}, Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.BurndownChart(c, remaining, opts) + core.Use(c, core.Settings{}) + r := project.BurndownChart(c, remaining, opts) if r.Changed() { log.Println("point", r.Point) } @@ -17,43 +17,43 @@ view := func(c *ui.Context) { } } ui.Render(view, 1000, 700, 1)`}, - {ID: "370", Name: "PriorityIndicator", ZhName: "优先级指示", Category: projectsCategory, Demo: projectsDemoPriorityIndicator, Keys: "静态展示,通过无障碍名称读取完整等级。", Code: `priority := mujica.PriorityHigh + {ID: "370", Name: "PriorityIndicator", ZhName: "优先级指示", Category: projectsCategory, Demo: projectsDemoPriorityIndicator, Keys: "静态展示,通过无障碍名称读取完整等级。", Code: `priority := project.PriorityHigh view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriorityIndicator(c, priority, mujica.PriorityIndicatorOptions{}) + core.Use(c, core.Settings{}) + project.PriorityIndicator(c, priority, project.PriorityIndicatorOptions{}) } ui.Render(view, 1000, 700, 1)`}, - {ID: "371", Name: "StatusSelect", ZhName: "状态选择", Category: projectsCategory, Demo: projectsDemoStatusSelect, Keys: "Enter/Space 或上下键打开;上下、Home/End 定位;Enter 确认;Escape 关闭。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} + {ID: "371", Name: "StatusSelect", ZhName: "状态选择", Category: projectsCategory, Demo: projectsDemoStatusSelect, Keys: "Enter/Space 或上下键打开;上下、Home/End 定位;Enter 确认;Escape 关闭。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} value := "todo" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.StatusSelect(c, &value, statuses, mujica.StatusSelectOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.StatusSelect(c, &value, statuses, project.StatusSelectOptions{}).Changed() { log.Println("status", value) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "372", Name: "AssigneePicker", ZhName: "负责人选择", Category: projectsCategory, Demo: projectsDemoAssigneePicker, Keys: "Enter/Space 打开;输入搜索;下键进入选项;Space/Enter 勾选;Backspace 移除最后一项;Escape 关闭。", Code: `members := []mujica.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} + {ID: "372", Name: "AssigneePicker", ZhName: "负责人选择", Category: projectsCategory, Demo: projectsDemoAssigneePicker, Keys: "Enter/Space 打开;输入搜索;下键进入选项;Space/Enter 勾选;Backspace 移除最后一项;Escape 关闭。", Code: `members := []project.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} ids := []string{"ann"} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.AssigneePicker(c, &ids, members, mujica.AssigneePickerOptions{Max: 2}).Changed() { + core.Use(c, core.Settings{}) + if project.AssigneePicker(c, &ids, members, project.AssigneePickerOptions{Max: 2}).Changed() { log.Println("assignees", ids) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "373", Name: "DueDatePicker", ZhName: "截止日期选择", Category: projectsCategory, Demo: projectsDemoDueDatePicker, Keys: "Tab 到快捷项、日期输入与日历按钮;Enter 选择;日历方向键移动;Escape 关闭。", Code: `var due mujica.Date + {ID: "373", Name: "DueDatePicker", ZhName: "截止日期选择", Category: projectsCategory, Demo: projectsDemoDueDatePicker, Keys: "Tab 到快捷项、日期输入与日历按钮;Enter 选择;日历方向键移动;Escape 关闭。", Code: `var due core.Date view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.DueDatePicker(c, &due, mujica.DueDatePickerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.DueDatePicker(c, &due, project.DueDatePickerOptions{}).Changed() { log.Println("due", due) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "374", Name: "LabelManager", ZhName: "标签管理", Category: projectsCategory, Demo: projectsDemoLabelManager, Keys: "Tab 移动;Space 勾选;新增输入 Enter 创建;编辑颜色按钮打开编辑;Enter 保存名称;Escape 结束编辑。", Code: `labels := []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}} + {ID: "374", Name: "LabelManager", ZhName: "标签管理", Category: projectsCategory, Demo: projectsDemoLabelManager, Keys: "Tab 移动;Space 勾选;新增输入 Enter 创建;编辑颜色按钮打开编辑;Enter 保存名称;Escape 结束编辑。", Code: `labels := []project.TaskLabel{{ID: "ui", Name: "UI", Color: datetime.EventInfo}} selected := []string{} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LabelManager(c, &labels, &selected, mujica.LabelManagerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.LabelManager(c, &labels, &selected, project.LabelManagerOptions{}).Changed() { log.Println("labels changed", len(labels), selected) } } diff --git a/examples/gallery/pages_projects_fields_demos.go b/examples/gallery/pages_projects_fields_demos.go index 0351b5b..52804d7 100644 --- a/examples/gallery/pages_projects_fields_demos.go +++ b/examples/gallery/pages_projects_fields_demos.go @@ -2,32 +2,38 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" "math" - mujica "mujicaui" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/project" ) func projectsDemoBurndownChart(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "369", func() struct { disabled bool - status mujica.DataStatus + status data.DataStatus total float64 values []float64 } { return struct { disabled bool - status mujica.DataStatus + status data.DataStatus total float64 values []float64 }{total: 40, values: []float64{40, 38, 32, 34, 26, math.NaN(), 20, 18}} }) projectsCheck(c, log, &s.disabled, "Disabled") - projectsChoice(c, log, &s.status, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Error"}}, "Data state") - if mujica.NumberInput(c, &s.total, mujica.NumberInputOptions{Min: 1, Max: 100, Step: 1, Label: "Total work"}).Changed() { + projectsChoice(c, log, &s.status, []input.SelectOption[data.DataStatus]{{Value: data.DataReady, Label: "Ready"}, {Value: data.DataLoading, Label: "Loading"}, {Value: data.DataFailed, Label: "Error"}}, "Data state") + if input.NumberInput(c, &s.total, input.NumberInputOptions{Min: 1, Max: 100, Step: 1, Label: "Total work"}).Changed() { log(fmt.Sprintf("total = %g", s.total)) } ui.Box(c).Disabled(s.disabled).FillWidth().Children(func() { - r := mujica.BurndownChart(c, s.values, mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown", Status: s.status, Error: "Data unavailable"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: s.total}) + r := project.BurndownChart(c, s.values, project.BurndownChartOptions{Frame: chart.ChartFrame{Label: "Release burndown", Status: s.status, Error: "Data unavailable"}, Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}, Total: s.total}) if r.Changed() { log(fmt.Sprintf("point = %d", r.Point)) } @@ -35,7 +41,7 @@ func projectsDemoBurndownChart(c *ui.Context, log func(string)) { log("toggle " + name) } if r.Retried() { - s.status = mujica.DataReady + s.status = data.DataReady log("retry") } }) @@ -43,17 +49,17 @@ func projectsDemoBurndownChart(c *ui.Context, log func(string)) { func projectsDemoPriorityIndicator(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "370", func() struct { - priority mujica.Priority + priority project.Priority iconOnly bool } { return struct { - priority mujica.Priority + priority project.Priority iconOnly bool - }{priority: mujica.PriorityHigh} + }{priority: project.PriorityHigh} }) projectsCheck(c, log, &s.iconOnly, "Icon only") - projectsChoice(c, log, &s.priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityNone, Label: "None"}, {Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityMedium, Label: "Medium"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority") - mujica.PriorityIndicator(c, s.priority, mujica.PriorityIndicatorOptions{IconOnly: s.iconOnly}) + projectsChoice(c, log, &s.priority, []input.SelectOption[project.Priority]{{Value: project.PriorityNone, Label: "None"}, {Value: project.PriorityLow, Label: "Low"}, {Value: project.PriorityMedium, Label: "Medium"}, {Value: project.PriorityHigh, Label: "High"}, {Value: project.PriorityUrgent, Label: "Urgent"}}, "Priority") + project.PriorityIndicator(c, s.priority, project.PriorityIndicatorOptions{IconOnly: s.iconOnly}) } func projectsDemoStatusSelect(c *ui.Context, log func(string)) { @@ -67,7 +73,7 @@ func projectsDemoStatusSelect(c *ui.Context, log func(string)) { }{value: "todo"} }) projectsCheck(c, log, &s.disabled, "Disabled") - if mujica.StatusSelect(c, &s.value, projectsStatuses, mujica.StatusSelectOptions{Disabled: s.disabled}).Changed() { + if project.StatusSelect(c, &s.value, projectsStatuses, project.StatusSelectOptions{Disabled: s.disabled}).Changed() { log("status " + s.value) } } @@ -85,27 +91,27 @@ func projectsDemoAssigneePicker(c *ui.Context, log func(string)) { }{ids: []string{"ann"}, max: 2} }) projectsCheck(c, log, &s.disabled, "Disabled") - if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Selection limit"}).Changed() { + if input.NumberInput(c, &s.max, input.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Selection limit"}).Changed() { log(fmt.Sprintf("limit = %g", s.max)) } - if mujica.AssigneePicker(c, &s.ids, projectsMembers, mujica.AssigneePickerOptions{Max: int(s.max), Disabled: s.disabled}).Changed() { + if project.AssigneePicker(c, &s.ids, projectsMembers, project.AssigneePickerOptions{Max: int(s.max), Disabled: s.disabled}).Changed() { log(fmt.Sprintf("assignees %v", s.ids)) } } func projectsDemoDueDatePicker(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "373", func() struct { - due mujica.Date + due core.Date disabled, done bool } { return struct { - due mujica.Date + due core.Date disabled, done bool - }{due: mujica.Date{Year: 2026, Month: 10, Day: 3}} + }{due: core.Date{Year: 2026, Month: 10, Day: 3}} }) projectsCheck(c, log, &s.disabled, "Disabled") projectsCheck(c, log, &s.done, "Completed") - if mujica.DueDatePicker(c, &s.due, mujica.DueDatePickerOptions{Done: s.done, Disabled: s.disabled}).Changed() { + if project.DueDatePicker(c, &s.due, project.DueDatePickerOptions{Done: s.done, Disabled: s.disabled}).Changed() { log(fmt.Sprintf("due %v", s.due)) } } @@ -116,7 +122,7 @@ func projectsDemoIssueCard(c *ui.Context, log func(string)) { projectsCheck(c, log, &s.disabled, "Disabled") projectsCheck(c, log, selected, "Selected") ui.Box(c).Width(300).Children(func() { - if mujica.IssueCard(c, s.tasks[0], mujica.IssueCardOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Selected: *selected, Disabled: s.disabled}).Clicked() { + if project.IssueCard(c, s.tasks[0], project.IssueCardOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Selected: *selected, Disabled: s.disabled}).Clicked() { *selected = !*selected log("open " + s.tasks[0].ID) } @@ -135,8 +141,8 @@ func projectsDemoIssueIdBadge(c *ui.Context, log func(string)) { }) projectsCheck(c, log, &s.disabled, "Disabled") projectsCheck(c, log, &s.copyable, "Copyable") - projectsChoice(c, log, &s.id, []mujica.SelectOption[string]{{Value: "ENG-123", Label: "ENG-123"}, {Value: "OPS-7", Label: "OPS-7"}}, "Issue key") - if mujica.IssueIdBadge(c, s.id, mujica.IssueIdBadgeOptions{Copyable: s.copyable, Disabled: s.disabled}).Changed() { + projectsChoice(c, log, &s.id, []input.SelectOption[string]{{Value: "ENG-123", Label: "ENG-123"}, {Value: "OPS-7", Label: "OPS-7"}}, "Issue key") + if project.IssueIdBadge(c, s.id, project.IssueIdBadgeOptions{Copyable: s.copyable, Disabled: s.disabled}).Changed() { log("copied " + s.id) } } diff --git a/examples/gallery/pages_projects_test.go b/examples/gallery/pages_projects_test.go index 8f022b5..6b09b69 100644 --- a/examples/gallery/pages_projects_test.go +++ b/examples/gallery/pages_projects_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/egoist/mygo/ui" - mujica "mujicaui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func projectsGalleryTester(t *testing.T, id string) (*ui.Tester, *[]string) { @@ -14,7 +15,7 @@ func projectsGalleryTester(t *testing.T, id string) (*ui.Tester, *[]string) { for _, p := range projectsPages { if p.ID == id { tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Gap(12).Padding(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) }) }, 1200, 1000) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/examples/gallery/pages_projects_time.go b/examples/gallery/pages_projects_time.go index c361623..91d54ff 100644 --- a/examples/gallery/pages_projects_time.go +++ b/examples/gallery/pages_projects_time.go @@ -1,47 +1,47 @@ package main var projectsTimePages = []page{ - {ID: "375", Name: "IssueCard", ZhName: "任务卡片", Category: projectsCategory, Demo: projectsDemoIssueCard, Keys: "Tab 聚焦;Enter/Space 激活。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}} -task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh} + {ID: "375", Name: "IssueCard", ZhName: "任务卡片", Category: projectsCategory, Demo: projectsDemoIssueCard, Keys: "Tab 聚焦;Enter/Space 激活。", Code: `statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} +task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueCard(c, task, mujica.IssueCardOptions{Statuses: statuses}).Clicked() { + core.Use(c, core.Settings{}) + if project.IssueCard(c, task, project.IssueCardOptions{Statuses: statuses}).Clicked() { log.Println("open", task.ID) } } ui.Render(view, 1000, 700, 1)`}, {ID: "376", Name: "IssueIdBadge", ZhName: "任务编号", Category: projectsCategory, Demo: projectsDemoIssueIdBadge, Keys: "Copyable 开启时 Tab 聚焦,Enter/Space 复制。", Code: `id := "ENG-123" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.IssueIdBadge(c, id, mujica.IssueIdBadgeOptions{Copyable: true}).Changed() { + core.Use(c, core.Settings{}) + if project.IssueIdBadge(c, id, project.IssueIdBadgeOptions{Copyable: true}).Changed() { log.Println("copied", id) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "377", Name: "MilestoneProgress", ZhName: "里程碑进度", Category: projectsCategory, Demo: projectsDemoMilestoneProgress, Keys: "静态展示,进度条通过无障碍名称与百分比读取。", Code: `milestone := mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} + {ID: "377", Name: "MilestoneProgress", ZhName: "里程碑进度", Category: projectsCategory, Demo: projectsDemoMilestoneProgress, Keys: "静态展示,进度条通过无障碍名称与百分比读取。", Code: `milestone := project.Milestone{Title: "Release", Due: core.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MilestoneProgress(c, milestone, mujica.MilestoneProgressOptions{}) + core.Use(c, core.Settings{}) + project.MilestoneProgress(c, milestone, project.MilestoneProgressOptions{}) } ui.Render(view, 1000, 700, 1)`}, - {ID: "378", Name: "WorkloadView", ZhName: "成员负载", Category: projectsCategory, Demo: projectsDemoWorkloadView, Keys: "静态行通过无障碍名称读取负载与超出量;滚动查看更多成员。", Code: `loads := []mujica.Workload{{Member: mujica.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} + {ID: "378", Name: "WorkloadView", ZhName: "成员负载", Category: projectsCategory, Demo: projectsDemoWorkloadView, Keys: "静态行通过无障碍名称读取负载与超出量;滚动查看更多成员。", Code: `loads := []project.Workload{{Member: project.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.WorkloadView(c, loads, mujica.WorkloadViewOptions{}) + core.Use(c, core.Settings{}) + project.WorkloadView(c, loads, project.WorkloadViewOptions{}) } ui.Render(view, 1000, 700, 1)`}, - {ID: "379", Name: "TimeTracker", ZhName: "工时计时", Category: projectsCategory, Demo: projectsDemoTimeTracker, Keys: "Tab 移动;Enter/Space 开始停止;停止后打开修正,编辑时长,Apply 提交。", Code: `var state mujica.TimeTrackerState + {ID: "379", Name: "TimeTracker", ZhName: "工时计时", Category: projectsCategory, Demo: projectsDemoTimeTracker, Keys: "Tab 移动;Enter/Space 开始停止;停止后打开修正,编辑时长,Apply 提交。", Code: `var state project.TimeTrackerState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.TimeTracker(c, &state, mujica.TimeTrackerOptions{}).Changed() { + core.Use(c, core.Settings{}) + if project.TimeTracker(c, &state, project.TimeTrackerOptions{}).Changed() { log.Println("tracked", state.Total(c.Now())) } } ui.Render(view, 1000, 700, 1)`}, - {ID: "380", Name: "PomodoroTimer", ZhName: "番茄钟", Category: projectsCategory, Demo: projectsDemoPomodoroTimer, Keys: "Tab 移动;Enter/Space 开始、暂停、跳过与重置。", Code: `var state mujica.PomodoroState + {ID: "380", Name: "PomodoroTimer", ZhName: "番茄钟", Category: projectsCategory, Demo: projectsDemoPomodoroTimer, Keys: "Tab 移动;Enter/Space 开始、暂停、跳过与重置。", Code: `var state project.PomodoroState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.PomodoroTimer(c, &state, mujica.PomodoroTimerOptions{Focus: 25 * time.Minute}) + core.Use(c, core.Settings{}) + r := project.PomodoroTimer(c, &state, project.PomodoroTimerOptions{Focus: 25 * time.Minute}) if r.Changed() { log.Println("timer", state.Phase(), state.Running()) } diff --git a/examples/gallery/pages_projects_time_demos.go b/examples/gallery/pages_projects_time_demos.go index 803394a..3e8153b 100644 --- a/examples/gallery/pages_projects_time_demos.go +++ b/examples/gallery/pages_projects_time_demos.go @@ -2,52 +2,56 @@ package main import ( "fmt" - "github.com/egoist/mygo/ui" - mujica "mujicaui" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/project" ) func projectsDemoMilestoneProgress(c *ui.Context, log func(string)) { done := ui.Local(c.Root(), "377", func() float64 { return 3 }) - if mujica.NumberInput(c, done, mujica.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Completed tasks"}).Changed() { + if input.NumberInput(c, done, input.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Completed tasks"}).Changed() { log(fmt.Sprintf("completed = %g", *done)) } - mujica.MilestoneProgress(c, mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: int(*done), Total: 8}, mujica.MilestoneProgressOptions{}) + project.MilestoneProgress(c, project.Milestone{Title: "Release", Due: core.Date{Year: 2026, Month: 10, Day: 14}, Done: int(*done), Total: 8}, project.MilestoneProgressOptions{}) } func projectsDemoWorkloadView(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "378", func() struct { assigned float64 - loads []mujica.Workload + loads []project.Workload version int } { return struct { assigned float64 - loads []mujica.Workload + loads []project.Workload version int - }{assigned: 46, loads: []mujica.Workload{{Member: projectsMembers[0], Assigned: 32, Capacity: 40}, {Member: projectsMembers[1], Assigned: 46, Capacity: 40}, {Member: projectsMembers[2], Assigned: 12, Capacity: 20}}} + }{assigned: 46, loads: []project.Workload{{Member: projectsMembers[0], Assigned: 32, Capacity: 40}, {Member: projectsMembers[1], Assigned: 46, Capacity: 40}, {Member: projectsMembers[2], Assigned: 12, Capacity: 20}}} }) - if mujica.NumberInput(c, &s.assigned, mujica.NumberInputOptions{Min: 0, Max: 80, Step: 1, Label: "Ben assigned hours"}).Changed() { + if input.NumberInput(c, &s.assigned, input.NumberInputOptions{Min: 0, Max: 80, Step: 1, Label: "Ben assigned hours"}).Changed() { s.loads[1].Assigned = s.assigned s.version++ log(fmt.Sprintf("assigned = %g", s.assigned)) } - ui.Box(c).Height(240).FillWidth().Children(func() { mujica.WorkloadView(c, s.loads, mujica.WorkloadViewOptions{Version: s.version}) }) + ui.Box(c).Height(240).FillWidth().Children(func() { project.WorkloadView(c, s.loads, project.WorkloadViewOptions{Version: s.version}) }) } func projectsDemoTimeTracker(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "379", func() struct { - timer mujica.TimeTrackerState + timer project.TimeTrackerState disabled bool } { return struct { - timer mujica.TimeTrackerState + timer project.TimeTrackerState disabled bool }{} }) projectsCheck(c, log, &s.disabled, "Disabled") ui.ScrollHorizontal(c).Height(120).FillWidth().Children(func() { - if mujica.TimeTracker(c, &s.timer, mujica.TimeTrackerOptions{Disabled: s.disabled}).Changed() { + if project.TimeTracker(c, &s.timer, project.TimeTrackerOptions{Disabled: s.disabled}).Changed() { log("tracked " + s.timer.Total(c.Now()).String()) } }) @@ -55,25 +59,25 @@ func projectsDemoTimeTracker(c *ui.Context, log func(string)) { func projectsDemoPomodoroTimer(c *ui.Context, log func(string)) { s := ui.Local(c.Root(), "380", func() struct { - timer mujica.PomodoroState + timer project.PomodoroState disabled, hidden bool seconds float64 } { return struct { - timer mujica.PomodoroState + timer project.PomodoroState disabled, hidden bool seconds float64 }{seconds: 10} }) projectsCheck(c, log, &s.disabled, "Disabled") projectsCheck(c, log, &s.hidden, "Hidden") - if mujica.NumberInput(c, &s.seconds, mujica.NumberInputOptions{Min: 1, Max: 1500, Step: 1, Label: "Focus seconds", Disabled: s.timer.Running()}).Changed() { + if input.NumberInput(c, &s.seconds, input.NumberInputOptions{Min: 1, Max: 1500, Step: 1, Label: "Focus seconds", Disabled: s.timer.Running()}).Changed() { log(fmt.Sprintf("focus = %g seconds", s.seconds)) } if s.hidden { return } - r := mujica.PomodoroTimer(c, &s.timer, mujica.PomodoroTimerOptions{Focus: time.Duration(s.seconds) * time.Second, Break: 5 * time.Second, LongBreak: 10 * time.Second, LongEvery: 2, Disabled: s.disabled}) + r := project.PomodoroTimer(c, &s.timer, project.PomodoroTimerOptions{Focus: time.Duration(s.seconds) * time.Second, Break: 5 * time.Second, LongBreak: 10 * time.Second, LongEvery: 2, Disabled: s.disabled}) if r.Changed() { log(fmt.Sprintf("timer phase = %d, running = %v", s.timer.Phase(), s.timer.Running())) } diff --git a/examples/gallery/pages_trading.go b/examples/gallery/pages_trading.go index 5a65056..d2d15db 100644 --- a/examples/gallery/pages_trading.go +++ b/examples/gallery/pages_trading.go @@ -7,39 +7,41 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const tradingCategory = "Trading" // tradingTrend shows and applies the rise-color switch. func tradingTrend(c *ui.Context, redUp *bool, log func(string)) { - if mujica.Checkbox(c, redUp, "Red means up (China)", mujica.CheckboxOptions{}).Changed() { + if input.Checkbox(c, redUp, "Red means up (China)", input.CheckboxOptions{}).Changed() { log(fmt.Sprintf("red up %v", *redUp)) } - t := mujica.GreenUp + t := core.GreenUp if *redUp { - t = mujica.RedUp + t = core.RedUp } - mujica.UseTrend(c, t) + core.UseTrend(c, t) } // tradingAction is a demo button at its natural width. func tradingAction(c *ui.Context, label string) bool { clicked := false ui.Row(c).Children(func() { - clicked = mujica.Button(c, label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() + clicked = input.Button(c, label, input.ButtonOptions{Variant: input.Secondary}).Clicked() }) return clicked } // tradingBook is a deterministic book around mid, moved by step. -func tradingBook(mid float64, step int) mujica.OrderBookData { - var b mujica.OrderBookData +func tradingBook(mid float64, step int) finance.OrderBookData { + var b finance.OrderBookData for i := range 12 { w := float64((i*7+step*3)%11 + 1) - b.Bids = append(b.Bids, mujica.BookLevel{Price: math.Round((mid-0.01*float64(i+1))*100) / 100, Size: w * 10}) - b.Asks = append(b.Asks, mujica.BookLevel{Price: math.Round((mid+0.01*float64(i))*100) / 100, Size: float64((i*5+step)%9+1) * 10}) + b.Bids = append(b.Bids, finance.BookLevel{Price: math.Round((mid-0.01*float64(i+1))*100) / 100, Size: w * 10}) + b.Asks = append(b.Asks, finance.BookLevel{Price: math.Round((mid+0.01*float64(i))*100) / 100, Size: float64((i*5+step)%9+1) * 10}) } b.Version = step return b @@ -90,12 +92,12 @@ var tradingPages = []page{ type tradingTickerState struct { redUp bool - items []mujica.TickerItem + items []finance.TickerItem } func tradingTickerDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "161", func() tradingTickerState { - return tradingTickerState{items: []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}, {Symbol: "OBLV", Price: 1204.5, ChangePercent: 0.4}, + return tradingTickerState{items: []finance.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}, {Symbol: "OBLV", Price: 1204.5, ChangePercent: 0.4}, {Symbol: "DOLO", Price: 7.25, ChangePercent: -0.8}, {Symbol: "MORT", Price: 33.1, ChangePercent: 1.2}, {Symbol: "TIMR", Price: 61.9, ChangePercent: -2.3}}} }) tradingTrend(c, &d.redUp, log) @@ -105,9 +107,9 @@ func tradingTickerDemo(c *ui.Context, log func(string)) { } log("prices moved") } - ui.Box(c).Width(560).Children(func() { mujica.TickerTape(c, d.items, mujica.TickerTapeOptions{}) }) + ui.Box(c).Width(560).Children(func() { finance.TickerTape(c, d.items, finance.TickerTapeOptions{}) }) formStates(c, func() { - ui.Box(c).Width(280).Children(func() { mujica.TickerTape(c, d.items[:2], mujica.TickerTapeOptions{Speed: 15}) }) + ui.Box(c).Width(280).Children(func() { finance.TickerTape(c, d.items[:2], finance.TickerTapeOptions{Speed: 15}) }) }) } @@ -119,20 +121,20 @@ type tradingQuoteState struct { func tradingQuoteDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "162", func() tradingQuoteState { return tradingQuoteState{price: 412.08} }) tradingTrend(c, &d.redUp, log) - if mujica.Slider(c, &d.price, mujica.SliderOptions{Min: 380, Max: 440, Step: 0.01, Label: "Price", ShowValue: true}).Changed() { + if input.Slider(c, &d.price, input.SliderOptions{Min: 380, Max: 440, Step: 0.01, Label: "Price", ShowValue: true}).Changed() { log(fmt.Sprintf("price %.2f", d.price)) } change := d.price - 417.7 ui.Row(c).Gap(16).Wrap().Children(func() { - mujica.QuoteCard(c, mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: d.price, Change: change, ChangePercent: change / 417.7 * 100}, - mujica.QuoteCardOptions{Chart: func() { - k := mujica.Tokens(c) + finance.QuoteCard(c, finance.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: d.price, Change: change, ChangePercent: change / 417.7 * 100}, + finance.QuoteCardOptions{Chart: func() { + k := core.Tokens(c) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { p.Line(r.X, r.Y+r.H*0.7, r.X+r.W/2, r.Y+r.H*0.3, 1.5, k.Info) p.Line(r.X+r.W/2, r.Y+r.H*0.3, r.X+r.W, r.Y+r.H*(0.5-float32(change)/40), 1.5, k.Info) }) }}) - mujica.QuoteCard(c, mujica.Quote{Symbol: "AVE", Price: 88.2}, mujica.QuoteCardOptions{}) + finance.QuoteCard(c, finance.Quote{Symbol: "AVE", Price: 88.2}, finance.QuoteCardOptions{}) }) } @@ -146,19 +148,19 @@ func tradingPriceDemo(c *ui.Context, log func(string)) { tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Thousands separators", "No flash"}, &d.grouping, &d.noFlash) ui.Row(c).Gap(8).Children(func() { - if mujica.Button(c, "Tick up", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Tick up", input.ButtonOptions{Variant: input.Secondary}).Clicked() { d.price += 12.5 log(fmt.Sprintf("price %.2f", d.price)) } - if mujica.Button(c, "Tick down", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + if input.Button(c, "Tick down", input.ButtonOptions{Variant: input.Secondary}).Clicked() { d.price -= 12.5 log(fmt.Sprintf("price %.2f", d.price)) } }) - mujica.PriceText(c, d.price, mujica.PriceTextOptions{Size: 28, Compare: true, Reference: 18234.5, NoFlash: d.noFlash, NoGrouping: !d.grouping}) + finance.PriceText(c, d.price, finance.PriceTextOptions{Size: 28, Compare: true, Reference: 18234.5, NoFlash: d.noFlash, NoGrouping: !d.grouping}) formStates(c, func() { - mujica.PriceText(c, 0.004521, mujica.PriceTextOptions{Decimals: 6}) - mujica.PriceText(c, 100, mujica.PriceTextOptions{Compare: true, Reference: 100}) + finance.PriceText(c, 0.004521, finance.PriceTextOptions{Decimals: 6}) + finance.PriceText(c, 100, finance.PriceTextOptions{Compare: true, Reference: 100}) }) } @@ -171,21 +173,21 @@ func tradingBadgeDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "164", func() tradingBadgeState { return tradingBadgeState{change: 1.25} }) tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Hide percent"}, &d.hidePercent) - if mujica.Slider(c, &d.change, mujica.SliderOptions{Min: -5, Max: 5, Step: 0.05, Label: "Change", ShowValue: true}).Changed() { + if input.Slider(c, &d.change, input.SliderOptions{Min: -5, Max: 5, Step: 0.05, Label: "Change", ShowValue: true}).Changed() { log(fmt.Sprintf("change %.2f", d.change)) } - mujica.PriceChangeBadge(c, d.change, d.change/1.47, mujica.PriceChangeBadgeOptions{HidePercent: d.hidePercent}) + finance.PriceChangeBadge(c, d.change, d.change/1.47, finance.PriceChangeBadgeOptions{HidePercent: d.hidePercent}) formStates(c, func() { - mujica.PriceChangeBadge(c, 2.4, 1.2, mujica.PriceChangeBadgeOptions{}) - mujica.PriceChangeBadge(c, -2.4, -1.2, mujica.PriceChangeBadgeOptions{}) - mujica.PriceChangeBadge(c, 0, 0, mujica.PriceChangeBadgeOptions{}) + finance.PriceChangeBadge(c, 2.4, 1.2, finance.PriceChangeBadgeOptions{}) + finance.PriceChangeBadge(c, -2.4, -1.2, finance.PriceChangeBadgeOptions{}) + finance.PriceChangeBadge(c, 0, 0, finance.PriceChangeBadgeOptions{}) }) } type tradingBookState struct { redUp, disabled bool step int - book mujica.OrderBookState + book finance.OrderBookState } func tradingBookDemo(c *ui.Context, log func(string)) { @@ -197,7 +199,7 @@ func tradingBookDemo(c *ui.Context, log func(string)) { log(fmt.Sprintf("book update %d", d.step)) } ui.Box(c).Width(320).Children(func() { - r := mujica.OrderBook(c, &d.book, tradingBook(100, d.step), mujica.OrderBookOptions{Depth: 6, Groups: []float64{0.01, 0.05, 0.1}, Disabled: d.disabled}) + r := finance.OrderBook(c, &d.book, tradingBook(100, d.step), finance.OrderBookOptions{Depth: 6, Groups: []float64{0.01, 0.05, 0.1}, Disabled: d.disabled}) if price, side, ok := r.Clicked(); ok { log(fmt.Sprintf("level %v at %.2f", side, price)) } @@ -217,18 +219,18 @@ func tradingLevel2Demo(c *ui.Context, log func(string)) { log(fmt.Sprintf("level 2 update %d", d.step)) } book := tradingBook(100, d.step) - var data mujica.Level2Data + var data finance.Level2Data for i := range 5 { - data.Bids = append(data.Bids, mujica.L2Level{Price: book.Bids[i].Price, Size: book.Bids[i].Size, Orders: int(book.Bids[i].Size / 10)}) - data.Asks = append(data.Asks, mujica.L2Level{Price: book.Asks[i].Price, Size: book.Asks[i].Size, Orders: int(book.Asks[i].Size / 10)}) + data.Bids = append(data.Bids, finance.L2Level{Price: book.Bids[i].Price, Size: book.Bids[i].Size, Orders: int(book.Bids[i].Size / 10)}) + data.Asks = append(data.Asks, finance.L2Level{Price: book.Asks[i].Price, Size: book.Asks[i].Size, Orders: int(book.Asks[i].Size / 10)}) } - ui.Box(c).Width(620).Children(func() { mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{}) }) + ui.Box(c).Width(620).Children(func() { finance.Level2Quotes(c, data, finance.Level2QuotesOptions{}) }) } type tradingTapeState struct { redUp bool - trades []mujica.TradePrint - tape mujica.TimeAndSalesState + trades []finance.TradePrint + tape finance.TimeAndSalesState } func tradingTapeDemo(c *ui.Context, log func(string)) { @@ -238,17 +240,17 @@ func tradingTapeDemo(c *ui.Context, log func(string)) { at := time.Date(2026, 10, 5, 9, 30, 0, 0, time.Local) for range 20 { n := len(d.trades) - side := mujica.SideBuy + side := finance.SideBuy if n%3 == 1 { - side = mujica.SideSell + side = finance.SideSell } - d.trades = append(d.trades, mujica.TradePrint{ID: fmt.Sprint(n), Time: at.Add(time.Duration(n) * time.Second), + d.trades = append(d.trades, finance.TradePrint{ID: fmt.Sprint(n), Time: at.Add(time.Duration(n) * time.Second), Price: 100 + float64(n%17)/100, Size: float64((n*37)%1500 + 1), Side: side}) } log(fmt.Sprintf("%d trades", len(d.trades))) } ui.Box(c).Width(460).Children(func() { - if mujica.TimeAndSales(c, &d.tape, d.trades, mujica.TimeAndSalesOptions{LargeSize: 1000}).Changed() { + if finance.TimeAndSales(c, &d.tape, d.trades, finance.TimeAndSalesOptions{LargeSize: 1000}).Changed() { log(fmt.Sprintf("paused %v", d.tape.Paused)) } }) @@ -257,18 +259,18 @@ func tradingTapeDemo(c *ui.Context, log func(string)) { type tradingLadderState struct { redUp, disabled bool last float64 - ladder mujica.DepthLadderState + ladder finance.DepthLadderState } func tradingLadderDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "168", func() tradingLadderState { return tradingLadderState{last: 100} }) tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Disabled"}, &d.disabled) - if mujica.Slider(c, &d.last, mujica.SliderOptions{Min: 99.9, Max: 100.1, Step: 0.01, Label: "Last price", ShowValue: true}).Changed() { + if input.Slider(c, &d.last, input.SliderOptions{Min: 99.9, Max: 100.1, Step: 0.01, Label: "Last price", ShowValue: true}).Changed() { log(fmt.Sprintf("last %.2f", d.last)) } ui.Box(c).Width(360).Children(func() { - r := mujica.DepthLadder(c, &d.ladder, d.last, tradingBook(100, 0), mujica.DepthLadderOptions{TickSize: 0.01, Rows: 11, Disabled: d.disabled}) + r := finance.DepthLadder(c, &d.ladder, d.last, tradingBook(100, 0), finance.DepthLadderOptions{TickSize: 0.01, Rows: 11, Disabled: d.disabled}) if price, side, ok := r.Clicked(); ok { log(fmt.Sprintf("order %v at %.2f", side, price)) } diff --git a/examples/gallery/pages_trading_code.go b/examples/gallery/pages_trading_code.go index b5fdbc7..eb5682c 100644 --- a/examples/gallery/pages_trading_code.go +++ b/examples/gallery/pages_trading_code.go @@ -2,80 +2,80 @@ package main // tradingCode holds each page's copyable snippet, the same as its doc and Example. var tradingCode = map[string]string{ - "TickerTape": `items := []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} + "TickerTape": `items := []finance.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TickerTape(c, items, mujica.TickerTapeOptions{}) + core.Use(c, core.Settings{}) + finance.TickerTape(c, items, finance.TickerTapeOptions{}) } ui.Render(view, 400, 300, 1)`, - "QuoteCard": `quote := mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} + "QuoteCard": `quote := finance.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.QuoteCard(c, quote, mujica.QuoteCardOptions{}) + core.Use(c, core.Settings{}) + finance.QuoteCard(c, quote, finance.QuoteCardOptions{}) } ui.Render(view, 400, 300, 1)`, "PriceText": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.UseTrend(c, mujica.RedUp) - mujica.PriceText(c, 18234.5, mujica.PriceTextOptions{Compare: true, Reference: 18000}) + core.Use(c, core.Settings{}) + core.UseTrend(c, core.RedUp) + finance.PriceText(c, 18234.5, finance.PriceTextOptions{Compare: true, Reference: 18000}) } ui.Render(view, 400, 300, 1)`, "PriceChangeBadge": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PriceChangeBadge(c, -5.62, -1.35, mujica.PriceChangeBadgeOptions{}) + core.Use(c, core.Settings{}) + finance.PriceChangeBadge(c, -5.62, -1.35, finance.PriceChangeBadgeOptions{}) } ui.Render(view, 400, 300, 1)`, - "OrderBook": `var state mujica.OrderBookState -book := mujica.OrderBookData{ - Bids: []mujica.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, - Asks: []mujica.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, + "OrderBook": `var state finance.OrderBookState +book := finance.OrderBookData{ + Bids: []finance.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, + Asks: []finance.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.OrderBook(c, &state, book, mujica.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { + core.Use(c, core.Settings{}) + if price, side, ok := finance.OrderBook(c, &state, book, finance.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { fmt.Println("order at", price, side) } } ui.Render(view, 400, 300, 1)`, - "Level2Quotes": `data := mujica.Level2Data{ - Bids: []mujica.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, - Asks: []mujica.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, + "Level2Quotes": `data := finance.Level2Data{ + Bids: []finance.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, + Asks: []finance.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{}) + core.Use(c, core.Settings{}) + finance.Level2Quotes(c, data, finance.Level2QuotesOptions{}) } ui.Render(view, 400, 300, 1)`, - "TimeAndSales": `var state mujica.TimeAndSalesState -trades := []mujica.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: mujica.SideBuy}} + "TimeAndSales": `var state finance.TimeAndSalesState +trades := []finance.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: finance.SideBuy}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TimeAndSales(c, &state, trades, mujica.TimeAndSalesOptions{LargeSize: 1000}) + core.Use(c, core.Settings{}) + finance.TimeAndSales(c, &state, trades, finance.TimeAndSalesOptions{LargeSize: 1000}) } ui.Render(view, 400, 300, 1)`, - "DepthLadder": `var state mujica.DepthLadderState -book := mujica.OrderBookData{Bids: []mujica.BookLevel{{Price: 100, Size: 5}}, Asks: []mujica.BookLevel{{Price: 100.01, Size: 2}}} + "DepthLadder": `var state finance.DepthLadderState +book := finance.OrderBookData{Bids: []finance.BookLevel{{Price: 100, Size: 5}}, Asks: []finance.BookLevel{{Price: 100.01, Size: 2}}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if price, side, ok := mujica.DepthLadder(c, &state, 100, book, mujica.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { + core.Use(c, core.Settings{}) + if price, side, ok := finance.DepthLadder(c, &state, 100, book, finance.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { fmt.Println("place", side, "at", price) } } ui.Render(view, 400, 300, 1)`, "SpreadIndicator": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{WarnPercent: 0.5}) + core.Use(c, core.Settings{}) + finance.SpreadIndicator(c, 99.95, 100.05, finance.SpreadIndicatorOptions{WarnPercent: 0.5}) } ui.Render(view, 400, 300, 1)`, "BidAskBar": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.BidAskBar(c, 6234, 3820, mujica.BidAskBarOptions{}) + core.Use(c, core.Settings{}) + finance.BidAskBar(c, 6234, 3820, finance.BidAskBarOptions{}) } ui.Render(view, 400, 300, 1)`, - "OrderEntry": `var state mujica.OrderEntryState + "OrderEntry": `var state finance.OrderEntryState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if order, ok := mujica.OrderEntry(c, &state, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { + core.Use(c, core.Settings{}) + if order, ok := finance.OrderEntry(c, &state, finance.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { fmt.Println("place", order.Quantity) state.EndSubmit("") } @@ -83,68 +83,68 @@ view := func(c *ui.Context) { ui.Render(view, 400, 300, 1)`, "QuickTradeButtons": `quantity := 10.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if side, q, ok := mujica.QuickTradeButtons(c, &quantity, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { + core.Use(c, core.Settings{}) + if side, q, ok := finance.QuickTradeButtons(c, &quantity, finance.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { fmt.Println(side, q) } } ui.Render(view, 400, 300, 1)`, - "PositionTable": `var state mujica.DataTableState[string] -rows := []mujica.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} + "PositionTable": `var state data.DataTableState[string] +rows := []finance.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.PositionTable(c, &state, rows, mujica.PositionTableOptions{}).Acted(); ok { + core.Use(c, core.Settings{}) + if id, ok := finance.PositionTable(c, &state, rows, finance.PositionTableOptions{}).Acted(); ok { fmt.Println("close", id) } } ui.Render(view, 400, 300, 1)`, - "OrderTable": `var state mujica.OrderTableState -rows := []mujica.OrderRow{{ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial}} + "OrderTable": `var state finance.OrderTableState +rows := []finance.OrderRow{{ID: "o1", Symbol: "MUJC", Side: finance.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: finance.OrderPartial}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if id, ok := mujica.OrderTable(c, &state, rows, mujica.OrderTableOptions{}).Acted(); ok { + core.Use(c, core.Settings{}) + if id, ok := finance.OrderTable(c, &state, rows, finance.OrderTableOptions{}).Acted(); ok { fmt.Println("cancel", id) } } ui.Render(view, 400, 300, 1)`, - "TradeHistoryTable": `var state mujica.TradeHistoryTableState -rows := []mujica.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: mujica.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} + "TradeHistoryTable": `var state finance.TradeHistoryTableState +rows := []finance.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: finance.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if fills, ok := mujica.TradeHistoryTable(c, &state, rows, mujica.TradeHistoryTableOptions{}).Exported(); ok { + core.Use(c, core.Settings{}) + if fills, ok := finance.TradeHistoryTable(c, &state, rows, finance.TradeHistoryTableOptions{}).Exported(); ok { fmt.Println("export", len(fills)) } } ui.Render(view, 400, 300, 1)`, "PnLDisplay": `hidden := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, mujica.PnLDisplayOptions{Currency: "$"}) + core.Use(c, core.Settings{}) + finance.PnLDisplay(c, finance.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, finance.PnLDisplayOptions{Currency: "$"}) } ui.Render(view, 400, 300, 1)`, "LeverageSlider": `leverage := 5.0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.LeverageSlider(c, &leverage, mujica.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { + core.Use(c, core.Settings{}) + if finance.LeverageSlider(c, &leverage, finance.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { fmt.Println("leverage", leverage) } } ui.Render(view, 400, 300, 1)`, "MarginIndicator": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.MarginIndicator(c, 0.86, mujica.MarginIndicatorOptions{}) + core.Use(c, core.Settings{}) + finance.MarginIndicator(c, 0.86, finance.MarginIndicatorOptions{}) } ui.Render(view, 400, 300, 1)`, "RiskMeter": `view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.RiskMeter(c, 4, mujica.RiskMeterOptions{}) + core.Use(c, core.Settings{}) + finance.RiskMeter(c, 4, finance.RiskMeterOptions{}) } ui.Render(view, 400, 300, 1)`, "OrderConfirmDialog": `open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.OrderConfirmDialog(c, &open, mujica.OrderConfirmDialogOptions{ - Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, + core.Use(c, core.Settings{}) + r := finance.OrderConfirmDialog(c, &open, finance.OrderConfirmDialogOptions{ + Lines: []finance.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, Risk: "Leveraged products can lose more than the deposit.", Acknowledge: "I understand the risk", }) diff --git a/examples/gallery/pages_trading_orders.go b/examples/gallery/pages_trading_orders.go index cecdc50..8096edb 100644 --- a/examples/gallery/pages_trading_orders.go +++ b/examples/gallery/pages_trading_orders.go @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type tradingSpreadState struct { @@ -15,17 +17,17 @@ type tradingSpreadState struct { func tradingSpreadDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "169", func() tradingSpreadState { return tradingSpreadState{ask: 100.05, warn: 0.5} }) - if mujica.Slider(c, &d.ask, mujica.SliderOptions{Min: 99.5, Max: 101, Step: 0.01, Label: "Ask (bid 99.95)", ShowValue: true}).Changed() { + if input.Slider(c, &d.ask, input.SliderOptions{Min: 99.5, Max: 101, Step: 0.01, Label: "Ask (bid 99.95)", ShowValue: true}).Changed() { log(fmt.Sprintf("ask %.2f", d.ask)) } - if mujica.Slider(c, &d.warn, mujica.SliderOptions{Min: 0, Max: 2, Step: 0.1, Label: "Warn at percent", ShowValue: true}).Changed() { + if input.Slider(c, &d.warn, input.SliderOptions{Min: 0, Max: 2, Step: 0.1, Label: "Warn at percent", ShowValue: true}).Changed() { log(fmt.Sprintf("warn %.1f%%", d.warn)) } - mujica.SpreadIndicator(c, 99.95, d.ask, mujica.SpreadIndicatorOptions{WarnPercent: d.warn}) + finance.SpreadIndicator(c, 99.95, d.ask, finance.SpreadIndicatorOptions{WarnPercent: d.warn}) formStates(c, func() { - mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{}) - mujica.SpreadIndicator(c, 99, 101, mujica.SpreadIndicatorOptions{WarnPercent: 0.5}) - mujica.SpreadIndicator(c, 100.1, 100, mujica.SpreadIndicatorOptions{}) + finance.SpreadIndicator(c, 99.95, 100.05, finance.SpreadIndicatorOptions{}) + finance.SpreadIndicator(c, 99, 101, finance.SpreadIndicatorOptions{WarnPercent: 0.5}) + finance.SpreadIndicator(c, 100.1, 100, finance.SpreadIndicatorOptions{}) }) } @@ -41,16 +43,16 @@ func tradingBarDemo(c *ui.Context, log func(string)) { name string p *float64 }{{"Bid volume", &d.bid}, {"Ask volume", &d.ask}} { - if mujica.Slider(c, v.p, mujica.SliderOptions{Min: 0, Max: 10000, Step: 1, Label: v.name, ShowValue: true}).Changed() { + if input.Slider(c, v.p, input.SliderOptions{Min: 0, Max: 10000, Step: 1, Label: v.name, ShowValue: true}).Changed() { log(fmt.Sprintf("%s %.0f", v.name, *v.p)) } } - ui.Box(c).Width(360).Children(func() { mujica.BidAskBar(c, d.bid, d.ask, mujica.BidAskBarOptions{}) }) + ui.Box(c).Width(360).Children(func() { finance.BidAskBar(c, d.bid, d.ask, finance.BidAskBarOptions{}) }) } type tradingEntryState struct { redUp, disabled, reject bool - entry mujica.OrderEntryState + entry finance.OrderEntryState } func tradingEntryDemo(c *ui.Context, log func(string)) { @@ -71,8 +73,8 @@ func tradingEntryDemo(c *ui.Context, log func(string)) { } tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Disabled", "Broker rejects"}, &d.disabled, &d.reject) - r := mujica.OrderEntry(c, &d.entry, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2, MaxQuantity: 5000, Disabled: d.disabled, - Check: func(o mujica.OrderDraft, v uint64) { + r := finance.OrderEntry(c, &d.entry, finance.OrderEntryOptions{Symbol: "MUJC", Last: 100.2, MaxQuantity: 5000, Disabled: d.disabled, + Check: func(o finance.OrderDraft, v uint64) { log(fmt.Sprintf("check started v%d", v)) go func() { time.Sleep(600 * time.Millisecond) @@ -108,7 +110,7 @@ func tradingQuickDemo(c *ui.Context, log func(string)) { tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Confirm first", "Disabled"}, &d.confirm, &d.disabled) ui.Box(c).Width(400).Children(func() { - if side, q, ok := mujica.QuickTradeButtons(c, &d.qty, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: d.confirm, Disabled: d.disabled}).Traded(); ok { + if side, q, ok := finance.QuickTradeButtons(c, &d.qty, finance.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: d.confirm, Disabled: d.disabled}).Traded(); ok { log(fmt.Sprintf("trade %v %v", side, q)) } }) @@ -116,13 +118,13 @@ func tradingQuickDemo(c *ui.Context, log func(string)) { type tradingPositionsState struct { redUp, disabled bool - rows []mujica.PositionRow - table mujica.DataTableState[string] + rows []finance.PositionRow + table data.DataTableState[string] } func tradingPositionsDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "173", func() tradingPositionsState { - return tradingPositionsState{rows: []mujica.PositionRow{ + return tradingPositionsState{rows: []finance.PositionRow{ {ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}, {ID: "p2", Symbol: "AVE", Quantity: 50, AvgCost: 90, Price: 88.2}, {ID: "p3", Symbol: "OBLV", Quantity: 8, AvgCost: 1180, Price: 1204.5}, @@ -131,7 +133,7 @@ func tradingPositionsDemo(c *ui.Context, log func(string)) { tradingTrend(c, &d.redUp, log) formToggles(c, []string{"Disabled"}, &d.disabled) ui.Box(c).Width(1000).Height(260).Children(func() { - r := mujica.PositionTable(c, &d.table, d.rows, mujica.PositionTableOptions{Disabled: d.disabled}) + r := finance.PositionTable(c, &d.table, d.rows, finance.PositionTableOptions{Disabled: d.disabled}) if id, ok := r.Acted(); ok { for i, p := range d.rows { if p.ID == id { @@ -150,26 +152,26 @@ func tradingPositionsDemo(c *ui.Context, log func(string)) { type tradingOrdersState struct { disabled bool - rows []mujica.OrderRow - table mujica.OrderTableState + rows []finance.OrderRow + table finance.OrderTableState version int } func tradingOrdersDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "174", func() tradingOrdersState { - return tradingOrdersState{rows: []mujica.OrderRow{ - {ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Type: mujica.OrderLimit, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial}, - {ID: "o2", Symbol: "AVE", Side: mujica.SideSell, Type: mujica.OrderMarket, Quantity: 10, Filled: 10, Status: mujica.OrderFilled}, - {ID: "o3", Symbol: "OBLV", Side: mujica.SideBuy, Type: mujica.OrderStop, Price: 12, Quantity: 5, Status: mujica.OrderCanceled}, - {ID: "o4", Symbol: "DOLO", Side: mujica.SideSell, Type: mujica.OrderLimit, Price: 7.25, Quantity: 30, Status: mujica.OrderOpen}, + return tradingOrdersState{rows: []finance.OrderRow{ + {ID: "o1", Symbol: "MUJC", Side: finance.SideBuy, Type: finance.OrderLimit, Price: 99.5, Quantity: 100, Filled: 40, Status: finance.OrderPartial}, + {ID: "o2", Symbol: "AVE", Side: finance.SideSell, Type: finance.OrderMarket, Quantity: 10, Filled: 10, Status: finance.OrderFilled}, + {ID: "o3", Symbol: "OBLV", Side: finance.SideBuy, Type: finance.OrderStop, Price: 12, Quantity: 5, Status: finance.OrderCanceled}, + {ID: "o4", Symbol: "DOLO", Side: finance.SideSell, Type: finance.OrderLimit, Price: 7.25, Quantity: 30, Status: finance.OrderOpen}, }} }) formToggles(c, []string{"Disabled"}, &d.disabled) ui.Box(c).Width(1000).Height(300).Children(func() { - if id, ok := mujica.OrderTable(c, &d.table, d.rows, mujica.OrderTableOptions{Disabled: d.disabled, Version: d.version}).Acted(); ok { + if id, ok := finance.OrderTable(c, &d.table, d.rows, finance.OrderTableOptions{Disabled: d.disabled, Version: d.version}).Acted(); ok { for i := range d.rows { if d.rows[i].ID == id { - d.rows[i].Status = mujica.OrderCanceled + d.rows[i].Status = finance.OrderCanceled } } d.version++ @@ -180,26 +182,26 @@ func tradingOrdersDemo(c *ui.Context, log func(string)) { type tradingHistoryState struct { disabled bool - rows []mujica.FillRow - table mujica.TradeHistoryTableState + rows []finance.FillRow + table finance.TradeHistoryTableState } func tradingHistoryDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "175", func() tradingHistoryState { - var rows []mujica.FillRow + var rows []finance.FillRow for i := range 30 { - side := mujica.SideBuy + side := finance.SideBuy if i%3 == 0 { - side = mujica.SideSell + side = finance.SideSell } - rows = append(rows, mujica.FillRow{ID: fmt.Sprint(i), Time: time.Date(2026, 9, 10+i/2, 9+i%6, 0, 0, 0, time.Local), + rows = append(rows, finance.FillRow{ID: fmt.Sprint(i), Time: time.Date(2026, 9, 10+i/2, 9+i%6, 0, 0, 0, time.Local), Symbol: []string{"MUJC", "AVE", "OBLV"}[i%3], Side: side, Price: 100 + float64(i), Quantity: float64(i%5+1) * 10, Fee: 0.5}) } return tradingHistoryState{rows: rows} }) formToggles(c, []string{"Disabled"}, &d.disabled) ui.Box(c).Width(1000).Height(360).Children(func() { - if fills, ok := mujica.TradeHistoryTable(c, &d.table, d.rows, mujica.TradeHistoryTableOptions{Disabled: d.disabled}).Exported(); ok { + if fills, ok := finance.TradeHistoryTable(c, &d.table, d.rows, finance.TradeHistoryTableOptions{Disabled: d.disabled}).Exported(); ok { log(fmt.Sprintf("export %d fills", len(fills))) } }) @@ -212,7 +214,7 @@ type tradingPnLState struct { func tradingPnLDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "176", func() tradingPnLState { return tradingPnLState{} }) tradingTrend(c, &d.redUp, log) - if mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &d.hidden, mujica.PnLDisplayOptions{Currency: "$"}).Changed() { + if finance.PnLDisplay(c, finance.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &d.hidden, finance.PnLDisplayOptions{Currency: "$"}).Changed() { log(fmt.Sprintf("hidden %v", d.hidden)) } } @@ -226,7 +228,7 @@ func tradingLeverageDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "177", func() tradingLeverageState { return tradingLeverageState{value: 5} }) formToggles(c, []string{"Disabled"}, &d.disabled) ui.Box(c).Width(380).Children(func() { - if mujica.LeverageSlider(c, &d.value, mujica.LeverageSliderOptions{Warning: 10, Danger: 50, Disabled: d.disabled}).Changed() { + if finance.LeverageSlider(c, &d.value, finance.LeverageSliderOptions{Warning: 10, Danger: 50, Disabled: d.disabled}).Changed() { log(fmt.Sprintf("leverage applied %v×", d.value)) } }) @@ -236,13 +238,13 @@ type tradingMarginState struct{ ratio float64 } func tradingMarginDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "178", func() tradingMarginState { return tradingMarginState{ratio: 0.55} }) - if mujica.Slider(c, &d.ratio, mujica.SliderOptions{Min: 0, Max: 1.4, Step: 0.01, Label: "Margin ratio", ShowValue: true}).Changed() { + if input.Slider(c, &d.ratio, input.SliderOptions{Min: 0, Max: 1.4, Step: 0.01, Label: "Margin ratio", ShowValue: true}).Changed() { log(fmt.Sprintf("ratio %.2f", d.ratio)) } - ui.Box(c).Width(360).Children(func() { mujica.MarginIndicator(c, d.ratio, mujica.MarginIndicatorOptions{}) }) + ui.Box(c).Width(360).Children(func() { finance.MarginIndicator(c, d.ratio, finance.MarginIndicatorOptions{}) }) formStates(c, func() { for _, r := range []float64{0.3, 0.85, 1.1} { - ui.Box(c).Width(260).Children(func() { mujica.MarginIndicator(c, r, mujica.MarginIndicatorOptions{}) }) + ui.Box(c).Width(260).Children(func() { finance.MarginIndicator(c, r, finance.MarginIndicatorOptions{}) }) } }) } @@ -251,10 +253,10 @@ type tradingRiskState struct{ level float64 } func tradingRiskDemo(c *ui.Context, log func(string)) { d := ui.Local(c.Root(), "179", func() tradingRiskState { return tradingRiskState{level: 3} }) - if mujica.Slider(c, &d.level, mujica.SliderOptions{Min: 1, Max: 5, Step: 1, Label: "Level", ShowValue: true}).Changed() { + if input.Slider(c, &d.level, input.SliderOptions{Min: 1, Max: 5, Step: 1, Label: "Level", ShowValue: true}).Changed() { log(fmt.Sprintf("level %.0f", d.level)) } - ui.Box(c).Width(360).Children(func() { mujica.RiskMeter(c, int(d.level), mujica.RiskMeterOptions{}) }) + ui.Box(c).Width(360).Children(func() { finance.RiskMeter(c, int(d.level), finance.RiskMeterOptions{}) }) } type tradingConfirmState struct { @@ -272,8 +274,8 @@ func tradingConfirmDemo(c *ui.Context, log func(string)) { if d.acknowledge { ack = "I understand leveraged products can lose more than the deposit" } - r := mujica.OrderConfirmDialog(c, &d.open, mujica.OrderConfirmDialogOptions{ - Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Side", Value: "Buy"}, {Label: "Type", Value: "Limit 100.20"}, + r := finance.OrderConfirmDialog(c, &d.open, finance.OrderConfirmDialogOptions{ + Lines: []finance.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Side", Value: "Buy"}, {Label: "Type", Value: "Limit 100.20"}, {Label: "Quantity", Value: "100"}, {Label: "Estimated amount", Value: "10,020.00"}}, Risk: "5× leverage: a 20% move against you wipes out the margin.", Acknowledge: ack, }) diff --git a/examples/gallery/web.go b/examples/gallery/web.go index 8180d6b..b45eea2 100644 --- a/examples/gallery/web.go +++ b/examples/gallery/web.go @@ -33,7 +33,7 @@ type mirror struct { copied bool probes int cursor ui.Cursor - down *input + down *webEvent enc png.Encoder } diff --git a/examples/gallery/web_input.go b/examples/gallery/web_input.go index 47230d6..90fde72 100644 --- a/examples/gallery/web_input.go +++ b/examples/gallery/web_input.go @@ -9,8 +9,8 @@ import ( "github.com/egoist/mygo/ui" ) -// input is one browser event. -type input struct { +// webEvent is one browser event. +type webEvent struct { Type string `json:"type"` X float32 `json:"x"` Y float32 `json:"y"` @@ -30,7 +30,7 @@ type input struct { } func (m *mirror) serveInput(w http.ResponseWriter, r *http.Request) { - var in input + var in webEvent if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -51,7 +51,7 @@ func (m *mirror) serveInput(w http.ResponseWriter, r *http.Request) { } // apply sends one event to the tester; the caller holds mu. -func (m *mirror) apply(in input) error { +func (m *mirror) apply(in webEvent) error { mods := modifiers(in.Mods) switch in.Type { case "down": @@ -127,7 +127,7 @@ func modifiers(m []bool) ui.Modifiers { } // applyWatchingClipboard applies in and notes any clipboard write it caused, even of equal text. -func (m *mirror) applyWatchingClipboard(in input) error { +func (m *mirror) applyWatchingClipboard(in webEvent) error { if in.Type == "paste" || in.Type == "command" && in.Text == "paste" { return m.apply(in) } diff --git a/examples/gallery/web_keys.go b/examples/gallery/web_keys.go index d57b520..e5556b5 100644 --- a/examples/gallery/web_keys.go +++ b/examples/gallery/web_keys.go @@ -1,6 +1,8 @@ package main -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" +) // keyCodes maps KeyboardEvent.code to MyGo keys. var keyCodes = map[string]ui.Key{ diff --git a/examples/gallery/web_test.go b/examples/gallery/web_test.go index b8798f9..0c7e8fc 100644 --- a/examples/gallery/web_test.go +++ b/examples/gallery/web_test.go @@ -13,7 +13,8 @@ import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // mirrorRig serves a one-field view through the web mirror. @@ -28,9 +29,9 @@ func newMirrorRig(t *testing.T) *mirrorRig { r := &mirrorRig{t: t} var h http.Handler r.m, h = newMirror(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(40).Children(func() { - mujica.TextInput(c, &r.text, mujica.TextInputOptions{Label: "Ledger"}) + input.TextInput(c, &r.text, input.TextInputOptions{Label: "Ledger"}) }) }) r.srv = httptest.NewServer(h) @@ -38,7 +39,7 @@ func newMirrorRig(t *testing.T) *mirrorRig { return r } -func (r *mirrorRig) post(in input) int { +func (r *mirrorRig) post(in webEvent) int { r.t.Helper() body, err := json.Marshal(in) if err != nil { @@ -97,15 +98,15 @@ func TestMirrorServesPageAndFirstNotice(t *testing.T) { func TestMirrorTypingAndIMEReachTheView(t *testing.T) { r := newMirrorRig(t) before := r.get("/frame.png") - if code := r.post(input{Type: "key", Code: "Tab"}); code != http.StatusNoContent { + if code := r.post(webEvent{Type: "key", Code: "Tab"}); code != http.StatusNoContent { t.Fatalf("tab: %d", code) } for _, ch := range "Ave" { - r.post(input{Type: "key", Code: "Key" + strings.ToUpper(string(ch)), Text: string(ch), Mods: []bool{ch == 'A'}}) + r.post(webEvent{Type: "key", Code: "Key" + strings.ToUpper(string(ch)), Text: string(ch), Mods: []bool{ch == 'A'}}) } - r.post(input{Type: "compose", Text: "宫廷", Caret: 2}) - r.post(input{Type: "compose"}) - r.post(input{Type: "text", Text: "宫廷"}) + r.post(webEvent{Type: "compose", Text: "宫廷", Caret: 2}) + r.post(webEvent{Type: "compose"}) + r.post(webEvent{Type: "text", Text: "宫廷"}) if got := r.value(); got != "Ave宫廷" { t.Fatalf("value %q, want %q", got, "Ave宫廷") } @@ -116,7 +117,7 @@ func TestMirrorTypingAndIMEReachTheView(t *testing.T) { func TestMirrorRejectsBadInput(t *testing.T) { r := newMirrorRig(t) - for name, in := range map[string]input{ + for name, in := range map[string]webEvent{ "unknown type": {Type: "teleport"}, "unmapped code": {Type: "key", Code: "Hyper"}, "tiny window": {Type: "resize", W: 10, H: 10, Scale: 1}, @@ -143,7 +144,7 @@ func mirrorWith(t *testing.T, view func(c *ui.Context)) (*mirror, *httptest.Serv return m, srv } -func postInput(t *testing.T, srv *httptest.Server, in input) { +func postInput(t *testing.T, srv *httptest.Server, in webEvent) { t.Helper() body, err := json.Marshal(in) if err != nil { @@ -163,7 +164,7 @@ func TestMirrorTimersFireWithoutInput(t *testing.T) { start := time.Now() var late bool m, _ := mirrorWith(t, func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) late = c.Now().Sub(start) > 300*time.Millisecond if !late { c.After(350 * time.Millisecond) @@ -180,25 +181,25 @@ func TestMirrorTimersFireWithoutInput(t *testing.T) { func TestMirrorModifiedClickNeedsReleaseInPlace(t *testing.T) { clicks := 0 _, srv := mirrorWith(t, func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Children(func() { - if mujica.Button(c, "Seal", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Seal", input.ButtonOptions{}).Clicked() { clicks++ } }) }) shift := []bool{true} - postInput(t, srv, input{Type: "down", X: 40, Y: 35, Mods: shift}) - postInput(t, srv, input{Type: "up", X: 400, Y: 400, Mods: shift}) - postInput(t, srv, input{Type: "down", X: 40, Y: 35, Mods: shift}) - postInput(t, srv, input{Type: "up", X: 41, Y: 35, Mods: shift}) + postInput(t, srv, webEvent{Type: "down", X: 40, Y: 35, Mods: shift}) + postInput(t, srv, webEvent{Type: "up", X: 400, Y: 400, Mods: shift}) + postInput(t, srv, webEvent{Type: "down", X: 40, Y: 35, Mods: shift}) + postInput(t, srv, webEvent{Type: "up", X: 41, Y: 35, Mods: shift}) if clicks != 1 { t.Fatalf("clicks %d, want 1 (released elsewhere must not click)", clicks) } } func TestMirrorForwardsRepeatedCopy(t *testing.T) { - m, _ := mirrorWith(t, func(c *ui.Context) { mujica.Use(c, mujica.Settings{}) }) + m, _ := mirrorWith(t, func(c *ui.Context) { core.Use(c, core.Settings{}) }) ch := make(chan []byte, 8) m.mu.Lock() m.subs[ch] = struct{}{} @@ -224,8 +225,8 @@ func TestMirrorForwardsRepeatedCopy(t *testing.T) { func TestMirrorMapsNumpad(t *testing.T) { r := newMirrorRig(t) - r.post(input{Type: "key", Code: "Tab"}) - if code := r.post(input{Type: "key", Code: "Numpad7", Text: "7"}); code != http.StatusNoContent { + r.post(webEvent{Type: "key", Code: "Tab"}) + if code := r.post(webEvent{Type: "key", Code: "Numpad7", Text: "7"}); code != http.StatusNoContent { t.Fatalf("numpad: %d", code) } if got := r.value(); got != "7" { @@ -235,9 +236,9 @@ func TestMirrorMapsNumpad(t *testing.T) { func TestMirrorForwardsEveryAnnouncement(t *testing.T) { m, srv := mirrorWith(t, func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Children(func() { - if mujica.Button(c, "Ring", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Ring", input.ButtonOptions{}).Clicked() { c.Announce("bell rung") } }) @@ -247,8 +248,8 @@ func TestMirrorForwardsEveryAnnouncement(t *testing.T) { m.subs[ch] = struct{}{} m.mu.Unlock() for range 2 { - postInput(t, srv, input{Type: "down", X: 40, Y: 35}) - postInput(t, srv, input{Type: "up", X: 40, Y: 35}) + postInput(t, srv, webEvent{Type: "down", X: 40, Y: 35}) + postInput(t, srv, webEvent{Type: "up", X: 40, Y: 35}) } heard := 0 for len(ch) > 0 { @@ -265,9 +266,9 @@ func TestMirrorModifiedClickReleasedJustOutsideDoesNotClick(t *testing.T) { clicks := 0 var box ui.Rect m, srv := mirrorWith(t, func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { - b := mujica.Button(c, "Seal", mujica.ButtonOptions{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) box = b.Bounds() if b.Clicked() { clicks++ @@ -280,8 +281,8 @@ func TestMirrorModifiedClickReleasedJustOutsideDoesNotClick(t *testing.T) { m.mu.Unlock() shift := []bool{true} y := r.Y + r.H/2 - postInput(t, srv, input{Type: "down", X: r.X + r.W - 1, Y: y, Mods: shift}) - postInput(t, srv, input{Type: "up", X: r.X + r.W + 2, Y: y, Mods: shift}) + postInput(t, srv, webEvent{Type: "down", X: r.X + r.W - 1, Y: y, Mods: shift}) + postInput(t, srv, webEvent{Type: "up", X: r.X + r.W + 2, Y: y, Mods: shift}) if clicks != 0 { t.Fatalf("clicks %d after releasing 2 DIP outside", clicks) } @@ -289,9 +290,9 @@ func TestMirrorModifiedClickReleasedJustOutsideDoesNotClick(t *testing.T) { func TestMirrorNotifiesEveryCopyOfEqualText(t *testing.T) { m, srv := mirrorWith(t, func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Children(func() { - if mujica.Button(c, "Copy", mujica.ButtonOptions{}).Clicked() { + if input.Button(c, "Copy", input.ButtonOptions{}).Clicked() { c.WriteClipboard("snippet") } }) @@ -301,8 +302,8 @@ func TestMirrorNotifiesEveryCopyOfEqualText(t *testing.T) { m.subs[ch] = struct{}{} m.mu.Unlock() for range 2 { - postInput(t, srv, input{Type: "down", X: 40, Y: 35}) - postInput(t, srv, input{Type: "up", X: 40, Y: 35}) + postInput(t, srv, webEvent{Type: "down", X: 40, Y: 35}) + postInput(t, srv, webEvent{Type: "up", X: 40, Y: 35}) } copies := 0 for len(ch) > 0 { diff --git a/examples/gallery/zz_account_test.go b/examples/gallery/zz_account_test.go index e986718..f9644e6 100644 --- a/examples/gallery/zz_account_test.go +++ b/examples/gallery/zz_account_test.go @@ -1,10 +1,13 @@ package main import ( - "github.com/egoist/mygo/ui" - mujica "mujicaui" "strings" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/account" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func accountClick(t *testing.T, tt *ui.Tester, label string) { @@ -25,7 +28,7 @@ func TestAccountPagesInteractiveEvents(t *testing.T) { "463": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "Dark") }, "464": func(t *testing.T, tt *ui.Tester) { accountClick(t, tt, "#355E80") }, "465": func(t *testing.T, tt *ui.Tester) { - value := mujica.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} + value := account.Shortcut{Mods: ui.Cmd | ui.Shift, Key: ui.KeyP} accountClick(t, tt, "Command palette: "+value.String()) tt.Key(ui.Cmd, ui.KeyK) }, @@ -86,7 +89,7 @@ func TestAccountPagesInteractiveEvents(t *testing.T) { } var events []string tt := ui.NewTester(func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Gap(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) }) }, 1100, 800) actions[p.ID](t, tt) diff --git a/examples/minimal/main.go b/examples/minimal/main.go index ac2c7ca..015e355 100644 --- a/examples/minimal/main.go +++ b/examples/minimal/main.go @@ -6,16 +6,17 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type app struct{ name string } func (a *app) view(c *ui.Context) { - mujica.Use(c, mujica.Settings{Locale: mujica.ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) ui.Column(c).Fill().Center().Gap(12).Children(func() { - mujica.TextInput(c, &a.name, mujica.TextInputOptions{Placeholder: "姓名"}) - if mujica.Button(c, "觐见", mujica.ButtonOptions{}).Clicked() { + input.TextInput(c, &a.name, input.TextInputOptions{Placeholder: "姓名"}) + if input.Button(c, "觐见", input.ButtonOptions{}).Clicked() { log.Println("hello", a.name) } }) diff --git a/activity_feed.go b/feedback/activity_feed.go similarity index 75% rename from activity_feed.go rename to feedback/activity_feed.go index c0406b1..b66b6bf 100644 --- a/activity_feed.go +++ b/feedback/activity_feed.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Activity is one event of an ActivityFeed: Actor did Action to Object at Time. @@ -25,12 +27,12 @@ type ActivityFeedOptions struct { // Location decides which day an event falls on; nil is time.Local. Location *time.Location // Today fixes the day called Today; zero uses the clock. - Today Date + Today core.Date } // activityRow is a day header (item -1) or an event. type activityRow struct { - day Date + day core.Date item int } @@ -52,12 +54,12 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui panic("mujica: ActivityFeed event needs an ID, Actor, Action and Time") } } - k := Tokens(c) - d := Density(c) - sc := ui.Scroll(c).MinHeight(0).Role(ui.RoleList).Label(msg(c, "activity_feed.label")) + k := core.Tokens(c) + d := core.Density(c) + sc := ui.Scroll(c).MinHeight(0).Role(ui.RoleList).Label(core.Msg(c, "activity_feed.label")) if len(items) == 0 { sc.Children(func() { - EmptyState(c, EmptyStateOptions{Title: msg(c, "activity_feed.empty"), Description: msg(c, "activity_feed.empty_hint"), Icon: icons.Must("clock")}) + EmptyState(c, EmptyStateOptions{Title: core.Msg(c, "activity_feed.empty"), Description: core.Msg(c, "activity_feed.empty_hint"), Icon: icons.Must("clock")}) }) return sc } @@ -66,9 +68,9 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui if cache.n != len(items) || cache.first != items[0].ID || cache.last != items[len(items)-1].ID || cache.loc != loc { cache.n, cache.first, cache.last, cache.loc = len(items), items[0].ID, items[len(items)-1].ID, loc cache.rows = cache.rows[:0] - var prev Date + var prev core.Date for i, a := range items { - if day := DateOf(a.Time.In(loc)); day != prev || i == 0 { + if day := core.DateOf(a.Time.In(loc)); day != prev || i == 0 { cache.rows = append(cache.rows, activityRow{day: day, item: -1}) prev = day } @@ -85,7 +87,7 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui } first := max(0, int(cache.scroll.Y/rowH)-2) last := min(len(cache.rows), int((cache.scroll.Y+max(view.H, rowH))/rowH)+3) - today := datetimeToday(c, opts.Today) + today := core.DatetimeToday(c, opts.Today) sc.Children(func() { ui.Box(c).FillWidth().Height(rowH * float32(len(cache.rows))).Children(func() { for r := first; r < last; r++ { @@ -104,18 +106,18 @@ func ActivityFeed(c *ui.Context, items []Activity, opts ActivityFeedOptions) *ui } // activityFeedHeader is a day's title: Today, Yesterday or the date, over an ornament rule. -func activityFeedHeader(c *ui.Context, k theme.Tokens, box *ui.Element, day, today Date) { - label := datetimeDayLabel(c, day) +func activityFeedHeader(c *ui.Context, k theme.Tokens, box *ui.Element, day, today core.Date) { + label := core.DatetimeDayLabel(c, day) switch day { case today: - label = msg(c, "activity_feed.today") + label = core.Msg(c, "activity_feed.today") case today.AddDays(-1): - label = msg(c, "activity_feed.yesterday") + label = core.Msg(c, "activity_feed.yesterday") } - box.Role(ui.RoleText).AlignItems(ui.End).Padding(0, Density(c).Unit()*3, Density(c).Unit(), Density(c).Unit()*3) + box.Role(ui.RoleText).AlignItems(ui.End).Padding(0, core.Density(c).Unit()*3, core.Density(c).Unit(), core.Density(c).Unit()*3) box.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 1, W: r.W, H: 1}, k.Ornament, 0) }) box.Children(func() { - ui.Text(c, label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize+1)).FontWeight(600).TextColor(k.Text).SingleLine() + ui.Text(c, label).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize+1)).FontWeight(600).TextColor(k.Text).SingleLine() }) } @@ -124,7 +126,7 @@ func activityFeedEvent(c *ui.Context, k theme.Tokens, box *ui.Element, a Activit when := a.Time.In(loc).Format("15:04") box.Role(ui.RoleListItem).Label(a.Actor + " " + a.Action + " " + a.Object + ", " + when) box.Children(func() { - Avatar(c, a.Actor, AvatarOptions{Size: 28}) + display.Avatar(c, a.Actor, display.AvatarOptions{Size: 28}) ui.Row(c).Grow(1).Shrink(1).MinWidth(0).Gap(4).Children(func() { ui.Text(c, a.Actor).Bold().TextColor(k.Text).SingleLine().Shrink(0) ui.Text(c, a.Action).TextColor(k.TextMuted).SingleLine().Shrink(0) @@ -132,6 +134,6 @@ func activityFeedEvent(c *ui.Context, k theme.Tokens, box *ui.Element, a Activit ui.Text(c, a.Object).TextColor(k.AccentText).SingleLine().Shrink(1).Ellipsis("…") } }) - ui.Text(c, when).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, when).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } diff --git a/activity_feed_test.go b/feedback/activity_feed_test.go similarity index 84% rename from activity_feed_test.go rename to feedback/activity_feed_test.go index 2d408de..fbfdbcf 100644 --- a/activity_feed_test.go +++ b/feedback/activity_feed_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "strconv" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func feedItems(n int) []Activity { @@ -20,8 +22,8 @@ func feedItems(n int) []Activity { func TestActivityFeedGroupsByDay(t *testing.T) { items := feedItems(6) // 18:00, 13:00, 08:00, 03:00 on the 5th, then 22:00 and 17:00 on the 4th tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: Date{2026, 10, 5}}).Size(600, 500) + core.Use(c, core.Settings{}) + ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: core.Date{Year: 2026, Month: 10, Day: 5}}).Size(600, 500) }, 640, 540) tt.Frame() for _, s := range []string{"Today", "Yesterday", "Mira edited Doc 0, 18:00", "Mira edited Doc 4, 22:00"} { @@ -38,8 +40,8 @@ func TestActivityFeedGroupsByDay(t *testing.T) { } // Another zone moves 03:00 UTC to the previous day. tt2 := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) - ActivityFeed(c, items, ActivityFeedOptions{Location: time.FixedZone("W", -5*3600), Today: Date{2026, 10, 5}}).Size(600, 500) + core.Use(c, core.Settings{Locale: core.ZhCN}) + ActivityFeed(c, items, ActivityFeedOptions{Location: time.FixedZone("W", -5*3600), Today: core.Date{Year: 2026, Month: 10, Day: 5}}).Size(600, 500) }, 640, 540) tt2.Frame() if _, ok := tt2.Find("Mira edited Doc 3, 22:00"); !ok || !tt2.HasText("今天") || !tt2.HasText("昨天") { @@ -50,8 +52,8 @@ func TestActivityFeedGroupsByDay(t *testing.T) { func TestActivityFeedVirtualizesAndEmpty(t *testing.T) { items := feedItems(100000) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: Date{2026, 10, 5}}).Size(600, 400) + core.Use(c, core.Settings{}) + ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: core.Date{Year: 2026, Month: 10, Day: 5}}).Size(600, 400) }, 640, 440) tt.Frame() if n := len(tt.Texts()); n == 0 || n > 120 { @@ -63,7 +65,7 @@ func TestActivityFeedVirtualizesAndEmpty(t *testing.T) { t.Fatal("first event still built after scrolling far") } tt3 := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ActivityFeed(c, nil, ActivityFeedOptions{}).Size(400, 300) }, 440, 340) if !tt3.HasText("No activity yet") { @@ -75,7 +77,7 @@ func TestActivityFeedVirtualizesAndEmpty(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ActivityFeed(c, []Activity{{Actor: "x"}}, ActivityFeedOptions{}) }, 200, 100) } @@ -86,7 +88,7 @@ func TestActivityFeedSnapshot(t *testing.T) { items[3].Actor, items[3].Action, items[3].Object = "Sakiko", "approved", "Merge request !42" snapshot(t, "activity_feed", 520, 320, func(c *ui.Context) { ui.Column(c).Padding(12).Children(func() { - ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: Date{2026, 10, 5}}).Size(496, 296) + ActivityFeed(c, items, ActivityFeedOptions{Location: time.UTC, Today: core.Date{Year: 2026, Month: 10, Day: 5}}).Size(496, 296) }) }) } diff --git a/alert.go b/feedback/alert.go similarity index 51% rename from alert.go rename to feedback/alert.go index 8d78108..b231d29 100644 --- a/alert.go +++ b/feedback/alert.go @@ -1,86 +1,38 @@ -package mujica +package feedback import ( - "fmt" "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) -// Severity is the meaning of a message. The zero value is SeverityInfo. -type Severity int - -const ( - SeverityInfo Severity = iota - SeveritySuccess - SeverityWarning - SeverityError -) - -func (s Severity) String() string { - switch s { - case SeverityInfo: - return "info" - case SeveritySuccess: - return "success" - case SeverityWarning: - return "warning" - case SeverityError: - return "error" - } - panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) -} - -// severityLook returns the color and icon that, with text, mark s. -func severityLook(k theme.Tokens, s Severity) (ui.Color, *ui.SVG) { - switch s { - case SeveritySuccess: - return k.Success, icons.Must("circle-check") - case SeverityWarning: - return k.Warning, icons.Must("triangle-alert") - case SeverityError: - return k.Danger, icons.Must("circle-x") - case SeverityInfo: - return k.Info, icons.Must("info") - } - panic(fmt.Sprintf("mujica: unknown Severity %d", int(s))) -} - // AlertOptions configure an Alert. type AlertOptions struct { - Severity Severity + Severity core.Severity // Title goes above the message in bold; empty shows none. Title string // Action labels a button at the end; empty shows none. Action string } -// AlertResult is what an Alert built and reported this frame. -type AlertResult struct { - // Element is nil while the alert is closed. - Element *ui.Element - // Action reports a click on the action button. - Action bool - // Closed reports the user dismissing the alert. - Closed bool -} - // Alert shows an inline message; a non-nil open adds a dismiss button that clears it. -func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertResult { +func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) core.AlertResult { if message == "" { panic("mujica: Alert needs a message") } if open != nil && !*open { - return AlertResult{} + return core.AlertResult{} } - k := Tokens(c) - u := Density(c).Unit() - tone, icon := severityLook(k, opts.Severity) - name := msg(c, "alert."+opts.Severity.String()) - var res AlertResult + k := core.Tokens(c) + u := core.Density(c).Unit() + tone, icon := core.SeverityLook(k, opts.Severity) + name := core.Msg(c, "alert."+opts.Severity.String()) + var res core.AlertResult box := ui.Row(c).FillWidth().AlignItems(ui.Start).Gap(u*3).Padding(u*3, u*3, u*3, u*4). Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border). Role(ui.RoleGroup).Label(name).Clip() @@ -90,7 +42,7 @@ func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertRe }) // Warnings and errors are read out when they appear or change. said := ui.Local(box, "alert.said", func() string { return "" }) - if news := name + ": " + message; opts.Severity >= SeverityWarning && *said != news { + if news := name + ": " + message; opts.Severity >= core.SeverityWarning && *said != news { *said = news c.Announce(news) } @@ -103,7 +55,7 @@ func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertRe ui.Text(c, message).TextColor(k.Text).LineHeight(theme.LineHeight) if opts.Action != "" { ui.Row(c).Margin(u, 0, 0, 0).Children(func() { - if Button(c, opts.Action, ButtonOptions{Variant: Outline}).Clicked() { + if input.Button(c, opts.Action, input.ButtonOptions{Variant: input.Outline}).Clicked() { slog.Debug("mujica: alert action", "severity", opts.Severity, "action", opts.Action) res.Action = true } @@ -111,7 +63,7 @@ func Alert(c *ui.Context, open *bool, message string, opts AlertOptions) AlertRe } }) if open != nil { - dismiss := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Label: msg(c, "alert.close")}) + dismiss := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("x"), Label: core.Msg(c, "alert.close")}) dismiss.TextColor(k.TextMuted).Padding(0).MinHeight(theme.MinHitSize).Margin(-u, -u, -u, 0) if dismiss.Clicked() { slog.Debug("mujica: alert closed", "severity", opts.Severity) diff --git a/alert_test.go b/feedback/alert_test.go similarity index 79% rename from alert_test.go rename to feedback/alert_test.go index af93006..58c31de 100644 --- a/alert_test.go +++ b/feedback/alert_test.go @@ -1,18 +1,20 @@ -package mujica +package feedback import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestAlertActionAndClose(t *testing.T) { open := true actions, closes := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - r := Alert(c, &open, "Disk almost full", AlertOptions{Severity: SeverityWarning, Title: "Storage", Action: "Free space"}) + core.Use(c, core.Settings{}) + r := Alert(c, &open, "Disk almost full", AlertOptions{Severity: core.SeverityWarning, Title: "Storage", Action: "Free space"}) if r.Action { actions++ } @@ -41,8 +43,8 @@ func TestAlertKeyboard(t *testing.T) { open := true actions := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Alert(c, &open, "Sync failed", AlertOptions{Severity: SeverityError, Action: "Retry"}).Action { + core.Use(c, core.Settings{}) + if Alert(c, &open, "Sync failed", AlertOptions{Severity: core.SeverityError, Action: "Retry"}).Action { actions++ } }, 500, 300) @@ -60,7 +62,7 @@ func TestAlertKeyboard(t *testing.T) { func TestAlertWithoutOpenHasNoDismiss(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Alert(c, nil, "Read only", AlertOptions{}) }, 500, 300) if _, ok := tt.Find("Dismiss"); ok { @@ -72,15 +74,15 @@ func TestAlertWithoutOpenHasNoDismiss(t *testing.T) { } func TestAlertAnnouncesWarningsOnce(t *testing.T) { - sev := SeverityInfo + sev := core.SeverityInfo tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) Alert(c, nil, "磁盘已满", AlertOptions{Severity: sev}) }, 500, 300) if a := tt.Announcements(); len(a) != 0 { t.Fatalf("info announced %q", a) } - sev = SeverityError + sev = core.SeverityError tt.Frame() tt.Frame() if a := tt.Announcements(); !slices.Equal(a, []string{"错误: 磁盘已满"}) { @@ -95,7 +97,7 @@ func TestAlertPanicsWithoutMessage(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Alert(c, nil, "", AlertOptions{}) }, 300, 200) } @@ -105,9 +107,9 @@ func TestAlertSnapshot(t *testing.T) { snapshot(t, "alert", 560, 400, func(c *ui.Context) { ui.Column(c).Padding(20).Gap(12).Children(func() { Alert(c, nil, "A new version is ready to install.", AlertOptions{}) - Alert(c, &open, "Your changes were saved.", AlertOptions{Severity: SeveritySuccess}) - Alert(c, nil, "The certificate expires in 3 days.", AlertOptions{Severity: SeverityWarning, Title: "Certificate"}) - Alert(c, &open, "The archive could not be read.", AlertOptions{Severity: SeverityError, Title: "Import failed", Action: "Retry"}) + Alert(c, &open, "Your changes were saved.", AlertOptions{Severity: core.SeveritySuccess}) + Alert(c, nil, "The certificate expires in 3 days.", AlertOptions{Severity: core.SeverityWarning, Title: "Certificate"}) + Alert(c, &open, "The archive could not be read.", AlertOptions{Severity: core.SeverityError, Title: "Import failed", Action: "Retry"}) }) }) } diff --git a/bars_loader.go b/feedback/bars_loader.go similarity index 89% rename from bars_loader.go rename to feedback/bars_loader.go index 23fd898..442522a 100644 --- a/bars_loader.go +++ b/feedback/bars_loader.go @@ -1,9 +1,9 @@ -package mujica +package feedback import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // BarsLoader shows four bars rising and falling in turn. diff --git a/bars_loader_test.go b/feedback/bars_loader_test.go similarity index 81% rename from bars_loader_test.go rename to feedback/bars_loader_test.go index 1f9392f..c623e03 100644 --- a/bars_loader_test.go +++ b/feedback/bars_loader_test.go @@ -1,6 +1,8 @@ -package mujica +package feedback -import "testing" +import ( + "testing" +) func TestBarsLoaderSnapshot(t *testing.T) { loaderSnapshot(t, "bars_loader", BarsLoader) } diff --git a/blink_highlight.go b/feedback/blink_highlight.go similarity index 86% rename from blink_highlight.go rename to feedback/blink_highlight.go index 1ad449a..6838196 100644 --- a/blink_highlight.go +++ b/feedback/blink_highlight.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BlinkHighlightOptions configure a BlinkHighlight. @@ -40,8 +41,8 @@ func BlinkHighlight(c *ui.Context, value float64, opts BlinkHighlightOptions) *u if format == nil { format = func(v float64) string { return strconv.FormatFloat(v, 'f', 2, 64) } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() row := ui.Row(c).AlignItems(ui.Center).Gap(u).Padding(0, u*1.5).Radius(theme.ControlRadius).Shrink(0) st := ui.Local(row, "blink", func() blinkState { return blinkState{} }) now := c.Now() @@ -64,10 +65,10 @@ func BlinkHighlight(c *ui.Context, value float64, opts BlinkHighlightOptions) *u } name := format(value) if st.dir != 0 { - name += ", " + msg(c, word) + name += ", " + core.Msg(c, word) } row.Label(name) - if dur := Motion(c, d); st.dir != 0 && !st.at.IsZero() && dur > 0 { + if dur := core.Motion(c, d); st.dir != 0 && !st.at.IsZero() && dur > 0 { if p := float32(now.Sub(st.at)) / float32(dur); p < 1 { row.Background(col.Alpha(0.22 * (1 - p))) row.DrawOver(func(*ui.Painter, ui.Rect) { c.AnimationFrame() }) diff --git a/blink_highlight_test.go b/feedback/blink_highlight_test.go similarity index 96% rename from blink_highlight_test.go rename to feedback/blink_highlight_test.go index d0d8938..085a1fc 100644 --- a/blink_highlight_test.go +++ b/feedback/blink_highlight_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "bytes" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBlinkHighlightFlashesOnceWithDirection(t *testing.T) { @@ -53,7 +55,7 @@ func TestBlinkHighlightInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BlinkHighlight(c, 1, BlinkHighlightOptions{Duration: -1}) }, 100, 50, 1) } diff --git a/cost_breakdown.go b/feedback/cost_breakdown.go similarity index 67% rename from cost_breakdown.go rename to feedback/cost_breakdown.go index f086929..967bcd7 100644 --- a/cost_breakdown.go +++ b/feedback/cost_breakdown.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CostItem is the usage and cost of one call. @@ -42,7 +44,7 @@ type CostBreakdownOptions struct { // CostBreakdownState holds the table's sort and selection and the sums. type CostBreakdownState struct { - Table DataTableState[string] + Table data.DataTableState[string] rows []costRow total costRow src *CostItem @@ -61,7 +63,7 @@ type costRow struct { } // CostBreakdown totals grouped costs; give the element a height. -func CostBreakdown(c *ui.Context, s *CostBreakdownState, items []CostItem, opts CostBreakdownOptions) DataTableView { +func CostBreakdown(c *ui.Context, s *CostBreakdownState, items []CostItem, opts CostBreakdownOptions) data.DataTableView { if opts.GroupBy < CostByModel || opts.GroupBy > CostByStep || opts.Decimals < 0 { panic("mujica: CostBreakdown needs a known GroupBy and non-negative Decimals") } @@ -76,7 +78,7 @@ func CostBreakdown(c *ui.Context, s *CostBreakdownState, items []CostItem, opts if len(items) > 0 { first = &items[0] } - unnamed := msg(c, "cost_breakdown.unnamed") + unnamed := core.Msg(c, "cost_breakdown.unnamed") if !s.built || s.src != first || s.n != len(items) || s.group != opts.GroupBy || s.version != opts.Version || s.unnamed != unnamed { s.rows, s.total = costSum(items, opts.GroupBy, unnamed) s.src, s.n, s.group, s.built = first, len(items), opts.GroupBy, true @@ -91,15 +93,15 @@ func CostBreakdown(c *ui.Context, s *CostBreakdownState, items []CostItem, opts } return r.cost / s.total.cost } - k := Tokens(c) - u := Density(c).Unit() - groupTitle := msg(c, []string{"cost_breakdown.model", "cost_breakdown.tool", "cost_breakdown.step"}[opts.GroupBy]) - cols := []DataColumn[costRow]{ + k := core.Tokens(c) + u := core.Density(c).Unit() + groupTitle := core.Msg(c, []string{"cost_breakdown.model", "cost_breakdown.tool", "cost_breakdown.step"}[opts.GroupBy]) + cols := []data.DataColumn[costRow]{ {ID: "name", Title: groupTitle, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.name, b.name) }, Text: func(r costRow) string { return r.name }}, - {ID: "calls", Title: msg(c, "cost_breakdown.calls"), Width: 80, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.calls, b.calls) }, Text: func(r costRow) string { return count(r.calls) }}, - {ID: "input", Title: msg(c, "cost_breakdown.input"), Width: 120, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.input, b.input) }, Text: func(r costRow) string { return count(r.input) }}, - {ID: "output", Title: msg(c, "cost_breakdown.output"), Width: 130, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.output, b.output) }, Text: func(r costRow) string { return count(r.output) }}, - {ID: "cost", Title: msg(c, "cost_breakdown.cost"), Width: 160, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.cost, b.cost) }, + {ID: "calls", Title: core.Msg(c, "cost_breakdown.calls"), Width: 80, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.calls, b.calls) }, Text: func(r costRow) string { return count(r.calls) }}, + {ID: "input", Title: core.Msg(c, "cost_breakdown.input"), Width: 120, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.input, b.input) }, Text: func(r costRow) string { return count(r.input) }}, + {ID: "output", Title: core.Msg(c, "cost_breakdown.output"), Width: 130, Align: ui.End, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.output, b.output) }, Text: func(r costRow) string { return count(r.output) }}, + {ID: "cost", Title: core.Msg(c, "cost_breakdown.cost"), Width: 160, Sortable: true, Compare: func(a, b costRow) int { return cmp.Compare(a.cost, b.cost) }, Text: func(r costRow) string { return money(r.cost) }, Cell: func(r costRow) { ui.Row(c).Grow(1).Gap(u * 2).AlignItems(ui.Center).Children(func() { @@ -108,19 +110,19 @@ func CostBreakdown(c *ui.Context, s *CostBreakdownState, items []CostItem, opts p.Fill(ui.Rect{X: b.X, Y: b.Y, W: b.W * float32(f), H: b.H}, k.AccentText, 1) }) ui.Text(c, money(r.cost)).FontFeatures("tnum").Shrink(0) - ui.Text(c, fmt.Sprintf("%.0f%%", 100*f)).FontFeatures("tnum").TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Width(36).TextAlign(ui.End).Shrink(0) + ui.Text(c, fmt.Sprintf("%.0f%%", 100*f)).FontFeatures("tnum").TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Width(36).TextAlign(ui.End).Shrink(0) }) }}, } - var v DataTableView + var v data.DataTableView col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2) col.Children(func() { - v = DataTable(c, &s.Table, s.rows, DataTableOptions[costRow, string]{Columns: cols, Key: func(r costRow) string { return r.name }, Empty: msg(c, "cost_breakdown.empty")}) + v = data.DataTable(c, &s.Table, s.rows, data.DataTableOptions[costRow, string]{Columns: cols, Key: func(r costRow) string { return r.name }, Empty: core.Msg(c, "cost_breakdown.empty")}) v.Element.Grow(1) ui.Row(c).AlignItems(ui.Center).Gap(u*4).Padding(u*2, u*3).Background(k.Background).Radius(theme.ControlRadius).Role(ui.RoleStatus). - Label(fmt.Sprintf(msg(c, "cost_breakdown.total_name"), money(s.total.cost), s.total.calls)).Children(func() { - ui.Text(c, msg(c, "cost_breakdown.total")).Font(theme.SerifFont).FontWeight(600).Grow(1) - ui.Text(c, fmt.Sprintf(msg(c, "cost_breakdown.tokens"), s.total.input, s.total.output)).FontFeatures("tnum").TextColor(k.TextMuted) + Label(fmt.Sprintf(core.Msg(c, "cost_breakdown.total_name"), money(s.total.cost), s.total.calls)).Children(func() { + ui.Text(c, core.Msg(c, "cost_breakdown.total")).Font(theme.SerifFont).FontWeight(600).Grow(1) + ui.Text(c, fmt.Sprintf(core.Msg(c, "cost_breakdown.tokens"), s.total.input, s.total.output)).FontFeatures("tnum").TextColor(k.TextMuted) ui.Text(c, money(s.total.cost)).FontWeight(700).FontFeatures("tnum") }) }) diff --git a/cost_breakdown_test.go b/feedback/cost_breakdown_test.go similarity index 95% rename from cost_breakdown_test.go rename to feedback/cost_breakdown_test.go index b3a2193..b77721c 100644 --- a/cost_breakdown_test.go +++ b/feedback/cost_breakdown_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "math" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func costSample() []CostItem { @@ -35,7 +37,7 @@ func TestCostBreakdownGroupsAndTotal(t *testing.T) { by := CostByModel items := costSample() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(400).Children(func() { CostBreakdown(c, &s, items, CostBreakdownOptions{GroupBy: by}).Element.Grow(1) }) @@ -78,7 +80,7 @@ func TestCostBreakdownInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) var s CostBreakdownState CostBreakdown(c, &s, tc.items, tc.opts) }, 300, 200, 1) @@ -101,7 +103,7 @@ func TestCostBreakdownVersionAndToolGroups(t *testing.T) { items := costSample() version := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CostBreakdown(c, &s, items, CostBreakdownOptions{GroupBy: CostByTool, Version: version}).Element.Height(300) }, 800, 400) if len(s.rows) != 2 || s.rows[0].name != "search" || s.rows[0].calls != 2 { @@ -122,13 +124,13 @@ func TestCostBreakdownLargeData(t *testing.T) { } var s CostBreakdownState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CostBreakdown(c, &s, items, CostBreakdownOptions{}).Element.Height(300) }, 800, 400) if s.total.cost != 100000 || s.Table.Rows() != 100000 || len(tt.Texts()) > 300 { t.Fatalf("rows %d texts %d total %v", s.Table.Rows(), len(tt.Texts()), s.total.cost) } - a, b := s.Table.rows.Visible() + a, b := s.Table.VisibleRows() if b-a > 30 { t.Fatalf("built %d rows", b-a) } diff --git a/count_up.go b/feedback/count_up.go similarity index 92% rename from count_up.go rename to feedback/count_up.go index 651ca62..a68510f 100644 --- a/count_up.go +++ b/feedback/count_up.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -8,7 +8,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CountUpOptions configure a CountUp. @@ -42,7 +43,7 @@ func CountUp(c *ui.Context, value float64, opts CountUpOptions) *ui.Element { if format == nil { format = func(v float64) string { return strconv.FormatFloat(v, 'f', opts.Decimals, 64) } } - dur := Motion(c, d) + dur := core.Motion(c, d) now := c.Now() row := ui.Row(c).Shrink(0).Label(format(value)) st := ui.Local(row, "count", func() countUpState { return countUpState{from: value, to: value} }) diff --git a/count_up_test.go b/feedback/count_up_test.go similarity index 89% rename from count_up_test.go rename to feedback/count_up_test.go index bd6fc21..ab689f4 100644 --- a/count_up_test.go +++ b/feedback/count_up_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "fmt" @@ -8,6 +8,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCountUpCountsAndSettles(t *testing.T) { @@ -46,7 +48,7 @@ func TestCountUpCountsAndSettles(t *testing.T) { func TestCountUpFormatAndInvalid(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CountUp(c, 12.345, CountUpOptions{Decimals: 1}) CountUp(c, 3, CountUpOptions{Format: func(v float64) string { return fmt.Sprintf("%.0f items", v) }}) }, 300, 80) @@ -60,7 +62,7 @@ func TestCountUpFormatAndInvalid(t *testing.T) { t.Errorf("%+v did not panic", o) } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); CountUp(c, 1, o) }, 100, 50, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); CountUp(c, 1, o) }, 100, 50, 1) }() } defer func() { @@ -68,7 +70,7 @@ func TestCountUpFormatAndInvalid(t *testing.T) { t.Fatal("NaN did not panic") } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); CountUp(c, nanValue(), CountUpOptions{}) }, 100, 50, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); CountUp(c, nanValue(), CountUpOptions{}) }, 100, 50, 1) } func nanValue() float64 { @@ -79,7 +81,7 @@ func nanValue() float64 { func TestCountUpSnapshot(t *testing.T) { snapshot(t, "count_up", 300, 100, func(c *ui.Context) { ui.Column(c).Padding(16).Gap(8).Children(func() { - CountUp(c, 12840, CountUpOptions{}).FontSize(FontSize(c, 28)).Font("Georgia, serif") + CountUp(c, 12840, CountUpOptions{}).FontSize(core.FontSize(c, 28)).Font("Georgia, serif") CountUp(c, 0.875, CountUpOptions{Format: func(v float64) string { return fmt.Sprintf("%.1f%%", v*100) }}) }) }) diff --git a/feedback/doc.go b/feedback/doc.go new file mode 100644 index 0000000..295cac1 --- /dev/null +++ b/feedback/doc.go @@ -0,0 +1,2 @@ +// Package feedback holds MujicaUI alerts, toasts, progress, empty states and agent ops loaders and motion. +package feedback diff --git a/dots_loader.go b/feedback/dots_loader.go similarity index 84% rename from dots_loader.go rename to feedback/dots_loader.go index 653c4f4..c3ab9ae 100644 --- a/dots_loader.go +++ b/feedback/dots_loader.go @@ -1,11 +1,12 @@ -package mujica +package feedback import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LoaderOptions configure the loaders: DotsLoader, BarsLoader, PulseLoader, OrbitLoader and WaveLoader. @@ -27,21 +28,21 @@ func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, p if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { panic("mujica: loader Size must be finite and not negative") } - k := Tokens(c) + k := core.Tokens(c) size := opts.Size if size == 0 { size = 16 } label := opts.Label if label == "" { - label = msg(c, key+".loading") + label = core.Msg(c, key+".loading") } col := opts.Color if col.A == 0 { col = k.AccentText } still := c.Preferences().ReduceMotion - row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleProgress).Label(label) + row := ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Shrink(0).Role(ui.RoleProgress).Label(label) row.Children(func() { ui.Box(c).Key(still).Size(size*aspect, size).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { t := 0.0 @@ -53,7 +54,7 @@ func loaderMark(c *ui.Context, opts LoaderOptions, key string, aspect float32, p }) // Reduced motion shows the words, as the mark stands still. if opts.ShowLabel || still { - ui.Text(c, label).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, label).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() } }) return row diff --git a/dots_loader_test.go b/feedback/dots_loader_test.go similarity index 81% rename from dots_loader_test.go rename to feedback/dots_loader_test.go index 5d6c7c1..55b0980 100644 --- a/dots_loader_test.go +++ b/feedback/dots_loader_test.go @@ -1,6 +1,8 @@ -package mujica +package feedback -import "testing" +import ( + "testing" +) func TestDotsLoaderSnapshot(t *testing.T) { loaderSnapshot(t, "dots_loader", DotsLoader) } diff --git a/empty_state.go b/feedback/empty_state.go similarity index 71% rename from empty_state.go rename to feedback/empty_state.go index 2951801..b2db56b 100644 --- a/empty_state.go +++ b/feedback/empty_state.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EmptyKind tells why there is nothing to show. The zero value is EmptyNoData. @@ -45,25 +47,25 @@ func EmptyState(c *ui.Context, opts EmptyStateOptions) EmptyStateResult { if opts.NoIcon && opts.Icon != nil { panic("mujica: EmptyState with both Icon and NoIcon") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() title, desc, icon := opts.Title, opts.Description, opts.Icon switch opts.Kind { case EmptyNoData: if opts.Query != "" { panic("mujica: EmptyState Query needs EmptyNoResults") } - title = cmp.Or(title, msg(c, "empty_state.no_data.title")) - desc = cmp.Or(desc, msg(c, "empty_state.no_data.description")) + title = cmp.Or(title, core.Msg(c, "empty_state.no_data.title")) + desc = cmp.Or(desc, core.Msg(c, "empty_state.no_data.description")) if icon == nil { icon = icons.Must("inbox") } case EmptyNoResults: - title = cmp.Or(title, msg(c, "empty_state.no_results.title")) + title = cmp.Or(title, core.Msg(c, "empty_state.no_results.title")) if desc == "" && opts.Query != "" { - desc = fmt.Sprintf(msg(c, "empty_state.no_results.query"), opts.Query) + desc = fmt.Sprintf(core.Msg(c, "empty_state.no_results.query"), opts.Query) } - desc = cmp.Or(desc, msg(c, "empty_state.no_results.description")) + desc = cmp.Or(desc, core.Msg(c, "empty_state.no_results.description")) if icon == nil { icon = icons.Must("search") } @@ -76,11 +78,11 @@ func EmptyState(c *ui.Context, opts EmptyStateOptions) EmptyStateResult { if !opts.NoIcon { ui.Icon(c, icon).Size(32, 32).TextColor(k.TextMuted).Margin(0, 0, u*2, 0) } - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).TextAlign(ui.Center) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).TextAlign(ui.Center) ui.Text(c, desc).TextColor(k.TextMuted).TextAlign(ui.Center).MaxWidth(360).LineHeight(theme.LineHeight) if opts.Action != "" { ui.Box(c).Margin(u*2, 0, 0, 0).Children(func() { - res.Action = Button(c, opts.Action, ButtonOptions{Variant: Secondary}).Clicked() + res.Action = input.Button(c, opts.Action, input.ButtonOptions{Variant: input.Secondary}).Clicked() }) } }) diff --git a/empty_state_test.go b/feedback/empty_state_test.go similarity index 87% rename from empty_state_test.go rename to feedback/empty_state_test.go index 246b611..80110ba 100644 --- a/empty_state_test.go +++ b/feedback/empty_state_test.go @@ -1,17 +1,18 @@ -package mujica +package feedback import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestEmptyStateKinds(t *testing.T) { opts := EmptyStateOptions{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EmptyState(c, opts) }, 500, 400) if !tt.HasText("Nothing here yet") || !tt.HasText("Items you add will appear here.") { @@ -32,7 +33,7 @@ func TestEmptyStateKinds(t *testing.T) { func TestEmptyStateActionKeyboard(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) if EmptyState(c, EmptyStateOptions{Kind: EmptyNoResults, Query: "发票", Action: "清除搜索"}).Action { n++ } @@ -62,7 +63,7 @@ func TestEmptyStatePanics(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); EmptyState(c, o) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); EmptyState(c, o) }, 300, 200) }() } } diff --git a/eval_result_table.go b/feedback/eval_result_table.go similarity index 67% rename from eval_result_table.go rename to feedback/eval_result_table.go index 8ed4d3d..b3ab7da 100644 --- a/eval_result_table.go +++ b/feedback/eval_result_table.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -9,7 +9,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // EvalCase is one evaluation case and its outcome. @@ -24,7 +26,7 @@ type EvalCase struct { type EvalResultTableOptions struct { // Decimals of scores; zero means 2. Decimals int - Status DataStatus + Status data.DataStatus Error string // Version changes when cases are edited in place. Version int @@ -33,7 +35,7 @@ type EvalResultTableOptions struct { // EvalResultTableResult is a built EvalResultTable and its events. type EvalResultTableResult struct { Element *ui.Element - Table DataTableView + Table data.DataTableView diff string } @@ -47,7 +49,7 @@ type evalSummary struct { } // EvalResultTable lists scored cases with a pass summary. -func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, opts EvalResultTableOptions) EvalResultTableResult { +func EvalResultTable(c *ui.Context, s *data.DataTableState[string], cases []EvalCase, opts EvalResultTableOptions) EvalResultTableResult { if opts.Decimals < 0 { panic("mujica: EvalResultTable Decimals is negative") } @@ -55,8 +57,8 @@ func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, if dec == 0 { dec = 2 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2) res := EvalResultTableResult{Element: col} summary := ui.Local(col, "summary", func() evalSummary { return evalSummary{} }) @@ -81,15 +83,15 @@ func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, passed := summary.passed verdict := func(ec EvalCase) string { if ec.Passed { - return msg(c, "eval_result_table.pass") + return core.Msg(c, "eval_result_table.pass") } - return msg(c, "eval_result_table.fail") + return core.Msg(c, "eval_result_table.fail") } - cols := []DataColumn[EvalCase]{ - {ID: "name", Title: msg(c, "eval_result_table.case"), Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Name, b.Name) }, Text: func(e EvalCase) string { return e.Name }}, - {ID: "score", Title: msg(c, "eval_result_table.score"), Width: 90, Align: ui.End, Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Score, b.Score) }, + cols := []data.DataColumn[EvalCase]{ + {ID: "name", Title: core.Msg(c, "eval_result_table.case"), Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Name, b.Name) }, Text: func(e EvalCase) string { return e.Name }}, + {ID: "score", Title: core.Msg(c, "eval_result_table.score"), Width: 90, Align: ui.End, Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(a.Score, b.Score) }, Text: func(e EvalCase) string { return strconv.FormatFloat(e.Score, 'f', dec, 64) }}, - {ID: "result", Title: msg(c, "eval_result_table.result"), Width: 110, Sortable: true, + {ID: "result", Title: core.Msg(c, "eval_result_table.result"), Width: 110, Sortable: true, Compare: func(a, b EvalCase) int { return cmp.Compare(evalRank(a), evalRank(b)) }, Text: verdict, Cell: func(e EvalCase) { icon, col := "circle-check", k.Success @@ -103,7 +105,7 @@ func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, }}, {ID: "diff", Title: "", Width: 48, Text: func(EvalCase) string { return "" }, Cell: func(e EvalCase) { - b := iconAction(c, icons.Must("arrow-left-right"), fmt.Sprintf(msg(c, "eval_result_table.diff"), e.Name)).Tooltip(msg(c, "eval_result_table.view")) + b := core.IconAction(c, icons.Must("arrow-left-right"), fmt.Sprintf(core.Msg(c, "eval_result_table.diff"), e.Name)).Tooltip(core.Msg(c, "eval_result_table.view")) if b.Clicked() && !b.IsDisabled() { res.diff = e.ID slog.Debug("mujica: eval result diff", "case", e.ID) @@ -113,14 +115,14 @@ func EvalResultTable(c *ui.Context, s *DataTableState[string], cases []EvalCase, col.Children(func() { if len(cases) > 0 { ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "eval_result_table.summary"), passed, len(cases))).FontWeight(600) + ui.Text(c, fmt.Sprintf(core.Msg(c, "eval_result_table.summary"), passed, len(cases))).FontWeight(600) ui.Box(c).Height(6).Width(160).Radius(1).Background(k.Danger.Mix(k.Surface, 0.6)).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * float32(passed) / float32(len(cases)), H: r.H}, k.Success, 1) }) }) } - res.Table = DataTable(c, s, cases, DataTableOptions[EvalCase, string]{Columns: cols, Key: func(e EvalCase) string { return e.ID }, - Empty: msg(c, "eval_result_table.empty"), Status: opts.Status, Error: opts.Error, Version: opts.Version}) + res.Table = data.DataTable(c, s, cases, data.DataTableOptions[EvalCase, string]{Columns: cols, Key: func(e EvalCase) string { return e.ID }, + Empty: core.Msg(c, "eval_result_table.empty"), Status: opts.Status, Error: opts.Error, Version: opts.Version}) res.Table.Element.Grow(1) }) res.Table.Element = col diff --git a/eval_result_table_test.go b/feedback/eval_result_table_test.go similarity index 85% rename from eval_result_table_test.go rename to feedback/eval_result_table_test.go index 3c30132..cae2b71 100644 --- a/eval_result_table_test.go +++ b/feedback/eval_result_table_test.go @@ -1,10 +1,13 @@ -package mujica +package feedback import ( "strconv" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func evalSample() []EvalCase { @@ -16,10 +19,10 @@ func evalSample() []EvalCase { } func TestEvalResultTableSummaryAndDiff(t *testing.T) { - var s DataTableState[string] + var s data.DataTableState[string] diff := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(400).Children(func() { r := EvalResultTable(c, &s, evalSample(), EvalResultTableOptions{}) r.Table.Element.Grow(1) @@ -56,8 +59,8 @@ func TestEvalResultTableInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s DataTableState[string] + core.Use(c, core.Settings{}) + var s data.DataTableState[string] EvalResultTable(c, &s, cases, EvalResultTableOptions{}) }, 300, 200, 1) }() @@ -67,7 +70,7 @@ func TestEvalResultTableInvalid(t *testing.T) { func TestEvalResultTableSnapshot(t *testing.T) { cases := evalSample() snapshot(t, "eval_result_table", 640, 300, func(c *ui.Context) { - s := ui.Local(c.Root(), "s", func() *DataTableState[string] { return &DataTableState[string]{} }) + s := ui.Local(c.Root(), "s", func() *data.DataTableState[string] { return &data.DataTableState[string]{} }) ui.Column(c).Padding(16).Height(300).Children(func() { EvalResultTable(c, *s, cases, EvalResultTableOptions{}).Table.Element.Grow(1) }) @@ -81,18 +84,18 @@ func TestEvalResultTableDuplicateIDs(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var state DataTableState[string] + core.Use(c, core.Settings{}) + var state data.DataTableState[string] EvalResultTable(c, &state, []EvalCase{{ID: "a"}, {ID: "a"}}, EvalResultTableOptions{}) }, 500, 300, 1) } func TestEvalResultTableDisabledKeyboardDiff(t *testing.T) { - var state DataTableState[string] + var state data.DataTableState[string] disabled := false diffs := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := EvalResultTable(c, &state, evalSample(), EvalResultTableOptions{}) r.Table.Element.Height(300) @@ -122,13 +125,13 @@ func TestEvalResultTableLargeDataAndVersion(t *testing.T) { for i := range cases { cases[i] = EvalCase{ID: strconv.Itoa(i), Name: "Case " + strconv.Itoa(i), Score: 1, Passed: true} } - var s DataTableState[string] + var s data.DataTableState[string] version := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EvalResultTable(c, &s, cases, EvalResultTableOptions{Version: version}).Element.Height(300) }, 700, 400) - a, b := s.rows.Visible() + a, b := s.VisibleRows() if b-a > 30 || !tt.HasText("100000 of 100000 passed") { t.Fatalf("rows %d summary missing", b-a) } diff --git a/feedback/example_essentials_test.go b/feedback/example_essentials_test.go new file mode 100644 index 0000000..5476876 --- /dev/null +++ b/feedback/example_essentials_test.go @@ -0,0 +1,34 @@ +package feedback_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" +) + +func ExampleUndoToast() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.UndoToast(c, &open, "Note deleted", feedback.UndoToastOptions{ + OnUndo: func() { + // restore the note + }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleActivityFeed() { + events := []feedback.Activity{ + {ID: "1", Actor: "Lin", Action: "commented on", Object: "Quarterly report", Time: time.Now()}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.ActivityFeed(c, events, feedback.ActivityFeedOptions{}).Size(380, 280) + } + ui.Render(view, 400, 300, 1) +} diff --git a/example_feedback_test.go b/feedback/example_feedback_test.go similarity index 50% rename from example_feedback_test.go rename to feedback/example_feedback_test.go index cd49f2e..8fec2a3 100644 --- a/example_feedback_test.go +++ b/feedback/example_feedback_test.go @@ -1,17 +1,19 @@ -package mujica_test +package feedback_test import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func ExampleAlert() { open := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Alert(c, &open, "The archive could not be read.", mujica.AlertOptions{ - Severity: mujica.SeverityError, + core.Use(c, core.Settings{}) + r := feedback.Alert(c, &open, "The archive could not be read.", feedback.AlertOptions{ + Severity: core.SeverityError, Title: "Import failed", Action: "Retry", }) @@ -24,11 +26,11 @@ func ExampleAlert() { func ExampleToaster() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - if mujica.Button(c, "Delete note", mujica.ButtonOptions{Variant: mujica.Danger}).Clicked() { - mujica.ShowToast(c, mujica.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) + core.Use(c, core.Settings{}) + if input.Button(c, "Delete note", input.ButtonOptions{Variant: input.Danger}).Clicked() { + feedback.ShowToast(c, feedback.ToastMessage{ID: "note-1", Message: "Note deleted", Action: "Undo"}) } - if r := mujica.Toaster(c); r.Action == "note-1" { + if r := feedback.Toaster(c); r.Action == "note-1" { // restore the note } } @@ -37,11 +39,11 @@ func ExampleToaster() { func ExampleProgress() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Gap(16).Children(func() { - mujica.Progress(c, 0.62, mujica.ProgressOptions{Label: "Uploading", ShowValue: true}) - mujica.Progress(c, 0, mujica.ProgressOptions{Shape: mujica.ProgressRing, Indeterminate: true, Label: "Connecting"}) - mujica.Meter(c, 93, mujica.MeterOptions{ + feedback.Progress(c, 0.62, feedback.ProgressOptions{Label: "Uploading", ShowValue: true}) + feedback.Progress(c, 0, feedback.ProgressOptions{Shape: feedback.ProgressRing, Indeterminate: true, Label: "Connecting"}) + feedback.Meter(c, 93, feedback.MeterOptions{ Max: 100, Label: "Disk", ShowValue: true, Levels: &ui.MeterLevels{Warning: 70, Critical: 90}, }) @@ -50,21 +52,13 @@ func ExampleProgress() { ui.Render(view, 400, 300, 1) } -func ExampleSpinner() { - view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Spinner(c, mujica.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) - } - ui.Render(view, 400, 300, 1) -} - func ExampleSkeleton() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(16).Gap(12).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Shape: mujica.SkeletonCircle}) + feedback.Skeleton(c, feedback.SkeletonOptions{Shape: feedback.SkeletonCircle}) ui.Column(c).Grow(1).Children(func() { - mujica.Skeleton(c, mujica.SkeletonOptions{Lines: 2}) + feedback.Skeleton(c, feedback.SkeletonOptions{Lines: 2}) }) }) } @@ -74,9 +68,9 @@ func ExampleSkeleton() { func ExampleEmptyState() { query := "quarterly" view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.EmptyState(c, mujica.EmptyStateOptions{ - Kind: mujica.EmptyNoResults, + core.Use(c, core.Settings{}) + r := feedback.EmptyState(c, feedback.EmptyStateOptions{ + Kind: feedback.EmptyNoResults, Query: query, Action: "Clear search", }) @@ -89,9 +83,9 @@ func ExampleEmptyState() { func ExampleResult() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.Result(c, mujica.ResultOptions{ - Kind: mujica.ResultForbidden, + core.Use(c, core.Settings{}) + r := feedback.Result(c, feedback.ResultOptions{ + Kind: feedback.ResultForbidden, Description: "You need the Editor role to change billing.", Actions: []string{"Request access", "Go back"}, }) @@ -105,23 +99,23 @@ func ExampleResult() { func ExampleStatusIndicator() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(16).Children(func() { - mujica.StatusIndicator(c, mujica.StatusSuccess, mujica.StatusIndicatorOptions{Label: "API online"}) - mujica.StatusIndicator(c, mujica.StatusOffline, mujica.StatusIndicatorOptions{}) + feedback.StatusIndicator(c, feedback.StatusSuccess, feedback.StatusIndicatorOptions{Label: "API online"}) + feedback.StatusIndicator(c, feedback.StatusOffline, feedback.StatusIndicatorOptions{}) }) } ui.Render(view, 400, 300, 1) } func ExampleNotificationCenter() { - items := []mujica.Notification{ + items := []feedback.Notification{ {ID: "a", Title: "Build failed", Body: "3 tests failing", Time: "2 min ago", Action: "View log"}, {ID: "b", Title: "Backup complete", Time: "Yesterday", Read: true}, } view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.NotificationCenter(c, &items, mujica.NotificationCenterOptions{}) + core.Use(c, core.Settings{}) + r := feedback.NotificationCenter(c, &items, feedback.NotificationCenterOptions{}) if r.Action == "a" { // open the build log } @@ -132,10 +126,10 @@ func ExampleNotificationCenter() { func ExampleLoadingOverlay() { loading := true view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - r := mujica.LoadingOverlay(c, loading, mujica.LoadingOverlayOptions{Cancel: "Cancel"}, func() { + core.Use(c, core.Settings{}) + r := feedback.LoadingOverlay(c, loading, feedback.LoadingOverlayOptions{Cancel: "Cancel"}, func() { ui.Text(c, "Quarterly report") - mujica.Button(c, "Export", mujica.ButtonOptions{}) + input.Button(c, "Export", input.ButtonOptions{}) }) if r.Canceled { loading = false diff --git a/feedback/example_motion_test.go b/feedback/example_motion_test.go new file mode 100644 index 0000000..6deb472 --- /dev/null +++ b/feedback/example_motion_test.go @@ -0,0 +1,223 @@ +package feedback_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" +) + +func ExampleInterruptButton() { + running, stopping := true, false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := feedback.InterruptButton(c, running, feedback.InterruptButtonOptions{Confirm: true, Interrupting: stopping}) + if r.Changed() { + stopping = true // cancel the run, then clear running and stopping + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTokenUsageChart() { + usage := []feedback.TokenUsage{{Label: "Plan", Input: 1200, Output: 300}, {Label: "Write", Input: 2100, Output: 2900}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.TokenUsageChart(c, usage, feedback.TokenUsageChartOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCostBreakdown() { + var state feedback.CostBreakdownState + items := []feedback.CostItem{ + {Model: "opus", Tool: "search", InputTokens: 1000, OutputTokens: 200, Cost: 0.30}, + {Model: "haiku", Tool: "search", InputTokens: 4000, OutputTokens: 500, Cost: 0.05}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.CostBreakdown(c, &state, items, feedback.CostBreakdownOptions{GroupBy: feedback.CostByModel}).Element.Height(260) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTraceViewer() { + var state feedback.TraceViewerState + spans := []feedback.TraceSpan{ + {ID: "run", Name: "agent.run", End: 900 * time.Millisecond}, + {ID: "plan", Parent: "run", Name: "llm.plan", Start: 10 * time.Millisecond, End: 210 * time.Millisecond, + Attrs: []feedback.TraceAttr{{Key: "model", Value: "opus"}}}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.TraceViewer(c, &state, spans, feedback.TraceViewerOptions{}).Element.Height(260) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEvalResultTable() { + var state data.DataTableState[string] + cases := []feedback.EvalCase{{ID: "c1", Name: "Greets by title", Score: 0.98, Passed: true}, {ID: "c2", Name: "Refuses forgery", Score: 0.41}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := feedback.EvalResultTable(c, &state, cases, feedback.EvalResultTableOptions{}) + r.Table.Element.Height(260) + if id, ok := r.Diff(); ok { + _ = id // open expected and actual output side by side + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToolRegistryPanel() { + var state data.ListState[string] + tools := []feedback.AgentTool{{Name: "web_search", Description: "Search the web", Enabled: true, + Params: []feedback.ToolParam{{Name: "query", Type: "string", Required: true}}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := feedback.ToolRegistryPanel(c, &state, tools, feedback.ToolRegistryPanelOptions{}) + r.Element.Height(260) + if name, on, ok := r.Toggled(); ok && name == tools[0].Name { + tools[0].Enabled = on + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDotsLoader() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.DotsLoader(c, feedback.LoaderOptions{Label: "Thinking", ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBarsLoader() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.BarsLoader(c, feedback.LoaderOptions{Size: 20, Label: "Indexing"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePulseLoader() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.PulseLoader(c, feedback.LoaderOptions{Label: "Listening", ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOrbitLoader() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.OrbitLoader(c, feedback.LoaderOptions{Size: 24, Label: "Connecting"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleWaveLoader() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.WaveLoader(c, feedback.LoaderOptions{Label: "Speaking", ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleShimmer() { + loading := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.Shimmer(c, loading, feedback.ShimmerOptions{Height: 48}, func() { + ui.Text(c, "Report ready") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCountUp() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.CountUp(c, 12840, feedback.CountUpOptions{Duration: 800 * time.Millisecond}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTypewriter() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := feedback.Typewriter(c, "The court assembles at dusk.", feedback.TypewriterOptions{Speed: 30, Skippable: true}) + if r.Changed() { + // show the next message + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleShimmerText() { + thinking := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.ShimmerText(c, "Thinking…", feedback.ShimmerTextOptions{Active: thinking}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStagger() { + guests := []string{"Sakiko", "Uika", "Mutsumi"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.Stagger(c, len(guests), feedback.StaggerOptions{}, func(i int) { + ui.Text(c, guests[i]) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleReorderTransition() { + order := []string{"Sakiko", "Uika", "Mutsumi"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.ReorderTransition(c, order, feedback.ReorderTransitionOptions{Gap: 4}, func(name string) { + ui.Text(c, name) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLayoutTransition() { + open := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.LayoutTransition(c, feedback.LayoutTransitionOptions{Clip: true}, func() { + ui.Text(c, "Program") + if open { + ui.Text(c, "Overture · Masquerade · Finale") + } + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePresence() { + show := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.Presence(c, show, feedback.PresenceOptions{Kind: feedback.PresenceSlide}, func() { + ui.Text(c, "Saved") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBlinkHighlight() { + price := 101.5 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + feedback.BlinkHighlight(c, price, feedback.BlinkHighlightOptions{}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/feedback_review_test.go b/feedback/feedback_review_test.go similarity index 92% rename from feedback_review_test.go rename to feedback/feedback_review_test.go index 39db79e..3001693 100644 --- a/feedback_review_test.go +++ b/feedback/feedback_review_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "math" @@ -6,14 +6,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestFeedbackTextFollowsTextScale(t *testing.T) { items := []Notification{{ID: "n", Title: "Backup complete", Read: true}, {ID: "u", Title: "Unread one"}} var empty, result float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { Progress(c, 0.5, ProgressOptions{ShowValue: true}) empty = EmptyState(c, EmptyStateOptions{Title: "Empty title", Description: "Short", NoIcon: true}).Element.Bounds().H @@ -56,7 +57,7 @@ func TestFeedbackRejectsInvalidInput(t *testing.T) { }, "skeleton shape": func(c *ui.Context) { Skeleton(c, SkeletonOptions{Shape: 5}) }, "skeleton NaN width": func(c *ui.Context) { Skeleton(c, SkeletonOptions{Width: float32(nan)}) }, - "spinner negative size": func(c *ui.Context) { Spinner(c, SpinnerOptions{Size: -4}) }, + "spinner negative size": func(c *ui.Context) { core.Spinner(c, core.SpinnerOptions{Size: -4}) }, "status unknown labeled": func(c *ui.Context) { StatusIndicator(c, 42, StatusIndicatorOptions{Label: "Custom"}) }, "alert severity": func(c *ui.Context) { Alert(c, nil, "x", AlertOptions{Severity: 9}) }, "toast severity": func(c *ui.Context) { ShowToast(c, ToastMessage{Message: "x", Severity: -1}) }, @@ -70,7 +71,7 @@ func TestFeedbackRejectsInvalidInput(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 200) }() } } @@ -78,7 +79,7 @@ func TestFeedbackRejectsInvalidInput(t *testing.T) { func TestNotificationTitleOnlyHitHeight(t *testing.T) { items := []Notification{{ID: "n", Title: "Backup complete", Read: true}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NotificationCenter(c, &items, NotificationCenterOptions{}) }, 600, 400) if r, ok := tt.Find("Backup complete"); !ok || r.H < theme.MinHitSize { @@ -89,7 +90,7 @@ func TestNotificationTitleOnlyHitHeight(t *testing.T) { func TestNotificationHeaderWrapsAtDoubleText(t *testing.T) { items := []Notification{{ID: "a", Title: "Build failed"}, {ID: "b", Title: "Backup complete", Read: true}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(520).Height(700).Children(func() { NotificationCenter(c, &items, NotificationCenterOptions{}) }) @@ -110,7 +111,7 @@ func TestNotificationHeaderWrapsAtDoubleText(t *testing.T) { func TestProgressRingCaptionFitsAtDoubleText(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { Progress(c, 1, ProgressOptions{Shape: ProgressRing, ShowValue: true}) }) @@ -139,7 +140,7 @@ func TestProgressResumesFromDisplayedValueAfterReducedMotion(t *testing.T) { v := 0.0 var bar ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Width(300).Children(func() { bar = Progress(c, v, ProgressOptions{}).Bounds() }) diff --git a/feedback/helpers_test.go b/feedback/helpers_test.go new file mode 100644 index 0000000..3a516ef --- /dev/null +++ b/feedback/helpers_test.go @@ -0,0 +1,72 @@ +package feedback + +import ( + "image" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func encode(t *testing.T, img image.Image) []byte { + t.Helper() + return snaptest.Encode(t, img) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +// tabTo presses Tab until the element labeled label has the focus. +func tabTo(t *testing.T, tt *ui.Tester, label string) { + t.Helper() + for range 60 { + if tt.Focused(label) { + return + } + tt.Key(0, ui.KeyTab) + } + t.Fatalf("Tab never reached %q", label) +} + +type chartHarness struct { + res chart.ChartResult + changes int + disabled bool + build func(c *ui.Context) chart.ChartResult +} + +// chartReadout returns the readout line, the text after the plot. +func chartReadout(tt *ui.Tester) string { + texts := tt.Texts() + return texts[len(texts)-1] +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} + +func (h *chartHarness) view(c *ui.Context) { + core.Use(c, core.Settings{Mode: core.Light}) + ui.Column(c).Padding(16).Width(480).Disabled(h.disabled).Children(func() { + h.res = h.build(c) + if h.res.Changed() { + h.changes++ + } + }) +} diff --git a/interrupt_button.go b/feedback/interrupt_button.go similarity index 56% rename from interrupt_button.go rename to feedback/interrupt_button.go index 0117d0f..b0390c8 100644 --- a/interrupt_button.go +++ b/feedback/interrupt_button.go @@ -1,11 +1,14 @@ -package mujica +package feedback import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" ) // InterruptButtonOptions configure an InterruptButton. @@ -19,24 +22,24 @@ type InterruptButtonOptions struct { } // InterruptButton stops a running agent; Changed reports the interrupt. -func InterruptButton(c *ui.Context, running bool, opts InterruptButtonOptions) ChoiceResult { +func InterruptButton(c *ui.Context, running bool, opts InterruptButtonOptions) core.ChoiceResult { if opts.Interrupting && !running { panic("mujica: InterruptButton cannot be Interrupting while not running") } label := opts.Label if label == "" { - label = msg(c, "interrupt_button.stop") + label = core.Msg(c, "interrupt_button.stop") } shown := label if opts.Interrupting { - shown = msg(c, "interrupt_button.stopping") + shown = core.Msg(c, "interrupt_button.stopping") } live := running && !opts.Interrupting - var res ChoiceResult + var res core.ChoiceResult wrap := ui.Row(c).Shrink(0) res.Element = wrap wrap.Children(func() { - b := Button(c, shown, ButtonOptions{Variant: Danger, Icon: icons.Must("circle-x"), Loading: opts.Interrupting, Disabled: !running}) + b := input.Button(c, shown, input.ButtonOptions{Variant: input.Danger, Icon: icons.Must("circle-x"), Loading: opts.Interrupting, Disabled: !running}) open := ui.Local(wrap, "confirm", func() bool { return false }) if !live || b.IsDisabled() { *open = false @@ -46,15 +49,15 @@ func InterruptButton(c *ui.Context, running bool, opts InterruptButtonOptions) C case clicked && opts.Confirm: *open = true case clicked: - res.changed = true + res = core.NewChoiceResult(res.Element, true) } - r := Popconfirm(c, b, open, PopconfirmOptions{Title: msg(c, "interrupt_button.confirm"), - Description: msg(c, "interrupt_button.detail"), ConfirmLabel: label, CancelLabel: msg(c, "interrupt_button.keep"), Danger: true}) + r := overlay.Popconfirm(c, b, open, overlay.PopconfirmOptions{Title: core.Msg(c, "interrupt_button.confirm"), + Description: core.Msg(c, "interrupt_button.detail"), ConfirmLabel: label, CancelLabel: core.Msg(c, "interrupt_button.keep"), Danger: true}) if r.Confirmed && live && !b.IsDisabled() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) } }) - if res.changed { + if res.Changed() { slog.Debug("mujica: interrupt button interrupt") } return res diff --git a/interrupt_button_test.go b/feedback/interrupt_button_test.go similarity index 94% rename from interrupt_button_test.go rename to feedback/interrupt_button_test.go index 42f58dc..cc217e8 100644 --- a/interrupt_button_test.go +++ b/feedback/interrupt_button_test.go @@ -1,16 +1,18 @@ -package mujica +package feedback import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestInterruptButtonDirectAndConfirmed(t *testing.T) { running, interrupting, confirm := true, false, false stops := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Children(func() { if InterruptButton(c, running, InterruptButtonOptions{Confirm: confirm, Interrupting: interrupting}).Changed() { stops++ @@ -60,7 +62,7 @@ func TestInterruptButtonInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) InterruptButton(c, false, InterruptButtonOptions{Interrupting: true}) }, 100, 50, 1) } @@ -79,7 +81,7 @@ func TestInterruptButtonAncestorDisabledClosesConfirmation(t *testing.T) { disabled := false stops := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if InterruptButton(c, true, InterruptButtonOptions{Confirm: true}).Changed() { stops++ diff --git a/layout_transition.go b/feedback/layout_transition.go similarity index 80% rename from layout_transition.go rename to feedback/layout_transition.go index f7e7596..e45f259 100644 --- a/layout_transition.go +++ b/feedback/layout_transition.go @@ -1,11 +1,12 @@ -package mujica +package feedback import ( "time" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LayoutTransitionOptions configure a LayoutTransition. @@ -34,7 +35,7 @@ func LayoutTransition(c *ui.Context, opts LayoutTransitionOptions, content func( if opts.Clip { box.Clip() } - box.Transition(ui.ElementTransition{Duration: Motion(c, d), Position: true, Size: true}) + box.Transition(ui.ElementTransition{Duration: core.Motion(c, d), Position: true, Size: true}) if content != nil { box.Children(content) } diff --git a/layout_transition_test.go b/feedback/layout_transition_test.go similarity index 85% rename from layout_transition_test.go rename to feedback/layout_transition_test.go index e5ecc77..5aa4848 100644 --- a/layout_transition_test.go +++ b/feedback/layout_transition_test.go @@ -1,10 +1,12 @@ -package mujica +package feedback import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestLayoutTransitionResizes(t *testing.T) { @@ -39,7 +41,7 @@ func TestLayoutTransitionResizes(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) LayoutTransition(c, LayoutTransitionOptions{Duration: -1}, nil) }, 100, 50, 1) } @@ -48,7 +50,7 @@ func TestLayoutTransitionSnapshot(t *testing.T) { snapshot(t, "layout_transition", 300, 100, func(c *ui.Context) { ui.Column(c).Padding(16).AlignItems(ui.Start).Children(func() { LayoutTransition(c, LayoutTransitionOptions{}, func() { - ui.Text(c, "A panel that grows").Padding(12).Background(Tokens(c).Surface).Radius(4) + ui.Text(c, "A panel that grows").Padding(12).Background(core.Tokens(c).Surface).Radius(4) }) }) }) diff --git a/loaders_test.go b/feedback/loaders_test.go similarity index 93% rename from loaders_test.go rename to feedback/loaders_test.go index 49f5ebb..ac7b684 100644 --- a/loaders_test.go +++ b/feedback/loaders_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "bytes" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // motionFrames builds view and returns its tester and a counter of built frames. @@ -13,7 +15,7 @@ func motionFrames(view func(c *ui.Context), w, h int) (*ui.Tester, *int) { n := new(int) tt := ui.NewTester(func(c *ui.Context) { *n++ - Use(c, Settings{}) + core.Use(c, core.Settings{}) view(c) }, w, h) return tt, n @@ -87,7 +89,7 @@ func TestLoadersInvalidSize(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) loader(c, LoaderOptions{Size: -1}) }, 100, 50, 1) }() @@ -99,7 +101,7 @@ func loaderSnapshot(t *testing.T, name string, loader func(c *ui.Context, o Load ui.Row(c).Padding(20).Gap(24).AlignItems(ui.Center).Children(func() { loader(c, LoaderOptions{}) loader(c, LoaderOptions{Size: 28, Label: "Thinking", ShowLabel: true}) - loader(c, LoaderOptions{Size: 20, Color: Tokens(c).Info}) + loader(c, LoaderOptions{Size: 20, Color: core.Tokens(c).Info}) }) }) } diff --git a/loading_overlay.go b/feedback/loading_overlay.go similarity index 83% rename from loading_overlay.go rename to feedback/loading_overlay.go index 5e826b4..dba6e2f 100644 --- a/loading_overlay.go +++ b/feedback/loading_overlay.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LoadingOverlayOptions configure a LoadingOverlay. @@ -30,9 +32,9 @@ func LoadingOverlay(c *ui.Context, loading bool, opts LoadingOverlayOptions, con if content == nil { panic("mujica: LoadingOverlay needs content") } - k := Tokens(c) - u := Density(c).Unit() - label := cmp.Or(opts.Label, msg(c, "loading_overlay.label")) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := cmp.Or(opts.Label, core.Msg(c, "loading_overlay.label")) var res LoadingOverlayResult box := ui.Box(c) was := ui.Local(box, "loading_overlay.loading", func() bool { return false }) @@ -65,13 +67,13 @@ func LoadingOverlay(c *ui.Context, loading bool, opts LoadingOverlayOptions, con } panel.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - Spinner(c, SpinnerOptions{Size: 20, Label: label}) + core.Spinner(c, core.SpinnerOptions{Size: 20, Label: label}) ui.Text(c, label).TextColor(k.Text) }) if opts.Cancel == "" { return } - b := Button(c, opts.Cancel, ButtonOptions{Variant: Secondary}) + b := input.Button(c, opts.Cancel, input.ButtonOptions{Variant: input.Secondary}) if held { b.Focus() } diff --git a/loading_overlay_test.go b/feedback/loading_overlay_test.go similarity index 87% rename from loading_overlay_test.go rename to feedback/loading_overlay_test.go index 67afefb..e0d41a1 100644 --- a/loading_overlay_test.go +++ b/feedback/loading_overlay_test.go @@ -1,10 +1,13 @@ -package mujica +package feedback import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type overlayProbe struct { @@ -16,11 +19,11 @@ type overlayProbe struct { func overlayTester(p *overlayProbe) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(12).Children(func() { r := LoadingOverlay(c, p.loading, LoadingOverlayOptions{Cancel: p.cancel, Label: p.label}, func() { ui.Column(c).Padding(24).Children(func() { - if Button(c, "Save", ButtonOptions{}).Clicked() { + if input.Button(c, "Save", input.ButtonOptions{}).Clicked() { p.saves++ } }) @@ -28,7 +31,7 @@ func overlayTester(p *overlayProbe) *ui.Tester { if r.Canceled { p.cancels++ } - if Button(c, "Outside", ButtonOptions{}).Clicked() { + if input.Button(c, "Outside", input.ButtonOptions{}).Clicked() { p.outside++ } }) @@ -108,10 +111,10 @@ func TestLoadingOverlaySnapshot(t *testing.T) { snapshot(t, "loading_overlay", 420, 220, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { LoadingOverlay(c, true, LoadingOverlayOptions{Cancel: "Cancel"}, func() { - ui.Column(c).Padding(16).Gap(8).AlignItems(ui.Start).Background(Tokens(c).Surface).Radius(4).Children(func() { + ui.Column(c).Padding(16).Gap(8).AlignItems(ui.Start).Background(core.Tokens(c).Surface).Radius(4).Children(func() { ui.Text(c, "Quarterly report") ui.Text(c, "Revenue, churn and pipeline for Q3.") - Button(c, "Export", ButtonOptions{}) + input.Button(c, "Export", input.ButtonOptions{}) }) }) }) diff --git a/notification_center.go b/feedback/notification_center.go similarity index 76% rename from notification_center.go rename to feedback/notification_center.go index cbcb116..6b65cf8 100644 --- a/notification_center.go +++ b/feedback/notification_center.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Notification is one in-app message. The caller owns and persists the list. @@ -60,9 +62,9 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC unread++ } } - k := Tokens(c) - u := Density(c).Unit() - title := cmp.Or(opts.Title, msg(c, "notification_center.title")) + k := core.Tokens(c) + u := core.Density(c).Unit() + title := cmp.Or(opts.Title, core.Msg(c, "notification_center.title")) var res NotificationCenterResult panel := ui.Column(c).FillWidth().Shrink(1).MinHeight(0).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border). Role(ui.RoleGroup).Label(title).Clip() @@ -70,20 +72,20 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC // Wraps so title and actions stay whole when text is enlarged. ui.Row(c).Wrap().AlignItems(ui.Center).Justify(ui.SpaceBetween).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() { ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text).SingleLine() + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.Text).SingleLine() if unread > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "notification_center.unread"), unread)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText).SingleLine() } }) ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { - if Button(c, msg(c, "notification_center.mark_all_read"), ButtonOptions{Variant: Ghost, Disabled: unread == 0}).Clicked() { + if input.Button(c, core.Msg(c, "notification_center.mark_all_read"), input.ButtonOptions{Variant: input.Ghost, Disabled: unread == 0}).Clicked() { for i := range *items { (*items)[i].Read = true } res.MarkedAllRead = true slog.Debug("mujica: notification_center mark all read", "count", unread) } - if Button(c, msg(c, "notification_center.clear_all"), ButtonOptions{Variant: Ghost, Disabled: len(*items) == 0}).Clicked() { + if input.Button(c, core.Msg(c, "notification_center.clear_all"), input.ButtonOptions{Variant: input.Ghost, Disabled: len(*items) == 0}).Clicked() { slog.Debug("mujica: notification_center clear all", "count", len(*items)) *items = (*items)[:0] res.ClearedAll = true @@ -94,8 +96,8 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC ui.Box(c).FillWidth().Height(1).Background(k.Ornament).Role(ui.RoleNone) if len(*items) == 0 && !res.ClearedAll { EmptyState(c, EmptyStateOptions{ - Title: msg(c, "notification_center.empty"), - Description: msg(c, "notification_center.empty_hint"), + Title: core.Msg(c, "notification_center.empty"), + Description: core.Msg(c, "notification_center.empty_hint"), Icon: icons.Must("bell"), }) return @@ -123,7 +125,7 @@ func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationC func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, res *NotificationCenterResult) { name := n.Title if !n.Read { - name = msg(c, "notification_center.unread_mark") + ": " + n.Title + name = core.Msg(c, "notification_center.unread_mark") + ": " + n.Title } row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2). Role(ui.RoleListItem) @@ -147,7 +149,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, t.Bold() } if n.Time != "" { - ui.Text(c, n.Time).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, n.Time).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if n.Body != "" { @@ -159,7 +161,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, slog.Debug("mujica: notification_center opened", "id", n.ID) } ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() { - if n.Action != "" && Button(c, n.Action, ButtonOptions{Variant: Outline}).Clicked() { + if n.Action != "" && input.Button(c, n.Action, input.ButtonOptions{Variant: input.Outline}).Clicked() { res.Action = n.ID slog.Debug("mujica: notification_center action", "id", n.ID) } @@ -167,11 +169,11 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, if n.Read { toggle, icon = "notification_center.mark_unread", icons.Must("circle") } - if notificationIconButton(c, k, u, icon, msg(c, toggle)).Clicked() { + if notificationIconButton(c, k, u, icon, core.Msg(c, toggle)).Clicked() { n.Read, res.Toggled = !n.Read, n.ID slog.Debug("mujica: notification_center toggled", "id", n.ID, "read", n.Read) } - if notificationIconButton(c, k, u, icons.Must("x"), msg(c, "notification_center.clear")).Clicked() { + if notificationIconButton(c, k, u, icons.Must("x"), core.Msg(c, "notification_center.clear")).Clicked() { res.Removed = n.ID } }) @@ -185,7 +187,7 @@ func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, } func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) *ui.Element { - b := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label}) + b := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icon, Label: label}) b.TextColor(k.TextMuted).Padding(0, u).Tooltip(label) return b } diff --git a/notification_center_test.go b/feedback/notification_center_test.go similarity index 96% rename from notification_center_test.go rename to feedback/notification_center_test.go index 20fef80..7f70230 100644 --- a/notification_center_test.go +++ b/feedback/notification_center_test.go @@ -1,9 +1,11 @@ -package mujica +package feedback import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func sampleNotifications() []Notification { @@ -16,7 +18,7 @@ func sampleNotifications() []Notification { func notificationTester(items *[]Notification, last *NotificationCenterResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(560).Children(func() { r := NotificationCenter(c, items, NotificationCenterOptions{}) r.Element = nil @@ -115,7 +117,7 @@ func TestNotificationCenterPanicsOnRepeatedID(t *testing.T) { }() items := []Notification{{ID: "x"}, {ID: "x"}} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NotificationCenter(c, &items, NotificationCenterOptions{}) }, 300, 200) } diff --git a/orbit_loader.go b/feedback/orbit_loader.go similarity index 92% rename from orbit_loader.go rename to feedback/orbit_loader.go index c7a0d0d..3b74ddf 100644 --- a/orbit_loader.go +++ b/feedback/orbit_loader.go @@ -1,11 +1,11 @@ -package mujica +package feedback import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // OrbitLoader moves a diamond around a ring of diamonds. diff --git a/orbit_loader_test.go b/feedback/orbit_loader_test.go similarity index 82% rename from orbit_loader_test.go rename to feedback/orbit_loader_test.go index 1a14aa0..abacede 100644 --- a/orbit_loader_test.go +++ b/feedback/orbit_loader_test.go @@ -1,6 +1,8 @@ -package mujica +package feedback -import "testing" +import ( + "testing" +) func TestOrbitLoaderSnapshot(t *testing.T) { loaderSnapshot(t, "orbit_loader", OrbitLoader) } diff --git a/presence.go b/feedback/presence.go similarity index 90% rename from presence.go rename to feedback/presence.go index 98f4e9f..d969461 100644 --- a/presence.go +++ b/feedback/presence.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PresenceKind is how content comes and goes. @@ -68,7 +69,7 @@ func Presence(c *ui.Context, show bool, opts PresenceOptions, content func()) *u return } box := ui.Column(c).AlignItems(ui.Stretch).Key("presence") - box.Transition(ui.ElementTransition{Duration: Motion(c, d), Enter: m, Exit: m}) + box.Transition(ui.ElementTransition{Duration: core.Motion(c, d), Enter: m, Exit: m}) box.Children(content) }) return holder diff --git a/presence_test.go b/feedback/presence_test.go similarity index 92% rename from presence_test.go rename to feedback/presence_test.go index bb32fdf..224f1ff 100644 --- a/presence_test.go +++ b/feedback/presence_test.go @@ -1,10 +1,12 @@ -package mujica +package feedback import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPresenceEntersExitsAndRemoves(t *testing.T) { @@ -56,7 +58,7 @@ func TestPresenceReducedMotionAndInvalid(t *testing.T) { t.Fatal("unknown kind did not panic") } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); Presence(c, true, PresenceOptions{Kind: 9}, nil) }, 100, 50, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); Presence(c, true, PresenceOptions{Kind: 9}, nil) }, 100, 50, 1) } func TestPresenceSnapshot(t *testing.T) { diff --git a/progress.go b/feedback/progress.go similarity index 85% rename from progress.go rename to feedback/progress.go index 1bac8af..e48dd71 100644 --- a/progress.go +++ b/feedback/progress.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ProgressShape is how a progress indicator draws. The zero value is ProgressLinear. @@ -42,12 +43,12 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { if !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { panic(fmt.Sprintf("mujica: Progress Size %v invalid", opts.Size)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() still := c.Preferences().ReduceMotion name := opts.Label if name == "" { - name = msg(c, "loading") + name = core.Msg(c, "loading") } acc := value if opts.Indeterminate { @@ -56,7 +57,7 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { caption := "" switch { case opts.Indeterminate && still: - caption = msg(c, "progress.indeterminate") + caption = core.Msg(c, "progress.indeterminate") case !opts.Indeterminate && opts.ShowValue: caption = strconv.Itoa(int(math.Round(value*100))) + "%" } @@ -73,7 +74,7 @@ func Progress(c *ui.Context, value float64, opts ProgressOptions) *ui.Element { if opts.Label != "" || caption != "" { ui.Row(c).Gap(u * 2).Children(func() { ui.Text(c, opts.Label).TextColor(k.Text).Grow(1).Shrink(1) - ui.Text(c, caption).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } bar := ui.Progress(c, acc).Label(name).FillWidth().Height(u * 1.5).Radius(theme.ControlRadius).Background(k.Border) @@ -114,7 +115,7 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c shown = progressAnimate(ring, shown, still) } stroke := max(3, d/10) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) tw, th := c.MeasureText(0, ui.Span{Text: caption, Size: size}) inside := caption != "" && !opts.Indeterminate && tw+4 <= d-2*stroke && th <= d-2*stroke ring.Draw(func(p *ui.Painter, r ui.Rect) { @@ -125,14 +126,14 @@ func progressRing(c *ui.Context, k theme.Tokens, u float32, acc float64, name, c switch { case !opts.Indeterminate: if shown > 0 { - p.StrokePath(progressArc(p, cx, cy, rad, 0, shown), stroke, k.AccentText) + p.StrokePath(core.ProgressArc(p, cx, cy, rad, 0, shown), stroke, k.AccentText) } case still: p.StrokePath(&track, stroke, k.AccentText) default: p.AnimationFrame() start := float32(p.Now().UnixMilli()%1200) / 1200 - p.StrokePath(progressArc(p, cx, cy, rad, start, start+0.25), stroke, k.AccentText) + p.StrokePath(core.ProgressArc(p, cx, cy, rad, start, start+0.25), stroke, k.AccentText) } }) if inside { @@ -164,23 +165,6 @@ func progressAnimate(e *ui.Element, target float32, still bool) float32 { return e.Animate("progress.value", target, theme.StateDuration) } -// progressArc is the arc from turn a to turn b, clockwise from twelve o'clock. -func progressArc(pt *ui.Painter, cx, cy, r float32, a, b float32) *ui.Path { - var p ui.Path - n := max(2, int(math.Ceil(float64((b-a)*64)))) - for i := 0; i <= n; i++ { - t := float64(a + (b-a)*float32(i)/float32(n)) - x := cx + r*float32(math.Sin(2*math.Pi*t)) - y := cy - r*float32(math.Cos(2*math.Pi*t)) - if i == 0 { - p.MoveTo(internal.Snap(pt, x), internal.Snap(pt, y)) - } else { - p.LineTo(internal.Snap(pt, x), internal.Snap(pt, y)) - } - } - return &p -} - // MeterOptions configure a Meter. type MeterOptions struct { // Min and Max bound the value; both zero mean 0 to 1. @@ -223,21 +207,21 @@ func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element { if l := opts.Levels; l != nil && !(l.Warning >= lo && l.Warning <= hi && l.Critical >= lo && l.Critical <= hi) { panic(fmt.Sprintf("mujica: Meter levels %+v outside %v..%v", *l, lo, hi)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() level := meterLevel(value, opts.Levels) tone := []ui.Color{k.Success, k.Warning, k.Danger}[level] var word string if opts.Levels != nil { low := opts.Levels.Critical < opts.Levels.Warning - word = msg(c, "meter.normal") + word = core.Msg(c, "meter.normal") switch { case level == 2: - word = msg(c, "meter.critical") + word = core.Msg(c, "meter.critical") case level == 1 && low: - word = msg(c, "meter.low_warning") + word = core.Msg(c, "meter.low_warning") case level == 1: - word = msg(c, "meter.warning") + word = core.Msg(c, "meter.warning") } } frac := func(v float64) float32 { return float32((v - lo) / (hi - lo)) } @@ -261,7 +245,7 @@ func Meter(c *ui.Context, value float64, opts MeterOptions) *ui.Element { if opts.Label != "" || caption != "" { ui.Row(c).Gap(u * 2).Children(func() { ui.Text(c, opts.Label).TextColor(k.Text).Grow(1).Shrink(1) - ui.Text(c, caption).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, caption).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } name := opts.Label diff --git a/progress_test.go b/feedback/progress_test.go similarity index 91% rename from progress_test.go rename to feedback/progress_test.go index 4016a1e..594cc0e 100644 --- a/progress_test.go +++ b/feedback/progress_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "math" @@ -6,14 +6,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestProgressReducedMotionJumpsMidTransition(t *testing.T) { v := 0.0 var bar ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Width(300).Children(func() { bar = Progress(c, v, ProgressOptions{}).Bounds() }) @@ -32,12 +33,12 @@ func progressPixel(tt *ui.Tester, x, y float32) ui.Color { } func TestProgressStaticFillContrast(t *testing.T) { - for _, m := range []Mode{Light, Dark} { + for _, m := range []core.Mode{core.Light, core.Dark} { var bar ui.Rect var k theme.Tokens tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: m}) - k = Tokens(c) + core.Use(c, core.Settings{Mode: m}) + k = core.Tokens(c) ui.Column(c).Width(300).Children(func() { bar = Progress(c, 0, ProgressOptions{Indeterminate: true}).Bounds() }) @@ -56,7 +57,7 @@ func TestProgressStaticFillContrast(t *testing.T) { func TestProgressValueText(t *testing.T) { v := 0.424 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Progress(c, v, ProgressOptions{Label: "Uploading", ShowValue: true}) Progress(c, 1, ProgressOptions{Shape: ProgressRing, Label: "Indexing", ShowValue: true}) }, 400, 300) @@ -75,7 +76,7 @@ func TestProgressValueText(t *testing.T) { func TestProgressIndeterminateReducedMotion(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Progress(c, math.NaN(), ProgressOptions{Indeterminate: true, Label: "Connecting"}) }, 400, 300) if tt.HasText("In progress") { @@ -95,7 +96,7 @@ func TestProgressIndeterminateReducedMotion(t *testing.T) { func TestProgressRingSize(t *testing.T) { var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { w = Progress(c, 0.5, ProgressOptions{Shape: ProgressRing, Size: 24}).Bounds().W }) @@ -125,7 +126,7 @@ func TestMeterLevels(t *testing.T) { func TestMeterWords(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) Meter(c, 90, MeterOptions{Max: 100, Levels: &ui.MeterLevels{Warning: 80, Critical: 95}, Label: "磁盘", ShowValue: true}) Meter(c, 15, MeterOptions{Max: 100, Levels: &ui.MeterLevels{Warning: 20, Critical: 10}, Label: "电量"}) }, 400, 300) @@ -153,7 +154,7 @@ func TestProgressAndMeterPanics(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 200) }() } } diff --git a/pulse_loader.go b/feedback/pulse_loader.go similarity index 88% rename from pulse_loader.go rename to feedback/pulse_loader.go index 69535c8..779075a 100644 --- a/pulse_loader.go +++ b/feedback/pulse_loader.go @@ -1,9 +1,9 @@ -package mujica +package feedback import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // PulseLoader shows one dot gently breathing, without a halo. diff --git a/pulse_loader_test.go b/feedback/pulse_loader_test.go similarity index 82% rename from pulse_loader_test.go rename to feedback/pulse_loader_test.go index 990fdf0..d4e856c 100644 --- a/pulse_loader_test.go +++ b/feedback/pulse_loader_test.go @@ -1,6 +1,8 @@ -package mujica +package feedback -import "testing" +import ( + "testing" +) func TestPulseLoaderSnapshot(t *testing.T) { loaderSnapshot(t, "pulse_loader", PulseLoader) } diff --git a/reorder_transition.go b/feedback/reorder_transition.go similarity index 88% rename from reorder_transition.go rename to feedback/reorder_transition.go index d96ccdd..2e62e09 100644 --- a/reorder_transition.go +++ b/feedback/reorder_transition.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ReorderTransitionOptions configure a ReorderTransition. @@ -34,7 +35,7 @@ func ReorderTransition[K comparable](c *ui.Context, keys []K, opts ReorderTransi } seen[k] = true } - move := ui.ElementTransition{Duration: Motion(c, d), Position: true, Enter: &ui.Motion{}, Exit: &ui.Motion{}} + move := ui.ElementTransition{Duration: core.Motion(c, d), Position: true, Enter: &ui.Motion{}, Exit: &ui.Motion{}} col := ui.Column(c).AlignItems(ui.Stretch).Gap(opts.Gap) type state struct { keys []K diff --git a/reorder_transition_test.go b/feedback/reorder_transition_test.go similarity index 91% rename from reorder_transition_test.go rename to feedback/reorder_transition_test.go index 43695ff..2c9b486 100644 --- a/reorder_transition_test.go +++ b/feedback/reorder_transition_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "math" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestReorderTransitionSlides(t *testing.T) { @@ -35,7 +37,7 @@ func TestReorderTransitionSlides(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ReorderTransition(c, []int{1, 1}, ReorderTransitionOptions{}, func(int) {}) }, 100, 50, 1) } @@ -44,7 +46,7 @@ func TestReorderTransitionSnapshot(t *testing.T) { snapshot(t, "reorder_transition", 300, 140, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { ReorderTransition(c, []string{"Sakiko", "Uika", "Mutsumi"}, ReorderTransitionOptions{Gap: 4}, func(k string) { - ui.Text(c, k).Padding(4, 8).Background(Tokens(c).Surface).Radius(4) + ui.Text(c, k).Padding(4, 8).Background(core.Tokens(c).Surface).Radius(4) }) }) }) @@ -59,7 +61,7 @@ func TestReorderTransitionInvalidGap(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ReorderTransition(c, []int{1}, ReorderTransitionOptions{Gap: gap}, func(int) {}) }, 200, 100, 1) }() diff --git a/result.go b/feedback/result.go similarity index 79% rename from result.go rename to feedback/result.go index 3ba96f1..cc55ebf 100644 --- a/result.go +++ b/feedback/result.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ResultKind is the outcome a Result page reports. The zero value is ResultSuccess. @@ -40,8 +42,8 @@ type ResultResult struct { // Result shows a task outcome and reports the chosen next step; the caller navigates. func Result(c *ui.Context, opts ResultOptions) ResultResult { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var tone ui.Color var icon *ui.SVG var key string @@ -55,12 +57,12 @@ func Result(c *ui.Context, opts ResultOptions) ResultResult { default: panic(fmt.Sprintf("mujica: unknown ResultKind %d", int(opts.Kind))) } - title := cmp.Or(opts.Title, msg(c, key)) + title := cmp.Or(opts.Title, core.Msg(c, key)) res := ResultResult{Action: -1} res.Element = ui.Column(c).FillWidth().AlignItems(ui.Center).Gap(u*3).Padding(u*10, u*6).Role(ui.RoleGroup).Label(title) res.Element.Children(func() { ui.Icon(c, icon).Size(48, 48).TextColor(tone) - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text).TextAlign(ui.Center) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).TextColor(k.Text).TextAlign(ui.Center) if opts.Description != "" { ui.Text(c, opts.Description).TextColor(k.TextMuted).TextAlign(ui.Center).MaxWidth(420).LineHeight(theme.LineHeight) } @@ -70,11 +72,11 @@ func Result(c *ui.Context, opts ResultOptions) ResultResult { } ui.Row(c).Gap(u * 3).Wrap().Justify(ui.Center).Children(func() { for i, a := range opts.Actions { - v := Secondary + v := input.Secondary if i == 0 { - v = Primary + v = input.Primary } - if Button(c, a, ButtonOptions{Variant: v}).Key(i).Clicked() { + if input.Button(c, a, input.ButtonOptions{Variant: v}).Key(i).Clicked() { slog.Debug("mujica: result action", "kind", opts.Kind, "index", i) res.Action = i } diff --git a/result_test.go b/feedback/result_test.go similarity index 85% rename from result_test.go rename to feedback/result_test.go index b7ec69f..692079e 100644 --- a/result_test.go +++ b/feedback/result_test.go @@ -1,15 +1,17 @@ -package mujica +package feedback import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestResultActions(t *testing.T) { got := []int{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if a := Result(c, ResultOptions{Kind: ResultFailure, Description: "The payment was declined.", Actions: []string{"Try again", "Back to cart"}}).Action; a >= 0 { got = append(got, a) } @@ -30,7 +32,7 @@ func TestResultActions(t *testing.T) { func TestResultTitles(t *testing.T) { for kind, want := range map[ResultKind]string{ResultSuccess: "操作成功", ResultFailure: "操作失败", ResultForbidden: "无访问权限"} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) Result(c, ResultOptions{Kind: kind}) }, 500, 400) if !tt.HasText(want) { @@ -38,7 +40,7 @@ func TestResultTitles(t *testing.T) { } } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Result(c, ResultOptions{Title: "Order placed"}) }, 500, 400) if !tt.HasText("Order placed") || tt.HasText("Done") { @@ -52,7 +54,7 @@ func TestResultPanicsOnUnknownKind(t *testing.T) { t.Fatal("no panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Result(c, ResultOptions{Kind: 7}) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); Result(c, ResultOptions{Kind: 7}) }, 300, 200) } func TestResultSnapshot(t *testing.T) { diff --git a/shimmer.go b/feedback/shimmer.go similarity index 86% rename from shimmer.go rename to feedback/shimmer.go index 3c2eab6..0420024 100644 --- a/shimmer.go +++ b/feedback/shimmer.go @@ -1,12 +1,13 @@ -package mujica +package feedback import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ShimmerOptions configure a Shimmer placeholder. @@ -31,14 +32,14 @@ func Shimmer(c *ui.Context, loading bool, opts ShimmerOptions, content func()) * } return box } - k := Tokens(c) + k := core.Tokens(c) h := opts.Height if h == 0 { - h = Density(c).ControlHeight() + h = core.Density(c).ControlHeight() } label := opts.Label if label == "" { - label = msg(c, "shimmer.loading") + label = core.Msg(c, "shimmer.loading") } fill := k.SurfaceHover if k.Dark { diff --git a/shimmer_test.go b/feedback/shimmer_test.go similarity index 94% rename from shimmer_test.go rename to feedback/shimmer_test.go index e04ac50..484e011 100644 --- a/shimmer_test.go +++ b/feedback/shimmer_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "bytes" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestShimmerOnlyWhileLoading(t *testing.T) { @@ -41,7 +43,7 @@ func TestShimmerInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Shimmer(c, true, ShimmerOptions{Width: -1}, nil) }, 100, 50, 1) } diff --git a/shimmer_text.go b/feedback/shimmer_text.go similarity index 90% rename from shimmer_text.go rename to feedback/shimmer_text.go index d22d6f4..54fe586 100644 --- a/shimmer_text.go +++ b/feedback/shimmer_text.go @@ -1,7 +1,9 @@ -package mujica +package feedback import ( "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // ShimmerTextOptions configure a ShimmerText. @@ -15,7 +17,7 @@ func ShimmerText(c *ui.Context, text string, opts ShimmerTextOptions) *ui.Elemen if text == "" { panic("mujica: ShimmerText needs text") } - k := Tokens(c) + k := core.Tokens(c) still := c.Preferences().ReduceMotion if !opts.Active || still { e := ui.Text(c, text).TextColor(k.TextMuted).SingleLine() @@ -34,7 +36,7 @@ func ShimmerText(c *ui.Context, text string, opts ShimmerTextOptions) *ui.Elemen at := t*float32(len(runes)+6) - 3 spans := make([]ui.Span, len(runes)) for i, ch := range runes { - d := max(0, 1-abs32(float32(i)-at)/3) + d := max(0, 1-core.Abs32(float32(i)-at)/3) spans[i] = ui.Span{Text: string(ch), Size: size, Color: k.TextMuted.Mix(k.Text, d)} } p.Clip(r, 0, func() { p.RichText(r.X, r.Y, 0, spans...) }) diff --git a/shimmer_text_test.go b/feedback/shimmer_text_test.go similarity index 92% rename from shimmer_text_test.go rename to feedback/shimmer_text_test.go index cf49d9b..79b66a1 100644 --- a/shimmer_text_test.go +++ b/feedback/shimmer_text_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "bytes" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestShimmerTextSweepsAndStops(t *testing.T) { @@ -38,7 +40,7 @@ func TestShimmerTextSweepsAndStops(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ShimmerText(c, "", ShimmerTextOptions{}) }, 100, 50, 1) } @@ -55,8 +57,8 @@ func TestShimmerTextSnapshot(t *testing.T) { func TestShimmerTextConstrainedWidth(t *testing.T) { var background ui.Color tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - background = Tokens(c).Background + core.Use(c, core.Settings{}) + background = core.Tokens(c).Background ui.Column(c).Padding(10).Children(func() { ShimmerText(c, "Thinking about the complete program and planning the next steps", ShimmerTextOptions{Active: true}).Width(80) }) diff --git a/skeleton.go b/feedback/skeleton.go similarity index 92% rename from skeleton.go rename to feedback/skeleton.go index 3c4eb04..259a1e5 100644 --- a/skeleton.go +++ b/feedback/skeleton.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SkeletonShape is the placeholder's form. The zero value is SkeletonText. @@ -37,7 +38,7 @@ func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element { if !(opts.Width >= 0) || !(opts.Height >= 0) || opts.Lines < 0 || math.IsInf(float64(opts.Width+opts.Height), 1) { panic(fmt.Sprintf("mujica: Skeleton size %vx%v, %d lines invalid", opts.Width, opts.Height, opts.Lines)) } - k := Tokens(c) + k := core.Tokens(c) still := c.Preferences().ReduceMotion fill := k.SurfaceHover if k.Dark { @@ -68,12 +69,12 @@ func Skeleton(c *ui.Context, opts SkeletonOptions) *ui.Element { case SkeletonRect: h := opts.Height if h == 0 { - h = Density(c).ControlHeight() + h = core.Density(c).ControlHeight() } e.Height(h).Draw(func(p *ui.Painter, r ui.Rect) { paint(p, r, theme.CardRadius) }) default: lines := max(opts.Lines, 1) - size := FontSize(c, theme.BodySize) + size := core.FontSize(c, theme.BodySize) line := size * theme.LineHeight e.Height(line * float32(lines)).Draw(func(p *ui.Painter, r ui.Rect) { bar := size * 0.8 diff --git a/skeleton_test.go b/feedback/skeleton_test.go similarity index 90% rename from skeleton_test.go rename to feedback/skeleton_test.go index a9b2661..db4ea5b 100644 --- a/skeleton_test.go +++ b/feedback/skeleton_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "bytes" @@ -7,13 +7,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestSkeletonSizes(t *testing.T) { var text, circle, rect, fixed ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) ui.Column(c).Width(300).AlignItems(ui.Start).Children(func() { text = Skeleton(c, SkeletonOptions{Lines: 3}).Bounds() circle = Skeleton(c, SkeletonOptions{Shape: SkeletonCircle}).Bounds() @@ -38,7 +39,7 @@ func TestSkeletonSizes(t *testing.T) { func TestSkeletonStaticUnderReducedMotion(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Skeleton(c, SkeletonOptions{Shape: SkeletonRect}) }, 200, 100) a := encode(t, tt.Image()) @@ -63,7 +64,7 @@ func TestSkeletonPanicsOnNegativeSize(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Skeleton(c, SkeletonOptions{Lines: -1}) }, 200, 100) } diff --git a/stagger.go b/feedback/stagger.go similarity index 88% rename from stagger.go rename to feedback/stagger.go index 7d0613e..30c07b4 100644 --- a/stagger.go +++ b/feedback/stagger.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StaggerOptions configure a Stagger. @@ -65,7 +66,7 @@ func Stagger(c *ui.Context, n int, opts StaggerOptions, item func(i int)) *ui.El box.Children(func() { for i := range upto { ui.Column(c).Key(i).AlignItems(ui.Stretch). - Transition(ui.ElementTransition{Duration: Motion(c, d), Enter: enter}).Children(func() { item(i) }) + Transition(ui.ElementTransition{Duration: core.Motion(c, d), Enter: enter}).Children(func() { item(i) }) } }) return box diff --git a/stagger_test.go b/feedback/stagger_test.go similarity index 89% rename from stagger_test.go rename to feedback/stagger_test.go index 30475db..25de492 100644 --- a/stagger_test.go +++ b/feedback/stagger_test.go @@ -1,10 +1,12 @@ -package mujica +package feedback import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStaggerRevealsInTurnOnce(t *testing.T) { @@ -38,7 +40,7 @@ func TestStaggerRevealsInTurnOnce(t *testing.T) { func TestStaggerReducedMotionAndInvalid(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if c.Preferences().ReduceMotion { Stagger(c, 3, StaggerOptions{}, func(i int) { ui.Textf(c, "Row %d", i) }) } @@ -52,7 +54,7 @@ func TestStaggerReducedMotionAndInvalid(t *testing.T) { t.Fatal("negative n did not panic") } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); Stagger(c, -1, StaggerOptions{}, nil) }, 100, 50, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); Stagger(c, -1, StaggerOptions{}, nil) }, 100, 50, 1) } func TestStaggerSnapshot(t *testing.T) { diff --git a/status_indicator.go b/feedback/status_indicator.go similarity index 80% rename from status_indicator.go rename to feedback/status_indicator.go index 1ebcf5a..2a6334a 100644 --- a/status_indicator.go +++ b/feedback/status_indicator.go @@ -1,12 +1,13 @@ -package mujica +package feedback import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Status is a system or item state. The zero value is StatusSuccess. @@ -45,11 +46,11 @@ type StatusIndicatorOptions struct { // StatusIndicator shows a status by its own shape and a word, never by color alone. func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) *ui.Element { key := "status_indicator." + status.String() // panics on an unknown Status - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() label := opts.Label if label == "" { - label = msg(c, key) + label = core.Msg(c, key) } var tone ui.Color var icon *ui.SVG @@ -68,11 +69,11 @@ func StatusIndicator(c *ui.Context, status Status, opts StatusIndicatorOptions) row := ui.Row(c).AlignItems(ui.Center).Gap(u * 1.5).Role(ui.RoleStatus).Label(label) row.Children(func() { if status == StatusBusy { - Spinner(c, SpinnerOptions{Size: 16, Color: tone, Label: label}) + core.Spinner(c, core.SpinnerOptions{Size: 16, Color: tone, Label: label}) } else { ui.Icon(c, icon).Size(16, 16).TextColor(tone) } - ui.Text(c, label).TextColor(k.Text).FontSize(FontSize(c, theme.BodySize)).SingleLine() + ui.Text(c, label).TextColor(k.Text).FontSize(core.FontSize(c, theme.BodySize)).SingleLine() }) return row } diff --git a/status_indicator_test.go b/feedback/status_indicator_test.go similarity index 85% rename from status_indicator_test.go rename to feedback/status_indicator_test.go index b47df01..f1033b4 100644 --- a/status_indicator_test.go +++ b/feedback/status_indicator_test.go @@ -1,9 +1,11 @@ -package mujica +package feedback import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStatusIndicatorWords(t *testing.T) { @@ -13,7 +15,7 @@ func TestStatusIndicatorWords(t *testing.T) { } for s, word := range want { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusIndicator(c, s, StatusIndicatorOptions{}) }, 300, 100) if !tt.HasText(word) { @@ -21,7 +23,7 @@ func TestStatusIndicatorWords(t *testing.T) { } } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) StatusIndicator(c, StatusOffline, StatusIndicatorOptions{}) StatusIndicator(c, StatusSuccess, StatusIndicatorOptions{Label: "Database online"}) }, 300, 100) @@ -35,7 +37,7 @@ func TestStatusIndicatorShapesDiffer(t *testing.T) { var imgs [][]byte for _, s := range []Status{StatusSuccess, StatusWarning, StatusError, StatusOffline} { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusIndicator(c, s, StatusIndicatorOptions{Label: "Same"}) }, 120, 40) imgs = append(imgs, encode(t, tt.Image())) @@ -55,7 +57,7 @@ func TestStatusIndicatorPanicsOnUnknown(t *testing.T) { t.Fatal("no panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); StatusIndicator(c, 42, StatusIndicatorOptions{}) }, 300, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); StatusIndicator(c, 42, StatusIndicatorOptions{}) }, 300, 100) } func TestStatusIndicatorSnapshot(t *testing.T) { diff --git a/toast.go b/feedback/toast.go similarity index 91% rename from toast.go rename to feedback/toast.go index 570205d..ed90eb0 100644 --- a/toast.go +++ b/feedback/toast.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Toast timing and stacking per window. @@ -20,7 +22,7 @@ const ( type ToastMessage struct { // ID dedupes: showing an ID already queued replaces it. Empty uses Message. ID string - Severity Severity + Severity core.Severity Message string // Action labels a button; Toaster reports its click by ID. Action string @@ -57,7 +59,7 @@ func toastLife(m ToastMessage) time.Duration { panic("mujica: negative toast Duration") case m.Duration > 0: return m.Duration - case m.Severity == SeverityError: + case m.Severity == core.SeverityError: return 0 } return ToastDuration @@ -195,12 +197,12 @@ func Toaster(c *ui.Context) ToasterResult { if len(q.items) == 0 { return res } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() w, _ := c.Size() ui.Overlay(c, func() { stack := ui.Column(c).Absolute().Right(u * 4).Bottom(u * 4).Gap(u * 2).AlignItems(ui.End). - PassThrough().Role(ui.RoleGroup).Label(msg(c, "toast.region")) + PassThrough().Role(ui.RoleGroup).Label(core.Msg(c, "toast.region")) stack.Children(func() { for i := range q.items { it := q.items[i] @@ -240,7 +242,7 @@ func Toaster(c *ui.Context) ToasterResult { // toastCard builds one toast and reports its events and whether hover or focus holds it. func toastCard(c *ui.Context, k theme.Tokens, u, width float32, m ToastMessage) (action, closed, held bool) { - tone, icon := severityLook(k, m.Severity) + tone, icon := core.SeverityLook(k, m.Severity) card := ui.Row(c).Key(m.ID).Width(width).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4). Radius(theme.PopupRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder). Role(ui.RoleStatus).Label(m.Message) @@ -256,10 +258,10 @@ func toastCard(c *ui.Context, k theme.Tokens, u, width float32, m ToastMessage) card.Children(func() { ui.Icon(c, icon).Size(20, 20).TextColor(tone) ui.Text(c, m.Message).TextColor(k.Text).Grow(1).Shrink(1).LineHeight(theme.LineHeight) - if m.Action != "" && Button(c, m.Action, ButtonOptions{Variant: Ghost}).Clicked() { + if m.Action != "" && input.Button(c, m.Action, input.ButtonOptions{Variant: input.Ghost}).Clicked() { action = true } - x := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Label: msg(c, "toast.close")}) + x := input.Button(c, "", input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("x"), Label: core.Msg(c, "toast.close")}) x.TextColor(k.TextMuted).Padding(0, u) if x.Clicked() { closed = true diff --git a/toast_test.go b/feedback/toast_test.go similarity index 91% rename from toast_test.go rename to feedback/toast_test.go index 5767a89..0c149eb 100644 --- a/toast_test.go +++ b/feedback/toast_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "slices" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func toastIDs(q *toastQueue, visible bool) []string { @@ -65,8 +68,8 @@ func TestToastQueueDedupesByID(t *testing.T) { func TestToastQueueErrorsStayAndPauseHolds(t *testing.T) { var q toastQueue t0 := time.Unix(0, 0) - q.push(ToastMessage{ID: "e", Severity: SeverityError, Message: "Failed"}, t0) - q.push(ToastMessage{ID: "w", Severity: SeverityWarning, Message: "Slow", Action: "Details"}, t0) + q.push(ToastMessage{ID: "e", Severity: core.SeverityError, Message: "Failed"}, t0) + q.push(ToastMessage{ID: "w", Severity: core.SeverityWarning, Message: "Slow", Action: "Details"}, t0) q.settle(t0.Add(4*time.Second), map[string]bool{"w": true}) q.expire(t0.Add(time.Hour)) if next := q.settle(t0.Add(time.Hour), map[string]bool{"w": true}); next != 0 { @@ -101,8 +104,8 @@ func TestToastQueuePanics(t *testing.T) { func toastTester(m ToastMessage, res *ToasterResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Notify", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Notify", input.ButtonOptions{}).Clicked() { ShowToast(c, m) } if r := Toaster(c); r != (ToasterResult{}) { @@ -161,8 +164,8 @@ func TestToastExpiresAndHoverPauses(t *testing.T) { var res ToasterResult const life = 40 * time.Millisecond tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Notify", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Notify", input.ButtonOptions{}).Clicked() { ShowToast(c, ToastMessage{Message: "Copied", Duration: life}) } res = Toaster(c) @@ -198,9 +201,9 @@ func TestToastExpiresAndHoverPauses(t *testing.T) { func TestToastSnapshot(t *testing.T) { snapshot(t, "toast", 480, 300, func(c *ui.Context) { if len(toasts(c).items) == 0 { - ShowToast(c, ToastMessage{ID: "1", Message: "Settings saved", Severity: SeveritySuccess}) + ShowToast(c, ToastMessage{ID: "1", Message: "Settings saved", Severity: core.SeveritySuccess}) ShowToast(c, ToastMessage{ID: "2", Message: "Note moved to trash", Action: "Undo"}) - ShowToast(c, ToastMessage{ID: "3", Message: "Upload failed: network lost", Severity: SeverityError}) + ShowToast(c, ToastMessage{ID: "3", Message: "Upload failed: network lost", Severity: core.SeverityError}) ShowToast(c, ToastMessage{ID: "4", Message: "Queued and not shown"}) } Toaster(c) diff --git a/token_usage_chart.go b/feedback/token_usage_chart.go similarity index 69% rename from token_usage_chart.go rename to feedback/token_usage_chart.go index dd3c326..c4d8b7c 100644 --- a/token_usage_chart.go +++ b/feedback/token_usage_chart.go @@ -1,9 +1,12 @@ -package mujica +package feedback import ( "strconv" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // TokenUsage is the tokens one session or step used. @@ -15,7 +18,7 @@ type TokenUsage struct { // TokenUsageChartOptions configure a TokenUsageChart. type TokenUsageChartOptions struct { // Frame names, sizes and states the chart. - Frame ChartFrame + Frame chart.ChartFrame // Line draws lines instead of stacked bars. Line bool // Version changes when usage is edited in place. @@ -27,13 +30,13 @@ type tokenUsageSeries struct { n int version int labels []string - series []ChartSeries + series []chart.ChartSeries } // TokenUsageChart shows input and output tokens per step. -func TokenUsageChart(c *ui.Context, usage []TokenUsage, opts TokenUsageChartOptions) ChartResult { +func TokenUsageChart(c *ui.Context, usage []TokenUsage, opts TokenUsageChartOptions) chart.ChartResult { if opts.Frame.Label == "" { - opts.Frame.Label = msg(c, "token_usage_chart.label") + opts.Frame.Label = core.Msg(c, "token_usage_chart.label") } holder := ui.Column(c).AlignItems(ui.Stretch) cache := ui.Local(holder, "series", func() *tokenUsageSeries { return &tokenUsageSeries{} }) @@ -61,19 +64,19 @@ func TokenUsageChart(c *ui.Context, usage []TokenUsage, opts TokenUsageChartOpti s.labels[at] += " … " + u.Label } } - s.series = []ChartSeries{{Name: msg(c, "token_usage_chart.input"), Values: in}, {Name: msg(c, "token_usage_chart.output"), Values: out}} + s.series = []chart.ChartSeries{{Name: core.Msg(c, "token_usage_chart.input"), Values: in}, {Name: core.Msg(c, "token_usage_chart.output"), Values: out}} s.src, s.n, s.version = first, len(usage), opts.Version } - s.series[0].Name = msg(c, "token_usage_chart.input") - s.series[1].Name = msg(c, "token_usage_chart.output") + s.series[0].Name = core.Msg(c, "token_usage_chart.input") + s.series[1].Name = core.Msg(c, "token_usage_chart.output") format := func(v float64) string { return strconv.FormatFloat(v, 'f', 0, 64) } - var res ChartResult + var res chart.ChartResult holder.Children(func() { if opts.Line { - res = LineChart(c, s.series, LineChartOptions{Frame: opts.Frame, Format: format}) + res = chart.LineChart(c, s.series, chart.LineChartOptions{Frame: opts.Frame, Format: format}) return } - res = BarChart(c, s.labels, s.series, BarChartOptions{Frame: opts.Frame, Stacked: true, Format: format}) + res = chart.BarChart(c, s.labels, s.series, chart.BarChartOptions{Frame: opts.Frame, Stacked: true, Format: format}) }) res.Element = holder return res diff --git a/token_usage_chart_test.go b/feedback/token_usage_chart_test.go similarity index 84% rename from token_usage_chart_test.go rename to feedback/token_usage_chart_test.go index 0096501..6da149a 100644 --- a/token_usage_chart_test.go +++ b/feedback/token_usage_chart_test.go @@ -1,10 +1,13 @@ -package mujica +package feedback import ( "strconv" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func tokenUsageSample() []TokenUsage { @@ -13,7 +16,7 @@ func tokenUsageSample() []TokenUsage { func TestTokenUsageChartBarsKeyboard(t *testing.T) { usage := tokenUsageSample() - h := &chartHarness{build: func(c *ui.Context) ChartResult { return TokenUsageChart(c, usage, TokenUsageChartOptions{}) }} + h := &chartHarness{build: func(c *ui.Context) chart.ChartResult { return TokenUsageChart(c, usage, TokenUsageChartOptions{}) }} tt := ui.NewTester(h.view, 520, 420) tabTo(t, tt, "Token usage") want := []string{"Plan · Input 1200", "Plan · Output 300", "Search · Input 4800"} @@ -30,8 +33,8 @@ func TestTokenUsageChartBarsKeyboard(t *testing.T) { func TestTokenUsageChartLineEmptyInvalid(t *testing.T) { usage := tokenUsageSample() - h := &chartHarness{build: func(c *ui.Context) ChartResult { - return TokenUsageChart(c, usage, TokenUsageChartOptions{Line: true, Frame: ChartFrame{Label: "Tokens per step"}}) + h := &chartHarness{build: func(c *ui.Context) chart.ChartResult { + return TokenUsageChart(c, usage, TokenUsageChartOptions{Line: true, Frame: chart.ChartFrame{Label: "Tokens per step"}}) }} tt := ui.NewTester(h.view, 520, 420) tabTo(t, tt, "Tokens per step") @@ -50,7 +53,7 @@ func TestTokenUsageChartLineEmptyInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TokenUsageChart(c, []TokenUsage{{Label: "x", Input: -1}}, TokenUsageChartOptions{}) }, 300, 200, 1) } @@ -65,13 +68,13 @@ func TestTokenUsageChartSnapshot(t *testing.T) { } func TestTokenUsageChartLocaleRefresh(t *testing.T) { - locale := EnUS + locale := core.EnUS usage := tokenUsageSample() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: locale}) + core.Use(c, core.Settings{Locale: locale}) TokenUsageChart(c, usage, TokenUsageChartOptions{}) }, 520, 420) - locale = ZhCN + locale = core.ZhCN tt.Frame() if !tt.HasText("输入") || !tt.HasText("输出") { t.Fatalf("stale legend: %q", tt.Texts()) @@ -85,7 +88,7 @@ func TestTokenUsageChartLargeDataBounded(t *testing.T) { } var cache *tokenUsageSeries tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TokenUsageChart(c, usage, TokenUsageChartOptions{}) cache = *ui.Local(r.Element, "series", func() *tokenUsageSeries { panic("missing series cache") }) }, 520, 420) diff --git a/tool_registry_panel.go b/feedback/tool_registry_panel.go similarity index 74% rename from tool_registry_panel.go rename to feedback/tool_registry_panel.go index 228df43..2107a84 100644 --- a/tool_registry_panel.go +++ b/feedback/tool_registry_panel.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "fmt" @@ -6,7 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolParam is a parameter in a tool's input schema. @@ -35,7 +38,7 @@ type ToolRegistryPanelOptions struct { // ToolRegistryResult is a built ToolRegistryPanel and its events. type ToolRegistryResult struct { Element *ui.Element - List ListView + List data.ListView toggled string on bool } @@ -52,9 +55,9 @@ type toolRegistryIndex struct { } // ToolRegistryPanel lists tools with switches and a schema preview. -func ToolRegistryPanel(c *ui.Context, s *ListState[string], tools []AgentTool, opts ToolRegistryPanelOptions) ToolRegistryResult { - k := Tokens(c) - u := Density(c).Unit() +func ToolRegistryPanel(c *ui.Context, s *data.ListState[string], tools []AgentTool, opts ToolRegistryPanelOptions) ToolRegistryResult { + k := core.Tokens(c) + u := core.Density(c).Unit() row := ui.Row(c).AlignItems(ui.Stretch).Gap(u * 3).Disabled(opts.Disabled) res := ToolRegistryResult{Element: row} index := ui.Local(row, "index", func() toolRegistryIndex { return toolRegistryIndex{} }) @@ -74,17 +77,17 @@ func ToolRegistryPanel(c *ui.Context, s *ListState[string], tools []AgentTool, o } row.Children(func() { ui.Column(c).Width(300).Shrink(0).AlignItems(ui.Stretch).Children(func() { - res.List = List(c, s, len(tools), ListOptions[string]{ + res.List = data.List(c, s, len(tools), data.ListOptions[string]{ Key: func(i int) string { return tools[i].Name }, Label: func(i int) string { return tools[i].Name }, - Empty: msg(c, "tool_registry_panel.empty"), + Empty: core.Msg(c, "tool_registry_panel.empty"), }, func(i int) { t := tools[i] ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, t.Name).Font(theme.MonoFont).FontWeight(600).SingleLine() - ui.Text(c, t.Description).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, t.Description).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) on := t.Enabled - Switch(c, &on, "", SwitchOptions{}).Label(fmt.Sprintf(msg(c, "tool_registry_panel.enable"), t.Name)) + input.Switch(c, &on, "", input.SwitchOptions{}).Label(fmt.Sprintf(core.Msg(c, "tool_registry_panel.enable"), t.Name)) if on != t.Enabled && !row.IsDisabled() { res.toggled, res.on = t.Name, on slog.Debug("mujica: tool registry toggle", "tool", t.Name, "on", on) @@ -97,7 +100,7 @@ func ToolRegistryPanel(c *ui.Context, s *ListState[string], tools []AgentTool, o name, ok := s.Selected() i, known := index.byName[name] if !ok || !known { - ui.Text(c, msg(c, "tool_registry_panel.choose")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "tool_registry_panel.choose")).TextColor(k.TextMuted) return } toolSchema(c, tools[i]) @@ -108,27 +111,27 @@ func ToolRegistryPanel(c *ui.Context, s *ListState[string], tools []AgentTool, o // toolSchema previews a tool's parameters as a typed list. func toolSchema(c *ui.Context, t AgentTool) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) - ui.Text(c, t.Name).Font(theme.SerifFont).FontWeight(600).FontSize(FontSize(c, theme.H3Size)) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) + ui.Text(c, t.Name).Font(theme.SerifFont).FontWeight(600).FontSize(core.FontSize(c, theme.H3Size)) if t.Description != "" { ui.Text(c, t.Description).TextColor(k.TextMuted) } ui.Box(c).Height(1).Background(k.Ornament).Role(ui.RoleNone) if len(t.Params) == 0 { - ui.Text(c, msg(c, "tool_registry_panel.no_params")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "tool_registry_panel.no_params")).TextColor(k.TextMuted) return } ui.Column(c).Key(t.Name).Grow(1).MinHeight(0).Children(func() { ui.List(c, nil, len(t.Params), func(i int) { p := t.Params[i] - ui.Column(c).Gap(2).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Column(c).Gap(2).MinHeight(core.Density(c).ControlHeight()).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, p.Name).Font(theme.MonoFont).FontWeight(600) ui.Text(c, p.Type).Font(theme.MonoFont).FontSize(small).TextColor(k.Info) if p.Required { - ui.Text(c, msg(c, "tool_registry_panel.required")).FontSize(small).TextColor(k.AccentText). + ui.Text(c, core.Msg(c, "tool_registry_panel.required")).FontSize(small).TextColor(k.AccentText). Padding(0, u).Border(theme.BorderWidth, k.AccentText).Radius(theme.ControlRadius) } }) diff --git a/tool_registry_panel_test.go b/feedback/tool_registry_panel_test.go similarity index 88% rename from tool_registry_panel_test.go rename to feedback/tool_registry_panel_test.go index 7ac6eda..6a9b2a2 100644 --- a/tool_registry_panel_test.go +++ b/feedback/tool_registry_panel_test.go @@ -1,10 +1,13 @@ -package mujica +package feedback import ( "strconv" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func toolSample() []AgentTool { @@ -18,12 +21,12 @@ func toolSample() []AgentTool { } func TestToolRegistryPanelToggleAndPreview(t *testing.T) { - var s ListState[string] + var s data.ListState[string] tools := toolSample() disabled := false var toggled []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(400).Children(func() { r := ToolRegistryPanel(c, &s, tools, ToolRegistryPanelOptions{Disabled: disabled}) r.Element.Grow(1) @@ -73,8 +76,8 @@ func TestToolRegistryPanelInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] ToolRegistryPanel(c, &s, []AgentTool{{Name: "a"}, {Name: "a"}}, ToolRegistryPanelOptions{}) }, 300, 200, 1) } @@ -82,7 +85,7 @@ func TestToolRegistryPanelInvalid(t *testing.T) { func TestToolRegistryPanelSnapshot(t *testing.T) { tools := toolSample() snapshotAfter(t, "tool_registry_panel", 760, 300, func(c *ui.Context) { - s := ui.Local(c.Root(), "s", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "s", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Height(300).Children(func() { ToolRegistryPanel(c, *s, tools, ToolRegistryPanelOptions{}).Element.Grow(1) }) @@ -94,11 +97,11 @@ func TestToolRegistryPanelLargeDataKeyboardAndAncestorDisabled(t *testing.T) { for i := range tools { tools[i] = AgentTool{Name: strconv.Itoa(i)} } - var s ListState[string] + var s data.ListState[string] disabled := false toggles := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := ToolRegistryPanel(c, &s, tools, ToolRegistryPanelOptions{}) r.Element.Height(300) @@ -107,7 +110,7 @@ func TestToolRegistryPanelLargeDataKeyboardAndAncestorDisabled(t *testing.T) { } }) }, 700, 400) - a, b := s.rows.Visible() + a, b := s.Rows.Visible() if b-a > 30 || len(tt.Texts()) > 150 { t.Fatalf("built %d rows", b-a) } @@ -133,9 +136,9 @@ func TestToolRegistryPanelLargeParameterPreview(t *testing.T) { for i := range tools[0].Params { tools[0].Params[i] = ToolParam{Name: "Param " + strconv.Itoa(i), Type: "string"} } - var s ListState[string] + var s data.ListState[string] tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolRegistryPanel(c, &s, tools, ToolRegistryPanelOptions{}).Element.Height(300) }, 700, 400) if err := tt.Click("Tool"); err != nil { diff --git a/trace_viewer.go b/feedback/trace_viewer.go similarity index 72% rename from trace_viewer.go rename to feedback/trace_viewer.go index ca108bd..53c7545 100644 --- a/trace_viewer.go +++ b/feedback/trace_viewer.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TraceAttr is a key and value recorded on a span. @@ -29,7 +31,7 @@ type TraceSpan struct { // TraceViewerState holds the tree's sort, open spans, selection and index. type TraceViewerState struct { - Tree TreeTableState[string] + Tree data.TreeTableState[string] index traceIndex src *TraceSpan n int @@ -46,14 +48,14 @@ type traceIndex struct { // TraceViewerOptions configure a TraceViewer. type TraceViewerOptions struct { - Status DataStatus + Status data.DataStatus Error string // Version changes when spans are edited in place. Version int } // TraceViewer shows a span tree with time bars and attributes. -func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts TraceViewerOptions) DataTableView { +func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts TraceViewerOptions) data.DataTableView { var first *TraceSpan if len(spans) > 0 { first = &spans[0] @@ -71,11 +73,11 @@ func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts Tra slog.Debug("mujica: trace viewer index", "spans", len(spans), "roots", len(s.index.roots)) } ix := s.index - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() span := func(id string) TraceSpan { return spans[ix.byID[id]] } - cols := []TreeColumn[string]{ - {ID: "name", Title: msg(c, "trace_viewer.span"), Compare: func(a, b string) int { return cmp.Compare(span(a).Name, span(b).Name) }, + cols := []data.TreeColumn[string]{ + {ID: "name", Title: core.Msg(c, "trace_viewer.span"), Compare: func(a, b string) int { return cmp.Compare(span(a).Name, span(b).Name) }, Cell: func(id string) { sp := span(id) if sp.Failed { @@ -83,16 +85,16 @@ func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts Tra } t := ui.Text(c, sp.Name).SingleLine() if sp.Failed { - t.TextColor(k.Danger).Label(sp.Name + ", " + msg(c, "trace_viewer.failed")) + t.TextColor(k.Danger).Label(sp.Name + ", " + core.Msg(c, "trace_viewer.failed")) } }}, - {ID: "duration", Title: msg(c, "trace_viewer.duration"), Width: 96, Align: ui.End, + {ID: "duration", Title: core.Msg(c, "trace_viewer.duration"), Width: 96, Align: ui.End, Compare: func(a, b string) int { return cmp.Compare(span(a).End-span(a).Start, span(b).End-span(b).Start) }, Cell: func(id string) { sp := span(id) ui.Text(c, traceDuration(sp.End-sp.Start)).FontFeatures("tnum").TextColor(k.TextMuted) }}, - {ID: "timeline", Title: msg(c, "trace_viewer.timeline"), Width: 260, Compare: func(a, b string) int { return cmp.Compare(span(a).Start, span(b).Start) }, + {ID: "timeline", Title: core.Msg(c, "trace_viewer.timeline"), Width: 260, Compare: func(a, b string) int { return cmp.Compare(span(a).Start, span(b).Start) }, Cell: func(id string) { sp := span(id) col := k.AccentText @@ -117,15 +119,15 @@ func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts Tra }) }}, } - var v DataTableView + var v data.DataTableView col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2) bounds := col.Bounds() internal.Remeasure(c, col, bounds.W, bounds.H) col.Children(func() { - v = TreeTable(c, &s.Tree, TreeTableOptions[string]{Columns: cols, Roots: ix.roots, + v = data.TreeTable(c, &s.Tree, data.TreeTableOptions[string]{Columns: cols, Roots: ix.roots, Children: func(id string) []string { return ix.kids[id] }, Label: func(id string) string { return span(id).Name }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "trace_viewer.empty")}) + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "trace_viewer.empty")}) v.Element.Grow(1) id, ok := s.Tree.Selected() if _, known := ix.byID[id]; !ok || !known { @@ -143,22 +145,22 @@ func TraceViewer(c *ui.Context, s *TraceViewerState, spans []TraceSpan, opts Tra // traceAttrs lists the chosen span's timing and attributes. func traceAttrs(c *ui.Context, sp TraceSpan, height float32) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ui.Column(c).Gap(u).Padding(u*3, u*4).Background(k.Background).Radius(theme.CardRadius).Shrink(0).Children(func() { - ui.Row(c).Gap(u * 3).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Row(c).Gap(u * 3).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Children(func() { ui.Text(c, sp.Name).Font(theme.SerifFont).FontWeight(600).SingleLine().Grow(1).MinWidth(0) - ui.Text(c, msg(c, "trace_viewer.start")+" "+traceDuration(sp.Start)+" · "+msg(c, "trace_viewer.duration")+" "+traceDuration(sp.End-sp.Start)). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, core.Msg(c, "trace_viewer.start")+" "+traceDuration(sp.Start)+" · "+core.Msg(c, "trace_viewer.duration")+" "+traceDuration(sp.End-sp.Start)). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) row := func(key, val string) { - ui.Row(c).Gap(u * 3).MinHeight(Density(c).ControlHeight()).Children(func() { - ui.Text(c, key).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Width(140).Shrink(0) - ui.Text(c, val).Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).MinWidth(0) + ui.Row(c).Gap(u * 3).MinHeight(core.Density(c).ControlHeight()).Children(func() { + ui.Text(c, key).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Width(140).Shrink(0) + ui.Text(c, val).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1).MinWidth(0) }) } if len(sp.Attrs) > 0 { - ui.Column(c).Key(sp.ID).Height(min(float32(len(sp.Attrs))*Density(c).ControlHeight(), max(Density(c).ControlHeight(), min(200, height/4)))).Children(func() { + ui.Column(c).Key(sp.ID).Height(min(float32(len(sp.Attrs))*core.Density(c).ControlHeight(), max(core.Density(c).ControlHeight(), min(200, height/4)))).Children(func() { ui.List(c, nil, len(sp.Attrs), func(i int) { a := sp.Attrs[i] row(a.Key, a.Value) diff --git a/trace_viewer_test.go b/feedback/trace_viewer_test.go similarity index 96% rename from trace_viewer_test.go rename to feedback/trace_viewer_test.go index 86449db..e1bf71c 100644 --- a/trace_viewer_test.go +++ b/feedback/trace_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "strconv" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func traceSample() []TraceSpan { @@ -45,7 +47,7 @@ func TestTraceViewerSelectShowsAttributes(t *testing.T) { var s TraceViewerState spans := traceSample() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(600).Children(func() { TraceViewer(c, &s, spans, TraceViewerOptions{}).Element.Grow(1) }) @@ -109,7 +111,7 @@ func TestTraceViewerLargeDataAndVersion(t *testing.T) { var s TraceViewerState version := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TraceViewer(c, &s, spans, TraceViewerOptions{Version: version}).Element.Height(300) }, 700, 400) if len(s.index.byID) != 100000 || len(tt.Texts()) > 150 { @@ -130,7 +132,7 @@ func TestTraceViewerLargeAttributePreview(t *testing.T) { } var s TraceViewerState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TraceViewer(c, &s, spans, TraceViewerOptions{}).Element.Height(500) }, 700, 600) if err := tt.Click("Root"); err != nil { @@ -148,7 +150,7 @@ func TestTraceViewerAttributesKeepTreeVisible(t *testing.T) { } var s TraceViewerState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TraceViewer(c, &s, spans, TraceViewerOptions{}).Element.Height(260) }, 700, 600) if err := tt.Click("First span"); err != nil { diff --git a/typewriter.go b/feedback/typewriter.go similarity index 82% rename from typewriter.go rename to feedback/typewriter.go index a95d7a9..01ac4ba 100644 --- a/typewriter.go +++ b/feedback/typewriter.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "log/slog" @@ -7,6 +7,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // TypewriterOptions configure a Typewriter. @@ -26,7 +29,7 @@ type typewriterState struct { } // Typewriter reveals text one character at a time. -func Typewriter(c *ui.Context, text string, opts TypewriterOptions) ChoiceResult { +func Typewriter(c *ui.Context, text string, opts TypewriterOptions) core.ChoiceResult { speed := opts.Speed if speed == 0 { speed = 40 @@ -38,8 +41,8 @@ func Typewriter(c *ui.Context, text string, opts TypewriterOptions) ChoiceResult if step <= 0 { panic("mujica: Typewriter Speed interval exceeds time.Duration") } - col := ui.Column(c).AlignItems(ui.Start).Gap(Density(c).Unit() * 2).Role(ui.RoleGroup).Label(text) - res := ChoiceResult{Element: col} + col := ui.Column(c).AlignItems(ui.Start).Gap(core.Density(c).Unit() * 2).Role(ui.RoleGroup).Label(text) + res := core.ChoiceResult{Element: col} st := ui.Local(col, "typing", func() *typewriterState { return &typewriterState{last: c.Now()} }) s := *st runes := []rune(text) @@ -70,7 +73,7 @@ func Typewriter(c *ui.Context, text string, opts TypewriterOptions) ChoiceResult col.Children(func() { ui.Text(c, string(runes[:s.shown])) if opts.Skippable && s.shown < len(runes) { - b := Button(c, msg(c, "typewriter.skip"), ButtonOptions{Variant: Ghost}) + b := input.Button(c, core.Msg(c, "typewriter.skip"), input.ButtonOptions{Variant: input.Ghost}) if b.Clicked() && !b.IsDisabled() { s.skipped, s.shown = true, len(runes) slog.Debug("mujica: typewriter skip") @@ -78,7 +81,7 @@ func Typewriter(c *ui.Context, text string, opts TypewriterOptions) ChoiceResult } }) if s.shown == len(runes) && !s.done { - s.done, res.changed = true, true + s.done, res = true, core.NewChoiceResult(res.Element, true) } return res } diff --git a/typewriter_test.go b/feedback/typewriter_test.go similarity index 91% rename from typewriter_test.go rename to feedback/typewriter_test.go index e2ffd04..f77c788 100644 --- a/typewriter_test.go +++ b/feedback/typewriter_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "math" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTypewriterRevealsFinishesAndSkips(t *testing.T) { @@ -51,13 +53,13 @@ func TestTypewriterRevealsFinishesAndSkips(t *testing.T) { func TestTypewriterReducedMotionAndInvalid(t *testing.T) { finished := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) finished = Typewriter(c, "All at once", TypewriterOptions{}).Changed() || finished }, 300, 80) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) _ = tt tt2 := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) finished = Typewriter(c, "Shown now", TypewriterOptions{}).Changed() || finished }, 300, 80) tt2.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -69,7 +71,7 @@ func TestTypewriterReducedMotionAndInvalid(t *testing.T) { t.Fatal("negative speed did not panic") } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); Typewriter(c, "x", TypewriterOptions{Speed: -1}) }, 100, 50, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); Typewriter(c, "x", TypewriterOptions{Speed: -1}) }, 100, 50, 1) } func TestTypewriterSnapshot(t *testing.T) { @@ -102,7 +104,7 @@ func TestTypewriterInvalidSpeedBoundary(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Typewriter(c, "text", TypewriterOptions{Speed: speed}) }, 200, 100, 1) }() @@ -123,7 +125,7 @@ func TestTypewriterMinimumIntervalBoundary(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Typewriter(c, "letter", TypewriterOptions{Speed: math.Nextafter(minimum, 0)}) }, 200, 100, 1) } diff --git a/undo_toast.go b/feedback/undo_toast.go similarity index 87% rename from undo_toast.go rename to feedback/undo_toast.go index bbbe138..8a36541 100644 --- a/undo_toast.go +++ b/feedback/undo_toast.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "cmp" @@ -9,8 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // UndoToastOptions configure an UndoToast. @@ -69,8 +71,8 @@ func UndoToast(c *ui.Context, open *bool, message string, opts UndoToastOptions) slog.Debug("mujica: undo_toast expired") return res } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ww, _ := c.Size() left := life - spent ui.Overlay(c, func() { @@ -85,11 +87,11 @@ func UndoToast(c *ui.Context, open *bool, message string, opts UndoToastOptions) ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).Children(func() { ui.Text(c, message).TextColor(k.Text).Grow(1).Shrink(1) secs := int(math.Ceil(left.Seconds())) - label := fmt.Sprintf("%s (%d)", cmp.Or(opts.Action, msg(c, "undo_toast.undo")), secs) - if Button(c, label, ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw")}).Clicked() { + label := fmt.Sprintf("%s (%d)", cmp.Or(opts.Action, core.Msg(c, "undo_toast.undo")), secs) + if input.Button(c, label, input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("refresh-cw")}).Clicked() { res.Undone = true } - if iconAction(c, icons.Must("x"), msg(c, "toast.close")).Clicked() { + if core.IconAction(c, icons.Must("x"), core.Msg(c, "toast.close")).Clicked() { res.Expired = true } }) diff --git a/undo_toast_test.go b/feedback/undo_toast_test.go similarity index 89% rename from undo_toast_test.go rename to feedback/undo_toast_test.go index 4c687d7..4378248 100644 --- a/undo_toast_test.go +++ b/feedback/undo_toast_test.go @@ -1,4 +1,4 @@ -package mujica +package feedback import ( "slices" @@ -6,12 +6,15 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func undoTester(open *bool, life time.Duration, undos *int, last *UndoToastResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Delete", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Delete", input.ButtonOptions{}).Clicked() { *open = true } if r := UndoToast(c, open, "File deleted", UndoToastOptions{Duration: life, OnUndo: func() { *undos++ }}); r.Undone || r.Expired { @@ -78,7 +81,7 @@ func TestUndoToastRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 200) }() } } diff --git a/wave_loader.go b/feedback/wave_loader.go similarity index 91% rename from wave_loader.go rename to feedback/wave_loader.go index 13eb3a0..867aa08 100644 --- a/wave_loader.go +++ b/feedback/wave_loader.go @@ -1,11 +1,11 @@ -package mujica +package feedback import ( "math" "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // WaveLoader draws a sine line flowing to the right. diff --git a/wave_loader_test.go b/feedback/wave_loader_test.go similarity index 81% rename from wave_loader_test.go rename to feedback/wave_loader_test.go index a4ae0f0..ee48f66 100644 --- a/wave_loader_test.go +++ b/feedback/wave_loader_test.go @@ -1,6 +1,8 @@ -package mujica +package feedback -import "testing" +import ( + "testing" +) func TestWaveLoaderSnapshot(t *testing.T) { loaderSnapshot(t, "wave_loader", WaveLoader) } diff --git a/archive_viewer.go b/files/archive_viewer.go similarity index 78% rename from archive_viewer.go rename to files/archive_viewer.go index ac764df..c170ca1 100644 --- a/archive_viewer.go +++ b/files/archive_viewer.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "cmp" @@ -11,6 +11,10 @@ import ( "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) // ArchiveEntry describes an archive member with a slash-separated Path. @@ -23,7 +27,7 @@ type ArchiveEntry struct { // ArchiveViewerState preserves folders, selection, sort and indexed entries. type ArchiveViewerState struct { - table TreeTableState[string] + table data.TreeTableState[string] index archiveIndex data *ArchiveEntry n int @@ -46,7 +50,7 @@ type ArchiveViewerOptions struct { // Revision changes when entries are edited in place. Revision uint64 // Status shows the caller's listing in progress or failed. - Status DataStatus + Status data.DataStatus Error string } @@ -69,7 +73,7 @@ func ArchiveViewer(c *ui.Context, entries []ArchiveEntry, s *ArchiveViewerState, } archiveBuild(entries, s) ix := &s.index - u := Density(c).Unit() + u := core.Density(c).Unit() size := func(p string) (int64, int64) { if e := ix.byPath[p]; e != nil && !e.Dir { return e.Size, e.Packed @@ -77,37 +81,37 @@ func ArchiveViewer(c *ui.Context, entries []ArchiveEntry, s *ArchiveViewerState, t := ix.sums[p] return t[0], t[1] } - cell := func(text string) { codeMono(c, text).TextColor(Tokens(c).TextMuted) } + cell := func(text string) { code.CodeMono(c, text).TextColor(core.Tokens(c).TextMuted) } var res ArchiveViewerResult res.Element = ui.Column(c).MinHeight(0).Gap(u * 2) if res.Element.IsDisabled() { copy := *s copy.table.Selection = filesSelectionCopy(s.table.Selection) - copy.table.tree.outline.Open = filesSelectionCopy(s.table.tree.outline.Open) + copy.table.Tree.Outline.Open = filesSelectionCopy(s.table.Tree.Outline.Open) copy.table.Layout = ui.TableLayout{Order: slices.Clone(s.table.Layout.Order), Widths: maps.Clone(s.table.Layout.Widths)} - copy.table.tree.outline.List.Columns = copy.table.Layout + copy.table.Tree.Outline.List.Columns = copy.table.Layout s = © } res.Element.Children(func() { - codeCaption(c, fmt.Sprintf(msg(c, "archive_viewer.summary"), ix.files, processListBytes(uint64(ix.size)), - processListBytes(uint64(ix.pack)), archiveRatio(ix.size, ix.pack))) - v := TreeTable(c, &s.table, TreeTableOptions[string]{ + code.CodeCaption(c, fmt.Sprintf(core.Msg(c, "archive_viewer.summary"), ix.files, code.ProcessListBytes(uint64(ix.size)), + code.ProcessListBytes(uint64(ix.pack)), archiveRatio(ix.size, ix.pack))) + v := data.TreeTable(c, &s.table, data.TreeTableOptions[string]{ Roots: ix.roots, Children: func(p string) []string { return ix.kids[p] }, Label: func(p string) string { return path.Base(p) }, Status: opts.Status, Error: opts.Error, - Empty: msg(c, "archive_viewer.empty"), - Columns: []TreeColumn[string]{ - {ID: "name", Title: msg(c, "archive_viewer.name"), Compare: strings.Compare, Cell: func(p string) { + Empty: core.Msg(c, "archive_viewer.empty"), + Columns: []data.TreeColumn[string]{ + {ID: "name", Title: core.Msg(c, "archive_viewer.name"), Compare: strings.Compare, Cell: func(p string) { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { FileIcon(c, p, FileIconOptions{Folder: ix.kids[p] != nil}) ui.Text(c, path.Base(p)).SingleLine() }) }}, - {ID: "size", Title: msg(c, "archive_viewer.size"), Width: 96, Align: ui.End, + {ID: "size", Title: core.Msg(c, "archive_viewer.size"), Width: 96, Align: ui.End, Compare: func(a, b string) int { x, _ := size(a); y, _ := size(b); return cmp.Compare(x, y) }, - Cell: func(p string) { n, _ := size(p); cell(processListBytes(uint64(n))) }}, - {ID: "packed", Title: msg(c, "archive_viewer.packed"), Width: 96, Align: ui.End, - Cell: func(p string) { _, n := size(p); cell(processListBytes(uint64(n))) }}, - {ID: "ratio", Title: msg(c, "archive_viewer.ratio"), Width: 72, Align: ui.End, + Cell: func(p string) { n, _ := size(p); cell(code.ProcessListBytes(uint64(n))) }}, + {ID: "packed", Title: core.Msg(c, "archive_viewer.packed"), Width: 96, Align: ui.End, + Cell: func(p string) { _, n := size(p); cell(code.ProcessListBytes(uint64(n))) }}, + {ID: "ratio", Title: core.Msg(c, "archive_viewer.ratio"), Width: 72, Align: ui.End, Cell: func(p string) { a, b := size(p); cell(archiveRatio(a, b)) }}, }, }) diff --git a/archive_viewer_test.go b/files/archive_viewer_test.go similarity index 94% rename from archive_viewer_test.go rename to files/archive_viewer_test.go index 7f15d59..6a67083 100644 --- a/archive_viewer_test.go +++ b/files/archive_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,6 +6,9 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func archiveSample() []ArchiveEntry { @@ -25,7 +28,7 @@ type archiveProbe struct { } func (p *archiveProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ArchiveViewer(c, p.entries, &p.s, p.opts) r.Element.Height(340) if r.Opened != nil { @@ -75,7 +78,7 @@ func TestArchiveViewerExpandAndOpen(t *testing.T) { } func TestArchiveViewerStatusAndInvalid(t *testing.T) { - tt := ui.NewTester((&archiveProbe{opts: ArchiveViewerOptions{Status: DataFailed, Error: "bad archive"}}).view, 620, 360) + tt := ui.NewTester((&archiveProbe{opts: ArchiveViewerOptions{Status: data.DataFailed, Error: "bad archive"}}).view, 620, 360) if !tt.HasText("bad archive") { t.Fatalf("texts %q", tt.Texts()) } @@ -112,7 +115,7 @@ func TestArchiveViewerAncestorDisabled(t *testing.T) { s := ArchiveViewerState{} disabled, opened := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := ArchiveViewer(c, []ArchiveEntry{{Path: "a.txt", Size: 10}}, &s, ArchiveViewerOptions{}) r.Element.Height(200) @@ -139,7 +142,7 @@ func TestArchiveViewerLargeAndRevision(t *testing.T) { s := ArchiveViewerState{} revision := uint64(0) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ArchiveViewer(c, entries, &s, ArchiveViewerOptions{Revision: revision}).Element.Height(220) }, 600, 250) n := 0 @@ -173,7 +176,7 @@ func TestArchiveViewerDisablingBeforeDeliveredKey(t *testing.T) { disabled := false entries := []ArchiveEntry{{Path: "a.txt", Size: 1}, {Path: "b.txt", Size: 1}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { ArchiveViewer(c, entries, &s, ArchiveViewerOptions{}).Element.Height(200) }) }, 600, 250) if err := tt.Click("a.txt"); err != nil { diff --git a/files/doc.go b/files/doc.go new file mode 100644 index 0000000..527bfbc --- /dev/null +++ b/files/doc.go @@ -0,0 +1,2 @@ +// Package files holds MujicaUI file browsing and collaboration. +package files diff --git a/files/example_files_test.go b/files/example_files_test.go new file mode 100644 index 0000000..7997889 --- /dev/null +++ b/files/example_files_test.go @@ -0,0 +1,181 @@ +package files_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/files" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleFileExplorer() { + var state files.FileExplorerState + opts := files.FileExplorerOptions{ + Roots: []string{"/court"}, + Folders: func(string) []string { return nil }, + Entries: func(string) []files.FileEntry { + return []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}} + }, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.FileExplorer(c, &state, opts).Element.Height(240) + } + ui.Render(view, 600, 300, 1) +} + +func ExampleFileGrid() { + entries := []files.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}} + var state files.FileGridState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.FileGrid(c, entries, &state, files.FileGridOptions{}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFileIcon() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.FileIcon(c, "seal.go", files.FileIconOptions{Size: 32}) + } + ui.Render(view, 100, 80, 1) +} + +func ExampleFilePreview() { + file := files.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.FilePreview(c, file, files.FilePreviewOptions{}).Element.Height(240) + } + ui.Render(view, 500, 300, 1) +} + +func ExamplePathBar() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.PathBar(c, "/court/chronicles/seals", files.PathBarOptions{}) + } + ui.Render(view, 400, 100, 1) +} + +func ExampleRenameInline() { + name, editing := "seal.txt", true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.RenameInline(c, &name, &editing, files.RenameInlineOptions{}) + } + ui.Render(view, 400, 100, 1) +} + +func ExampleFileOperationProgress() { + op := files.FileOperation{Kind: files.FileCopy, Done: 2, Total: 10, Current: "seal.go"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.FileOperationProgress(c, op, files.FileOperationOptions{}) + } + ui.Render(view, 500, 240, 1) +} + +func ExampleTransferQueue() { + items := []files.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: files.TransferActive, Done: 25, Total: 100}} + var state files.TransferQueueState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.TransferQueue(c, items, &state, files.TransferQueueOptions{}).Element.Height(240) + } + ui.Render(view, 500, 300, 1) +} + +func ExampleStorageUsage() { + segments := []files.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.StorageUsage(c, segments, files.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}) + } + ui.Render(view, 500, 240, 1) +} + +func ExampleRecentFiles() { + now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) + recent := []files.RecentFile{{Path: "/court/seal.go", Opened: now}} + var state files.RecentFilesState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.RecentFiles(c, recent, &state, files.RecentFilesOptions{Now: now}).Element.Height(240) + } + ui.Render(view, 500, 300, 1) +} + +func ExampleArchiveViewer() { + entries := []files.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}} + var state files.ArchiveViewerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.ArchiveViewer(c, entries, &state, files.ArchiveViewerOptions{}).Element.Height(240) + } + ui.Render(view, 600, 300, 1) +} + +func ExamplePresenceAvatars() { + people := []files.Collaborator{{ID: "ada", Name: "Ada Lovelace"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.PresenceAvatars(c, people, files.PresenceAvatarsOptions{}) + } + ui.Render(view, 300, 100, 1) +} + +func ExampleReactions() { + items := []files.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.Reactions(c, items, files.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}}) + } + ui.Render(view, 400, 100, 1) +} + +func ExampleShareDialog() { + open := false + members := []files.ShareMember{{ID: "ada", Name: "Ada", Permission: files.PermissionEdit, Owner: true}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "Share", input.ButtonOptions{}).Clicked() { + open = true + } + files.ShareDialog(c, &open, &members, files.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"}) + } + ui.Render(view, 600, 400, 1) +} + +func ExamplePermissionSelect() { + value := files.PermissionView + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.PermissionSelect(c, &value, files.PermissionSelectOptions{}) + } + ui.Render(view, 300, 100, 1) +} + +func ExampleLiveIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + files.LiveIndicator(c, files.LiveOffline, files.LiveIndicatorOptions{}) + } + ui.Render(view, 500, 100, 1) +} + +func ExampleRemoteCursor() { + carets := []files.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Box(c).Size(300, 100).Children(func() { + files.RemoteCursor(c, carets, files.RemoteCursorOptions{ + Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} }, + }) + }) + } + ui.Render(view, 320, 120, 1) +} diff --git a/file_explorer.go b/files/file_explorer.go similarity index 85% rename from file_explorer.go rename to files/file_explorer.go index 4910f89..182154c 100644 --- a/file_explorer.go +++ b/files/file_explorer.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "log/slog" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileEntry identifies a file or folder by slash-separated Path. @@ -38,8 +41,8 @@ type FileExplorerOptions struct { type FileExplorerState struct { // Current defaults to the first root. Current string - tree TreeState[string] - list ListState[string] + tree data.TreeState[string] + list data.ListState[string] back, forward []string data *FileEntry n int @@ -95,8 +98,8 @@ func FileExplorer(c *ui.Context, s *FileExplorerState, opts FileExplorerOptions) if s.Current == "" { s.Current = opts.Roots[0] } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res FileExplorerResult res.Element = ui.Column(c).MinHeight(0).Gap(u * 2).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() @@ -104,7 +107,7 @@ func FileExplorer(c *ui.Context, s *FileExplorerState, opts FileExplorerOptions) copy := *s copy.list.Selection = filesSelectionCopy(s.list.Selection) copy.tree.Selection = filesSelectionCopy(s.tree.Selection) - copy.tree.tree.outline.Open = filesSelectionCopy(s.tree.tree.outline.Open) + copy.tree.Tree.Outline.Open = filesSelectionCopy(s.tree.Tree.Outline.Open) s = © } at := s.Current @@ -133,7 +136,7 @@ func FileExplorer(c *ui.Context, s *FileExplorerState, opts FileExplorerOptions) {"chevron-right", "file_explorer.forward", len(s.forward) > 0, func() { s.step(&s.forward, &s.back) }}, {"arrow-up", "file_explorer.up", fileExplorerParent(s.Current, opts.Roots) != s.Current, func() { s.Navigate(fileExplorerParent(s.Current, opts.Roots)) }}, } { - a := iconAction(c, icons.Must(b.icon), msg(c, b.key)).Disabled(!b.on) + a := core.IconAction(c, icons.Must(b.icon), core.Msg(c, b.key)).Disabled(!b.on) if a.Clicked() && b.on { move(b.do) } @@ -142,7 +145,7 @@ func FileExplorer(c *ui.Context, s *FileExplorerState, opts FileExplorerOptions) move(func() { s.Navigate(r.Navigated) }) } }) - ui.Row(c).Grow(1).MinHeight(Density(c).ControlHeight() * 4).Gap(u * 2).AlignItems(ui.Stretch).Children(func() { + ui.Row(c).Grow(1).MinHeight(core.Density(c).ControlHeight() * 4).Gap(u * 2).AlignItems(ui.Stretch).Children(func() { fileExplorerTree(c, s, opts, move) ui.Column(c).Grow(1).MinWidth(0).Children(func() { entries := opts.Entries(s.Current) @@ -165,9 +168,9 @@ func FileExplorer(c *ui.Context, s *FileExplorerState, opts FileExplorerOptions) } s.data, s.n, s.revision = first, len(entries), opts.Revision } - v := List(c, &s.list, len(entries), ListOptions[string]{Multiple: true, + v := data.List(c, &s.list, len(entries), data.ListOptions[string]{Multiple: true, Key: func(i int) string { return entries[i].Path }, Label: func(i int) string { return entries[i].Name }, - Empty: msg(c, "file_explorer.empty")}, func(i int) { fileExplorerRow(c, k, entries[i]) }) + Empty: core.Msg(c, "file_explorer.empty")}, func(i int) { fileExplorerRow(c, k, entries[i]) }) v.Element.Grow(1) res.Changed = v.Changed() && !disabled if key, ok := s.list.Selected(); ok && v.Submitted() && !disabled { @@ -204,8 +207,8 @@ func fileExplorerTree(c *ui.Context, s *FileExplorerState, opts FileExplorerOpti for _, a := range fileExplorerAncestors(s.Current, opts.Roots) { s.tree.SetOpen(a, true) } - s.tree.tree.sel, s.tree.tree.has = s.Current, true - v := Tree(c, &s.tree, TreeOptions[string]{Roots: opts.Roots, Children: opts.Folders, + s.tree.Tree.Sel, s.tree.Tree.Has = s.Current, true + v := data.Tree(c, &s.tree, data.TreeOptions[string]{Roots: opts.Roots, Children: opts.Folders, Label: func(p string) string { return path.Base(p) }}) v.Element.Width(220).Shrink(0) if v.Changed() { @@ -246,9 +249,9 @@ func fileExplorerRow(c *ui.Context, k theme.Tokens, e FileEntry) { FileIcon(c, e.Name, FileIconOptions{Folder: e.Dir}) ui.Text(c, e.Name).SingleLine().Grow(1).MinWidth(0) if !e.Dir { - ui.Text(c, processListBytes(uint64(max(e.Size, 0)))).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Shrink(0) + ui.Text(c, code.ProcessListBytes(uint64(max(e.Size, 0)))).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Shrink(0) } if !e.Modified.IsZero() { - ui.Text(c, e.Modified.Format("2006-01-02 15:04")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Shrink(0) + ui.Text(c, e.Modified.Format("2006-01-02 15:04")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Shrink(0) } } diff --git a/file_explorer_test.go b/files/file_explorer_test.go similarity index 97% rename from file_explorer_test.go rename to files/file_explorer_test.go index 5fb6f2b..cad9a0b 100644 --- a/file_explorer_test.go +++ b/files/file_explorer_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -8,6 +8,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // fileFS maps folders to entry names, with trailing directory slashes. @@ -49,7 +51,7 @@ type explorerProbe struct { } func (p *explorerProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FileExplorer(c, &p.s, p.opts) r.Element.Height(380) if r.Navigated { @@ -182,7 +184,7 @@ func TestFileExplorerPrunesRemovedSelection(t *testing.T) { var s FileExplorerState opts := FileExplorerOptions{Roots: []string{"/court"}, Folders: func(string) []string { return nil }, Entries: func(string) []FileEntry { return entries }} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileExplorer(c, &s, opts).Element.Height(380) }, 720, 400) if err := tt.Click("b.txt"); err != nil { @@ -204,7 +206,7 @@ func TestFileExplorerLargeAndRevision(t *testing.T) { var s FileExplorerState opts := FileExplorerOptions{Roots: []string{"/court"}, Folders: func(string) []string { return nil }, Entries: func(string) []FileEntry { return entries }} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileExplorer(c, &s, opts).Element.Height(380) }, 720, 400) n := 0 diff --git a/file_grid.go b/files/file_grid.go similarity index 86% rename from file_grid.go rename to files/file_grid.go index 46217c8..3ec0c59 100644 --- a/file_grid.go +++ b/files/file_grid.go @@ -1,11 +1,13 @@ -package mujica +package files import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileGridOptions configure a FileGrid. @@ -29,7 +31,7 @@ type FileGridResult struct { // FileGridState is a FileGrid's selection and place. type FileGridState struct { - list ListState[string] + list data.ListState[string] data *FileEntry n int revision uint64 @@ -57,8 +59,8 @@ func FileGrid(c *ui.Context, entries []FileEntry, s *FileGridState, opts FileGri if w == 0 { w = 128 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() if w < theme.MinHitSize { panic("mujica: FileGrid ItemWidth is below the minimum hit size") } @@ -93,7 +95,7 @@ func FileGrid(c *ui.Context, entries []FileEntry, s *FileGridState, opts FileGri } } if moved { - s.list.grid = ui.GridState{} + s.list.Grid = ui.GridState{} s.paths = make([]string, len(entries)) for i, entry := range entries { s.paths[i] = entry.Path @@ -102,16 +104,16 @@ func FileGrid(c *ui.Context, entries []FileEntry, s *FileGridState, opts FileGri s.data, s.n, s.revision = first, len(entries), opts.Revision } res.Element.Children(func() { - v := List(c, &s.list, len(entries), ListOptions[string]{Multiple: true, Grid: true, ItemWidth: w, ItemHeight: thumb*0.75 + 56, + v := data.List(c, &s.list, len(entries), data.ListOptions[string]{Multiple: true, Grid: true, ItemWidth: w, ItemHeight: thumb*0.75 + 56, Key: func(i int) string { return entries[i].Path }, Label: func(i int) string { return entries[i].Name }, - Empty: msg(c, "file_grid.empty")}, func(i int) { + Empty: core.Msg(c, "file_grid.empty")}, func(i int) { e := entries[i] ui.Box(c).Height(thumb * 0.75).Shrink(0).Center().Clip().Radius(theme.ControlRadius).Background(k.Background).Children(func() { if opts.Thumbnail == nil || !opts.Thumbnail(e) { FileIcon(c, e.Name, FileIconOptions{Folder: e.Dir, Size: 40}) } }) - ui.Text(c, e.Name).SingleLine().TextAlign(ui.Center).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, e.Name).SingleLine().TextAlign(ui.Center).FontSize(core.FontSize(c, theme.CaptionSize)) }) v.Element.Grow(1) res.Changed = v.Changed() && !disabled diff --git a/file_grid_test.go b/files/file_grid_test.go similarity index 95% rename from file_grid_test.go rename to files/file_grid_test.go index 869ab03..c706592 100644 --- a/file_grid_test.go +++ b/files/file_grid_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,13 +6,15 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestFileGridFreshSliceKeepsRangeAndScroll(t *testing.T) { entries := fileGridEntries(6) var s FileGridState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileGrid(c, slices.Clone(entries), &s, FileGridOptions{}).Element.Height(320) }, 600, 340) if err := tt.Click("crest-3.png"); err != nil { @@ -53,13 +55,13 @@ type gridProbe struct { } func (p *gridProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FileGrid(c, p.entries, &p.s, FileGridOptions{Disabled: p.disabled, Thumbnail: func(e FileEntry) bool { if FileKindOf(e.Name) != FileImage { return false } p.thumbs++ - ui.Box(c).Fill().Background(Tokens(c).Info.Alpha(0.25)) + ui.Box(c).Fill().Background(core.Tokens(c).Info.Alpha(0.25)) return true }}) r.Element.Height(320) @@ -136,7 +138,7 @@ func TestFileGridRejectsSmallWidth(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileGrid(c, fileGridEntries(3), &FileGridState{}, FileGridOptions{ItemWidth: 1}) }, 400, 300) } diff --git a/file_icon.go b/files/file_icon.go similarity index 90% rename from file_icon.go rename to files/file_icon.go index 97ddb73..306591c 100644 --- a/file_icon.go +++ b/files/file_icon.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "path" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileKind is the family a file belongs to. @@ -81,20 +82,20 @@ func FileIcon(c *ui.Context, name string, opts FileIconOptions) *ui.Element { if opts.Open && !opts.Folder { panic("mujica: FileIcon Open needs Folder") } - k := Tokens(c) + k := core.Tokens(c) size := opts.Size if size == 0 { size = 20 } kind := FileKindOf(name) - label := msg(c, "file_icon.file") + label := core.Msg(c, "file_icon.file") if ext := fileExt(name); ext != "" { label = strings.ToUpper(ext) + " " + label } if opts.Folder { - kind, label = FileFolder, msg(c, "file_icon.folder") + kind, label = FileFolder, core.Msg(c, "file_icon.folder") if opts.Open { - label = msg(c, "file_icon.open") + label = core.Msg(c, "file_icon.open") } } col := fileKindColor(k, kind) @@ -117,7 +118,7 @@ func FileIcon(c *ui.Context, name string, opts FileIconOptions) *ui.Element { if len(ext) > 3 { ext = ext[:3] } - ui.Text(c, ext).Font(theme.MonoFont).FontSize(FontSize(c, size*0.3)).FontWeight(700).TextColor(k.Surface). + ui.Text(c, ext).Font(theme.MonoFont).FontSize(core.FontSize(c, size*0.3)).FontWeight(700).TextColor(k.Surface). Background(col).Radius(1).Padding(0, 1).Absolute().Bottom(size * 0.12).Left(0).Role(ui.RoleNone) } } diff --git a/file_icon_test.go b/files/file_icon_test.go similarity index 92% rename from file_icon_test.go rename to files/file_icon_test.go index 5060fe3..7a90996 100644 --- a/file_icon_test.go +++ b/files/file_icon_test.go @@ -1,11 +1,12 @@ -package mujica +package files import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestFileKindOf(t *testing.T) { @@ -19,7 +20,7 @@ func TestFileKindOf(t *testing.T) { func TestFileIconLabels(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { FileIcon(c, "main.go", FileIconOptions{}) FileIcon(c, "src", FileIconOptions{Folder: true}) @@ -53,7 +54,7 @@ func TestFileIconBadOptionsPanic(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileIcon(c, "a", FileIconOptions{Open: true}) }, 100, 100) } diff --git a/file_operation_progress.go b/files/file_operation_progress.go similarity index 63% rename from file_operation_progress.go rename to files/file_operation_progress.go index 9f18ca2..c34ac0b 100644 --- a/file_operation_progress.go +++ b/files/file_operation_progress.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,8 +6,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileOperationKind is what a file operation does. @@ -58,13 +62,13 @@ func FileOperationProgress(c *ui.Context, op FileOperation, opts FileOperationOp if op.Kind < FileCopy || op.Kind > FileDelete { panic("mujica: FileOperation kind out of range") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res FileOperationResult - res.Element = codePanel(c).Padding(u * 4).Gap(u * 3).Disabled(opts.Disabled).Role(ui.RoleGroup) + res.Element = code.CodePanel(c).Padding(u * 4).Gap(u * 3).Disabled(opts.Disabled).Role(ui.RoleGroup) all := ui.Local(res.Element, "file_operation_progress.all", func() bool { return false }) disabled := res.Element.IsDisabled() - verb := msg(c, [...]string{"file_operation_progress.copying", "file_operation_progress.moving", "file_operation_progress.deleting"}[op.Kind]) + verb := core.Msg(c, [...]string{"file_operation_progress.copying", "file_operation_progress.moving", "file_operation_progress.deleting"}[op.Kind]) title := fmt.Sprintf(verb, op.Done, op.Total) res.Element.Label(title) frac := 0.0 @@ -75,18 +79,18 @@ func FileOperationProgress(c *ui.Context, op FileOperation, opts FileOperationOp frac = float64(op.Done) / float64(op.Total) } res.Element.Children(func() { - Progress(c, frac, ProgressOptions{Label: title, ShowValue: true}) + feedback.Progress(c, frac, feedback.ProgressOptions{Label: title, ShowValue: true}) ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { ui.Column(c).Grow(1).MinWidth(0).Children(func() { if op.Current != "" { - ui.Text(c, op.Current).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, op.Current).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } if op.BytesTotal > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "file_operation_progress.bytes"), processListBytes(uint64(op.BytesDone)), processListBytes(uint64(op.BytesTotal)))). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "file_operation_progress.bytes"), code.ProcessListBytes(uint64(op.BytesDone)), code.ProcessListBytes(uint64(op.BytesTotal)))). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) - if Button(c, msg(c, "file_operation_progress.cancel"), ButtonOptions{Variant: Secondary}).Clicked() && !disabled { + if input.Button(c, core.Msg(c, "file_operation_progress.cancel"), input.ButtonOptions{Variant: input.Secondary}).Clicked() && !disabled { res.Canceled = true } }) @@ -96,16 +100,16 @@ func FileOperationProgress(c *ui.Context, op FileOperation, opts FileOperationOp ui.Column(c).Gap(u*2).Padding(u*3).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Warning).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Warning).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(16, 16) - ui.Text(c, fmt.Sprintf(msg(c, "file_operation_progress.conflict"), op.Conflict)).TextColor(k.Text).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "file_operation_progress.conflict"), op.Conflict)).TextColor(k.Text).Role(ui.RoleStatus) }) - Checkbox(c, all, msg(c, "file_operation_progress.all"), CheckboxOptions{Disabled: opts.Disabled}) + input.Checkbox(c, all, core.Msg(c, "file_operation_progress.all"), input.CheckboxOptions{Disabled: opts.Disabled}) ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { for _, b := range []struct { key string - v ButtonVariant + v input.ButtonVariant cho ConflictChoice - }{{"file_operation_progress.skip", Ghost, ConflictSkip}, {"file_operation_progress.keepBoth", Secondary, ConflictKeepBoth}, {"file_operation_progress.replace", Danger, ConflictReplace}} { - if Button(c, msg(c, b.key), ButtonOptions{Variant: b.v}).Clicked() && !disabled { + }{{"file_operation_progress.skip", input.Ghost, ConflictSkip}, {"file_operation_progress.keepBoth", input.Secondary, ConflictKeepBoth}, {"file_operation_progress.replace", input.Danger, ConflictReplace}} { + if input.Button(c, core.Msg(c, b.key), input.ButtonOptions{Variant: b.v}).Clicked() && !disabled { res.Resolved, res.ApplyAll = b.cho, *all } } diff --git a/file_operation_progress_test.go b/files/file_operation_progress_test.go similarity index 97% rename from file_operation_progress_test.go rename to files/file_operation_progress_test.go index 018f78e..29b0de1 100644 --- a/file_operation_progress_test.go +++ b/files/file_operation_progress_test.go @@ -1,9 +1,11 @@ -package mujica +package files import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type opProbe struct { @@ -15,7 +17,7 @@ type opProbe struct { } func (p *opProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FileOperationProgress(c, p.op, FileOperationOptions{Disabled: p.disabled}) r.Element.Width(480) if r.Canceled { diff --git a/file_preview.go b/files/file_preview.go similarity index 54% rename from file_preview.go rename to files/file_preview.go index 8ee8829..c60541c 100644 --- a/file_preview.go +++ b/files/file_preview.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,8 +7,14 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PreviewFile holds caller-provided preview content. @@ -25,7 +31,7 @@ type PreviewFile struct { // FilePreviewOptions configure a FilePreview. type FilePreviewOptions struct { // Status shows the caller's read in progress or failed. - Status DataStatus + Status data.DataStatus Error string } @@ -39,7 +45,7 @@ type FilePreviewResult struct { var filePreviewLanguages = map[string]string{"go": "go", "js": "javascript", "ts": "typescript", "py": "python", "json": "json", "sh": "shell", "sql": "sql"} type previewCodeState struct { - viewer CodeViewerState + viewer code.CodeViewerState text string lines []string } @@ -49,33 +55,33 @@ func FilePreview(c *ui.Context, f PreviewFile, opts FilePreviewOptions) FilePrev if f.Name == "" { panic("mujica: FilePreview needs a Name") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res FilePreviewResult - res.Element = codePanel(c).Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "file_preview.label"), f.Name)) - code := ui.Local(res.Element, "file_preview.code", func() previewCodeState { return previewCodeState{} }) + res.Element = code.CodePanel(c).Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "file_preview.label"), f.Name)) + codeState := ui.Local(res.Element, "file_preview.code", func() previewCodeState { return previewCodeState{} }) if res.Element.Shortcut(0, ui.KeyEscape) && !res.Element.IsDisabled() { res.Closed = true } res.Element.Children(func() { - codeHeader(c, func() { + code.CodeHeader(c, func() { FileIcon(c, f.Name, FileIconOptions{}) ui.Text(c, f.Name).FontWeight(600).SingleLine().MinWidth(0).Shrink(1) if f.Size > 0 { - ui.Text(c, processListBytes(uint64(f.Size))).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) + ui.Text(c, code.ProcessListBytes(uint64(f.Size))).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) } ui.Spacer(c) - if Button(c, msg(c, "file_preview.open"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() { + if input.Button(c, core.Msg(c, "file_preview.open"), input.ButtonOptions{Variant: input.Ghost}).MinHeight(theme.MinHitSize).Clicked() { res.Opened = true } - if iconAction(c, icons.Must("x"), msg(c, "file_preview.close")).Clicked() { + if core.IconAction(c, icons.Must("x"), core.Msg(c, "file_preview.close")).Clicked() { res.Closed = true } }) body := ui.Column(c).Grow(1).MinHeight(u * 40).Padding(u * 3) body.Children(func() { - if opts.Status != DataReady { - res.Retried = dataPlaceholder(c, opts.Status, opts.Error, "", "") + if opts.Status != data.DataReady { + res.Retried = data.DataPlaceholder(c, opts.Status, opts.Error, "", "") return } lang := f.Language @@ -85,24 +91,24 @@ func FilePreview(c *ui.Context, f PreviewFile, opts FilePreviewOptions) FilePrev switch { case f.Image != nil: ui.Box(c).Grow(1).Center().Clip().Children(func() { - Image(c, f.Image, ImageOptions{Alt: f.Name}).Element.MaxWidthPercent(100).MaxHeightPercent(100) + display.Image(c, f.Image, display.ImageOptions{Alt: f.Name}).Element.MaxWidthPercent(100).MaxHeightPercent(100) }) case lang != "" || FileKindOf(f.Name) == FileCode: - if code.lines == nil || code.text != f.Text { - code.text, code.lines = f.Text, strings.Split(f.Text, "\n") + if codeState.lines == nil || codeState.text != f.Text { + codeState.text, codeState.lines = f.Text, strings.Split(f.Text, "\n") } if lang == "" { lang = fileExt(f.Name) } - CodeViewer(c, code.lines, &code.viewer, CodeViewerOptions{Language: lang, Label: f.Name}).Element.Grow(1) + code.CodeViewer(c, codeState.lines, &codeState.viewer, code.CodeViewerOptions{Language: lang, Label: f.Name}).Element.Grow(1) case f.Text != "" || FileKindOf(f.Name) == FileText: - ScrollArea(c, nil, ScrollAreaOptions{Label: f.Name}, func() { - ui.Text(c, f.Text).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).Selectable() + layout.ScrollArea(c, nil, layout.ScrollAreaOptions{Label: f.Name}, func() { + ui.Text(c, f.Text).Font(theme.MonoFont).FontSize(core.FontSize(c, code.CodeSize)).Selectable() }).Grow(1) default: ui.Column(c).Grow(1).Center().Gap(u * 2).Children(func() { FileIcon(c, f.Name, FileIconOptions{Size: 48}) - ui.Text(c, fmt.Sprintf(msg(c, "file_preview.unsupported"), strings.ToUpper(fileExt(f.Name)))).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "file_preview.unsupported"), strings.ToUpper(fileExt(f.Name)))).TextColor(k.TextMuted).Role(ui.RoleStatus) }) } }) diff --git a/file_preview_test.go b/files/file_preview_test.go similarity index 91% rename from file_preview_test.go rename to files/file_preview_test.go index 347ff9d..34373e4 100644 --- a/file_preview_test.go +++ b/files/file_preview_test.go @@ -1,10 +1,13 @@ -package mujica +package files import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) type previewProbe struct { @@ -14,7 +17,7 @@ type previewProbe struct { } func (p *previewProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FilePreview(c, p.f, p.opts) r.Element.Size(480, 300) for _, e := range []struct { @@ -62,7 +65,7 @@ func TestFilePreviewActions(t *testing.T) { } func TestFilePreviewStatusAndNoName(t *testing.T) { - p := &previewProbe{f: PreviewFile{Name: "big.log"}, opts: FilePreviewOptions{Status: DataFailed, Error: "read denied"}} + p := &previewProbe{f: PreviewFile{Name: "big.log"}, opts: FilePreviewOptions{Status: data.DataFailed, Error: "read denied"}} tt := ui.NewTester(p.view, 500, 320) if !tt.HasText("read denied") { t.Fatalf("texts %q", tt.Texts()) @@ -100,14 +103,14 @@ func TestFilePreviewLargeCodeCache(t *testing.T) { file := PreviewFile{Name: "large.go", Text: strings.Repeat("var seal = 1\n", 100000)} var state *previewCodeState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FilePreview(c, file, FilePreviewOptions{}) r.Element.Size(480, 300) state = ui.Local(r.Element, "file_preview.code", func() previewCodeState { return previewCodeState{} }) }, 500, 320) - before := state.viewer.data + before := state.viewer.Data tt.Frame() - if before == nil || state.viewer.data != before || len(state.lines) != 100001 { + if before == nil || state.viewer.Data != before || len(state.lines) != 100001 { t.Fatal("unchanged preview rebuilt code cache") } n := 0 @@ -121,7 +124,7 @@ func TestFilePreviewLargeCodeCache(t *testing.T) { } file.Text = "package court" tt.Frame() - if len(state.lines) != 1 || state.lines[0] != "package court" || state.viewer.data == before { + if len(state.lines) != 1 || state.lines[0] != "package court" || state.viewer.Data == before { t.Fatal("changed preview retained code cache") } } diff --git a/files/helpers_test.go b/files/helpers_test.go new file mode 100644 index 0000000..857828e --- /dev/null +++ b/files/helpers_test.go @@ -0,0 +1,24 @@ +package files + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/live_indicator.go b/files/live_indicator.go similarity index 63% rename from live_indicator.go rename to files/live_indicator.go index 03607f8..8c2e6a8 100644 --- a/live_indicator.go +++ b/files/live_indicator.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LiveStatus is the state of real-time sync. @@ -33,21 +35,21 @@ type LiveIndicatorOptions struct { } // LiveIndicator displays sync status, announces changes and offers retries. -func LiveIndicator(c *ui.Context, status LiveStatus, opts LiveIndicatorOptions) ChoiceResult { +func LiveIndicator(c *ui.Context, status LiveStatus, opts LiveIndicatorOptions) core.ChoiceResult { if status < LiveSynced || status > LiveError { panic("mujica: LiveStatus out of range") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() now := opts.Now if now.IsZero() { now = c.Now() } - text := msg(c, [...]string{"live_indicator.synced", "live_indicator.syncing", "live_indicator.reconnecting", "live_indicator.offline", "live_indicator.error"}[status]) + text := core.Msg(c, [...]string{"live_indicator.synced", "live_indicator.syncing", "live_indicator.reconnecting", "live_indicator.offline", "live_indicator.error"}[status]) if status == LiveError && opts.Error != "" { text += ": " + opts.Error } - var res ChoiceResult + var res core.ChoiceResult res.Element = ui.Row(c).Gap(u * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize) last := ui.Local(res.Element, "live_indicator.last", func() LiveStatus { return status }) if *last != status { @@ -62,19 +64,19 @@ func LiveIndicator(c *ui.Context, status LiveStatus, opts LiveIndicatorOptions) box.Children(func() { switch status { case LiveSyncing, LiveReconnecting: - Spinner(c, SpinnerOptions{Size: 14, Label: text}) + core.Spinner(c, core.SpinnerOptions{Size: 14, Label: text}) default: name := [...]string{"circle-check", "", "", "wifi-off", "circle-alert"}[status] ui.Icon(c, icons.Must(name)).Size(16, 16).Shrink(0).TextColor(col) } - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() if !opts.LastSynced.IsZero() && status != LiveSyncing { - ui.Text(c, liveAgo(c, now.Sub(opts.LastSynced))).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, liveAgo(c, now.Sub(opts.LastSynced))).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if status == LiveOffline || status == LiveError { - if Button(c, msg(c, "live_indicator.retry"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() && !res.Element.IsDisabled() { - res.changed = true + if input.Button(c, core.Msg(c, "live_indicator.retry"), input.ButtonOptions{Variant: input.Ghost}).MinHeight(theme.MinHitSize).Clicked() && !res.Element.IsDisabled() { + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: live_indicator retry") } } @@ -86,11 +88,11 @@ func LiveIndicator(c *ui.Context, status LiveStatus, opts LiveIndicatorOptions) func liveAgo(c *ui.Context, d time.Duration) string { switch { case d < time.Minute: - return msg(c, "live_indicator.justNow") + return core.Msg(c, "live_indicator.justNow") case d < time.Hour: - return fmt.Sprintf(msg(c, "live_indicator.minutes"), int(d.Minutes())) + return fmt.Sprintf(core.Msg(c, "live_indicator.minutes"), int(d.Minutes())) case d < 24*time.Hour: - return fmt.Sprintf(msg(c, "live_indicator.hours"), int(d.Hours())) + return fmt.Sprintf(core.Msg(c, "live_indicator.hours"), int(d.Hours())) } - return fmt.Sprintf(msg(c, "live_indicator.days"), int(d.Hours()/24)) + return fmt.Sprintf(core.Msg(c, "live_indicator.days"), int(d.Hours()/24)) } diff --git a/live_indicator_test.go b/files/live_indicator_test.go similarity index 94% rename from live_indicator_test.go rename to files/live_indicator_test.go index bffb04e..f853671 100644 --- a/live_indicator_test.go +++ b/files/live_indicator_test.go @@ -1,10 +1,12 @@ -package mujica +package files import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var liveNow = time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) @@ -16,7 +18,7 @@ type liveProbe struct { } func (p *liveProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(12).Children(func() { if LiveIndicator(c, p.status, LiveIndicatorOptions{LastSynced: liveNow.Add(-3 * time.Minute), Now: liveNow, Error: p.err}).Changed() { p.retried++ @@ -62,7 +64,7 @@ func TestLiveIndicatorStates(t *testing.T) { func TestLiveAgo(t *testing.T) { var got []string ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) got = []string{liveAgo(c, 10*time.Second), liveAgo(c, 5*time.Hour), liveAgo(c, 50*time.Hour)} }, 10, 10) if got[0] != "just now" || got[1] != "5 h ago" || got[2] != "2 days ago" { diff --git a/path_bar.go b/files/path_bar.go similarity index 82% rename from path_bar.go rename to files/path_bar.go index 8b5f6f7..0619edf 100644 --- a/path_bar.go +++ b/files/path_bar.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "log/slog" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PathBarOptions configure a PathBar. @@ -65,10 +67,10 @@ func PathBar(c *ui.Context, p string, opts PathBarOptions) PathBarResult { if len(segs) == 0 { panic("mujica: PathBar needs a path") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var res PathBarResult - e := ui.Row(c).Grow(1).MinWidth(0).Gap(u).AlignItems(ui.Center).Role(ui.RoleToolbar).Label(msg(c, "path_bar.label")). + e := ui.Row(c).Grow(1).MinWidth(0).Gap(u).AlignItems(ui.Center).Role(ui.RoleToolbar).Label(core.Msg(c, "path_bar.label")). FocusGroup(ui.Horizontal).Disabled(opts.Disabled) res.Element = e was := e.Bounds() @@ -90,11 +92,11 @@ func PathBar(c *ui.Context, p string, opts PathBarOptions) PathBarResult { } if i >= 1 && i <= hidden { if i == 1 { - more := pathBarCrumb(c, "…", false).Label(msg(c, "path_bar.more")) + more := pathBarCrumb(c, "…", false).Label(core.Msg(c, "path_bar.more")) if more.Clicked() && !disabled { *open = !*open } - Popover(c, more, open, PopoverOptions{Label: msg(c, "path_bar.more")}, func() { + overlay.Popover(c, more, open, overlay.PopoverOptions{Label: core.Msg(c, "path_bar.more")}, func() { for _, h := range segs[1 : hidden+1] { if pathBarCrumb(c, h.label, false).Key(h.path).Clicked() { goTo(h) @@ -117,9 +119,9 @@ func pathBarFold(c *ui.Context, segs []pathSegment, room float32) int { if room <= 0 { return 0 } - u := Density(c).Unit() + u := core.Density(c).Unit() width := func(s string) float32 { - w, _ := c.MeasureText(0, ui.Span{Text: s, Size: FontSize(c, theme.BodySize)}) + w, _ := c.MeasureText(0, ui.Span{Text: s, Size: core.FontSize(c, theme.BodySize)}) return max(w+4*u, theme.MinHitSize) + 12 + 2*u } total := float32(0) @@ -139,8 +141,8 @@ func pathBarFold(c *ui.Context, segs []pathSegment, room float32) int { // pathBarCrumb builds a button, emphasizing the current segment. func pathBarCrumb(c *ui.Context, label string, current bool) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*2). Radius(theme.ControlRadius).Shrink(0).TextColor(k.TextMuted) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -151,7 +153,7 @@ func pathBarCrumb(c *ui.Context, label string, current bool) *ui.Element { b.Background(k.SurfaceHover).TextColor(k.Text) } if current { - b.TextColor(k.Text).FontWeight(600).Description(msg(c, "path_bar.current")) + b.TextColor(k.Text).FontWeight(600).Description(core.Msg(c, "path_bar.current")) } return b.Children(func() { ui.Text(c, label).SingleLine() }) } diff --git a/path_bar_test.go b/files/path_bar_test.go similarity index 96% rename from path_bar_test.go rename to files/path_bar_test.go index 3b5dc37..14e0526 100644 --- a/path_bar_test.go +++ b/files/path_bar_test.go @@ -1,14 +1,16 @@ -package mujica +package files import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func pathBarTester(p *string, opts PathBarOptions, w int, went *[]string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Width(float32(w)).Children(func() { if r := PathBar(c, *p, opts); r.Navigated != "" { *went = append(*went, r.Navigated) diff --git a/permission_select.go b/files/permission_select.go similarity index 64% rename from permission_select.go rename to files/permission_select.go index 023b872..cf57a61 100644 --- a/permission_select.go +++ b/files/permission_select.go @@ -1,9 +1,12 @@ -package mujica +package files import ( "log/slog" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // Permission defines access to a shared item. @@ -27,24 +30,24 @@ func permissionName(c *ui.Context, p Permission) string { if p < PermissionView || p > PermissionEdit { panic("mujica: Permission out of range") } - return msg(c, [...]string{"permission_select.view", "permission_select.comment", "permission_select.edit"}[p]) + return core.Msg(c, [...]string{"permission_select.view", "permission_select.comment", "permission_select.edit"}[p]) } // PermissionSelect chooses can view, can comment or can edit. -func PermissionSelect(c *ui.Context, value *Permission, opts PermissionSelectOptions) ChoiceResult { +func PermissionSelect(c *ui.Context, value *Permission, opts PermissionSelectOptions) core.ChoiceResult { if value == nil { panic("mujica: PermissionSelect needs a value") } permissionName(c, *value) - options := make([]SelectOption[Permission], 3) + options := make([]input.SelectOption[Permission], 3) for p := PermissionView; p <= PermissionEdit; p++ { - options[p] = SelectOption[Permission]{Value: p, Label: permissionName(c, p)} + options[p] = input.SelectOption[Permission]{Value: p, Label: permissionName(c, p)} } label := opts.Label if label == "" { - label = msg(c, "permission_select.label") + label = core.Msg(c, "permission_select.label") } - r := Select(c, value, options, SelectOptions{Label: label, Disabled: opts.Disabled}) + r := input.Select(c, value, options, input.SelectOptions{Label: label, Disabled: opts.Disabled}) if r.Changed() { slog.Debug("mujica: permission_select change", "permission", *value) } diff --git a/permission_select_test.go b/files/permission_select_test.go similarity index 95% rename from permission_select_test.go rename to files/permission_select_test.go index 0e1c147..e7442ff 100644 --- a/permission_select_test.go +++ b/files/permission_select_test.go @@ -1,14 +1,16 @@ -package mujica +package files import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func permissionSelectTester(v *Permission, disabled bool, changed *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(12).Children(func() { if PermissionSelect(c, v, PermissionSelectOptions{Label: "Ada", Disabled: disabled}).Changed() { *changed++ @@ -73,7 +75,7 @@ func TestPermissionSelectSnapshot(t *testing.T) { func TestPermissionSelectDisabledClosesAndStaysClosed(t *testing.T) { v, disabled := PermissionView, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { PermissionSelect(c, &v, PermissionSelectOptions{Label: "Permission"}) }) }, 400, 300) if err := tt.Click("Can view"); err != nil { diff --git a/presence_avatars.go b/files/presence_avatars.go similarity index 80% rename from presence_avatars.go rename to files/presence_avatars.go index 9816c85..226a783 100644 --- a/presence_avatars.go +++ b/files/presence_avatars.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Collaborator is someone working on the same document. @@ -49,9 +52,9 @@ func PresenceAvatars(c *ui.Context, people []Collaborator, opts PresenceAvatarsO if size == 0 { size = 28 } - u := Density(c).Unit() + u := core.Density(c).Unit() var res PresenceAvatarsResult - res.Element = ui.Row(c).AlignItems(ui.Center).Gap(u).Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "presence_avatars.label"), len(people))). + res.Element = ui.Row(c).AlignItems(ui.Center).Gap(u).Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "presence_avatars.label"), len(people))). FocusGroup(ui.Horizontal).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() open := ui.Local(res.Element, "presence_avatars.open", func() bool { return false }) @@ -79,15 +82,15 @@ func PresenceAvatars(c *ui.Context, people []Collaborator, opts PresenceAvatarsO } } if extra := len(people) - limit; extra > 0 { - more := ui.ButtonBase(c).Size(max(size+4, theme.MinHitSize), max(size+4, theme.MinHitSize)).Radius((size + 4) / 2).Label(fmt.Sprintf(msg(c, "presence_avatars.more"), extra)) + more := ui.ButtonBase(c).Size(max(size+4, theme.MinHitSize), max(size+4, theme.MinHitSize)).Radius((size + 4) / 2).Label(fmt.Sprintf(core.Msg(c, "presence_avatars.more"), extra)) presenceStyle(c, more, size, false) more.Children(func() { - ui.Text(c, fmt.Sprintf("+%d", extra)).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600) + ui.Text(c, fmt.Sprintf("+%d", extra)).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600) }) if more.Clicked() && !disabled { *open = !*open } - Popover(c, more, open, PopoverOptions{Label: msg(c, "presence_avatars.others")}, func() { + overlay.Popover(c, more, open, overlay.PopoverOptions{Label: core.Msg(c, "presence_avatars.others")}, func() { rows.Key = func(i int) any { return people[limit+i].ID } ui.List(c, rows, extra, func(i int) { p := people[limit+i] @@ -97,7 +100,7 @@ func PresenceAvatars(c *ui.Context, people []Collaborator, opts PresenceAvatarsO } ui.Text(c, p.Name).SingleLine() }) - }).Width(260).Height(min(240, float32(extra)*Density(c).ControlHeight())) + }).Width(260).Height(min(240, float32(extra)*core.Density(c).ControlHeight())) }) } }) @@ -109,18 +112,18 @@ func presenceAvatar(c *ui.Context, p Collaborator, size float32, following bool) if p.ID == "" { panic("mujica: Collaborator needs an ID") } - k := Tokens(c) + k := core.Tokens(c) key := "presence_avatars.follow" if following { key = "presence_avatars.unfollow" } - b := ui.ButtonBase(c).Key(p.ID).Size(max(size+4, theme.MinHitSize), max(size+4, theme.MinHitSize)).Radius((size + 4) / 2).Label(fmt.Sprintf(msg(c, key), p.Name)).Tooltip(p.Name) + b := ui.ButtonBase(c).Key(p.ID).Size(max(size+4, theme.MinHitSize), max(size+4, theme.MinHitSize)).Radius((size + 4) / 2).Label(fmt.Sprintf(core.Msg(c, key), p.Name)).Tooltip(p.Name) if p.Away { - b.Description(msg(c, "presence_avatars.away")) + b.Description(core.Msg(c, "presence_avatars.away")) } presenceStyle(c, b, size, following) return b.Children(func() { - Avatar(c, p.Name, AvatarOptions{Image: p.Image, Size: size}) + display.Avatar(c, p.Name, display.AvatarOptions{Image: p.Image, Size: size}) dot := ui.Box(c).Size(9, 9).Radius(4.5).Absolute().Right(0).Bottom(0).Border(1.5, k.Surface).Role(ui.RoleNone) if p.Away { dot.Background(k.Surface).Border(1.5, k.TextMuted) @@ -132,7 +135,7 @@ func presenceAvatar(c *ui.Context, p Collaborator, size float32, following bool) // presenceStyle styles hover, focus and following rings. func presenceStyle(c *ui.Context, b *ui.Element, size float32, following bool) { - k := Tokens(c) + k := core.Tokens(c) b.Shrink(0).Background(k.Surface).FocusRing(false) if b.Hovered() { b.Background(k.SurfaceHover) diff --git a/presence_avatars_test.go b/files/presence_avatars_test.go similarity index 92% rename from presence_avatars_test.go rename to files/presence_avatars_test.go index 1099044..81330bb 100644 --- a/presence_avatars_test.go +++ b/files/presence_avatars_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func presenceSample() []Collaborator { @@ -22,7 +24,7 @@ type presenceProbe struct { } func (p *presenceProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(12).Children(func() { r := PresenceAvatars(c, p.people, PresenceAvatarsOptions{Max: p.max, Following: p.following, Disabled: p.disabled}) if r.Follow != "" { @@ -94,7 +96,7 @@ func TestPresenceAvatarsSnapshot(t *testing.T) { func TestPresenceAvatarsSmallHitArea(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PresenceAvatars(c, presenceSample(), PresenceAvatarsOptions{Size: 12, Max: 1}) }, 300, 200) for _, name := range []string{"Follow Ada Lovelace", "4 more people"} { @@ -110,7 +112,10 @@ func TestPresenceAvatarsLargeOverflow(t *testing.T) { for i := range people { people[i] = Collaborator{ID: fmt.Sprint(i), Name: fmt.Sprintf("Member %d", i)} } - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); PresenceAvatars(c, people, PresenceAvatarsOptions{Max: 1}) }, 400, 300) + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + PresenceAvatars(c, people, PresenceAvatarsOptions{Max: 1}) + }, 400, 300) if err := tt.Click("99999 more people"); err != nil { t.Fatal(err) } diff --git a/reactions.go b/files/reactions.go similarity index 74% rename from reactions.go rename to files/reactions.go index 37fd785..6b79076 100644 --- a/reactions.go +++ b/files/reactions.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Reaction is an emoji and who reacted with it. @@ -41,9 +43,9 @@ func Reactions(c *ui.Context, reactions []Reaction, opts ReactionsOptions) React if len(opts.Choices) == 0 { panic("mujica: Reactions needs Choices") } - u := Density(c).Unit() + u := core.Density(c).Unit() var res ReactionsResult - res.Element = ui.Row(c).Wrap().Gap(u * 2).AlignItems(ui.Center).Role(ui.RoleGroup).Label(msg(c, "reactions.label")).Disabled(opts.Disabled) + res.Element = ui.Row(c).Wrap().Gap(u * 2).AlignItems(ui.Center).Role(ui.RoleGroup).Label(core.Msg(c, "reactions.label")).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() open := ui.Local(res.Element, "reactions.open", func() bool { return false }) *open = *open && !disabled @@ -56,19 +58,19 @@ func Reactions(c *ui.Context, reactions []Reaction, opts ReactionsOptions) React res.Toggled = r.Emoji } } - add := iconAction(c, icons.Must("plus"), msg(c, "reactions.add")) + add := core.IconAction(c, icons.Must("plus"), core.Msg(c, "reactions.add")) if add.Clicked() && !disabled { *open = !*open } - Popover(c, add, open, PopoverOptions{Label: msg(c, "reactions.pick")}, func() { + overlay.Popover(c, add, open, overlay.PopoverOptions{Label: core.Msg(c, "reactions.pick")}, func() { ui.Grid(c).Columns(min(len(opts.Choices), 6)).Gap(u).FocusGroup(ui.Horizontal).Children(func() { for _, e := range opts.Choices { b := ui.ButtonBase(c).Key(e).Size(36, 36).Radius(theme.ControlRadius).Label(e) - internal.Ring(b, Tokens(c).Focus, theme.ControlRadius) + internal.Ring(b, core.Tokens(c).Focus, theme.ControlRadius) if b.Hovered() { - b.Background(Tokens(c).SurfaceHover) + b.Background(core.Tokens(c).SurfaceHover) } - b.Children(func() { ui.Text(c, e).FontSize(FontSize(c, 20)) }) + b.Children(func() { ui.Text(c, e).FontSize(core.FontSize(c, 20)) }) if b.Clicked() && !disabled { res.Added, *open = e, false } @@ -84,14 +86,14 @@ func Reactions(c *ui.Context, reactions []Reaction, opts ReactionsOptions) React // reactionChip frames and marks the current user's reaction. func reactionChip(c *ui.Context, r Reaction) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() - label := fmt.Sprintf(msg(c, "reactions.count"), r.Emoji, r.Count) + k := core.Tokens(c) + u := core.Density(c).Unit() + label := fmt.Sprintf(core.Msg(c, "reactions.count"), r.Emoji, r.Count) if r.Mine { - label += ", " + msg(c, "reactions.mine") + label += ", " + core.Msg(c, "reactions.mine") } b := ui.ButtonBase(c).Key(r.Emoji).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Padding(0, u*2).Gap(u). - Radius(theme.MinHitSize / 2).Role(ui.RoleToggleButton).Selected(r.Mine).Label(label).Transition(colorFade) + Radius(theme.MinHitSize / 2).Role(ui.RoleToggleButton).Selected(r.Mine).Label(label).Transition(core.ColorFade) if len(r.People) > 0 { b.Tooltip(strings.Join(r.People, ", ")) } diff --git a/reactions_test.go b/files/reactions_test.go similarity index 96% rename from reactions_test.go rename to files/reactions_test.go index d83a652..fade9e1 100644 --- a/reactions_test.go +++ b/files/reactions_test.go @@ -1,9 +1,11 @@ -package mujica +package files import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type reactionsProbe struct { @@ -16,7 +18,7 @@ type reactionsProbe struct { var reactionChoices = []string{"👍", "❤️", "🎉", "👀", "🙏", "😂", "🔥"} func (p *reactionsProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(12).Children(func() { r := Reactions(c, p.items, ReactionsOptions{Choices: reactionChoices, Disabled: p.disabled}) if r.Toggled != "" { diff --git a/recent_files.go b/files/recent_files.go similarity index 77% rename from recent_files.go rename to files/recent_files.go index 1c67560..9290bef 100644 --- a/recent_files.go +++ b/files/recent_files.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "cmp" @@ -10,8 +10,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RecentFile is a file the user opened. @@ -70,25 +73,25 @@ func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts Re s.revision = opts.Revision } recentGroup(files, s, now) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var res RecentFilesResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = code.CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() if disabled { copy := *s s = © } res.Element.Children(func() { - codeHeader(c, func() { - ui.Text(c, msg(c, "recent_files.title")).Font(theme.SerifFont).FontWeight(600).Grow(1) - if len(files) > 0 && Button(c, msg(c, "recent_files.clear"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() && !disabled { + code.CodeHeader(c, func() { + ui.Text(c, core.Msg(c, "recent_files.title")).Font(theme.SerifFont).FontWeight(600).Grow(1) + if len(files) > 0 && input.Button(c, core.Msg(c, "recent_files.clear"), input.ButtonOptions{Variant: input.Ghost}).MinHeight(theme.MinHitSize).Clicked() && !disabled { res.Cleared = true slog.Debug("mujica: recent_files clear") } }) if len(files) == 0 { - codeCaption(c, msg(c, "recent_files.empty")).Padding(d.Unit() * 4) + code.CodeCaption(c, core.Msg(c, "recent_files.empty")).Padding(d.Unit() * 4) return } s.rows.Selected = &s.selected @@ -106,10 +109,10 @@ func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts Re } return recentGroupName(c, s.flat[i].group) } - list := codeRows(c, &s.rows, len(s.flat), func(i int) { + list := code.CodeRows(c, &s.rows, len(s.flat), func(i int) { r := s.flat[i] if r.file < 0 { - ui.Text(c, recentGroupName(c, r.group)).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, recentGroupName(c, r.group)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.Ornament.Mix(k.Text, 0.2)).Grow(1) return } @@ -117,14 +120,14 @@ func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts Re FileIcon(c, f.Path, FileIconOptions{}) ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, path.Base(f.Path)).SingleLine() - codeCaption(c, path.Dir(f.Path)) + code.CodeCaption(c, path.Dir(f.Path)) }) when := f.Opened.Format("15:04") if r.group >= 2 { when = f.Opened.Format("Jan 2") } - codeCaption(c, when).Shrink(0) - if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "recent_files.remove"), path.Base(f.Path))).Clicked() && !disabled { + code.CodeCaption(c, when).Shrink(0) + if core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "recent_files.remove"), path.Base(f.Path))).Clicked() && !disabled { res.Removed = f.Path slog.Debug("mujica: recent_files remove") } @@ -140,7 +143,7 @@ func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts Re } func recentGroupName(c *ui.Context, g int) string { - return msg(c, [...]string{"recent_files.today", "recent_files.yesterday", "recent_files.week", "recent_files.earlier"}[g]) + return core.Msg(c, [...]string{"recent_files.today", "recent_files.yesterday", "recent_files.week", "recent_files.earlier"}[g]) } // recentGroup caches files sorted into relative-day groups. diff --git a/recent_files_test.go b/files/recent_files_test.go similarity index 97% rename from recent_files_test.go rename to files/recent_files_test.go index da63658..00f26d8 100644 --- a/recent_files_test.go +++ b/files/recent_files_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var recentNow = time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC) @@ -29,7 +31,7 @@ type recentProbe struct { } func (p *recentProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := RecentFiles(c, p.files, &p.s, RecentFilesOptions{Now: recentNow, Disabled: p.disabled}) r.Element.Height(420) if r.Opened != nil { @@ -123,7 +125,7 @@ func TestRecentFilesLargeAndRevision(t *testing.T) { s := RecentFilesState{} revision := uint64(0) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RecentFiles(c, files, &s, RecentFilesOptions{Now: recentNow, Revision: revision}).Element.Height(220) }, 500, 250) n := 0 diff --git a/remote_cursor.go b/files/remote_cursor.go similarity index 86% rename from remote_cursor.go rename to files/remote_cursor.go index 1e1f5a7..109d2de 100644 --- a/remote_cursor.go +++ b/files/remote_cursor.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RemoteCaret describes a collaborator's caret and selection offsets. @@ -32,7 +34,7 @@ func RemoteCursor(c *ui.Context, carets []RemoteCaret, opts RemoteCursorOptions) if opts.Caret == nil { panic("mujica: RemoteCursor needs Caret") } - k := Tokens(c) + k := core.Tokens(c) names := make([]string, len(carets)) for i, rc := range carets { if rc.ID == "" || rc.Offset < 0 || rc.Anchor < 0 { @@ -40,10 +42,10 @@ func RemoteCursor(c *ui.Context, carets []RemoteCaret, opts RemoteCursorOptions) } names[i] = rc.Name } - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) e := ui.Box(c).Absolute().Top(0).Left(0).Right(0).Bottom(0).PassThrough().Clip().Role(ui.RoleNone) if len(carets) > 0 { - e.Role(ui.RoleStatus).Label(fmt.Sprintf(msg(c, "remote_cursor.label"), strings.Join(names, ", "))) + e.Role(ui.RoleStatus).Label(fmt.Sprintf(core.Msg(c, "remote_cursor.label"), strings.Join(names, ", "))) } return e.Draw(func(p *ui.Painter, r ui.Rect) { for _, rc := range carets { @@ -80,6 +82,6 @@ func remoteRect(p *ui.Painter, r ui.Rect) ui.Rect { func remoteColor(k theme.Tokens, id string) ui.Color { h := fnv.New32a() h.Write([]byte(id)) - pal := flamegraphPalette(k) + pal := code.FlamegraphPalette(k) return pal[h.Sum32()%uint32(len(pal))] } diff --git a/remote_cursor_test.go b/files/remote_cursor_test.go similarity index 86% rename from remote_cursor_test.go rename to files/remote_cursor_test.go index 72ad2d1..036bda4 100644 --- a/remote_cursor_test.go +++ b/files/remote_cursor_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "image/color" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // remoteMono maps offsets onto 40-column, 8×20 DIP cells. @@ -27,9 +29,9 @@ func remoteMono() RemoteCursorOptions { func remoteTester(carets []RemoteCaret, button *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Box(c).Size(320, 120).Children(func() { - if Button(c, "Underneath", ButtonOptions{}).Size(320, 120).Clicked() { + if input.Button(c, "Underneath", input.ButtonOptions{}).Size(320, 120).Clicked() { *button++ } RemoteCursor(c, carets, remoteMono()) @@ -74,11 +76,11 @@ func TestRemoteCursorInvalid(t *testing.T) { func TestRemoteCursorSnapshot(t *testing.T) { snapshot(t, "remote_cursor", 360, 120, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) ui.Box(c).Margin(12).Size(320, 80).Background(k.Surface).Children(func() { ui.Column(c).Children(func() { for _, l := range []string{"The seal of the court is kept", "by the chancellor, who answers"} { - ui.Text(c, l).Font(theme.MonoFont).FontSize(FontSize(c, 13)).Height(20) + ui.Text(c, l).Font(theme.MonoFont).FontSize(core.FontSize(c, 13)).Height(20) } }) RemoteCursor(c, []RemoteCaret{{ID: "ada", Name: "Ada", Offset: 48, Anchor: 43}, {ID: "blaise", Name: "Blaise", Offset: 8, Anchor: 8}}, remoteMono()) diff --git a/rename_inline.go b/files/rename_inline.go similarity index 92% rename from rename_inline.go rename to files/rename_inline.go index 3633525..f5d39dc 100644 --- a/rename_inline.go +++ b/files/rename_inline.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "log/slog" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RenameInlineOptions configure a RenameInline. @@ -103,7 +105,7 @@ func RenameInline(c *ui.Context, name *string, editing *bool, opts RenameInlineO slog.Debug("mujica: rename_inline end", "renamed", res.Renamed) } ui.Box(c).Key(st.gen).Grow(1).MinWidth(0).Children(func() { - in := TextInput(c, &st.draft, TextInputOptions{Label: msg(c, "rename_inline.label"), Error: st.err}) + in := input.TextInput(c, &st.draft, input.TextInputOptions{Label: core.Msg(c, "rename_inline.label"), Error: st.err}) in.Element.FillWidth() in.Input.AutoFocus() if in.Changed() { @@ -129,7 +131,7 @@ func RenameInline(c *ui.Context, name *string, editing *bool, opts RenameInlineO func renameCheck(c *ui.Context, name string, validate func(string) string) string { if strings.TrimSpace(name) == "" { - return msg(c, "rename_inline.empty") + return core.Msg(c, "rename_inline.empty") } if validate != nil { return validate(name) @@ -139,10 +141,10 @@ func renameCheck(c *ui.Context, name string, validate func(string) string) strin // renameSelection marks the stem for replacement on initial input. func renameSelection(c *ui.Context, in *ui.Element, st *renameState) { - k := Tokens(c) + k := core.Tokens(c) stem := renameStem(st.draft, st.folder) if st.stemSel && st.tail == "" { - w, _ := c.MeasureText(0, ui.Span{Text: stem, Size: FontSize(c, theme.BodySize)}) + w, _ := c.MeasureText(0, ui.Span{Text: stem, Size: core.FontSize(c, theme.BodySize)}) in.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + 2, W: w, H: r.H - 4}, k.Selection, 2) }) diff --git a/rename_inline_test.go b/files/rename_inline_test.go similarity index 95% rename from rename_inline_test.go rename to files/rename_inline_test.go index f530ee1..412f944 100644 --- a/rename_inline_test.go +++ b/files/rename_inline_test.go @@ -1,10 +1,13 @@ -package mujica +package files import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type renameProbe struct { @@ -15,7 +18,7 @@ type renameProbe struct { } func (p *renameProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(12).Gap(8).Children(func() { r := RenameInline(c, &p.name, &p.editing, RenameInlineOptions{Folder: p.folder, Disabled: p.disabled, Validate: func(n string) string { @@ -30,7 +33,7 @@ func (p *renameProbe) view(c *ui.Context) { if r.Canceled { p.canceled++ } - Button(c, "Elsewhere", ButtonOptions{}) + input.Button(c, "Elsewhere", input.ButtonOptions{}) }) } diff --git a/share_dialog.go b/files/share_dialog.go similarity index 66% rename from share_dialog.go rename to files/share_dialog.go index f8babc0..cd1966a 100644 --- a/share_dialog.go +++ b/files/share_dialog.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,8 +7,13 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ShareMember is someone the item is shared with. @@ -63,28 +68,28 @@ func ShareDialog(c *ui.Context, open *bool, members *[]ShareMember, opts ShareDi *open = false } draft := ui.Local(site, "share_dialog.draft", func() shareDraft { return shareDraft{} }) - u := Density(c).Unit() - res.Element = Dialog(c, open, DialogOptions{Title: fmt.Sprintf(msg(c, "share_dialog.title"), opts.Title), Width: 480}, func() { + u := core.Density(c).Unit() + res.Element = overlay.Dialog(c, open, overlay.DialogOptions{Title: fmt.Sprintf(core.Msg(c, "share_dialog.title"), opts.Title), Width: 480}, func() { ui.Column(c).Gap(u * 3).Disabled(opts.Disabled).Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() { - in := TextInput(c, &draft.email, TextInputOptions{Placeholder: msg(c, "share_dialog.placeholder"), Label: msg(c, "share_dialog.invite")}) + in := input.TextInput(c, &draft.email, input.TextInputOptions{Placeholder: core.Msg(c, "share_dialog.placeholder"), Label: core.Msg(c, "share_dialog.invite")}) in.Element.Grow(1).MinWidth(0) - PermissionSelect(c, &draft.perm, PermissionSelectOptions{Label: msg(c, "share_dialog.invitePermission")}) - send := Button(c, msg(c, "share_dialog.send"), ButtonOptions{Disabled: strings.TrimSpace(draft.email) == ""}) + PermissionSelect(c, &draft.perm, PermissionSelectOptions{Label: core.Msg(c, "share_dialog.invitePermission")}) + send := input.Button(c, core.Msg(c, "share_dialog.send"), input.ButtonOptions{Disabled: strings.TrimSpace(draft.email) == ""}) if (send.Clicked() || in.Submitted()) && strings.TrimSpace(draft.email) != "" && !send.IsDisabled() { res.Invited = &ShareInvite{Email: strings.TrimSpace(draft.email), Permission: draft.perm} draft.email = "" slog.Debug("mujica: share_dialog invite") } }) - ui.Text(c, msg(c, "share_dialog.people")).Font(theme.SerifFont).FontWeight(600) + ui.Text(c, core.Msg(c, "share_dialog.people")).Font(theme.SerifFont).FontWeight(600) shareMembers(c, members, &res, &draft.rows) if opts.Link != "" { - Divider(c, DividerOptions{}) + layout.Divider(c, layout.DividerOptions{}) ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Icon(c, icons.Must("external-link")).Size(16, 16).Shrink(0).TextColor(Tokens(c).TextMuted) + ui.Icon(c, icons.Must("external-link")).Size(16, 16).Shrink(0).TextColor(core.Tokens(c).TextMuted) ui.Text(c, opts.Link).SingleLine().Grow(1).MinWidth(0).Selectable() - b := Button(c, msg(c, "share_dialog.copy"), ButtonOptions{Variant: Secondary, Icon: icons.Must("copy")}) + b := input.Button(c, core.Msg(c, "share_dialog.copy"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("copy")}) if b.Clicked() && !b.IsDisabled() { if opts.OnCopy != nil { opts.OnCopy(opts.Link) @@ -92,7 +97,7 @@ func ShareDialog(c *ui.Context, open *bool, members *[]ShareMember, opts ShareDi c.WriteClipboard(opts.Link) } res.Copied = true - c.Announce(msg(c, "share_dialog.copied")) + c.Announce(core.Msg(c, "share_dialog.copied")) slog.Debug("mujica: share_dialog copy link") } }) @@ -104,8 +109,8 @@ func ShareDialog(c *ui.Context, open *bool, members *[]ShareMember, opts ShareDi // shareMembers virtualizes membership permissions and removal actions. func shareMembers(c *ui.Context, members *[]ShareMember, res *ShareDialogResult, rows *ui.ListState) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() remove := -1 rows.Key = func(i int) any { return (*members)[i].ID } _, h := c.Size() @@ -114,26 +119,26 @@ func shareMembers(c *ui.Context, members *[]ShareMember, res *ShareDialogResult, if m.ID == "" { panic("mujica: ShareMember needs an ID") } - ui.Row(c).Key(m.ID).Gap(u * 2).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Children(func() { - Avatar(c, m.Name, AvatarOptions{Size: 28}) + ui.Row(c).Key(m.ID).Gap(u * 2).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Children(func() { + display.Avatar(c, m.Name, display.AvatarOptions{Size: 28}) ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, m.Name).SingleLine() if m.Email != "" { - ui.Text(c, m.Email).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, m.Email).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if m.Owner { - ui.Text(c, msg(c, "share_dialog.owner")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "share_dialog.owner")).TextColor(k.TextMuted) return } if PermissionSelect(c, &m.Permission, PermissionSelectOptions{Label: m.Name}).Changed() { res.Changed = m.ID } - if b := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "share_dialog.remove"), m.Name)); b.Clicked() && !b.IsDisabled() { + if b := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "share_dialog.remove"), m.Name)); b.Clicked() && !b.IsDisabled() { remove = i } }) - }).Height(min(float32(len(*members))*(Density(c).ControlHeight()+u), max(28, h*0.4))).FillWidth() + }).Height(min(float32(len(*members))*(core.Density(c).ControlHeight()+u), max(28, h*0.4))).FillWidth() if remove >= 0 { res.Removed = (*members)[remove].ID *members = append((*members)[:remove:remove], (*members)[remove+1:]...) diff --git a/share_dialog_test.go b/files/share_dialog_test.go similarity index 96% rename from share_dialog_test.go rename to files/share_dialog_test.go index dc427a7..e5b5e7b 100644 --- a/share_dialog_test.go +++ b/files/share_dialog_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type shareProbe struct { @@ -26,7 +28,7 @@ func newShareProbe() *shareProbe { } func (p *shareProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ShareDialog(c, &p.open, &p.members, ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", Disabled: p.disabled}) if r.Invited != nil { p.invites = append(p.invites, *r.Invited) @@ -115,7 +117,7 @@ func TestShareDialogAncestorDisabledCloses(t *testing.T) { open, disabled := true, false members := []ShareMember{{ID: "a", Name: "Ada"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { ShareDialog(c, &open, &members, ShareDialogOptions{Title: "Decree"}) }) @@ -132,7 +134,7 @@ func TestShareDialogCopyCallback(t *testing.T) { members := []ShareMember{} var copied []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ShareDialog(c, &open, &members, ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", OnCopy: func(s string) { copied = append(copied, s) }}) }, 600, 400) if err := tt.Click("Copy link"); err != nil { @@ -149,7 +151,7 @@ func TestShareDialogLargeList(t *testing.T) { members[i] = ShareMember{ID: fmt.Sprint(i), Name: fmt.Sprintf("Member %d", i)} } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ShareDialog(c, &open, &members, ShareDialogOptions{Title: "Roster"}) }, 640, 520) n := 0 @@ -168,7 +170,7 @@ func TestShareDialogSameTitleIndependent(t *testing.T) { membersA, membersB := []ShareMember{}, []ShareMember{} var invited *ShareInvite tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { ShareDialog(c, &openA, &membersA, ShareDialogOptions{Title: "Ledger"}) r := ShareDialog(c, &openB, &membersB, ShareDialogOptions{Title: "Ledger"}) diff --git a/storage_usage.go b/files/storage_usage.go similarity index 80% rename from storage_usage.go rename to files/storage_usage.go index 9cfb8da..f07d45c 100644 --- a/storage_usage.go +++ b/files/storage_usage.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StorageSegment is one category of used space. @@ -41,8 +43,8 @@ func StorageUsage(c *ui.Context, segments []StorageSegment, opts StorageUsageOpt if opts.Capacity <= 0 { panic("mujica: StorageUsage needs a positive Capacity") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() e := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(opts.Label) s := ui.Local(e, "storage_usage.data", func() storageUsageState { return storageUsageState{} }) var first *StorageSegment @@ -61,16 +63,16 @@ func StorageUsage(c *ui.Context, segments []StorageSegment, opts StorageUsageOpt s.data, s.n, s.capacity, s.revision = first, len(segments), opts.Capacity, opts.Revision } used := s.used - pal := flamegraphPalette(k) + pal := code.FlamegraphPalette(k) pct := func(b int64) float64 { return 100 * float64(b) / float64(opts.Capacity) } - bytes := func(b int64) string { return processListBytes(uint64(b)) } + bytes := func(b int64) string { return code.ProcessListBytes(uint64(b)) } readout := func(i int) string { if i < 0 || i >= len(segments) { - return fmt.Sprintf(msg(c, "storage_usage.free"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) + return fmt.Sprintf(core.Msg(c, "storage_usage.free"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) } return fmt.Sprintf("%s: %s (%.1f%%)", segments[i].Label, bytes(segments[i].Bytes), pct(segments[i].Bytes)) } - summary := fmt.Sprintf(msg(c, "storage_usage.used"), bytes(used), bytes(opts.Capacity)) + summary := fmt.Sprintf(core.Msg(c, "storage_usage.used"), bytes(used), bytes(opts.Capacity)) focus := ui.Local(e, "storage_usage.focus", func() int { return 0 }) *focus = min(*focus, len(segments)) e.Children(func() { @@ -125,23 +127,23 @@ func StorageUsage(c *ui.Context, segments []StorageSegment, opts StorageUsageOpt } }) if focused { - ui.Text(c, readout(at)).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) + ui.Text(c, readout(at)).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) } ui.List(c, &s.rows, len(segments)+1, func(i int) { if i == len(segments) { - storageLegend(c, k.Border.Mix(k.Surface, 0.4), msg(c, "storage_usage.freeLabel"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) + storageLegend(c, k.Border.Mix(k.Surface, 0.4), core.Msg(c, "storage_usage.freeLabel"), bytes(opts.Capacity-used), pct(opts.Capacity-used)) return } seg := segments[i] storageLegend(c, pal[i%len(pal)], seg.Label, bytes(seg.Bytes), pct(seg.Bytes)) - }).Height(min(float32(len(segments)+1)*Density(c).ControlHeight(), Density(c).ControlHeight()*6)) + }).Height(min(float32(len(segments)+1)*core.Density(c).ControlHeight(), core.Density(c).ControlHeight()*6)) }) return e } func storageLegend(c *ui.Context, col ui.Color, label, size string, pct float64) { - k := Tokens(c) - ui.Row(c).MinHeight(Density(c).ControlHeight()).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { + k := core.Tokens(c) + ui.Row(c).MinHeight(core.Density(c).ControlHeight()).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Box(c).Size(10, 10).Radius(2).Shrink(0).Background(col).Role(ui.RoleNone) ui.Text(c, label).Grow(1).MinWidth(0).SingleLine() ui.Text(c, size).TextColor(k.TextMuted).Shrink(0) diff --git a/storage_usage_test.go b/files/storage_usage_test.go similarity index 96% rename from storage_usage_test.go rename to files/storage_usage_test.go index da1d876..a695499 100644 --- a/storage_usage_test.go +++ b/files/storage_usage_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func storageSample() []StorageSegment { @@ -14,7 +16,7 @@ func storageSample() []StorageSegment { func storageTester(segs []StorageSegment, capacity int64) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StorageUsage(c, segs, StorageUsageOptions{Capacity: capacity, Label: "Court disk"}).Width(420) }, 460, 300) } diff --git a/transfer_queue.go b/files/transfer_queue.go similarity index 81% rename from transfer_queue.go rename to files/transfer_queue.go index 3f3e592..e2c4b44 100644 --- a/transfer_queue.go +++ b/files/transfer_queue.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/code" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TransferStatus is where a transfer is. @@ -97,16 +99,16 @@ func TransferQueue(c *ui.Context, items []QueuedTransfer, s *TransferQueueState, } s.data, s.n, s.revision, s.started = first, len(items), opts.Revision, true } - d := Density(c) + d := core.Density(c) var res TransferQueueResult - res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element = code.CodePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() res.Element.Children(func() { - codeHeader(c, func() { - ui.Text(c, fmt.Sprintf(msg(c, "transfer_queue.summary"), s.active, s.failed)).Role(ui.RoleStatus) + code.CodeHeader(c, func() { + ui.Text(c, fmt.Sprintf(core.Msg(c, "transfer_queue.summary"), s.active, s.failed)).Role(ui.RoleStatus) }) if len(items) == 0 { - codeCaption(c, msg(c, "transfer_queue.empty")).Padding(d.Unit() * 4) + code.CodeCaption(c, core.Msg(c, "transfer_queue.empty")).Padding(d.Unit() * 4) return } s.rows.Key = func(i int) any { return items[i].ID } @@ -123,8 +125,8 @@ func TransferQueue(c *ui.Context, items []QueuedTransfer, s *TransferQueueState, // transferRow builds one transfer and returns the action clicked. func transferRow(c *ui.Context, t QueuedTransfer) TransferAction { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() action := TransferNone ui.Row(c).MinHeight(d.ControlHeight()+2*u).Padding(u, u*3).Gap(u * 3).AlignItems(ui.Center).Children(func() { @@ -135,7 +137,7 @@ func transferRow(c *ui.Context, t QueuedTransfer) TransferAction { if t.Upload { dir, key = "arrow-up", "transfer_queue.upload" } - ui.Icon(c, icons.Must(dir)).Size(14, 14).Shrink(0).TextColor(k.TextMuted).Label(msg(c, key)).Role(ui.RoleImage) + ui.Icon(c, icons.Must(dir)).Size(14, 14).Shrink(0).TextColor(k.TextMuted).Label(core.Msg(c, key)).Role(ui.RoleImage) ui.Text(c, t.Name).SingleLine().MinWidth(0) }) frac := float32(0) @@ -169,7 +171,7 @@ func transferRow(c *ui.Context, t QueuedTransfer) TransferAction { acts = append(acts, TransferRemove) for _, a := range acts { icon, key := transferActionIcon(a) - if iconAction(c, icon, fmt.Sprintf(msg(c, key), t.Name)).Clicked() { + if core.IconAction(c, icon, fmt.Sprintf(core.Msg(c, key), t.Name)).Clicked() { action = a } } @@ -178,21 +180,21 @@ func transferRow(c *ui.Context, t QueuedTransfer) TransferAction { } func transferStatusText(c *ui.Context, t QueuedTransfer, frac float32) { - k := Tokens(c) + k := core.Tokens(c) text, col := "", k.TextMuted switch t.Status { case TransferQueued: - text = msg(c, "transfer_queue.queued") + text = core.Msg(c, "transfer_queue.queued") case TransferActive: - text = fmt.Sprintf("%.0f%% · %s / %s", frac*100, processListBytes(uint64(t.Done)), processListBytes(uint64(t.Total))) + text = fmt.Sprintf("%.0f%% · %s / %s", frac*100, code.ProcessListBytes(uint64(t.Done)), code.ProcessListBytes(uint64(t.Total))) case TransferPaused: - text = fmt.Sprintf(msg(c, "transfer_queue.paused"), frac*100) + text = fmt.Sprintf(core.Msg(c, "transfer_queue.paused"), frac*100) case TransferFailed: - text, col = fmt.Sprintf(msg(c, "transfer_queue.failed"), t.Error), k.Danger + text, col = fmt.Sprintf(core.Msg(c, "transfer_queue.failed"), t.Error), k.Danger case TransferDone: - text, col = msg(c, "transfer_queue.done"), k.Success + text, col = core.Msg(c, "transfer_queue.done"), k.Success } - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() } var transferPauseIcon = ui.MustParseSVG([]byte(``)) diff --git a/transfer_queue_test.go b/files/transfer_queue_test.go similarity index 96% rename from transfer_queue_test.go rename to files/transfer_queue_test.go index a98e9f2..1283dd1 100644 --- a/transfer_queue_test.go +++ b/files/transfer_queue_test.go @@ -1,4 +1,4 @@ -package mujica +package files import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func transferSample() []QueuedTransfer { @@ -25,7 +27,7 @@ type transferProbe struct { } func (p *transferProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TransferQueue(c, p.items, &p.s, TransferQueueOptions{Disabled: p.disabled}) r.Element.Height(380) if r.Action != TransferNone { @@ -102,7 +104,7 @@ func TestTransferQueueLargeAndRevision(t *testing.T) { s := TransferQueueState{} revision := uint64(0) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TransferQueue(c, items, &s, TransferQueueOptions{Revision: revision}).Element.Height(220) }, 500, 250) n := 0 diff --git a/asset_allocation_chart.go b/finance/asset_allocation_chart.go similarity index 70% rename from asset_allocation_chart.go rename to finance/asset_allocation_chart.go index cd26015..13a28fa 100644 --- a/asset_allocation_chart.go +++ b/finance/asset_allocation_chart.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,9 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type AllocationItem struct { @@ -20,13 +23,13 @@ type AllocationItem struct { // AssetAllocationChartOptions configure an AssetAllocationChart. type AssetAllocationChartOptions struct { - Frame ChartFrame + Frame chart.ChartFrame Version int Tolerance float64 Format func(v float64) string } -func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAllocationChartOptions) ChartResult { +func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAllocationChartOptions) chart.ChartResult { if opts.Tolerance < 0 { panic("mujica: AssetAllocationChart Tolerance is negative") } @@ -34,14 +37,14 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc if tol == 0 { tol = 5 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() pal := internal.Palette(k) - var res ChartResult + var res chart.ChartResult row := ui.Row(c).Gap(u * 6).AlignItems(ui.Start).Wrap() row.Children(func() { memo := ui.Local(row, "mujica.allocation", func() allocationSlices { return allocationSlices{} }) - if id := chartID(items) + fmt.Sprint(opts.Version) + msg(c, "pie_chart.other"); id != memo.id { + if id := chart.ChartID(items) + fmt.Sprint(opts.Version) + core.Msg(c, "pie_chart.other"); id != memo.id { total, targets := 0.0, 0.0 for _, it := range items { if it.Name == "" || it.Value < 0 || it.Target < 0 || math.IsNaN(it.Value) || math.IsNaN(it.Target) || math.IsInf(it.Value, 0) || math.IsInf(it.Target, 0) { @@ -55,14 +58,14 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc } memo.total = total - memo.id, memo.slices = id, make([]ChartSlice, len(items)) + memo.id, memo.slices = id, make([]chart.ChartSlice, len(items)) for i, it := range items { - memo.slices[i] = ChartSlice{Label: it.Name, Value: it.Value} + memo.slices[i] = chart.ChartSlice{Label: it.Name, Value: it.Value} } - memo.parts = pieMerge(memo.slices, 1, msg(c, "pie_chart.other")) + memo.parts = chart.PieMerge(memo.slices, 1, core.Msg(c, "pie_chart.other")) memo.markers = make([]int, len(items)) for i, it := range items { - at := slices.IndexFunc(memo.parts, func(part ChartSlice) bool { return part.Label == it.Name }) + at := slices.IndexFunc(memo.parts, func(part chart.ChartSlice) bool { return part.Label == it.Name }) if at < 0 { at = len(memo.parts) - 1 } @@ -76,24 +79,24 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc frame.Height = 220 } ui.Column(c).Width(max(frame.Height+40, 300)).Children(func() { - res = PieChart(c, parts, PieChartOptions{Frame: frame, Donut: true, Format: opts.Format}) + res = chart.PieChart(c, parts, chart.PieChartOptions{Frame: frame, Donut: true, Format: opts.Format}) }) if total == 0 { return } - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) ui.Column(c).MinWidth(340).Grow(1).Children(func() { g := ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(2)).GapX(u * 2).GapY(u * 2). Role(ui.RoleTable).Label(frame.Label) g.Children(func() { for _, h := range []string{"asset_allocation_chart.asset", "asset_allocation_chart.weight", "asset_allocation_chart.target", "asset_allocation_chart.drift"} { - ui.Text(c, msg(c, h)).FontSize(size).TextColor(k.TextMuted).Role(ui.RoleColumnHeader) + ui.Text(c, core.Msg(c, h)).FontSize(size).TextColor(k.TextMuted).Role(ui.RoleColumnHeader) } }) state := ui.Local(row, "mujica.allocation.rows", func() ui.ListState { return ui.ListState{} }) ui.List(c, state, len(items), func(i int) { it := items[i] - ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(2)).GapX(u * 2).MinHeight(Density(c).ControlHeight()).Children(func() { + ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(2)).GapX(u * 2).MinHeight(core.Density(c).ControlHeight()).Children(func() { w := it.Value / total * 100 drift := w - it.Target ui.Row(c).Key(it.Name).Gap(u * 2).AlignItems(ui.Center).Children(func() { @@ -106,7 +109,7 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc ui.Text(c, fmt.Sprintf("%.1f%%", it.Target)).FontFeatures("tnum").TextColor(k.TextMuted).JustifySelf(ui.End) allocationDrift(c, drift, tol) }) - }).Height(min(float32(len(items))*Density(c).ControlHeight(), 240)) + }).Height(min(float32(len(items))*core.Density(c).ControlHeight(), 240)) }) }) res.Element = row @@ -115,15 +118,15 @@ func AssetAllocationChart(c *ui.Context, items []AllocationItem, opts AssetAlloc type allocationSlices struct { id string - slices []ChartSlice + slices []chart.ChartSlice total float64 - parts []ChartSlice + parts []chart.ChartSlice markers []int } func allocationDrift(c *ui.Context, drift, tol float64) { - k := Tokens(c) - text := statisticSigned(drift, 1) + " pt" + k := core.Tokens(c) + text := data.StatisticSigned(drift, 1) + " pt" over := math.Abs(drift) > tol ink := k.TextMuted if over { @@ -136,7 +139,7 @@ func allocationDrift(c *ui.Context, drift, tol float64) { if drift < 0 { word = "asset_allocation_chart.under" } - ui.Text(c, msg(c, word)).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, word)).FontSize(core.FontSize(c, theme.CaptionSize)) } ui.Text(c, text).FontFeatures("tnum") }) diff --git a/asset_allocation_chart_test.go b/finance/asset_allocation_chart_test.go similarity index 81% rename from asset_allocation_chart_test.go rename to finance/asset_allocation_chart_test.go index 9e513dc..127df60 100644 --- a/asset_allocation_chart_test.go +++ b/finance/asset_allocation_chart_test.go @@ -1,17 +1,20 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestAssetAllocationChartWeightsDriftAndKeys(t *testing.T) { items := []AllocationItem{{"Stocks", 60, 50}, {"Bonds", 30, 40}, {"Cash", 10, 10}} - var last ChartResult + var last chart.ChartResult tt := chatTester(960, 420, func(c *ui.Context) { - last = AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: ChartFrame{Label: "Allocation"}}) + last = AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) }) if !tt.HasText("60.0%") || !tt.HasText("+10.0 pt") || !tt.HasText("−10.0 pt") || !tt.HasText("Over") || !tt.HasText("Under") { t.Fatalf("weights %q", tt.Texts()) @@ -26,14 +29,14 @@ func TestAssetAllocationChartWeightsDriftAndKeys(t *testing.T) { t.Fatalf("point %d", last.Point) } marketsPanic(t, func(c *ui.Context) { - AssetAllocationChart(c, []AllocationItem{{"Bad", 1, 101}}, AssetAllocationChartOptions{Frame: ChartFrame{Label: "x"}}) + AssetAllocationChart(c, []AllocationItem{{"Bad", 1, 101}}, AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "x"}}) }) } func TestAssetAllocationChartSnapshot(t *testing.T) { snapshot(t, "asset_allocation_chart", 760, 380, func(c *ui.Context) { ui.Column(c).Padding(16).Gap(12).Fill().Children(func() { - AssetAllocationChart(c, []AllocationItem{{"Stocks", 62000, 55}, {"Bonds", 25000, 30}, {"Cash", 8000, 10}, {"Gold", 5000, 5}}, AssetAllocationChartOptions{Frame: ChartFrame{Label: "Allocation"}}) + AssetAllocationChart(c, []AllocationItem{{"Stocks", 62000, 55}, {"Bonds", 25000, 30}, {"Cash", 8000, 10}, {"Gold", 5000, 5}}, AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) }) }) @@ -42,7 +45,7 @@ func TestAssetAllocationChartSnapshot(t *testing.T) { func TestAssetAllocationChartMergedMarkers(t *testing.T) { items := []AllocationItem{{"Tiny A", 0.1, 0}, {"Tiny B", 0.1, 0}, {"Stocks", 99.8, 0}} tt := chatTester(960, 420, func(c *ui.Context) { - AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: ChartFrame{Label: "Allocation"}}) + AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) }) r, _ := tt.Find("99.8%") asset, _ := tt.Find("Asset") diff --git a/bid_ask_bar.go b/finance/bid_ask_bar.go similarity index 62% rename from bid_ask_bar.go rename to finance/bid_ask_bar.go index b8d23ec..81cfb7b 100644 --- a/bid_ask_bar.go +++ b/finance/bid_ask_bar.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BidAskBarOptions configure a BidAskBar. @@ -25,8 +27,8 @@ func BidAskBar(c *ui.Context, bid, ask float64, opts BidAskBarOptions) *ui.Eleme if opts.Decimals < 0 { panic("mujica: BidAskBar with negative Decimals") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() total := bid + ask share := 0.5 if total > 0 { @@ -34,11 +36,11 @@ func BidAskBar(c *ui.Context, bid, ask float64, opts BidAskBarOptions) *ui.Eleme } label := opts.Label if label == "" { - label = msg(c, "bid_ask_bar.label") + label = core.Msg(c, "bid_ask_bar.label") } - buy := fmt.Sprintf(msg(c, "bid_ask_bar.buy"), statisticFormat(share*100, 1), statisticFormat(bid, opts.Decimals)) - sell := fmt.Sprintf(msg(c, "bid_ask_bar.sell"), statisticFormat(ask, opts.Decimals), statisticFormat((1-share)*100, 1)) - up, down := trendColor(c, 1), trendColor(c, -1) + buy := fmt.Sprintf(core.Msg(c, "bid_ask_bar.buy"), data.StatisticFormat(share*100, 1), data.StatisticFormat(bid, opts.Decimals)) + sell := fmt.Sprintf(core.Msg(c, "bid_ask_bar.sell"), data.StatisticFormat(ask, opts.Decimals), data.StatisticFormat((1-share)*100, 1)) + up, down := core.TrendColor(c, 1), core.TrendColor(c, -1) upText, downText := up, down if total == 0 { up, down, upText, downText = k.Border, k.Border, k.TextMuted, k.TextMuted @@ -46,8 +48,8 @@ func BidAskBar(c *ui.Context, bid, ask float64, opts BidAskBarOptions) *ui.Eleme e := ui.Column(c).Gap(u).MinWidth(160).Role(ui.RoleMeter).Label(label).Description(buy + ", " + sell) e.Children(func() { ui.Row(c).Justify(ui.SpaceBetween).Gap(u * 2).Children(func() { - ui.Text(c, buy).TextColor(upText).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() - ui.Text(c, sell).TextColor(downText).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Text(c, buy).TextColor(upText).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Text(c, sell).TextColor(downText).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() }) ui.Box(c).Height(8).FillWidth().Draw(func(p *ui.Painter, r ui.Rect) { split := r.X + r.W*float32(share) diff --git a/bid_ask_bar_test.go b/finance/bid_ask_bar_test.go similarity index 75% rename from bid_ask_bar_test.go rename to finance/bid_ask_bar_test.go index 89084d8..3a6918c 100644 --- a/bid_ask_bar_test.go +++ b/finance/bid_ask_bar_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBidAskBarShares(t *testing.T) { bid, ask := 620.0, 380.0 - tt := tradingTester(GreenUp, 400, 100, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 100, func(c *ui.Context) { ui.Box(c).Width(300).Children(func() { BidAskBar(c, bid, ask, BidAskBarOptions{}) }) }) if !tt.HasText("Buy 62.0% · 620") || !tt.HasText("380 · Sell 38.0%") { @@ -24,7 +26,7 @@ func TestBidAskBarShares(t *testing.T) { t.Fatal("negative volume accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { BidAskBar(c, -1, 2, BidAskBarOptions{}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { BidAskBar(c, -1, 2, BidAskBarOptions{}) }) } func TestBidAskBarSnapshot(t *testing.T) { diff --git a/currency_converter.go b/finance/currency_converter.go similarity index 62% rename from currency_converter.go rename to finance/currency_converter.go index 7c0ac34..9398d83 100644 --- a/currency_converter.go +++ b/finance/currency_converter.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -8,8 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type Currency struct { @@ -70,15 +73,15 @@ func CurrencyConverter(c *ui.Context, s *CurrencyConverterState, currencies []Cu if s.From != s.rateFrom || s.To != s.rateTo { rate = 0 } - k := Tokens(c) - u := Density(c).Unit() - options := make([]SelectOption[string], len(currencies)) + k := core.Tokens(c) + u := core.Density(c).Unit() + options := make([]input.SelectOption[string], len(currencies)) for i, cur := range currencies { - options[i] = SelectOption[string]{Value: cur.Code, Label: cur.Code + " · " + cur.Name} + options[i] = input.SelectOption[string]{Value: cur.Code, Label: cur.Code + " · " + cur.Name} } v := CurrencyConverterView{Result: math.NaN()} if rate > 0 { - v.Result = numberInputSpec{decimals: to.Decimals}.round(s.Amount * rate) + v.Result = input.NumberInputSpec{Decimals: to.Decimals}.Round(s.Amount * rate) } v.Element = ui.Column(c).Gap(u*3).Padding(u*4).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Disabled(opts.Disabled) @@ -89,22 +92,22 @@ func CurrencyConverter(c *ui.Context, s *CurrencyConverterState, currencies []Cu } v.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.End).Wrap().Children(func() { - FormField(c, msg(c, "currency_converter.amount"), FormFieldOptions{}, func() *ui.Element { - r := NumberInput(c, &s.Amount, NumberInputOptions{Min: 0, Max: 1e15, Step: math.Pow(10, -float64(from.Decimals)), - Precision: from.Decimals, Label: msg(c, "currency_converter.amount"), Disabled: opts.Disabled}) + input.FormField(c, core.Msg(c, "currency_converter.amount"), input.FormFieldOptions{}, func() *ui.Element { + r := input.NumberInput(c, &s.Amount, input.NumberInputOptions{Min: 0, Max: 1e15, Step: math.Pow(10, -float64(from.Decimals)), + Precision: from.Decimals, Label: core.Msg(c, "currency_converter.amount"), Disabled: opts.Disabled}) return r.Element.Width(170) }) - FormField(c, msg(c, "currency_converter.from"), FormFieldOptions{}, func() *ui.Element { - r := Select(c, &s.From, options, SelectOptions{Label: msg(c, "currency_converter.from"), Disabled: opts.Disabled}) + input.FormField(c, core.Msg(c, "currency_converter.from"), input.FormFieldOptions{}, func() *ui.Element { + r := input.Select(c, &s.From, options, input.SelectOptions{Label: core.Msg(c, "currency_converter.from"), Disabled: opts.Disabled}) v.PairChanged = v.PairChanged || r.Changed() return r.Element }) - swap := iconAction(c, icons.Must("arrow-up-down"), msg(c, "currency_converter.swap")).Rotate(90) + swap := core.IconAction(c, icons.Must("arrow-up-down"), core.Msg(c, "currency_converter.swap")).Rotate(90) if swap.Clicked() && !swap.IsDisabled() { s.From, s.To, v.PairChanged = s.To, s.From, true } - FormField(c, msg(c, "currency_converter.to"), FormFieldOptions{}, func() *ui.Element { - r := Select(c, &s.To, options, SelectOptions{Label: msg(c, "currency_converter.to"), Disabled: opts.Disabled}) + input.FormField(c, core.Msg(c, "currency_converter.to"), input.FormFieldOptions{}, func() *ui.Element { + r := input.Select(c, &s.To, options, input.SelectOptions{Label: core.Msg(c, "currency_converter.to"), Disabled: opts.Disabled}) v.PairChanged = v.PairChanged || r.Changed() return r.Element }) @@ -113,23 +116,23 @@ func CurrencyConverter(c *ui.Context, s *CurrencyConverterState, currencies []Cu if v.PairChanged { v.Result = math.NaN() } else if rate > 0 { - v.Result = numberInputSpec{decimals: to.Decimals}.round(s.Amount * rate) + v.Result = input.NumberInputSpec{Decimals: to.Decimals}.Round(s.Amount * rate) } if rate == 0 || v.PairChanged { - ui.Text(c, msg(c, "currency_converter.no_rate")).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "currency_converter.no_rate")).TextColor(k.TextMuted).Role(ui.RoleStatus) return } ui.Row(c).Gap(u * 2).AlignItems(ui.End).Role(ui.RoleStatus).Label(fmt.Sprintf("%s %s = %s %s", - statisticFormat(s.Amount, from.Decimals), from.Code, statisticFormat(v.Result, to.Decimals), to.Code)).Children(func() { - ui.Text(c, statisticFormat(s.Amount, from.Decimals)+" "+from.Code+" =").TextColor(k.TextMuted).FontFeatures("tnum") - ui.Text(c, statisticFormat(v.Result, to.Decimals)+" "+to.Code).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)). + data.StatisticFormat(s.Amount, from.Decimals), from.Code, data.StatisticFormat(v.Result, to.Decimals), to.Code)).Children(func() { + ui.Text(c, data.StatisticFormat(s.Amount, from.Decimals)+" "+from.Code+" =").TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, data.StatisticFormat(v.Result, to.Decimals)+" "+to.Code).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)). TextColor(k.Text).FontFeatures("tnum") }) - note := fmt.Sprintf(msg(c, "currency_converter.rate"), from.Code, rate, to.Code) + note := fmt.Sprintf(core.Msg(c, "currency_converter.rate"), from.Code, rate, to.Code) if !opts.RateTime.IsZero() { note += " · " + opts.RateTime.Format("2006-01-02 15:04") } - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) if v.Element.IsDisabled() { v.PairChanged = false diff --git a/currency_converter_test.go b/finance/currency_converter_test.go similarity index 98% rename from currency_converter_test.go rename to finance/currency_converter_test.go index fa2d927..1de6009 100644 --- a/currency_converter_test.go +++ b/finance/currency_converter_test.go @@ -1,9 +1,10 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "math" "testing" + + "github.com/egoist/mygo/ui" ) func TestCurrencyConverterPrecisionAmountAndNoRate(t *testing.T) { diff --git a/depth_ladder.go b/finance/depth_ladder.go similarity index 75% rename from depth_ladder.go rename to finance/depth_ladder.go index 6b76af6..62cb2e1 100644 --- a/depth_ladder.go +++ b/finance/depth_ladder.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,9 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DepthLadderOptions configure a DepthLadder. TickSize is required. @@ -47,7 +50,7 @@ func (r DepthLadderResult) Clicked() (price float64, side OrderSide, ok bool) { // DepthLadder is a price ladder around last; cell clicks order. func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBookData, opts DepthLadderOptions) DepthLadderResult { - dec := priceDecimals("DepthLadder", opts.Decimals) + dec := core.PriceDecimals("DepthLadder", opts.Decimals) if opts.SizeDecimals < 0 || opts.Rows < 0 { panic("mujica: DepthLadder SizeDecimals and Rows cannot be negative") } @@ -62,10 +65,10 @@ func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBoo } depthLadderRefresh(s, book, scale, tick) center := int64(math.Round(last*scale/float64(tick))) * tick - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r DepthLadderResult - r.Element = ui.Column(c).Gap(d.Unit()).Disabled(opts.Disabled).Role(ui.RoleTable).Label(msg(c, "depth_ladder.label")) + r.Element = ui.Column(c).Gap(d.Unit()).Disabled(opts.Disabled).Role(ui.RoleTable).Label(core.Msg(c, "depth_ladder.label")) disabled := r.Element.IsDisabled() move := func(by int) { if !disabled && by != 0 { @@ -81,9 +84,9 @@ func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBoo } r.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "depth_ladder.bid")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.End) - ui.Text(c, msg(c, "depth_ladder.price")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.Center) - ui.Text(c, msg(c, "depth_ladder.ask")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Grow(1).Basis(0) + ui.Text(c, core.Msg(c, "depth_ladder.bid")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.End) + ui.Text(c, core.Msg(c, "depth_ladder.price")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1).Basis(0).TextAlign(ui.Center) + ui.Text(c, core.Msg(c, "depth_ladder.ask")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1).Basis(0) }) grid := ui.Column(c).FocusGroup(ui.Vertical | ui.Horizontal) grid.HandleInput(func(ev ui.InputEvent) bool { @@ -101,7 +104,7 @@ func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBoo } }) if s.Offset != 0 { - if Button(c, msg(c, "depth_ladder.recenter"), ButtonOptions{Variant: Ghost, Icon: icons.Must("refresh-cw"), Disabled: disabled}).Clicked() { + if input.Button(c, core.Msg(c, "depth_ladder.recenter"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("refresh-cw"), Disabled: disabled}).Clicked() { s.Offset = 0 } } @@ -111,13 +114,13 @@ func DepthLadder(c *ui.Context, s *DepthLadderState, last float64, book OrderBoo // depthLadderRow is a buy cell, the price, a sell cell. func depthLadderRow(c *ui.Context, r *DepthLadderResult, s *DepthLadderState, t int64, current bool, scale float64, dec, sizeDec int) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) price := float64(t) / scale - text := statisticFormat(price, dec) + text := data.StatisticFormat(price, dec) ui.Row(c).Key(t).MinHeight(d.ControlHeight()).AlignItems(ui.Stretch).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { cell := func(side OrderSide, size float64) { - label := fmt.Sprintf(msg(c, fmt.Sprintf("depth_ladder.order%d", side)), text) + label := fmt.Sprintf(core.Msg(c, fmt.Sprintf("depth_ladder.order%d", side)), text) b := ui.ButtonBase(c).Grow(1).Basis(0).Padding(0, d.Unit()*2).Label(label) if side == SideBuy { b.Justify(ui.End) @@ -126,11 +129,11 @@ func depthLadderRow(c *ui.Context, r *DepthLadderResult, s *DepthLadderState, t } internal.Ring(b, k.Focus, 0) if b.Hovered() { - b.Background(trendColor(c, side.sign()).Alpha(0.14)) + b.Background(core.TrendColor(c, side.sign()).Alpha(0.14)) } b.Children(func() { if size > 0 { - ui.Text(c, statisticFormat(size, sizeDec)).TextColor(trendColor(c, side.sign())).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Text(c, data.StatisticFormat(size, sizeDec)).TextColor(core.TrendColor(c, side.sign())).FontFeatures("tnum").FontWeight(600).SingleLine() } }) if b.Clicked() && !b.IsDisabled() { @@ -141,7 +144,7 @@ func depthLadderRow(c *ui.Context, r *DepthLadderResult, s *DepthLadderState, t cell(SideBuy, s.cache.bids[t]) mid := ui.Row(c).Grow(1).Basis(0).Justify(ui.Center).AlignItems(ui.Center).Gap(d.Unit()).Background(k.Background) if current { - mid.Background(k.Selection).Label(msg(c, "depth_ladder.last") + " " + text) + mid.Background(k.Selection).Label(core.Msg(c, "depth_ladder.last") + " " + text) mid.DrawOver(func(p *ui.Painter, rect ui.Rect) { internal.Frame(p, rect, 2, 4, k.AccentText) }) diff --git a/depth_ladder_test.go b/finance/depth_ladder_test.go similarity index 86% rename from depth_ladder_test.go rename to finance/depth_ladder_test.go index 6af2c74..cb70206 100644 --- a/depth_ladder_test.go +++ b/finance/depth_ladder_test.go @@ -1,9 +1,11 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func depthLadderBook() OrderBookData { @@ -14,7 +16,7 @@ func TestDepthLadderCentersAndOrders(t *testing.T) { var s DepthLadderState var price float64 var side OrderSide - tt := tradingTester(GreenUp, 400, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 600, func(c *ui.Context) { if p, sd, ok := DepthLadder(c, &s, 100.004, depthLadderBook(), DepthLadderOptions{TickSize: 0.01, Rows: 7}).Clicked(); ok { price, side = p, sd } @@ -43,7 +45,7 @@ func TestDepthLadderCentersAndOrders(t *testing.T) { func TestDepthLadderScrollsAndRecenters(t *testing.T) { var s DepthLadderState - tt := tradingTester(GreenUp, 400, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 600, func(c *ui.Context) { DepthLadder(c, &s, 100, depthLadderBook(), DepthLadderOptions{TickSize: 0.01, Rows: 7}) }) _ = tt.Click("Buy at 100.00") @@ -70,7 +72,7 @@ func TestDepthLadderLargeBook(t *testing.T) { for i := range 100_000 { book.Bids = append(book.Bids, BookLevel{Price: 100 - float64(i)/100, Size: 1}) } - tt := tradingTester(GreenUp, 400, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 600, func(c *ui.Context) { DepthLadder(c, &s, 100, book, DepthLadderOptions{TickSize: 0.01, Rows: 9}) }) if n := len(tt.Texts()); n > 60 { @@ -87,7 +89,7 @@ func TestDepthLadderLargeBook(t *testing.T) { func TestDepthLadderDisabledAndBadTick(t *testing.T) { var s DepthLadderState clicked := false - tt := tradingTester(GreenUp, 400, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 600, func(c *ui.Context) { _, _, clicked = DepthLadder(c, &s, 100, depthLadderBook(), DepthLadderOptions{TickSize: 0.01, Disabled: true}).Clicked() }) _ = tt.Click("Buy at 100.00") @@ -100,7 +102,7 @@ func TestDepthLadderDisabledAndBadTick(t *testing.T) { t.Fatal("zero tick accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { DepthLadder(c, &s, 100, depthLadderBook(), DepthLadderOptions{}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { DepthLadder(c, &s, 100, depthLadderBook(), DepthLadderOptions{}) }) } func TestDepthLadderSnapshot(t *testing.T) { diff --git a/finance/doc.go b/finance/doc.go new file mode 100644 index 0000000..a4bcc0c --- /dev/null +++ b/finance/doc.go @@ -0,0 +1,2 @@ +// Package finance holds MujicaUI quotes, order entry, trading and market views. +package finance diff --git a/earnings_calendar.go b/finance/earnings_calendar.go similarity index 59% rename from earnings_calendar.go rename to finance/earnings_calendar.go index 87092ed..0d560b6 100644 --- a/earnings_calendar.go +++ b/finance/earnings_calendar.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type EarningsTiming int @@ -20,7 +22,7 @@ const ( type EarningsEvent struct { ID, Symbol, Name string - Date Date + Date core.Date Timing EarningsTiming EPSEstimate float64 EPSActual float64 @@ -29,44 +31,44 @@ type EarningsEvent struct { // EarningsCalendarOptions configure an EarningsCalendar. type EarningsCalendarOptions struct { Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } -func EarningsCalendar(c *ui.Context, s *DataTableState[string], events []EarningsEvent, opts EarningsCalendarOptions) DataTableView { +func EarningsCalendar(c *ui.Context, s *data.DataTableState[string], events []EarningsEvent, opts EarningsCalendarOptions) data.DataTableView { if s == nil { panic("mujica: EarningsCalendar needs a state") } - cols := []DataColumn[EarningsEvent]{ - {ID: "date", Title: msg(c, "earnings_calendar.date"), Width: 110, Sortable: true, + cols := []data.DataColumn[EarningsEvent]{ + {ID: "date", Title: core.Msg(c, "earnings_calendar.date"), Width: 110, Sortable: true, Compare: func(a, b EarningsEvent) int { return a.Date.Compare(b.Date) }, Text: func(e EarningsEvent) string { return e.Date.String() }}, - {ID: "timing", Title: msg(c, "earnings_calendar.timing"), Width: 130, Sortable: true, + {ID: "timing", Title: core.Msg(c, "earnings_calendar.timing"), Width: 130, Sortable: true, Compare: func(a, b EarningsEvent) int { return cmp.Compare(a.Timing, b.Timing) }, - Text: func(e EarningsEvent) string { return msg(c, earningsTimings[earningsTimingCheck(e.Timing)].key) }, + Text: func(e EarningsEvent) string { return core.Msg(c, earningsTimings[earningsTimingCheck(e.Timing)].key) }, Cell: func(e EarningsEvent) { earningsTiming(c, e.Timing) }}, - {ID: "symbol", Title: msg(c, "earnings_calendar.symbol"), Width: 90, Sortable: true, + {ID: "symbol", Title: core.Msg(c, "earnings_calendar.symbol"), Width: 90, Sortable: true, Compare: func(a, b EarningsEvent) int { return cmp.Compare(a.Symbol, b.Symbol) }, Text: func(e EarningsEvent) string { return e.Symbol }}, - {ID: "name", Title: msg(c, "earnings_calendar.name"), Text: func(e EarningsEvent) string { return e.Name }}, - {ID: "estimate", Title: msg(c, "earnings_calendar.estimate"), Width: 90, Align: ui.End, Sortable: true, + {ID: "name", Title: core.Msg(c, "earnings_calendar.name"), Text: func(e EarningsEvent) string { return e.Name }}, + {ID: "estimate", Title: core.Msg(c, "earnings_calendar.estimate"), Width: 90, Align: ui.End, Sortable: true, Compare: func(a, b EarningsEvent) int { return cmp.Compare(a.EPSEstimate, b.EPSEstimate) }, Text: func(e EarningsEvent) string { return earningsValue(e.EPSEstimate) }}, - {ID: "actual", Title: msg(c, "earnings_calendar.actual"), Width: 90, Align: ui.End, Sortable: true, + {ID: "actual", Title: core.Msg(c, "earnings_calendar.actual"), Width: 90, Align: ui.End, Sortable: true, Compare: func(a, b EarningsEvent) int { return cmp.Compare(a.EPSActual, b.EPSActual) }, Text: func(e EarningsEvent) string { return earningsValue(e.EPSActual) }}, - {ID: "surprise", Title: msg(c, "earnings_calendar.surprise"), Width: 150, Align: ui.End, + {ID: "surprise", Title: core.Msg(c, "earnings_calendar.surprise"), Width: 150, Align: ui.End, Text: func(e EarningsEvent) string { return earningsSurpriseText(e) }, Cell: func(e EarningsEvent) { if d := e.EPSActual - e.EPSEstimate; !math.IsNaN(d) { tradingSigned(c, d, earningsSurpriseText(e)) } else { - ui.Text(c, "—").TextColor(Tokens(c).TextMuted).FillWidth().TextAlign(ui.End) + ui.Text(c, "—").TextColor(core.Tokens(c).TextMuted).FillWidth().TextAlign(ui.End) } }}, } - var v DataTableView + var v data.DataTableView col := ui.Column(c).MinHeight(0).Grow(1).Disabled(opts.Disabled) opts.Disabled = col.IsDisabled() if opts.Disabled { @@ -74,8 +76,8 @@ func EarningsCalendar(c *ui.Context, s *DataTableState[string], events []Earning s = © } col.Children(func() { - v = DataTable(c, s, events, DataTableOptions[EarningsEvent, string]{Columns: cols, Key: func(e EarningsEvent) string { return e.ID }, - Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: msg(c, "earnings_calendar.empty")}) + v = data.DataTable(c, s, events, data.DataTableOptions[EarningsEvent, string]{Columns: cols, Key: func(e EarningsEvent) string { return e.ID }, + Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "earnings_calendar.empty")}) v.Element.Grow(1) }) v.Element = col @@ -95,11 +97,11 @@ func earningsTimingCheck(t EarningsTiming) EarningsTiming { // earningsTiming shows the timing as an icon and words. func earningsTiming(c *ui.Context, t EarningsTiming) { - k := Tokens(c) + k := core.Tokens(c) tm := earningsTimings[earningsTimingCheck(t)] ui.Row(c).Gap(4).AlignItems(ui.Center).Children(func() { ui.Icon(c, icons.Must(tm.icon)).Size(14, 14).TextColor(k.TextMuted) - ui.Text(c, msg(c, tm.key)).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, core.Msg(c, tm.key)).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) } @@ -107,7 +109,7 @@ func earningsValue(v float64) string { if math.IsNaN(v) { return "—" } - return statisticFormat(v, 2) + return data.StatisticFormat(v, 2) } func earningsSurpriseText(e EarningsEvent) string { @@ -116,7 +118,7 @@ func earningsSurpriseText(e EarningsEvent) string { return "—" } if e.EPSEstimate == 0 { - return statisticSigned(d, 2) + return data.StatisticSigned(d, 2) } - return statisticSigned(d, 2) + " (" + statisticSigned(d/math.Abs(e.EPSEstimate)*100, 1) + "%)" + return data.StatisticSigned(d, 2) + " (" + data.StatisticSigned(d/math.Abs(e.EPSEstimate)*100, 1) + "%)" } diff --git a/earnings_calendar_test.go b/finance/earnings_calendar_test.go similarity index 56% rename from earnings_calendar_test.go rename to finance/earnings_calendar_test.go index 56ec7fa..ce6b111 100644 --- a/earnings_calendar_test.go +++ b/finance/earnings_calendar_test.go @@ -1,14 +1,18 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "math" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestEarningsCalendarTimingActualAndKeys(t *testing.T) { - var s DataTableState[string] - rows := []EarningsEvent{{ID: "a", Symbol: "AAPL", Date: Date{2026, 10, 30}, Timing: EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}, {ID: "b", Symbol: "JPM", Date: Date{2026, 10, 11}, Timing: EarningsBeforeOpen, EPSEstimate: 4, EPSActual: math.NaN()}} + var s data.DataTableState[string] + rows := []EarningsEvent{{ID: "a", Symbol: "AAPL", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}, {ID: "b", Symbol: "JPM", Date: core.Date{Year: 2026, Month: 10, Day: 11}, Timing: EarningsBeforeOpen, EPSEstimate: 4, EPSActual: math.NaN()}} tt := chatTester(960, 420, func(c *ui.Context) { EarningsCalendar(c, &s, rows, EarningsCalendarOptions{}).Element.Height(380) }) if !tt.HasText("Before open") || !tt.HasText("After close") || !tt.HasText("+0.04 (+2.5%)") || !tt.HasText("—") { t.Fatalf("texts %q", tt.Texts()) @@ -28,10 +32,10 @@ func TestEarningsCalendarTimingActualAndKeys(t *testing.T) { } func TestEarningsCalendarSnapshot(t *testing.T) { snapshot(t, "earnings_calendar", 900, 300, func(c *ui.Context) { - es := ui.Local(c.Root(), "snapshot.earnings_calendar", func() DataTableState[string] { return DataTableState[string]{} }) + es := ui.Local(c.Root(), "snapshot.earnings_calendar", func() data.DataTableState[string] { return data.DataTableState[string]{} }) ui.Column(c).Padding(16).Gap(12).Fill().Children(func() { - EarningsCalendar(c, es, []EarningsEvent{{ID: "1", Symbol: "AAPL", Name: "Apple", Date: Date{2026, 10, 30}, Timing: EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}, - {ID: "2", Symbol: "JPM", Name: "JPMorgan", Date: Date{2026, 10, 11}, Timing: EarningsBeforeOpen, EPSEstimate: 4.0, EPSActual: math.NaN()}}, EarningsCalendarOptions{}).Element.Height(240) + EarningsCalendar(c, es, []EarningsEvent{{ID: "1", Symbol: "AAPL", Name: "Apple", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}, + {ID: "2", Symbol: "JPM", Name: "JPMorgan", Date: core.Date{Year: 2026, Month: 10, Day: 11}, Timing: EarningsBeforeOpen, EPSEstimate: 4.0, EPSActual: math.NaN()}}, EarningsCalendarOptions{}).Element.Height(240) }) }) diff --git a/economic_calendar.go b/finance/economic_calendar.go similarity index 51% rename from economic_calendar.go rename to finance/economic_calendar.go index adf2e71..0630210 100644 --- a/economic_calendar.go +++ b/finance/economic_calendar.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -10,8 +10,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EconomicEvent is a scheduled release; Actual stays empty until published. @@ -27,7 +31,7 @@ type EconomicCalendarState struct { Countries []string // MinImportance hides events below it; zero shows all. MinImportance int - Table DataTableState[string] + Table data.DataTableState[string] cache economicCache } @@ -35,26 +39,26 @@ type economicCache struct { key string rows []EconomicEvent data string - countries []SelectOption[string] + countries []input.SelectOption[string] } // EconomicCalendarOptions configure an EconomicCalendar. type EconomicCalendarOptions struct { Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } -func EconomicCalendar(c *ui.Context, s *EconomicCalendarState, events []EconomicEvent, opts EconomicCalendarOptions) DataTableView { +func EconomicCalendar(c *ui.Context, s *EconomicCalendarState, events []EconomicEvent, opts EconomicCalendarOptions) data.DataTableView { if s == nil { panic("mujica: EconomicCalendar needs a state") } if s.MinImportance < 0 || s.MinImportance > 3 { panic("mujica: EconomicCalendar MinImportance must be 0..3") } - u := Density(c).Unit() - var v DataTableView + u := core.Density(c).Unit() + var v data.DataTableView col := ui.Column(c).Gap(u * 2).MinHeight(0).Grow(1).Disabled(opts.Disabled) opts.Disabled = col.IsDisabled() if opts.Disabled { @@ -62,7 +66,7 @@ func EconomicCalendar(c *ui.Context, s *EconomicCalendarState, events []Economic s = © } col.Children(func() { - if data := chartID(events) + fmt.Sprint(opts.Version); data != s.cache.data { + if data := chart.ChartID(events) + fmt.Sprint(opts.Version); data != s.cache.data { s.cache.data, s.cache.countries = data, nil seen := map[string]bool{} for _, e := range events { @@ -71,43 +75,45 @@ func EconomicCalendar(c *ui.Context, s *EconomicCalendarState, events []Economic panic("mujica: EconomicEvent needs an ID and Importance 1..3") } seen[e.ID] = true - if !slices.ContainsFunc(s.cache.countries, func(o SelectOption[string]) bool { return o.Value == e.Country }) { - s.cache.countries = append(s.cache.countries, SelectOption[string]{Value: e.Country, Label: e.Country}) + if !slices.ContainsFunc(s.cache.countries, func(o input.SelectOption[string]) bool { return o.Value == e.Country }) { + s.cache.countries = append(s.cache.countries, input.SelectOption[string]{Value: e.Country, Label: e.Country}) } } - slices.SortFunc(s.cache.countries, func(a, b SelectOption[string]) int { return cmp.Compare(a.Value, b.Value) }) + slices.SortFunc(s.cache.countries, func(a, b input.SelectOption[string]) int { return cmp.Compare(a.Value, b.Value) }) } countries := s.cache.countries ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Wrap().Children(func() { - MultiSelect(c, &s.Countries, countries, MultiSelectOptions{Placeholder: msg(c, "economic_calendar.all_countries"), - Label: msg(c, "economic_calendar.countries"), Disabled: opts.Disabled}).Element.MinWidth(220) - imp := []RadioItem[int]{{Value: 0, Label: msg(c, "economic_calendar.all")}, {Value: 2, Label: msg(c, "economic_calendar.medium_up")}, - {Value: 3, Label: msg(c, "economic_calendar.high")}} - RadioGroup(c, &s.MinImportance, imp, RadioGroupOptions{Horizontal: true, Label: msg(c, "economic_calendar.importance"), Disabled: opts.Disabled}) + input.MultiSelect(c, &s.Countries, countries, input.MultiSelectOptions{Placeholder: core.Msg(c, "economic_calendar.all_countries"), + Label: core.Msg(c, "economic_calendar.countries"), Disabled: opts.Disabled}).Element.MinWidth(220) + imp := []input.RadioItem[int]{{Value: 0, Label: core.Msg(c, "economic_calendar.all")}, {Value: 2, Label: core.Msg(c, "economic_calendar.medium_up")}, + {Value: 3, Label: core.Msg(c, "economic_calendar.high")}} + input.RadioGroup(c, &s.MinImportance, imp, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "economic_calendar.importance"), Disabled: opts.Disabled}) }) rows := economicFilter(s, events, opts.Version) - cols := []DataColumn[EconomicEvent]{ - {ID: "time", Title: msg(c, "economic_calendar.time"), Width: 130, Sortable: true, + cols := []data.DataColumn[EconomicEvent]{ + {ID: "time", Title: core.Msg(c, "economic_calendar.time"), Width: 130, Sortable: true, Compare: func(a, b EconomicEvent) int { return a.Time.Compare(b.Time) }, Text: func(e EconomicEvent) string { return e.Time.Format("01-02 15:04") }}, - {ID: "country", Title: msg(c, "economic_calendar.country"), Width: 80, Sortable: true, + {ID: "country", Title: core.Msg(c, "economic_calendar.country"), Width: 80, Sortable: true, Compare: func(a, b EconomicEvent) int { return cmp.Compare(a.Country, b.Country) }, Text: func(e EconomicEvent) string { return e.Country }}, - {ID: "importance", Title: msg(c, "economic_calendar.importance"), Width: 110, Sortable: true, + {ID: "importance", Title: core.Msg(c, "economic_calendar.importance"), Width: 110, Sortable: true, Compare: func(a, b EconomicEvent) int { return cmp.Compare(a.Importance, b.Importance) }, - Text: func(e EconomicEvent) string { return msg(c, fmt.Sprintf("economic_calendar.level_%d", e.Importance)) }, - Cell: func(e EconomicEvent) { economicImportance(c, e.Importance) }}, - {ID: "event", Title: msg(c, "economic_calendar.event"), Text: func(e EconomicEvent) string { return e.Title }}, - {ID: "actual", Title: msg(c, "economic_calendar.actual"), Width: 90, Align: ui.End, + Text: func(e EconomicEvent) string { + return core.Msg(c, fmt.Sprintf("economic_calendar.level_%d", e.Importance)) + }, + Cell: func(e EconomicEvent) { economicImportance(c, e.Importance) }}, + {ID: "event", Title: core.Msg(c, "economic_calendar.event"), Text: func(e EconomicEvent) string { return e.Title }}, + {ID: "actual", Title: core.Msg(c, "economic_calendar.actual"), Width: 90, Align: ui.End, Text: func(e EconomicEvent) string { return cmp.Or(e.Actual, "—") }, Cell: func(e EconomicEvent) { ui.Text(c, cmp.Or(e.Actual, "—")).FontWeight(700).FontFeatures("tnum").JustifySelf(ui.End).FillWidth().TextAlign(ui.End) }}, - {ID: "forecast", Title: msg(c, "economic_calendar.forecast"), Width: 90, Align: ui.End, Text: func(e EconomicEvent) string { return cmp.Or(e.Forecast, "—") }}, - {ID: "previous", Title: msg(c, "economic_calendar.previous"), Width: 90, Align: ui.End, Text: func(e EconomicEvent) string { return cmp.Or(e.Previous, "—") }}, + {ID: "forecast", Title: core.Msg(c, "economic_calendar.forecast"), Width: 90, Align: ui.End, Text: func(e EconomicEvent) string { return cmp.Or(e.Forecast, "—") }}, + {ID: "previous", Title: core.Msg(c, "economic_calendar.previous"), Width: 90, Align: ui.End, Text: func(e EconomicEvent) string { return cmp.Or(e.Previous, "—") }}, } - v = DataTable(c, &s.Table, rows, DataTableOptions[EconomicEvent, string]{Columns: cols, Key: func(e EconomicEvent) string { return e.ID }, - Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: msg(c, "economic_calendar.empty")}) + v = data.DataTable(c, &s.Table, rows, data.DataTableOptions[EconomicEvent, string]{Columns: cols, Key: func(e EconomicEvent) string { return e.ID }, + Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "economic_calendar.empty")}) v.Element.Grow(1) }) v.Element = col @@ -116,8 +122,8 @@ func EconomicCalendar(c *ui.Context, s *EconomicCalendarState, events []Economic // economicImportance draws one to three diamonds and names the level. func economicImportance(c *ui.Context, level int) { - k := Tokens(c) - word := msg(c, fmt.Sprintf("economic_calendar.level_%d", level)) + k := core.Tokens(c) + word := core.Msg(c, fmt.Sprintf("economic_calendar.level_%d", level)) ui.Row(c).Gap(4).AlignItems(ui.Center).Label(word).Children(func() { ui.Box(c).Size(34, 10).Draw(func(p *ui.Painter, r ui.Rect) { for i := range 3 { @@ -129,12 +135,12 @@ func economicImportance(c *ui.Context, level int) { } } }) - ui.Text(c, word).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, word).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) } func economicFilter(s *EconomicCalendarState, events []EconomicEvent, version int) []EconomicEvent { - key := fmt.Sprint(chartID(events), version, s.MinImportance, strings.Join(s.Countries, "\x00")) + key := fmt.Sprint(chart.ChartID(events), version, s.MinImportance, strings.Join(s.Countries, "\x00")) if key == s.cache.key { return s.cache.rows } diff --git a/economic_calendar_test.go b/finance/economic_calendar_test.go similarity index 99% rename from economic_calendar_test.go rename to finance/economic_calendar_test.go index 07b2f5a..1c2fde3 100644 --- a/economic_calendar_test.go +++ b/finance/economic_calendar_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestEconomicCalendarCountryAndImportance(t *testing.T) { diff --git a/finance/example_markets_test.go b/finance/example_markets_test.go new file mode 100644 index 0000000..a2f077f --- /dev/null +++ b/finance/example_markets_test.go @@ -0,0 +1,247 @@ +package finance_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" +) + +func ExampleOptionChain() { + var state finance.OptionChainState + rows := []finance.OptionStrike{{Strike: 100, Call: finance.OptionQuote{Bid: 3, Ask: 3.2, Last: 3.1}, Put: finance.OptionQuote{Bid: 2, Ask: 2.2, Last: 2.1}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.OptionChain(c, &state, []string{"Oct 17", "Nov 21"}, rows, finance.OptionChainOptions{Spot: 100.4}) + if r.Changed || r.Submitted || r.ExpiryChanged { + fmt.Println(state.Expiry, state.Strike, state.Side) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleGreeksTable() { + rows := []finance.GreeksRow{{Label: "AAPL 100 C", Greeks: finance.OptionGreeks{Delta: 0.5, Gamma: 0.03, Theta: -0.1, Vega: 0.2, Rho: 0.05}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.GreeksTable(c, rows, finance.GreeksTableOptions{Notes: true}) + } + ui.Render(view, 960, 440, 1) +} + +func ExamplePayoffDiagram() { + legs := []finance.OptionLeg{{Kind: finance.LegCall, Strike: 100, Premium: 3, Quantity: 1}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.PayoffDiagram(c, legs, finance.PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Call payoff"}, Spot: 103}) + if r.Changed() || r.Retried() { + fmt.Println(r.Point) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleWatchlist() { + var state finance.WatchlistState + groups := []finance.WatchGroup{{Name: "Technology", Items: []finance.WatchItem{{Symbol: "AAPL", Name: "Apple", Price: 227.5, Change: 2.7, ChangePercent: 1.2}}}, {Name: "Energy", Items: []finance.WatchItem{{Symbol: "XOM", Price: 117.4}}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.Watchlist(c, &state, groups, finance.WatchlistOptions{}) + if r.Changed || r.Submitted || r.Moved { + fmt.Println(state.Selected, r.Rows, r.To) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleSymbolSearch() { + var state finance.SymbolSearchState + rows := []finance.MarketSymbol{{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.SymbolSearch(c, &state, rows, finance.SymbolSearchOptions{Markets: []string{"NASDAQ", "NYSE"}, Recent: rows}) + if r.Changed || r.Picked || r.Retry { + fmt.Println(state.Query, state.Market, r.Chosen.Code) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleSymbolBadge() { + symbol := finance.MarketSymbol{Code: "AAPL", Name: "Apple", Market: "NASDAQ", Type: "Stock"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.SymbolBadge(c, symbol, finance.SymbolBadgeOptions{Clickable: true}) + if r.Changed() { + fmt.Println(symbol.Code) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleScreener() { + var state finance.ScreenerState + fields := []finance.ScreenerField{{ID: "pe", Label: "P/E", Decimals: 1}} + rows := []finance.ScreenerRow{{Symbol: "AAPL", Values: map[string]float64{"pe": 34}}, {Symbol: "XOM", Values: map[string]float64{"pe": 13}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.Screener(c, &state, fields, rows, finance.ScreenerOptions{}) + if r.Changed || r.Save || r.Table.Changed() || r.Table.Submitted() || r.Table.SortChanged() || r.Table.Retried() { + fmt.Println(state.Conditions) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleMarketOverview() { + indices := []finance.IndexQuote{{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.MarketOverview(c, indices, finance.MarketOverviewOptions{Breadth: finance.MarketBreadth{Advancing: 2100, Declining: 1400, Unchanged: 300}}) + if r.Chosen >= 0 { + fmt.Println(indices[r.Chosen].Name) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleIndexCard() { + quote := finance.IndexQuote{Name: "S&P 500", Price: 5751.07, Change: 51.13, ChangePercent: 0.9, Series: []float64{5700, 5730, 5720, 5751}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.IndexCard(c, quote, finance.IndexCardOptions{Clickable: true}) + if r.Changed() { + fmt.Println(quote.Name) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleMarketStatus() { + now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.MarketStatus(c, finance.MarketOpen, finance.MarketStatusOptions{Market: "NYSE", Now: now, Next: now.Add(5 * time.Hour), NextPhase: finance.MarketAfterHours}) + } + ui.Render(view, 960, 440, 1) +} + +func ExampleTradingSessionClock() { + now := time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC) + markets := []finance.SessionMarket{{Name: "NYSE", Location: time.FixedZone("EDT", -4*3600), Sessions: []finance.TradingSession{{Phase: finance.MarketOpen, Start: 570, End: 960}}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.TradingSessionClock(c, markets, finance.TradingSessionClockOptions{Now: now}) + } + ui.Render(view, 960, 440, 1) +} + +func ExampleEconomicCalendar() { + var state finance.EconomicCalendarState + rows := []finance.EconomicEvent{{ID: "jobs", Country: "US", Title: "Payrolls", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Importance: 3, Forecast: "150K", Previous: "142K", Actual: "254K"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.EconomicCalendar(c, &state, rows, finance.EconomicCalendarOptions{}) + if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { + fmt.Println(state.Table.Selected()) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleEarningsCalendar() { + var state data.DataTableState[string] + rows := []finance.EarningsEvent{{ID: "a", Symbol: "AAPL", Name: "Apple", Date: core.Date{Year: 2026, Month: 10, Day: 30}, Timing: finance.EarningsAfterClose, EPSEstimate: 1.6, EPSActual: 1.64}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.EarningsCalendar(c, &state, rows, finance.EarningsCalendarOptions{}) + if r.Changed() || r.Submitted() || r.SortChanged() || r.Retried() { + fmt.Println(state.Selected()) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleNewsFeed() { + var state finance.NewsFeedState + rows := []finance.NewsItem{{ID: "a", Title: "Apple unveils new chips", Source: "Wire", Time: time.Date(2026, 10, 5, 14, 30, 0, 0, time.UTC), Symbols: []string{"AAPL"}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.NewsFeed(c, &state, rows, finance.NewsFeedOptions{}) + if r.Opened != "" || r.Symbol != "" || r.Retry { + fmt.Println(r.Opened, r.Symbol) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleSentimentGauge() { + value := 72.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.SentimentGauge(c, value, finance.SentimentGaugeOptions{Label: "Fear & Greed"}) + } + ui.Render(view, 960, 440, 1) +} + +func ExamplePortfolioSummary() { + account := "" + accounts := []finance.PortfolioAccount{{ID: "a", Name: "Brokerage", Total: 120000, DayPnL: 1250, TotalReturn: 20000, Cost: 100000}, {ID: "b", Name: "Retirement", Total: 80000, DayPnL: -300, TotalReturn: 5000, Cost: 75000}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.PortfolioSummary(c, &account, accounts, finance.PortfolioSummaryOptions{}) + if r.Changed() { + fmt.Println(account) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleAssetAllocationChart() { + items := []finance.AllocationItem{{Name: "Stocks", Value: 60, Target: 50}, {Name: "Bonds", Value: 30, Target: 40}, {Name: "Cash", Value: 10, Target: 10}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.AssetAllocationChart(c, items, finance.AssetAllocationChartOptions{Frame: chart.ChartFrame{Label: "Allocation"}}) + if r.Changed() || r.Retried() { + fmt.Println(r.Point) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExamplePerformanceChart() { + values := []float64{100, 110, 95, 120} + benchmark := []float64{100, 105, 108, 112} + times := []time.Time{time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC), time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.PerformanceChart(c, values, finance.PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: times, Benchmark: benchmark}) + if r.Changed() || r.Retried() { + fmt.Println(r.Point) + } + if name, ok := r.Toggled(); ok { + fmt.Println(name) + } + } + ui.Render(view, 960, 440, 1) +} + +func ExampleCurrencyConverter() { + state := finance.CurrencyConverterState{Amount: 100, From: "USD", To: "JPY"} + currencies := []finance.Currency{{Code: "USD", Name: "Dollar", Decimals: 2}, {Code: "JPY", Name: "Yen", Decimals: 0}} + rate := 149.37 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.CurrencyConverter(c, &state, currencies, finance.CurrencyConverterOptions{Rate: rate}) + if r.PairChanged { + rate = 0 + fmt.Println(state.From, state.To) + } + } + ui.Render(view, 960, 440, 1) +} diff --git a/finance/example_trading_test.go b/finance/example_trading_test.go new file mode 100644 index 0000000..e0f464f --- /dev/null +++ b/finance/example_trading_test.go @@ -0,0 +1,223 @@ +package finance_test + +import ( + "fmt" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/finance" +) + +func ExampleTickerTape() { + items := []finance.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.TickerTape(c, items, finance.TickerTapeOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleQuoteCard() { + quote := finance.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.QuoteCard(c, quote, finance.QuoteCardOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePriceText() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + core.UseTrend(c, core.RedUp) + finance.PriceText(c, 18234.5, finance.PriceTextOptions{Compare: true, Reference: 18000}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePriceChangeBadge() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.PriceChangeBadge(c, -5.62, -1.35, finance.PriceChangeBadgeOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOrderBook() { + var state finance.OrderBookState + book := finance.OrderBookData{ + Bids: []finance.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}}, + Asks: []finance.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if price, side, ok := finance.OrderBook(c, &state, book, finance.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok { + fmt.Println("order at", price, side) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLevel2Quotes() { + data := finance.Level2Data{ + Bids: []finance.L2Level{{Price: 100.00, Size: 500, Orders: 12}}, + Asks: []finance.L2Level{{Price: 100.01, Size: 200, Orders: 3}}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.Level2Quotes(c, data, finance.Level2QuotesOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTimeAndSales() { + var state finance.TimeAndSalesState + trades := []finance.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: finance.SideBuy}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.TimeAndSales(c, &state, trades, finance.TimeAndSalesOptions{LargeSize: 1000}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDepthLadder() { + var state finance.DepthLadderState + book := finance.OrderBookData{Bids: []finance.BookLevel{{Price: 100, Size: 5}}, Asks: []finance.BookLevel{{Price: 100.01, Size: 2}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if price, side, ok := finance.DepthLadder(c, &state, 100, book, finance.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok { + fmt.Println("place", side, "at", price) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSpreadIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.SpreadIndicator(c, 99.95, 100.05, finance.SpreadIndicatorOptions{WarnPercent: 0.5}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBidAskBar() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.BidAskBar(c, 6234, 3820, finance.BidAskBarOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOrderEntry() { + var state finance.OrderEntryState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if order, ok := finance.OrderEntry(c, &state, finance.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok { + fmt.Println("place", order.Quantity) + state.EndSubmit("") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleQuickTradeButtons() { + quantity := 10.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if side, q, ok := finance.QuickTradeButtons(c, &quantity, finance.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok { + fmt.Println(side, q) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePositionTable() { + var state data.DataTableState[string] + rows := []finance.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := finance.PositionTable(c, &state, rows, finance.PositionTableOptions{}).Acted(); ok { + fmt.Println("close", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOrderTable() { + var state finance.OrderTableState + rows := []finance.OrderRow{{ID: "o1", Symbol: "MUJC", Side: finance.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: finance.OrderPartial}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if id, ok := finance.OrderTable(c, &state, rows, finance.OrderTableOptions{}).Acted(); ok { + fmt.Println("cancel", id) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTradeHistoryTable() { + var state finance.TradeHistoryTableState + rows := []finance.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: finance.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if fills, ok := finance.TradeHistoryTable(c, &state, rows, finance.TradeHistoryTableOptions{}).Exported(); ok { + fmt.Println("export", len(fills)) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePnLDisplay() { + hidden := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.PnLDisplay(c, finance.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, finance.PnLDisplayOptions{Currency: "$"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLeverageSlider() { + leverage := 5.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if finance.LeverageSlider(c, &leverage, finance.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() { + fmt.Println("leverage", leverage) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMarginIndicator() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.MarginIndicator(c, 0.86, finance.MarginIndicatorOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRiskMeter() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + finance.RiskMeter(c, 4, finance.RiskMeterOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOrderConfirmDialog() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := finance.OrderConfirmDialog(c, &open, finance.OrderConfirmDialogOptions{ + Lines: []finance.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}}, + Risk: "Leveraged products can lose more than the deposit.", + Acknowledge: "I understand the risk", + }) + if r.Confirmed { + fmt.Println("send order") + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/greeks_table.go b/finance/greeks_table.go similarity index 77% rename from greeks_table.go rename to finance/greeks_table.go index c2367ff..dfc5b74 100644 --- a/greeks_table.go +++ b/finance/greeks_table.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OptionGreeks are an option's sensitivities. @@ -36,21 +38,21 @@ func GreeksTable(c *ui.Context, rows []GreeksRow, opts GreeksTableOptions) *ui.E panic("mujica: GreeksTable Decimals is negative") } dec := cmp.Or(opts.Decimals, 4) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() col := ui.Column(c).Gap(u * 2) col.Children(func() { g := ui.Grid(c).ColumnTracks(ui.Fr(2), ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1)). GapX(u*2).Padding(u*2, u*3).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). - Role(ui.RoleTable).Label(msg(c, "greeks_table.title")) + Role(ui.RoleTable).Label(core.Msg(c, "greeks_table.title")) g.Children(func() { - ui.Text(c, msg(c, "greeks_table.contract")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, core.Msg(c, "greeks_table.contract")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)). MinHeight(d.ControlHeight()).Role(ui.RoleColumnHeader) for i, name := range greekNames { h := ui.Row(c).Key(name).Gap(2).AlignItems(ui.Center).Justify(ui.End).MinHeight(d.ControlHeight()). - Role(ui.RoleColumnHeader).Label(name).Description(msg(c, "greeks_table.note_"+greekKey(i))) - h.Tooltip(msg(c, "greeks_table.note_"+greekKey(i))) + Role(ui.RoleColumnHeader).Label(name).Description(core.Msg(c, "greeks_table.note_"+greekKey(i))) + h.Tooltip(core.Msg(c, "greeks_table.note_"+greekKey(i))) h.Children(func() { ui.Text(c, name).SingleLine().Font(theme.SerifFont).TextColor(k.Text) ui.Icon(c, icons.Must("info")).Size(12, 12).TextColor(k.TextMuted) @@ -67,7 +69,7 @@ func GreeksTable(c *ui.Context, rows []GreeksRow, opts GreeksTableOptions) *ui.E for i, v := range vals { s := "—" if !math.IsNaN(v) { - s = statisticSigned(v, dec) + s = data.StatisticSigned(v, dec) } ui.Text(c, s).Key([2]int{ri, i}).JustifySelf(ui.End).FontFeatures("tnum").TextColor(k.Text). Role(ui.RoleCell).Label(greekNames[i] + " " + s) @@ -78,8 +80,8 @@ func GreeksTable(c *ui.Context, rows []GreeksRow, opts GreeksTableOptions) *ui.E if opts.Notes { ui.Column(c).Gap(u).Children(func() { for i, name := range greekNames { - ui.RichText(c, ui.Span{Text: name + " ", Weight: 700}, ui.Span{Text: msg(c, "greeks_table.note_"+greekKey(i))}). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.RichText(c, ui.Span{Text: name + " ", Weight: 700}, ui.Span{Text: core.Msg(c, "greeks_table.note_"+greekKey(i))}). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) } diff --git a/greeks_table_test.go b/finance/greeks_table_test.go similarity index 94% rename from greeks_table_test.go rename to finance/greeks_table_test.go index 217e132..11c7298 100644 --- a/greeks_table_test.go +++ b/finance/greeks_table_test.go @@ -1,10 +1,12 @@ -package mujica +package finance import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func greeksRows() []GreeksRow { @@ -42,7 +44,7 @@ func TestGreeksTableNotes(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) GreeksTable(c, nil, GreeksTableOptions{Decimals: -1}) }, 100, 100, 1) } diff --git a/finance/helpers_test.go b/finance/helpers_test.go new file mode 100644 index 0000000..a3e71c0 --- /dev/null +++ b/finance/helpers_test.go @@ -0,0 +1,31 @@ +package finance + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +func chatTester(w, h int, view func(c *ui.Context)) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + ui.Column(c).Padding(10).Fill().Children(func() { view(c) }) + }, w, h) +} + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/index_card.go b/finance/index_card.go similarity index 76% rename from index_card.go rename to finance/index_card.go index dc682cf..2b34c5a 100644 --- a/index_card.go +++ b/finance/index_card.go @@ -1,12 +1,14 @@ -package mujica +package finance import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type IndexQuote struct { @@ -27,13 +29,13 @@ type IndexCardOptions struct { Disabled bool } -func IndexCard(c *ui.Context, q IndexQuote, opts IndexCardOptions) ChoiceResult { +func IndexCard(c *ui.Context, q IndexQuote, opts IndexCardOptions) core.ChoiceResult { if q.Name == "" { panic("mujica: IndexCard needs a Name") } - dec := priceDecimals("IndexCard", opts.Decimals) - k := Tokens(c) - u := Density(c).Unit() + dec := core.PriceDecimals("IndexCard", opts.Decimals) + k := core.Tokens(c) + u := core.Density(c).Unit() var e *ui.Element ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { if opts.Clickable { @@ -55,12 +57,12 @@ func IndexCard(c *ui.Context, q IndexQuote, opts IndexCardOptions) ChoiceResult PriceChangeBadge(c, q.Change, q.ChangePercent, PriceChangeBadgeOptions{Decimals: dec}) }) if len(q.Series) > 0 { - Sparkline(c, q.Series, SparklineOptions{Frame: ChartFrame{Label: q.Name}, Width: 120}) + chart.Sparkline(c, q.Series, chart.SparklineOptions{Frame: chart.ChartFrame{Label: q.Name}, Width: 120}) } }) clicked := opts.Clickable && e.Clicked() && !e.IsDisabled() if clicked { slog.Debug("mujica: index_card click", "name", q.Name) } - return ChoiceResult{Element: e, changed: clicked} + return core.NewChoiceResult(e, clicked) } diff --git a/index_card_test.go b/finance/index_card_test.go similarity index 98% rename from index_card_test.go rename to finance/index_card_test.go index b1c9e4e..f299d29 100644 --- a/index_card_test.go +++ b/finance/index_card_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestIndexCardQuoteAndKeyboard(t *testing.T) { diff --git a/level2_quotes.go b/finance/level2_quotes.go similarity index 70% rename from level2_quotes.go rename to finance/level2_quotes.go index 0df1966..b945aab 100644 --- a/level2_quotes.go +++ b/finance/level2_quotes.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // L2Level is a level's price, size and order count. @@ -39,7 +41,7 @@ type level2Mark struct { // Level2Quotes lists bid and ask levels; changes flash and mark. func Level2Quotes(c *ui.Context, data Level2Data, opts Level2QuotesOptions) *ui.Element { - dec := priceDecimals("Level2Quotes", opts.Decimals) + dec := core.PriceDecimals("Level2Quotes", opts.Decimals) if opts.SizeDecimals < 0 || opts.Levels < 0 { panic("mujica: Level2Quotes SizeDecimals and Levels cannot be negative") } @@ -47,8 +49,8 @@ func Level2Quotes(c *ui.Context, data Level2Data, opts Level2QuotesOptions) *ui. if n == 0 { n = 5 } - u := Density(c).Unit() - e := ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Role(ui.RoleTable).Label(msg(c, "level2_quotes.label")) + u := core.Density(c).Unit() + e := ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Role(ui.RoleTable).Label(core.Msg(c, "level2_quotes.label")) marks := ui.Local(e, "marks", func() map[string]*level2Mark { return map[string]*level2Mark{} }) e.Children(func() { for _, side := range []OrderSide{SideBuy, SideSell} { @@ -72,9 +74,9 @@ func Level2Quotes(c *ui.Context, data Level2Data, opts Level2QuotesOptions) *ui. // level2Row is one level with its move mark. func level2Row(c *ui.Context, marks map[string]*level2Mark, side OrderSide, i int, l *L2Level, dec, sizeDec int) { - k := Tokens(c) - d := Density(c) - name := fmt.Sprintf(msg(c, fmt.Sprintf("level2_quotes.side%d", side)), i+1) + k := core.Tokens(c) + d := core.Density(c) + name := fmt.Sprintf(core.Msg(c, fmt.Sprintf("level2_quotes.side%d", side)), i+1) key := fmt.Sprint(side, i) m := marks[key] if m == nil { @@ -85,7 +87,7 @@ func level2Row(c *ui.Context, marks map[string]*level2Mark, side OrderSide, i in Radius(theme.ControlRadius).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border) if l == nil { row.Children(func() { - ui.Text(c, name).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Width(28) + ui.Text(c, name).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Width(28) ui.Text(c, "—").TextColor(k.TextMuted) }) return @@ -94,27 +96,27 @@ func level2Row(c *ui.Context, marks map[string]*level2Mark, side OrderSide, i in // A new price is a new level, not a move. m.dir, m.at = 0, c.Now() if l.Price == m.price { - m.dir = trendSign(l.Size - m.size) + m.dir = core.TrendSign(l.Size - m.size) } slog.Debug("mujica: level2_quotes level changed", "level", name) } m.price, m.size, m.seen = l.Price, l.Size, true - if dur := Motion(c, priceTextFlash); dur > 0 && !m.at.IsZero() { + if dur := core.Motion(c, priceTextFlash); dur > 0 && !m.at.IsZero() { if t := c.Now().Sub(m.at); t < dur { row.Background(k.Selection.Alpha(1 - float32(float64(t)/float64(dur)))) c.AnimationFrame() } } - col := trendColor(c, side.sign()) - sizeText := statisticFormat(l.Size, sizeDec) - orders := fmt.Sprintf(msg(c, "level2_quotes.orders"), l.Orders) - row.Role(ui.RoleRow).Label(name + " " + statisticFormat(l.Price, dec) + " " + sizeText + " " + orders) + col := core.TrendColor(c, side.sign()) + sizeText := data.StatisticFormat(l.Size, sizeDec) + orders := fmt.Sprintf(core.Msg(c, "level2_quotes.orders"), l.Orders) + row.Role(ui.RoleRow).Label(name + " " + data.StatisticFormat(l.Price, dec) + " " + sizeText + " " + orders) row.Children(func() { - ui.Text(c, name).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Width(28) - ui.Text(c, statisticFormat(l.Price, dec)).TextColor(col).FontFeatures("tnum").FontWeight(600).Grow(1).Basis(0).SingleLine() + ui.Text(c, name).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Width(28) + ui.Text(c, data.StatisticFormat(l.Price, dec)).TextColor(col).FontFeatures("tnum").FontWeight(600).Grow(1).Basis(0).SingleLine() ui.Row(c).Gap(2).AlignItems(ui.Center).Justify(ui.End).Grow(1).Basis(0).Children(func() { if m.dir != 0 { - ui.Icon(c, trendIcon(m.dir)).Size(12, 12).TextColor(trendColor(c, m.dir)) + ui.Icon(c, core.TrendIcon(m.dir)).Size(12, 12).TextColor(core.TrendColor(c, m.dir)) } ui.Text(c, sizeText).FontFeatures("tnum").SingleLine() }) diff --git a/level2_quotes_test.go b/finance/level2_quotes_test.go similarity index 90% rename from level2_quotes_test.go rename to finance/level2_quotes_test.go index 492b5fe..2d1e155 100644 --- a/level2_quotes_test.go +++ b/finance/level2_quotes_test.go @@ -1,10 +1,12 @@ -package mujica +package finance import ( "bytes" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func level2Sample() Level2Data { @@ -15,7 +17,7 @@ func level2Sample() Level2Data { } func TestLevel2QuotesLevels(t *testing.T) { - tt := tradingTester(GreenUp, 600, 300, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 600, 300, func(c *ui.Context) { ui.Box(c).Width(560).Children(func() { Level2Quotes(c, level2Sample(), Level2QuotesOptions{Levels: 3}) }) }) for _, name := range []string{"B1 100.00 500 12 orders", "S2 100.02 900 21 orders"} { @@ -32,7 +34,7 @@ func TestLevel2QuotesLevels(t *testing.T) { func TestLevel2QuotesHighlightsChangedLevel(t *testing.T) { data := level2Sample() var marks *map[string]*level2Mark - tt := tradingTester(GreenUp, 600, 300, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 600, 300, func(c *ui.Context) { e := Level2Quotes(c, data, Level2QuotesOptions{Levels: 2}) marks = ui.Local(e, "marks", func() map[string]*level2Mark { return nil }) }) diff --git a/leverage_slider.go b/finance/leverage_slider.go similarity index 78% rename from leverage_slider.go rename to finance/leverage_slider.go index dd2e101..06db9e8 100644 --- a/leverage_slider.go +++ b/finance/leverage_slider.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LeverageSliderOptions configure a LeverageSlider. @@ -30,7 +33,7 @@ type leverageSliderState struct { } // LeverageSlider drafts a leverage; Apply sets *value. -func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) ChoiceResult { +func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) core.ChoiceResult { levels := opts.Levels if levels == nil { levels = []float64{1, 2, 3, 5, 10, 20, 50, 100} @@ -41,13 +44,13 @@ func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) C if opts.Warning < 0 || opts.Danger < 0 || opts.Danger != 0 && opts.Danger <= opts.Warning { panic("mujica: LeverageSlider Danger must be above Warning") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "leverage_slider.label") + label = core.Msg(c, "leverage_slider.label") } - var r ChoiceResult + var r core.ChoiceResult r.Element = ui.Column(c).Gap(d.Unit() * 2).MinWidth(260).Disabled(opts.Disabled) st := ui.Local(r.Element, "lev", func() leverageSliderState { return leverageSliderState{draft: *value, seen: *value} }) if *value != st.seen { @@ -60,9 +63,9 @@ func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) C zone := func(v float64) (string, ui.Color, string) { switch { case opts.Danger > 0 && v >= opts.Danger: - return msg(c, "leverage_slider.danger"), k.Danger, "circle-alert" + return core.Msg(c, "leverage_slider.danger"), k.Danger, "circle-alert" case opts.Warning > 0 && v >= opts.Warning: - return msg(c, "leverage_slider.warning"), k.Warning, "triangle-alert" + return core.Msg(c, "leverage_slider.warning"), k.Warning, "triangle-alert" } return "", k.TextMuted, "" } @@ -70,18 +73,18 @@ func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) C note, col, icon := zone(st.draft) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, label).TextColor(k.TextMuted).Grow(1) - ui.Text(c, leverageText(st.draft)).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum") + ui.Text(c, leverageText(st.draft)).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum") }) leverageTrack(c, st, levels, disabled, zone) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).MinHeight(d.ControlHeight()).Children(func() { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(col).Grow(1).Children(func() { if icon != "" { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).Role(ui.RoleStatus) + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).Role(ui.RoleStatus) } }) - if Button(c, msg(c, "leverage_slider.apply"), ButtonOptions{Disabled: disabled || st.draft == *value}).Clicked() && !disabled { - *value, st.seen, r.changed = st.draft, st.draft, true + if input.Button(c, core.Msg(c, "leverage_slider.apply"), input.ButtonOptions{Disabled: disabled || st.draft == *value}).Clicked() && !disabled { + *value, st.seen, r = st.draft, st.draft, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: leverage_slider applied", "leverage", *value) } }) @@ -89,11 +92,11 @@ func LeverageSlider(c *ui.Context, value *float64, opts LeverageSliderOptions) C return r } -func leverageText(v float64) string { return statisticFormat(v, 0) + "×" } +func leverageText(v float64) string { return data.StatisticFormat(v, 0) + "×" } // leverageTrack is the slider with zones, ticks and thumb. func leverageTrack(c *ui.Context, st *leverageSliderState, levels []float64, disabled bool, zone func(float64) (string, ui.Color, string)) { - k := Tokens(c) + k := core.Tokens(c) n := len(levels) at := slices.Index(levels, st.draft) set := func(i int) { @@ -105,7 +108,7 @@ func leverageTrack(c *ui.Context, st *leverageSliderState, levels []float64, dis } const pad = 10 t := ui.Box(c).Height(theme.MinHitSize+16).FillWidth().Padding(0, pad).Focusable().Role(ui.RoleSlider). - Label(msg(c, "leverage_slider.label")).Description(leverageText(st.draft)) + Label(core.Msg(c, "leverage_slider.label")).Description(leverageText(st.draft)) t.FocusRing(false) if !disabled { switch { @@ -139,7 +142,7 @@ func leverageTrack(c *ui.Context, st *leverageSliderState, levels []float64, dis } p.Line(x0, y, x0+w, y, 2, k.ControlBorder) p.Line(x0, y, xs(at), y, 2, k.AccentText) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) widest, _ := p.MeasureText(0, ui.Span{Text: leverageText(levels[n-1]), Size: size}) // Crowded labels skip levels, counted back from the last. every := max(1, int(math.Ceil(float64((widest+8)/(w/float32(n-1)))))) diff --git a/leverage_slider_test.go b/finance/leverage_slider_test.go similarity index 94% rename from leverage_slider_test.go rename to finance/leverage_slider_test.go index fbbf4ce..09b150e 100644 --- a/leverage_slider_test.go +++ b/finance/leverage_slider_test.go @@ -1,11 +1,12 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type leverageProbe struct { @@ -16,7 +17,7 @@ type leverageProbe struct { func (p *leverageProbe) tester(density theme.Density) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: density}) + core.Use(c, core.Settings{Density: density}) ui.Column(c).Padding(10).Children(func() { if LeverageSlider(c, &p.value, p.opts).Changed() { p.applied++ diff --git a/margin_indicator.go b/finance/margin_indicator.go similarity index 59% rename from margin_indicator.go rename to finance/margin_indicator.go index 4c59696..805a8a4 100644 --- a/margin_indicator.go +++ b/finance/margin_indicator.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MarginIndicatorOptions configure a MarginIndicator. Zero lines mean 80% and 100%. @@ -27,29 +29,29 @@ func MarginIndicator(c *ui.Context, ratio float64, opts MarginIndicatorOptions) if !(warn > 0 && warn < liq) || ratio < 0 || math.IsNaN(ratio) { panic(fmt.Sprintf("mujica: MarginIndicator ratio %v, lines %v and %v: need 0 < Warning < Liquidation", ratio, warn, liq)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() label := opts.Label if label == "" { - label = msg(c, "margin_indicator.label") + label = core.Msg(c, "margin_indicator.label") } - status, col, icon := msg(c, "margin_indicator.safe"), k.Success, "circle-check" + status, col, icon := core.Msg(c, "margin_indicator.safe"), k.Success, "circle-check" switch { case ratio >= liq: - status, col, icon = msg(c, "margin_indicator.liquidation"), k.Danger, "circle-x" + status, col, icon = core.Msg(c, "margin_indicator.liquidation"), k.Danger, "circle-x" case ratio >= warn: - status, col, icon = msg(c, "margin_indicator.warning"), k.Warning, "triangle-alert" + status, col, icon = core.Msg(c, "margin_indicator.warning"), k.Warning, "triangle-alert" } - pct := func(v float64) string { return statisticFormat(v*100, 1) + "%" } + pct := func(v float64) string { return data.StatisticFormat(v*100, 1) + "%" } top := math.Max(liq*1.25, ratio) e := ui.Column(c).Gap(u * 2).MinWidth(220).Role(ui.RoleMeter).Label(label). - Description(fmt.Sprintf(msg(c, "margin_indicator.spoken"), pct(ratio), status, pct(warn), pct(liq))) + Description(fmt.Sprintf(core.Msg(c, "margin_indicator.spoken"), pct(ratio), status, pct(warn), pct(liq))) e.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, pct(ratio)).FontSize(FontSize(c, theme.H3Size)).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Text(c, pct(ratio)).FontSize(core.FontSize(c, theme.H3Size)).FontFeatures("tnum").FontWeight(600).SingleLine() ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(col).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, status).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() + ui.Text(c, status).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() }) }) ui.Box(c).Height(14).FillWidth().Draw(func(p *ui.Painter, r ui.Rect) { @@ -63,8 +65,8 @@ func MarginIndicator(c *ui.Context, ratio float64, opts MarginIndicatorOptions) } }) ui.Row(c).Gap(u * 4).Wrap().Children(func() { - ui.Text(c, msg(c, "margin_indicator.warning_line")+" "+pct(warn)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) - ui.Text(c, msg(c, "margin_indicator.liquidation_line")+" "+pct(liq)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "margin_indicator.warning_line")+" "+pct(warn)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "margin_indicator.liquidation_line")+" "+pct(liq)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) }) return e diff --git a/margin_indicator_test.go b/finance/margin_indicator_test.go similarity index 77% rename from margin_indicator_test.go rename to finance/margin_indicator_test.go index 0eefd39..ab891c7 100644 --- a/margin_indicator_test.go +++ b/finance/margin_indicator_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMarginIndicatorStates(t *testing.T) { ratio := 0.42 - tt := tradingTester(GreenUp, 400, 160, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 160, func(c *ui.Context) { ui.Box(c).Width(300).Children(func() { MarginIndicator(c, ratio, MarginIndicatorOptions{}) }) }) for _, tc := range []struct { @@ -29,7 +31,7 @@ func TestMarginIndicatorStates(t *testing.T) { t.Fatal("warning above liquidation accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { MarginIndicator(c, 1, MarginIndicatorOptions{Warning: 2, Liquidation: 1}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { MarginIndicator(c, 1, MarginIndicatorOptions{Warning: 2, Liquidation: 1}) }) } func TestMarginIndicatorSnapshot(t *testing.T) { diff --git a/market_overview.go b/finance/market_overview.go similarity index 72% rename from market_overview.go rename to finance/market_overview.go index 41c8ba0..b74436b 100644 --- a/market_overview.go +++ b/finance/market_overview.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MarketBreadth counts the issues that rose, fell and stayed flat. @@ -31,8 +32,8 @@ func MarketOverview(c *ui.Context, indices []IndexQuote, opts MarketOverviewOpti if b.Advancing < 0 || b.Declining < 0 || b.Unchanged < 0 { panic("mujica: MarketOverview breadth counts must not be negative") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() v := MarketOverviewView{Chosen: -1} v.Element = ui.Column(c).Gap(u * 3).Disabled(opts.Disabled) v.Element.Children(func() { @@ -49,8 +50,8 @@ func MarketOverview(c *ui.Context, indices []IndexQuote, opts MarketOverviewOpti marketBreadthBar(c, b, total) } if !opts.AsOf.IsZero() { - ui.Text(c, fmt.Sprintf(msg(c, "market_overview.as_of"), opts.AsOf.Format("2006-01-02 15:04"))). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "market_overview.as_of"), opts.AsOf.Format("2006-01-02 15:04"))). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) if v.Chosen >= 0 { @@ -60,10 +61,10 @@ func MarketOverview(c *ui.Context, indices []IndexQuote, opts MarketOverviewOpti } func marketBreadthBar(c *ui.Context, b MarketBreadth, total int) { - k := Tokens(c) - u := Density(c).Unit() - up, down := trendColor(c, 1), trendColor(c, -1) - label := fmt.Sprintf(msg(c, "market_overview.breadth"), b.Advancing, b.Declining, b.Unchanged) + k := core.Tokens(c) + u := core.Density(c).Unit() + up, down := core.TrendColor(c, 1), core.TrendColor(c, -1) + label := fmt.Sprintf(core.Msg(c, "market_overview.breadth"), b.Advancing, b.Declining, b.Unchanged) ui.Column(c).Gap(u).MaxWidth(640).FillWidth().Role(ui.RoleMeter).Label(label).Children(func() { ui.Box(c).FillWidth().Height(10).Draw(func(p *ui.Painter, r ui.Rect) { x := r.X @@ -82,9 +83,9 @@ func marketBreadthBar(c *ui.Context, b MarketBreadth, total int) { sign int key string }{{b.Advancing, 1, "market_overview.up"}, {b.Unchanged, 0, "market_overview.flat"}, {b.Declining, -1, "market_overview.down"}} { - ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(trendColor(c, part.sign)).Children(func() { - ui.Icon(c, trendIcon(part.sign)).Size(12, 12) - ui.Text(c, fmt.Sprintf(msg(c, part.key), part.n)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(core.TrendColor(c, part.sign)).Children(func() { + ui.Icon(c, core.TrendIcon(part.sign)).Size(12, 12) + ui.Text(c, fmt.Sprintf(core.Msg(c, part.key), part.n)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") }) } }) diff --git a/market_overview_test.go b/finance/market_overview_test.go similarity index 98% rename from market_overview_test.go rename to finance/market_overview_test.go index f988965..ad10545 100644 --- a/market_overview_test.go +++ b/finance/market_overview_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestMarketOverviewBreadthStampAndChoice(t *testing.T) { diff --git a/market_status.go b/finance/market_status.go similarity index 69% rename from market_status.go rename to finance/market_status.go index 446cf20..e318417 100644 --- a/market_status.go +++ b/finance/market_status.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type MarketPhase int @@ -38,13 +39,13 @@ type MarketStatusOptions struct { func MarketStatus(c *ui.Context, phase MarketPhase, opts MarketStatusOptions) *ui.Element { marketPhaseCheck(phase) marketPhaseCheck(opts.NextPhase) - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() now := opts.Now if now.IsZero() { now = c.Now() } - word := msg(c, marketPhases[phase].key) + word := core.Msg(c, marketPhases[phase].key) ink := k.TextMuted if phase == MarketOpen { ink = k.Success @@ -55,7 +56,7 @@ func MarketStatus(c *ui.Context, phase MarketPhase, opts MarketStatusOptions) *u } next := "" if !opts.Next.IsZero() { - next = fmt.Sprintf(msg(c, "market_status.next"), msg(c, marketPhases[opts.NextPhase].key), opts.Next.Format("15:04"), + next = fmt.Sprintf(core.Msg(c, "market_status.next"), core.Msg(c, marketPhases[opts.NextPhase].key), opts.Next.Format("15:04"), marketStatusIn(c, opts.Next.Sub(now))) label += ", " + next @@ -65,12 +66,12 @@ func MarketStatus(c *ui.Context, phase MarketPhase, opts MarketStatusOptions) *u ui.Row(c).Gap(u).AlignItems(ui.Center).Padding(u/2, u*2).Radius(theme.ControlRadius).Border(theme.BorderWidth, ink).Children(func() { ui.Icon(c, icons.Must(marketPhases[phase].icon)).Size(14, 14).TextColor(ink) if opts.Market != "" { - ui.Text(c, opts.Market).Bold().FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() + ui.Text(c, opts.Market).Bold().FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() } - ui.Text(c, word).FontSize(FontSize(c, theme.CaptionSize)).TextColor(ink).FontWeight(600).SingleLine() + ui.Text(c, word).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(ink).FontWeight(600).SingleLine() }) if next != "" { - ui.Text(c, next).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, next).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if opts.Now.IsZero() && opts.Next.After(now) { @@ -87,11 +88,11 @@ func marketPhaseCheck(p MarketPhase) { func marketStatusIn(c *ui.Context, d time.Duration) string { if d <= 0 { - return msg(c, "market_status.now") + return core.Msg(c, "market_status.now") } m := int(d.Round(time.Minute) / time.Minute) if m >= 60 { - return fmt.Sprintf(msg(c, "market_status.hm"), m/60, m%60) + return fmt.Sprintf(core.Msg(c, "market_status.hm"), m/60, m%60) } - return fmt.Sprintf(msg(c, "market_status.m"), max(m, 1)) + return fmt.Sprintf(core.Msg(c, "market_status.m"), max(m, 1)) } diff --git a/market_status_test.go b/finance/market_status_test.go similarity index 98% rename from market_status_test.go rename to finance/market_status_test.go index 103e048..81dabab 100644 --- a/market_status_test.go +++ b/finance/market_status_test.go @@ -1,9 +1,10 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" "time" + + "github.com/egoist/mygo/ui" ) func TestMarketStatusPhasesAndNext(t *testing.T) { diff --git a/markets_fixture_test.go b/finance/markets_fixture_test.go similarity index 70% rename from markets_fixture_test.go rename to finance/markets_fixture_test.go index 1e3251d..341fa01 100644 --- a/markets_fixture_test.go +++ b/finance/markets_fixture_test.go @@ -1,10 +1,14 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "math" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // mktNow is the fixed clock of the markets tests: 10:30 in New York. @@ -41,11 +45,11 @@ func mktTimes(n int) []time.Time { return out } -func mktHeat() HeatNode { - return HeatNode{Name: "Market", Children: []HeatNode{ - {Name: "Technology", Children: []HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}, {Name: "NVDA", Cap: 2200, Change: 3.4}}}, - {Name: "Energy", Children: []HeatNode{{Name: "XOM", Cap: 450, Change: -2.1}, {Name: "CVX", Cap: 300, Change: -1.4}}}, - {Name: "Health", Children: []HeatNode{{Name: "LLY", Cap: 700, Change: 0.4}, {Name: "JNJ", Cap: 380, Change: 0}}}, +func mktHeat() chart.HeatNode { + return chart.HeatNode{Name: "Market", Children: []chart.HeatNode{ + {Name: "Technology", Children: []chart.HeatNode{{Name: "AAPL", Cap: 3000, Change: 1.2}, {Name: "MSFT", Cap: 2800, Change: -0.6}, {Name: "NVDA", Cap: 2200, Change: 3.4}}}, + {Name: "Energy", Children: []chart.HeatNode{{Name: "XOM", Cap: 450, Change: -2.1}, {Name: "CVX", Cap: 300, Change: -1.4}}}, + {Name: "Health", Children: []chart.HeatNode{{Name: "LLY", Cap: 700, Change: 0.4}, {Name: "JNJ", Cap: 380, Change: 0}}}, }} } @@ -67,5 +71,5 @@ func marketsPanic(t *testing.T, view func(*ui.Context)) { t.Fatal("expected panic") } }() - ui.Render(func(c *ui.Context) { Use(c, Settings{}); view(c) }, 960, 500, 1) + ui.Render(func(c *ui.Context) { core.Use(c, core.Settings{}); view(c) }, 960, 500, 1) } diff --git a/markets_regression_test.go b/finance/markets_regression_test.go similarity index 79% rename from markets_regression_test.go rename to finance/markets_regression_test.go index 17b14d8..c5b3019 100644 --- a/markets_regression_test.go +++ b/finance/markets_regression_test.go @@ -1,14 +1,19 @@ -package mujica +package finance import ( "fmt" - "github.com/egoist/mygo/ui" "math" - "mujicaui/internal" - "mujicaui/theme" "strings" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestMarketsAncestorDisabled(t *testing.T) { @@ -17,8 +22,8 @@ func TestMarketsAncestorDisabled(t *testing.T) { var last NewsFeedView items := []NewsItem{{ID: "a", Title: "Alpha", Source: "Wire", Time: mktNow}, {ID: "b", Title: "Beta", Source: "Wire", Time: mktNow}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - ui.Column(c).Height(360).Disabled(disabled).Children(func() { last = NewsFeed(c, &s, items, NewsFeedOptions{Today: DateOf(mktNow)}) }) + core.Use(c, core.Settings{}) + ui.Column(c).Height(360).Disabled(disabled).Children(func() { last = NewsFeed(c, &s, items, NewsFeedOptions{Today: core.DateOf(mktNow)}) }) }, 640, 400) tt.Frame() _ = tt.Click("Alpha") @@ -53,7 +58,7 @@ func TestGreeksTableLarge(t *testing.T) { func TestSymbolSearchLarge(t *testing.T) { rows := make([]MarketSymbol, 100000) for i := range rows { - rows[i] = MarketSymbol{Code: statisticFormat(float64(i), 0), Name: "Company"} + rows[i] = MarketSymbol{Code: data.StatisticFormat(float64(i), 0), Name: "Company"} } s := SymbolSearchState{Query: "company"} tt := chatTester(640, 400, func(c *ui.Context) { SymbolSearch(c, &s, rows, SymbolSearchOptions{}) }) @@ -90,7 +95,7 @@ func TestMarketsLargeViews(t *testing.T) { EconomicCalendar(c, s, econ, EconomicCalendarOptions{}).Element.Height(350) }}, {"earnings", func(c *ui.Context) { - s := ui.Local(c.Root(), "large.earn", func() DataTableState[string] { return DataTableState[string]{} }) + s := ui.Local(c.Root(), "large.earn", func() data.DataTableState[string] { return data.DataTableState[string]{} }) EarningsCalendar(c, s, earn, EarningsCalendarOptions{}).Element.Height(350) }}, {"screener", func(c *ui.Context) { @@ -112,21 +117,21 @@ func TestMarketsLargeViews(t *testing.T) { } } func TestMarketHeatmapLargeGeometry(t *testing.T) { - m := marketHeatMemo{} + m := chart.MarketHeatMemo{} for i := range 100000 { - m.tiles = append(m.tiles, marketHeatTile{name: fmt.Sprint(i), cap: 1, change: 1}) - m.caps = append(m.caps, 1) + m.Tiles = append(m.Tiles, chart.MarketHeatTile{Name: fmt.Sprint(i), Cap: 1, Change: 1}) + m.Caps = append(m.Caps, 1) } - m.layout(m.caps, ui.Rect{W: 640, H: 240}) - if count := len(m.visible) + len(m.small); count > 640*240/16+640/4+240/4 { + m.Layout(m.Caps, ui.Rect{W: 640, H: 240}) + if count := len(m.Visible) + len(m.Small); count > 640*240/16+640/4+240/4 { t.Fatalf("built %d marks", count) } - if len(m.rects) != 100000 { - t.Fatalf("keyboard points %d", len(m.rects)) + if len(m.Rects) != 100000 { + t.Fatalf("keyboard points %d", len(m.Rects)) } - first := &m.rects[0] - m.layout(m.caps, ui.Rect{W: 640, H: 240}) - if first != &m.rects[0] { + first := &m.Rects[0] + m.Layout(m.Caps, ui.Rect{W: 640, H: 240}) + if first != &m.Rects[0] { t.Fatal("idle layout recomputed") } } @@ -142,17 +147,17 @@ func TestMarketsPaletteContrast(t *testing.T) { func TestMarketsChartStatesAndDisabled(t *testing.T) { for _, name := range []string{"payoff", "heatmap", "allocation", "performance"} { t.Run(name, func(t *testing.T) { - for _, status := range []DataStatus{DataLoading, DataFailed} { - frame := ChartFrame{Label: "Chart", Height: 180, Status: status, Error: "Caller failed"} + for _, status := range []data.DataStatus{data.DataLoading, data.DataFailed} { + frame := chart.ChartFrame{Label: "Chart", Height: 180, Status: status, Error: "Caller failed"} retry := false tt := chatTester(960, 420, func(c *ui.Context) { - var r ChartResult + var r chart.ChartResult switch name { case "payoff": r = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: frame}) case "heatmap": path := ui.Local(c.Root(), "path", func() []string { return nil }) - r = MarketHeatmap(c, path, mktHeat(), MarketHeatmapOptions{Frame: frame}) + r = chart.MarketHeatmap(c, path, mktHeat(), chart.MarketHeatmapOptions{Frame: frame}) case "allocation": r = AssetAllocationChart(c, []AllocationItem{{"Stocks", 1, 100}}, AssetAllocationChartOptions{Frame: frame}) case "performance": @@ -160,7 +165,7 @@ func TestMarketsChartStatesAndDisabled(t *testing.T) { } retry = retry || r.Retried() }) - if status == DataFailed { + if status == data.DataFailed { _ = tt.Click("Retry") if !retry { t.Fatal("retry missing") @@ -183,17 +188,17 @@ func TestMarketsLargeChartGeometry(t *testing.T) { } for _, name := range []string{"payoff", "performance", "allocation"} { t.Run(name, func(t *testing.T) { - var geometry *chartMemoEntry + var geometry *chart.ChartMemoEntry tt := chatTester(960, 600, func(c *ui.Context) { - var r ChartResult - frame := ChartFrame{Label: "Large"} + var r chart.ChartResult + frame := chart.ChartFrame{Label: "Large"} switch name { case "payoff": r = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: frame, Points: 100000}) - geometry = ui.Local(r.Element, "mujica.memo.payoff.geometry", func() chartMemoEntry { return chartMemoEntry{} }) + geometry = ui.Local(r.Element, "mujica.memo.payoff.geometry", func() chart.ChartMemoEntry { return chart.ChartMemoEntry{} }) case "performance": r = PerformanceChart(c, values, PerformanceChartOptions{Frame: frame, Times: times}) - geometry = ui.Local(r.Element, "mujica.memo.performance.geometry.Portfolio", func() chartMemoEntry { return chartMemoEntry{} }) + geometry = ui.Local(r.Element, "mujica.memo.performance.geometry.Portfolio", func() chart.ChartMemoEntry { return chart.ChartMemoEntry{} }) case "allocation": r = AssetAllocationChart(c, items, AssetAllocationChartOptions{Frame: frame}) } @@ -203,13 +208,13 @@ func TestMarketsLargeChartGeometry(t *testing.T) { t.Fatalf("built %d texts", len(tt.Texts())) } if geometry != nil { - points := geometry.val.([]int) + points := geometry.Val.([]int) if len(points) > 4*960 { t.Fatalf("built %d points", len(points)) } first := &points[0] tt.Frame() - if first != &geometry.val.([]int)[0] { + if first != &geometry.Val.([]int)[0] { t.Fatal("idle geometry recomputed") } } diff --git a/news_feed.go b/finance/news_feed.go similarity index 78% rename from news_feed.go rename to finance/news_feed.go index 7cc72ab..27b419d 100644 --- a/news_feed.go +++ b/finance/news_feed.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,8 +7,13 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type NewsItem struct { @@ -29,7 +34,7 @@ type NewsFeedState struct { } func newsFeedUnread(s *NewsFeedState, items []NewsItem, version int) int { - key := fmt.Sprint(chartID(items), version, s.readVersion) + key := fmt.Sprint(chart.ChartID(items), version, s.readVersion) if key != s.count.key { s.count.key, s.count.unread = key, 0 for _, it := range items { @@ -44,9 +49,9 @@ func newsFeedUnread(s *NewsFeedState, items []NewsItem, version int) int { // NewsFeedOptions configure a NewsFeed. type NewsFeedOptions struct { // Today decides the time stamps; zero uses the window clock. - Today Date + Today core.Date Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -68,10 +73,10 @@ func NewsFeed(c *ui.Context, s *NewsFeedState, items []NewsItem, opts NewsFeedOp today := opts.Today if today.IsZero() { - today = DateOf(c.Now()) + today = core.DateOf(c.Now()) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var v NewsFeedView v.Element = ui.Column(c).Gap(d.Unit() * 2).Grow(1).MinHeight(0).Disabled(opts.Disabled) opts.Disabled = v.Element.IsDisabled() @@ -85,8 +90,8 @@ func NewsFeed(c *ui.Context, s *NewsFeedState, items []NewsItem, opts NewsFeedOp v.Element.Children(func() { unread := newsFeedUnread(s, items, opts.Version) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "news_feed.unread"), unread)).TextColor(k.TextMuted).Grow(1).Role(ui.RoleStatus) - if Button(c, msg(c, "news_feed.mark_all"), ButtonOptions{Variant: Ghost, Disabled: opts.Disabled || unread == 0}).Clicked() { + ui.Text(c, fmt.Sprintf(core.Msg(c, "news_feed.unread"), unread)).TextColor(k.TextMuted).Grow(1).Role(ui.RoleStatus) + if input.Button(c, core.Msg(c, "news_feed.mark_all"), input.ButtonOptions{Variant: input.Ghost, Disabled: opts.Disabled || unread == 0}).Clicked() { for _, it := range items { s.Read[it.ID] = true } @@ -96,7 +101,7 @@ func NewsFeed(c *ui.Context, s *NewsFeedState, items []NewsItem, opts NewsFeedOp } }) if len(items) == 0 { - v.Retry = dataPlaceholder(c, opts.Status, opts.Error, "", msg(c, "news_feed.empty")) + v.Retry = data.DataPlaceholder(c, opts.Status, opts.Error, "", core.Msg(c, "news_feed.empty")) return } cursor := &s.cursor @@ -107,7 +112,7 @@ func NewsFeed(c *ui.Context, s *NewsFeedState, items []NewsItem, opts NewsFeedOp s.rows.Key = func(i int) any { return items[i].ID } s.rows.Label = func(i int) string { return items[i].Title } s.rows.Selected = cursor - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(items), func(i int) { it := items[i] read := s.Read[it.ID] @@ -129,7 +134,7 @@ func NewsFeed(c *ui.Context, s *NewsFeedState, items []NewsItem, opts NewsFeedOp t.TextColor(k.TextMuted) } ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Wrap().Children(func() { - ui.Text(c, it.Source+" · "+dateSeparatorWhen(c, it.Time, today)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, it.Source+" · "+chat.DateSeparatorWhen(c, it.Time, today)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) for _, sym := range it.Symbols { ui.Row(c).Key(sym).Children(func() { if SymbolBadge(c, MarketSymbol{Code: sym}, SymbolBadgeOptions{Clickable: true, Disabled: opts.Disabled}).Changed() { diff --git a/news_feed_test.go b/finance/news_feed_test.go similarity index 87% rename from news_feed_test.go rename to finance/news_feed_test.go index 2c2c87d..be7f724 100644 --- a/news_feed_test.go +++ b/finance/news_feed_test.go @@ -1,8 +1,11 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestNewsFeedReadSourceSymbolsAndKeyboard(t *testing.T) { @@ -10,7 +13,7 @@ func TestNewsFeedReadSourceSymbolsAndKeyboard(t *testing.T) { var got NewsFeedView rows := []NewsItem{{ID: "a", Title: "Alpha", Source: "Wire", Time: mktNow, Symbols: []string{"AAPL"}}, {ID: "b", Title: "Beta", Source: "Wire", Time: mktNow}} tt := chatTester(640, 420, func(c *ui.Context) { - v := NewsFeed(c, &s, rows, NewsFeedOptions{Today: DateOf(mktNow)}) + v := NewsFeed(c, &s, rows, NewsFeedOptions{Today: core.DateOf(mktNow)}) v.Element.Height(380) if v.Opened != "" || v.Symbol != "" { got = v @@ -42,7 +45,7 @@ func TestNewsFeedSnapshot(t *testing.T) { ns := ui.Local(c.Root(), "snapshot.news_feed", func() NewsFeedState { return NewsFeedState{} }) ui.Column(c).Padding(16).Gap(12).Fill().Children(func() { NewsFeed(c, ns, []NewsItem{{ID: "1", Title: "Apple unveils new chips", Source: "Reuters", Time: mktNow, Symbols: []string{"AAPL"}}, - {ID: "2", Title: "Oil falls as supply grows", Source: "Bloomberg", Time: mktNow.Add(-26 * 3600e9), Symbols: []string{"XOM", "CVX"}}}, NewsFeedOptions{Today: DateOf(mktNow)}).Element.Height(300) + {ID: "2", Title: "Oil falls as supply grows", Source: "Bloomberg", Time: mktNow.Add(-26 * 3600e9), Symbols: []string{"XOM", "CVX"}}}, NewsFeedOptions{Today: core.DateOf(mktNow)}).Element.Height(300) }) }) diff --git a/option_chain.go b/finance/option_chain.go similarity index 76% rename from option_chain.go rename to finance/option_chain.go index efc11f7..64c3a17 100644 --- a/option_chain.go +++ b/finance/option_chain.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OptionQuote is one contract's market. @@ -78,9 +81,9 @@ func OptionChain(c *ui.Context, s *OptionChainState, expiries []string, strikes panic("mujica: OptionChain strikes must ascend") } - dec := priceDecimals("OptionChain", opts.Decimals) - k := Tokens(c) - d := Density(c) + dec := core.PriceDecimals("OptionChain", opts.Decimals) + k := core.Tokens(c) + d := core.Density(c) var v OptionChainView v.Element = ui.Column(c).Gap(d.Unit() * 2).Grow(1).MinHeight(0).Disabled(opts.Disabled) opts.Disabled = v.Element.IsDisabled() @@ -95,13 +98,13 @@ func OptionChain(c *ui.Context, s *OptionChainState, expiries []string, strikes } v.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { - items := make([]SelectOption[string], len(expiries)) + items := make([]input.SelectOption[string], len(expiries)) for i, e := range expiries { - items[i] = SelectOption[string]{Value: e, Label: e} + items[i] = input.SelectOption[string]{Value: e, Label: e} } - v.ExpiryChanged = Select(c, &s.Expiry, items, SelectOptions{Label: msg(c, "option_chain.expiry"), Disabled: opts.Disabled}).Changed() + v.ExpiryChanged = input.Select(c, &s.Expiry, items, input.SelectOptions{Label: core.Msg(c, "option_chain.expiry"), Disabled: opts.Disabled}).Changed() if opts.Spot > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "option_chain.spot"), statisticFormat(opts.Spot, dec))).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "option_chain.spot"), data.StatisticFormat(opts.Spot, dec))).TextColor(k.TextMuted) } }) optionChainHeader(c) @@ -119,33 +122,33 @@ func OptionChain(c *ui.Context, s *OptionChainState, expiries []string, strikes var optionChainCols = [3]string{"option_chain.bid", "option_chain.ask", "option_chain.last"} func optionChainHeader(c *ui.Context) { - t := Tokens(c) - d := Density(c) - size := FontSize(c, theme.CaptionSize) + t := core.Tokens(c) + d := core.Density(c) + size := core.FontSize(c, theme.CaptionSize) ui.Row(c).Padding(0, d.Unit()*3).Children(func() { - ui.Text(c, msg(c, "option_chain.calls")).Grow(1).Basis(0).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) + ui.Text(c, core.Msg(c, "option_chain.calls")).Grow(1).Basis(0).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) ui.Box(c).Width(90) - ui.Text(c, msg(c, "option_chain.puts")).Grow(1).Basis(0).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) + ui.Text(c, core.Msg(c, "option_chain.puts")).Grow(1).Basis(0).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) }) ui.Row(c).Padding(0, d.Unit()*3).Children(func() { for side, title := range []string{"option_chain.calls", "option_chain.puts"} { if side == 1 { - ui.Text(c, msg(c, "option_chain.strike")).Width(90).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) + ui.Text(c, core.Msg(c, "option_chain.strike")).Width(90).TextAlign(ui.Center).FontSize(size).TextColor(t.TextMuted) } ui.Row(c).Grow(1).Basis(0).Children(func() { for _, col := range optionChainCols { - ui.Text(c, msg(c, col)).Grow(1).Basis(0).TextAlign(ui.End).FontSize(size).TextColor(t.TextMuted) + ui.Text(c, core.Msg(c, col)).Grow(1).Basis(0).TextAlign(ui.End).FontSize(size).TextColor(t.TextMuted) } - }).Label(msg(c, title)) + }).Label(core.Msg(c, title)) } }) } func optionChainRows(c *ui.Context, s *OptionChainState, strikes []OptionStrike, opts OptionChainOptions, dec int) (changed, submitted bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) if len(strikes) == 0 { - ui.Text(c, msg(c, "option_chain.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) + ui.Text(c, core.Msg(c, "option_chain.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) return false, false } s.cursor = -1 @@ -167,11 +170,11 @@ func optionChainRows(c *ui.Context, s *OptionChainState, strikes []OptionStrike, s.rows.ScrollTo(min(atm, len(strikes)-1), ui.Center) } s.rows.Key = func(i int) any { return strikes[i].Strike } - s.rows.Label = func(i int) string { return statisticFormat(strikes[i].Strike, dec) } + s.rows.Label = func(i int) string { return data.StatisticFormat(strikes[i].Strike, dec) } s.rows.Selected = cursor clickedRow, clickedSide := -1, OptionCall double := false - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(strikes), func(i int) { o := strikes[i] chosen := s.Chosen && i == s.cursor @@ -199,7 +202,7 @@ func optionChainRows(c *ui.Context, s *OptionChainState, strikes []OptionStrike, }) half.Children(func() { for _, v := range []float64{q.Bid, q.Ask, q.Last} { - ui.Text(c, statisticFormat(v, dec)).Grow(1).Basis(0).TextAlign(ui.End).FontFeatures("tnum").SingleLine() + ui.Text(c, data.StatisticFormat(v, dec)).Grow(1).Basis(0).TextAlign(ui.End).FontFeatures("tnum").SingleLine() } }) } @@ -236,20 +239,20 @@ func optionChainRows(c *ui.Context, s *OptionChainState, strikes []OptionStrike, } func optionChainStrike(c *ui.Context, strike float64, dec int, atm bool) { - k := Tokens(c) + k := core.Tokens(c) ui.Row(c).Width(90).Gap(4).Justify(ui.Center).AlignItems(ui.Center).Background(k.Background).Children(func() { if atm { ui.Box(c).Size(8, 8).Draw(func(p *ui.Painter, r ui.Rect) { p.FillPath(internal.Diamond(r.X+4, r.Y+4, 8), k.AccentText) }) } - ui.Text(c, statisticFormat(strike, dec)).FontWeight(700).FontFeatures("tnum") + ui.Text(c, data.StatisticFormat(strike, dec)).FontWeight(700).FontFeatures("tnum") }) } func optionChainName(c *ui.Context, strike float64, side OptionSide, q OptionQuote, dec int, itm bool) string { - s := fmt.Sprintf(msg(c, []string{"option_chain.call_name", "option_chain.put_name"}[side]), statisticFormat(strike, dec), - statisticFormat(q.Bid, dec), statisticFormat(q.Ask, dec), statisticFormat(q.Last, dec)) + s := fmt.Sprintf(core.Msg(c, []string{"option_chain.call_name", "option_chain.put_name"}[side]), data.StatisticFormat(strike, dec), + data.StatisticFormat(q.Bid, dec), data.StatisticFormat(q.Ask, dec), data.StatisticFormat(q.Last, dec)) if itm { - s += ", " + msg(c, "option_chain.itm") + s += ", " + core.Msg(c, "option_chain.itm") } return s } diff --git a/option_chain_test.go b/finance/option_chain_test.go similarity index 96% rename from option_chain_test.go rename to finance/option_chain_test.go index 05736be..33f0d46 100644 --- a/option_chain_test.go +++ b/finance/option_chain_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func optionChainTester(s *OptionChainState, strikes []OptionStrike, opts OptionChainOptions, got *OptionChainView) *ui.Tester { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Height(420).Children(func() { v := OptionChain(c, s, []string{"Oct 17", "Nov 21"}, strikes, opts) if v.Changed || v.Submitted || v.ExpiryChanged { @@ -90,7 +92,7 @@ func TestOptionChainRejectsUnsortedStrikes(t *testing.T) { }() var s OptionChainState ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) OptionChain(c, &s, []string{"x"}, []OptionStrike{{Strike: 2}, {Strike: 1}}, OptionChainOptions{}) }, 100, 100, 1) } diff --git a/order_book.go b/finance/order_book.go similarity index 81% rename from order_book.go rename to finance/order_book.go index dbf2871..c982d45 100644 --- a/order_book.go +++ b/finance/order_book.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OrderSide is the buy or sell side. @@ -99,7 +102,7 @@ func (r OrderBookResult) Clicked() (price float64, side OrderSide, ok bool) { // OrderBook shows clickable asks over bids with depth bars. func OrderBook(c *ui.Context, s *OrderBookState, book OrderBookData, opts OrderBookOptions) OrderBookResult { - dec := priceDecimals("OrderBook", opts.Decimals) + dec := core.PriceDecimals("OrderBook", opts.Decimals) if opts.SizeDecimals < 0 || opts.Depth < 0 { panic("mujica: OrderBook SizeDecimals and Depth cannot be negative") } @@ -114,19 +117,19 @@ func OrderBook(c *ui.Context, s *OrderBookState, book OrderBookData, opts OrderB panic(fmt.Sprintf("mujica: OrderBook Group %v is not one of Groups", s.Group)) } orderBookRefresh(s, book, dec) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var r OrderBookResult - r.Element = ui.Column(c).Gap(d.Unit()).MinWidth(240).Disabled(opts.Disabled).Role(ui.RoleTable).Label(msg(c, "order_book.label")) + r.Element = ui.Column(c).Gap(d.Unit()).MinWidth(240).Disabled(opts.Disabled).Role(ui.RoleTable).Label(core.Msg(c, "order_book.label")) r.Element.Children(func() { if len(opts.Groups) > 0 { - var choices []SelectOption[float64] + var choices []input.SelectOption[float64] for _, g := range opts.Groups { - choices = append(choices, SelectOption[float64]{Value: g, Label: statisticFormat(g, dec)}) + choices = append(choices, input.SelectOption[float64]{Value: g, Label: data.StatisticFormat(g, dec)}) } ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "order_book.group")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) - if Select(c, &s.Group, choices, SelectOptions{Label: msg(c, "order_book.group"), Disabled: opts.Disabled}).Changed() { + ui.Text(c, core.Msg(c, "order_book.group")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) + if input.Select(c, &s.Group, choices, input.SelectOptions{Label: core.Msg(c, "order_book.group"), Disabled: opts.Disabled}).Changed() { slog.Debug("mujica: order_book group", "step", s.Group) } }) @@ -170,11 +173,11 @@ func orderBookSum(ls []BookLevel) float64 { // orderBookHeader names the columns. func orderBookHeader(c *ui.Context) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) ui.Row(c).Padding(0, d.Unit()*2).Children(func() { for i, key := range []string{"order_book.price", "order_book.size", "order_book.total"} { - t := ui.Text(c, msg(c, key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1).Basis(0) + t := ui.Text(c, core.Msg(c, key)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1).Basis(0) if i > 0 { t.TextAlign(ui.End) } @@ -184,13 +187,13 @@ func orderBookHeader(c *ui.Context) { // orderBookRow is one clickable level with its depth bar. func orderBookRow(c *ui.Context, r *OrderBookResult, l BookLevel, cum, total float64, side OrderSide, dec, sizeDec int) { - k := Tokens(c) - d := Density(c) - col := trendColor(c, side.sign()) - price := statisticFormat(l.Price, dec) + k := core.Tokens(c) + d := core.Density(c) + col := core.TrendColor(c, side.sign()) + price := data.StatisticFormat(l.Price, dec) b := ui.ButtonBase(c).Key(fmt.Sprint(side, price)).MinHeight(d.ControlHeight()). Padding(0, d.Unit()*2).Radius(theme.ControlRadius). - Label(fmt.Sprintf(msg(c, "order_book.level"), msg(c, fmt.Sprintf("order_book.side%d", side)), price, statisticFormat(l.Size, sizeDec))) + Label(fmt.Sprintf(core.Msg(c, "order_book.level"), core.Msg(c, fmt.Sprintf("order_book.side%d", side)), price, data.StatisticFormat(l.Size, sizeDec))) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) @@ -205,8 +208,8 @@ func orderBookRow(c *ui.Context, r *OrderBookResult, l BookLevel, cum, total flo }) b.Children(func() { ui.Text(c, price).TextColor(col).FontFeatures("tnum").FontWeight(600).Grow(1).Basis(0).SingleLine() - ui.Text(c, statisticFormat(l.Size, sizeDec)).FontFeatures("tnum").TextAlign(ui.End).Grow(1).Basis(0).SingleLine() - ui.Text(c, statisticFormat(cum, sizeDec)).FontFeatures("tnum").TextColor(k.TextMuted).TextAlign(ui.End).Grow(1).Basis(0).SingleLine() + ui.Text(c, data.StatisticFormat(l.Size, sizeDec)).FontFeatures("tnum").TextAlign(ui.End).Grow(1).Basis(0).SingleLine() + ui.Text(c, data.StatisticFormat(cum, sizeDec)).FontFeatures("tnum").TextColor(k.TextMuted).TextAlign(ui.End).Grow(1).Basis(0).SingleLine() }) if b.Clicked() && !b.IsDisabled() { r.price, r.side = l.Price, side diff --git a/order_book_test.go b/finance/order_book_test.go similarity index 85% rename from order_book_test.go rename to finance/order_book_test.go index b0d6485..a6fccbe 100644 --- a/order_book_test.go +++ b/finance/order_book_test.go @@ -1,9 +1,11 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func orderBookSample() OrderBookData { @@ -17,7 +19,7 @@ func TestOrderBookClickReportsPrice(t *testing.T) { var s OrderBookState var price float64 var side OrderSide - tt := tradingTester(GreenUp, 400, 500, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 500, func(c *ui.Context) { if p, sd, ok := OrderBook(c, &s, orderBookSample(), OrderBookOptions{}).Clicked(); ok { price, side = p, sd } @@ -40,7 +42,7 @@ func TestOrderBookClickReportsPrice(t *testing.T) { func TestOrderBookGroupsLevelsByTicks(t *testing.T) { s := OrderBookState{Group: 0.05} - tt := tradingTester(GreenUp, 400, 500, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 500, func(c *ui.Context) { OrderBook(c, &s, orderBookSample(), OrderBookOptions{Groups: []float64{0.01, 0.05, 0.1}}) }) // Bids round down, asks round up. @@ -61,7 +63,7 @@ func TestOrderBookGroupsLevelsByTicks(t *testing.T) { func TestOrderBookAggregatesOnlyOnChange(t *testing.T) { var s OrderBookState book := orderBookSample() - tt := tradingTester(GreenUp, 400, 500, func(c *ui.Context) { OrderBook(c, &s, book, OrderBookOptions{}) }) + tt := tradingTester(core.GreenUp, 400, 500, func(c *ui.Context) { OrderBook(c, &s, book, OrderBookOptions{}) }) first := &s.cache.bids[0] tt.Frame() if &s.cache.bids[0] != first { @@ -78,7 +80,7 @@ func TestOrderBookAggregatesOnlyOnChange(t *testing.T) { func TestOrderBookDisabledAndBadGroup(t *testing.T) { var s OrderBookState clicked := false - tt := tradingTester(GreenUp, 400, 500, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 500, func(c *ui.Context) { _, _, clicked = OrderBook(c, &s, orderBookSample(), OrderBookOptions{Disabled: true}).Clicked() }) _ = tt.Click("Bid 100.00, size 5") @@ -91,7 +93,7 @@ func TestOrderBookDisabledAndBadGroup(t *testing.T) { } }() bad := OrderBookState{Group: 0.015} - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { OrderBook(c, &bad, orderBookSample(), OrderBookOptions{Groups: []float64{0.015}}) }) } @@ -113,7 +115,7 @@ func TestOrderBookLargeBook(t *testing.T) { book.Bids = append(book.Bids, BookLevel{Price: 1000 - float64(i)/100, Size: 1}) book.Asks = append(book.Asks, BookLevel{Price: 1000.01 + float64(i)/100, Size: 1}) } - tt := tradingTester(GreenUp, 400, 900, func(c *ui.Context) { OrderBook(c, &s, book, OrderBookOptions{}) }) + tt := tradingTester(core.GreenUp, 400, 900, func(c *ui.Context) { OrderBook(c, &s, book, OrderBookOptions{}) }) if n := len(tt.Texts()); n > 80 { t.Fatalf("built %d texts", n) } diff --git a/order_confirm_dialog.go b/finance/order_confirm_dialog.go similarity index 67% rename from order_confirm_dialog.go rename to finance/order_confirm_dialog.go index 357b5b0..8e15070 100644 --- a/order_confirm_dialog.go +++ b/finance/order_confirm_dialog.go @@ -1,12 +1,15 @@ -package mujica +package finance import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OrderSummaryLine is one labeled value of an order summary. @@ -30,28 +33,28 @@ type OrderConfirmDialogOptions struct { type orderConfirmState struct{ ack bool } // OrderConfirmDialog summarizes an order; Cancel has the focus. -func OrderConfirmDialog(c *ui.Context, open *bool, opts OrderConfirmDialogOptions) AlertDialogResult { +func OrderConfirmDialog(c *ui.Context, open *bool, opts OrderConfirmDialogOptions) overlay.AlertDialogResult { if len(opts.Lines) == 0 { panic("mujica: OrderConfirmDialog needs summary Lines") } - var res AlertDialogResult - k := Tokens(c) - u := Density(c).Unit() + var res overlay.AlertDialogResult + k := core.Tokens(c) + u := core.Density(c).Unit() title, confirm := opts.Title, opts.ConfirmLabel if title == "" { - title = msg(c, "order_confirm_dialog.title") + title = core.Msg(c, "order_confirm_dialog.title") } if confirm == "" { - confirm = msg(c, "order_confirm_dialog.confirm") + confirm = core.Msg(c, "order_confirm_dialog.confirm") } was := *open var st *orderConfirmState - Dialog(c, open, DialogOptions{Title: title, KeepOnBackdrop: true, HideClose: true, Actions: func() { - if Button(c, msg(c, "order_confirm_dialog.cancel"), ButtonOptions{Variant: Secondary}).AutoFocus().Clicked() { + overlay.Dialog(c, open, overlay.DialogOptions{Title: title, KeepOnBackdrop: true, HideClose: true, Actions: func() { + if input.Button(c, core.Msg(c, "order_confirm_dialog.cancel"), input.ButtonOptions{Variant: input.Secondary}).AutoFocus().Clicked() { res.Canceled = true } locked := opts.Acknowledge != "" && !st.ack - if Button(c, confirm, ButtonOptions{Disabled: locked}).Clicked() && !locked { + if input.Button(c, confirm, input.ButtonOptions{Disabled: locked}).Clicked() && !locked { res.Confirmed = true } }}, func() { @@ -60,7 +63,7 @@ func OrderConfirmDialog(c *ui.Context, open *bool, opts OrderConfirmDialogOption st = ui.Local(list, "ack", func() orderConfirmState { return orderConfirmState{} }) list.Children(func() { for _, l := range opts.Lines { - ui.Row(c).Gap(u*4).MinHeight(Density(c).ControlHeight()).AlignItems(ui.Center). + ui.Row(c).Gap(u*4).MinHeight(core.Density(c).ControlHeight()).AlignItems(ui.Center). BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { ui.Text(c, l.Label).TextColor(k.TextMuted).Grow(1) ui.Text(c, l.Value).FontFeatures("tnum").FontWeight(600) @@ -74,7 +77,7 @@ func OrderConfirmDialog(c *ui.Context, open *bool, opts OrderConfirmDialogOption }) } if opts.Acknowledge != "" { - Checkbox(c, &st.ack, opts.Acknowledge, CheckboxOptions{}) + input.Checkbox(c, &st.ack, opts.Acknowledge, input.CheckboxOptions{}) } }) if was && !*open && !res.Confirmed { diff --git a/order_confirm_dialog_test.go b/finance/order_confirm_dialog_test.go similarity index 84% rename from order_confirm_dialog_test.go rename to finance/order_confirm_dialog_test.go index 68d48f2..e328e90 100644 --- a/order_confirm_dialog_test.go +++ b/finance/order_confirm_dialog_test.go @@ -1,13 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" ) -func orderConfirmTester(open *bool, opts OrderConfirmDialogOptions, res *AlertDialogResult) *ui.Tester { - return tradingTester(GreenUp, 600, 500, func(c *ui.Context) { +func orderConfirmTester(open *bool, opts OrderConfirmDialogOptions, res *overlay.AlertDialogResult) *ui.Tester { + return tradingTester(core.GreenUp, 600, 500, func(c *ui.Context) { if r := OrderConfirmDialog(c, open, opts); r.Confirmed || r.Canceled { *res = r } @@ -18,7 +21,7 @@ var orderConfirmLines = []OrderSummaryLine{{"Symbol", "MUJC"}, {"Side", "Buy"}, func TestOrderConfirmDialogFocusesCancel(t *testing.T) { open := true - var res AlertDialogResult + var res overlay.AlertDialogResult tt := orderConfirmTester(&open, OrderConfirmDialogOptions{Lines: orderConfirmLines, Risk: "Leveraged products can lose more than the deposit."}, &res) if !tt.Focused("Cancel") || !tt.HasText("10,020.00") || !tt.HasText("Leveraged products can lose more than the deposit.") { t.Fatalf("texts %q", tt.Texts()) @@ -27,7 +30,7 @@ func TestOrderConfirmDialogFocusesCancel(t *testing.T) { if !res.Canceled || open { t.Fatalf("Enter on Cancel: %+v open %v", res, open) } - open, res = true, AlertDialogResult{} + open, res = true, overlay.AlertDialogResult{} tt.Frame() if err := tt.Click("Place order"); err != nil { t.Fatal(err) @@ -39,7 +42,7 @@ func TestOrderConfirmDialogFocusesCancel(t *testing.T) { func TestOrderConfirmDialogAcknowledgeAndEscape(t *testing.T) { open := true - var res AlertDialogResult + var res overlay.AlertDialogResult tt := orderConfirmTester(&open, OrderConfirmDialogOptions{Lines: orderConfirmLines, Acknowledge: "I understand the risk"}, &res) _ = tt.Click("Place order") if res.Confirmed || !open { @@ -50,7 +53,7 @@ func TestOrderConfirmDialogAcknowledgeAndEscape(t *testing.T) { if !res.Confirmed { t.Fatal("acknowledged confirm refused") } - open, res = true, AlertDialogResult{} + open, res = true, overlay.AlertDialogResult{} tt.Frame() _ = tt.Click("Place order") if res.Confirmed { diff --git a/order_entry.go b/finance/order_entry.go similarity index 59% rename from order_entry.go rename to finance/order_entry.go index 0c95398..29dcbb3 100644 --- a/order_entry.go +++ b/finance/order_entry.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,7 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OrderType is limit, market, stop or stop limit. @@ -60,7 +64,7 @@ type OrderEntryOptions struct { // OrderEntryState holds the draft and its form state. type OrderEntryState struct { Draft OrderDraft - Form FormState + Form input.FormState started bool } @@ -104,37 +108,37 @@ func OrderEntry(c *ui.Context, s *OrderEntryState, opts OrderEntryOptions) Order if !s.started { s.started, s.Draft = true, initial } - priceSpec := numberInputResolve(NumberInputOptions{Step: tick}) - k := Tokens(c) - u := Density(c).Unit() + priceSpec := input.NumberInputResolve(input.NumberInputOptions{Step: tick}) + k := core.Tokens(c) + u := core.Density(c).Unit() var r OrderEntryResult - r.Element = ui.Column(c).Gap(u * 4).MaxWidth(360).Disabled(opts.Disabled).Role(ui.RoleGroup).Label(msg(c, "order_entry.label")) + r.Element = ui.Column(c).Gap(u * 4).MaxWidth(360).Disabled(opts.Disabled).Role(ui.RoleGroup).Label(core.Msg(c, "order_entry.label")) disabled := r.Element.IsDisabled() r.Element.Children(func() { side := 0 if s.Draft.Side == SideSell { side = 1 } - sides := []Segment{{Label: msg(c, "order_entry.buy")}, {Label: msg(c, "order_entry.sell")}} - SegmentedControl(c, &side, sides, SegmentedControlOptions{Equal: true, Label: msg(c, "order_entry.side"), Disabled: disabled}) + sides := []navigation.Segment{{Label: core.Msg(c, "order_entry.buy")}, {Label: core.Msg(c, "order_entry.sell")}} + navigation.SegmentedControl(c, &side, sides, navigation.SegmentedControlOptions{Equal: true, Label: core.Msg(c, "order_entry.side"), Disabled: disabled}) s.Draft.Side = SideBuy + OrderSide(side) - var types []SelectOption[OrderType] + var types []input.SelectOption[OrderType] for t := OrderLimit; t <= OrderStopLimit; t++ { - types = append(types, SelectOption[OrderType]{Value: t, Label: msg(c, fmt.Sprintf("order_entry.type%d", t))}) + types = append(types, input.SelectOption[OrderType]{Value: t, Label: core.Msg(c, fmt.Sprintf("order_entry.type%d", t))}) } ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { - ui.Text(c, opts.Symbol).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) - Select(c, &s.Draft.Type, types, SelectOptions{Label: msg(c, "order_entry.type"), Disabled: disabled}) + ui.Text(c, opts.Symbol).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + input.Select(c, &s.Draft.Type, types, input.SelectOptions{Label: core.Msg(c, "order_entry.type"), Disabled: disabled}) }) - submit := fmt.Sprintf(msg(c, fmt.Sprintf("order_entry.submit%d", s.Draft.Side)), opts.Symbol) - res := Form(c, &s.Form, FormOptions{SubmitLabel: submit}, func(f *FormScope) { + submit := fmt.Sprintf(core.Msg(c, fmt.Sprintf("order_entry.submit%d", s.Draft.Side)), opts.Symbol) + res := input.Form(c, &s.Form, input.FormOptions{SubmitLabel: submit}, func(f *input.FormScope) { orderEntryFields(c, f, s, opts, initial, tick, lot, disabled) }) ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "order_entry.estimate")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "order_entry.estimate")).TextColor(k.TextMuted) est := "—" if p := orderEntryPrice(s.Draft, opts.Last); p > 0 && s.Draft.Quantity > 0 { - est = "≈ " + statisticFormat(p*s.Draft.Quantity, priceSpec.decimals) + est = "≈ " + data.StatisticFormat(p*s.Draft.Quantity, priceSpec.Decimals) } ui.Text(c, est).FontFeatures("tnum").FontWeight(600) }) @@ -158,39 +162,39 @@ func orderEntryPrice(o OrderDraft, last float64) float64 { } // orderEntryFields registers the fields the order type needs. -func orderEntryFields(c *ui.Context, f *FormScope, s *OrderEntryState, opts OrderEntryOptions, initial OrderDraft, tick, lot float64, disabled bool) { +func orderEntryFields(c *ui.Context, f *input.FormScope, s *OrderEntryState, opts OrderEntryOptions, initial OrderDraft, tick, lot float64, disabled bool) { maxQ := opts.MaxQuantity - FormItem(c, f, "quantity", &s.Draft, FormItemOptions[OrderDraft]{Label: msg(c, "order_entry.quantity"), Required: true, Initial: initial, + input.FormItem(c, f, "quantity", &s.Draft, input.FormItemOptions[OrderDraft]{Label: core.Msg(c, "order_entry.quantity"), Required: true, Initial: initial, Validate: func(o OrderDraft) string { return orderEntryCheck(c, o, opts, maxQ) }, Async: opts.Check, }, func() *ui.Element { - return NumberInput(c, &s.Draft.Quantity, NumberInputOptions{Step: lot, Disabled: disabled, Error: s.Form.Error("quantity")}).Input + return input.NumberInput(c, &s.Draft.Quantity, input.NumberInputOptions{Step: lot, Disabled: disabled, Error: s.Form.Error("quantity")}).Input }) if s.Draft.Type == OrderStop || s.Draft.Type == OrderStopLimit { - FormItem(c, f, "trigger", &s.Draft.Trigger, FormItemOptions[float64]{Label: msg(c, "order_entry.trigger"), Required: true, Initial: initial.Trigger, + input.FormItem(c, f, "trigger", &s.Draft.Trigger, input.FormItemOptions[float64]{Label: core.Msg(c, "order_entry.trigger"), Required: true, Initial: initial.Trigger, Validate: func(v float64) string { return orderEntryTrigger(c, s.Draft, opts.Last) }, }, func() *ui.Element { - return NumberInput(c, &s.Draft.Trigger, NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("trigger")}).Input + return input.NumberInput(c, &s.Draft.Trigger, input.NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("trigger")}).Input }) } if s.Draft.Type == OrderLimit || s.Draft.Type == OrderStopLimit { - FormItem(c, f, "price", &s.Draft.Price, FormItemOptions[float64]{Label: msg(c, "order_entry.price"), Required: true, Initial: initial.Price, + input.FormItem(c, f, "price", &s.Draft.Price, input.FormItemOptions[float64]{Label: core.Msg(c, "order_entry.price"), Required: true, Initial: initial.Price, Validate: func(v float64) string { if v <= 0 { - return msg(c, "order_entry.price_positive") + return core.Msg(c, "order_entry.price_positive") } return "" }, }, func() *ui.Element { - return NumberInput(c, &s.Draft.Price, NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("price")}).Input + return input.NumberInput(c, &s.Draft.Price, input.NumberInputOptions{Step: tick, Disabled: disabled, Error: s.Form.Error("price")}).Input }) } - var tifs []SelectOption[TimeInForce] + var tifs []input.SelectOption[TimeInForce] for t := TIFDay; t <= TIFFillOrKill; t++ { - tifs = append(tifs, SelectOption[TimeInForce]{Value: t, Label: msg(c, fmt.Sprintf("order_entry.tif%d", t))}) + tifs = append(tifs, input.SelectOption[TimeInForce]{Value: t, Label: core.Msg(c, fmt.Sprintf("order_entry.tif%d", t))}) } - FormItem(c, f, "tif", &s.Draft.TIF, FormItemOptions[TimeInForce]{Label: msg(c, "order_entry.tif"), Initial: initial.TIF}, func() *ui.Element { - return Select(c, &s.Draft.TIF, tifs, SelectOptions{Label: msg(c, "order_entry.tif"), Disabled: disabled}).Element + input.FormItem(c, f, "tif", &s.Draft.TIF, input.FormItemOptions[TimeInForce]{Label: core.Msg(c, "order_entry.tif"), Initial: initial.TIF}, func() *ui.Element { + return input.Select(c, &s.Draft.TIF, tifs, input.SelectOptions{Label: core.Msg(c, "order_entry.tif"), Disabled: disabled}).Element }) } @@ -198,9 +202,9 @@ func orderEntryFields(c *ui.Context, f *FormScope, s *OrderEntryState, opts Orde func orderEntryCheck(c *ui.Context, o OrderDraft, opts OrderEntryOptions, maxQ float64) string { switch { case o.Quantity <= 0: - return msg(c, "order_entry.quantity_positive") + return core.Msg(c, "order_entry.quantity_positive") case maxQ > 0 && o.Quantity > maxQ: - return fmt.Sprintf(msg(c, "order_entry.quantity_max"), statisticFormat(maxQ, 0)) + return fmt.Sprintf(core.Msg(c, "order_entry.quantity_max"), data.StatisticFormat(maxQ, 0)) } if opts.Validate != nil { return opts.Validate(o) @@ -212,11 +216,11 @@ func orderEntryCheck(c *ui.Context, o OrderDraft, opts OrderEntryOptions, maxQ f func orderEntryTrigger(c *ui.Context, o OrderDraft, last float64) string { switch { case o.Trigger <= 0: - return msg(c, "order_entry.trigger_positive") + return core.Msg(c, "order_entry.trigger_positive") case last > 0 && o.Side == SideBuy && o.Trigger <= last: - return msg(c, "order_entry.trigger_above") + return core.Msg(c, "order_entry.trigger_above") case last > 0 && o.Side == SideSell && o.Trigger >= last: - return msg(c, "order_entry.trigger_below") + return core.Msg(c, "order_entry.trigger_below") } return "" } diff --git a/order_entry_test.go b/finance/order_entry_test.go similarity index 96% rename from order_entry_test.go rename to finance/order_entry_test.go index 58e9a19..168bc3b 100644 --- a/order_entry_test.go +++ b/finance/order_entry_test.go @@ -1,9 +1,11 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type orderEntryProbe struct { @@ -15,7 +17,7 @@ type orderEntryProbe struct { } func (p *orderEntryProbe) tester() *ui.Tester { - return tradingTester(GreenUp, 520, 700, func(c *ui.Context) { + return tradingTester(core.GreenUp, 520, 700, func(c *ui.Context) { if o, ok := OrderEntry(c, &p.s, p.opts).Submitted(); ok { p.orders = append(p.orders, o) } diff --git a/order_table.go b/finance/order_table.go similarity index 62% rename from order_table.go rename to finance/order_table.go index 7e558ff..8e8368d 100644 --- a/order_table.go +++ b/finance/order_table.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -7,7 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OrderStatus is where an order stands. @@ -63,7 +67,7 @@ type OrderTableOptions struct { Decimals, QuantityDecimals int // Version must change when rows change in place. Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -71,13 +75,13 @@ type OrderTableOptions struct { // OrderTableState is the status filter and table state. type OrderTableState struct { Filter OrderFilter - Table DataTableState[string] + Table data.DataTableState[string] cache tradingRows[OrderRow] } // OrderTable lists orders with filter, progress and Cancel. func OrderTable(c *ui.Context, s *OrderTableState, rows []OrderRow, opts OrderTableOptions) TableActionResult { - dec := priceDecimals("OrderTable", opts.Decimals) + dec := core.PriceDecimals("OrderTable", opts.Decimals) if opts.QuantityDecimals < 0 { panic("mujica: OrderTable with negative QuantityDecimals") } @@ -89,51 +93,51 @@ func OrderTable(c *ui.Context, s *OrderTableState, rows []OrderRow, opts OrderTa return s.Filter.keeps(o.Status) }) var r TableActionResult - text := func(id, key string, t func(OrderRow) string) DataColumn[OrderRow] { - return DataColumn[OrderRow]{ID: id, Title: msg(c, key), Sortable: true, Text: t, + text := func(id, key string, t func(OrderRow) string) data.DataColumn[OrderRow] { + return data.DataColumn[OrderRow]{ID: id, Title: core.Msg(c, key), Sortable: true, Text: t, Compare: func(a, b OrderRow) int { return cmp.Compare(t(a), t(b)) }} } - cols := []DataColumn[OrderRow]{ + cols := []data.DataColumn[OrderRow]{ text("symbol", "position_table.symbol", func(o OrderRow) string { return o.Symbol }), - {ID: "side", Title: msg(c, "order_table.side"), Text: func(o OrderRow) string { return msg(c, fmt.Sprintf("order_entry.side%d", o.Side)) }, + {ID: "side", Title: core.Msg(c, "order_table.side"), Text: func(o OrderRow) string { return core.Msg(c, fmt.Sprintf("order_entry.side%d", o.Side)) }, Cell: func(o OrderRow) { tradingSide(c, o.Side) }}, - text("type", "order_entry.type", func(o OrderRow) string { return msg(c, fmt.Sprintf("order_entry.type%d", o.Type)) }), - {ID: "price", Title: msg(c, "order_book.price"), Align: ui.End, Sortable: true, + text("type", "order_entry.type", func(o OrderRow) string { return core.Msg(c, fmt.Sprintf("order_entry.type%d", o.Type)) }), + {ID: "price", Title: core.Msg(c, "order_book.price"), Align: ui.End, Sortable: true, Compare: func(a, b OrderRow) int { return cmp.Compare(a.Price, b.Price) }, Text: func(o OrderRow) string { if o.Type == OrderMarket { - return msg(c, "order_table.market") + return core.Msg(c, "order_table.market") } - return statisticFormat(o.Price, dec) + return data.StatisticFormat(o.Price, dec) }}, - {ID: "filled", Title: msg(c, "order_table.filled"), Width: 140, + {ID: "filled", Title: core.Msg(c, "order_table.filled"), Width: 140, Text: func(o OrderRow) string { - return statisticFormat(o.Filled, qd) + " / " + statisticFormat(o.Quantity, qd) + return data.StatisticFormat(o.Filled, qd) + " / " + data.StatisticFormat(o.Quantity, qd) }, Cell: func(o OrderRow) { orderTableProgress(c, o, qd) }}, - text("status", "order_table.status", func(o OrderRow) string { return msg(c, fmt.Sprintf("order_table.status%d", o.Status)) }), - {ID: "action", Title: msg(c, "position_table.action"), Width: 96, Text: func(OrderRow) string { return "" }, + text("status", "order_table.status", func(o OrderRow) string { return core.Msg(c, fmt.Sprintf("order_table.status%d", o.Status)) }), + {ID: "action", Title: core.Msg(c, "position_table.action"), Width: 96, Text: func(OrderRow) string { return "" }, Cell: func(o OrderRow) { - if o.Status.working() && Button(c, msg(c, "order_table.cancel"), ButtonOptions{Variant: Secondary}). - Label(msg(c, "order_table.cancel")+" "+o.Symbol).Clicked() { + if o.Status.working() && input.Button(c, core.Msg(c, "order_table.cancel"), input.ButtonOptions{Variant: input.Secondary}). + Label(core.Msg(c, "order_table.cancel")+" "+o.Symbol).Clicked() { r.id = o.ID } }}, } - d := Density(c) + d := core.Density(c) r.Element = ui.Column(c).Gap(d.Unit() * 2).Disabled(opts.Disabled).Grow(1).FillWidth().MinHeight(0) r.Element.Children(func() { - var segs []Segment + var segs []navigation.Segment for f := OrdersAll; f <= OrdersClosed; f++ { - segs = append(segs, Segment{Label: msg(c, fmt.Sprintf("order_table.filter%d", f))}) + segs = append(segs, navigation.Segment{Label: core.Msg(c, fmt.Sprintf("order_table.filter%d", f))}) } at := int(s.Filter) - if SegmentedControl(c, &at, segs, SegmentedControlOptions{Label: msg(c, "order_table.filter"), Disabled: opts.Disabled}).Changed() { + if navigation.SegmentedControl(c, &at, segs, navigation.SegmentedControlOptions{Label: core.Msg(c, "order_table.filter"), Disabled: opts.Disabled}).Changed() { s.Filter = OrderFilter(at) slog.Debug("mujica: order_table filter", "filter", s.Filter) } - r.Table = DataTable(c, &s.Table, shown, DataTableOptions[OrderRow, string]{Columns: cols, Key: func(o OrderRow) string { return o.ID }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "order_table.empty")}) + r.Table = data.DataTable(c, &s.Table, shown, data.DataTableOptions[OrderRow, string]{Columns: cols, Key: func(o OrderRow) string { return o.ID }, + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "order_table.empty")}) r.Table.Element.Grow(1) }) if r.Element.IsDisabled() { @@ -147,22 +151,22 @@ func OrderTable(c *ui.Context, s *OrderTableState, rows []OrderRow, opts OrderTa // orderTableProgress shows the filled share as bar and count. func orderTableProgress(c *ui.Context, o OrderRow, qd int) { - k := Tokens(c) + k := core.Tokens(c) share := float32(o.Filled / o.Quantity) - ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).FillWidth().Children(func() { + ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).FillWidth().Children(func() { ui.Box(c).Size(48, 6).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.SurfaceHover, theme.ControlRadius) p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * share, H: r.H}, k.AccentText, theme.ControlRadius) }) - ui.Text(c, statisticFormat(o.Filled, qd)+" / "+statisticFormat(o.Quantity, qd)).FontFeatures("tnum").FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, data.StatisticFormat(o.Filled, qd)+" / "+data.StatisticFormat(o.Quantity, qd)).FontFeatures("tnum").FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) } // tradingSide names a side with its arrow and trend color. func tradingSide(c *ui.Context, side OrderSide) { - ui.Row(c).Gap(2).AlignItems(ui.Center).TextColor(trendColor(c, side.sign())).Children(func() { - ui.Icon(c, trendIcon(side.sign())).Size(12, 12) - ui.Text(c, msg(c, fmt.Sprintf("order_entry.side%d", side))).FontWeight(600).SingleLine() + ui.Row(c).Gap(2).AlignItems(ui.Center).TextColor(core.TrendColor(c, side.sign())).Children(func() { + ui.Icon(c, core.TrendIcon(side.sign())).Size(12, 12) + ui.Text(c, core.Msg(c, fmt.Sprintf("order_entry.side%d", side))).FontWeight(600).SingleLine() }) } diff --git a/order_table_test.go b/finance/order_table_test.go similarity index 96% rename from order_table_test.go rename to finance/order_table_test.go index be31cfa..9ebd7b9 100644 --- a/order_table_test.go +++ b/finance/order_table_test.go @@ -1,10 +1,12 @@ -package mujica +package finance import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func orderTableSample() []OrderRow { @@ -17,7 +19,7 @@ func orderTableSample() []OrderRow { } func orderTableTester(s *OrderTableState, rows *[]OrderRow, opts *OrderTableOptions, acted *string) *ui.Tester { - return tradingTester(GreenUp, 1000, 400, func(c *ui.Context) { + return tradingTester(core.GreenUp, 1000, 400, func(c *ui.Context) { ui.Box(c).Width(960).Height(320).Children(func() { if id, ok := OrderTable(c, s, *rows, *opts).Acted(); ok { *acted = id diff --git a/payoff_diagram.go b/finance/payoff_diagram.go similarity index 82% rename from payoff_diagram.go rename to finance/payoff_diagram.go index 19ab808..3bc47a5 100644 --- a/payoff_diagram.go +++ b/finance/payoff_diagram.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type OptionLegKind int @@ -72,14 +74,14 @@ func Breakevens(legs []OptionLeg, lo, hi float64) []float64 { // PayoffDiagramOptions configure a PayoffDiagram. type PayoffDiagramOptions struct { - Frame ChartFrame + Frame chart.ChartFrame Spot float64 Low, High float64 Points int Format func(v float64) string } -func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) ChartResult { +func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) chart.ChartResult { lo, hi := payoffRange(legs, opts) n := opts.Points if n == 0 { @@ -88,15 +90,15 @@ func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) C if n < 2 { panic("mujica: PayoffDiagram needs at least 2 Points") } - ch := newChart(c, opts.Frame) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + ch := chart.NewChart(c, opts.Frame) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) type data struct { px, pl, be []float64 gx, gp []float64 ylo, yhi float64 } - d := chartMemo(ch, "payoff", fmt.Sprint(legs, lo, hi, n), func() data { + d := chart.ChartMemo(ch, "payoff", fmt.Sprint(legs, lo, hi, n), func() data { var v data v.be = Breakevens(legs, lo, hi) v.ylo, v.yhi = 0, 0 @@ -124,19 +126,19 @@ func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) C if len(legs) == 0 { n = 0 } - format := func(v float64) string { return chartValue(c, v, opts.Format) } + format := func(v float64) string { return chart.ChartValue(c, v, opts.Format) } ylo, yhi, step := internal.Nice(d.ylo, d.yhi, 4) ticks := internal.Ticks(ylo, yhi, step) geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 56, 26) + plot := chart.ChartInset(r, 56, 26) return plot, internal.Linear{D0: lo, D1: hi, R0: float64(plot.X), R1: float64(plot.X + plot.W)}, internal.Linear{D0: ylo, D1: yhi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } - up, down := trendColor(c, 1), trendColor(c, -1) - spec := ChartSpec{Marks: n} + up, down := core.TrendColor(c, 1), core.TrendColor(c, -1) + spec := chart.ChartSpec{Marks: n} spec.Readout = func(i int) string { - word := [3]string{"payoff_diagram.loss", "payoff_diagram.even", "payoff_diagram.profit"}[trendSign(d.pl[i])+1] - return fmt.Sprintf(msg(c, "payoff_diagram.readout"), format(d.px[i]), msg(c, word), format(d.pl[i])) + word := [3]string{"payoff_diagram.loss", "payoff_diagram.even", "payoff_diagram.profit"}[core.TrendSign(d.pl[i])+1] + return fmt.Sprintf(core.Msg(c, "payoff_diagram.readout"), format(d.px[i]), core.Msg(c, word), format(d.pl[i])) } spec.Pick = func(r ui.Rect, x, y float32) int { plot, xs, _ := geom(r) @@ -160,7 +162,7 @@ func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) C zero := internal.Snap(p, ys.Map(0)) pt := func(x, v float64) (float32, float32) { return internal.Snap(p, xs.Map(x)), internal.Snap(p, ys.Map(v)) } // Exact corners: the grid plus strikes and breakevens. - indices := chartMemo(ch, "payoff.geometry", [2]any{chartID(d.gx), plot.W}, func() []int { + indices := chart.ChartMemo(ch, "payoff.geometry", [2]any{chart.ChartID(d.gx), plot.W}, func() []int { return internal.Columns(len(d.gx), func(i int) float32 { return float32((d.gx[i]-lo)/(hi-lo)) * plot.W }, func(i int) float64 { return d.gp[i] }) }) @@ -191,14 +193,14 @@ func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) C for _, b := range d.be { bx, _ := pt(b, 0) p.FillPath(internal.Diamond(bx, zero, 9), k.Text) - chartText(p, format(b), bx, zero-6, size, k.Text, 0.5, 1) + chart.ChartText(p, format(b), bx, zero-6, size, k.Text, 0.5, 1) } if opts.Spot > lo && opts.Spot < hi { sx, _ := pt(opts.Spot, 0) for y := plot.Y; y < plot.Y+plot.H; y += 6 { p.Line(sx, y, sx, min(y+3, plot.Y+plot.H), 1, k.Info) } - chartText(p, msg(c, "payoff_diagram.spot")+" "+format(opts.Spot), sx+4, plot.Y, size, k.Info, 0, 0) + chart.ChartText(p, core.Msg(c, "payoff_diagram.spot")+" "+format(opts.Spot), sx+4, plot.Y, size, k.Info, 0, 0) } if active >= 0 { x, y := pt(d.px[active], d.pl[active]) @@ -207,14 +209,14 @@ func PayoffDiagram(c *ui.Context, legs []OptionLeg, opts PayoffDiagramOptions) C internal.Marker(p, 0, x, y, 9, k.AccentText) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) if len(d.be) > 0 && n > 0 { parts := make([]string, len(d.be)) for i, b := range d.be { parts[i] = format(b) } res.Element.Children(func() { - ui.Text(c, msg(c, "payoff_diagram.breakevens")+" "+strings.Join(parts, ", ")).FontSize(size).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "payoff_diagram.breakevens")+" "+strings.Join(parts, ", ")).FontSize(size).TextColor(k.TextMuted) }) } return res diff --git a/payoff_diagram_test.go b/finance/payoff_diagram_test.go similarity index 80% rename from payoff_diagram_test.go rename to finance/payoff_diagram_test.go index 4467156..10fa18a 100644 --- a/payoff_diagram_test.go +++ b/finance/payoff_diagram_test.go @@ -1,12 +1,15 @@ -package mujica +package finance import ( - "mujicaui/theme" "slices" "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func bullSpread() []OptionLeg { @@ -34,9 +37,9 @@ func TestPayoffMath(t *testing.T) { } func TestPayoffDiagramKeysAndText(t *testing.T) { - var last ChartResult + var last chart.ChartResult tt := chatTester(640, 380, func(c *ui.Context) { - last = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: ChartFrame{Label: "Spread"}, Spot: 103, Low: 90, High: 120, Points: 31}) + last = PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Spread"}, Spot: 103, Low: 90, High: 120, Points: 31}) }) if !tt.HasText("Breakeven: 102") { t.Fatalf("texts %q", tt.Texts()) @@ -58,7 +61,7 @@ func TestPayoffDiagramKeysAndText(t *testing.T) { func TestPayoffDiagramEmptyAndBadRange(t *testing.T) { tt := chatTester(640, 380, func(c *ui.Context) { - PayoffDiagram(c, nil, PayoffDiagramOptions{Frame: ChartFrame{Label: "Empty"}}) + PayoffDiagram(c, nil, PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Empty"}}) }) if !tt.HasText("No data") { t.Fatalf("texts %q", tt.Texts()) @@ -69,15 +72,15 @@ func TestPayoffDiagramEmptyAndBadRange(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: ChartFrame{Label: "x"}, Low: 10, High: 5}) + core.Use(c, core.Settings{}) + PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "x"}, Low: 10, High: 5}) }, 100, 100, 1) } func TestPayoffDiagramSnapshot(t *testing.T) { snapshot(t, "payoff_diagram", 640, 360, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { - PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: ChartFrame{Label: "Bull call spread"}, Spot: 103}) + PayoffDiagram(c, bullSpread(), PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Bull call spread"}, Spot: 103}) }) }) } @@ -85,7 +88,7 @@ func TestPayoffDiagramSnapshot(t *testing.T) { func TestPayoffDiagramSparseGridKeepsPeak(t *testing.T) { legs := []OptionLeg{{Kind: LegCall, Strike: 90, Premium: 1, Quantity: 1}, {Kind: LegCall, Strike: 100, Quantity: -2}, {Kind: LegCall, Strike: 110, Quantity: 1}} tt := chatTester(640, 380, func(c *ui.Context) { - PayoffDiagram(c, legs, PayoffDiagramOptions{Frame: ChartFrame{Label: "Butterfly"}, Low: 80, High: 120, Points: 2}) + PayoffDiagram(c, legs, PayoffDiagramOptions{Frame: chart.ChartFrame{Label: "Butterfly"}, Low: 80, High: 120, Points: 2}) }) r, ok := tt.Find("Butterfly") if !ok { diff --git a/performance_chart.go b/finance/performance_chart.go similarity index 61% rename from performance_chart.go rename to finance/performance_chart.go index 986fb5d..e50f3c7 100644 --- a/performance_chart.go +++ b/finance/performance_chart.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -8,13 +8,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PerformanceChartOptions configure a PerformanceChart. type PerformanceChartOptions struct { - Frame ChartFrame + Frame chart.ChartFrame Version int // Times dates each value; required, ascending. Times []time.Time @@ -42,29 +45,29 @@ func MaxDrawdown(values []float64) Drawdown { return dd } -func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOptions) ChartResult { +func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOptions) chart.ChartResult { if opts.Benchmark != nil && len(opts.Benchmark) != len(values) { panic("mujica: PerformanceChart Benchmark length differs") } if len(opts.Times) != len(values) { panic("mujica: PerformanceChart Times length differs") } - ch := newChart(c, opts.Frame) - k := Tokens(c) + ch := chart.NewChart(c, opts.Frame) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) name := opts.Name if name == "" { - name = msg(c, "performance_chart.portfolio") + name = core.Msg(c, "performance_chart.portfolio") } bname := opts.BenchmarkName if bname == "" { - bname = msg(c, "performance_chart.benchmark") + bname = core.Msg(c, "performance_chart.benchmark") } if opts.Benchmark != nil && name == bname { panic("mujica: PerformanceChart series names must differ") } - series := chartMemo(ch, "returns", [4]string{chartID(values), chartID(opts.Benchmark), name + fmt.Sprint(opts.Version), bname}, func() []ChartSeries { + series := chart.ChartMemo(ch, "returns", [4]string{chart.ChartID(values), chart.ChartID(opts.Benchmark), name + fmt.Sprint(opts.Version), bname}, func() []chart.ChartSeries { for _, vs := range [][]float64{values, opts.Benchmark} { for _, v := range vs { if !(v > 0) || math.IsInf(v, 0) { @@ -73,22 +76,22 @@ func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOpti } } - out := []ChartSeries{{Name: name, Values: performanceReturns(values)}} + out := []chart.ChartSeries{{Name: name, Values: performanceReturns(values)}} if opts.Benchmark != nil { - out = append(out, ChartSeries{Name: bname, Values: performanceReturns(opts.Benchmark)}) + out = append(out, chart.ChartSeries{Name: bname, Values: performanceReturns(opts.Benchmark)}) } return out }) n := len(values) - ax := chartMemo(ch, "x", chartID(opts.Times)+fmt.Sprint(opts.Version), func() chartX { return newChartX(n, nil, opts.Times) }) - dd := chartMemo(ch, "dd", chartID(values)+fmt.Sprint(opts.Version), func() Drawdown { return MaxDrawdown(values) }) - lo, hi, ok := chartYRange(ch, series, true, nil) + ax := chart.ChartMemo(ch, "x", chart.ChartID(opts.Times)+fmt.Sprint(opts.Version), func() chart.ChartX { return chart.NewChartX(n, nil, opts.Times) }) + dd := chart.ChartMemo(ch, "dd", chart.ChartID(values)+fmt.Sprint(opts.Version), func() Drawdown { return MaxDrawdown(values) }) + lo, hi, ok := chart.ChartYRange(ch, series, true, nil) if !ok { n = 0 } - keys := make([]ChartLegendItem, len(series)) + keys := make([]chart.ChartLegendItem, len(series)) for i, s := range series { - keys[i] = ChartLegendItem{Name: s.Name, Color: pal[i], Shape: i} + keys[i] = chart.ChartLegendItem{Name: s.Name, Color: pal[i], Shape: i} } var ticks []float64 var step float64 @@ -96,23 +99,23 @@ func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOpti lo, hi, step = internal.Nice(lo, hi, 4) ticks = internal.Ticks(lo, hi, step) } - pct := func(v float64) string { return statisticSigned(v, 2) + "%" } + pct := func(v float64) string { return data.StatisticSigned(v, 2) + "%" } geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 56, 26) - return plot, ax.scale(plot), internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} + plot := chart.ChartInset(r, 56, 26) + return plot, ax.Scale(plot), internal.Linear{D0: lo, D1: hi, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } - spec := ChartSpec{Marks: n, Legend: keys, Hidden: ch.toggles(true)} + spec := chart.ChartSpec{Marks: n, Legend: keys, Hidden: ch.Toggles(true)} spec.Pick = func(r ui.Rect, x, y float32) int { plot, xs, _ := geom(r) if !plot.Contains(x, y) { return -1 } - return ax.nearest(xs.Invert(x)) + return ax.Nearest(xs.Invert(x)) } spec.Readout = func(i int) string { parts := []string{opts.Times[i].Format("2006-01-02")} for _, s := range series { - if !ch.hidden(s.Name) { + if !ch.Hidden(s.Name) { parts = append(parts, s.Name+" "+pct(s.Values[i])) } } @@ -120,26 +123,26 @@ func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOpti } spec.Draw = func(p *ui.Painter, r ui.Rect, active int) { plot, xs, ys := geom(r) - x := func(i int) float32 { return internal.Snap(p, xs.Map(ax.vals[i])) } - if dd.Percent < 0 && !ch.hidden(name) { + x := func(i int) float32 { return internal.Snap(p, xs.Map(ax.Vals[i])) } + if dd.Percent < 0 && !ch.Hidden(name) { band := ui.Rect{X: x(dd.Peak), Y: plot.Y, W: x(dd.Trough) - x(dd.Peak), H: plot.H} - p.Fill(band, trendColor(c, -1).Alpha(0.1), 0) - p.Line(band.X, plot.Y, band.X, plot.Y+plot.H, 1, trendColor(c, -1).Alpha(0.5)) - p.Line(band.X+band.W, plot.Y, band.X+band.W, plot.Y+plot.H, 1, trendColor(c, -1).Alpha(0.5)) - chartText(p, fmt.Sprintf(msg(c, "performance_chart.drawdown"), pct(dd.Percent)), band.X+4, plot.Y+2, size, chartInk(k, k.Surface), 0, 0) + p.Fill(band, core.TrendColor(c, -1).Alpha(0.1), 0) + p.Line(band.X, plot.Y, band.X, plot.Y+plot.H, 1, core.TrendColor(c, -1).Alpha(0.5)) + p.Line(band.X+band.W, plot.Y, band.X+band.W, plot.Y+plot.H, 1, core.TrendColor(c, -1).Alpha(0.5)) + chart.ChartText(p, fmt.Sprintf(core.Msg(c, "performance_chart.drawdown"), pct(dd.Percent)), band.X+4, plot.Y+2, size, chart.ChartInk(k, k.Surface), 0, 0) } internal.AxisY(p, plot, ys, ticks, func(v float64) string { return internal.FormatTick(v, step) + "%" }, size, k) - tx, tl := ax.ticks(plot) + tx, tl := ax.Ticks(plot) internal.AxisX(p, plot, tx, tl, size, k) zero := internal.Snap(p, ys.Map(0)) p.Line(plot.X, zero, plot.X+plot.W, zero, 1, k.ControlBorder) for si, s := range series { - if ch.hidden(s.Name) { + if ch.Hidden(s.Name) { continue } var line ui.Path - idx := chartMemo(ch, "performance.geometry."+s.Name, [3]any{chartID(s.Values), chartID(ax.vals), plot.W}, func() []int { - return internal.Columns(n, func(i int) float32 { return xs.Map(ax.vals[i]) - plot.X }, func(i int) float64 { return s.Values[i] }) + idx := chart.ChartMemo(ch, "performance.geometry."+s.Name, [3]any{chart.ChartID(s.Values), chart.ChartID(ax.Vals), plot.W}, func() []int { + return internal.Columns(n, func(i int) float32 { return xs.Map(ax.Vals[i]) - plot.X }, func(i int) float64 { return s.Values[i] }) }) for j, i := range idx { if j == 0 { @@ -157,17 +160,17 @@ func PerformanceChart(c *ui.Context, values []float64, opts PerformanceChartOpti if active >= 0 { p.Line(x(active), plot.Y, x(active), plot.Y+plot.H, 1, k.TextMuted) for si, s := range series { - if !ch.hidden(s.Name) { + if !ch.Hidden(s.Name) { internal.Marker(p, si, x(active), ys.Map(s.Values[active]), 13, k.Surface) internal.Marker(p, si, x(active), ys.Map(s.Values[active]), 9, keys[si].Color) } } } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) if dd.Percent < 0 && n > 0 { res.Element.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "performance_chart.drawdown_range"), pct(dd.Percent), + ui.Text(c, fmt.Sprintf(core.Msg(c, "performance_chart.drawdown_range"), pct(dd.Percent), opts.Times[dd.Peak].Format("2006-01-02"), opts.Times[dd.Trough].Format("2006-01-02"))).FontSize(size).TextColor(k.TextMuted) }) } diff --git a/performance_chart_test.go b/finance/performance_chart_test.go similarity index 71% rename from performance_chart_test.go rename to finance/performance_chart_test.go index 9bfe10f..9e719b0 100644 --- a/performance_chart_test.go +++ b/finance/performance_chart_test.go @@ -1,17 +1,20 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "math" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" ) func TestPerformanceChartBenchmarkDrawdownAndKeys(t *testing.T) { values := []float64{100, 110, 88, 120} times := mktTimes(4) - var last ChartResult + var last chart.ChartResult tt := chatTester(760, 420, func(c *ui.Context) { - last = PerformanceChart(c, values, PerformanceChartOptions{Frame: ChartFrame{Label: "Performance"}, Times: times, Benchmark: []float64{100, 105, 108, 112}}) + last = PerformanceChart(c, values, PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: times, Benchmark: []float64{100, 105, 108, 112}}) }) if dd := MaxDrawdown(values); dd.Peak != 1 || dd.Trough != 2 || math.Abs(dd.Percent+20) > 1e-9 { t.Fatalf("drawdown %+v", dd) @@ -30,14 +33,14 @@ func TestPerformanceChartBenchmarkDrawdownAndKeys(t *testing.T) { t.Fatalf("legend %q", tt.Texts()) } marketsPanic(t, func(c *ui.Context) { - PerformanceChart(c, []float64{-1}, PerformanceChartOptions{Frame: ChartFrame{Label: "x"}}) + PerformanceChart(c, []float64{-1}, PerformanceChartOptions{Frame: chart.ChartFrame{Label: "x"}}) }) } func TestPerformanceChartSnapshot(t *testing.T) { snapshot(t, "performance_chart", 720, 380, func(c *ui.Context) { ui.Column(c).Padding(16).Gap(12).Fill().Children(func() { - PerformanceChart(c, mktSeries(120, 0), PerformanceChartOptions{Frame: ChartFrame{Label: "Performance"}, Times: mktTimes(120), Benchmark: mktSeries(120, 1)}) + PerformanceChart(c, mktSeries(120, 0), PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: mktTimes(120), Benchmark: mktSeries(120, 1)}) }) }) @@ -45,7 +48,7 @@ func TestPerformanceChartSnapshot(t *testing.T) { func TestPerformanceChartRequiresTimes(t *testing.T) { marketsPanic(t, func(c *ui.Context) { - PerformanceChart(c, []float64{100, 110}, PerformanceChartOptions{Frame: ChartFrame{Label: "x"}}) + PerformanceChart(c, []float64{100, 110}, PerformanceChartOptions{Frame: chart.ChartFrame{Label: "x"}}) }) } @@ -53,17 +56,17 @@ func TestPerformanceChartDateVersion(t *testing.T) { values := []float64{100, 105, 110} times := mktTimes(3) version := 0 - var axis chartX + var axis chart.ChartX tt := chatTester(760, 420, func(c *ui.Context) { - r := PerformanceChart(c, values, PerformanceChartOptions{Frame: ChartFrame{Label: "Performance"}, Times: times, Version: version}) - memo := ui.Local(r.Element, "mujica.memo.x", func() chartMemoEntry { return chartMemoEntry{} }) - axis = memo.val.(chartX) + r := PerformanceChart(c, values, PerformanceChartOptions{Frame: chart.ChartFrame{Label: "Performance"}, Times: times, Version: version}) + memo := ui.Local(r.Element, "mujica.memo.x", func() chart.ChartMemoEntry { return chart.ChartMemoEntry{} }) + axis = memo.Val.(chart.ChartX) }) times[2] = times[0].AddDate(0, 0, 9) values[2] = 120 version++ tt.Frame() - if got := axis.vals[2]; got != float64(times[2].UnixNano())/1e9 { + if got := axis.Vals[2]; got != float64(times[2].UnixNano())/1e9 { t.Fatalf("axis time %v", got) } } diff --git a/pnl_display.go b/finance/pnl_display.go similarity index 53% rename from pnl_display.go rename to finance/pnl_display.go index 605745d..b05a66e 100644 --- a/pnl_display.go +++ b/finance/pnl_display.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "log/slog" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PnL is profit and loss: realized, unrealized and today's. @@ -24,47 +26,47 @@ type PnLDisplayOptions struct { } // PnLDisplay shows three profits signed; *hidden masks them. -func PnLDisplay(c *ui.Context, pnl PnL, hidden *bool, opts PnLDisplayOptions) ChoiceResult { - dec := priceDecimals("PnLDisplay", opts.Decimals) - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = ui.Row(c).Gap(u * 6).AlignItems(ui.Start).Wrap().Role(ui.RoleGroup).Label(msg(c, "pnl_display.label")) +func PnLDisplay(c *ui.Context, pnl PnL, hidden *bool, opts PnLDisplayOptions) core.ChoiceResult { + dec := core.PriceDecimals("PnLDisplay", opts.Decimals) + k := core.Tokens(c) + u := core.Density(c).Unit() + var r core.ChoiceResult + r.Element = ui.Row(c).Gap(u * 6).AlignItems(ui.Start).Wrap().Role(ui.RoleGroup).Label(core.Msg(c, "pnl_display.label")) r.Element.Children(func() { for _, item := range []struct { key string v float64 }{{"pnl_display.realized", pnl.Realized}, {"pnl_display.unrealized", pnl.Unrealized}, {"pnl_display.today", pnl.Today}} { - title := msg(c, item.key) - sign := trendSign(item.v) - text := statisticSigned(item.v, dec) + title := core.Msg(c, item.key) + sign := core.TrendSign(item.v) + text := data.StatisticSigned(item.v, dec) if opts.Currency != "" { - text = pnlDisplaySign(sign) + opts.Currency + statisticFormat(math.Abs(item.v), dec) + text = pnlDisplaySign(sign) + opts.Currency + data.StatisticFormat(math.Abs(item.v), dec) } - col := trendColor(c, sign) - spoken := title + ", " + trendWord(c, sign) + " " + text + col := core.TrendColor(c, sign) + spoken := title + ", " + core.TrendWord(c, sign) + " " + text if *hidden { - text, col, spoken = "••••", k.TextMuted, title+", "+msg(c, "pnl_display.masked") + text, col, spoken = "••••", k.TextMuted, title+", "+core.Msg(c, "pnl_display.masked") } ui.Column(c).Gap(u).Label(spoken).Role(ui.RoleText).Children(func() { - ui.Text(c, title).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleNone) + ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleNone) ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(col).Role(ui.RoleNone).Children(func() { if !*hidden { - ui.Icon(c, trendIcon(sign)).Size(16, 16) + ui.Icon(c, core.TrendIcon(sign)).Size(16, 16) } - ui.Text(c, text).FontSize(FontSize(c, theme.H3Size)).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Text(c, text).FontSize(core.FontSize(c, theme.H3Size)).FontFeatures("tnum").FontWeight(600).SingleLine() }) }) } - name := msg(c, "pnl_display.hide") + name := core.Msg(c, "pnl_display.hide") icon := icons.Must("eye-off") if *hidden { - name, icon = msg(c, "pnl_display.show"), icons.Must("eye") + name, icon = core.Msg(c, "pnl_display.show"), icons.Must("eye") } - b := iconAction(c, icon, name) + b := core.IconAction(c, icon, name) if b.Clicked() && !b.IsDisabled() { *hidden = !*hidden - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: pnl_display mask", "hidden", *hidden) } }) diff --git a/pnl_display_test.go b/finance/pnl_display_test.go similarity index 89% rename from pnl_display_test.go rename to finance/pnl_display_test.go index b258102..f4a0645 100644 --- a/pnl_display_test.go +++ b/finance/pnl_display_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPnLDisplaySignsAndMask(t *testing.T) { hidden, n := false, 0 - tt := tradingTester(GreenUp, 520, 160, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 520, 160, func(c *ui.Context) { if PnLDisplay(c, PnL{Realized: 1200.5, Unrealized: -340, Today: 0}, &hidden, PnLDisplayOptions{Currency: "$"}).Changed() { n++ } diff --git a/portfolio_summary.go b/finance/portfolio_summary.go similarity index 58% rename from portfolio_summary.go rename to finance/portfolio_summary.go index 219e4d5..713d813 100644 --- a/portfolio_summary.go +++ b/finance/portfolio_summary.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -8,8 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PortfolioAccount is one account's value and profit. @@ -32,14 +35,14 @@ type PortfolioSummaryOptions struct { // PortfolioSummaryView is a built summary; Changed reports another account chosen. -func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccount, opts PortfolioSummaryOptions) ChoiceResult { +func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccount, opts PortfolioSummaryOptions) core.ChoiceResult { if account == nil { panic("mujica: PortfolioSummary needs an account") } - dec := priceDecimals("PortfolioSummary", opts.Decimals) + dec := core.PriceDecimals("PortfolioSummary", opts.Decimals) cur := cmp.Or(opts.Currency, "$") - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var sum PortfolioAccount found := *account == "" for _, a := range accounts { @@ -58,32 +61,32 @@ func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccoun panic("mujica: PortfolioSummary account " + *account + " is not listed") } before := *account - v := ChoiceResult{} + v := core.ChoiceResult{} v.Element = ui.Column(c).Gap(u*3).Padding(u*4).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Disabled(opts.Disabled) v.Element.Children(func() { if len(accounts) > 1 { - items := []RadioItem[string]{{Value: "", Label: msg(c, "portfolio_summary.all")}} + items := []input.RadioItem[string]{{Value: "", Label: core.Msg(c, "portfolio_summary.all")}} for _, a := range accounts { - items = append(items, RadioItem[string]{Value: a.ID, Label: a.Name}) + items = append(items, input.RadioItem[string]{Value: a.ID, Label: a.Name}) } - RadioGroup(c, account, items, RadioGroupOptions{Horizontal: true, Label: msg(c, "portfolio_summary.accounts"), Disabled: opts.Disabled}) + input.RadioGroup(c, account, items, input.RadioGroupOptions{Horizontal: true, Label: core.Msg(c, "portfolio_summary.accounts"), Disabled: opts.Disabled}) } ui.Box(c).FillWidth().Height(9).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, 6, 5, k.Ornament) }) ui.Row(c).Wrap().Gap(u * 8).Children(func() { - portfolioFigure(c, msg(c, "portfolio_summary.total"), func() { - ui.Text(c, cur+statisticFormat(sum.Total, dec)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H1Size)).FontFeatures("tnum") + portfolioFigure(c, core.Msg(c, "portfolio_summary.total"), func() { + ui.Text(c, cur+data.StatisticFormat(sum.Total, dec)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H1Size)).FontFeatures("tnum") }) prev := sum.Total - sum.DayPnL - portfolioFigure(c, msg(c, "portfolio_summary.day"), func() { + portfolioFigure(c, core.Msg(c, "portfolio_summary.day"), func() { portfolioSigned(c, sum.DayPnL, cur, dec, portfolioPercent(sum.DayPnL, prev)) }) - portfolioFigure(c, msg(c, "portfolio_summary.return"), func() { + portfolioFigure(c, core.Msg(c, "portfolio_summary.return"), func() { portfolioSigned(c, sum.TotalReturn, cur, dec, portfolioPercent(sum.TotalReturn, sum.Cost)) }) }) }) - v.changed = *account != before && !v.Element.IsDisabled() + v = core.NewChoiceResult(v.Element, *account != before && !v.Element.IsDisabled()) if v.Changed() { slog.Debug("mujica: portfolio_summary account", "id", *account) } @@ -92,24 +95,24 @@ func PortfolioSummary(c *ui.Context, account *string, accounts []PortfolioAccoun func portfolioFigure(c *ui.Context, title string, value func()) { ui.Column(c).Gap(2).Role(ui.RoleGroup).Label(title).Children(func() { - ui.Text(c, title).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted) + ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted) value() }) } func portfolioSigned(c *ui.Context, v float64, cur string, dec int, pct float64) { - sign := trendSign(v) - text := cur + statisticFormat(math.Abs(v), dec) + sign := core.TrendSign(v) + text := cur + data.StatisticFormat(math.Abs(v), dec) if sign > 0 { text = "+" + text } else if sign < 0 { text = "−" + text } - ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(trendColor(c, sign)).Label(trendWord(c, sign) + " " + text).Children(func() { - ui.Icon(c, trendIcon(sign)).Size(16, 16) - ui.Text(c, text).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum") + ui.Row(c).Gap(4).AlignItems(ui.Center).TextColor(core.TrendColor(c, sign)).Label(core.TrendWord(c, sign) + " " + text).Children(func() { + ui.Icon(c, core.TrendIcon(sign)).Size(16, 16) + ui.Text(c, text).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).FontFeatures("tnum") if !math.IsNaN(pct) { - ui.Text(c, fmt.Sprintf("(%s%%)", statisticSigned(pct, 2))).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf("(%s%%)", data.StatisticSigned(pct, 2))).FontFeatures("tnum") } }) } diff --git a/portfolio_summary_test.go b/finance/portfolio_summary_test.go similarity index 98% rename from portfolio_summary_test.go rename to finance/portfolio_summary_test.go index b939b52..81ceef2 100644 --- a/portfolio_summary_test.go +++ b/finance/portfolio_summary_test.go @@ -1,9 +1,10 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "math" "testing" + + "github.com/egoist/mygo/ui" ) func TestPortfolioSummaryAccountsAndTotals(t *testing.T) { diff --git a/position_table.go b/finance/position_table.go similarity index 65% rename from position_table.go rename to finance/position_table.go index d868894..7534cd6 100644 --- a/position_table.go +++ b/finance/position_table.go @@ -1,10 +1,14 @@ -package mujica +package finance import ( "cmp" "log/slog" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // PositionRow is one holding; ID must be stable across updates. @@ -35,7 +39,7 @@ type PositionTableOptions struct { Decimals int // Version must change when rows change in place. Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -43,7 +47,7 @@ type PositionTableOptions struct { // TableActionResult is a trading table and its acted row. type TableActionResult struct { Element *ui.Element - Table DataTableView + Table data.DataTableView id string } @@ -51,22 +55,22 @@ type TableActionResult struct { func (r TableActionResult) Acted() (string, bool) { return r.id, r.id != "" } // PositionTable lists holdings with floating profit and Close. -func PositionTable(c *ui.Context, s *DataTableState[string], rows []PositionRow, opts PositionTableOptions) TableActionResult { - dec := priceDecimals("PositionTable", opts.Decimals) +func PositionTable(c *ui.Context, s *data.DataTableState[string], rows []PositionRow, opts PositionTableOptions) TableActionResult { + dec := core.PriceDecimals("PositionTable", opts.Decimals) var r TableActionResult - num := func(id, key string, v func(PositionRow) float64) DataColumn[PositionRow] { - return DataColumn[PositionRow]{ID: id, Title: msg(c, key), Align: ui.End, Sortable: true, + num := func(id, key string, v func(PositionRow) float64) data.DataColumn[PositionRow] { + return data.DataColumn[PositionRow]{ID: id, Title: core.Msg(c, key), Align: ui.End, Sortable: true, Compare: func(a, b PositionRow) int { return cmp.Compare(v(a), v(b)) }, - Text: func(p PositionRow) string { return statisticFormat(v(p), dec) }} + Text: func(p PositionRow) string { return data.StatisticFormat(v(p), dec) }} } pnl := num("pnl", "position_table.pnl", PositionRow.Unrealized) - pnl.Cell = func(p PositionRow) { tradingSigned(c, p.Unrealized(), statisticSigned(p.Unrealized(), dec)) } + pnl.Cell = func(p PositionRow) { tradingSigned(c, p.Unrealized(), data.StatisticSigned(p.Unrealized(), dec)) } pct := num("pct", "position_table.pnl_percent", PositionRow.UnrealizedPercent) pct.Cell = func(p PositionRow) { - tradingSigned(c, p.UnrealizedPercent(), statisticSigned(p.UnrealizedPercent(), 2)+"%") + tradingSigned(c, p.UnrealizedPercent(), data.StatisticSigned(p.UnrealizedPercent(), 2)+"%") } - cols := []DataColumn[PositionRow]{ - {ID: "symbol", Title: msg(c, "position_table.symbol"), Sortable: true, + cols := []data.DataColumn[PositionRow]{ + {ID: "symbol", Title: core.Msg(c, "position_table.symbol"), Sortable: true, Compare: func(a, b PositionRow) int { return cmp.Compare(a.Symbol, b.Symbol) }, Text: func(p PositionRow) string { return p.Symbol }}, num("qty", "position_table.quantity", func(p PositionRow) float64 { return p.Quantity }), @@ -74,18 +78,18 @@ func PositionTable(c *ui.Context, s *DataTableState[string], rows []PositionRow, num("price", "position_table.price", func(p PositionRow) float64 { return p.Price }), num("value", "position_table.value", PositionRow.MarketValue), pnl, pct, - {ID: "action", Title: msg(c, "position_table.action"), Width: 96, + {ID: "action", Title: core.Msg(c, "position_table.action"), Width: 96, Text: func(PositionRow) string { return "" }, Cell: func(p PositionRow) { - if Button(c, msg(c, "position_table.close"), ButtonOptions{Variant: Secondary}).Label(msg(c, "position_table.close") + " " + p.Symbol).Clicked() { + if input.Button(c, core.Msg(c, "position_table.close"), input.ButtonOptions{Variant: input.Secondary}).Label(core.Msg(c, "position_table.close") + " " + p.Symbol).Clicked() { r.id = p.ID } }}, } r.Element = ui.Column(c).Disabled(opts.Disabled).Grow(1).FillWidth().MinHeight(0) r.Element.Children(func() { - r.Table = DataTable(c, s, rows, DataTableOptions[PositionRow, string]{Columns: cols, Key: func(p PositionRow) string { return p.ID }, - Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: msg(c, "position_table.empty")}) + r.Table = data.DataTable(c, s, rows, data.DataTableOptions[PositionRow, string]{Columns: cols, Key: func(p PositionRow) string { return p.ID }, + Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "position_table.empty")}) r.Table.Element.Grow(1) }) if r.Element.IsDisabled() { @@ -99,9 +103,9 @@ func PositionTable(c *ui.Context, s *DataTableState[string], rows []PositionRow, // tradingSigned shows a signed amount with its arrow. func tradingSigned(c *ui.Context, v float64, text string) { - sign := trendSign(v) - ui.Row(c).Gap(2).AlignItems(ui.Center).Justify(ui.End).FillWidth().TextColor(trendColor(c, sign)).Children(func() { - ui.Icon(c, trendIcon(sign)).Size(12, 12) + sign := core.TrendSign(v) + ui.Row(c).Gap(2).AlignItems(ui.Center).Justify(ui.End).FillWidth().TextColor(core.TrendColor(c, sign)).Children(func() { + ui.Icon(c, core.TrendIcon(sign)).Size(12, 12) ui.Text(c, text).FontFeatures("tnum").FontWeight(600).SingleLine() }) } diff --git a/position_table_test.go b/finance/position_table_test.go similarity index 80% rename from position_table_test.go rename to finance/position_table_test.go index 95e4a40..7139702 100644 --- a/position_table_test.go +++ b/finance/position_table_test.go @@ -1,10 +1,13 @@ -package mujica +package finance import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func positionSample() []PositionRow { @@ -15,10 +18,10 @@ func positionSample() []PositionRow { } func TestPositionTableValuesAndClose(t *testing.T) { - var s DataTableState[string] + var s data.DataTableState[string] rows := positionSample() closed := "" - tt := tradingTester(GreenUp, 1000, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 400, func(c *ui.Context) { ui.Box(c).Width(960).Height(300).Children(func() { if id, ok := PositionTable(c, &s, rows, PositionTableOptions{}).Acted(); ok { closed = id @@ -40,9 +43,9 @@ func TestPositionTableValuesAndClose(t *testing.T) { // Selection follows the row ID when rows reorder and change. func TestPositionTableStableIDs(t *testing.T) { - var s DataTableState[string] + var s data.DataTableState[string] rows := positionSample() - tt := tradingTester(GreenUp, 1000, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 400, func(c *ui.Context) { ui.Box(c).Width(960).Height(300).Children(func() { PositionTable(c, &s, rows, PositionTableOptions{}) }) }) _ = tt.Click("AVE") @@ -54,13 +57,13 @@ func TestPositionTableStableIDs(t *testing.T) { } func TestPositionTableDisabledAndLargeData(t *testing.T) { - var s DataTableState[string] + var s data.DataTableState[string] rows := make([]PositionRow, 100_000) for i := range rows { rows[i] = PositionRow{ID: fmt.Sprint(i), Symbol: fmt.Sprintf("S%05d", i), Quantity: 1, AvgCost: 1, Price: 2} } closed := false - tt := tradingTester(GreenUp, 1000, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 400, func(c *ui.Context) { ui.Box(c).Width(960).Height(300).Children(func() { _, closed = PositionTable(c, &s, rows, PositionTableOptions{Disabled: true}).Acted() }) @@ -77,7 +80,7 @@ func TestPositionTableDisabledAndLargeData(t *testing.T) { func TestPositionTableSnapshot(t *testing.T) { snapshot(t, "position_table", 1040, 220, func(c *ui.Context) { ui.Column(c).Padding(16).Height(200).Width(1010).Children(func() { - s := ui.Local(c.Root(), "snap", func() DataTableState[string] { return DataTableState[string]{} }) + s := ui.Local(c.Root(), "snap", func() data.DataTableState[string] { return data.DataTableState[string]{} }) PositionTable(c, s, positionSample(), PositionTableOptions{}) }) }) diff --git a/price_change_badge.go b/finance/price_change_badge.go similarity index 51% rename from price_change_badge.go rename to finance/price_change_badge.go index 5989747..51f13aa 100644 --- a/price_change_badge.go +++ b/finance/price_change_badge.go @@ -1,9 +1,11 @@ -package mujica +package finance import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PriceChangeBadgeOptions configure a PriceChangeBadge. @@ -16,22 +18,22 @@ type PriceChangeBadgeOptions struct { // PriceChangeBadge shows a signed change and percent with an arrow. func PriceChangeBadge(c *ui.Context, change, percent float64, opts PriceChangeBadgeOptions) *ui.Element { - dec := priceDecimals("PriceChangeBadge", opts.Decimals) - sign := trendSign(change) + dec := core.PriceDecimals("PriceChangeBadge", opts.Decimals) + sign := core.TrendSign(change) if sign == 0 { - sign = trendSign(percent) + sign = core.TrendSign(percent) } - col := trendColor(c, sign) - u := Density(c).Unit() - text := statisticSigned(change, dec) + col := core.TrendColor(c, sign) + u := core.Density(c).Unit() + text := data.StatisticSigned(change, dec) if !opts.HidePercent { - text += " (" + statisticSigned(percent, 2) + "%)" + text += " (" + data.StatisticSigned(percent, 2) + "%)" } e := ui.Row(c).Gap(u).AlignItems(ui.Center).Padding(u/2, u*2).Radius(theme.ControlRadius).Shrink(0). - Background(col.Alpha(0.12)).TextColor(col).Role(ui.RoleText).Label(trendWord(c, sign) + " " + text) + Background(col.Alpha(0.12)).TextColor(col).Role(ui.RoleText).Label(core.TrendWord(c, sign) + " " + text) e.Children(func() { - ui.Icon(c, trendIcon(sign)).Size(14, 14) - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() + ui.Icon(c, core.TrendIcon(sign)).Size(14, 14) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").FontWeight(600).SingleLine() }) return e } diff --git a/price_change_badge_test.go b/finance/price_change_badge_test.go similarity index 88% rename from price_change_badge_test.go rename to finance/price_change_badge_test.go index 42a6a1d..52b5b4d 100644 --- a/price_change_badge_test.go +++ b/finance/price_change_badge_test.go @@ -1,13 +1,15 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPriceChangeBadgeSignsAndWords(t *testing.T) { - tt := tradingTester(GreenUp, 400, 160, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 160, func(c *ui.Context) { PriceChangeBadge(c, 1.25, 0.85, PriceChangeBadgeOptions{}) PriceChangeBadge(c, -3, -1.5, PriceChangeBadgeOptions{Decimals: 1}) PriceChangeBadge(c, 0, 0, PriceChangeBadgeOptions{HidePercent: true}) diff --git a/finance/price_text.go b/finance/price_text.go new file mode 100644 index 0000000..f6f8999 --- /dev/null +++ b/finance/price_text.go @@ -0,0 +1,84 @@ +package finance + +import ( + "log/slog" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// priceTextFlash is the tint's fade, three state steps long. +const priceTextFlash = 3 * theme.StateDuration + +// PriceTextOptions configure a PriceText. +type PriceTextOptions struct { + // Decimals shown; zero means 2. + Decimals int + // NoGrouping drops the thousands separators. + NoGrouping bool + // Compare marks the price's move from Reference. + Compare bool + Reference float64 + // NoFlash keeps the price still when it changes. + NoFlash bool + // Size is the font size; zero is the body size. + Size float32 +} + +type priceTextState struct { + last float64 + seen bool + at time.Time + dir int +} + +// PriceText shows a price; a change tints it once. +func PriceText(c *ui.Context, value float64, opts PriceTextOptions) *ui.Element { + dec := core.PriceDecimals("PriceText", opts.Decimals) + if opts.Size < 0 { + panic("mujica: PriceText with negative Size") + } + size := opts.Size + if size == 0 { + size = theme.BodySize + } + text := data.StatisticFormat(value, dec) + if opts.NoGrouping { + text = core.PriceUngrouped(value, dec) + } + e := ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Padding(0, 2).Radius(theme.ControlRadius).Shrink(0) + st := ui.Local(e, "price", func() priceTextState { return priceTextState{} }) + if st.seen && value != st.last { + st.at, st.dir = c.Now(), core.TrendSign(value-st.last) + slog.Debug("mujica: price_text changed", "dir", st.dir) + } + st.last, st.seen = value, true + sign := 0 + if opts.Compare { + sign = core.TrendSign(value - opts.Reference) + } + col := core.Tokens(c).Text + spoken := text + if opts.Compare { + col = core.TrendColor(c, sign) + spoken += ", " + core.TrendWord(c, sign) + } + if d := core.Motion(c, priceTextFlash); d > 0 && !opts.NoFlash && !st.at.IsZero() { + if t := c.Now().Sub(st.at); t < d { + e.Background(core.TrendColor(c, st.dir).Alpha(0.2 * float32(1-float64(t)/float64(d)))) + c.AnimationFrame() + } + } + e.Role(ui.RoleText).Label(spoken).TextColor(col) + e.Children(func() { + if opts.Compare { + ui.Icon(c, core.TrendIcon(sign)).Size(size*0.85, size*0.85) + } + ui.Text(c, text).FontSize(core.FontSize(c, size)).FontFeatures("tnum").FontWeight(500).SingleLine() + }) + return e +} diff --git a/price_text_test.go b/finance/price_text_test.go similarity index 73% rename from price_text_test.go rename to finance/price_text_test.go index bb21a2c..4adbd8f 100644 --- a/price_text_test.go +++ b/finance/price_text_test.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "bytes" @@ -7,21 +7,22 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // tradingTester runs view in the default theme with trend t. -func tradingTester(t Trend, w, h int, view func(c *ui.Context)) *ui.Tester { +func tradingTester(t core.Trend, w, h int, view func(c *ui.Context)) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - UseTrend(c, t) + core.Use(c, core.Settings{}) + core.UseTrend(c, t) ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { view(c) }) }, w, h) } func TestPriceTextFormatAndTrend(t *testing.T) { price := 12345.678 - tt := tradingTester(GreenUp, 300, 100, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 300, 100, func(c *ui.Context) { PriceText(c, price, PriceTextOptions{Decimals: 2, Compare: true, Reference: 12000}) }) if !tt.HasText("12,345.68") { @@ -35,7 +36,7 @@ func TestPriceTextFormatAndTrend(t *testing.T) { if _, ok := tt.Find("11,000.00, down"); !ok { t.Fatalf("down not spoken: %q", tt.Texts()) } - tt = tradingTester(GreenUp, 300, 100, func(c *ui.Context) { + tt = tradingTester(core.GreenUp, 300, 100, func(c *ui.Context) { PriceText(c, 1234.5, PriceTextOptions{Decimals: 3, NoGrouping: true}) }) if !tt.HasText("1234.500") { @@ -45,9 +46,9 @@ func TestPriceTextFormatAndTrend(t *testing.T) { func TestTrendColorsFollowUseTrend(t *testing.T) { var up, down [2]ui.Color - for i, tr := range []Trend{GreenUp, RedUp} { + for i, tr := range []core.Trend{core.GreenUp, core.RedUp} { tradingTester(tr, 100, 100, func(c *ui.Context) { - up[i], down[i] = trendColor(c, 1), trendColor(c, -1) + up[i], down[i] = core.TrendColor(c, 1), core.TrendColor(c, -1) }) } k := theme.Light() @@ -59,7 +60,7 @@ func TestTrendColorsFollowUseTrend(t *testing.T) { t.Fatal("unknown trend accepted") } }() - tradingTester(Trend(7), 100, 100, func(*ui.Context) {}) + tradingTester(core.Trend(7), 100, 100, func(*ui.Context) {}) } // A change tints once; reduced motion never tints. @@ -67,11 +68,11 @@ func TestPriceTextFlashesOnceAndNotUnderReducedMotion(t *testing.T) { for _, still := range []bool{false, true} { prefs := ui.Preferences{ReduceMotion: still, TextScale: 1} price := 10.0 - tt := tradingTester(GreenUp, 200, 60, func(c *ui.Context) { PriceText(c, price, PriceTextOptions{}) }) + tt := tradingTester(core.GreenUp, 200, 60, func(c *ui.Context) { PriceText(c, price, PriceTextOptions{}) }) tt.SetPreferences(prefs) price = 10.5 tt.Frame() - fresh := tradingTester(GreenUp, 200, 60, func(c *ui.Context) { PriceText(c, price, PriceTextOptions{}) }) + fresh := tradingTester(core.GreenUp, 200, 60, func(c *ui.Context) { PriceText(c, price, PriceTextOptions{}) }) fresh.SetPreferences(prefs) fresh.Frame() same := bytes.Equal(tt.Image().Pix, fresh.Image().Pix) @@ -92,7 +93,7 @@ func TestPriceTextRejectsNegativeDecimals(t *testing.T) { t.Fatal("negative decimals accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { PriceText(c, 1, PriceTextOptions{Decimals: -1}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { PriceText(c, 1, PriceTextOptions{Decimals: -1}) }) } func TestPriceTextSnapshot(t *testing.T) { diff --git a/quick_trade_buttons.go b/finance/quick_trade_buttons.go similarity index 68% rename from quick_trade_buttons.go rename to finance/quick_trade_buttons.go index d0926c1..48dc868 100644 --- a/quick_trade_buttons.go +++ b/finance/quick_trade_buttons.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,8 +7,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QuickTradeButtonsOptions configure QuickTradeButtons. Symbol is required. @@ -54,27 +58,27 @@ func QuickTradeButtons(c *ui.Context, quantity *float64, opts QuickTradeButtonsO if at < 0 { panic(fmt.Sprintf("mujica: QuickTradeButtons quantity %v is not one of Presets", *quantity)) } - u := Density(c).Unit() + u := core.Density(c).Unit() var r QuickTradeResult - r.Element = ui.Column(c).Gap(u * 2).Disabled(opts.Disabled).Role(ui.RoleGroup).Label(msg(c, "quick_trade_buttons.label")) + r.Element = ui.Column(c).Gap(u * 2).Disabled(opts.Disabled).Role(ui.RoleGroup).Label(core.Msg(c, "quick_trade_buttons.label")) st := ui.Local(r.Element, "quick", func() quickTradeState { return quickTradeState{} }) disabled := r.Element.IsDisabled() if disabled { st.asking = 0 } - qty := statisticFormat(*quantity, opts.Decimals) + qty := data.StatisticFormat(*quantity, opts.Decimals) r.Element.Children(func() { - var segs []Segment + var segs []navigation.Segment for _, p := range presets { - segs = append(segs, Segment{Label: statisticFormat(p, opts.Decimals)}) + segs = append(segs, navigation.Segment{Label: data.StatisticFormat(p, opts.Decimals)}) } - if SegmentedControl(c, &at, segs, SegmentedControlOptions{Equal: true, Label: msg(c, "quick_trade_buttons.quantity"), Disabled: disabled}).Changed() { + if navigation.SegmentedControl(c, &at, segs, navigation.SegmentedControlOptions{Equal: true, Label: core.Msg(c, "quick_trade_buttons.quantity"), Disabled: disabled}).Changed() { *quantity = presets[at] - qty = statisticFormat(*quantity, opts.Decimals) + qty = data.StatisticFormat(*quantity, opts.Decimals) } ui.Row(c).Gap(u * 2).Children(func() { for _, side := range []OrderSide{SideBuy, SideSell} { - label := fmt.Sprintf(msg(c, fmt.Sprintf("quick_trade_buttons.side%d", side)), qty) + label := fmt.Sprintf(core.Msg(c, fmt.Sprintf("quick_trade_buttons.side%d", side)), qty) b := quickTradeButton(c, label, side) if b.Clicked() && !disabled { if opts.Confirm { @@ -84,10 +88,10 @@ func QuickTradeButtons(c *ui.Context, quantity *float64, opts QuickTradeButtonsO } } open := st.asking == side - res := Popconfirm(c, b, &open, PopconfirmOptions{ - Title: fmt.Sprintf(msg(c, fmt.Sprintf("quick_trade_buttons.confirm%d", side)), qty, opts.Symbol), - Description: msg(c, "quick_trade_buttons.market"), - ConfirmLabel: msg(c, fmt.Sprintf("order_entry.side%d", side)), + res := overlay.Popconfirm(c, b, &open, overlay.PopconfirmOptions{ + Title: fmt.Sprintf(core.Msg(c, fmt.Sprintf("quick_trade_buttons.confirm%d", side)), qty, opts.Symbol), + Description: core.Msg(c, "quick_trade_buttons.market"), + ConfirmLabel: core.Msg(c, fmt.Sprintf("order_entry.side%d", side)), Danger: true, }) if res.Confirmed && !disabled { @@ -107,9 +111,9 @@ func QuickTradeButtons(c *ui.Context, quantity *float64, opts QuickTradeButtonsO // quickTradeButton is a large button tinted by its side. func quickTradeButton(c *ui.Context, label string, side OrderSide) *ui.Element { - k := Tokens(c) - d := Density(c) - col := trendColor(c, side.sign()) + k := core.Tokens(c) + d := core.Density(c) + col := core.TrendColor(c, side.sign()) b := ui.ButtonBase(c).Grow(1).Basis(0).MinHeight(d.ControlHeight()+d.Unit()*2).Radius(theme.ControlRadius). Border(theme.BorderWidth*1.5, col).TextColor(col).Background(col.Alpha(0.1)).Label(label) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -120,7 +124,7 @@ func quickTradeButton(c *ui.Context, label string, side OrderSide) *ui.Element { b.Background(col.Alpha(0.18)) } b.Children(func() { - ui.Icon(c, trendIcon(side.sign())).Size(16, 16) + ui.Icon(c, core.TrendIcon(side.sign())).Size(16, 16) ui.Text(c, label).FontWeight(700).SingleLine() }) return b diff --git a/quick_trade_buttons_test.go b/finance/quick_trade_buttons_test.go similarity index 94% rename from quick_trade_buttons_test.go rename to finance/quick_trade_buttons_test.go index c764999..cc96f8d 100644 --- a/quick_trade_buttons_test.go +++ b/finance/quick_trade_buttons_test.go @@ -1,9 +1,11 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type quickTradeProbe struct { @@ -14,7 +16,7 @@ type quickTradeProbe struct { } func (p *quickTradeProbe) tester() *ui.Tester { - return tradingTester(GreenUp, 520, 400, func(c *ui.Context) { + return tradingTester(core.GreenUp, 520, 400, func(c *ui.Context) { ui.Box(c).Width(400).Children(func() { if side, q, ok := QuickTradeButtons(c, &p.qty, p.opts).Traded(); ok { p.trades, p.sizes = append(p.trades, side), append(p.sizes, q) diff --git a/quote_card.go b/finance/quote_card.go similarity index 75% rename from quote_card.go rename to finance/quote_card.go index a35e886..cf5ceaf 100644 --- a/quote_card.go +++ b/finance/quote_card.go @@ -1,9 +1,10 @@ -package mujica +package finance import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Quote is an instrument's price and change since the close. @@ -27,16 +28,16 @@ func QuoteCard(c *ui.Context, q Quote, opts QuoteCardOptions) *ui.Element { if q.Symbol == "" { panic("mujica: QuoteCard needs a Symbol") } - dec := priceDecimals("QuoteCard", opts.Decimals) - k := Tokens(c) - u := Density(c).Unit() + dec := core.PriceDecimals("QuoteCard", opts.Decimals) + k := core.Tokens(c) + u := core.Density(c).Unit() e := ui.Column(c).Gap(u*2).Padding(u*4).MinWidth(200).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup).Label(q.Symbol) e.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.End).Children(func() { - ui.Text(c, q.Symbol).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).SingleLine() + ui.Text(c, q.Symbol).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).SingleLine() if q.Name != "" { - ui.Text(c, q.Name).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine(). + ui.Text(c, q.Name).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine(). Ellipsis("…").MinWidth(0).Shrink(1) } }) diff --git a/quote_card_test.go b/finance/quote_card_test.go similarity index 81% rename from quote_card_test.go rename to finance/quote_card_test.go index 6d53d20..8973726 100644 --- a/quote_card_test.go +++ b/finance/quote_card_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestQuoteCardShowsQuoteAndChartSlot(t *testing.T) { charted := false - tt := tradingTester(RedUp, 400, 240, func(c *ui.Context) { + tt := tradingTester(core.RedUp, 400, 240, func(c *ui.Context) { QuoteCard(c, Quote{Symbol: "600519", Name: "Kweichow Moutai", Price: 1688.5, Change: 12.3, ChangePercent: 0.73}, QuoteCardOptions{Chart: func() { charted = true; ui.Text(c, "chart") }}) }) @@ -25,7 +27,7 @@ func TestQuoteCardShowsQuoteAndChartSlot(t *testing.T) { t.Fatal("empty symbol accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { QuoteCard(c, Quote{}, QuoteCardOptions{}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { QuoteCard(c, Quote{}, QuoteCardOptions{}) }) } func TestQuoteCardSnapshot(t *testing.T) { @@ -33,7 +35,7 @@ func TestQuoteCardSnapshot(t *testing.T) { ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { QuoteCard(c, Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35}, QuoteCardOptions{Chart: func() { - k := Tokens(c) + k := core.Tokens(c) ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) { p.Line(r.X, r.Y+r.H*0.3, r.X+r.W, r.Y+r.H*0.7, 1.5, k.Info) }) diff --git a/risk_meter.go b/finance/risk_meter.go similarity index 77% rename from risk_meter.go rename to finance/risk_meter.go index d0a5685..6ff7096 100644 --- a/risk_meter.go +++ b/finance/risk_meter.go @@ -1,12 +1,13 @@ -package mujica +package finance import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RiskLevel is one step of a RiskMeter's scale. @@ -28,26 +29,26 @@ func RiskMeter(c *ui.Context, level int, opts RiskMeterOptions) *ui.Element { levels := opts.Levels if levels == nil { for i := range 5 { - levels = append(levels, RiskLevel{Label: msg(c, fmt.Sprintf("risk_meter.level%d", i+1))}) + levels = append(levels, RiskLevel{Label: core.Msg(c, fmt.Sprintf("risk_meter.level%d", i+1))}) } } if len(levels) < 2 || level < 1 || level > len(levels) { panic(fmt.Sprintf("mujica: RiskMeter level %d outside 1..%d", level, len(levels))) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() label := opts.Label if label == "" { - label = msg(c, "risk_meter.label") + label = core.Msg(c, "risk_meter.label") } cur := levels[level-1] - spoken := fmt.Sprintf(msg(c, "risk_meter.value"), level, len(levels), cur.Label) + spoken := fmt.Sprintf(core.Msg(c, "risk_meter.value"), level, len(levels), cur.Label) col := riskMeterColor(k, level, len(levels)) e := ui.Column(c).Gap(u * 2).MinWidth(200).Role(ui.RoleMeter).Label(label).Description(spoken + " " + cur.Description) e.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Text(c, cur.Label).TextColor(col).FontWeight(600).SingleLine() - ui.Text(c, fmt.Sprintf("%d / %d", level, len(levels))).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf("%d / %d", level, len(levels))).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") }) ui.Box(c).Height(18).FillWidth().Draw(func(p *ui.Painter, r ui.Rect) { n := len(levels) @@ -66,7 +67,7 @@ func RiskMeter(c *ui.Context, level int, opts RiskMeterOptions) *ui.Element { } }) if cur.Description != "" { - ui.Text(c, cur.Description).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, cur.Description).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) } }) return e diff --git a/risk_meter_test.go b/finance/risk_meter_test.go similarity index 83% rename from risk_meter_test.go rename to finance/risk_meter_test.go index 83058ed..2fce45a 100644 --- a/risk_meter_test.go +++ b/finance/risk_meter_test.go @@ -1,16 +1,18 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestRiskMeterLevelsAndText(t *testing.T) { level := 4 levels := []RiskLevel{{Label: "Conservative", Description: "Capital first."}, {Label: "Balanced"}, {Label: "Aggressive", Description: "Losses can exceed half."}} custom := false - tt := tradingTester(GreenUp, 400, 160, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 160, func(c *ui.Context) { if custom { RiskMeter(c, level, RiskMeterOptions{Levels: levels}) } else { @@ -30,7 +32,7 @@ func TestRiskMeterLevelsAndText(t *testing.T) { t.Fatal("level 0 accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { RiskMeter(c, 0, RiskMeterOptions{}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { RiskMeter(c, 0, RiskMeterOptions{}) }) } func TestRiskMeterSnapshot(t *testing.T) { diff --git a/screener.go b/finance/screener.go similarity index 64% rename from screener.go rename to finance/screener.go index 9da5112..4065025 100644 --- a/screener.go +++ b/finance/screener.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -9,8 +9,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type ScreenerField struct { @@ -63,7 +67,7 @@ func ScreenerMatch(row ScreenerRow, conds []ScreenerCondition) bool { // ScreenerState is the conditions and the result table's state. type ScreenerState struct { Conditions []ScreenerCondition - Table DataTableState[string] + Table data.DataTableState[string] cache screenerCache } @@ -76,7 +80,7 @@ type screenerCache struct { type ScreenerOptions struct { // Version must change when rows change in place. Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -85,7 +89,7 @@ type ScreenerOptions struct { type ScreenerView struct { Element *ui.Element Changed, Save bool - Table DataTableView + Table data.DataTableView } func Screener(c *ui.Context, s *ScreenerState, fields []ScreenerField, rows []ScreenerRow, opts ScreenerOptions) ScreenerView { @@ -104,8 +108,8 @@ func Screener(c *ui.Context, s *ScreenerState, fields []ScreenerField, rows []Sc panic("mujica: Screener condition is invalid") } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() before := fmt.Sprint(s.Conditions) var v ScreenerView v.Element = ui.Column(c).Gap(u * 3).Grow(1).MinHeight(0).Disabled(opts.Disabled) @@ -116,29 +120,29 @@ func Screener(c *ui.Context, s *ScreenerState, fields []ScreenerField, rows []Sc s = © } v.Element.Children(func() { - fieldItems := make([]SelectOption[string], len(fields)) + fieldItems := make([]input.SelectOption[string], len(fields)) for i, f := range fields { - fieldItems[i] = SelectOption[string]{Value: f.ID, Label: f.Label} + fieldItems[i] = input.SelectOption[string]{Value: f.ID, Label: f.Label} } - opItems := make([]SelectOption[ScreenerOp], len(screenerOps)) + opItems := make([]input.SelectOption[ScreenerOp], len(screenerOps)) for i, o := range screenerOps { - opItems[i] = SelectOption[ScreenerOp]{Value: ScreenerOp(i), Label: o + " " + msg(c, fmt.Sprintf("screener.op_%d", i))} + opItems[i] = input.SelectOption[ScreenerOp]{Value: ScreenerOp(i), Label: o + " " + core.Msg(c, fmt.Sprintf("screener.op_%d", i))} } ui.Column(c).Gap(u*2).Padding(u*3).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). - Role(ui.RoleGroup).Label(msg(c, "screener.conditions")).Children(func() { + Role(ui.RoleGroup).Label(core.Msg(c, "screener.conditions")).Children(func() { if len(s.Conditions) == 0 { - ui.Text(c, msg(c, "screener.none")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "screener.none")).TextColor(k.TextMuted) } for i := 0; i < len(s.Conditions); i++ { cd := &s.Conditions[i] f := screenerField(fields, cd.Field) removed := false ui.Row(c).Key(i).Gap(u * 2).AlignItems(ui.Center).Children(func() { - Select(c, &cd.Field, fieldItems, SelectOptions{Label: msg(c, "screener.field"), Disabled: opts.Disabled}) - Select(c, &cd.Op, opItems, SelectOptions{Label: msg(c, "screener.compare"), Disabled: opts.Disabled}).Element.MinWidth(120) - NumberInput(c, &cd.Value, NumberInputOptions{Step: math.Pow(10, -float64(f.Decimals)), Precision: f.Decimals, - Label: msg(c, "screener.value"), Disabled: opts.Disabled}).Element.Width(150) - x := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "screener.remove"), i+1)) + input.Select(c, &cd.Field, fieldItems, input.SelectOptions{Label: core.Msg(c, "screener.field"), Disabled: opts.Disabled}) + input.Select(c, &cd.Op, opItems, input.SelectOptions{Label: core.Msg(c, "screener.compare"), Disabled: opts.Disabled}).Element.MinWidth(120) + input.NumberInput(c, &cd.Value, input.NumberInputOptions{Step: math.Pow(10, -float64(f.Decimals)), Precision: f.Decimals, + Label: core.Msg(c, "screener.value"), Disabled: opts.Disabled}).Element.Width(150) + x := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "screener.remove"), i+1)) removed = x.Clicked() && !x.IsDisabled() }) if removed { @@ -147,25 +151,25 @@ func Screener(c *ui.Context, s *ScreenerState, fields []ScreenerField, rows []Sc } } ui.Row(c).Gap(u * 2).Children(func() { - if Button(c, msg(c, "screener.add"), ButtonOptions{Variant: Secondary, Icon: icons.Must("plus"), Disabled: opts.Disabled}).Clicked() { + if input.Button(c, core.Msg(c, "screener.add"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("plus"), Disabled: opts.Disabled}).Clicked() { s.Conditions = append(s.Conditions, ScreenerCondition{Field: fields[0].ID}) } - v.Save = Button(c, msg(c, "screener.save"), ButtonOptions{Variant: Ghost, Disabled: opts.Disabled}).Clicked() + v.Save = input.Button(c, core.Msg(c, "screener.save"), input.ButtonOptions{Variant: input.Ghost, Disabled: opts.Disabled}).Clicked() }) }) matched := screenerFilter(s, rows, opts.Version) - ui.Text(c, fmt.Sprintf(msg(c, "screener.matches"), len(matched), len(rows))).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, fmt.Sprintf(core.Msg(c, "screener.matches"), len(matched), len(rows))).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).Role(ui.RoleStatus) - cols := []DataColumn[ScreenerRow]{{ID: "symbol", Title: msg(c, "screener.symbol"), Sortable: true, + cols := []data.DataColumn[ScreenerRow]{{ID: "symbol", Title: core.Msg(c, "screener.symbol"), Sortable: true, Compare: func(a, b ScreenerRow) int { return cmp.Compare(a.Symbol, b.Symbol) }, Text: func(r ScreenerRow) string { return r.Symbol }}} for _, f := range fields { - cols = append(cols, DataColumn[ScreenerRow]{ID: f.ID, Title: f.Label, Align: ui.End, Sortable: true, + cols = append(cols, data.DataColumn[ScreenerRow]{ID: f.ID, Title: f.Label, Align: ui.End, Sortable: true, Compare: func(a, b ScreenerRow) int { return cmp.Compare(a.Values[f.ID], b.Values[f.ID]) }, - Text: func(r ScreenerRow) string { return statisticFormat(r.Values[f.ID], f.Decimals) }}) + Text: func(r ScreenerRow) string { return data.StatisticFormat(r.Values[f.ID], f.Decimals) }}) } - v.Table = DataTable(c, &s.Table, matched, DataTableOptions[ScreenerRow, string]{Columns: cols, Key: func(r ScreenerRow) string { return r.Symbol }, - Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: msg(c, "screener.empty")}) + v.Table = data.DataTable(c, &s.Table, matched, data.DataTableOptions[ScreenerRow, string]{Columns: cols, Key: func(r ScreenerRow) string { return r.Symbol }, + Version: opts.Version, Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "screener.empty")}) v.Table.Element.Grow(1) }) v.Changed = fmt.Sprint(s.Conditions) != before @@ -191,7 +195,7 @@ func screenerField(fields []ScreenerField, id string) ScreenerField { } func screenerFilter(s *ScreenerState, rows []ScreenerRow, version int) []ScreenerRow { - key := fmt.Sprint(chartID(rows), version, s.Conditions) + key := fmt.Sprint(chart.ChartID(rows), version, s.Conditions) if key != s.cache.key { s.cache.key, s.cache.rows = key, nil for _, r := range rows { diff --git a/screener_test.go b/finance/screener_test.go similarity index 99% rename from screener_test.go rename to finance/screener_test.go index cc95661..22b5480 100644 --- a/screener_test.go +++ b/finance/screener_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestScreenerConditionsResultsSave(t *testing.T) { diff --git a/sentiment_gauge.go b/finance/sentiment_gauge.go similarity index 69% rename from sentiment_gauge.go rename to finance/sentiment_gauge.go index 6cc1c23..5681be3 100644 --- a/sentiment_gauge.go +++ b/finance/sentiment_gauge.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SentimentGaugeOptions configure a SentimentGauge. @@ -16,7 +19,7 @@ type SentimentGaugeOptions struct { Label string // Size is the dial width; zero is 240 DIPs. Size float32 - Status DataStatus + Status data.DataStatus Error string Empty bool Retry func() @@ -38,25 +41,25 @@ func SentimentGauge(c *ui.Context, value float64, opts SentimentGaugeOptions) *u if w == 0 { w = 240 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() dialHeight := max(w/2+8, u*30) band := sentimentBand(value) - word := msg(c, sentimentBands[band]) + word := core.Msg(c, sentimentBands[band]) label := fmt.Sprintf("%.0f, %s", value, word) - if opts.Status == DataLoading || opts.Status == DataLoadingMore || opts.Status == DataUnloaded { - label = msg(c, "list.loading") - } else if opts.Status == DataFailed { - label = dataError(c, opts.Error) + if opts.Status == data.DataLoading || opts.Status == data.DataLoadingMore || opts.Status == data.DataUnloaded { + label = core.Msg(c, "list.loading") + } else if opts.Status == data.DataFailed { + label = data.DataError(c, opts.Error) } else if opts.Empty { - label = msg(c, "chart.empty") + label = core.Msg(c, "chart.empty") } if opts.Label != "" { label = opts.Label + ": " + label } pal := internal.Palette(k) cols := [5]ui.Color{pal[0], pal[3], pal[5], pal[1], pal[2]} - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) col := ui.Column(c).Gap(u).AlignItems(ui.Center).Role(ui.RoleMeter).Label(label).Focusable() internal.Ring(col, k.Focus, theme.ControlRadius) if !col.IsDisabled() { @@ -70,15 +73,15 @@ func SentimentGauge(c *ui.Context, value float64, opts SentimentGaugeOptions) *u if opts.Label != "" { ui.Text(c, opts.Label).Font(theme.SerifFont).TextColor(k.TextMuted) } - if opts.Status != DataReady || opts.Empty { + if opts.Status != data.DataReady || opts.Empty { ui.Box(c).Size(w, dialHeight).Children(func() { ui.Column(c).Fill().Children(func() { - if dataPlaceholder(c, opts.Status, opts.Error, "", msg(c, "chart.empty")) && opts.Retry != nil { + if data.DataPlaceholder(c, opts.Status, opts.Error, "", core.Msg(c, "chart.empty")) && opts.Retry != nil { opts.Retry() } }) }) - ui.Box(c).Height(FontSize(c, theme.H1Size)*theme.LineHeight + u + FontSize(c, theme.BodySize)*theme.LineHeight) + ui.Box(c).Height(core.FontSize(c, theme.H1Size)*theme.LineHeight + u + core.FontSize(c, theme.BodySize)*theme.LineHeight) return } ui.Box(c).Size(w, dialHeight).Draw(func(p *ui.Painter, r ui.Rect) { @@ -94,10 +97,10 @@ func SentimentGauge(c *ui.Context, value float64, opts SentimentGaugeOptions) *u needle.MoveTo(internal.Snap(p, cx), internal.Snap(p, cy)).LineTo(internal.Snap(p, tx), internal.Snap(p, ty)) p.StrokePath(&needle, 2.5, k.Text) p.FillPath(internal.Diamond(internal.Snap(p, cx), internal.Snap(p, cy), 10), k.Text) - chartText(p, "0", r.X+2, cy+4, size, k.TextMuted, 0, 0) - chartText(p, "100", r.X+r.W-2, cy+4, size, k.TextMuted, 1, 0) + chart.ChartText(p, "0", r.X+2, cy+4, size, k.TextMuted, 0, 0) + chart.ChartText(p, "100", r.X+r.W-2, cy+4, size, k.TextMuted, 1, 0) }) - ui.Text(c, fmt.Sprintf("%.0f", value)).Font(theme.SerifFont).FontSize(FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf("%.0f", value)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum") ui.Text(c, word).FontWeight(600).TextColor(k.Text) }) return col diff --git a/sentiment_gauge_test.go b/finance/sentiment_gauge_test.go similarity index 91% rename from sentiment_gauge_test.go rename to finance/sentiment_gauge_test.go index d2466be..077b543 100644 --- a/sentiment_gauge_test.go +++ b/finance/sentiment_gauge_test.go @@ -1,8 +1,11 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestSentimentGaugeBandsAndBoundary(t *testing.T) { @@ -34,7 +37,7 @@ func TestSentimentGaugeKeyboardReadout(t *testing.T) { } func TestSentimentGaugeStatesKeepSize(t *testing.T) { - status := DataReady + status := data.DataReady empty := false retried := 0 var element *ui.Element @@ -42,18 +45,18 @@ func TestSentimentGaugeStatesKeepSize(t *testing.T) { element = SentimentGauge(c, 72, SentimentGaugeOptions{Label: "Sentiment", Status: status, Empty: empty, Error: "Caller failed", Retry: func() { retried++ }}) }) height := element.Bounds().H - status = DataLoading + status = data.DataLoading tt.Frame() if element.Bounds().H != height { t.Fatalf("loading height %v, ready %v", element.Bounds().H, height) } - status = DataFailed + status = data.DataFailed tt.Frame() _ = tt.Click("Retry") if retried != 1 { t.Fatalf("retries %d", retried) } - status = DataReady + status = data.DataReady empty = true tt.Frame() if !tt.HasText("No data") || element.Bounds().H != height { diff --git a/spread_indicator.go b/finance/spread_indicator.go similarity index 57% rename from spread_indicator.go rename to finance/spread_indicator.go index be7c7fa..b954310 100644 --- a/spread_indicator.go +++ b/finance/spread_indicator.go @@ -1,12 +1,14 @@ -package mujica +package finance import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SpreadIndicatorOptions configure a SpreadIndicator. @@ -22,30 +24,30 @@ func SpreadIndicator(c *ui.Context, bid, ask float64, opts SpreadIndicatorOption if opts.WarnPercent < 0 { panic("mujica: SpreadIndicator with negative WarnPercent") } - dec := priceDecimals("SpreadIndicator", opts.Decimals) - k := Tokens(c) - u := Density(c).Unit() + dec := core.PriceDecimals("SpreadIndicator", opts.Decimals) + k := core.Tokens(c) + u := core.Density(c).Unit() spread := ask - bid pct := 0.0 if mid := (ask + bid) / 2; mid > 0 { pct = spread / mid * 100 } - text := fmt.Sprintf(msg(c, "spread_indicator.value"), statisticFormat(spread, dec), statisticFormat(pct, 3)) + text := fmt.Sprintf(core.Msg(c, "spread_indicator.value"), data.StatisticFormat(spread, dec), data.StatisticFormat(pct, 3)) note, col, icon := "", k.TextMuted, "" switch { case spread < 0: - note, col, icon = msg(c, "spread_indicator.crossed"), k.Danger, "circle-alert" + note, col, icon = core.Msg(c, "spread_indicator.crossed"), k.Danger, "circle-alert" case opts.WarnPercent > 0 && pct >= opts.WarnPercent: - note, col, icon = msg(c, "spread_indicator.wide"), k.Warning, "triangle-alert" + note, col, icon = core.Msg(c, "spread_indicator.wide"), k.Warning, "triangle-alert" } e := ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Shrink(0).Role(ui.RoleStatus).Label(text + " " + note) e.Children(func() { - ui.Text(c, msg(c, "spread_indicator.title")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, "spread_indicator.title")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) ui.Text(c, text).FontFeatures("tnum").FontWeight(500).SingleLine() if note != "" { ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(col).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() }) } }) diff --git a/spread_indicator_test.go b/finance/spread_indicator_test.go similarity index 78% rename from spread_indicator_test.go rename to finance/spread_indicator_test.go index c9835e4..55b6fb9 100644 --- a/spread_indicator_test.go +++ b/finance/spread_indicator_test.go @@ -1,14 +1,16 @@ -package mujica +package finance import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSpreadIndicatorValueAndThreshold(t *testing.T) { bid, ask := 99.95, 100.05 - tt := tradingTester(GreenUp, 400, 100, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 100, func(c *ui.Context) { SpreadIndicator(c, bid, ask, SpreadIndicatorOptions{WarnPercent: 0.5}) }) if !tt.HasText("0.10 (0.100%)") || tt.HasText("Wide spread") { @@ -29,7 +31,7 @@ func TestSpreadIndicatorValueAndThreshold(t *testing.T) { t.Fatal("negative threshold accepted") } }() - tradingTester(GreenUp, 100, 100, func(c *ui.Context) { SpreadIndicator(c, 1, 2, SpreadIndicatorOptions{WarnPercent: -1}) }) + tradingTester(core.GreenUp, 100, 100, func(c *ui.Context) { SpreadIndicator(c, 1, 2, SpreadIndicatorOptions{WarnPercent: -1}) }) } func TestSpreadIndicatorSnapshot(t *testing.T) { diff --git a/symbol_badge.go b/finance/symbol_badge.go similarity index 81% rename from symbol_badge.go rename to finance/symbol_badge.go index fd46479..3a28af0 100644 --- a/symbol_badge.go +++ b/finance/symbol_badge.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "log/slog" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MarketSymbol is a tradable instrument as the caller lists it. @@ -26,12 +27,12 @@ type SymbolBadgeOptions struct { } // SymbolBadge shows a code with its market and type tags. -func SymbolBadge(c *ui.Context, s MarketSymbol, opts SymbolBadgeOptions) ChoiceResult { +func SymbolBadge(c *ui.Context, s MarketSymbol, opts SymbolBadgeOptions) core.ChoiceResult { if s.Code == "" { panic("mujica: SymbolBadge needs a Code") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var e *ui.Element wrap := ui.Row(c).Shrink(0).Disabled(opts.Disabled) wrap.Children(func() { @@ -41,7 +42,7 @@ func SymbolBadge(c *ui.Context, s MarketSymbol, opts SymbolBadgeOptions) ChoiceR if e.Hovered() { e.Background(k.SurfaceHover) } - e.Transition(colorFade) + e.Transition(core.ColorFade) } else { e = ui.Row(c).Role(ui.RoleText) } @@ -53,7 +54,7 @@ func SymbolBadge(c *ui.Context, s MarketSymbol, opts SymbolBadgeOptions) ChoiceR for _, tag := range []string{s.Market, s.Type} { if tag != "" { ui.Row(c).Padding(1, u).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Border).Background(k.Background).Children(func() { - ui.Text(c, tag).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, tag).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) } } @@ -62,7 +63,7 @@ func SymbolBadge(c *ui.Context, s MarketSymbol, opts SymbolBadgeOptions) ChoiceR if clicked { slog.Debug("mujica: symbol_badge click", "code", s.Code) } - return ChoiceResult{Element: e, changed: clicked} + return core.NewChoiceResult(e, clicked) } // symbolBadgeName reads code, name, market and type. diff --git a/symbol_badge_test.go b/finance/symbol_badge_test.go similarity index 98% rename from symbol_badge_test.go rename to finance/symbol_badge_test.go index 1a8ae62..9232317 100644 --- a/symbol_badge_test.go +++ b/finance/symbol_badge_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestSymbolBadgeClickKeysAndDisabled(t *testing.T) { diff --git a/symbol_search.go b/finance/symbol_search.go similarity index 71% rename from symbol_search.go rename to finance/symbol_search.go index 2a3a2c5..e09fd30 100644 --- a/symbol_search.go +++ b/finance/symbol_search.go @@ -1,13 +1,16 @@ -package mujica +package finance import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type SymbolSearchState struct { @@ -42,8 +45,8 @@ func SymbolSearch(c *ui.Context, s *SymbolSearchState, results []MarketSymbol, o if s == nil { panic("mujica: SymbolSearch needs a state") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var v SymbolSearchView v.Element = ui.Column(c).Gap(u * 2).Disabled(opts.Disabled) opts.Disabled = v.Element.IsDisabled() @@ -53,18 +56,18 @@ func SymbolSearch(c *ui.Context, s *SymbolSearchState, results []MarketSymbol, o } v.Element.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - in := SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "symbol_search.placeholder"), Disabled: opts.Disabled}) + in := input.SearchInput(c, &s.Query, input.SearchInputOptions{Placeholder: core.Msg(c, "symbol_search.placeholder"), Disabled: opts.Disabled}) in.Element.Grow(1) v.Changed = in.Changed() if in.Input.Shortcut(0, ui.KeyDown) && !in.Input.IsDisabled() { s.focusFirst = true } if opts.Markets != nil { - items := []SelectOption[string]{{Value: "", Label: msg(c, "symbol_search.all_markets")}} + items := []input.SelectOption[string]{{Value: "", Label: core.Msg(c, "symbol_search.all_markets")}} for _, m := range opts.Markets { - items = append(items, SelectOption[string]{Value: m, Label: m}) + items = append(items, input.SelectOption[string]{Value: m, Label: m}) } - r := Select(c, &s.Market, items, SelectOptions{Label: msg(c, "symbol_search.market"), Disabled: opts.Disabled}) + r := input.Select(c, &s.Market, items, input.SelectOptions{Label: core.Msg(c, "symbol_search.market"), Disabled: opts.Disabled}) r.Element.MinWidth(140) v.Changed = v.Changed || r.Changed() } @@ -75,20 +78,20 @@ func SymbolSearch(c *ui.Context, s *SymbolSearchState, results []MarketSymbol, o ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) ui.Text(c, opts.Error).Role(ui.RoleStatus).Grow(1) - v.Retry = Button(c, msg(c, "symbol_search.retry"), ButtonOptions{Variant: Secondary}).Clicked() + v.Retry = input.Button(c, core.Msg(c, "symbol_search.retry"), input.ButtonOptions{Variant: input.Secondary}).Clicked() }) return case opts.Loading && len(results) == 0: - Spinner(c, SpinnerOptions{Label: msg(c, "symbol_search.searching"), ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "symbol_search.searching"), ShowLabel: true}) return case s.Query == "": - list, title = opts.Recent, msg(c, "symbol_search.recent") + list, title = opts.Recent, core.Msg(c, "symbol_search.recent") case len(results) == 0: - ui.Text(c, msg(c, "symbol_search.no_results")).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "symbol_search.no_results")).TextColor(k.TextMuted).Role(ui.RoleStatus) return } if title != "" && len(list) > 0 { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } rows := ui.Local(v.Element, "mujica.symbol.rows", func() ui.ListState { return ui.ListState{} }) rows.Key = func(i int) any { return list[i].Market + ":" + list[i].Code } @@ -98,7 +101,7 @@ func SymbolSearch(c *ui.Context, s *SymbolSearchState, results []MarketSymbol, o s.cursor = 0 rows.ScrollTo(0, ui.Start) } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) resultList := ui.List(c, rows, len(list), func(i int) { if symbolSearchRow(c, list[i], i == s.cursor) { s.cursor = i @@ -126,8 +129,8 @@ func SymbolSearch(c *ui.Context, s *SymbolSearchState, results []MarketSymbol, o } func symbolSearchRow(c *ui.Context, sym MarketSymbol, selected bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b := ui.ButtonBase(c).Key(sym.Market+":"+sym.Code).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 3). Justify(ui.Start).Radius(theme.ControlRadius).Label(symbolBadgeName(sym)).Role(ui.RoleListItem) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -142,7 +145,7 @@ func symbolSearchRow(c *ui.Context, sym MarketSymbol, selected bool) bool { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) } if sym.Market != "" { - ui.Text(c, sym.Market).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, sym.Market).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) return b.Clicked() && !b.IsDisabled() diff --git a/symbol_search_test.go b/finance/symbol_search_test.go similarity index 99% rename from symbol_search_test.go rename to finance/symbol_search_test.go index 014a01e..aa7ef7c 100644 --- a/symbol_search_test.go +++ b/finance/symbol_search_test.go @@ -1,8 +1,9 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" + + "github.com/egoist/mygo/ui" ) func TestSymbolSearchResultsAndAsync(t *testing.T) { diff --git a/ticker_tape.go b/finance/ticker_tape.go similarity index 75% rename from ticker_tape.go rename to finance/ticker_tape.go index 67cc954..13824ae 100644 --- a/ticker_tape.go +++ b/finance/ticker_tape.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TickerItem is one symbol on a TickerTape. @@ -43,7 +45,7 @@ func TickerTape(c *ui.Context, items []TickerItem, opts TickerTapeOptions) *ui.E if opts.Speed < 0 || opts.PerPage < 0 { panic("mujica: TickerTape Speed and PerPage cannot be negative") } - dec := priceDecimals("TickerTape", opts.Decimals) + dec := core.PriceDecimals("TickerTape", opts.Decimals) speed, per := opts.Speed, opts.PerPage if speed == 0 { speed = 40 @@ -53,10 +55,10 @@ func TickerTape(c *ui.Context, items []TickerItem, opts TickerTapeOptions) *ui.E } label := opts.Label if label == "" { - label = msg(c, "ticker_tape.label") + label = core.Msg(c, "ticker_tape.label") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) e := ui.Row(c).MinHeight(d.ControlHeight()).AlignItems(ui.Center).Background(k.Surface). BorderWidth(theme.BorderWidth, 0).BorderColor(k.Border).Focusable().Role(ui.RoleGroup).Label(label) e.FocusRing(false) @@ -78,7 +80,7 @@ func TickerTape(c *ui.Context, items []TickerItem, opts TickerTapeOptions) *ui.E if !paused { c.AnimationFrame() } - e.Description(msg(c, "ticker_tape.pause_hint")) + e.Description(core.Msg(c, "ticker_tape.pause_hint")) e.ClipX().Children(func() { strip := ui.Row(c).Absolute().Top(0).Bottom(0).AlignItems(ui.Center) if w := strip.Bounds().W / 2; w > 0 { @@ -120,7 +122,7 @@ func tickerTapePages(c *ui.Context, e *ui.Element, st *tickerTapeState, items [] if e.Shortcut(0, ui.KeyLeft) { turn(-1) } - k := Tokens(c) + k := core.Tokens(c) e.Children(func() { ui.Row(c).Grow(1).MinWidth(0).ClipX().AlignItems(ui.Center).Children(func() { for _, it := range items[st.page*per : min(len(items), (st.page+1)*per)] { @@ -130,11 +132,11 @@ func tickerTapePages(c *ui.Context, e *ui.Element, st *tickerTapeState, items [] if pages < 2 { return } - if iconAction(c, icons.Must("chevron-left"), msg(c, "ticker_tape.previous")).Clicked() { + if core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "ticker_tape.previous")).Clicked() { turn(-1) } - ui.Text(c, fmt.Sprintf("%d / %d", st.page+1, pages)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") - if iconAction(c, icons.Must("chevron-right"), msg(c, "ticker_tape.next")).Clicked() { + ui.Text(c, fmt.Sprintf("%d / %d", st.page+1, pages)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + if core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "ticker_tape.next")).Clicked() { turn(1) } }) @@ -142,14 +144,14 @@ func tickerTapePages(c *ui.Context, e *ui.Element, st *tickerTapeState, items [] // tickerTapeItem is one symbol, price and change. func tickerTapeItem(c *ui.Context, it TickerItem, dec int) { - u := Density(c).Unit() - sign := trendSign(it.ChangePercent) + u := core.Density(c).Unit() + sign := core.TrendSign(it.ChangePercent) ui.Row(c).Gap(u*1.5).AlignItems(ui.Center).Padding(0, u*4).Shrink(0).Children(func() { ui.Text(c, it.Symbol).FontWeight(600).SingleLine() - ui.Text(c, statisticFormat(it.Price, dec)).FontFeatures("tnum").SingleLine() - ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(trendColor(c, sign)).Label(trendWord(c, sign) + " " + statisticSigned(it.ChangePercent, 2) + "%").Children(func() { - ui.Icon(c, trendIcon(sign)).Size(12, 12) - ui.Text(c, statisticSigned(it.ChangePercent, 2)+"%").FontFeatures("tnum").SingleLine() + ui.Text(c, data.StatisticFormat(it.Price, dec)).FontFeatures("tnum").SingleLine() + ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(core.TrendColor(c, sign)).Label(core.TrendWord(c, sign) + " " + data.StatisticSigned(it.ChangePercent, 2) + "%").Children(func() { + ui.Icon(c, core.TrendIcon(sign)).Size(12, 12) + ui.Text(c, data.StatisticSigned(it.ChangePercent, 2)+"%").FontFeatures("tnum").SingleLine() }) }) } diff --git a/ticker_tape_test.go b/finance/ticker_tape_test.go similarity index 90% rename from ticker_tape_test.go rename to finance/ticker_tape_test.go index 12073db..ba1eb55 100644 --- a/ticker_tape_test.go +++ b/finance/ticker_tape_test.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func tickerTapeItems(n int) []TickerItem { @@ -26,7 +28,7 @@ func tickerTapeX(t *testing.T, tt *ui.Tester) float32 { } func TestTickerTapeScrollsAndPausesOnHover(t *testing.T) { - tt := tradingTester(GreenUp, 400, 80, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 80, func(c *ui.Context) { ui.Box(c).Width(380).Children(func() { TickerTape(c, tickerTapeItems(6), TickerTapeOptions{Speed: 200}) }) }) x0 := tickerTapeX(t, tt) @@ -47,7 +49,7 @@ func TestTickerTapeScrollsAndPausesOnHover(t *testing.T) { } func TestTickerTapePausesOnFocus(t *testing.T) { - tt := tradingTester(GreenUp, 400, 80, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 400, 80, func(c *ui.Context) { ui.Box(c).Width(380).Children(func() { TickerTape(c, tickerTapeItems(6), TickerTapeOptions{Speed: 200}) }) }) tt.Key(0, ui.KeyTab) @@ -63,7 +65,7 @@ func TestTickerTapePausesOnFocus(t *testing.T) { } func TestTickerTapePagesUnderReducedMotion(t *testing.T) { - tt := tradingTester(GreenUp, 500, 80, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 500, 80, func(c *ui.Context) { TickerTape(c, tickerTapeItems(5), TickerTapeOptions{PerPage: 2}) }) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/time_and_sales.go b/finance/time_and_sales.go similarity index 63% rename from time_and_sales.go rename to finance/time_and_sales.go index 0a6c186..d937538 100644 --- a/time_and_sales.go +++ b/finance/time_and_sales.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -7,8 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TradePrint is one executed trade on the tape. @@ -29,7 +32,7 @@ type TimeAndSalesOptions struct { LargeSize float64 // Height of the list in DIPs; zero means 320. Height float32 - Status DataStatus + Status data.DataStatus Error string } @@ -39,12 +42,12 @@ type TimeAndSalesState struct { Paused bool frozen int shown int - list ListState[string] + list data.ListState[string] } // TimeAndSales lists trades newest on top; Paused holds the view. -func TimeAndSales(c *ui.Context, s *TimeAndSalesState, trades []TradePrint, opts TimeAndSalesOptions) ChoiceResult { - dec := priceDecimals("TimeAndSales", opts.Decimals) +func TimeAndSales(c *ui.Context, s *TimeAndSalesState, trades []TradePrint, opts TimeAndSalesOptions) core.ChoiceResult { + dec := core.PriceDecimals("TimeAndSales", opts.Decimals) if opts.SizeDecimals < 0 || opts.LargeSize < 0 || opts.Height < 0 { panic("mujica: TimeAndSales SizeDecimals, LargeSize and Height cannot be negative") } @@ -61,28 +64,28 @@ func TimeAndSales(c *ui.Context, s *TimeAndSalesState, trades []TradePrint, opts s.list.ScrollTo(0) } s.shown = n - k := Tokens(c) - d := Density(c) - var r ChoiceResult + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult r.Element = ui.Column(c).Gap(d.Unit() * 2).Height(height) r.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "time_and_sales.title")).Font(theme.SerifFont).FontWeight(600).Grow(1) - label := msg(c, "time_and_sales.pause") + ui.Text(c, core.Msg(c, "time_and_sales.title")).Font(theme.SerifFont).FontWeight(600).Grow(1) + label := core.Msg(c, "time_and_sales.pause") if s.Paused { - label = msg(c, "time_and_sales.resume") + label = core.Msg(c, "time_and_sales.resume") if waiting := len(trades) - n; waiting > 0 { - label = fmt.Sprintf(msg(c, "time_and_sales.resume_count"), waiting) + label = fmt.Sprintf(core.Msg(c, "time_and_sales.resume_count"), waiting) } } - if Button(c, label, ButtonOptions{Variant: Secondary}).Clicked() { - s.Paused, r.changed = !s.Paused, true + if input.Button(c, label, input.ButtonOptions{Variant: input.Secondary}).Clicked() { + s.Paused, r = !s.Paused, core.NewChoiceResult(r.Element, true) slog.Debug("mujica: time_and_sales follow", "paused", s.Paused) } }) ui.Row(c).Padding(0, d.Unit()*4).Gap(d.Unit() * 2).Children(func() { for i, key := range []string{"time_and_sales.time", "time_and_sales.price", "time_and_sales.size"} { - t := ui.Text(c, msg(c, key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1).Basis(0) + t := ui.Text(c, core.Msg(c, key)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Grow(1).Basis(0) if i > 0 { t.TextAlign(ui.End) } @@ -90,13 +93,13 @@ func TimeAndSales(c *ui.Context, s *TimeAndSalesState, trades []TradePrint, opts ui.Box(c).Width(16) }) at := func(i int) TradePrint { return trades[n-1-i] } - List(c, &s.list, n, ListOptions[string]{ + data.List(c, &s.list, n, data.ListOptions[string]{ Key: func(i int) string { return at(i).ID }, Label: func(i int) string { t := at(i) return timeAndSalesSpoken(c, t, dec, opts) }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "time_and_sales.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "time_and_sales.empty"), }, func(i int) { timeAndSalesRow(c, at(i), dec, opts) }).Element.Grow(1) }) return r @@ -108,25 +111,25 @@ func timeAndSalesLarge(t TradePrint, opts TimeAndSalesOptions) bool { // timeAndSalesSpoken reads a trade with its side and size. func timeAndSalesSpoken(c *ui.Context, t TradePrint, dec int, opts TimeAndSalesOptions) string { - s := fmt.Sprintf("%s %s %s %s", t.Time.Format("15:04:05"), msg(c, fmt.Sprintf("order_book.side%d", t.Side)), - statisticFormat(t.Price, dec), statisticFormat(t.Size, opts.SizeDecimals)) + s := fmt.Sprintf("%s %s %s %s", t.Time.Format("15:04:05"), core.Msg(c, fmt.Sprintf("order_book.side%d", t.Side)), + data.StatisticFormat(t.Price, dec), data.StatisticFormat(t.Size, opts.SizeDecimals)) if timeAndSalesLarge(t, opts) { - s += ", " + msg(c, "time_and_sales.large") + s += ", " + core.Msg(c, "time_and_sales.large") } return s } // timeAndSalesRow is one trade with its side and large mark. func timeAndSalesRow(c *ui.Context, t TradePrint, dec int, opts TimeAndSalesOptions) { - k := Tokens(c) + k := core.Tokens(c) large := timeAndSalesLarge(t, opts) sign := t.Side.sign() ui.Text(c, t.Time.Format("15:04:05")).FontFeatures("tnum").TextColor(k.TextMuted).Grow(1).Basis(0).SingleLine() - ui.Row(c).Gap(2).Justify(ui.End).AlignItems(ui.Center).Grow(1).Basis(0).TextColor(trendColor(c, sign)).Children(func() { - ui.Icon(c, trendIcon(sign)).Size(12, 12) - ui.Text(c, statisticFormat(t.Price, dec)).FontFeatures("tnum").SingleLine() + ui.Row(c).Gap(2).Justify(ui.End).AlignItems(ui.Center).Grow(1).Basis(0).TextColor(core.TrendColor(c, sign)).Children(func() { + ui.Icon(c, core.TrendIcon(sign)).Size(12, 12) + ui.Text(c, data.StatisticFormat(t.Price, dec)).FontFeatures("tnum").SingleLine() }) - size := ui.Text(c, statisticFormat(t.Size, opts.SizeDecimals)).FontFeatures("tnum").TextAlign(ui.End).Grow(1).Basis(0).SingleLine() + size := ui.Text(c, data.StatisticFormat(t.Size, opts.SizeDecimals)).FontFeatures("tnum").TextAlign(ui.End).Grow(1).Basis(0).SingleLine() mark := ui.Box(c).Size(16, 16).Shrink(0) if large { size.FontWeight(700) diff --git a/time_and_sales_test.go b/finance/time_and_sales_test.go similarity index 89% rename from time_and_sales_test.go rename to finance/time_and_sales_test.go index 5bb8056..48922a9 100644 --- a/time_and_sales_test.go +++ b/finance/time_and_sales_test.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func timeAndSalesTrades(n int) []TradePrint { @@ -25,7 +27,7 @@ func TestTimeAndSalesNewestFirstAndLargeMark(t *testing.T) { var s TimeAndSalesState trades := timeAndSalesTrades(3) trades[1].Size = 5000 - tt := tradingTester(GreenUp, 500, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 500, 400, func(c *ui.Context) { TimeAndSales(c, &s, trades, TimeAndSalesOptions{LargeSize: 1000}) }) newest, _ := tt.Find("09:30:02") @@ -45,7 +47,7 @@ func TestTimeAndSalesPauseHoldsView(t *testing.T) { var s TimeAndSalesState trades := timeAndSalesTrades(2) n := 0 - tt := tradingTester(GreenUp, 500, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 500, 400, func(c *ui.Context) { if TimeAndSales(c, &s, trades, TimeAndSalesOptions{}).Changed() { n++ } @@ -67,7 +69,7 @@ func TestTimeAndSalesPauseHoldsView(t *testing.T) { func TestTimeAndSalesBuildsOnlyVisibleRows(t *testing.T) { var s TimeAndSalesState trades := timeAndSalesTrades(100_000) - tt := tradingTester(GreenUp, 500, 400, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 500, 400, func(c *ui.Context) { TimeAndSales(c, &s, trades, TimeAndSalesOptions{}) }) if !tt.HasText("13:16:39") { diff --git a/trade_history_table.go b/finance/trade_history_table.go similarity index 59% rename from trade_history_table.go rename to finance/trade_history_table.go index 2f64054..f3745ff 100644 --- a/trade_history_table.go +++ b/finance/trade_history_table.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "cmp" @@ -8,7 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // FillRow is one of the account's fills; ID is stable. @@ -28,7 +32,7 @@ type TradeHistoryTableOptions struct { Decimals, QuantityDecimals int // Version must change when rows change in place. Version int - Status DataStatus + Status data.DataStatus Error string Disabled bool } @@ -36,15 +40,15 @@ type TradeHistoryTableOptions struct { // TradeHistoryTableState is the date range and table state. type TradeHistoryTableState struct { // Range keeps fills on its days; zero keeps all. - Range DateRange - Table DataTableState[string] + Range datetime.DateRange + Table data.DataTableState[string] cache tradingRows[FillRow] } // TradeHistoryResult is a built table and its export request. type TradeHistoryResult struct { Element *ui.Element - Table DataTableView + Table data.DataTableView exported []FillRow export bool } @@ -54,7 +58,7 @@ func (r TradeHistoryResult) Exported() ([]FillRow, bool) { return r.exported, r. // TradeHistoryTable lists fills by date range, with export. func TradeHistoryTable(c *ui.Context, s *TradeHistoryTableState, rows []FillRow, opts TradeHistoryTableOptions) TradeHistoryResult { - dec := priceDecimals("TradeHistoryTable", opts.Decimals) + dec := core.PriceDecimals("TradeHistoryTable", opts.Decimals) if opts.QuantityDecimals < 0 { panic("mujica: TradeHistoryTable with negative QuantityDecimals") } @@ -63,47 +67,47 @@ func TradeHistoryTable(c *ui.Context, s *TradeHistoryTableState, rows []FillRow, if rng.IsZero() { return true } - d := DateOf(f.Time) + d := core.DateOf(f.Time) return d.Compare(rng.Start) >= 0 && d.Compare(rng.End) <= 0 }) - num := func(id, key string, dp int, v func(FillRow) float64) DataColumn[FillRow] { - return DataColumn[FillRow]{ID: id, Title: msg(c, key), Align: ui.End, Sortable: true, + num := func(id, key string, dp int, v func(FillRow) float64) data.DataColumn[FillRow] { + return data.DataColumn[FillRow]{ID: id, Title: core.Msg(c, key), Align: ui.End, Sortable: true, Compare: func(a, b FillRow) int { return cmp.Compare(v(a), v(b)) }, - Text: func(f FillRow) string { return statisticFormat(v(f), dp) }} + Text: func(f FillRow) string { return data.StatisticFormat(v(f), dp) }} } - cols := []DataColumn[FillRow]{ - {ID: "time", Title: msg(c, "trade_history_table.time"), Sortable: true, Width: 180, + cols := []data.DataColumn[FillRow]{ + {ID: "time", Title: core.Msg(c, "trade_history_table.time"), Sortable: true, Width: 180, Compare: func(a, b FillRow) int { return a.Time.Compare(b.Time) }, Text: func(f FillRow) string { return f.Time.Format("2006-01-02 15:04:05") }}, - {ID: "symbol", Title: msg(c, "position_table.symbol"), Sortable: true, + {ID: "symbol", Title: core.Msg(c, "position_table.symbol"), Sortable: true, Compare: func(a, b FillRow) int { return cmp.Compare(a.Symbol, b.Symbol) }, Text: func(f FillRow) string { return f.Symbol }}, - {ID: "side", Title: msg(c, "order_table.side"), Text: func(f FillRow) string { return msg(c, fmt.Sprintf("order_entry.side%d", f.Side)) }, + {ID: "side", Title: core.Msg(c, "order_table.side"), Text: func(f FillRow) string { return core.Msg(c, fmt.Sprintf("order_entry.side%d", f.Side)) }, Cell: func(f FillRow) { tradingSide(c, f.Side) }}, num("price", "order_book.price", dec, func(f FillRow) float64 { return f.Price }), num("qty", "order_entry.quantity", opts.QuantityDecimals, func(f FillRow) float64 { return f.Quantity }), num("fee", "trade_history_table.fee", dec, func(f FillRow) float64 { return f.Fee }), } - d := Density(c) + d := core.Density(c) var r TradeHistoryResult r.Element = ui.Column(c).Gap(d.Unit() * 2).Disabled(opts.Disabled).Grow(1).FillWidth().MinHeight(0) disabled := r.Element.IsDisabled() r.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - if DateRangePicker(c, &s.Range, DateRangePickerOptions{Label: msg(c, "trade_history_table.range"), Disabled: disabled}).Changed() { + if datetime.DateRangePicker(c, &s.Range, datetime.DateRangePickerOptions{Label: core.Msg(c, "trade_history_table.range"), Disabled: disabled}).Changed() { slog.Debug("mujica: trade_history_table range", "days", s.Range.Days()) } - if !s.Range.IsZero() && Button(c, msg(c, "trade_history_table.all"), ButtonOptions{Variant: Ghost, Disabled: disabled}).Clicked() && !disabled { - s.Range = DateRange{} + if !s.Range.IsZero() && input.Button(c, core.Msg(c, "trade_history_table.all"), input.ButtonOptions{Variant: input.Ghost, Disabled: disabled}).Clicked() && !disabled { + s.Range = datetime.DateRange{} } ui.Box(c).Grow(1) - if Button(c, msg(c, "trade_history_table.export"), ButtonOptions{Variant: Secondary, Icon: icons.Must("upload"), Disabled: disabled || len(shown) == 0}).Clicked() && !disabled { + if input.Button(c, core.Msg(c, "trade_history_table.export"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("upload"), Disabled: disabled || len(shown) == 0}).Clicked() && !disabled { r.exported, r.export = shown, true slog.Debug("mujica: trade_history_table export", "rows", len(shown)) } }) - r.Table = DataTable(c, &s.Table, shown, DataTableOptions[FillRow, string]{Columns: cols, Key: func(f FillRow) string { return f.ID }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "trade_history_table.empty")}) + r.Table = data.DataTable(c, &s.Table, shown, data.DataTableOptions[FillRow, string]{Columns: cols, Key: func(f FillRow) string { return f.ID }, + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "trade_history_table.empty")}) r.Table.Element.Grow(1) }) return r diff --git a/trade_history_table_test.go b/finance/trade_history_table_test.go similarity index 84% rename from trade_history_table_test.go rename to finance/trade_history_table_test.go index c2a07b8..d46a7b7 100644 --- a/trade_history_table_test.go +++ b/finance/trade_history_table_test.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) func tradeHistorySample() []FillRow { @@ -18,9 +21,9 @@ func tradeHistorySample() []FillRow { } func TestTradeHistoryTableRangeAndExport(t *testing.T) { - s := TradeHistoryTableState{Range: DateRange{Start: Date{2026, 10, 2}, End: Date{2026, 10, 4}}} + s := TradeHistoryTableState{Range: datetime.DateRange{Start: core.Date{Year: 2026, Month: 10, Day: 2}, End: core.Date{Year: 2026, Month: 10, Day: 4}}} var exported []FillRow - tt := tradingTester(GreenUp, 1000, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 600, func(c *ui.Context) { ui.Box(c).Width(960).Height(400).Children(func() { if rows, ok := TradeHistoryTable(c, &s, tradeHistorySample(), TradeHistoryTableOptions{}).Exported(); ok { exported = rows @@ -51,7 +54,7 @@ func TestTradeHistoryTableRangeAndExport(t *testing.T) { func TestTradeHistoryTableDisabled(t *testing.T) { var s TradeHistoryTableState exported := false - tt := tradingTester(GreenUp, 1000, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 600, func(c *ui.Context) { ui.Box(c).Width(960).Height(400).Children(func() { _, exported = TradeHistoryTable(c, &s, tradeHistorySample(), TradeHistoryTableOptions{Disabled: true}).Exported() }) @@ -78,7 +81,7 @@ func TestTradeHistoryTableLargeData(t *testing.T) { for i := range rows { rows[i] = FillRow{ID: fmt.Sprint(i), Time: time.Date(2026, 1, 1, 0, 0, i, 0, time.UTC), Symbol: "S", Side: SideSell, Price: 1, Quantity: 1} } - tt := tradingTester(GreenUp, 1000, 600, func(c *ui.Context) { + tt := tradingTester(core.GreenUp, 1000, 600, func(c *ui.Context) { ui.Box(c).Width(960).Height(400).Children(func() { TradeHistoryTable(c, &s, rows, TradeHistoryTableOptions{}) }) }) if n := len(tt.Texts()); n > 400 { diff --git a/trading_session_clock.go b/finance/trading_session_clock.go similarity index 91% rename from trading_session_clock.go rename to finance/trading_session_clock.go index 28cccef..d98a95c 100644 --- a/trading_session_clock.go +++ b/finance/trading_session_clock.go @@ -1,12 +1,13 @@ -package mujica +package finance import ( "time" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type TradingSession struct { @@ -33,9 +34,9 @@ func TradingSessionClock(c *ui.Context, markets []SessionMarket, opts TradingSes if now.IsZero() { now = c.Now() } - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize) g := ui.Grid(c).ColumnTracks(ui.FitContent(), ui.FitContent(), ui.Fr(1)).GapX(u * 4).GapY(u * 3).AlignItems(ui.Center) g.Children(func() { for _, m := range markets { @@ -63,7 +64,7 @@ func TradingSessionClock(c *ui.Context, markets []SessionMarket, opts TradingSes x := internal.Snap(p, bar.X+bar.W*minute/1440) p.FillPath(internal.Diamond(x, bar.Y+bar.H+3, 8), k.Text) p.Line(x, bar.Y-2, x, bar.Y+bar.H, 1.5, k.Text) - }).Role(ui.RoleImage).Label(m.Name + " " + msg(c, marketPhases[phase].key)) + }).Role(ui.RoleImage).Label(m.Name + " " + core.Msg(c, marketPhases[phase].key)) } }) // Times and phases move on the minute. diff --git a/trading_session_clock_test.go b/finance/trading_session_clock_test.go similarity index 99% rename from trading_session_clock_test.go rename to finance/trading_session_clock_test.go index 6e3c0c1..03c9493 100644 --- a/trading_session_clock_test.go +++ b/finance/trading_session_clock_test.go @@ -1,9 +1,10 @@ -package mujica +package finance import ( - "github.com/egoist/mygo/ui" "testing" "time" + + "github.com/egoist/mygo/ui" ) func TestTradingSessionClockZonesBoundariesAndHoliday(t *testing.T) { diff --git a/watchlist.go b/finance/watchlist.go similarity index 77% rename from watchlist.go rename to finance/watchlist.go index 1c6c104..773076e 100644 --- a/watchlist.go +++ b/finance/watchlist.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "log/slog" @@ -6,9 +6,13 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // WatchItem is a watched symbol with its latest quote. @@ -65,12 +69,12 @@ func Watchlist(c *ui.Context, s *WatchlistState, groups []WatchGroup, opts Watch if s == nil || len(groups) == 0 { panic("mujica: Watchlist needs a state and a group") } - dec := priceDecimals("Watchlist", opts.Decimals) + dec := core.PriceDecimals("Watchlist", opts.Decimals) if s.Group < 0 || s.Group >= len(groups) { panic("mujica: Watchlist Group out of range") } - d := Density(c) + d := core.Density(c) var v WatchlistView v.Element = ui.Column(c).Gap(d.Unit() * 2).Grow(1).MinHeight(0).Disabled(opts.Disabled) opts.Disabled = v.Element.IsDisabled() @@ -83,17 +87,17 @@ func Watchlist(c *ui.Context, s *WatchlistState, groups []WatchGroup, opts Watch } v.Element.Children(func() { ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - tabs := make([]Tab, len(groups)) + tabs := make([]navigation.Tab, len(groups)) for i, g := range groups { - tabs[i] = Tab{Label: g.Name} + tabs[i] = navigation.Tab{Label: g.Name} } ui.Column(c).Grow(1).MinWidth(0).Children(func() { - Tabs(c, &s.Group, tabs, TabsOptions{Label: msg(c, "watchlist.groups")}) + navigation.Tabs(c, &s.Group, tabs, navigation.TabsOptions{Label: core.Msg(c, "watchlist.groups")}) }) - DropdownMenu(c, "", DropdownMenuOptions{Icon: icons.Must("settings"), Label: msg(c, "watchlist.columns"), Disabled: opts.Disabled}, func(m *PopupMenu) { + overlay.DropdownMenu(c, "", overlay.DropdownMenuOptions{Icon: icons.Must("settings"), Label: core.Msg(c, "watchlist.columns"), Disabled: opts.Disabled}, func(m *overlay.PopupMenu) { for col, key := range watchColumns { on := !s.Hidden[WatchColumn(col)] - if m.Check(msg(c, key), &on, PopupMenuItemOptions{KeepOpen: true}) { + if m.Check(core.Msg(c, key), &on, overlay.PopupMenuItemOptions{KeepOpen: true}) { s.Hidden[WatchColumn(col)] = !on } } @@ -115,8 +119,8 @@ func Watchlist(c *ui.Context, s *WatchlistState, groups []WatchGroup, opts Watch } func watchlistRows(c *ui.Context, s *WatchlistState, items []WatchItem, dec int, v *WatchlistView, disabled bool) (changed, submitted bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) s.cursor = slices.IndexFunc(items, func(it WatchItem) bool { return it.Symbol == s.Selected }) before := s.cursor cursor := &s.cursor @@ -132,10 +136,10 @@ func watchlistRows(c *ui.Context, s *WatchlistState, items []WatchItem, dec int, s.rows.Reorder = func(rows []int, to int) { v.Moved, v.Rows, v.To = true, slices.Clone(rows), to } } if len(items) == 0 { - ui.Text(c, msg(c, "watchlist.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) + ui.Text(c, core.Msg(c, "watchlist.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) return false, false } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) list := ui.List(c, &s.rows, len(items), func(i int) { it := items[i] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { @@ -143,7 +147,7 @@ func watchlistRows(c *ui.Context, s *WatchlistState, items []WatchItem, dec int, ui.Column(c).Grow(1).MinWidth(80).Children(func() { ui.Text(c, it.Symbol).Font(theme.MonoFont).FontWeight(700).SingleLine() if it.Name != "" { - ui.Text(c, it.Name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, it.Name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if !s.Hidden[WatchPrice] { @@ -155,7 +159,7 @@ func watchlistRows(c *ui.Context, s *WatchlistState, items []WatchItem, dec int, }) } if !s.Hidden[WatchVolume] { - ui.Text(c, statisticFormat(it.Volume, 0)).Width(90).TextAlign(ui.End).FontFeatures("tnum").TextColor(k.TextMuted) + ui.Text(c, data.StatisticFormat(it.Volume, 0)).Width(90).TextAlign(ui.End).FontFeatures("tnum").TextColor(k.TextMuted) } if items[i].Symbol == s.Selected { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) diff --git a/watchlist_test.go b/finance/watchlist_test.go similarity index 97% rename from watchlist_test.go rename to finance/watchlist_test.go index 389764a..713fd5f 100644 --- a/watchlist_test.go +++ b/finance/watchlist_test.go @@ -1,4 +1,4 @@ -package mujica +package finance import ( "fmt" @@ -6,11 +6,13 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func watchlistTester(s *WatchlistState, groups *[]WatchGroup, opts WatchlistOptions, got *WatchlistView) *ui.Tester { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Height(400).Children(func() { v := Watchlist(c, s, *groups, opts) if v.Changed || v.Submitted || v.Moved { diff --git a/ansi_text.go b/git/ansi_text.go similarity index 89% rename from ansi_text.go rename to git/ansi_text.go index 0795f99..47822fb 100644 --- a/ansi_text.go +++ b/git/ansi_text.go @@ -1,12 +1,13 @@ -package mujica +package git import ( "strings" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AnsiTextOptions configure an AnsiText. @@ -17,7 +18,7 @@ type AnsiTextOptions struct { // AnsiText renders terminal output with its SGR styles. func AnsiText(c *ui.Context, text string, opts AnsiTextOptions) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) col := ui.Column(c).Font(theme.MonoFont).TextColor(k.Text).Shrink(0) col.Children(func() { var st internal.AnsiStyle diff --git a/ansi_text_test.go b/git/ansi_text_test.go similarity index 91% rename from ansi_text_test.go rename to git/ansi_text_test.go index 1107284..ed6f963 100644 --- a/ansi_text_test.go +++ b/git/ansi_text_test.go @@ -1,17 +1,18 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestAnsiTextStripsSGRKeepsUnknown(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AnsiText(c, "\x1b[1;31merror\x1b[0m: bad\n\x1b[2Kcleared", AnsiTextOptions{}) }, 400, 100) if !tt.HasText("error: bad") { diff --git a/blame_view.go b/git/blame_view.go similarity index 86% rename from blame_view.go rename to git/blame_view.go index 0bac5bc..1a3df55 100644 --- a/blame_view.go +++ b/git/blame_view.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BlameLine is a source line and its last commit. @@ -53,8 +54,8 @@ func BlameView(c *ui.Context, s *BlameViewState, lines []BlameLine, opts BlameVi if layout == "" { layout = "2006-01-02" } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() digits := float32(len(strconv.Itoa(len(lines)))) s.list.Label = func(i int) string { return lines[i].Text } @@ -70,22 +71,22 @@ func BlameView(c *ui.Context, s *BlameViewState, lines []BlameLine, opts BlameVi } row.Children(func() { gut := ui.Row(c).Width(240).Shrink(0).AlignItems(ui.Center).Gap(u*2).Padding(0, u*3).Background(k.Surface). - Label(fmt.Sprintf(msg(c, "blame_view.line"), i+1, ln.Author, gitShortHash(ln.Hash))) + Label(fmt.Sprintf(core.Msg(c, "blame_view.line"), i+1, ln.Author, gitShortHash(ln.Hash))) gut.Tooltip(fmt.Sprintf("%s\n%s · %s\n%s", ln.Hash, ln.Author, ln.When.Format(layout), ln.Summary)) if gut.Hovered() { gut.Background(k.SurfaceHover) } - gut.Transition(colorFade) + gut.Transition(core.ColorFade) gut.Children(func() { if !head { return } - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) ui.Text(c, gitShortHash(ln.Hash)).Font(theme.MonoFont).FontSize(small).TextColor(k.AccentText).Shrink(0) ui.Text(c, ln.Author).FontSize(small).SingleLine().Grow(1).MinWidth(0) ui.Text(c, ln.When.Format(layout)).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted).Shrink(0) }) - ui.Text(c, strconv.Itoa(i+1)).Font(theme.MonoFont).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). + ui.Text(c, strconv.Itoa(i+1)).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). TextColor(k.TextMuted).Width(digits*8+u*4).TextAlign(ui.End).Padding(u, u*2, 0, 0).Shrink(0) spans, _ := internal.CodeSpans(k, opts.Language, ln.Text, s.blocks[i]) ui.RichText(c, spans...).Font(theme.MonoFont).NoWrap().Padding(u, u*2).AlignSelf(ui.Center) diff --git a/blame_view_test.go b/git/blame_view_test.go similarity index 94% rename from blame_view_test.go rename to git/blame_view_test.go index 3120bf7..6cd981d 100644 --- a/blame_view_test.go +++ b/git/blame_view_test.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func blameSample() []BlameLine { @@ -27,7 +29,7 @@ func blameSample() []BlameLine { func TestBlameViewGroupsCommits(t *testing.T) { var s BlameViewState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BlameView(c, &s, blameSample(), BlameViewOptions{Language: "go"}).Height(300) }, 700, 340) count := 0 @@ -51,7 +53,7 @@ func TestBlameViewGroupsCommits(t *testing.T) { func TestBlameViewHoverDetails(t *testing.T) { var s BlameViewState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BlameView(c, &s, blameSample(), BlameViewOptions{}).Height(300) }, 700, 340) r, _ := tt.Find("Uika") @@ -72,7 +74,7 @@ func TestBlameViewVirtualized(t *testing.T) { } var s BlameViewState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) BlameView(c, &s, lines, BlameViewOptions{}).Height(400) }, 700, 440) if n := len(tt.Texts()); n > 400 { diff --git a/branch_list.go b/git/branch_list.go similarity index 75% rename from branch_list.go rename to git/branch_list.go index 7e7cfe7..72bf095 100644 --- a/branch_list.go +++ b/git/branch_list.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Branch is a local or remote branch. @@ -34,18 +36,18 @@ type GitAction struct { type BranchListOptions struct { // Actions show on every row; Action returns their IDs. Actions []GitAction - Status DataStatus + Status data.DataStatus Error string } // BranchList shows branches with upstream and ahead/behind counts. -func BranchList(c *ui.Context, s *ListState[string], branches []Branch, opts BranchListOptions) GitListResult { +func BranchList(c *ui.Context, s *data.ListState[string], branches []Branch, opts BranchListOptions) GitListResult { gitActionsCheck(opts.Actions) var r GitListResult - v := List(c, s, len(branches), ListOptions[string]{ + v := data.List(c, s, len(branches), data.ListOptions[string]{ Key: func(i int) string { return branchKey(branches[i]) }, Label: func(i int) string { return branches[i].Name }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "branch_list.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "branch_list.empty"), }, func(i int) { b := branches[i] branchListRow(c, b) @@ -66,9 +68,9 @@ func branchKey(b Branch) string { } func branchListRow(c *ui.Context, b Branch) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) mark := ui.Box(c).Size(16, 16).Shrink(0).Center().Role(ui.RoleNone) if b.Current { mark.TextColor(k.AccentText).Children(func() { ui.Icon(c, icons.Must("check")).Size(14, 14) }) @@ -77,17 +79,17 @@ func branchListRow(c *ui.Context, b Branch) { name := ui.Text(c, b.Name).SingleLine().MinWidth(0).Shrink(1) if b.Current { name.FontWeight(600) - ui.Text(c, msg(c, "branch_list.current")).FontSize(small).TextColor(k.AccentText).Shrink(0) + ui.Text(c, core.Msg(c, "branch_list.current")).FontSize(small).TextColor(k.AccentText).Shrink(0) } ui.Spacer(c) if b.Upstream != "" { ui.Row(c).Gap(u).AlignItems(ui.Center).Shrink(0).TextColor(k.TextMuted).Children(func() { ui.Text(c, b.Upstream).FontSize(small).SingleLine().MaxWidth(160) if b.Ahead > 0 { - ui.Text(c, fmt.Sprintf("↑%d", b.Ahead)).FontSize(small).FontFeatures("tnum").Label(fmt.Sprintf(msg(c, "branch_list.ahead"), b.Ahead)) + ui.Text(c, fmt.Sprintf("↑%d", b.Ahead)).FontSize(small).FontFeatures("tnum").Label(fmt.Sprintf(core.Msg(c, "branch_list.ahead"), b.Ahead)) } if b.Behind > 0 { - ui.Text(c, fmt.Sprintf("↓%d", b.Behind)).FontSize(small).FontFeatures("tnum").Label(fmt.Sprintf(msg(c, "branch_list.behind"), b.Behind)) + ui.Text(c, fmt.Sprintf("↓%d", b.Behind)).FontSize(small).FontFeatures("tnum").Label(fmt.Sprintf(core.Msg(c, "branch_list.behind"), b.Behind)) } }) } @@ -111,7 +113,7 @@ func gitActionsCheck(actions []GitAction) { // gitRowActions shows row actions and records the one used. func gitRowActions(c *ui.Context, actions []GitAction, item string, r *GitListResult) { for _, a := range actions { - b := iconAction(c, icons.Must(a.Icon), a.Label).Tooltip(a.Label).Shrink(0) + b := core.IconAction(c, icons.Must(a.Icon), a.Label).Tooltip(a.Label).Shrink(0) if b.Clicked() && !b.IsDisabled() { r.action, r.item = a.ID, item slog.Debug("mujica: git list action", "action", a.ID, "item", item) diff --git a/branch_list_test.go b/git/branch_list_test.go similarity index 87% rename from branch_list_test.go rename to git/branch_list_test.go index 51afde4..9efb442 100644 --- a/branch_list_test.go +++ b/git/branch_list_test.go @@ -1,9 +1,12 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func branchSample() []Branch { @@ -17,11 +20,11 @@ func branchSample() []Branch { } func TestBranchListRowsAndActions(t *testing.T) { - var s ListState[string] + var s data.ListState[string] var acted [][2]string actions := []GitAction{{ID: "checkout", Label: "Check out", Icon: "git-branch"}, {ID: "delete", Label: "Delete", Icon: "trash-2"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := BranchList(c, &s, branchSample(), BranchListOptions{Actions: actions}) r.Element.Height(300) if a, item, ok := r.Action(); ok { @@ -67,8 +70,8 @@ func TestBranchListInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] BranchList(c, &s, tc.b, BranchListOptions{Actions: tc.a}).Element.Height(100) }, 300, 120, 1) }) @@ -77,7 +80,7 @@ func TestBranchListInvalid(t *testing.T) { func TestBranchListSnapshot(t *testing.T) { snapshot(t, "branch_list", 600, 260, func(c *ui.Context) { - s := ui.Local(c.Root(), "list", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "list", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Children(func() { BranchList(c, *s, branchSample(), BranchListOptions{Actions: []GitAction{{ID: "checkout", Label: "Check out", Icon: "git-branch"}}}).Element.Height(220) }) diff --git a/branch_selector.go b/git/branch_selector.go similarity index 81% rename from branch_selector.go rename to git/branch_selector.go index 642b4ab..4941ade 100644 --- a/branch_selector.go +++ b/git/branch_selector.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "log/slog" @@ -6,9 +6,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BranchSelectorOptions configure a BranchSelector. @@ -48,17 +51,17 @@ func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts Bra for _, b := range branches { branchKey(b) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() tr := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(0, u*3).Gap(u*2).Radius(theme.ControlRadius). Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Disabled(opts.Disabled). - Label(msg(c, "branch_selector.label") + ": " + *selected) + Label(core.Msg(c, "branch_selector.label") + ": " + *selected) internal.Ring(tr, k.Focus, theme.ControlRadius) if tr.Hovered() { tr.Background(k.SurfaceHover) } - tr.Transition(colorFade) + tr.Transition(core.ColorFade) st := ui.Local(tr, "state", func() *branchSelectorState { return &branchSelectorState{} }) s := *st res := BranchSelectorResult{Element: tr} @@ -80,8 +83,8 @@ func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts Bra }) b := tr.Bounds() ui.PopoverBase(c, tr, &s.open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(6, 0, 0, 0).Padding(u * 2).Gap(u * 2).Width(300).MaxHeight(popoverRoom(c, b)).AlignItems(ui.Stretch) + core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, b)) + panel.Margin(6, 0, 0, 0).Padding(u * 2).Gap(u * 2).Width(300).MaxHeight(overlay.PopoverRoom(c, b)).AlignItems(ui.Stretch) var first *Branch if len(branches) > 0 { first = &branches[0] @@ -95,7 +98,7 @@ func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts Bra if create { n++ } - in := TextInput(c, &s.query, TextInputOptions{Placeholder: msg(c, "branch_selector.search"), Label: msg(c, "branch_selector.search"), + in := input.TextInput(c, &s.query, input.TextInputOptions{Placeholder: core.Msg(c, "branch_selector.search"), Label: core.Msg(c, "branch_selector.search"), Leading: func() { ui.Icon(c, icons.Must("search")).Size(14, 14).TextColor(k.TextMuted) }}) in.Input.AutoFocus() if in.Changed() { @@ -115,20 +118,20 @@ func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts Bra } ui.Scroll(c).Shrink(1).Gap(2).Role(ui.RoleList).Children(func() { if n == 0 { - ui.Text(c, msg(c, "branch_selector.empty")).TextColor(k.TextMuted).Padding(u * 3) + ui.Text(c, core.Msg(c, "branch_selector.empty")).TextColor(k.TextMuted).Padding(u * 3) } group := -1 for i, br := range found { if g := branchSelectorGroup(br); g != group { group = g - ui.Text(c, msg(c, []string{"branch_selector.local", "branch_selector.remote"}[g])).Font(theme.SerifFont). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Padding(u*2, u*2, u, u*2) + ui.Text(c, core.Msg(c, []string{"branch_selector.local", "branch_selector.remote"}[g])).Font(theme.SerifFont). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Padding(u*2, u*2, u, u*2) } if branchSelectorRow(c, s, i, br.Name, br.Name == *selected, "git-branch") { pick = i } } - if create && branchSelectorRow(c, s, len(found), msg(c, "branch_selector.create")+" “"+strings.TrimSpace(s.query)+"”", false, "plus") { + if create && branchSelectorRow(c, s, len(found), core.Msg(c, "branch_selector.create")+" “"+strings.TrimSpace(s.query)+"”", false, "plus") { pick = len(found) } }) @@ -151,8 +154,8 @@ func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts Bra // branchSelectorRow builds row i and reports a click on it. func branchSelectorRow(c *ui.Context, s *branchSelectorState, i int, label string, current bool, icon string) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2).Radius(theme.ControlRadius). Role(ui.RoleListItem).Label(label).Cursor(ui.CursorPointer) @@ -165,7 +168,7 @@ func branchSelectorRow(c *ui.Context, s *branchSelectorState, i int, label strin } else if row.Hovered() { row.Background(k.SurfaceHover) } - row.Transition(colorFade) + row.Transition(core.ColorFade) row.Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(k.TextMuted) t := ui.Text(c, label).SingleLine().Grow(1).MinWidth(0) diff --git a/branch_selector_test.go b/git/branch_selector_test.go similarity index 95% rename from branch_selector_test.go rename to git/branch_selector_test.go index 33ec927..6334c64 100644 --- a/branch_selector_test.go +++ b/git/branch_selector_test.go @@ -1,15 +1,17 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBranchSelectorSearchAndChoose(t *testing.T) { sel, changes := "main", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if BranchSelector(c, &sel, branchSample(), BranchSelectorOptions{}).Changed() { changes++ } @@ -45,7 +47,7 @@ func TestBranchSelectorSearchAndChoose(t *testing.T) { func TestBranchSelectorCreateAndDisabled(t *testing.T) { sel, created, disabled := "main", "", false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := BranchSelector(c, &sel, branchSample(), BranchSelectorOptions{AllowCreate: true, Disabled: disabled}) if name, ok := r.Created(); ok { created = name diff --git a/changes_list.go b/git/changes_list.go similarity index 81% rename from changes_list.go rename to git/changes_list.go index c313e18..8ce2062 100644 --- a/changes_list.go +++ b/git/changes_list.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChangedFile is a changed file in the tree or index. @@ -58,7 +60,7 @@ func ChangesList(c *ui.Context, s *ChangesListState, files []ChangedFile, opts C s.src, s.n, s.cursor = first, len(files), -1 slog.Debug("mujica: changes list regroup", "files", len(files), "staged", s.staged) } - k := Tokens(c) + k := core.Tokens(c) var r GitListResult s.list.Selected = &s.cursor s.list.Header = func(i int) bool { return s.rows[i] < 0 } @@ -78,7 +80,7 @@ func ChangesList(c *ui.Context, s *ChangesListState, files []ChangedFile, opts C outer := ui.Column(c).AlignItems(ui.Stretch).Disabled(opts.Disabled) var e *ui.Element outer.Children(func() { - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) defer restore() e = ui.List(c, &s.list, len(s.rows), func(i int) { changesListRow(c, s, files, i, &r) }) e.Grow(1).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) @@ -95,8 +97,8 @@ func ChangesList(c *ui.Context, s *ChangesListState, files []ChangedFile, opts C // changesListRow builds row i: a header or a file. func changesListRow(c *ui.Context, s *ChangesListState, files []ChangedFile, i int, r *GitListResult) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() f := s.rows[i] row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2, 0, u*3) @@ -106,10 +108,10 @@ func changesListRow(c *ui.Context, s *ChangesListState, files []ChangedFile, i i key, all, icon, count = "changes_list.unstaged", "stage_all", "plus", len(files)-s.staged } row.Background(k.Background).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, key), count)).Font(theme.SerifFont).FontWeight(600). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Grow(1) - b := iconAction(c, icons.Must(icon), msg(c, "changes_list."+all)).Disabled(count == 0) - if b.Tooltip(msg(c, "changes_list."+all)).Clicked() && !b.IsDisabled() { + ui.Text(c, fmt.Sprintf(core.Msg(c, key), count)).Font(theme.SerifFont).FontWeight(600). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Grow(1) + b := core.IconAction(c, icons.Must(icon), core.Msg(c, "changes_list."+all)).Disabled(count == 0) + if b.Tooltip(core.Msg(c, "changes_list."+all)).Clicked() && !b.IsDisabled() { r.action = all } }) @@ -126,14 +128,14 @@ func changesListRow(c *ui.Context, s *ChangesListState, files []ChangedFile, i i dir := path.Dir(cf.Path) ui.Text(c, path.Base(cf.Path)).SingleLine().Shrink(0).MaxWidth(240) if dir != "." { - ui.Text(c, dir).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0) + ui.Text(c, dir).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0) } ui.Spacer(c) id, icon := "stage", "plus" if cf.Staged { id, icon = "unstage", "minus" } - b := iconAction(c, icons.Must(icon), msg(c, "changes_list."+id)+" "+cf.Path).Tooltip(msg(c, "changes_list."+id)) + b := core.IconAction(c, icons.Must(icon), core.Msg(c, "changes_list."+id)+" "+cf.Path).Tooltip(core.Msg(c, "changes_list."+id)) if b.Clicked() && !b.IsDisabled() { r.action, r.item = id, cf.Path } diff --git a/changes_list_test.go b/git/changes_list_test.go similarity index 94% rename from changes_list_test.go rename to git/changes_list_test.go index 0cf540d..731a468 100644 --- a/changes_list_test.go +++ b/git/changes_list_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func changesSample() []ChangedFile { @@ -20,7 +22,7 @@ func TestChangesListGroupsAndStage(t *testing.T) { files := changesSample() var acted [][2]string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ChangesList(c, &s, files, ChangesListOptions{}) r.Element.Height(320) if a, item, ok := r.Action(); ok { @@ -58,7 +60,7 @@ func TestChangesListDisabledAndEmptyGroups(t *testing.T) { files := []ChangedFile{{Path: "a.go", Status: GitModified}} acted := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ChangesList(c, &s, files, ChangesListOptions{Disabled: true}) r.Element.Height(200) _, _, ok := r.Action() @@ -78,7 +80,7 @@ func TestChangesListDisabledAndEmptyGroups(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) var s ChangesListState ChangesList(c, &s, []ChangedFile{{Status: GitAdded}}, ChangesListOptions{}) }, 300, 100, 1) diff --git a/command_block.go b/git/command_block.go similarity index 73% rename from command_block.go rename to git/command_block.go index 8d0c87f..cad4ad1 100644 --- a/command_block.go +++ b/git/command_block.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -7,9 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CommandBlockOptions configure a CommandBlock. @@ -34,7 +36,7 @@ func (r CommandBlockResult) Toggled() bool { return r.toggled } func (r CommandBlockResult) Copied() (string, bool) { return r.copied, r.copied != "" } // CommandBlock shows a command run; *open shows its output. -func CommandBlock(c *ui.Context, run CommandRun, open *bool, opts CommandBlockOptions) CommandBlockResult { +func CommandBlock(c *ui.Context, run agent.CommandRun, open *bool, opts CommandBlockOptions) CommandBlockResult { if run.Command == "" { panic("mujica: CommandBlock needs a Command") } @@ -49,8 +51,8 @@ func CommandBlock(c *ui.Context, run CommandRun, open *bool, opts CommandBlockOp if maxH == 0 { maxH = 320 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() box := ui.Column(c).AlignItems(ui.Stretch).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).Clip() res := CommandBlockResult{Element: box} @@ -71,8 +73,8 @@ func CommandBlock(c *ui.Context, run CommandRun, open *bool, opts CommandBlockOp if tr.Hovered() { tr.Background(k.SurfaceHover) } - tr.Transition(colorFade) - turn := tr.Animate("mujica.turn", commandBlockTurn(*open), Motion(c, theme.StateDuration)) + tr.Transition(core.ColorFade) + turn := tr.Animate("mujica.turn", commandBlockTurn(*open), core.Motion(c, theme.StateDuration)) tr.Children(func() { ui.Icon(c, icons.Must("chevron-right")).Size(14, 14).Shrink(0).Rotate(turn).TextColor(k.TextMuted) ui.Text(c, prompt).Font(theme.MonoFont).TextColor(k.Ornament.Mix(k.Text, 0.35)).Shrink(0) @@ -80,21 +82,21 @@ func CommandBlock(c *ui.Context, run CommandRun, open *bool, opts CommandBlockOp ui.RichText(c, spans...).Font(theme.MonoFont).SingleLine().MinWidth(0) }) commandBlockStatus(c, run) - if iconAction(c, icons.Must("copy"), msg(c, "command_block.copy_command")).Clicked() && !box.IsDisabled() { + if core.IconAction(c, icons.Must("copy"), core.Msg(c, "command_block.copy_command")).Clicked() && !box.IsDisabled() { c.WriteClipboard(run.Command) res.copied = "command" } - if iconAction(c, icons.Must("file-text"), msg(c, "command_block.copy_output")).Disabled(run.Output == "").Clicked() && !box.IsDisabled() { + if core.IconAction(c, icons.Must("file-text"), core.Msg(c, "command_block.copy_output")).Disabled(run.Output == "").Clicked() && !box.IsDisabled() { c.WriteClipboard(internal.StripAnsi(run.Output)) res.copied = "output" } }) if *open { ui.Box(c).Height(1).Background(k.Border).Role(ui.RoleNone) - out := ui.ScrollBoth(c).MaxHeight(maxH).Padding(u*3, u*4).Background(k.Background).Label(msg(c, "command_block.output")) + out := ui.ScrollBoth(c).MaxHeight(maxH).Padding(u*3, u*4).Background(k.Background).Label(core.Msg(c, "command_block.output")) out.Children(func() { if run.Output == "" { - ui.Text(c, msg(c, "command_block.no_output")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, "command_block.no_output")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) return } AnsiText(c, run.Output, AnsiTextOptions{}) @@ -108,12 +110,12 @@ func CommandBlock(c *ui.Context, run CommandRun, open *bool, opts CommandBlockOp } // commandBlockStatus shows running, success or failure with icon and words. -func commandBlockStatus(c *ui.Context, run CommandRun) { - k := Tokens(c) - u := Density(c).Unit() - ui.Row(c).AlignItems(ui.Center).Gap(u * 1.5).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).Children(func() { +func commandBlockStatus(c *ui.Context, run agent.CommandRun) { + k := core.Tokens(c) + u := core.Density(c).Unit() + ui.Row(c).AlignItems(ui.Center).Gap(u * 1.5).Shrink(0).FontSize(core.FontSize(c, theme.CaptionSize)).Children(func() { if run.Running { - Spinner(c, SpinnerOptions{Label: msg(c, "command_block.running"), ShowLabel: true, Size: 12}) + core.Spinner(c, core.SpinnerOptions{Label: core.Msg(c, "command_block.running"), ShowLabel: true, Size: 12}) return } icon, col := "circle-check", k.Success @@ -122,7 +124,7 @@ func commandBlockStatus(c *ui.Context, run CommandRun) { } ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(col).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, fmt.Sprintf(msg(c, "command_block.exit"), run.ExitCode)).FontFeatures("tnum").SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "command_block.exit"), run.ExitCode)).FontFeatures("tnum").SingleLine() }) ui.Text(c, commandBlockDuration(run.Duration)).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() }) diff --git a/command_block_test.go b/git/command_block_test.go similarity index 71% rename from command_block_test.go rename to git/command_block_test.go index 589b857..4ea5d0d 100644 --- a/command_block_test.go +++ b/git/command_block_test.go @@ -1,21 +1,24 @@ -package mujica +package git import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func commandBlockSample() CommandRun { - return CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m mujicaui 3.2s", ExitCode: 0, Duration: 3200 * time.Millisecond} +func commandBlockSample() agent.CommandRun { + return agent.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m mujicaui 3.2s", ExitCode: 0, Duration: 3200 * time.Millisecond} } func TestCommandBlockToggleAndCopy(t *testing.T) { open, toggles := false, 0 var copied []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommandBlock(c, commandBlockSample(), &open, CommandBlockOptions{}) if r.Toggled() { toggles++ @@ -52,10 +55,10 @@ func TestCommandBlockToggleAndCopy(t *testing.T) { func TestCommandBlockFailureRunningDisabled(t *testing.T) { open := true - run := CommandRun{Command: "make", Output: "boom", ExitCode: 2, Duration: 90 * time.Second} + run := agent.CommandRun{Command: "make", Output: "boom", ExitCode: 2, Duration: 90 * time.Second} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { CommandBlock(c, run, &open, CommandBlockOptions{Prompt: "❯"}) }) @@ -78,7 +81,7 @@ func TestCommandBlockFailureRunningDisabled(t *testing.T) { } func TestCommandBlockInvalid(t *testing.T) { - for _, run := range []CommandRun{{}, {Command: "x", Duration: -1}} { + for _, run := range []agent.CommandRun{{}, {Command: "x", Duration: -1}} { func() { defer func() { if recover() == nil { @@ -87,7 +90,7 @@ func TestCommandBlockInvalid(t *testing.T) { }() open := false ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommandBlock(c, run, &open, CommandBlockOptions{}) }, 100, 50, 1) }() @@ -98,8 +101,8 @@ func TestCommandBlockSnapshot(t *testing.T) { snapshot(t, "command_block", 620, 300, func(c *ui.Context) { a, b := true, false ui.Column(c).Padding(16).Gap(12).Children(func() { - CommandBlock(c, CommandRun{Command: "go test -run Git ./...", Output: "\x1b[32mok\x1b[0m \tmujicaui\t3.214s\n\x1b[33mwarn\x1b[0m slow test: TestCommitGraphVirtualized", Duration: 3214 * time.Millisecond}, &a, CommandBlockOptions{}) - CommandBlock(c, CommandRun{Command: "git push origin main", ExitCode: 1, Duration: 640 * time.Millisecond, Output: "rejected"}, &b, CommandBlockOptions{}) + CommandBlock(c, agent.CommandRun{Command: "go test -run Git ./...", Output: "\x1b[32mok\x1b[0m \tmujicaui\t3.214s\n\x1b[33mwarn\x1b[0m slow test: TestCommitGraphVirtualized", Duration: 3214 * time.Millisecond}, &a, CommandBlockOptions{}) + CommandBlock(c, agent.CommandRun{Command: "git push origin main", ExitCode: 1, Duration: 640 * time.Millisecond, Output: "rejected"}, &b, CommandBlockOptions{}) }) }) } diff --git a/commit_graph.go b/git/commit_graph.go similarity index 90% rename from commit_graph.go rename to git/commit_graph.go index 1bb1bde..f7e887a 100644 --- a/commit_graph.go +++ b/git/commit_graph.go @@ -1,17 +1,19 @@ -package mujica +package git import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CommitGraphState is a CommitGraph's selection and its lane layout. type CommitGraphState struct { - List ListState[string] + List data.ListState[string] rows []commitGraphRow // lanes is the widest the graph gets. lanes int @@ -25,7 +27,7 @@ type CommitGraphOptions struct { TimeFormat string // MaxLanes caps the graph width in lanes; zero means 8. MaxLanes int - Status DataStatus + Status data.DataStatus Error string } @@ -63,17 +65,17 @@ func CommitGraph(c *ui.Context, s *CommitGraphState, commits []Commit, opts Comm slog.Debug("mujica: commit graph layout", "commits", len(commits), "lanes", s.lanes) } layout := gitTimeLayout(opts.TimeFormat) - lane := Density(c).Unit() * 4 + lane := core.Density(c).Unit() * 4 width := float32(min(max(s.lanes, 1), maxLanes)) * lane - h := Density(c).ControlHeight() - v := List(c, &s.List, len(commits), ListOptions[string]{ + h := core.Density(c).ControlHeight() + v := data.List(c, &s.List, len(commits), data.ListOptions[string]{ Key: func(i int) string { return gitCommitHash(commits[i]) }, Label: func(i int) string { return commits[i].Subject }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "commit_list.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "commit_list.empty"), }, func(i int) { row := s.rows[i] ui.Box(c).Size(width, h).Shrink(0).Clip().Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { - commitGraphDraw(p, Tokens(c), r, row, lane) + commitGraphDraw(p, core.Tokens(c), r, row, lane) }) commitListRow(c, commits[i], layout) }) diff --git a/commit_graph_test.go b/git/commit_graph_test.go similarity index 95% rename from commit_graph_test.go rename to git/commit_graph_test.go index 2945268..f947201 100644 --- a/commit_graph_test.go +++ b/git/commit_graph_test.go @@ -1,10 +1,12 @@ -package mujica +package git import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // commitGraphSample is main with a feature branch merged back. @@ -50,7 +52,7 @@ func TestCommitGraphVirtualized(t *testing.T) { commits := gitHistory(100000) var s CommitGraphState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommitGraph(c, &s, commits, CommitGraphOptions{}).Element.Height(400) }, 700, 440) if n := len(tt.Texts()); n > 400 { @@ -71,7 +73,7 @@ func TestCommitGraphChooseAndKeys(t *testing.T) { var s CommitGraphState changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommitGraph(c, &s, commitGraphSample(), CommitGraphOptions{}) r.Element.Height(300) if r.Changed() { diff --git a/commit_input.go b/git/commit_input.go similarity index 67% rename from commit_input.go rename to git/commit_input.go index 4ec91fe..df71ba8 100644 --- a/commit_input.go +++ b/git/commit_input.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CommitMessage is a commit's title and body. @@ -53,8 +55,8 @@ func CommitInput(c *ui.Context, m *CommitMessage, opts CommitInputOptions) Commi if limit < 0 { panic("mujica: CommitInput TitleLimit is negative") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2).Disabled(opts.Disabled) res := CommitInputResult{Element: col} st := ui.Local(col, "state", func() *commitInputState { return &commitInputState{} }) @@ -67,29 +69,29 @@ func CommitInput(c *ui.Context, m *CommitMessage, opts CommitInputOptions) Commi blocked := opts.Busy || s.pending n := utf8.RuneCountInString(m.Title) col.Children(func() { - title := TextInput(c, &m.Title, TextInputOptions{Placeholder: msg(c, "commit_input.title"), Label: msg(c, "commit_input.title"), + title := input.TextInput(c, &m.Title, input.TextInputOptions{Placeholder: core.Msg(c, "commit_input.title"), Label: core.Msg(c, "commit_input.title"), Disabled: opts.Disabled, ReadOnly: blocked, Trailing: func() { col := k.TextMuted if n > limit { col = k.Warning } - name := fmt.Sprintf(msg(c, "commit_input.count"), n, limit) + name := fmt.Sprintf(core.Msg(c, "commit_input.count"), n, limit) if n > limit { - name = fmt.Sprintf(msg(c, "commit_input.long"), n-limit) + name = fmt.Sprintf(core.Msg(c, "commit_input.long"), n-limit) } - ui.Text(c, fmt.Sprintf("%d/%d", n, limit)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(col).Shrink(0). + ui.Text(c, fmt.Sprintf("%d/%d", n, limit)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").TextColor(col).Shrink(0). Label(name).Tooltip(name) }}) - body := TextArea(c, &m.Body, TextAreaOptions{Placeholder: msg(c, "commit_input.body"), Label: msg(c, "commit_input.body"), + body := input.TextArea(c, &m.Body, input.TextAreaOptions{Placeholder: core.Msg(c, "commit_input.body"), Label: core.Msg(c, "commit_input.body"), MinHeight: 72, MaxHeight: 200, Disabled: opts.Disabled, ReadOnly: blocked}) empty := strings.TrimSpace(m.Title) == "" send := (title.Submitted() || body.Submitted()) && !empty ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { - if Button(c, msg(c, "commit_input.amend"), ButtonOptions{Variant: Secondary, Icon: icons.Must("rotate-ccw"), Disabled: blocked || empty}).Clicked() { + if input.Button(c, core.Msg(c, "commit_input.amend"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("rotate-ccw"), Disabled: blocked || empty}).Clicked() { res.amend = true } - if Button(c, msg(c, "commit_input.commit"), ButtonOptions{Icon: icons.Must("check"), Loading: opts.Busy, Disabled: s.pending || empty}).Clicked() { + if input.Button(c, core.Msg(c, "commit_input.commit"), input.ButtonOptions{Icon: icons.Must("check"), Loading: opts.Busy, Disabled: s.pending || empty}).Clicked() { send = true } }) diff --git a/commit_input_test.go b/git/commit_input_test.go similarity index 92% rename from commit_input_test.go rename to git/commit_input_test.go index ae70439..eb8df58 100644 --- a/commit_input_test.go +++ b/git/commit_input_test.go @@ -1,10 +1,12 @@ -package mujica +package git import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCommitInputCommitOnceWhileBusy(t *testing.T) { @@ -12,7 +14,7 @@ func TestCommitInputCommitOnceWhileBusy(t *testing.T) { busy := false commits, amends := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommitInput(c, &m, CommitInputOptions{Busy: busy}) if r.Committed() { commits++ @@ -52,7 +54,7 @@ func TestCommitInputCommitOnceWhileBusy(t *testing.T) { func TestCommitInputTitleHint(t *testing.T) { m := CommitMessage{Title: strings.Repeat("x", 55)} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommitInput(c, &m, CommitInputOptions{}) }, 600, 400) if _, ok := tt.Find("Summary is 5 characters over the suggested length"); !ok { @@ -69,7 +71,7 @@ func TestCommitInputDisabledAndInvalid(t *testing.T) { m := CommitMessage{Title: "x"} acted := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommitInput(c, &m, CommitInputOptions{Disabled: true}) acted = acted || r.Committed() || r.Amended() }, 600, 400) @@ -84,7 +86,7 @@ func TestCommitInputDisabledAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommitInput(c, &m, CommitInputOptions{TitleLimit: -1}) }, 300, 100, 1) } diff --git a/commit_list.go b/git/commit_list.go similarity index 79% rename from commit_list.go rename to git/commit_list.go index 161e7e6..8f3f58c 100644 --- a/commit_list.go +++ b/git/commit_list.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Commit is one commit of a history, newest first. @@ -42,17 +44,17 @@ func (r GitListResult) Action() (action, item string, ok bool) { type CommitListOptions struct { // TimeFormat lays out commit times; empty means "2006-01-02 15:04". TimeFormat string - Status DataStatus + Status data.DataStatus Error string } // CommitList shows commits to choose from; give it a height. -func CommitList(c *ui.Context, s *ListState[string], commits []Commit, opts CommitListOptions) GitListResult { +func CommitList(c *ui.Context, s *data.ListState[string], commits []Commit, opts CommitListOptions) GitListResult { layout := gitTimeLayout(opts.TimeFormat) - v := List(c, s, len(commits), ListOptions[string]{ + v := data.List(c, s, len(commits), data.ListOptions[string]{ Key: func(i int) string { return gitCommitHash(commits[i]) }, Label: func(i int) string { return commits[i].Subject }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "commit_list.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "commit_list.empty"), }, func(i int) { commitListRow(c, commits[i], layout) }) r := GitListResult{Element: v.Element, changed: v.Changed(), submitted: v.Submitted()} if r.changed || r.submitted { @@ -63,10 +65,10 @@ func CommitList(c *ui.Context, s *ListState[string], commits []Commit, opts Comm } func commitListRow(c *ui.Context, cm Commit, layout string) { - k := Tokens(c) + k := core.Tokens(c) gitRefs(c, cm.Refs) ui.Text(c, cm.Subject).SingleLine().Grow(1).MinWidth(0) - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) ui.Text(c, cm.Author).FontSize(small).TextColor(k.TextMuted).SingleLine().MaxWidth(140).Shrink(0) ui.Text(c, gitShortHash(cm.Hash)).Font(theme.MonoFont).FontSize(small).TextColor(k.AccentText).Shrink(0) ui.Text(c, cm.When.Format(layout)).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted).Shrink(0) @@ -74,10 +76,10 @@ func commitListRow(c *ui.Context, cm Commit, layout string) { // gitRefs shows branch and tag names as small outlined pills. func gitRefs(c *ui.Context, refs []string) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() for _, ref := range refs { - ui.Text(c, ref).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.AccentText).SingleLine(). + ui.Text(c, ref).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.AccentText).SingleLine(). Padding(0, u*1.5).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.AccentText).Shrink(0).MaxWidth(160) } } diff --git a/commit_list_test.go b/git/commit_list_test.go similarity index 85% rename from commit_list_test.go rename to git/commit_list_test.go index 1dd521f..b562602 100644 --- a/commit_list_test.go +++ b/git/commit_list_test.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) var gitTestTime = time.Date(2026, 10, 5, 9, 30, 0, 0, time.UTC) @@ -29,10 +32,10 @@ func gitHistory(n int) []Commit { func TestCommitListChooseAndOpen(t *testing.T) { commits := gitHistory(5) commits[0].Refs = []string{"main"} - var s ListState[string] + var s data.ListState[string] changes, opens := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := CommitList(c, &s, commits, CommitListOptions{}) r.Element.Height(300) if r.Changed() { @@ -59,9 +62,9 @@ func TestCommitListChooseAndOpen(t *testing.T) { } func TestCommitListEmptyAndInvalid(t *testing.T) { - var s ListState[string] + var s data.ListState[string] tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommitList(c, &s, nil, CommitListOptions{}) }, 400, 200) if !tt.HasText("No commits") { @@ -73,8 +76,8 @@ func TestCommitListEmptyAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] CommitList(c, &s, []Commit{{Subject: "x"}}, CommitListOptions{}) }, 300, 100, 1) } @@ -83,7 +86,7 @@ func TestCommitListSnapshot(t *testing.T) { commits := gitHistory(4) commits[0].Refs = []string{"main", "v1.1"} snapshot(t, "commit_list", 640, 220, func(c *ui.Context) { - s := ui.Local(c.Root(), "list", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "list", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Children(func() { CommitList(c, *s, commits, CommitListOptions{}).Element.Height(180) }) diff --git a/conflict_resolver.go b/git/conflict_resolver.go similarity index 77% rename from conflict_resolver.go rename to git/conflict_resolver.go index 9806b4e..d37811f 100644 --- a/conflict_resolver.go +++ b/git/conflict_resolver.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ConflictResolverState holds conflict blocks and their choices. @@ -71,7 +72,7 @@ type ConflictResolverOptions struct { } // ConflictResolver resolves the git conflict blocks of text. -func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts ConflictResolverOptions) ChoiceResult { +func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts ConflictResolverOptions) core.ChoiceResult { if !s.built || s.text != text { s.parts, s.bad = conflictParse(internal.SplitLines(text)) s.choices = make([]MergeChoice, len(s.parts)) @@ -79,14 +80,14 @@ func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts _, total := s.Resolved() slog.Debug("mujica: conflict resolver parse", "conflicts", total, "badLine", s.bad) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 3).Disabled(opts.Disabled) - res := ChoiceResult{Element: col} + res := core.ChoiceResult{Element: col} col.Children(func() { done, total := s.Resolved() ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "conflict_resolver.resolved"), done, total)).FontWeight(600).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "conflict_resolver.resolved"), done, total)).FontWeight(600).Role(ui.RoleStatus) if total > 0 { ui.Box(c).Height(4).Grow(1).Radius(2).Background(k.Border).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y, W: r.W * float32(done) / float32(total), H: r.H}, k.Success, 2) @@ -96,11 +97,11 @@ func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts if s.bad > 0 { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(16, 16) - ui.Text(c, fmt.Sprintf(msg(c, "conflict_resolver.broken"), s.bad)).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "conflict_resolver.broken"), s.bad)).Role(ui.RoleStatus) }) } if total == 0 && s.bad == 0 { - ui.Text(c, msg(c, "conflict_resolver.none")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "conflict_resolver.none")).TextColor(k.TextMuted) } n := 0 for i, p := range s.parts { @@ -109,7 +110,7 @@ func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts } n++ if conflictBlock(c, s, i, n) && !col.IsDisabled() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) } } }) @@ -118,8 +119,8 @@ func ConflictResolver(c *ui.Context, s *ConflictResolverState, text string, opts // conflictBlock shows one conflict and its choice buttons. func conflictBlock(c *ui.Context, s *ConflictResolverState, i, n int) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() p := s.parts[i] choice := s.choices[i] changed := false @@ -131,8 +132,8 @@ func conflictBlock(c *ui.Context, s *ConflictResolverState, i, n int) bool { icon, colr, key = "circle-check", k.Success, "conflict_resolver.done" } ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(colr) - ui.Text(c, fmt.Sprintf(msg(c, "conflict_resolver.title"), n, p.line)).Font(theme.SerifFont).FontWeight(600) - ui.Text(c, msg(c, key)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(colr) + ui.Text(c, fmt.Sprintf(core.Msg(c, "conflict_resolver.title"), n, p.line)).Font(theme.SerifFont).FontWeight(600) + ui.Text(c, core.Msg(c, key)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(colr) }) ui.Grid(c).ColumnTracks(ui.Fr(1), ui.Fr(1)).Children(func() { side := func(title, ref string, lines []string, col ui.Color, on bool) { @@ -145,14 +146,14 @@ func conflictBlock(c *ui.Context, s *ConflictResolverState, i, n int) bool { if on { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) } - ui.Text(c, title).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(col) - ui.Text(c, ref).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, title).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(col) + ui.Text(c, ref).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) mergeLines(c, lines, false) }) } - side(msg(c, "conflict_resolver.current"), p.oursRef, p.ours, k.Info, choice == MergeOurs || choice == MergeBoth) - side(msg(c, "conflict_resolver.incoming"), p.theirsRef, p.theirs, k.Warning, choice == MergeTheirs || choice == MergeBoth) + side(core.Msg(c, "conflict_resolver.current"), p.oursRef, p.ours, k.Info, choice == MergeOurs || choice == MergeBoth) + side(core.Msg(c, "conflict_resolver.incoming"), p.theirsRef, p.theirs, k.Warning, choice == MergeTheirs || choice == MergeBoth) }) ui.Row(c).Gap(u*2).Padding(u*2, u*3).Wrap().Children(func() { for _, opt := range []struct { @@ -162,7 +163,7 @@ func conflictBlock(c *ui.Context, s *ConflictResolverState, i, n int) bool { if opt.choice == MergeUnresolved && choice == MergeUnresolved { continue } - if mergeChoiceButton(c, msg(c, "conflict_resolver."+opt.key), choice == opt.choice && opt.choice != MergeUnresolved).Clicked() && choice != opt.choice { + if mergeChoiceButton(c, core.Msg(c, "conflict_resolver."+opt.key), choice == opt.choice && opt.choice != MergeUnresolved).Clicked() && choice != opt.choice { s.choices[i], changed = opt.choice, true slog.Debug("mujica: conflict resolver choose", "block", n, "choice", opt.key) } diff --git a/conflict_resolver_test.go b/git/conflict_resolver_test.go similarity index 95% rename from conflict_resolver_test.go rename to git/conflict_resolver_test.go index 9318d60..f41408a 100644 --- a/conflict_resolver_test.go +++ b/git/conflict_resolver_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const conflictText = "intro\n<<<<<<< HEAD\nours one\n=======\ntheirs one\n>>>>>>> feature\nmiddle\n<<<<<<< HEAD\nours two\n||||||| base\nold two\n=======\ntheirs two\n>>>>>>> feature\nend" @@ -23,7 +25,7 @@ func TestConflictResolverAcceptAndCount(t *testing.T) { var s ConflictResolverState changes := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Children(func() { if ConflictResolver(c, &s, conflictText, ConflictResolverOptions{}).Changed() { changes++ @@ -56,7 +58,7 @@ func TestConflictResolverAcceptAndCount(t *testing.T) { func TestConflictResolverBrokenAndDisabled(t *testing.T) { var s ConflictResolverState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ConflictResolver(c, &s, "a\n<<<<<<< HEAD\nx", ConflictResolverOptions{}) }, 600, 300) if !tt.HasText("Conflict markers are not closed after line 2") { @@ -67,7 +69,7 @@ func TestConflictResolverBrokenAndDisabled(t *testing.T) { } var d ConflictResolverState tt = ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ConflictResolver(c, &d, conflictText, ConflictResolverOptions{Disabled: true}) }, 800, 900) _ = tt.Click("Accept current") diff --git a/diff_stat.go b/git/diff_stat.go similarity index 84% rename from diff_stat.go rename to git/diff_stat.go index cba272f..fa387cb 100644 --- a/diff_stat.go +++ b/git/diff_stat.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DiffStatOptions configure a DiffStat. @@ -27,14 +28,14 @@ func DiffStat(c *ui.Context, added, deleted int, opts DiffStatOptions) *ui.Eleme if n < 0 { panic("mujica: DiffStat Blocks is negative") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() plus, minus := diffStatBlocks(added, deleted, n) row := ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Shrink(0). - Label(fmt.Sprintf(msg(c, "diff_stat.label"), added, deleted)) + Label(fmt.Sprintf(core.Msg(c, "diff_stat.label"), added, deleted)) row.Children(func() { mono := func(s string, col ui.Color) { - ui.Text(c, s).Font(theme.MonoFont).FontFeatures("tnum").FontSize(FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() + ui.Text(c, s).Font(theme.MonoFont).FontFeatures("tnum").FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() } mono(fmt.Sprintf("+%d", added), k.Success) mono(fmt.Sprintf("−%d", deleted), k.Danger) diff --git a/diff_stat_test.go b/git/diff_stat_test.go similarity index 92% rename from diff_stat_test.go rename to git/diff_stat_test.go index feeefc1..6ace273 100644 --- a/diff_stat_test.go +++ b/git/diff_stat_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestDiffStatBlocks(t *testing.T) { @@ -25,7 +27,7 @@ func TestDiffStatBlocks(t *testing.T) { func TestDiffStatTextAndName(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DiffStat(c, 12, 3, DiffStatOptions{}) }, 300, 60) if !tt.HasText("+12") || !tt.HasText("−3") { @@ -42,7 +44,7 @@ func TestDiffStatTextAndName(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DiffStat(c, tc.a, tc.d, DiffStatOptions{Blocks: tc.n}) }, 100, 50, 1) }() diff --git a/diff_viewer.go b/git/diff_viewer.go similarity index 89% rename from diff_viewer.go rename to git/diff_viewer.go index 839c0d7..70d0e0c 100644 --- a/diff_viewer.go +++ b/git/diff_viewer.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DiffViewerState keeps a DiffViewer's diff, opened folds and scroll place. @@ -59,9 +60,9 @@ func DiffViewer(c *ui.Context, s *DiffViewerState, from, to string, opts DiffVie s.rows = diffViewerRows(s.diff, ctx, s.expanded, opts.SideBySide) s.split, s.context, s.dirty = opts.SideBySide, ctx, false } - k := Tokens(c) + k := core.Tokens(c) digits := float32(len(strconv.Itoa(max(len(s.diff), 1)))) - numW := digits*8 + Density(c).Unit()*3 + numW := digits*8 + core.Density(c).Unit()*3 e := ui.List(c, &s.list, len(s.rows), func(i int) { r := s.rows[i] if r.hidden > 0 { @@ -92,15 +93,15 @@ func DiffViewer(c *ui.Context, s *DiffViewerState, from, to string, opts DiffVie // diffViewerCell shows one diff line with numbers, sign and tint. func diffViewerCell(c *ui.Context, d internal.DiffLine, language string, numW float32, both bool) { - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) sign, col, name := " ", k.TextMuted, "" switch d.Op { case internal.DiffDelete: - sign, col, name = "−", k.Danger, msg(c, "diff_viewer.deleted") + sign, col, name = "−", k.Danger, core.Msg(c, "diff_viewer.deleted") case internal.DiffInsert: - sign, col, name = "+", k.Success, msg(c, "diff_viewer.added") + sign, col, name = "+", k.Success, core.Msg(c, "diff_viewer.added") } cell := ui.Row(c).Grow(1).MinWidth(0).AlignItems(ui.Center) if d.Op != internal.DiffEqual { @@ -134,20 +135,20 @@ func diffViewerCell(c *ui.Context, d internal.DiffLine, language string, numW fl // diffViewerFold is the button standing for hidden unchanged lines. func diffViewerFold(c *ui.Context, s *DiffViewerState, r diffViewerRow) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).Justify(ui.Start).Gap(u*2).MinHeight(theme.MinHitSize).Padding(0, u*3).Background(k.Background). TextColor(k.TextMuted).Cursor(ui.CursorPointer) - label := fmt.Sprintf(msg(c, "diff_viewer.fold"), r.hidden) + label := fmt.Sprintf(core.Msg(c, "diff_viewer.fold"), r.hidden) b.Label(label) internal.Ring(b, k.Focus, 0) if b.Hovered() { b.Background(k.SurfaceHover).TextColor(k.Text) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { ui.Text(c, "⋯").FontWeight(700) - ui.Text(c, label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, label).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)) }) if b.Clicked() && !b.IsDisabled() { s.expanded[r.fold] = true diff --git a/diff_viewer_test.go b/git/diff_viewer_test.go similarity index 94% rename from diff_viewer_test.go rename to git/diff_viewer_test.go index 702a6e7..78f6883 100644 --- a/diff_viewer_test.go +++ b/git/diff_viewer_test.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) const diffOld = "package main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hi\")\n}\n" @@ -42,7 +43,7 @@ func TestDiffViewerUnfoldAndModes(t *testing.T) { to := strings.Repeat("same\n", 20) + "new\n" split := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DiffViewer(c, &s, from, to, DiffViewerOptions{SideBySide: split}).Height(760) }, 700, 800) if !tt.HasText("17 unchanged lines") { @@ -80,7 +81,7 @@ func TestDiffViewerVirtualized(t *testing.T) { from, to := a.String(), b.String() var s DiffViewerState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DiffViewer(c, &s, from, to, DiffViewerOptions{}).Height(400) }, 700, 440) if n := len(tt.Texts()); n > 400 { diff --git a/git/doc.go b/git/doc.go new file mode 100644 index 0000000..8cee7e8 --- /dev/null +++ b/git/doc.go @@ -0,0 +1,2 @@ +// Package git holds MujicaUI version control views: diffs, commits, branches and merges. +package git diff --git a/git/example_git_test.go b/git/example_git_test.go new file mode 100644 index 0000000..59b1362 --- /dev/null +++ b/git/example_git_test.go @@ -0,0 +1,266 @@ +package git_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/agent" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/git" +) + +func ExampleDiffViewer() { + var state git.DiffViewerState + from := "func main() {\n\tprintln(\"hi\")\n}\n" + to := "func main() {\n\tprintln(\"hello\")\n}\n" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.DiffViewer(c, &state, from, to, git.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleInlineDiff() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.InlineDiff(c, "The court meets at noon", "The court meets at dusk", git.InlineDiffOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleThreeWayMerge() { + var state git.ThreeWayMergeState + base, local, remote := "a\nb\nc", "a\nB1\nc", "a\nB2\nc" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.ThreeWayMerge(c, &state, base, local, remote, git.ThreeWayMergeOptions{}) + r.Element.Height(280) + if done, total := state.Resolved(); r.Changed() && done == total { + _ = state.Result() // write the merged file + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleConflictResolver() { + var state git.ConflictResolverState + text := "a\n<<<<<<< HEAD\nmine\n=======\ntheirs\n>>>>>>> feature\nb" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if git.ConflictResolver(c, &state, text, git.ConflictResolverOptions{}).Changed() { + _ = state.Result() // save the file once every block is resolved + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChangesList() { + var state git.ChangesListState + files := []git.ChangedFile{ + {Path: "main.go", Status: git.GitModified, Staged: true}, + {Path: "notes.txt", Status: git.GitUntracked}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.ChangesList(c, &state, files, git.ChangesListOptions{}) + r.Element.Height(240) + if action, path, ok := r.Action(); ok { + _, _ = action, path // run git add or git restore --staged + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommitInput() { + msg := git.CommitMessage{} + busy := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.CommitInput(c, &msg, git.CommitInputOptions{Busy: busy}) + if r.Committed() || r.Amended() { + busy = true // run git commit, then clear busy + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommitGraph() { + var state git.CommitGraphState + commits := []git.Commit{ + {Hash: "c3", Parents: []string{"c2", "b1"}, Subject: "Merge branch", Refs: []string{"main"}}, + {Hash: "b1", Parents: []string{"c1"}, Subject: "Feature"}, + {Hash: "c2", Parents: []string{"c1"}, Subject: "Fix"}, + {Hash: "c1", Subject: "Initial commit"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.CommitGraph(c, &state, commits, git.CommitGraphOptions{}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommitList() { + var state data.ListState[string] + commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.CommitList(c, &state, commits, git.CommitListOptions{}) + r.Element.Height(240) + if hash, ok := state.Selected(); ok && r.Submitted() { + _ = hash // show the commit + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBranchSelector() { + branch := "main" + branches := []git.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.BranchSelector(c, &branch, branches, git.BranchSelectorOptions{AllowCreate: true}) + if name, ok := r.Created(); ok { + _ = name // run git switch -c name + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBranchList() { + var state data.ListState[string] + branches := []git.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}} + actions := []git.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.BranchList(c, &state, branches, git.BranchListOptions{Actions: actions}) + r.Element.Height(200) + if action, name, ok := r.Action(); ok { + _, _ = action, name // run the action on the branch + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTagList() { + var state data.ListState[string] + tags := []git.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.TagList(c, &state, tags, git.TagListOptions{}) + r.Element.Height(200) + if _, hash, ok := r.Action(); ok { + _ = hash // show the tagged commit + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStashList() { + var state data.ListState[string] + stashes := []git.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.StashList(c, &state, stashes, git.StashListOptions{}) + r.Element.Height(200) + if action, ref, ok := r.Action(); ok { + _, _ = action, ref // run git stash apply, pop or drop + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBlameView() { + var state git.BlameViewState + lines := []git.BlameLine{ + {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "package main"}, + {Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "func main() {}"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.BlameView(c, &state, lines, git.BlameViewOptions{Language: "go"}).Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFileHistory() { + var state data.ListState[string] + commits := []git.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.FileHistory(c, &state, "main.go", commits, git.FileHistoryOptions{}) + r.Element.Height(240) + if _, hash, ok := r.Action(); ok { + _ = hash // open the diff of main.go at hash + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePullRequestCard() { + pr := git.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main", + ChecksPassed: 3, Reviewers: []git.Reviewer{{Name: "Uika", State: git.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if git.PullRequestCard(c, pr, git.PullRequestCardOptions{}).Changed() { + // open the pull request + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleReviewComment() { + thread := git.ReviewThread{File: "main.go", Line: 12, Messages: []git.ReviewMessage{ + {Author: "Uika", When: time.Now(), Body: "Can this return early?"}, + }} + draft := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.ReviewComment(c, thread, &draft, git.ReviewCommentOptions{}) + if text, ok := r.Replied(); ok { + thread.Messages = append(thread.Messages, git.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text}) + } + if r.Resolved() || r.Reopened() { + thread.Resolved = !thread.Resolved + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDiffStat() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.DiffStat(c, 120, 8, git.DiffStatOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleGitStatusBadge() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.GitStatusBadge(c, git.GitModified, git.GitStatusBadgeOptions{ShowLabel: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommandBlock() { + open := true + run := agent.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := git.CommandBlock(c, run, &open, git.CommandBlockOptions{}) + if what, ok := r.Copied(); ok { + _ = what // "command" or "output" is on the clipboard + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAnsiText() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + git.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", git.AnsiTextOptions{}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/file_history.go b/git/file_history.go similarity index 58% rename from file_history.go rename to git/file_history.go index d13a1fb..55b28a8 100644 --- a/file_history.go +++ b/git/file_history.go @@ -1,27 +1,29 @@ -package mujica +package git import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FileHistoryOptions configure a FileHistory. type FileHistoryOptions struct { // TimeFormat lays out commit times; empty means "2006-01-02 15:04". TimeFormat string - Status DataStatus + Status data.DataStatus Error string } // FileHistory lists commits that changed path; Action "diff" opens one. -func FileHistory(c *ui.Context, s *ListState[string], path string, commits []Commit, opts FileHistoryOptions) GitListResult { +func FileHistory(c *ui.Context, s *data.ListState[string], path string, commits []Commit, opts FileHistoryOptions) GitListResult { if path == "" { panic("mujica: FileHistory needs a path") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() layout := gitTimeLayout(opts.TimeFormat) var r GitListResult col := ui.Column(c).AlignItems(ui.Stretch).Gap(u * 2) @@ -29,13 +31,13 @@ func FileHistory(c *ui.Context, s *ListState[string], path string, commits []Com ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { ui.Icon(c, icons.Must("history")).Size(16, 16).TextColor(k.Ornament.Mix(k.Text, 0.35)) ui.Text(c, path).Font(theme.MonoFont).FontWeight(600).SingleLine().MinWidth(0) - ui.Text(c, msg(c, "file_history.title")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) + ui.Text(c, core.Msg(c, "file_history.title")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Shrink(0) }) - actions := []GitAction{{ID: "diff", Label: msg(c, "file_history.diff"), Icon: "arrow-left-right"}} - v := List(c, s, len(commits), ListOptions[string]{ + actions := []GitAction{{ID: "diff", Label: core.Msg(c, "file_history.diff"), Icon: "arrow-left-right"}} + v := data.List(c, s, len(commits), data.ListOptions[string]{ Key: func(i int) string { return gitCommitHash(commits[i]) }, Label: func(i int) string { return commits[i].Subject }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "file_history.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "file_history.empty"), }, func(i int) { commitListRow(c, commits[i], layout) gitRowActions(c, actions, commits[i].Hash, &r) diff --git a/file_history_test.go b/git/file_history_test.go similarity index 79% rename from file_history_test.go rename to git/file_history_test.go index 21fcbac..cd4a228 100644 --- a/file_history_test.go +++ b/git/file_history_test.go @@ -1,17 +1,20 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestFileHistoryDiffAction(t *testing.T) { - var s ListState[string] + var s data.ListState[string] commits := gitHistory(3) diff := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FileHistory(c, &s, "commit_graph.go", commits, FileHistoryOptions{}) r.Element.Height(240) if a, item, ok := r.Action(); ok && a == "diff" { @@ -34,9 +37,9 @@ func TestFileHistoryDiffAction(t *testing.T) { } func TestFileHistoryEmptyAndInvalid(t *testing.T) { - var s ListState[string] + var s data.ListState[string] tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FileHistory(c, &s, "a.go", nil, FileHistoryOptions{}).Element.Height(200) }, 400, 240) if !tt.HasText("No history for this file") { @@ -48,8 +51,8 @@ func TestFileHistoryEmptyAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] FileHistory(c, &s, "", nil, FileHistoryOptions{}) }, 300, 120, 1) } @@ -57,7 +60,7 @@ func TestFileHistoryEmptyAndInvalid(t *testing.T) { func TestFileHistorySnapshot(t *testing.T) { commits := gitHistory(3) snapshot(t, "file_history", 640, 200, func(c *ui.Context) { - s := ui.Local(c.Root(), "list", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "list", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Children(func() { FileHistory(c, *s, "internal/git.go", commits, FileHistoryOptions{}).Element.Height(170) }) diff --git a/git_status_badge.go b/git/git_status_badge.go similarity index 75% rename from git_status_badge.go rename to git/git_status_badge.go index b9b44a4..5781295 100644 --- a/git_status_badge.go +++ b/git/git_status_badge.go @@ -1,9 +1,10 @@ -package mujica +package git import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // GitStatus is a file's change state in version control. @@ -38,20 +39,20 @@ type GitStatusBadgeOptions struct { // GitStatusBadge marks a status by letter, outline and color. func GitStatusBadge(c *ui.Context, status GitStatus, opts GitStatusBadgeOptions) *ui.Element { - k := Tokens(c) - name := msg(c, "git_status_badge."+gitStatusKeys[gitStatusIndex(status)]) + k := core.Tokens(c) + name := core.Msg(c, "git_status_badge."+gitStatusKeys[gitStatusIndex(status)]) col := gitStatusColor(k, status) - row := ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit() * 1.5).Shrink(0).Label(name).Role(ui.RoleImage) + row := ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit() * 1.5).Shrink(0).Label(name).Role(ui.RoleImage) if !opts.ShowLabel { row.Tooltip(name) } row.Children(func() { ui.Box(c).Size(18, 18).Shrink(0).Center().Radius(theme.ControlRadius).Border(theme.BorderWidth, col). Children(func() { - ui.Text(c, status.Letter()).Font(theme.MonoFont).FontWeight(700).FontSize(FontSize(c, theme.CaptionSize)).TextColor(col) + ui.Text(c, status.Letter()).Font(theme.MonoFont).FontWeight(700).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(col) }) if opts.ShowLabel { - ui.Text(c, name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) return row diff --git a/git_status_badge_test.go b/git/git_status_badge_test.go similarity index 90% rename from git_status_badge_test.go rename to git/git_status_badge_test.go index 2888fdc..20fe46c 100644 --- a/git_status_badge_test.go +++ b/git/git_status_badge_test.go @@ -1,14 +1,16 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestGitStatusBadgeLettersAndNames(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(8).Children(func() { for s := GitModified; s <= GitCopied; s++ { GitStatusBadge(c, s, GitStatusBadgeOptions{}) @@ -30,7 +32,7 @@ func TestGitStatusBadgeLettersAndNames(t *testing.T) { func TestGitStatusBadgeLabelAndInvalid(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) GitStatusBadge(c, GitDeleted, GitStatusBadgeOptions{ShowLabel: true}) }, 300, 80) if !tt.HasText("已删除") { @@ -42,7 +44,7 @@ func TestGitStatusBadgeLabelAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) GitStatusBadge(c, GitStatus(9), GitStatusBadgeOptions{}) }, 100, 50, 1) } diff --git a/git/helpers_test.go b/git/helpers_test.go new file mode 100644 index 0000000..9b24b0a --- /dev/null +++ b/git/helpers_test.go @@ -0,0 +1,30 @@ +package git + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/inline_diff.go b/git/inline_diff.go similarity index 83% rename from inline_diff.go rename to git/inline_diff.go index bcbca60..2715869 100644 --- a/inline_diff.go +++ b/git/inline_diff.go @@ -1,10 +1,11 @@ -package mujica +package git import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // InlineDiffOptions configure an InlineDiff. @@ -21,7 +22,7 @@ type inlineDiffCache struct { // InlineDiff shows old and new lines with changed words marked. func InlineDiff(c *ui.Context, from, to string, opts InlineDiffOptions) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) col := ui.Column(c).AlignItems(ui.Stretch).Shrink(0).Radius(theme.ControlRadius).Clip() cache := ui.Local(col, "words", func() *inlineDiffCache { return &inlineDiffCache{} }) w := *cache @@ -33,15 +34,15 @@ func InlineDiff(c *ui.Context, from, to string, opts InlineDiffOptions) *ui.Elem col.Font(theme.MonoFont) } col.Children(func() { - inlineDiffLine(c, "−", msg(c, "inline_diff.removed"), w.a, k.Danger, k) - inlineDiffLine(c, "+", msg(c, "inline_diff.added"), w.b, k.Success, k) + inlineDiffLine(c, "−", core.Msg(c, "inline_diff.removed"), w.a, k.Danger, k) + inlineDiffLine(c, "+", core.Msg(c, "inline_diff.added"), w.b, k.Success, k) }) return col } // inlineDiffLine shows one side with its changed words marked. func inlineDiffLine(c *ui.Context, sign, name string, spans []internal.WordSpan, col ui.Color, k theme.Tokens) { - u := Density(c).Unit() + u := core.Density(c).Unit() out := make([]ui.Span, 0, len(spans)) plain := "" for _, s := range spans { diff --git a/inline_diff_test.go b/git/inline_diff_test.go similarity index 91% rename from inline_diff_test.go rename to git/inline_diff_test.go index 1f78728..e87e284 100644 --- a/inline_diff_test.go +++ b/git/inline_diff_test.go @@ -1,14 +1,16 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestInlineDiffNamesAndShowsBothLines(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) InlineDiff(c, "The court meets at noon", "The court meets at dusk", InlineDiffOptions{}) }, 600, 120) if _, ok := tt.Find("Old: The court meets at noon"); !ok { @@ -23,7 +25,7 @@ func TestInlineDiffCachesWords(t *testing.T) { from, to := "a b", "a c" var cache *inlineDiffCache tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) e := InlineDiff(c, from, to, InlineDiffOptions{Mono: true}) cache = *ui.Local(e, "words", func() *inlineDiffCache { return nil }) }, 300, 100) diff --git a/pull_request_card.go b/git/pull_request_card.go similarity index 83% rename from pull_request_card.go rename to git/pull_request_card.go index e51706b..c5b73da 100644 --- a/pull_request_card.go +++ b/git/pull_request_card.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PullRequestState is where a pull request stands. @@ -55,19 +57,19 @@ type PullRequestCardOptions struct { } // PullRequestCard summarizes a pull request; Changed reports opening. -func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) ChoiceResult { +func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) core.ChoiceResult { if pr.Number <= 0 || pr.Title == "" { panic("mujica: PullRequest needs a positive Number and a Title") } if pr.ChecksPassed < 0 || pr.ChecksFailed < 0 || pr.ChecksPending < 0 || pr.Files < 0 { panic("mujica: PullRequest counts must not be negative") } - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) card := ui.Column(c).AlignItems(ui.Stretch).Gap(u*2).Padding(u*4).Background(k.Surface). Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).Disabled(opts.Disabled) - res := ChoiceResult{Element: card} + res := core.ChoiceResult{Element: card} card.Children(func() { ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { pullRequestStateBadge(c, pr.State) @@ -77,13 +79,13 @@ func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) if title.Hovered() { title.Background(k.SurfaceHover) } - title.Transition(colorFade) + title.Transition(core.ColorFade) title.Children(func() { ui.Text(c, pr.Title).FontWeight(600).SingleLine().MinWidth(0) ui.Text(c, fmt.Sprintf("#%d", pr.Number)).TextColor(k.TextMuted).FontFeatures("tnum").Shrink(0) }) if title.Clicked() && !title.IsDisabled() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: pull request card open", "number", pr.Number) } }) @@ -104,7 +106,7 @@ func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) }) } ui.Spacer(c) - ui.Text(c, fmt.Sprintf(msg(c, "pull_request_card.files"), pr.Files)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf(core.Msg(c, "pull_request_card.files"), pr.Files)).TextColor(k.TextMuted).FontFeatures("tnum") DiffStat(c, pr.Added, pr.Deleted, DiffStatOptions{}) }) }) @@ -112,8 +114,8 @@ func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) } func pullRequestStateBadge(c *ui.Context, st PullRequestState) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() icon, col, key := "git-pull-request", k.Success, "open" switch st { case PullRequestOpen: @@ -129,13 +131,13 @@ func pullRequestStateBadge(c *ui.Context, st PullRequestState) { ui.Row(c).AlignItems(ui.Center).Gap(u).Padding(2, u*2).Radius(theme.ControlRadius).Border(theme.BorderWidth, col). TextColor(col).Shrink(0).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, msg(c, "pull_request_card."+key)).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() + ui.Text(c, core.Msg(c, "pull_request_card."+key)).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() }) } func pullRequestChecks(c *ui.Context, pr PullRequest) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Children(func() { for _, ch := range []struct { n int @@ -149,7 +151,7 @@ func pullRequestChecks(c *ui.Context, pr PullRequest) { if ch.n == 0 { continue } - name := fmt.Sprintf(msg(c, ch.key), ch.n) + name := fmt.Sprintf(core.Msg(c, ch.key), ch.n) ui.Row(c).AlignItems(ui.Center).Gap(u).TextColor(ch.col).Label(name).Tooltip(name).Children(func() { ui.Icon(c, icons.Must(ch.icon)).Size(14, 14) ui.Text(c, fmt.Sprint(ch.n)).FontFeatures("tnum") @@ -159,7 +161,7 @@ func pullRequestChecks(c *ui.Context, pr PullRequest) { } func pullRequestReviewer(c *ui.Context, r Reviewer) { - k := Tokens(c) + k := core.Tokens(c) key, icon, col := "pull_request_card.review_pending", "clock", k.TextMuted switch r.State { case ReviewPending: @@ -170,9 +172,9 @@ func pullRequestReviewer(c *ui.Context, r Reviewer) { default: panic("mujica: unknown ReviewState") } - name := r.Name + ": " + msg(c, key) + name := r.Name + ": " + core.Msg(c, key) ui.Box(c).Size(28, 28).Shrink(0).Label(name).Tooltip(name).Children(func() { - Avatar(c, r.Name, AvatarOptions{Size: 24}).Absolute().Left(0).Top(0) + display.Avatar(c, r.Name, display.AvatarOptions{Size: 24}).Absolute().Left(0).Top(0) ui.Box(c).Size(14, 14).Radius(7).Background(k.Surface).Center().Absolute().Right(0).Bottom(0).TextColor(col). Children(func() { ui.Icon(c, icons.Must(icon)).Size(10, 10) }) }) diff --git a/pull_request_card_test.go b/git/pull_request_card_test.go similarity index 94% rename from pull_request_card_test.go rename to git/pull_request_card_test.go index caa4960..c37b8a9 100644 --- a/pull_request_card_test.go +++ b/git/pull_request_card_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func pullRequestSample() PullRequest { @@ -17,7 +19,7 @@ func TestPullRequestCardShowsAndOpens(t *testing.T) { opens := 0 disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if PullRequestCard(c, pullRequestSample(), PullRequestCardOptions{Disabled: disabled}).Changed() { opens++ } @@ -48,7 +50,7 @@ func TestPullRequestCardStatesAndInvalid(t *testing.T) { pr := pullRequestSample() pr.State = st tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PullRequestCard(c, pr, PullRequestCardOptions{}) }, 700, 240) if !tt.HasText(word) { @@ -63,7 +65,7 @@ func TestPullRequestCardStatesAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PullRequestCard(c, pr, PullRequestCardOptions{}) }, 300, 100, 1) }() diff --git a/review_comment.go b/git/review_comment.go similarity index 76% rename from review_comment.go rename to git/review_comment.go index 5731307..6b6b0d3 100644 --- a/review_comment.go +++ b/git/review_comment.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -8,8 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ReviewMessage is one comment of a review thread. @@ -55,9 +58,9 @@ func ReviewComment(c *ui.Context, thread ReviewThread, draft *string, opts Revie if thread.File == "" || thread.Line <= 0 || len(thread.Messages) == 0 { panic("mujica: ReviewThread needs a File, a positive Line and a message") } - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) layout := gitTimeLayout(opts.TimeFormat) card := ui.Column(c).AlignItems(ui.Stretch).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Disabled(opts.Disabled).Clip() @@ -69,7 +72,7 @@ func ReviewComment(c *ui.Context, thread ReviewThread, draft *string, opts Revie if thread.Resolved { ui.Row(c).AlignItems(ui.Center).Gap(u).TextColor(k.Success).Children(func() { ui.Icon(c, icons.Must("circle-check")).Size(14, 14) - ui.Text(c, msg(c, "review_comment.resolved")).FontWeight(600) + ui.Text(c, core.Msg(c, "review_comment.resolved")).FontWeight(600) }) } }) @@ -79,7 +82,7 @@ func ReviewComment(c *ui.Context, thread ReviewThread, draft *string, opts Revie ui.Box(c).Height(1).Background(k.Border).Margin(0, 0, u*2, 0).Role(ui.RoleNone) } ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { - Avatar(c, m.Author, AvatarOptions{Size: 20}) + display.Avatar(c, m.Author, display.AvatarOptions{Size: 20}) ui.Text(c, m.Author).FontWeight(600).SingleLine() ui.Text(c, m.When.Format(layout)).FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum") }) @@ -89,14 +92,14 @@ func ReviewComment(c *ui.Context, thread ReviewThread, draft *string, opts Revie ui.Box(c).Height(1).Background(k.Border).Role(ui.RoleNone) ui.Column(c).Gap(u * 2).Padding(u * 3).AlignItems(ui.Stretch).Children(func() { if !thread.Resolved { - in := TextArea(c, draft, TextAreaOptions{Placeholder: msg(c, "review_comment.placeholder"), Label: msg(c, "review_comment.field"), + in := input.TextArea(c, draft, input.TextAreaOptions{Placeholder: core.Msg(c, "review_comment.placeholder"), Label: core.Msg(c, "review_comment.field"), MinHeight: 56, MaxHeight: 160, Disabled: opts.Disabled}) send := in.Submitted() ui.Row(c).Gap(u * 2).Justify(ui.End).Children(func() { - if Button(c, msg(c, "review_comment.resolve"), ButtonOptions{Variant: Secondary, Icon: icons.Must("check"), Disabled: opts.Disabled}).Clicked() { + if input.Button(c, core.Msg(c, "review_comment.resolve"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("check"), Disabled: opts.Disabled}).Clicked() { res.resolved = true } - if Button(c, msg(c, "review_comment.reply"), ButtonOptions{Icon: icons.Must("reply"), Disabled: opts.Disabled || strings.TrimSpace(*draft) == ""}).Clicked() { + if input.Button(c, core.Msg(c, "review_comment.reply"), input.ButtonOptions{Icon: icons.Must("reply"), Disabled: opts.Disabled || strings.TrimSpace(*draft) == ""}).Clicked() { send = true } }) @@ -106,7 +109,7 @@ func ReviewComment(c *ui.Context, thread ReviewThread, draft *string, opts Revie return } ui.Row(c).Justify(ui.End).Children(func() { - if Button(c, msg(c, "review_comment.reopen"), ButtonOptions{Variant: Secondary, Icon: icons.Must("rotate-ccw"), Disabled: opts.Disabled}).Clicked() { + if input.Button(c, core.Msg(c, "review_comment.reopen"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("rotate-ccw"), Disabled: opts.Disabled}).Clicked() { res.reopened = true } }) diff --git a/review_comment_test.go b/git/review_comment_test.go similarity index 94% rename from review_comment_test.go rename to git/review_comment_test.go index f2b8711..2e1dcd3 100644 --- a/review_comment_test.go +++ b/git/review_comment_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func reviewSample() ReviewThread { @@ -19,7 +21,7 @@ func TestReviewCommentReplyResolveReopen(t *testing.T) { var replies []string resolves, reopens := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ReviewComment(c, thread, &draft, ReviewCommentOptions{}) if s, ok := r.Replied(); ok { replies = append(replies, s) @@ -62,7 +64,7 @@ func TestReviewCommentDisabledAndInvalid(t *testing.T) { draft := "Text" acted := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ReviewComment(c, reviewSample(), &draft, ReviewCommentOptions{Disabled: true}) _, replied := r.Replied() acted = acted || replied || r.Resolved() @@ -80,7 +82,7 @@ func TestReviewCommentDisabledAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ReviewComment(c, th, &draft, ReviewCommentOptions{}) }, 300, 100, 1) }() diff --git a/stash_list.go b/git/stash_list.go similarity index 61% rename from stash_list.go rename to git/stash_list.go index f505ca0..d0e8714 100644 --- a/stash_list.go +++ b/git/stash_list.go @@ -1,10 +1,12 @@ -package mujica +package git import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Stash is one stash entry. @@ -18,26 +20,26 @@ type Stash struct { // StashListOptions configure a StashList. type StashListOptions struct { // Status and Error show the load state. - Status DataStatus + Status data.DataStatus Error string } // StashList shows stashes with apply, pop and drop actions. -func StashList(c *ui.Context, s *ListState[string], stashes []Stash, opts StashListOptions) GitListResult { +func StashList(c *ui.Context, s *data.ListState[string], stashes []Stash, opts StashListOptions) GitListResult { var r GitListResult - k := Tokens(c) + k := core.Tokens(c) actions := []GitAction{ - {ID: "apply", Label: msg(c, "stash_list.apply"), Icon: "check"}, - {ID: "pop", Label: msg(c, "stash_list.pop"), Icon: "upload"}, - {ID: "drop", Label: msg(c, "stash_list.drop"), Icon: "trash-2"}, + {ID: "apply", Label: core.Msg(c, "stash_list.apply"), Icon: "check"}, + {ID: "pop", Label: core.Msg(c, "stash_list.pop"), Icon: "upload"}, + {ID: "drop", Label: core.Msg(c, "stash_list.drop"), Icon: "trash-2"}, } - v := List(c, s, len(stashes), ListOptions[string]{ + v := data.List(c, s, len(stashes), data.ListOptions[string]{ Key: func(i int) string { return stashListKey(stashes[i]) }, Label: func(i int) string { return stashes[i].Message }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "stash_list.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "stash_list.empty"), }, func(i int) { st := stashes[i] - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) ui.Icon(c, icons.Must("archive")).Size(14, 14).Shrink(0).TextColor(k.TextMuted) ui.Text(c, st.Ref).Font(theme.MonoFont).FontSize(small).TextColor(k.AccentText).Shrink(0) ui.Text(c, st.Message).SingleLine().Grow(1).MinWidth(0) diff --git a/stash_list_test.go b/git/stash_list_test.go similarity index 81% rename from stash_list_test.go rename to git/stash_list_test.go index fd76dd0..3f61ee8 100644 --- a/stash_list_test.go +++ b/git/stash_list_test.go @@ -1,9 +1,12 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func stashSample() []Stash { @@ -14,10 +17,10 @@ func stashSample() []Stash { } func TestStashListActions(t *testing.T) { - var s ListState[string] + var s data.ListState[string] var acted [][2]string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := StashList(c, &s, stashSample(), StashListOptions{}) r.Element.Height(200) if a, item, ok := r.Action(); ok { @@ -42,9 +45,9 @@ func TestStashListActions(t *testing.T) { } func TestStashListEmptyAndInvalid(t *testing.T) { - var s ListState[string] + var s data.ListState[string] tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) StashList(c, &s, nil, StashListOptions{}) }, 400, 200) if !tt.HasText("没有贮藏") { @@ -56,15 +59,15 @@ func TestStashListEmptyAndInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] StashList(c, &s, []Stash{{Message: "x"}}, StashListOptions{}).Element.Height(100) }, 300, 120, 1) } func TestStashListSnapshot(t *testing.T) { snapshot(t, "stash_list", 640, 140, func(c *ui.Context) { - s := ui.Local(c.Root(), "list", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "list", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Children(func() { StashList(c, *s, stashSample(), StashListOptions{}).Element.Height(100) }) diff --git a/tag_list.go b/git/tag_list.go similarity index 63% rename from tag_list.go rename to git/tag_list.go index 7e14756..4a8e945 100644 --- a/tag_list.go +++ b/git/tag_list.go @@ -1,10 +1,12 @@ -package mujica +package git import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // GitTag is a tag and the commit it names. @@ -17,26 +19,26 @@ type GitTag struct { // TagListOptions configure a TagList. type TagListOptions struct { - Status DataStatus + Status data.DataStatus Error string } // TagList shows tags; Action "jump" carries the target hash. -func TagList(c *ui.Context, s *ListState[string], tags []GitTag, opts TagListOptions) GitListResult { +func TagList(c *ui.Context, s *data.ListState[string], tags []GitTag, opts TagListOptions) GitListResult { var r GitListResult - k := Tokens(c) - v := List(c, s, len(tags), ListOptions[string]{ + k := core.Tokens(c) + v := data.List(c, s, len(tags), data.ListOptions[string]{ Key: func(i int) string { return tagListKey(tags[i]) }, Label: func(i int) string { return tags[i].Name }, - Status: opts.Status, Error: opts.Error, Empty: msg(c, "tag_list.empty"), + Status: opts.Status, Error: opts.Error, Empty: core.Msg(c, "tag_list.empty"), }, func(i int) { t := tags[i] - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) ui.Icon(c, icons.Must("tag")).Size(14, 14).Shrink(0).TextColor(k.TextMuted) ui.Text(c, t.Name).FontWeight(600).SingleLine().Shrink(0).MaxWidth(200) ui.Text(c, t.Annotation).FontSize(small).TextColor(k.TextMuted).SingleLine().Grow(1).MinWidth(0) ui.Text(c, gitShortHash(t.Target)).Font(theme.MonoFont).FontSize(small).TextColor(k.AccentText).Shrink(0) - gitRowActions(c, []GitAction{{ID: "jump", Label: msg(c, "tag_list.jump"), Icon: "git-commit-horizontal"}}, t.Target, &r) + gitRowActions(c, []GitAction{{ID: "jump", Label: core.Msg(c, "tag_list.jump"), Icon: "git-commit-horizontal"}}, t.Target, &r) }) r.Element, r.changed, r.submitted = v.Element, v.Changed(), v.Submitted() return r diff --git a/tag_list_test.go b/git/tag_list_test.go similarity index 80% rename from tag_list_test.go rename to git/tag_list_test.go index 5f4ada8..ee9918f 100644 --- a/tag_list_test.go +++ b/git/tag_list_test.go @@ -1,9 +1,12 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func tagSample() []GitTag { @@ -14,10 +17,10 @@ func tagSample() []GitTag { } func TestTagListJumpAndSelect(t *testing.T) { - var s ListState[string] + var s data.ListState[string] jump := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TagList(c, &s, tagSample(), TagListOptions{}) r.Element.Height(200) if a, item, ok := r.Action(); ok && a == "jump" { @@ -48,15 +51,15 @@ func TestTagListInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - var s ListState[string] + core.Use(c, core.Settings{}) + var s data.ListState[string] TagList(c, &s, []GitTag{{Name: "v1"}}, TagListOptions{}).Element.Height(100) }, 300, 120, 1) } func TestTagListSnapshot(t *testing.T) { snapshot(t, "tag_list", 600, 140, func(c *ui.Context) { - s := ui.Local(c.Root(), "list", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "list", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(16).Children(func() { TagList(c, *s, tagSample(), TagListOptions{}).Element.Height(100) }) diff --git a/three_way_merge.go b/git/three_way_merge.go similarity index 85% rename from three_way_merge.go rename to git/three_way_merge.go index 7a7ab59..2f46326 100644 --- a/three_way_merge.go +++ b/git/three_way_merge.go @@ -1,4 +1,4 @@ -package mujica +package git import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MergeChoice is how a conflict is resolved. @@ -94,7 +95,7 @@ type ThreeWayMergeOptions struct { } // ThreeWayMerge shows four columns per chunk; give it a height. -func ThreeWayMerge(c *ui.Context, s *ThreeWayMergeState, base, ours, theirs string, opts ThreeWayMergeOptions) ChoiceResult { +func ThreeWayMerge(c *ui.Context, s *ThreeWayMergeState, base, ours, theirs string, opts ThreeWayMergeOptions) core.ChoiceResult { if !s.built || s.base != base || s.ours != ours || s.theirs != theirs { s.chunks = mergeChunks(internal.SplitLines(base), internal.SplitLines(ours), internal.SplitLines(theirs)) s.choices = make([]MergeChoice, len(s.chunks)) @@ -102,25 +103,25 @@ func ThreeWayMerge(c *ui.Context, s *ThreeWayMergeState, base, ours, theirs stri _, total := s.Resolved() slog.Debug("mujica: three way merge chunks", "chunks", len(s.chunks), "conflicts", total) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := ui.Column(c).AlignItems(ui.Stretch).Disabled(opts.Disabled).Background(k.Surface). Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Clip() - res := ChoiceResult{Element: col} + res := core.ChoiceResult{Element: col} col.Children(func() { done, total := s.Resolved() ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*3).Background(k.Background).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "three_way_merge.resolved"), done, total)).FontWeight(600).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "three_way_merge.resolved"), done, total)).FontWeight(600).Role(ui.RoleStatus) }) ui.Grid(c).ColumnTracks(ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1)).Background(k.Background).Children(func() { for _, key := range []string{"ours", "base", "theirs", "result"} { - ui.Text(c, msg(c, "three_way_merge."+key)).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, core.Msg(c, "three_way_merge."+key)).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.Ornament.Mix(k.Text, 0.35)).Padding(u, u*3) } }) ui.List(c, &s.list, len(s.chunks), func(i int) { if threeWayMergeChunk(c, s, i) && !col.IsDisabled() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) } }).Grow(1) }) @@ -129,8 +130,8 @@ func ThreeWayMerge(c *ui.Context, s *ThreeWayMergeState, base, ours, theirs stri // threeWayMergeChunk builds chunk i and reports a new choice. func threeWayMergeChunk(c *ui.Context, s *ThreeWayMergeState, i int) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ch := s.chunks[i] changed := false ui.Column(c).AlignItems(ui.Stretch).Children(func() { @@ -160,7 +161,7 @@ func threeWayMergeChunk(c *ui.Context, s *ThreeWayMergeState, i int) bool { if ch.conflict && choice == MergeUnresolved { ui.Row(c).Gap(u).AlignItems(ui.Center).TextColor(k.Danger).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14) - ui.Text(c, msg(c, "three_way_merge.unresolved")).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, "three_way_merge.unresolved")).FontSize(core.FontSize(c, theme.CaptionSize)) }) return } @@ -175,7 +176,7 @@ func threeWayMergeChunk(c *ui.Context, s *ThreeWayMergeState, i int) bool { choice MergeChoice key string }{{MergeOurs, "take_ours"}, {MergeTheirs, "take_theirs"}, {MergeBoth, "take_both"}, {MergeBase, "take_base"}} { - if mergeChoiceButton(c, msg(c, "three_way_merge."+opt.key), s.choices[i] == opt.choice).Clicked() && s.choices[i] != opt.choice { + if mergeChoiceButton(c, core.Msg(c, "three_way_merge."+opt.key), s.choices[i] == opt.choice).Clicked() && s.choices[i] != opt.choice { s.choices[i], changed = opt.choice, true slog.Debug("mujica: three way merge choose", "chunk", i, "choice", opt.key) } @@ -187,8 +188,8 @@ func threeWayMergeChunk(c *ui.Context, s *ThreeWayMergeState, i int) bool { // mergeChoiceButton is a toggle marking the chosen side. func mergeChoiceButton(c *ui.Context, label string, on bool) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Padding(0, u*3).Gap(u*1.5).Radius(theme.ControlRadius). Border(theme.BorderWidth, k.ControlBorder).Label(label).TextColor(k.Text) internal.Ring(b, k.Focus, theme.ControlRadius) @@ -200,32 +201,32 @@ func mergeChoiceButton(c *ui.Context, label string, on bool) *ui.Element { case b.Hovered(): b.Background(k.SurfaceHover) } - b.Transition(colorFade) + b.Transition(core.ColorFade) b.Children(func() { if on { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) } - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) return b } // mergeLines shows code lines, folding long stable runs. func mergeLines(c *ui.Context, lines []string, fold bool) { - k := Tokens(c) + k := core.Tokens(c) show := func(ls []string) { for _, l := range ls { if l == "" { l = " " } - t := ui.Text(c, l).Font(theme.MonoFont).NoWrap().FontSize(FontSize(c, theme.CaptionSize)) + t := ui.Text(c, l).Font(theme.MonoFont).NoWrap().FontSize(core.FontSize(c, theme.CaptionSize)) if fold { t.TextColor(k.TextMuted) } } } if len(lines) == 0 { - ui.Text(c, "∅").TextColor(k.TextMuted).Label(msg(c, "three_way_merge.empty")) + ui.Text(c, "∅").TextColor(k.TextMuted).Label(core.Msg(c, "three_way_merge.empty")) return } if !fold || len(lines) <= 5 { @@ -233,8 +234,8 @@ func mergeLines(c *ui.Context, lines []string, fold bool) { return } show(lines[:2]) - ui.Text(c, fmt.Sprintf(msg(c, "diff_viewer.fold"), len(lines)-4)).Font(theme.SerifFont). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "diff_viewer.fold"), len(lines)-4)).Font(theme.SerifFont). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) show(lines[len(lines)-2:]) } diff --git a/three_way_merge_test.go b/git/three_way_merge_test.go similarity index 96% rename from three_way_merge_test.go rename to git/three_way_merge_test.go index 87e7551..78fea8b 100644 --- a/three_way_merge_test.go +++ b/git/three_way_merge_test.go @@ -1,9 +1,11 @@ -package mujica +package git import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) const ( @@ -38,7 +40,7 @@ func TestThreeWayMergeTakeSides(t *testing.T) { changes := 0 disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ThreeWayMerge(c, &s, mergeBase, mergeOurs, mergeTheirs, ThreeWayMergeOptions{Disabled: disabled}) r.Element.Height(560) if r.Changed() { diff --git a/go.mod b/go.mod index fae75d6..7ca7ec5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module mujicaui +module github.com/ZacharyZhang-NY/MujicaUI go 1.27.1 diff --git a/banner.go b/input/banner.go similarity index 62% rename from banner.go rename to input/banner.go index db5f1e1..f534d2a 100644 --- a/banner.go +++ b/input/banner.go @@ -1,41 +1,42 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BannerOptions configure a Banner. type BannerOptions struct { - Severity Severity + Severity core.Severity // Action labels a button after the message; empty shows none. Action string } // Banner is a full-width notice for the top of a page; a non-nil open adds a close button. -func Banner(c *ui.Context, open *bool, message string, opts BannerOptions) AlertResult { +func Banner(c *ui.Context, open *bool, message string, opts BannerOptions) core.AlertResult { if message == "" { panic("mujica: Banner needs a message") } - k := Tokens(c) - tone, icon := severityLook(k, opts.Severity) + k := core.Tokens(c) + tone, icon := core.SeverityLook(k, opts.Severity) if open != nil && !*open { - return AlertResult{} + return core.AlertResult{} } - u := Density(c).Unit() - name := msg(c, "alert."+opts.Severity.String()) - var res AlertResult - bar := ui.Row(c).FillWidth().MinHeight(Density(c).ControlHeight()+2*u).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*3, u*2, u*4). + u := core.Density(c).Unit() + name := core.Msg(c, "alert."+opts.Severity.String()) + var res core.AlertResult + bar := ui.Row(c).FillWidth().MinHeight(core.Density(c).ControlHeight()+2*u).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*3, u*2, u*4). Background(k.Surface.Mix(tone, 0.08)).Role(ui.RoleGroup).Label(name) bar.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 2, W: r.W, H: 2}, tone, 0) }) said := ui.Local(bar, "banner.said", func() string { return "" }) - if news := name + ": " + message; opts.Severity >= SeverityWarning && *said != news { + if news := name + ": " + message; opts.Severity >= core.SeverityWarning && *said != news { *said = news c.Announce(news) } @@ -46,7 +47,7 @@ func Banner(c *ui.Context, open *bool, message string, opts BannerOptions) Alert slog.Debug("mujica: banner action", "severity", opts.Severity) res.Action = true } - if open != nil && iconAction(c, icons.Must("x"), msg(c, "alert.close")).Clicked() { + if open != nil && core.IconAction(c, icons.Must("x"), core.Msg(c, "alert.close")).Clicked() { slog.Debug("mujica: banner closed", "severity", opts.Severity) *open, res.Closed = false, true } diff --git a/banner_test.go b/input/banner_test.go similarity index 78% rename from banner_test.go rename to input/banner_test.go index bba858e..79212bb 100644 --- a/banner_test.go +++ b/input/banner_test.go @@ -1,18 +1,20 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBannerActionCloseAndAnnounce(t *testing.T) { open := true - var last AlertResult + var last core.AlertResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if r := Banner(c, &open, "Maintenance tonight at 22:00", BannerOptions{Severity: SeverityWarning, Action: "Details"}); r.Action || r.Closed { + core.Use(c, core.Settings{}) + if r := Banner(c, &open, "Maintenance tonight at 22:00", BannerOptions{Severity: core.SeverityWarning, Action: "Details"}); r.Action || r.Closed { last = r } }, 700, 200) @@ -35,9 +37,9 @@ func TestBannerActionCloseAndAnnounce(t *testing.T) { func TestBannerDisabledAndFixed(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { - Banner(c, &open, "Read only", BannerOptions{Severity: SeverityError}) + Banner(c, &open, "Read only", BannerOptions{Severity: core.SeverityError}) }) Banner(c, nil, "Always here", BannerOptions{}) }, 700, 200) @@ -58,7 +60,7 @@ func TestBannerDisabledAndFixed(t *testing.T) { t.Error("no panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 100) }() } } @@ -68,8 +70,8 @@ func TestBannerSnapshot(t *testing.T) { snapshot(t, "banner", 640, 200, func(c *ui.Context) { ui.Column(c).Gap(8).Children(func() { Banner(c, &open, "A new version is available.", BannerOptions{Action: "Update"}) - Banner(c, &open, "Your trial ends in 3 days.", BannerOptions{Severity: SeverityWarning}) - Banner(c, nil, "Sync is failing; changes stay on this device.", BannerOptions{Severity: SeverityError, Action: "Retry"}) + Banner(c, &open, "Your trial ends in 3 days.", BannerOptions{Severity: core.SeverityWarning}) + Banner(c, nil, "Sync is failing; changes stay on this device.", BannerOptions{Severity: core.SeverityError, Action: "Retry"}) }) }) } diff --git a/baseline_states_test.go b/input/baseline_states_test.go similarity index 99% rename from baseline_states_test.go rename to input/baseline_states_test.go index e7dc8da..5d59eb8 100644 --- a/baseline_states_test.go +++ b/input/baseline_states_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "testing" diff --git a/bulk_action_bar.go b/input/bulk_action_bar.go similarity index 65% rename from bulk_action_bar.go rename to input/bulk_action_bar.go index 0ca0e62..524707e 100644 --- a/bulk_action_bar.go +++ b/input/bulk_action_bar.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BulkActionBarOptions configure a BulkActionBar. @@ -16,19 +17,19 @@ type BulkActionBarOptions struct { } // BulkActionBar shows while selected > 0 with the count, actions and a clear button; Changed reports a clear. -func BulkActionBar(c *ui.Context, selected int, opts BulkActionBarOptions, actions func()) ChoiceResult { +func BulkActionBar(c *ui.Context, selected int, opts BulkActionBarOptions, actions func()) core.ChoiceResult { if selected < 0 { panic(fmt.Sprintf("mujica: BulkActionBar selected %d is negative", selected)) } if selected == 0 { - return ChoiceResult{} + return core.ChoiceResult{} } - k := Tokens(c) - u := Density(c).Unit() - count := fmt.Sprintf(msg(c, "bulk_action_bar.selected"), selected) - bar := ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).MinHeight(Density(c).ControlHeight() + 4*u). + k := core.Tokens(c) + u := core.Density(c).Unit() + count := fmt.Sprintf(core.Msg(c, "bulk_action_bar.selected"), selected) + bar := ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*2, u*2, u*2, u*4).MinHeight(core.Density(c).ControlHeight() + 4*u). Role(ui.RoleToolbar).Label(count).Disabled(opts.Disabled) - overlayStyle(c, bar, overlayEnter(c, bar)) + core.OverlayStyle(c, bar, core.OverlayEnter(c, bar)) said := ui.Local(bar, "bulk_action_bar.said", func() int { return 0 }) if *said != selected { *said = selected @@ -43,11 +44,11 @@ func BulkActionBar(c *ui.Context, selected int, opts BulkActionBarOptions, actio actions() } }) - b := Button(c, msg(c, "bulk_action_bar.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("x")}) + b := Button(c, core.Msg(c, "bulk_action_bar.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("x")}) clear = b.Clicked() || bar.Shortcut(0, ui.KeyEscape) && !bar.IsDisabled() }) if clear { slog.Debug("mujica: bulk_action_bar clear", "selected", selected) } - return ChoiceResult{bar, clear} + return core.NewChoiceResult(bar, clear) } diff --git a/bulk_action_bar_test.go b/input/bulk_action_bar_test.go similarity index 88% rename from bulk_action_bar_test.go rename to input/bulk_action_bar_test.go index b1c14c2..1bf6582 100644 --- a/bulk_action_bar_test.go +++ b/input/bulk_action_bar_test.go @@ -1,17 +1,19 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestBulkActionBarCountActionsClear(t *testing.T) { selected, deletes := 0, 0 dis := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Button(c, "Select", ButtonOptions{}).Clicked() { selected += 3 } @@ -60,7 +62,7 @@ func TestBulkActionBarRejectsNegative(t *testing.T) { t.Fatal("no panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); BulkActionBar(c, -1, BulkActionBarOptions{}, nil) }, 200, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); BulkActionBar(c, -1, BulkActionBarOptions{}, nil) }, 200, 100) } func TestBulkActionBarSnapshot(t *testing.T) { diff --git a/button.go b/input/button.go similarity index 85% rename from button.go rename to input/button.go index d3c2e2f..c7f9b84 100644 --- a/button.go +++ b/input/button.go @@ -1,10 +1,11 @@ -package mujica +package input import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ButtonVariant is a button's emphasis. The zero value is Primary. @@ -37,8 +38,8 @@ func Button(c *ui.Context, label string, opts ButtonOptions) *ui.Element { if label == "" && opts.Label == "" { panic("mujica: Button without a label needs Label to name it") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) pad, gap := d.Unit()*4, d.Unit()*2 if opts.Loading && opts.Icon == nil && label != "" { // The spinner slot comes out of the padding, so the width holds. @@ -51,14 +52,14 @@ func Button(c *ui.Context, label string, opts ButtonOptions) *ui.Element { if opts.Label != "" { b.Label(opts.Label) } - bg, fg, border := buttonColors(k, opts.Variant, b.Hovered() && !opts.Disabled, b.Pressed()) + bg, fg, border := ButtonColors(k, opts.Variant, b.Hovered() && !opts.Disabled, b.Pressed()) b.Background(bg).TextColor(fg) if border.A > 0 { b.Border(theme.BorderWidth, border) } b.Children(func() { if opts.Loading { - Spinner(c, SpinnerOptions{Size: buttonSpinnerSize, Color: fg}) + core.Spinner(c, core.SpinnerOptions{Size: buttonSpinnerSize, Color: fg}) } else if opts.Icon != nil { ui.Icon(c, opts.Icon).Size(buttonSpinnerSize, buttonSpinnerSize) } @@ -69,7 +70,7 @@ func Button(c *ui.Context, label string, opts ButtonOptions) *ui.Element { return b } -func buttonColors(k theme.Tokens, v ButtonVariant, hover, pressed bool) (bg, fg, border ui.Color) { +func ButtonColors(k theme.Tokens, v ButtonVariant, hover, pressed bool) (bg, fg, border ui.Color) { switch v { case Secondary: bg, fg, border = k.Surface, k.Text, k.ControlBorder diff --git a/button_group.go b/input/button_group.go similarity index 92% rename from button_group.go rename to input/button_group.go index 3a057b7..59990e9 100644 --- a/button_group.go +++ b/input/button_group.go @@ -1,12 +1,13 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ButtonGroupItem is one button of a ButtonGroup. Give Label, Icon or both; icon-only needs Name. @@ -40,8 +41,8 @@ func ButtonGroup(c *ui.Context, items []ButtonGroupItem, opts ButtonGroupOptions if len(items) == 0 { panic("mujica: ButtonGroup needs items") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) orient, last := ui.Horizontal, len(items)-1 g := ui.Row(c).AlignItems(ui.Stretch).Shrink(0) if opts.Vertical { @@ -70,7 +71,7 @@ func ButtonGroup(c *ui.Context, items []ButtonGroupItem, opts ButtonGroupOptions } else if b.Hovered() { bg = k.SurfaceHover } - b.Background(bg).TextColor(k.Text).Transition(colorFade) + b.Background(bg).TextColor(k.Text).Transition(core.ColorFade) b.Radius(buttonGroupCorners(i, last, opts.Vertical)...) b.Border(theme.BorderWidth, k.ControlBorder).BorderWidth(buttonGroupEdges(i, opts.Vertical)...) b.Children(func() { diff --git a/button_group_test.go b/input/button_group_test.go similarity index 91% rename from button_group_test.go rename to input/button_group_test.go index ead0aa7..e655dc5 100644 --- a/button_group_test.go +++ b/input/button_group_test.go @@ -1,11 +1,12 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) var buttonGroupTestItems = []ButtonGroupItem{{Label: "Left"}, {Label: "Center", Disabled: true}, {Label: "Right"}} @@ -13,7 +14,7 @@ var buttonGroupTestItems = []ButtonGroupItem{{Label: "Left"}, {Label: "Center", func TestButtonGroupClickAndArrows(t *testing.T) { var got []int tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if i := ButtonGroup(c, buttonGroupTestItems, ButtonGroupOptions{Label: "Align"}).ClickedItem(); i >= 0 { got = append(got, i) } @@ -36,7 +37,7 @@ func TestButtonGroupClickAndArrows(t *testing.T) { func TestButtonGroupOneTabStop(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ButtonGroup(c, buttonGroupTestItems, ButtonGroupOptions{Vertical: true}) Button(c, "After", ButtonOptions{}) }, 400, 300) @@ -57,7 +58,7 @@ func TestButtonGroupOneTabStop(t *testing.T) { func TestButtonGroupDisabledGroup(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ButtonGroup(c, buttonGroupTestItems, ButtonGroupOptions{Disabled: true}).ClickedItem() >= 0 { n++ } diff --git a/button_test.go b/input/button_test.go similarity index 86% rename from button_test.go rename to input/button_test.go index 8a1bc96..8d6bdd8 100644 --- a/button_test.go +++ b/input/button_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "strings" @@ -6,14 +6,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestButtonClickOnce(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) if Button(c, "Save", ButtonOptions{}).Clicked() { n++ } @@ -29,7 +30,7 @@ func TestButtonClickOnce(t *testing.T) { func TestButtonKeyboard(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Button(c, "Save", ButtonOptions{}).Clicked() { n++ } @@ -46,7 +47,7 @@ func TestButtonBlockedStates(t *testing.T) { for _, opts := range []ButtonOptions{{Disabled: true}, {Loading: true}} { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Button(c, "Pay", opts).Clicked() { n++ } @@ -70,7 +71,7 @@ func TestButtonMinHeight(t *testing.T) { }{{theme.Compact, 28}, {theme.Default, 36}, {theme.Comfortable, 44}} { var h float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: tc.density}) + core.Use(c, core.Settings{Density: tc.density}) ui.Column(c).AlignItems(ui.Start).Children(func() { h = Button(c, "Go", ButtonOptions{}).Bounds().H }) @@ -85,7 +86,7 @@ func TestButtonMinHeight(t *testing.T) { func TestButtonIconAndDanger(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Button(c, "", ButtonOptions{Variant: Danger, Icon: icons.Must("trash-2"), Label: "Delete"}).Clicked() { n++ } @@ -103,11 +104,11 @@ func buttonDensitySnapshots(t *testing.T, name string, w, h int, view func(c *ui t.Helper() for _, d := range []theme.Density{theme.Compact, theme.Comfortable} { snapshot(t, name+"-"+strings.ToLower(d.String()), w, h, func(c *ui.Context) { - m := Light - if Tokens(c).Dark { - m = Dark + m := core.Light + if core.Tokens(c).Dark { + m = core.Dark } - Use(c, Settings{Mode: m, Density: d}) + core.Use(c, core.Settings{Mode: m, Density: d}) view(c) }) } diff --git a/cascader.go b/input/cascader.go similarity index 86% rename from cascader.go rename to input/cascader.go index 186eb47..1bfb260 100644 --- a/cascader.go +++ b/input/cascader.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CascaderNode is one choice at a level of a Cascader. @@ -66,7 +67,7 @@ type cascaderState struct { type cascaderKey struct{} // Cascader chooses a path through levels the caller supplies, loading children on demand. -func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult { +func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) core.ChoiceResult { if opts.Level == nil { panic("mujica: Cascader needs Level") } @@ -89,17 +90,17 @@ func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult } } } - selectField(c, tr, st.open, func() { + SelectField(c, tr, st.open, func() { if len(*path) == 0 { - ui.Text(c, selectOr(opts.Placeholder, msg(c, "select.placeholder"))).SingleLine().TextColor(Tokens(c).TextMuted).Grow(1) + ui.Text(c, selectOr(opts.Placeholder, core.Msg(c, "select.placeholder"))).SingleLine().TextColor(core.Tokens(c).TextMuted).Grow(1) return } ui.Text(c, cascaderLabels(opts, *path)).SingleLine().Grow(1).Shrink(1) }) ui.PopoverBase(c, tr, &st.open, func(panel *ui.Element) { - selectPanel(c, panel) + SelectPanel(c, panel) panel.Padding(0) - ui.Row(c).AlignItems(ui.Stretch).Dividers(theme.BorderWidth, Tokens(c).Border).Children(func() { + ui.Row(c).AlignItems(ui.Stretch).Dividers(theme.BorderWidth, core.Tokens(c).Border).Children(func() { for depth := 0; depth <= len(st.active); depth++ { if !cascaderColumn(c, st, path, opts, depth) { break @@ -111,7 +112,7 @@ func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult if changed { slog.Debug("mujica: cascader changed", "label", opts.Label, "path", *path) } - return ChoiceResult{tr, changed} + return core.NewChoiceResult(tr, changed) } // cascaderLabels writes the full path of labels, as "Europe / France / Paris". @@ -130,7 +131,7 @@ func cascaderLabels(opts CascaderOptions, path []string) string { // cascaderColumn builds the column at depth and reports whether a deeper one may follow. func cascaderColumn(c *ui.Context, st *cascaderState, path *[]string, opts CascaderOptions, depth int) bool { - k := Tokens(c) + k := core.Tokens(c) prefix := slices.Clone(st.active[:depth]) lv := opts.Level(prefix) id := strings.Join(prefix, "\x00") @@ -151,23 +152,23 @@ func cascaderColumn(c *ui.Context, st *cascaderState, path *[]string, opts Casca switch { case lv.Status == CascaderIdle || lv.Status == CascaderLoading: ui.Row(c).Gap(8).Padding(8).AlignItems(ui.Center).Role(ui.RoleStatus).Children(func() { - Spinner(c, SpinnerOptions{}) - ui.Text(c, msg(c, "cascader.loading")).TextColor(k.TextMuted) + core.Spinner(c, core.SpinnerOptions{}) + ui.Text(c, core.Msg(c, "cascader.loading")).TextColor(k.TextMuted) }) case lv.Status == CascaderFailed: ui.Column(c).Gap(8).Padding(8).AlignItems(ui.Start).Children(func() { - text := msg(c, "cascader.failed") + text := core.Msg(c, "cascader.failed") if lv.Err != nil { text += ": " + lv.Err.Error() } ui.Text(c, text).TextColor(k.Danger) - if Button(c, msg(c, "cascader.retry"), ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw")}).Clicked() { + if Button(c, core.Msg(c, "cascader.retry"), ButtonOptions{Variant: Secondary, Icon: icons.Must("refresh-cw")}).Clicked() { slog.Debug("mujica: cascader retry", "path", prefix) opts.Load(prefix) } }) case len(lv.Nodes) == 0: - selectNote(c, msg(c, "cascader.empty")) + SelectNote(c, core.Msg(c, "cascader.empty")) default: ui.Scroll(c).MaxHeight(260).Padding(4).Children(func() { entry := slices.IndexFunc(lv.Nodes, func(n CascaderNode) bool { return !n.Disabled }) @@ -183,8 +184,8 @@ func cascaderColumn(c *ui.Context, st *cascaderState, path *[]string, opts Casca } func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts CascaderOptions, prefix []string, n CascaderNode, on, first bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) full := append(slices.Clone(prefix), n.Value) depth := len(prefix) b := ui.ButtonBase(c).Key(n.Value).Disabled(n.Disabled).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2). @@ -226,7 +227,7 @@ func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts Cascader b.TextColor(k.AccentText) } b.Children(func() { - selectCheck(c, chosen) + SelectCheck(c, chosen) ui.Text(c, n.Label).SingleLine().Grow(1).Shrink(1) if !n.Leaf { ui.Box(c).Size(14, 14).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/cascader_test.go b/input/cascader_test.go similarity index 97% rename from cascader_test.go rename to input/cascader_test.go index 7d5dbe2..007770d 100644 --- a/cascader_test.go +++ b/input/cascader_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "errors" @@ -7,6 +7,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // cascaderFake is a caller's store: roots are known, children load on request. @@ -38,7 +40,7 @@ func (f *cascaderFake) finish(key string, lv CascaderLevel) { f.levels[key] = lv func cascaderTester(path *[]string, f *cascaderFake, anyLevel bool, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if Cascader(c, path, CascaderOptions{Level: f.level, Load: f.load, AnyLevel: anyLevel, Label: "Place"}).Changed() { *changes++ @@ -148,7 +150,7 @@ func TestCascaderNeedsLevel(t *testing.T) { }() var path []string ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Cascader(c, &path, CascaderOptions{}) }, 100, 100, 1) } diff --git a/checkbox.go b/input/checkbox.go similarity index 89% rename from checkbox.go rename to input/checkbox.go index 4fde3cc..76c543a 100644 --- a/checkbox.go +++ b/input/checkbox.go @@ -1,12 +1,13 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CheckboxOptions configure a Checkbox. @@ -24,8 +25,8 @@ func Checkbox(c *ui.Context, value *bool, label string, opts CheckboxOptions) *u if value == nil { panic("mujica: Checkbox needs a value") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) // Disabled on a wrapper blocks the base's click before it toggles. var row *ui.Element wrap := ui.Row(c).Shrink(0).Disabled(opts.Disabled) @@ -40,7 +41,7 @@ func Checkbox(c *ui.Context, value *bool, label string, opts CheckboxOptions) *u *value = true } if opts.Indeterminate { - row.Description(msg(c, "checkbox.mixed")) + row.Description(core.Msg(c, "checkbox.mixed")) } on, mixed := *value, opts.Indeterminate hover, pressed := row.Hovered(), row.Pressed() @@ -48,7 +49,7 @@ func Checkbox(c *ui.Context, value *bool, label string, opts CheckboxOptions) *u bg, border := checkboxColors(k, on || mixed, hover, pressed) box := ui.Box(c).Size(checkboxSize, checkboxSize).Shrink(0).Radius(theme.ControlRadius). Background(bg).Border(theme.BorderWidth, border) - box.Transition(colorFade) + box.Transition(core.ColorFade) box.DrawOver(func(p *ui.Painter, r ui.Rect) { switch { case mixed: @@ -102,11 +103,11 @@ type CheckboxGroupOptions struct { } // CheckboxGroup shows a select-all box over items: checked, mixed or clear. -func CheckboxGroup(c *ui.Context, label string, items []CheckboxItem, opts CheckboxGroupOptions) ChoiceResult { +func CheckboxGroup(c *ui.Context, label string, items []CheckboxItem, opts CheckboxGroupOptions) core.ChoiceResult { if len(items) == 0 { panic("mujica: CheckboxGroup needs items") } - d := Density(c) + d := core.Density(c) on, enabled, enabledOn := 0, 0, 0 for _, it := range items { if it.Value == nil { @@ -122,7 +123,7 @@ func CheckboxGroup(c *ui.Context, label string, items []CheckboxItem, opts Check } } } - var r ChoiceResult + var r core.ChoiceResult r.Element = ui.Column(c).Shrink(0).Disabled(opts.Disabled || enabled == 0) r.Element.Children(func() { all := on == len(items) @@ -135,13 +136,13 @@ func CheckboxGroup(c *ui.Context, label string, items []CheckboxItem, opts Check *it.Value = target } } - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: checkbox_group select_all", "label", label, "checked", target) } ui.Column(c).Padding(0, 0, 0, checkboxSize+d.Unit()*2).Role(ui.RoleGroup).Label(label).Children(func() { for _, it := range items { if Checkbox(c, it.Value, it.Label, CheckboxOptions{Disabled: it.Disabled}).Changed() { - r.changed = true + r = core.NewChoiceResult(r.Element, true) } } }) diff --git a/checkbox_test.go b/input/checkbox_test.go similarity index 94% rename from checkbox_test.go rename to input/checkbox_test.go index d8befba..bc8c095 100644 --- a/checkbox_test.go +++ b/input/checkbox_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCheckboxLabelClickAndSpace(t *testing.T) { on, changes := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Checkbox(c, &on, "Remember me", CheckboxOptions{}).Changed() { changes++ } @@ -29,7 +31,7 @@ func TestCheckboxIndeterminateChecks(t *testing.T) { for _, start := range []bool{false, true} { on := start tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Checkbox(c, &on, "Some", CheckboxOptions{Indeterminate: true}) }, 300, 200) _ = tt.Click("Some") @@ -42,7 +44,7 @@ func TestCheckboxIndeterminateChecks(t *testing.T) { func TestCheckboxDisabled(t *testing.T) { on := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Checkbox(c, &on, "Off", CheckboxOptions{Disabled: true}) }, 300, 200) _ = tt.Click("Off") @@ -58,7 +60,7 @@ func TestCheckboxGroupSelectAll(t *testing.T) { changes := 0 items := []CheckboxItem{{Label: "Mail", Value: &mail}, {Label: "Calendar", Value: &cal}, {Label: "Admin", Value: &locked, Disabled: true}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if CheckboxGroup(c, "Notifications", items, CheckboxGroupOptions{}).Changed() { changes++ } diff --git a/choice_blocked_test.go b/input/choice_blocked_test.go similarity index 97% rename from choice_blocked_test.go rename to input/choice_blocked_test.go index c2a4ca8..4e78bb3 100644 --- a/choice_blocked_test.go +++ b/input/choice_blocked_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // choiceBetween builds control between two buttons, for Tab order checks. func choiceBetween(control func(c *ui.Context)) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Gap(10).Width(300).Children(func() { Button(c, "Before", ButtonOptions{}) control(c) diff --git a/choice_chips.go b/input/choice_chips.go similarity index 85% rename from choice_chips.go rename to input/choice_chips.go index ac94e76..fe0657c 100644 --- a/choice_chips.go +++ b/input/choice_chips.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChoiceChipsOptions configure ChoiceChips. @@ -21,15 +22,15 @@ type ChoiceChipsOptions struct { } // ChoiceChips are wrapping filter chips toggling values in *value; arrows move between them. -func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], opts ChoiceChipsOptions) ChoiceResult { +func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], opts ChoiceChipsOptions) core.ChoiceResult { if value == nil { panic("mujica: ChoiceChips needs a value") } if !opts.Multiple && len(*value) > 1 { panic("mujica: single ChoiceChips holds more than one value") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) g := ui.Row(c).Wrap().Gap(d.Unit() * 2).Disabled(opts.Disabled).Role(ui.RoleGroup).FocusGroup(ui.Horizontal) if opts.Label != "" { g.Label(opts.Label) @@ -50,7 +51,7 @@ func ChoiceChips[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], if changed { slog.Debug("mujica: choice_chips changed", "label", opts.Label, "value", *value) } - return ChoiceResult{g, changed} + return core.NewChoiceResult(g, changed) } func choiceChipLook[T comparable](c *ui.Context, k theme.Tokens, d theme.Density, b *ui.Element, on bool, it ToggleItem[T]) { @@ -66,7 +67,7 @@ func choiceChipLook[T comparable](c *ui.Context, k theme.Tokens, d theme.Density case b.Hovered(): bg = k.SurfaceHover } - b.Background(bg).TextColor(fg).Border(theme.BorderWidth, line).Transition(colorFade) + b.Background(bg).TextColor(fg).Border(theme.BorderWidth, line).Transition(core.ColorFade) if it.Name != "" { b.Label(it.Name) } diff --git a/choice_chips_test.go b/input/choice_chips_test.go similarity index 90% rename from choice_chips_test.go rename to input/choice_chips_test.go index 531a86a..d3bdd6e 100644 --- a/choice_chips_test.go +++ b/input/choice_chips_test.go @@ -1,17 +1,19 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var chipItems = []ToggleItem[string]{{Value: "open", Label: "Open"}, {Value: "draft", Label: "Draft"}, {Value: "closed", Label: "Closed", Disabled: true}, {Value: "merged", Label: "Merged"}} func chipTester(v *[]string, opts *ChoiceChipsOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ChoiceChips(c, v, chipItems, *opts).Changed() { *n++ } @@ -72,7 +74,7 @@ func TestChoiceChipsRejectsTwoInSingle(t *testing.T) { } }() v := []string{"open", "draft"} - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); ChoiceChips(c, &v, chipItems, ChoiceChipsOptions{}) }, 300, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); ChoiceChips(c, &v, chipItems, ChoiceChipsOptions{}) }, 300, 100) } func TestChoiceChipsSnapshot(t *testing.T) { diff --git a/choice_frame_test.go b/input/choice_frame_test.go similarity index 94% rename from choice_frame_test.go rename to input/choice_frame_test.go index c3c5f07..665292c 100644 --- a/choice_frame_test.go +++ b/input/choice_frame_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // Keys delivered in the frame a control or option turns disabled must change nothing. @@ -46,7 +48,7 @@ func TestCascaderRightAsRowDisables(t *testing.T) { var path []string f := newCascaderFake() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Cascader(c, &path, CascaderOptions{Level: f.level, Load: f.load, AnyLevel: true, Label: "Place"}) }, 700, 500) tt.Key(0, ui.KeyTab) @@ -68,7 +70,7 @@ func TestSelectEnterOnOptionAsItDisables(t *testing.T) { v := "apple" options := slices.Clone(selectTestOptions) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Select(c, &v, options, SelectOptions{Label: "Fruit"}) }, 400, 400) tt.Key(0, ui.KeyTab) @@ -87,7 +89,7 @@ func TestEntryFocusSkipsDisabledFirstRow(t *testing.T) { "": {Nodes: []CascaderNode{{Value: "aq", Label: "Antarctica", Disabled: true}, {Value: "eu", Label: "Europe", Leaf: true}}}, }} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Cascader(c, &path, CascaderOptions{Level: f.level, Load: f.load, Label: "Place"}) }, 700, 500) tt.Key(0, ui.KeyTab) @@ -98,7 +100,7 @@ func TestEntryFocusSkipsDisabledFirstRow(t *testing.T) { nodes := []TreeSelectNode{{ID: "a", Label: "Archived", Disabled: true}, {ID: "b", Label: "Brass"}, {ID: "c", Label: "Copper"}} var sel []string tt = ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TreeSelect(c, &sel, nodes, TreeSelectOptions{Label: "Metal"}) }, 500, 500) tt.Key(0, ui.KeyTab) diff --git a/choice_review_test.go b/input/choice_review_test.go similarity index 96% rename from choice_review_test.go rename to input/choice_review_test.go index ddf5f24..8cee1ba 100644 --- a/choice_review_test.go +++ b/input/choice_review_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "image/color" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // A popup left open must stop taking choices once its control is disabled. @@ -35,7 +36,7 @@ func TestChoicePopupsCloseWhenDisabled(t *testing.T) { for _, tc := range cases { disabled = false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { tc.build(c) }) }, 700, 500) if err := tt.Click(tc.open); err != nil { @@ -127,7 +128,7 @@ func TestChoiceHitAreas(t *testing.T) { } font := "" tt = ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Combobox(c, &font, comboboxTestFonts, ComboboxOptions{}) }, 400, 300) if r, _ := tt.Find("Show options"); r.W < theme.MinHitSize || r.H < theme.MinHitSize { @@ -140,7 +141,7 @@ func TestChoiceRowsFollowDensity(t *testing.T) { v, on := "apple", false var sw *ui.Element tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: d}) + core.Use(c, core.Settings{Density: d}) ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { sw = Switch(c, &on, "Wi-Fi", SwitchOptions{}) Select(c, &v, selectTestOptions, SelectOptions{Label: "Fruit"}) diff --git a/color_palette.go b/input/color_palette.go similarity index 85% rename from color_palette.go rename to input/color_palette.go index 89b0df0..0fa0e0d 100644 --- a/color_palette.go +++ b/input/color_palette.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ColorSwatch is one preset of a ColorPalette; Name is read out. @@ -28,7 +29,7 @@ type ColorPaletteOptions struct { } // ColorPalette picks *value from preset swatches in a grid; arrows move in two dimensions. -func ColorPalette(c *ui.Context, value *ui.Color, swatches []ColorSwatch, opts ColorPaletteOptions) ChoiceResult { +func ColorPalette(c *ui.Context, value *ui.Color, swatches []ColorSwatch, opts ColorPaletteOptions) core.ChoiceResult { if value == nil || len(swatches) == 0 { panic("mujica: ColorPalette needs a value and swatches") } @@ -44,8 +45,8 @@ func ColorPalette(c *ui.Context, value *ui.Color, swatches []ColorSwatch, opts C panic("mujica: ColorPalette swatch needs a Name") } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() g := ui.Column(c).Gap(u * 2).Disabled(opts.Disabled).Role(ui.RoleRadioGroup).Label(opts.Label) disabled := g.IsDisabled() changed := false @@ -72,16 +73,16 @@ func ColorPalette(c *ui.Context, value *ui.Color, swatches []ColorSwatch, opts C } if opts.Custom { ui.Row(c).Children(func() { - if ColorPicker(c, value, ColorPickerOptions{Label: msg(c, "color_palette.custom"), Disabled: disabled}).Changed() { + if ColorPicker(c, value, ColorPickerOptions{Label: core.Msg(c, "color_palette.custom"), Disabled: disabled}).Changed() { changed = true } }) } }) if changed { - slog.Debug("mujica: color_palette changed", "color", colorPickerHex(*value)) + slog.Debug("mujica: color_palette changed", "color", ColorPickerHex(*value)) } - return ChoiceResult{g, changed} + return core.NewChoiceResult(g, changed) } // colorPaletteSwatch is one square swatch; the chosen one has an inset frame and a check. diff --git a/color_palette_test.go b/input/color_palette_test.go similarity index 92% rename from color_palette_test.go rename to input/color_palette_test.go index 93eb283..8fdac0b 100644 --- a/color_palette_test.go +++ b/input/color_palette_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func paletteSwatches() []ColorSwatch { @@ -20,7 +22,7 @@ func TestColorPaletteChooseAndKeyboard(t *testing.T) { n := 0 sw := paletteSwatches() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ColorPalette(c, &v, sw, ColorPaletteOptions{Columns: 4, Custom: true, Disabled: dis, Label: "Tag color"}).Changed() { n++ } @@ -63,7 +65,7 @@ func TestColorPaletteRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); f(c) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); f(c) }, 300, 200) }() } } diff --git a/color_picker.go b/input/color_picker.go similarity index 88% rename from color_picker.go rename to input/color_picker.go index 5faabf7..6124b16 100644 --- a/color_picker.go +++ b/input/color_picker.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -9,9 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ColorPickerOptions configure a ColorPicker. @@ -53,11 +54,11 @@ type colorPickerState struct { // ColorPicker is a swatch button opening a panel that picks *color. func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorPickerParts { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "color_picker.label") + label = core.Msg(c, "color_picker.label") } presets := opts.Presets if presets == nil { @@ -82,7 +83,7 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP st.hsv = x next := x.color() if next != *color { - slog.Debug("mujica: color picker changed", "color", colorPickerHex(next)) + slog.Debug("mujica: color picker changed", "color", ColorPickerHex(next)) *color = next parts.changed = true } @@ -101,7 +102,7 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP border = k.ControlBorder.Mix(k.Text, 0.25) trigger.Background(k.SurfaceHover) } - trigger.Border(theme.BorderWidth, border).Description(colorPickerHex(*color)) + trigger.Border(theme.BorderWidth, border).Description(ColorPickerHex(*color)) now := *color trigger.Children(func() { ui.Box(c).Size(d.Unit()*6, d.Unit()*6).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Border).Clip().Shrink(0). @@ -109,17 +110,17 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP colorPickerCheckers(p, r, 4) p.Fill(r, now, 0) }) - ui.Text(c, strings.ToUpper(colorPickerHex(now))).Font(theme.MonoFont).SingleLine() + ui.Text(c, strings.ToUpper(ColorPickerHex(now))).Font(theme.MonoFont).SingleLine() ui.Icon(c, icons.Must("chevron-down")).Size(16, 16).TextColor(k.TextMuted) }) opened := st.open && !st.wasOpen ui.PopoverBase(c, trigger, &st.open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Margin(d.Unit(), 0, 0, 0).Padding(d.Unit() * 3).Gap(d.Unit() * 3).Width(260) colorPickerSquare(c, st, set, opened) x := st.hsv h := x.h - if colorPickerSlider(c, &h, 360, msg(c, "color_picker.hue"), func(p *ui.Painter, r ui.Rect) { + if colorPickerSlider(c, &h, 360, core.Msg(c, "color_picker.hue"), func(p *ui.Painter, r ui.Rect) { hues := []ui.Color{ui.RGB(255, 0, 0), ui.RGB(255, 255, 0), ui.RGB(0, 255, 0), ui.RGB(0, 255, 255), ui.RGB(0, 0, 255), ui.RGB(255, 0, 255), ui.RGB(255, 0, 0)} w := r.W / 6 for i := range 6 { @@ -130,7 +131,7 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP } a := x.a opaque := colorPickerHSVA{x.h, x.s, x.v, 1}.color() - if colorPickerSlider(c, &a, 1, msg(c, "color_picker.alpha"), func(p *ui.Painter, r ui.Rect) { + if colorPickerSlider(c, &a, 1, core.Msg(c, "color_picker.alpha"), func(p *ui.Painter, r ui.Rect) { colorPickerCheckers(p, r, 4) p.FillGradient(r, ui.LinearGradient{From: opaque.Alpha(0), To: opaque, Angle: 90}, 0) }).Changed() { @@ -139,7 +140,7 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP colorPickerFields(c, st, color, set) ui.Grid(c).ColumnTracks(ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1), ui.Fr(1)).Gap(d.Unit() * 1.5).Children(func() { for _, sw := range presets { - hex := strings.ToUpper(colorPickerHex(sw)) + hex := strings.ToUpper(ColorPickerHex(sw)) b := ui.ButtonBase(c).Height(theme.MinHitSize).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Border).Label(hex).Clip() internal.Ring(b, k.Focus, theme.ControlRadius) chosen := *color == sw @@ -169,10 +170,10 @@ func ColorPicker(c *ui.Context, color *ui.Color, opts ColorPickerOptions) ColorP // colorPickerSquare picks saturation across and brightness up. func colorPickerSquare(c *ui.Context, st *colorPickerState, set func(colorPickerHSVA, bool), opened bool) { - k := Tokens(c) + k := core.Tokens(c) x := st.hsv sq := ui.Box(c).Height(150).Radius(theme.ControlRadius).Focusable().Role(ui.RoleSlider).Cursor(ui.CursorCrosshair). - Label(msg(c, "color_picker.square")) + Label(core.Msg(c, "color_picker.square")) sq.FocusRing(false) if opened { sq.Focus() @@ -194,7 +195,7 @@ func colorPickerSquare(c *ui.Context, st *colorPickerState, set func(colorPicker set(colorPickerHSVA{x.h, x.s, x.v - 0.01, x.a}, true) } x = st.hsv - sq.Description(fmt.Sprintf(msg(c, "color_picker.sv"), x.s*100, x.v*100)) + sq.Description(fmt.Sprintf(core.Msg(c, "color_picker.sv"), x.s*100, x.v*100)) hue := colorPickerHSVA{x.h, 1, 1, 1}.color() sq.Draw(func(p *ui.Painter, r ui.Rect) { p.FillGradient(r, ui.LinearGradient{From: ui.RGB(255, 255, 255), To: hue, Angle: 90}, theme.ControlRadius) @@ -211,7 +212,7 @@ func colorPickerSquare(c *ui.Context, st *colorPickerState, set func(colorPicker // colorPickerSlider is a channel slider from 0 to hi over the track track paints. func colorPickerSlider(c *ui.Context, v *float64, hi float64, name string, track func(p *ui.Painter, r ui.Rect)) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) const knob, look = 10, 18 s := ui.SliderBase(c, v, 0, hi).Height(theme.MinHitSize).PaddingX(knob / 2).FocusRing(false).Label(name) frac := float32(*v / hi) @@ -234,7 +235,7 @@ func colorPickerSlider(c *ui.Context, v *float64, hi float64, name string, track // sync shows col in every field. func (st *colorPickerState) sync(col ui.Color) { - st.hex = strings.TrimPrefix(strings.ToUpper(colorPickerHex(col)), "#") + st.hex = strings.TrimPrefix(strings.ToUpper(ColorPickerHex(col)), "#") st.r, st.g, st.b = strconv.Itoa(int(col.R)), strconv.Itoa(int(col.G)), strconv.Itoa(int(col.B)) st.alpha = strconv.Itoa(int(math.Round(float64(col.A) / 255 * 100))) st.bad = "" @@ -242,8 +243,8 @@ func (st *colorPickerState) sync(col ui.Color) { // colorPickerFields edits the color as HEX, RGB and opacity text. func colorPickerFields(c *ui.Context, st *colorPickerState, color *ui.Color, set func(colorPickerHSVA, bool)) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) apply := func(field string, text *string, col ui.Color, ok bool) { if !ok { st.bad = field @@ -269,10 +270,10 @@ func colorPickerFields(c *ui.Context, st *colorPickerState, color *ui.Color, set for _, f := range []struct { name, label string text *string - }{{"R", msg(c, "color_picker.red"), &st.r}, {"G", msg(c, "color_picker.green"), &st.g}, - {"B", msg(c, "color_picker.blue"), &st.b}, {"A %", msg(c, "color_picker.percent"), &st.alpha}} { + }{{"R", core.Msg(c, "color_picker.red"), &st.r}, {"G", core.Msg(c, "color_picker.green"), &st.g}, + {"B", core.Msg(c, "color_picker.blue"), &st.b}, {"A %", core.Msg(c, "color_picker.percent"), &st.alpha}} { ui.Column(c).Gap(d.Unit()).Children(func() { - ui.Text(c, f.name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, f.name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) in := InputGroup(c, f.text, InputGroupOptions{Label: f.label, Invalid: st.bad == f.name}) editing = editing || in.Input.Focused() if !in.Changed() { @@ -299,13 +300,13 @@ func colorPickerFields(c *ui.Context, st *colorPickerState, color *ui.Color, set st.sync(*color) } if st.bad != "" { - ui.Text(c, msg(c, "color_picker.invalid")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) + ui.Text(c, core.Msg(c, "color_picker.invalid")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) } }) } -// colorPickerHex returns col as #rrggbb, or #rrggbbaa when translucent. -func colorPickerHex(col ui.Color) string { +// ColorPickerHex returns col as #rrggbb, or #rrggbbaa when translucent. +func ColorPickerHex(col ui.Color) string { if col.A == 255 { return fmt.Sprintf("#%02x%02x%02x", col.R, col.G, col.B) } diff --git a/color_picker_test.go b/input/color_picker_test.go similarity index 95% rename from color_picker_test.go rename to input/color_picker_test.go index 2a85958..1f4538b 100644 --- a/color_picker_test.go +++ b/input/color_picker_test.go @@ -1,16 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func colorPickerTester(col *ui.Color, opts ColorPickerOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { if ColorPicker(c, col, opts).Changed() { *changes++ @@ -132,7 +133,7 @@ func TestColorPickerDisabledWhileOpen(t *testing.T) { col, n := ui.Hex("#808080"), 0 opts := ColorPickerOptions{Label: "Shade"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ColorPicker(c, &col, opts).Changed() { n++ } @@ -163,7 +164,7 @@ func TestColorPickerSwatchHitArea(t *testing.T) { func TestColorPickerSliderHitAreaCompact(t *testing.T) { col := ui.Hex("#7D2034") tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) ColorPicker(c, &col, ColorPickerOptions{Label: "Accent"}) }, 400, 560) _ = tt.Click("Accent") diff --git a/combobox.go b/input/combobox.go similarity index 88% rename from combobox.go rename to input/combobox.go index 01a9d8c..97325ff 100644 --- a/combobox.go +++ b/input/combobox.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ComboboxOptions configure a Combobox. @@ -27,7 +28,7 @@ type ComboboxOptions struct { // ComboboxResult is a Combobox's field, its value change and its query. type ComboboxResult struct { - ChoiceResult + core.ChoiceResult query string queryChanged bool } @@ -43,8 +44,8 @@ type comboboxKey struct{ name string } // Combobox chooses *value by typing to filter options and picking one, or typed text with AllowCustom. func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptions) ComboboxResult { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) old := *value f := ui.Row(c).MinHeight(d.ControlHeight()).MinWidth(200).AlignItems(ui.Center).Padding(0, d.Unit()*2, 0, 0). Gap(d.Unit() * 2).Radius(theme.ControlRadius).Background(k.Surface).Cursor(ui.CursorText).Disabled(opts.Disabled) @@ -77,10 +78,10 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio p.Input.Label(opts.Label) } if opts.Loading { - Spinner(c, SpinnerOptions{Size: 14}) + core.Spinner(c, core.SpinnerOptions{Size: 14}) } arrow := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Cursor(ui.CursorPointer). - Role(ui.RoleButton).Label(msg(c, "combobox.toggle")) + Role(ui.RoleButton).Label(core.Msg(c, "combobox.toggle")) if arrow.Clicked() { p.SetOpen(!p.Open()) p.Input.Focus() @@ -113,11 +114,11 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio *lastHi = -1 } p.Popup(func(panel *ui.Element) { - selectPanel(c, panel) + SelectPanel(c, panel) ui.Scroll(c).MaxHeight(240).Children(func() { if custom != "" { it := p.Item(custom) - selectRow(c, it, it.Highlighted(), false, fmt.Sprintf(msg(c, "combobox.use"), custom)) + SelectRow(c, it, it.Highlighted(), false, fmt.Sprintf(core.Msg(c, "combobox.use"), custom)) } for i, o := range shown { it := p.Item(o) @@ -125,7 +126,7 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio *lastHi = i it.ScrollIntoView() } - selectRow(c, it, it.Highlighted(), o == *value, o) + SelectRow(c, it, it.Highlighted(), o == *value, o) } }) }) @@ -160,7 +161,7 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio if changed { slog.Debug("mujica: combobox changed", "label", opts.Label, "value", *value) } - return ComboboxResult{ChoiceResult{f, changed}, *query, queryChanged} + return ComboboxResult{core.NewChoiceResult(f, changed), *query, queryChanged} } // comboboxShown returns the options containing query, those starting with it first. @@ -194,16 +195,16 @@ func comboboxHas(options []string, s string) bool { func comboboxStatus(c *ui.Context, r ui.Rect, loading bool) { ui.Overlay(c, func() { panel := ui.Row(c).Absolute().Left(r.X).Top(r.Y + r.H).MinWidth(r.W).PassThrough().Role(ui.RoleStatus) - selectPanel(c, panel) + SelectPanel(c, panel) panel.Children(func() { if loading { ui.Row(c).Gap(8).Padding(8, 12).AlignItems(ui.Center).Children(func() { - Spinner(c, SpinnerOptions{}) - ui.Text(c, msg(c, "combobox.loading")).TextColor(Tokens(c).TextMuted) + core.Spinner(c, core.SpinnerOptions{}) + ui.Text(c, core.Msg(c, "combobox.loading")).TextColor(core.Tokens(c).TextMuted) }) return } - selectNote(c, msg(c, "combobox.no_results")) + SelectNote(c, core.Msg(c, "combobox.no_results")) }) }) } diff --git a/combobox_test.go b/input/combobox_test.go similarity index 97% rename from combobox_test.go rename to input/combobox_test.go index 4a5072f..9b8b356 100644 --- a/combobox_test.go +++ b/input/combobox_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var comboboxTestFonts = []string{"Garamond", "Georgia", "Baskerville", "Didot", "Bodoni"} @@ -17,7 +19,7 @@ type comboboxRun struct { func comboboxTester(r *comboboxRun, options func(query string) []string, opts ComboboxOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Gap(10).Children(func() { res := Combobox(c, &r.value, options(r.query), opts) if res.Changed() { @@ -117,7 +119,7 @@ func TestComboboxAsyncResults(t *testing.T) { } opts := ComboboxOptions{ExternalFilter: true} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) opts.Loading = loading res := Combobox(c, &r.value, remote(r.query), opts) if res.QueryChanged() { diff --git a/copy_button.go b/input/copy_button.go similarity index 72% rename from copy_button.go rename to input/copy_button.go index 2dd07c9..3272537 100644 --- a/copy_button.go +++ b/input/copy_button.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // copyButtonShown is how long the button says Copied. @@ -21,9 +22,9 @@ type CopyButtonOptions struct { } // CopyButton puts text on the clipboard, then shows and announces Copied for two seconds. -func CopyButton(c *ui.Context, text string, opts CopyButtonOptions) ChoiceResult { - copied := msg(c, "copy_button.copied") - name := msg(c, "copy_button.copy") +func CopyButton(c *ui.Context, text string, opts CopyButtonOptions) core.ChoiceResult { + copied := core.Msg(c, "copy_button.copied") + name := core.Msg(c, "copy_button.copy") label, icon := opts.Label, icons.Must("copy") var b *ui.Element box := ui.Box(c) @@ -44,12 +45,12 @@ func CopyButton(c *ui.Context, text string, opts CopyButtonOptions) ChoiceResult } b = Button(c, label, bo) if opts.Label == "" { - b.Padding(0).Size(Density(c).ControlHeight(), Density(c).ControlHeight()).Tooltip(name) + b.Padding(0).Size(core.Density(c).ControlHeight(), core.Density(c).ControlHeight()).Tooltip(name) } else { // Room for the longer word, so the width holds. w1, _ := c.MeasureText(0, ui.Span{Text: copied}) w2, _ := c.MeasureText(0, ui.Span{Text: opts.Label}) - b.MinWidth(max(w1, w2) + buttonSpinnerSize + Density(c).Unit()*10 + 2*theme.BorderWidth) + b.MinWidth(max(w1, w2) + buttonSpinnerSize + core.Density(c).Unit()*10 + 2*theme.BorderWidth) } }) if b.Clicked() && !b.IsDisabled() { @@ -58,7 +59,7 @@ func CopyButton(c *ui.Context, text string, opts CopyButtonOptions) ChoiceResult c.Announce(copied) c.Invalidate() slog.Debug("mujica: copy_button copied", "bytes", len(text)) - return ChoiceResult{box, true} + return core.NewChoiceResult(box, true) } - return ChoiceResult{box, false} + return core.NewChoiceResult(box, false) } diff --git a/copy_button_test.go b/input/copy_button_test.go similarity index 92% rename from copy_button_test.go rename to input/copy_button_test.go index 4e6d07b..60f8ff2 100644 --- a/copy_button_test.go +++ b/input/copy_button_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "slices" @@ -6,13 +6,15 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCopyButtonCopiesAndAnnounces(t *testing.T) { n := 0 var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { r := CopyButton(c, "sk-live-123", CopyButtonOptions{Label: "Copy key"}) if r.Changed() { @@ -41,7 +43,7 @@ func TestCopyButtonCopiesAndAnnounces(t *testing.T) { func TestCopyButtonKeyboardRevertAndDisabled(t *testing.T) { dis := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) CopyButton(c, "abc", CopyButtonOptions{Disabled: dis}) }, 300, 200) tt.Key(0, ui.KeyTab) diff --git a/currency_input.go b/input/currency_input.go similarity index 93% rename from currency_input.go rename to input/currency_input.go index 9b8f1cf..45240bc 100644 --- a/currency_input.go +++ b/input/currency_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,6 +8,8 @@ import ( "strings" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // CurrencyInputOptions configure a CurrencyInput. @@ -96,7 +98,7 @@ func CurrencyInput(c *ui.Context, value *int64, opts CurrencyInputOptions) Input panic("mujica: CurrencyInput needs a value") } dec := currencyInputDigits(opts) - k := Tokens(c) + k := core.Tokens(c) var r InputResult look := &textInputLook{disabled: opts.Disabled, err: opts.Error} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { @@ -120,15 +122,15 @@ func CurrencyInput(c *ui.Context, value *int64, opts CurrencyInputOptions) Input *text = CurrencyFormat(*value, dec, false) } *was = focused - in := textInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 16).Label(opts.Label) + in := TextInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 16).Label(opts.Label) if in.Changed() && !look.disabled { v, why := currencyInputParse(*text, dec) *bad = "" switch { case why == "currency_input.precision": - *bad = fmt.Sprintf(msg(c, why), dec) + *bad = fmt.Sprintf(core.Msg(c, why), dec) case why != "": - *bad = msg(c, why) + *bad = core.Msg(c, why) case v != *value: *value, r.changed = v, true } diff --git a/currency_input_test.go b/input/currency_input_test.go similarity index 94% rename from currency_input_test.go rename to input/currency_input_test.go index cef2209..56e828a 100644 --- a/currency_input_test.go +++ b/input/currency_input_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestCurrencyFormatAndParse(t *testing.T) { @@ -44,7 +46,7 @@ func TestCurrencyInputTypingGroupingAndErrors(t *testing.T) { dis := false n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if CurrencyInput(c, &v, CurrencyInputOptions{Symbol: "$", Label: "Price", Disabled: dis}).Changed() { n++ } @@ -84,7 +86,7 @@ func TestCurrencyInputTypingGroupingAndErrors(t *testing.T) { func TestCurrencyInputNoFractionAndRejects(t *testing.T) { v := int64(1500) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CurrencyInput(c, &v, CurrencyInputOptions{Symbol: "¥", NoFraction: true, Label: "Amount"}) }, 300, 100) tt.Key(0, ui.KeyTab) @@ -103,7 +105,7 @@ func TestCurrencyInputNoFractionAndRejects(t *testing.T) { t.Errorf("%+v: no panic", o) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); CurrencyInput(c, &v, o) }, 200, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); CurrencyInput(c, &v, o) }, 200, 100) }() } } diff --git a/input/doc.go b/input/doc.go new file mode 100644 index 0000000..cc89113 --- /dev/null +++ b/input/doc.go @@ -0,0 +1,2 @@ +// Package input holds MujicaUI buttons, text fields, choices, form controls and general essentials. +package input diff --git a/dock_panel.go b/input/dock_panel.go similarity index 92% rename from dock_panel.go rename to input/dock_panel.go index 8d34e49..26f6b48 100644 --- a/dock_panel.go +++ b/input/dock_panel.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DockZone is an area of a DockPanel. @@ -41,7 +43,7 @@ type DockPanelOptions struct { type dockPanelDrag struct{ id string } // DockPanel arranges panels in left, center, right and bottom zones; tabs drag to an edge or zone to dock there. -func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPanelOptions) ChoiceResult { +func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPanelOptions) core.ChoiceResult { if layout == nil || len(items) == 0 { panic("mujica: DockPanel needs a layout and items") } @@ -76,8 +78,8 @@ func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPan layout.Active[z] = ids[0] } } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() root := ui.Box(c).Disabled(opts.Disabled).MinHeight(0) disabled := root.IsDisabled() // follow is the tab that moved; it keeps the focus in its new zone. @@ -113,7 +115,7 @@ func DockPanel(c *ui.Context, layout *DockLayout, items []DockItem, opts DockPan } *was = dragging }) - return ChoiceResult{root, changed} + return core.NewChoiceResult(root, changed) } // dockPanelZone builds zone z's tabs and shown panel; it reports a tab of it being dragged. @@ -123,7 +125,7 @@ func dockPanelZone(c *ui.Context, k theme.Tokens, u float32, layout *DockLayout, return false } box := ui.Column(c).Key(z).MinWidth(0).MinHeight(0).Background(k.Surface).Clip().Role(ui.RoleGroup). - Label(msg(c, fmt.Sprintf("dock_panel.zone%d", z))) + Label(core.Msg(c, fmt.Sprintf("dock_panel.zone%d", z))) switch z { case DockLeft, DockRight: box.Width(220).Shrink(0).Border(theme.BorderWidth, k.Border) @@ -145,8 +147,8 @@ func dockPanelZone(c *ui.Context, k theme.Tokens, u float32, layout *DockLayout, base.List.Gap(u).Padding(theme.FocusGap+theme.FocusWidth, u*2, 0, u*2).Children(func() { for i, id := range ids { tab := base.Tab(i).Drag(dockPanelDrag{id}) - tabsStyle(c, tab, Tab{Label: byID[id].Title}, i == at, false) - tab.Description(msg(c, "dock_panel.hint")) + navigation.TabsStyle(c, tab, navigation.Tab{Label: byID[id].Title}, i == at, false) + tab.Description(core.Msg(c, "dock_panel.hint")) if *follow == id { tab.Focus() *follow = "" diff --git a/dock_panel_test.go b/input/dock_panel_test.go similarity index 92% rename from dock_panel_test.go rename to input/dock_panel_test.go index 20b5aa0..5d954a2 100644 --- a/dock_panel_test.go +++ b/input/dock_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func dockItems(c *ui.Context) []DockItem { @@ -17,7 +19,7 @@ func dockItems(c *ui.Context) []DockItem { func dockTester(l *DockLayout, dis *bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := DockPanel(c, l, dockItems(c), DockPanelOptions{Disabled: *dis}) r.Element.Size(800, 500) if r.Changed() { @@ -87,7 +89,7 @@ func TestDockPanelRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); DockPanel(c, &l, dockItems(c), DockPanelOptions{}) }, 300, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); DockPanel(c, &l, dockItems(c), DockPanelOptions{}) }, 300, 200) }() } } diff --git a/input/example_choice_test.go b/input/example_choice_test.go new file mode 100644 index 0000000..d9e6554 --- /dev/null +++ b/input/example_choice_test.go @@ -0,0 +1,134 @@ +package input_test + +import ( + "log" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleSwitch() { + wifi := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Switch(c, &wifi, "Wi-Fi", input.SwitchOptions{}).Changed() { + log.Println("Wi-Fi:", wifi) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToggle() { + bold := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Toggle(c, &bold, "", input.ToggleOptions{Icon: icons.Must("bold"), Label: "Bold"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToggleGroup() { + align := []string{"left"} + items := []input.ToggleItem[string]{ + {Value: "left", Label: "Left"}, + {Value: "center", Label: "Center"}, + {Value: "right", Label: "Right"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.ToggleGroup(c, &align, items, input.ToggleGroupOptions{Label: "Align"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSlider() { + volume := 40.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Slider(c, &volume, input.SliderOptions{Step: 5, ShowValue: true, Label: "Volume"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRangeSlider() { + low, high := 20.0, 80.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.RangeSlider(c, &low, &high, input.RangeSliderOptions{Step: 10, Label: "Price"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSelect() { + plan := "" + options := []input.SelectOption[string]{ + {Value: "free", Label: "Free"}, + {Value: "pro", Label: "Pro"}, + {Value: "team", Label: "Team", Disabled: true}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Select(c, &plan, options, input.SelectOptions{Clearable: true, Label: "Plan"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMultiSelect() { + tags := []int{1} + options := []input.SelectOption[int]{ + {Value: 1, Label: "Rose"}, + {Value: 2, Label: "Lily"}, + {Value: 3, Label: "Iris"}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.MultiSelect(c, &tags, options, input.MultiSelectOptions{Max: 2, Label: "Flowers"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCombobox() { + font := "" + fonts := []string{"Garamond", "Georgia", "Didot"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.Combobox(c, &font, fonts, input.ComboboxOptions{AllowCustom: true, Label: "Font"}) + if r.QueryChanged() { + log.Println("search:", r.Query()) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCascader() { + var path []string + level := func(p []string) input.CascaderLevel { + if len(p) == 0 { + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "fr", Label: "France"}}} + } + return input.CascaderLevel{Nodes: []input.CascaderNode{{Value: "paris", Label: "Paris", Leaf: true}}} + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Cascader(c, &path, input.CascaderOptions{Level: level, Label: "City"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTreeSelect() { + var roles []string + nodes := []input.TreeSelectNode{ + {ID: "court", Label: "Court", Children: []input.TreeSelectNode{ + {ID: "king", Label: "King"}, + {ID: "queen", Label: "Queen"}, + }}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.TreeSelect(c, &roles, nodes, input.TreeSelectOptions{Multiple: true, Label: "Roles"}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/input/example_essentials_test.go b/input/example_essentials_test.go new file mode 100644 index 0000000..2096003 --- /dev/null +++ b/input/example_essentials_test.go @@ -0,0 +1,209 @@ +package input_test + +import ( + "strings" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleCopyButton() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.CopyButton(c, "https://example.com/invite/42", input.CopyButtonOptions{Label: "Copy link"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHoldToConfirm() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.HoldToConfirm(c, "Delete project", input.HoldToConfirmOptions{}).Changed() { + // delete the project + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBulkActionBar() { + selected := 3 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.BulkActionBar(c, selected, input.BulkActionBarOptions{}, func() { + input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) + }) + if r.Changed() { + selected = 0 + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleScrubInput() { + opacity := 0.6 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.ScrubInput(c, "Opacity", &opacity, input.ScrubInputOptions{Max: 1, Step: 0.1}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCurrencyInput() { + cents := int64(123450) + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.CurrencyInput(c, ¢s, input.CurrencyInputOptions{Symbol: "$", Label: "Price"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMaskedInput() { + phone := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.MaskedInput(c, &phone, input.MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMentionInput() { + text, mentions := "", []input.Mention(nil) + people := []input.MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Bob"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.MentionInput(c, &text, &mentions, input.MentionInputOptions{ + Label: "Comment", + Candidates: func(q string) []input.MentionCandidate { + var out []input.MentionCandidate + for _, p := range people { + if strings.HasPrefix(strings.ToLower(p.Name), strings.ToLower(q)) { + out = append(out, p) + } + } + return out + }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChoiceChips() { + var status []string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.ChoiceChips(c, &status, []input.ToggleItem[string]{ + {Value: "open", Label: "Open"}, {Value: "closed", Label: "Closed"}, + }, input.ChoiceChipsOptions{Multiple: true, Label: "Status"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleKnob() { + gain := 35.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Knob(c, &gain, input.KnobOptions{Label: "Gain", Ticks: 11}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVerticalSlider() { + level := 40.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.VerticalSlider(c, &level, input.VerticalSliderOptions{Step: 10, Label: "Level"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleColorPalette() { + color := ui.Hex("#7D2034") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.ColorPalette(c, &color, []input.ColorSwatch{ + {Color: ui.Hex("#7D2034"), Name: "Wine"}, + {Color: ui.Hex("#AD9164"), Name: "Old gold"}, + {Color: ui.Hex("#355E80"), Name: "Slate blue"}, + }, input.ColorPaletteOptions{Custom: true, Label: "Tag color"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSignaturePad() { + var strokes [][]input.SignaturePoint + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.SignaturePad(c, &strokes, input.SignaturePadOptions{}).Changed() { + _ = input.SignaturePath(strokes) // SVG path data to store + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBanner() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.Banner(c, &open, "Maintenance tonight at 22:00.", input.BannerOptions{ + Severity: core.SeverityWarning, + Action: "Details", + }) + if r.Action { + // show the details + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMasonry() { + titles := []string{"Sea", "Forest", "Desert", "Glacier"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Masonry(c, len(titles), input.MasonryOptions{ + Key: func(i int) string { return titles[i] }, + }, func(i int) { + ui.Text(c, titles[i]) + }).Size(380, 280) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleResizablePanelGroup() { + var sizes []float32 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.ResizablePanelGroup(c, &sizes, []input.ResizablePanel{ + {Label: "Files", Collapsible: true, Build: func() { ui.Text(c, "Files") }}, + {Label: "Editor", Build: func() { ui.Text(c, "Editor") }}, + }, input.ResizablePanelGroupOptions{}) + if r.Committed { + // save sizes + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDockPanel() { + var layout input.DockLayout + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.DockPanel(c, &layout, []input.DockItem{ + {ID: "files", Title: "Files", Build: func() { ui.Text(c, "Files") }}, + {ID: "terminal", Title: "Terminal", Build: func() { ui.Text(c, "$") }}, + }, input.DockPanelOptions{}) + if r.Changed() { + // save layout + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleQRCode() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.QRCode(c, "https://example.com/mujica", input.QRCodeOptions{Level: input.QRQuartile}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/input/example_form_test.go b/input/example_form_test.go new file mode 100644 index 0000000..9ed0b91 --- /dev/null +++ b/input/example_form_test.go @@ -0,0 +1,125 @@ +package input_test + +import ( + "fmt" + + "github.com/egoist/mygo" + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleForm() { + var state input.FormState + name := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + res := input.Form(c, &state, input.FormOptions{}, func(f *input.FormScope) { + input.FormItem(c, f, "name", &name, input.FormItemOptions[string]{Label: "Name", Required: true}, func() *ui.Element { + return input.InputGroup(c, &name, input.InputGroupOptions{Invalid: state.Error("name") != ""}).Input + }) + }) + if res.Submitted() { + state.EndSubmit("") // after saving name + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFormField() { + email := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.FormField(c, "Email", input.FormFieldOptions{Required: true, Description: "For receipts."}, func() *ui.Element { + return input.InputGroup(c, &email, input.InputGroupOptions{}).Input + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleInputGroup() { + site := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + g := input.InputGroup(c, &site, input.InputGroupOptions{ + Prefix: "https://", Label: "Site", + Buttons: []input.InputGroupButton{{Label: "Open"}}, + }) + if g.Clicked == 0 { + c.OpenURL("https://" + site) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePinInput() { + code := "" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.PinInput(c, &code, input.PinInputOptions{Length: 6}).Completed() { + fmt.Println("verify", code) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTagsInput() { + tags := []string{"gothic"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.TagsInput(c, &tags, input.TagsInputOptions{Max: 5, Placeholder: "Add a tag", Label: "Tags"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRating() { + stars := 3.5 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Rating(c, &stars, input.RatingOptions{Step: 0.5, ShowValue: true}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleColorPicker() { + tint := ui.Hex("#7D2034") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.ColorPicker(c, &tint, input.ColorPickerOptions{Label: "Tint"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFilePicker() { + var paths []string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.FilePicker(c, &paths, input.FilePickerOptions{ + Multiple: true, + Filters: []mygo.FileFilter{{Name: "Scores", Extensions: []string{"pdf", "mid"}}}, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFileDropZone() { + var files []input.DroppedFile + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.FileDropZone(c, &files, input.FileDropZoneOptions{ + Extensions: []string{"png", "jpg"}, MaxFiles: 3, MaxSize: 5 << 20, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTransfer() { + chosen := []string{"uika"} + items := []input.TransferItem{{ID: "uika", Label: "Uika"}, {ID: "mutsumi", Label: "Mutsumi"}, {ID: "nyamu", Label: "Nyamu", Disabled: true}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.Transfer(c, &chosen, items, input.TransferOptions{}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/input/example_input_test.go b/input/example_input_test.go new file mode 100644 index 0000000..77c32b1 --- /dev/null +++ b/input/example_input_test.go @@ -0,0 +1,131 @@ +package input_test + +import ( + "fmt" + "strings" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" +) + +func ExampleButton() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "Save", input.ButtonOptions{Icon: icons.Must("check")}).Clicked() { + fmt.Println("saved") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleIconButton() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.IconButton(c, icons.Must("settings"), "Settings", input.IconButtonOptions{Variant: input.Ghost}).Clicked() { + fmt.Println("open settings") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleButtonGroup() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + items := []input.ButtonGroupItem{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}} + if i := input.ButtonGroup(c, items, input.ButtonGroupOptions{Label: "Range"}).ClickedItem(); i >= 0 { + fmt.Println("chose", items[i].Label) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTextInput() { + var name string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.TextInput(c, &name, input.TextInputOptions{Label: "Name", Placeholder: "Your name", Clearable: true}) + if r.Submitted() { + fmt.Println("hello", name) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTextArea() { + var notes string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := input.TextArea(c, ¬es, input.TextAreaOptions{Label: "Notes", MaxHeight: 160, MaxLength: 200}) + if r.Submitted() { + fmt.Println("send", notes) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleNumberInput() { + copies := 1.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.NumberInput(c, &copies, input.NumberInputOptions{Min: 1, Max: 99, Label: "Copies"}).Changed() { + fmt.Println("copies", copies) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePasswordInput() { + var password string + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.PasswordInput(c, &password, input.PasswordInputOptions{Label: "Password"}).Submitted() { + fmt.Println("signing in") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSearchInput() { + var query string + current := 0 + text := "rose lys rose crown rose" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + matches := 0 + if query != "" { + matches = strings.Count(text, query) + } + r := input.SearchInput(c, &query, input.SearchInputOptions{Find: true, Matches: matches, Current: ¤t}) + if r.Moved() { + fmt.Println("show match", current) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCheckbox() { + var mail, calendar bool + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + input.CheckboxGroup(c, "Notifications", []input.CheckboxItem{ + {Label: "Mail", Value: &mail}, + {Label: "Calendar", Value: &calendar}, + }, input.CheckboxGroupOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRadioGroup() { + size := "m" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + items := []input.RadioItem[string]{{Value: "s", Label: "Small"}, {Value: "m", Label: "Medium"}, {Value: "l", Label: "Large"}} + if input.RadioGroup(c, &size, items, input.RadioGroupOptions{Label: "Size"}).Changed() { + fmt.Println("size", size) + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/file_drop_zone.go b/input/file_drop_zone.go similarity index 85% rename from file_drop_zone.go rename to input/file_drop_zone.go index 828b88e..b6b92d4 100644 --- a/file_drop_zone.go +++ b/input/file_drop_zone.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "errors" @@ -13,9 +13,10 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DroppedFile is a file a FileDropZone accepted. @@ -57,7 +58,7 @@ type fileDropZoneState struct { } // FileDropZone checks dropped or browsed files and lists them; it never reads or uploads them. -func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) ChoiceResult { +func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) core.ChoiceResult { if opts.MaxFiles < 0 || opts.MaxSize < 0 { panic("mujica: FileDropZone limits must not be negative") } @@ -66,14 +67,14 @@ func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) panic(fmt.Sprintf("mujica: FileDropZone extension %q: give it without the dot", ext)) } } - k := Tokens(c) - d := Density(c) - parts := ChoiceResult{} + k := core.Tokens(c) + d := core.Density(c) + parts := core.ChoiceResult{} col := ui.Column(c).Gap(d.Unit() * 2) parts.Element = col st := ui.Local(col, "zone", func() fileDropZoneState { return fileDropZoneState{} }) reject := func(path, key string, args ...any) { - st.rejected = append(st.rejected, fileDropZoneReject{path, fmt.Sprintf(msg(c, key), args...)}) + st.rejected = append(st.rejected, fileDropZoneReject{path, fmt.Sprintf(core.Msg(c, key), args...)}) slog.Debug("mujica: file drop zone rejected", "reason", key) } // take checks types now and sizes off the UI thread. @@ -119,7 +120,7 @@ func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) reject(s.path, "file_drop_zone.count", opts.MaxFiles) default: *files = append(slices.Clone(*files), DroppedFile{s.path, s.size}) - parts.changed = true + parts = core.NewChoiceResult(parts.Element, true) } } if len(st.pending) > 0 { @@ -158,13 +159,13 @@ func FileDropZone(c *ui.Context, files *[]DroppedFile, opts FileDropZoneOptions) }) hint := opts.Hint if hint == "" { - hint = msg(c, "file_drop_zone.hint") + hint = core.Msg(c, "file_drop_zone.hint") } zone.Children(func() { ui.Icon(c, icons.Must("upload")).Size(24, 24).TextColor(k.AccentText) ui.Text(c, hint).TextAlign(ui.Center) - ui.Text(c, fileDropZoneRules(c, opts)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).TextAlign(ui.Center) - b := Button(c, msg(c, "file_drop_zone.browse"), ButtonOptions{Variant: Outline, Loading: st.browse != nil, Disabled: opts.Disabled}) + ui.Text(c, fileDropZoneRules(c, opts)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).TextAlign(ui.Center) + b := Button(c, core.Msg(c, "file_drop_zone.browse"), ButtonOptions{Variant: Outline, Loading: st.browse != nil, Disabled: opts.Disabled}) if b.Clicked() { ch := make(chan filePickerResult, 1) st.browse = ch @@ -204,18 +205,18 @@ func fileDropZoneCheck(c *ui.Context, st *fileDropZoneState) { } // fileDropZoneList shows accepted, checking and rejected files. -func fileDropZoneList(c *ui.Context, st *fileDropZoneState, files *[]DroppedFile, parts *ChoiceResult, disabled bool) { - k := Tokens(c) - d := Density(c) +func fileDropZoneList(c *ui.Context, st *fileDropZoneState, files *[]DroppedFile, parts *core.ChoiceResult, disabled bool) { + k := core.Tokens(c) + d := core.Density(c) row := func(key string, icon string, color ui.Color, name, detail string, remove func()) { ui.Row(c).Key(key).Gap(d.Unit()*2).AlignItems(ui.Center).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2). BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { ui.Icon(c, icons.Must(icon)).Size(16, 16).TextColor(color) ui.Text(c, name).SingleLine().Ellipsis("…").MinWidth(0).Shrink(1) - ui.Text(c, detail).TextColor(color).FontSize(FontSize(c, theme.CaptionSize)).SingleLine().Grow(1) + ui.Text(c, detail).TextColor(color).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine().Grow(1) if remove != nil { x := ui.ButtonBase(c).Size(28, 28).Radius(theme.ControlRadius).TextColor(k.TextMuted). - Label(fmt.Sprintf(msg(c, "file_drop_zone.remove"), name)).Disabled(disabled) + Label(fmt.Sprintf(core.Msg(c, "file_drop_zone.remove"), name)).Disabled(disabled) internal.Ring(x, k.Focus, theme.ControlRadius) if x.Hovered() { x.Background(k.SurfaceHover).TextColor(k.Text) @@ -231,17 +232,17 @@ func fileDropZoneList(c *ui.Context, st *fileDropZoneState, files *[]DroppedFile for i, f := range *files { row("ok"+f.Path, "file", k.TextMuted, filepath.Base(f.Path), fileDropZoneSize(f.Size), func() { *files = slices.Delete(slices.Clone(*files), i, i+1) - parts.changed = true + *parts = core.NewChoiceResult(parts.Element, true) slog.Debug("mujica: file drop zone removed", "count", len(*files)) }) } for _, p := range st.pending { - row("pending"+p, "loader-circle", k.TextMuted, filepath.Base(p), msg(c, "file_drop_zone.checking"), nil) + row("pending"+p, "loader-circle", k.TextMuted, filepath.Base(p), core.Msg(c, "file_drop_zone.checking"), nil) } for i, r := range st.rejected { name := filepath.Base(r.path) if r.path == "" { - name = msg(c, "file_drop_zone.dialog") + name = core.Msg(c, "file_drop_zone.dialog") } row(fmt.Sprint("bad", i, r.path), "circle-alert", k.Danger, name, r.reason, func() { st.rejected = slices.Delete(slices.Clone(st.rejected), i, i+1) @@ -257,13 +258,13 @@ func fileDropZoneRules(c *ui.Context, opts FileDropZoneOptions) string { rules = append(rules, strings.ToUpper(strings.Join(opts.Extensions, ", "))) } if opts.MaxFiles > 0 { - rules = append(rules, fmt.Sprintf(msg(c, "file_drop_zone.max_files"), opts.MaxFiles)) + rules = append(rules, fmt.Sprintf(core.Msg(c, "file_drop_zone.max_files"), opts.MaxFiles)) } if opts.MaxSize > 0 { - rules = append(rules, fmt.Sprintf(msg(c, "file_drop_zone.max_size"), fileDropZoneSize(opts.MaxSize))) + rules = append(rules, fmt.Sprintf(core.Msg(c, "file_drop_zone.max_size"), fileDropZoneSize(opts.MaxSize))) } if len(rules) == 0 { - return msg(c, "file_drop_zone.any") + return core.Msg(c, "file_drop_zone.any") } return strings.Join(rules, " · ") } diff --git a/file_drop_zone_test.go b/input/file_drop_zone_test.go similarity index 97% rename from file_drop_zone_test.go rename to input/file_drop_zone_test.go index ccd4527..32569c7 100644 --- a/file_drop_zone_test.go +++ b/input/file_drop_zone_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "os" @@ -7,6 +7,8 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // fileDropZoneFiles writes files of the given sizes into a temp dir. @@ -23,7 +25,7 @@ func fileDropZoneFiles(t *testing.T, sizes map[string]int) string { func fileDropZoneTester(files *[]DroppedFile, opts FileDropZoneOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if FileDropZone(c, files, opts).Changed() { *changes++ } diff --git a/file_picker.go b/input/file_picker.go similarity index 83% rename from file_picker.go rename to input/file_picker.go index ffd2b91..34214d4 100644 --- a/file_picker.go +++ b/input/file_picker.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -9,8 +9,9 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FilePickerOptions configure a FilePicker. @@ -64,8 +65,8 @@ func FilePicker(c *ui.Context, paths *[]string, opts FilePickerOptions) FilePick } } } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) parts := FilePickerParts{} col := ui.Column(c).Gap(d.Unit() * 2).AlignItems(ui.Start) parts.Element = col @@ -76,7 +77,7 @@ func FilePicker(c *ui.Context, paths *[]string, opts FilePickerOptions) FilePick st.result = nil switch { case r.err != nil: - st.err = fmt.Sprintf(msg(c, "file_picker.failed"), r.err) + st.err = fmt.Sprintf(core.Msg(c, "file_picker.failed"), r.err) slog.Debug("mujica: file picker failed", "err", r.err) case r.paths == nil: parts.canceled = true @@ -91,9 +92,9 @@ func FilePicker(c *ui.Context, paths *[]string, opts FilePickerOptions) FilePick } label := opts.Label if label == "" { - label = msg(c, "file_picker.choose_file") + label = core.Msg(c, "file_picker.choose_file") if opts.Directory { - label = msg(c, "file_picker.choose_folder") + label = core.Msg(c, "file_picker.choose_folder") } } icon := icons.Must("file") @@ -117,16 +118,16 @@ func FilePicker(c *ui.Context, paths *[]string, opts FilePickerOptions) FilePick } switch { case st.err != "": - ui.Text(c, st.err).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) + ui.Text(c, st.err).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) case len(*paths) == 0: - ui.Text(c, msg(c, "file_picker.none")).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.Msg(c, "file_picker.none")).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)) default: ui.Column(c).Gap(d.Unit()).Children(func() { for _, p := range *paths { ui.Row(c).Key(p).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Icon(c, icon).Size(14, 14).TextColor(k.TextMuted) ui.Text(c, filepath.Base(p)).SingleLine() - ui.Text(c, filepath.Dir(p)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).SingleLine().Ellipsis("…").MinWidth(0).Shrink(1) + ui.Text(c, filepath.Dir(p)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine().Ellipsis("…").MinWidth(0).Shrink(1) }) } }) diff --git a/file_picker_test.go b/input/file_picker_test.go similarity index 97% rename from file_picker_test.go rename to input/file_picker_test.go index 459c847..6c4126c 100644 --- a/file_picker_test.go +++ b/input/file_picker_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "errors" @@ -8,6 +8,8 @@ import ( "github.com/egoist/mygo" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type filePickerProbe struct { @@ -33,7 +35,7 @@ func (p *filePickerProbe) stub(t *testing.T) { func (p *filePickerProbe) tester(opts FilePickerOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := FilePicker(c, &p.paths, opts) if r.Changed() { p.changes++ diff --git a/form.go b/input/form.go similarity index 84% rename from form.go rename to input/form.go index c3ea2b1..98f25ac 100644 --- a/form.go +++ b/input/form.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,24 +7,26 @@ import ( "slices" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // FormState is a form's check and submit state; use it on the UI thread only. type FormState struct { - fields map[string]*formFieldState - order []string + Fields map[string]*FormFieldState + Order []string resets []func() seq uint64 tried bool submitting bool - awaiting bool - requested bool + Awaiting bool + Requested bool focus string formError string generation int } -type formFieldState struct { +type FormFieldState struct { snapshot string version uint64 touched bool @@ -41,7 +43,7 @@ type formFieldState struct { // Version returns the version of name's value, which changes whenever the value does. func (s *FormState) Version(name string) uint64 { - if f := s.fields[name]; f != nil { + if f := s.Fields[name]; f != nil { return f.version } return 0 @@ -49,7 +51,7 @@ func (s *FormState) Version(name string) uint64 { // Resolve delivers name's async result for version, "" if valid; stale versions return false. func (s *FormState) Resolve(name string, version uint64, err string) bool { - f := s.fields[name] + f := s.Fields[name] if f == nil || f.version != version { slog.Debug("mujica: form async result dropped", "field", name, "version", version) return false @@ -60,7 +62,7 @@ func (s *FormState) Resolve(name string, version uint64, err string) bool { } // Submit asks the form to submit in its next frame, as the submit button does. -func (s *FormState) Submit() { s.requested = true } +func (s *FormState) Submit() { s.Requested = true } // Submitting reports whether a submit is in progress. func (s *FormState) Submitting() bool { return s.submitting } @@ -76,7 +78,7 @@ func (s *FormState) EndSubmit(formError string) { // Error returns the error name shows now, "" when valid, untouched or pending. func (s *FormState) Error(name string) string { - f := s.fields[name] + f := s.Fields[name] if f == nil || !(f.touched || s.tried) { return "" } @@ -130,12 +132,12 @@ type FormItemOptions[T any] struct { // Form lays out and checks its fields and shows submit and reset buttons. func Form(c *ui.Context, st *FormState, opts FormOptions, fields func(f *FormScope)) FormParts { - if st.fields == nil { - st.fields = map[string]*formFieldState{} + if st.Fields == nil { + st.Fields = map[string]*FormFieldState{} } - st.order, st.resets = st.order[:0], nil - d := Density(c) - k := Tokens(c) + st.Order, st.resets = st.Order[:0], nil + d := core.Density(c) + k := core.Tokens(c) parts := FormParts{} col := ui.Column(c).Gap(d.Unit() * 5) parts.Element = col @@ -155,13 +157,13 @@ func Form(c *ui.Context, st *FormState, opts FormOptions, fields func(f *FormSco ui.Row(c).Gap(d.Unit() * 3).Children(func() { submit, reset := opts.SubmitLabel, opts.ResetLabel if submit == "" { - submit = msg(c, "form.submit") + submit = core.Msg(c, "form.submit") } if reset == "" { - reset = msg(c, "form.reset") + reset = core.Msg(c, "form.reset") } - if Button(c, submit, ButtonOptions{Loading: st.submitting || st.awaiting}).Clicked() { - st.requested = true + if Button(c, submit, ButtonOptions{Loading: st.submitting || st.Awaiting}).Clicked() { + st.Requested = true } if Button(c, reset, ButtonOptions{Variant: Secondary, Disabled: st.submitting}).Clicked() { formReset(st) @@ -169,13 +171,13 @@ func Form(c *ui.Context, st *FormState, opts FormOptions, fields func(f *FormSco } }) }) - if st.requested { - st.requested = false - if !st.submitting && !st.awaiting { + if st.Requested { + st.Requested = false + if !st.submitting && !st.Awaiting { formStartSubmit(st) } } - if st.awaiting { + if st.Awaiting { parts.submitted = formSettle(c, st) } return parts @@ -183,31 +185,31 @@ func Form(c *ui.Context, st *FormState, opts FormOptions, fields func(f *FormSco // formStartSubmit validates every field and starts the async checks still owed. func formStartSubmit(st *FormState) { - st.tried, st.awaiting, st.formError = true, true, "" - for _, name := range st.order { - f := st.fields[name] + st.tried, st.Awaiting, st.formError = true, true, "" + for _, name := range st.Order { + f := st.Fields[name] f.err = f.check() if f.err == "" { f.startAsync() } } - slog.Debug("mujica: form submit requested", "fields", len(st.order)) + slog.Debug("mujica: form submit requested", "fields", len(st.Order)) } // formSettle finishes a waiting submit once no check is pending; it reports success. func formSettle(c *ui.Context, st *FormState) bool { // Current values only: later edits and cross-field rules count. - for _, name := range st.order { - f := st.fields[name] + for _, name := range st.Order { + f := st.Fields[name] f.refresh() f.err = f.check() } first, pending := "", false - for _, name := range st.order { + for _, name := range st.Order { if st.Error(name) != "" && first == "" { first = name } - f := st.fields[name] + f := st.Fields[name] // A value edited since its check owes a new one. if f.hasAsync && f.err == "" && f.asyncFor != f.version { f.startAsync() @@ -216,13 +218,13 @@ func formSettle(c *ui.Context, st *FormState) bool { } switch { case first != "": - st.awaiting, st.focus = false, first - c.Announce(msg(c, "form.failed")) + st.Awaiting, st.focus = false, first + c.Announce(core.Msg(c, "form.failed")) c.Invalidate() slog.Debug("mujica: form submit failed", "first", first) case pending: default: - st.awaiting, st.submitting = false, true + st.Awaiting, st.submitting = false, true slog.Debug("mujica: form submitted") return true } @@ -234,9 +236,9 @@ func formReset(st *FormState) { for _, r := range st.resets { r() } - st.fields = map[string]*formFieldState{} + st.Fields = map[string]*FormFieldState{} st.generation++ - st.tried, st.awaiting, st.requested, st.focus, st.formError = false, false, false, "", "" + st.tried, st.Awaiting, st.Requested, st.focus, st.formError = false, false, false, "", "" slog.Debug("mujica: form reset") } @@ -246,20 +248,20 @@ func FormItem[T any](c *ui.Context, f *FormScope, name string, value *T, opts Fo if name == "" || opts.Label == "" { panic("mujica: FormItem needs a name and a Label") } - if slices.Contains(st.order, name) { + if slices.Contains(st.Order, name) { panic("mujica: FormItem " + name + " registered twice") } - st.order = append(st.order, name) - fs := st.fields[name] + st.Order = append(st.Order, name) + fs := st.Fields[name] if fs == nil { st.seq++ - fs = &formFieldState{snapshot: fmt.Sprintf("%#v", *value), version: st.seq} - st.fields[name] = fs + fs = &FormFieldState{snapshot: fmt.Sprintf("%#v", *value), version: st.seq} + st.Fields[name] = fs } fs.hasAsync = opts.Async != nil fs.check = func() string { if opts.Required && formEmpty(*value) { - return msg(c, "form.required") + return core.Msg(c, "form.required") } if opts.Validate != nil { return opts.Validate(*value) @@ -290,7 +292,7 @@ func FormItem[T any](c *ui.Context, f *FormScope, name string, value *T, opts Fo st.resets = append(st.resets, func() { *value = initial }) desc := opts.Description if fs.pending { - desc = msg(c, "form.checking") + desc = core.Msg(c, "form.checking") } var ctrl *ui.Element field := formFieldBuild(c, name, opts.Label, FormFieldOptions{Required: opts.Required, Description: desc, Error: st.Error(name)}, f.horizontal, func() *ui.Element { diff --git a/form_field.go b/input/form_field.go similarity index 87% rename from form_field.go rename to input/form_field.go index ee589fc..44f61aa 100644 --- a/form_field.go +++ b/input/form_field.go @@ -1,9 +1,10 @@ -package mujica +package input import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FormFieldOptions configure a FormField. @@ -29,7 +30,7 @@ func FormField(c *ui.Context, label string, opts FormFieldOptions, control func( // formFieldGrid holds fields whose labels sit in a column beside their controls. func formFieldGrid(c *ui.Context, fields func()) *ui.Element { - d := Density(c) + d := core.Density(c) return ui.Grid(c).ColumnTracks(ui.FitContent(), ui.Fr(1)).GapX(d.Unit() * 3).GapY(d.Unit() * 4). AlignItems(ui.Start).Children(fields) } @@ -40,8 +41,8 @@ func formFieldBuild(c *ui.Context, key, label string, opts FormFieldOptions, bes if label == "" { panic("mujica: FormField needs a label") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) var head, content, ctrl *ui.Element body := func() { head = ui.Row(c) @@ -71,15 +72,15 @@ func formFieldBuild(c *ui.Context, key, label string, opts FormFieldOptions, bes panic("mujica: FormField " + label + ": control returned nil") } if opts.Description != "" { - ui.Text(c, opts.Description).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) + ui.Text(c, opts.Description).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) } if opts.Error != "" { - ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) + ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleNone) } }) ctrl.Label(label).Description(opts.Description).Error(opts.Error) if opts.Required { - ctrl.Description(msg(c, "form.required")) + ctrl.Description(core.Msg(c, "form.required")) } if ctrl.IsDisabled() { text.TextColor(k.TextMuted) diff --git a/form_field_test.go b/input/form_field_test.go similarity index 94% rename from form_field_test.go rename to input/form_field_test.go index 80e61d3..b1f40f2 100644 --- a/form_field_test.go +++ b/input/form_field_test.go @@ -1,16 +1,18 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestFormFieldLabelFocusesControl(t *testing.T) { name := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FormField(c, "Name", FormFieldOptions{Required: true, Description: "As on your seal."}, func() *ui.Element { return InputGroup(c, &name, InputGroupOptions{}).Input }) @@ -31,7 +33,7 @@ func TestFormFieldLabelFocusesControl(t *testing.T) { func TestFormFieldTargetsReturnedControl(t *testing.T) { tags, site := []string{"rose"}, "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(12).Children(func() { FormField(c, "Flowers", FormFieldOptions{Error: "Too few."}, func() *ui.Element { return TagsInput(c, &tags, TagsInputOptions{}).Input @@ -61,7 +63,7 @@ func TestFormFieldErrorAndLayout(t *testing.T) { v := "" horizontal := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FormField(c, "Email", FormFieldOptions{Error: "Enter an email address.", Horizontal: horizontal}, func() *ui.Element { return InputGroup(c, &v, InputGroupOptions{Prefix: "mailto:"}).Input }) @@ -90,7 +92,7 @@ func TestFormFieldNeedsLabel(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) FormField(c, "", FormFieldOptions{}, func() *ui.Element { return ui.Box(c) }) }, 100, 100, 1) } diff --git a/form_test.go b/input/form_test.go similarity index 98% rename from form_test.go rename to input/form_test.go index 64ce79b..31324eb 100644 --- a/form_test.go +++ b/input/form_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type formProbe struct { @@ -19,7 +21,7 @@ type formProbe struct { } func (p *formProbe) run(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) p.view(c) } @@ -220,7 +222,7 @@ func TestFormResetClearsControlState(t *testing.T) { var st FormState tags, seen := []string{}, "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Form(c, &st, FormOptions{}, func(f *FormScope) { FormItem(c, f, "tags", &tags, FormItemOptions[[]string]{Label: "Tags"}, func() *ui.Element { return TagsInput(c, &tags, TagsInputOptions{Validate: func(s string) string { @@ -287,7 +289,7 @@ func TestFormCrossFieldRuleAtSettle(t *testing.T) { var version uint64 submits := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) res := Form(c, &st, FormOptions{}, func(f *FormScope) { FormItem(c, f, "user", &user, FormItemOptions[string]{Label: "User", Async: func(_ string, v uint64) { version = v }}, func() *ui.Element { return InputGroup(c, &user, InputGroupOptions{}).Input diff --git a/input/helpers_test.go b/input/helpers_test.go new file mode 100644 index 0000000..20b9d39 --- /dev/null +++ b/input/helpers_test.go @@ -0,0 +1,36 @@ +package input + +import ( + "image" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func encode(t *testing.T, img image.Image) []byte { + t.Helper() + return snaptest.Encode(t, img) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/hold_to_confirm.go b/input/hold_to_confirm.go similarity index 82% rename from hold_to_confirm.go rename to input/hold_to_confirm.go index 76f1440..5a4f061 100644 --- a/hold_to_confirm.go +++ b/input/hold_to_confirm.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // HoldToConfirmOptions configure a HoldToConfirm. @@ -44,7 +45,7 @@ func (s *holdToConfirmState) step(held bool, now time.Time, d time.Duration) (pr } // HoldToConfirm runs a dangerous action only after the pointer or Space is held for Duration. -func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) ChoiceResult { +func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) core.ChoiceResult { if label == "" { panic("mujica: HoldToConfirm needs a label") } @@ -55,10 +56,10 @@ func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) Choic if d == 0 { d = 1500 * time.Millisecond } - k := Tokens(c) - den := Density(c) + k := core.Tokens(c) + den := core.Density(c) b := ui.ButtonBase(c).MinHeight(den.ControlHeight()).MinWidth(theme.MinHitSize).Padding(0, den.Unit()*4). - Radius(theme.ControlRadius).Disabled(opts.Disabled).Description(msg(c, "hold_to_confirm.hint")).Clip() + Radius(theme.ControlRadius).Disabled(opts.Disabled).Description(core.Msg(c, "hold_to_confirm.hint")).Clip() internal.Ring(b, k.Focus, theme.ControlRadius) st := ui.Local(b, "hold_to_confirm", func() *holdToConfirmState { return &holdToConfirmState{} }) s := *st @@ -88,12 +89,12 @@ func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) Choic c.AnimationFrame() } } - bg, fg, _ := buttonColors(k, Danger, b.Hovered() && !disabled, false) + bg, fg, _ := ButtonColors(k, Danger, b.Hovered() && !disabled, false) b.Background(bg).TextColor(fg) text := label if held && still && progress < 1 { left := d - time.Duration(progress*float64(d)) - text = fmt.Sprintf(msg(c, "hold_to_confirm.left"), left.Seconds()) + text = fmt.Sprintf(core.Msg(c, "hold_to_confirm.left"), left.Seconds()) } b.Draw(func(p *ui.Painter, r ui.Rect) { if progress > 0 && !still { @@ -102,12 +103,12 @@ func HoldToConfirm(c *ui.Context, label string, opts HoldToConfirmOptions) Choic }) // Room for the countdown, so the width holds. w1, _ := c.MeasureText(0, ui.Span{Text: label}) - w2, _ := c.MeasureText(0, ui.Span{Text: fmt.Sprintf(msg(c, "hold_to_confirm.left"), d.Seconds())}) + w2, _ := c.MeasureText(0, ui.Span{Text: fmt.Sprintf(core.Msg(c, "hold_to_confirm.left"), d.Seconds())}) b.MinWidth(max(w1, w2) + den.Unit()*8) b.Children(func() { ui.Text(c, text).SingleLine() }) if confirmed { - c.Announce(msg(c, "hold_to_confirm.done")) + c.Announce(core.Msg(c, "hold_to_confirm.done")) slog.Debug("mujica: hold_to_confirm confirmed", "label", label) } - return ChoiceResult{b, confirmed} + return core.NewChoiceResult(b, confirmed) } diff --git a/hold_to_confirm_test.go b/input/hold_to_confirm_test.go similarity index 96% rename from hold_to_confirm_test.go rename to input/hold_to_confirm_test.go index 062449e..13be311 100644 --- a/hold_to_confirm_test.go +++ b/input/hold_to_confirm_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "strings" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestHoldToConfirmStep(t *testing.T) { @@ -32,7 +34,7 @@ func TestHoldToConfirmStep(t *testing.T) { func holdTester(dis *bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).Children(func() { if HoldToConfirm(c, "Delete repo", HoldToConfirmOptions{Duration: 60 * time.Millisecond, Disabled: *dis}).Changed() { *n++ @@ -82,7 +84,7 @@ func TestHoldToConfirmPointerHold(t *testing.T) { func TestHoldToConfirmSpaceHold(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) // A nanosecond hold completes in the frames between Space down and up. if HoldToConfirm(c, "Drop table", HoldToConfirmOptions{Duration: time.Nanosecond}).Changed() { n++ diff --git a/icon_button.go b/input/icon_button.go similarity index 57% rename from icon_button.go rename to input/icon_button.go index 306c238..47a588a 100644 --- a/icon_button.go +++ b/input/icon_button.go @@ -1,10 +1,9 @@ -package mujica +package input import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // IconButtonOptions configure an IconButton. @@ -25,7 +24,7 @@ func IconButton(c *ui.Context, icon *ui.SVG, label string, opts IconButtonOption if label == "" { panic("mujica: IconButton needs an action name") } - h := Density(c).ControlHeight() + h := core.Density(c).ControlHeight() b := Button(c, "", ButtonOptions{Variant: opts.Variant, Icon: icon, Loading: opts.Loading, Disabled: opts.Disabled, Label: label}) b.Padding(0).Size(h, h) tip := opts.Tooltip @@ -34,18 +33,3 @@ func IconButton(c *ui.Context, icon *ui.SVG, label string, opts IconButtonOption } return b.Tooltip(tip) } - -// iconAction is a square, borderless button showing icon, named label. -func iconAction(c *ui.Context, icon *ui.SVG, label string) *ui.Element { - k := Tokens(c) - b := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Label(label).TextColor(k.TextMuted) - internal.Ring(b, k.Focus, theme.ControlRadius) - switch { - case b.Pressed(): - b.Background(k.Selection).TextColor(k.Text) - case b.Hovered(): - b.Background(k.SurfaceHover).TextColor(k.Text) - } - b.Children(func() { ui.Icon(c, icon).Size(16, 16) }) - return b -} diff --git a/icon_button_test.go b/input/icon_button_test.go similarity index 91% rename from icon_button_test.go rename to input/icon_button_test.go index 9b0794a..70afae1 100644 --- a/icon_button_test.go +++ b/input/icon_button_test.go @@ -1,18 +1,19 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestIconButtonClickByNameAndKeyboard(t *testing.T) { n := 0 var w, h float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { b := IconButton(c, icons.Must("settings"), "Settings", IconButtonOptions{Variant: Ghost}) w, h = b.Bounds().W, b.Bounds().H @@ -37,7 +38,7 @@ func TestIconButtonBlockedStates(t *testing.T) { for _, opts := range []IconButtonOptions{{Disabled: true}, {Loading: true}} { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if IconButton(c, icons.Must("trash-2"), "Delete", opts).Clicked() { n++ } @@ -58,7 +59,7 @@ func TestIconButtonNeedsName(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IconButton(c, icons.Must("x"), "", IconButtonOptions{}) }, 100, 100) } diff --git a/input_group.go b/input/input_group.go similarity index 96% rename from input_group.go rename to input/input_group.go index 885654c..f7eeb82 100644 --- a/input_group.go +++ b/input/input_group.go @@ -1,10 +1,11 @@ -package mujica +package input import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // InputGroupButton is a button joined to an InputGroup's border. @@ -60,8 +61,8 @@ func InputGroup(c *ui.Context, value *string, opts InputGroupOptions) InputGroup panic("mujica: InputGroupButton needs a Label") } } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) parts := InputGroupParts{Clicked: -1} g := ui.Row(c).MinHeight(d.ControlHeight()).AlignItems(ui.Stretch).Radius(theme.ControlRadius). Padding(theme.BorderWidth).Background(k.Surface).Disabled(opts.Disabled) diff --git a/input_group_test.go b/input/input_group_test.go similarity index 93% rename from input_group_test.go rename to input/input_group_test.go index 91bb733..580056c 100644 --- a/input_group_test.go +++ b/input/input_group_test.go @@ -1,12 +1,13 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestInputGroupTypingAndButtons(t *testing.T) { @@ -16,7 +17,7 @@ func TestInputGroupTypingAndButtons(t *testing.T) { Buttons: []InputGroupButton{{Label: "Clear", Icon: icons.Must("x"), IconOnly: true}, {Label: "Go"}}, } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) p := InputGroup(c, &value, opts) if p.Clicked >= 0 { clicked = p.Clicked @@ -56,7 +57,7 @@ func TestInputGroupTypingAndButtons(t *testing.T) { func TestInputGroupTabOrder(t *testing.T) { value := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) InputGroup(c, &value, InputGroupOptions{Label: "Query", Buttons: []InputGroupButton{{Label: "Back", Leading: true}, {Label: "Search"}}}) }, 500, 120) tt.Key(0, ui.KeyTab) @@ -77,7 +78,7 @@ func TestInputGroupBlockedStates(t *testing.T) { for _, opts := range []InputGroupOptions{{Label: "Code", Disabled: true}, {Label: "Code", ReadOnly: true}} { value, clicked := "abc", -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) opts.Buttons = []InputGroupButton{{Label: "Copy"}} if p := InputGroup(c, &value, opts); p.Clicked >= 0 { clicked = p.Clicked @@ -114,7 +115,7 @@ func TestInputGroupSnapshot(t *testing.T) { func TestInputGroupDisabledAfterFocus(t *testing.T) { value, disabled, changes := "abc", false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if InputGroup(c, &value, InputGroupOptions{Label: "Code"}).Changed() { changes++ @@ -134,7 +135,7 @@ func TestInputGroupDisabledAfterFocus(t *testing.T) { func TestInputGroupHitAreasCompact(t *testing.T) { value := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) InputGroup(c, &value, InputGroupOptions{Label: "Query", Buttons: []InputGroupButton{{Label: "Clear", Icon: icons.Must("x"), IconOnly: true}}}) }, 400, 100) if r, ok := tt.Find("Clear"); !ok || r.W < 28 || r.H < 28 { @@ -146,7 +147,7 @@ func TestInputGroupHitAreasCompact(t *testing.T) { func TestInputGroupDisabledEditCommands(t *testing.T) { value, disabled, changes := "secret", false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if InputGroup(c, &value, InputGroupOptions{Label: "Code", Disabled: disabled}).Changed() { changes++ } diff --git a/input_review2_test.go b/input/input_review2_test.go similarity index 95% rename from input_review2_test.go rename to input/input_review2_test.go index 77b882c..9d76a0b 100644 --- a/input_review2_test.go +++ b/input/input_review2_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "image/color" @@ -7,15 +7,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestInputsObeyDisabledAncestor(t *testing.T) { text, area, num, find, cur := "", "", 5.0, "a", 0 off, events := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(8).Disabled(off).Children(func() { for _, r := range []InputResult{ TextInput(c, &text, TextInputOptions{Label: "Text"}), @@ -85,7 +86,7 @@ func TestButtonLoadingKeepsLabelAndWidth(t *testing.T) { loading := false var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: d}) + core.Use(c, core.Settings{Density: d}) ui.Row(c).Children(func() { w = Button(c, "Save", ButtonOptions{Icon: icon, Loading: loading}).Bounds().W }) @@ -111,7 +112,7 @@ func TestButtonLoadingKeepsLabelAndWidth(t *testing.T) { func numberInputStepTester(v *float64, opts NumberInputOptions) *ui.Tester { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NumberInput(c, v, opts) }, 400, 200) tt.Key(0, ui.KeyTab) @@ -157,7 +158,7 @@ func TestRadioGroupKeysByEquality(t *testing.T) { v := radioSameName{0} items := []RadioItem[radioSameName]{{Value: radioSameName{1}, Label: "One"}, {Value: radioSameName{2}, Label: "Two"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RadioGroup(c, &v, items, RadioGroupOptions{}) }, 300, 200) _ = tt.Click("Two") diff --git a/input_review_test.go b/input/input_review_test.go similarity index 92% rename from input_review_test.go rename to input/input_review_test.go index df53518..7101a2f 100644 --- a/input_review_test.go +++ b/input/input_review_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "bytes" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestButtonLoadingKeepsWidth(t *testing.T) { @@ -14,7 +15,7 @@ func TestButtonLoadingKeepsWidth(t *testing.T) { loading := false var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Children(func() { w = Button(c, "Save", ButtonOptions{Icon: icon, Loading: loading}).Bounds().W }) @@ -38,7 +39,7 @@ func inputReviewPanics(t *testing.T, name string, view func(c *ui.Context)) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) view(c) }, 200, 100) } @@ -55,7 +56,7 @@ func TestIconOnlyButtonsNeedNames(t *testing.T) { func TestTextInputDisabledWhileFocused(t *testing.T) { s, off, focused, changes := "", false, false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TextInput(c, &s, TextInputOptions{Label: "Name", Disabled: off}) focused = r.Input.Focused() if r.Changed() || r.Submitted() { @@ -76,7 +77,7 @@ func TestTextInputDisabledWhileFocused(t *testing.T) { func TestTextAreaDisabledWhileFocused(t *testing.T) { s, off := "", false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TextArea(c, &s, TextAreaOptions{Label: "Notes", Disabled: off}) }, 400, 300) _ = tt.Click("Notes") @@ -90,7 +91,7 @@ func TestTextAreaDisabledWhileFocused(t *testing.T) { func TestCheckboxDisabledWhileFocused(t *testing.T) { on, off, focused := false, false, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) focused = Checkbox(c, &on, "Sync", CheckboxOptions{Disabled: off}).Focused() }, 300, 200) tt.Key(0, ui.KeyTab) @@ -106,7 +107,7 @@ func TestRadioGroupDisabledWhileFocused(t *testing.T) { v, off := "a", false items := []RadioItem[string]{{Value: "a", Label: "A"}, {Value: "b", Label: "B"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RadioGroup(c, &v, items, RadioGroupOptions{Disabled: off}) }, 300, 200) tt.Key(0, ui.KeyTab) @@ -121,7 +122,7 @@ func TestRadioGroupDisabledWhileFocused(t *testing.T) { func TestTextAreaPlainEnterSubmits(t *testing.T) { s, submits := "", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TextArea(c, &s, TextAreaOptions{Label: "Chat", SubmitKey: ui.KeyEnter}).Submitted() { submits++ } @@ -139,7 +140,7 @@ func TestRadioGroupFocusFollowsValue(t *testing.T) { v := "" items := []RadioItem[string]{{Value: "a", Label: "A"}, {Value: "b", Label: "B"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RadioGroup(c, &v, items, RadioGroupOptions{}) }, 300, 200) tt.Key(0, ui.KeyTab) @@ -155,7 +156,7 @@ func TestRadioGroupFocusFollowsValue(t *testing.T) { func inputReviewStates(t *testing.T, name string, view func(c *ui.Context)) (plain, hover, pressed []byte) { t.Helper() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Dark}) + core.Use(c, core.Settings{Mode: core.Dark}) view(c) }, 300, 200) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) diff --git a/knob.go b/input/knob.go similarity index 75% rename from knob.go rename to input/knob.go index b2d5cf7..01ab188 100644 --- a/knob.go +++ b/input/knob.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // KnobOptions configure a Knob. Min and Max both zero mean 0 to 100. @@ -35,29 +36,29 @@ const knobSweep = 0.75 const knobDragPixels float32 = 160 // Knob sets *value by dragging up and down, the arrows, Home and End; the dial shows the value and ticks. -func Knob(c *ui.Context, value *float64, opts KnobOptions) ChoiceResult { +func Knob(c *ui.Context, value *float64, opts KnobOptions) core.ChoiceResult { if value == nil { panic("mujica: Knob needs a value") } if opts.Ticks < 0 || !(opts.Size >= 0) || math.IsInf(float64(opts.Size), 1) { panic(fmt.Sprintf("mujica: Knob Ticks %d or Size %v invalid", opts.Ticks, opts.Size)) } - g, _ := sliderSetup(opts.Min, opts.Max, opts.Step, nil, false, c) + g, _ := SliderSetup(opts.Min, opts.Max, opts.Step, nil, false, c) format := sliderFormat(opts.Format, g) size := cmp.Or(opts.Size, 56) - k := Tokens(c) + k := core.Tokens(c) old := g.snap(*value) *value = old - col := ui.Column(c).AlignItems(ui.Center).Gap(Density(c).Unit()).Disabled(opts.Disabled) + col := ui.Column(c).AlignItems(ui.Center).Gap(core.Density(c).Unit()).Disabled(opts.Disabled) disabled := col.IsDisabled() var s *ui.Element col.Children(func() { if opts.Label != "" { - ui.Text(c, opts.Label).TextColor(k.Text).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, opts.Label).TextColor(k.Text).FontSize(core.FontSize(c, theme.CaptionSize)) } var next float64 var dragged bool - s, next, dragged = sliderControl(c, old, g, g.lo, g.hi) + s, next, dragged = SliderControl(c, old, g, g.lo, g.hi) want := ui.Local(s, "knob.want", func() float64 { return math.NaN() }) if dragged { if math.IsNaN(*want) { @@ -72,28 +73,28 @@ func Knob(c *ui.Context, value *float64, opts KnobOptions) ChoiceResult { if !disabled { *value = next } - s.Size(size, size).FocusRing(false).Cursor(ui.CursorResizeNS).Label(cmp.Or(opts.Label, msg(c, "knob.label"))) + s.Size(size, size).FocusRing(false).Cursor(ui.CursorResizeNS).Label(cmp.Or(opts.Label, core.Msg(c, "knob.label"))) hot := s.Hovered() || dragged v := *value s.Draw(func(p *ui.Painter, r ui.Rect) { knobPaint(p, r, k, g, v, opts.Ticks, hot, s.FocusVisible()) }) - ui.Text(c, format(*value)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + ui.Text(c, format(*value)).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") }) changed := *value != old if changed { slog.Debug("mujica: knob changed", "label", opts.Label, "value", *value) } - return ChoiceResult{col, changed} + return core.NewChoiceResult(col, changed) } // knobPaint draws the track arc, the value arc, ticks and the diamond pointer. -func knobPaint(p *ui.Painter, r ui.Rect, k theme.Tokens, g sliderGeom, v float64, ticks int, hot, focus bool) { +func knobPaint(p *ui.Painter, r ui.Rect, k theme.Tokens, g SliderGeom, v float64, ticks int, hot, focus bool) { cx, cy := internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2) rad := r.W/2 - 6 start := float32(-knobSweep / 2) frac := float32((v - g.lo) / (g.hi - g.lo)) - p.StrokePath(progressArc(p, cx, cy, rad, start, start+knobSweep), 3, k.Border) + p.StrokePath(core.ProgressArc(p, cx, cy, rad, start, start+knobSweep), 3, k.Border) if frac > 0 { - p.StrokePath(progressArc(p, cx, cy, rad, start, start+knobSweep*frac), 3, k.AccentText) + p.StrokePath(core.ProgressArc(p, cx, cy, rad, start, start+knobSweep*frac), 3, k.AccentText) } for i := range ticks { t := float64(start) + knobSweep*float64(i)/float64(max(ticks-1, 1)) diff --git a/knob_test.go b/input/knob_test.go similarity index 91% rename from knob_test.go rename to input/knob_test.go index 79c3136..f65d805 100644 --- a/knob_test.go +++ b/input/knob_test.go @@ -1,14 +1,16 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func knobTester(v *float64, opts *KnobOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(40).Children(func() { if Knob(c, v, *opts).Changed() { *n++ @@ -81,7 +83,7 @@ func TestKnobRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); Knob(c, &v, o) }, 200, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); Knob(c, &v, o) }, 200, 200) }() } } diff --git a/masked_input.go b/input/masked_input.go similarity index 97% rename from masked_input.go rename to input/masked_input.go index 8e7fc8a..bfa9124 100644 --- a/masked_input.go +++ b/input/masked_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MaskedInputOptions configure a MaskedInput. @@ -98,7 +99,7 @@ func MaskedInput(c *ui.Context, value *string, opts MaskedInputOptions) InputRes panic("mujica: MaskedInput value does not fit its Mask") } } - k := Tokens(c) + k := core.Tokens(c) var r InputResult look := &textInputLook{disabled: opts.Disabled, err: opts.Error} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { @@ -178,7 +179,7 @@ func MaskedInput(c *ui.Context, value *string, opts MaskedInputOptions) InputRes } shown := []rune(MaskFormat(opts.Mask, *value)) at := maskedInputAt(mask, s.caret) - size := FontSize(c, theme.BodySize) + size := core.FontSize(c, theme.BodySize) w, h := c.MeasureText(0, ui.Span{Text: string(shown[:at]), Size: size}) if h == 0 { _, h = c.MeasureText(0, ui.Span{Text: "0", Size: size}) diff --git a/masked_input_test.go b/input/masked_input_test.go similarity index 93% rename from masked_input_test.go rename to input/masked_input_test.go index 8b1b8d0..17cfce7 100644 --- a/masked_input_test.go +++ b/input/masked_input_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMaskFormat(t *testing.T) { @@ -20,7 +22,7 @@ func TestMaskFormat(t *testing.T) { func maskedTester(v *string, dis *bool, n, submits *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MaskedInput(c, v, MaskedInputOptions{Mask: "(999) 999-9999", Label: "Phone", Disabled: *dis}) if r.Changed() { *n++ @@ -108,7 +110,7 @@ func TestMaskedInputRejects(t *testing.T) { } }() v := tc[1] - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); MaskedInput(c, &v, MaskedInputOptions{Mask: tc[0]}) }, 200, 100) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); MaskedInput(c, &v, MaskedInputOptions{Mask: tc[0]}) }, 200, 100) }() } } diff --git a/masonry.go b/input/masonry.go similarity index 98% rename from masonry.go rename to input/masonry.go index 3c1d7ad..5df0acd 100644 --- a/masonry.go +++ b/input/masonry.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -7,6 +7,8 @@ import ( "sort" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // MasonryOptions configure a Masonry. @@ -51,7 +53,7 @@ func Masonry(c *ui.Context, n int, opts MasonryOptions, item func(i int)) *ui.El if n < 0 || opts.ColumnWidth < 0 || opts.Gap < 0 || opts.Estimate < 0 { panic("mujica: Masonry count and sizes must not be negative") } - u := Density(c).Unit() + u := core.Density(c).Unit() colMin := cmp.Or(opts.ColumnWidth, 220) gap := cmp.Or(opts.Gap, 3*u) est := cmp.Or(opts.Estimate, 160) diff --git a/masonry_test.go b/input/masonry_test.go similarity index 95% rename from masonry_test.go rename to input/masonry_test.go index 71349e8..ad7107f 100644 --- a/masonry_test.go +++ b/input/masonry_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "strconv" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMasonryPlaceShortestColumn(t *testing.T) { @@ -33,7 +35,7 @@ func TestMasonryVirtualizesAndMeasures(t *testing.T) { n := 100000 width := 700 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Masonry(c, n, MasonryOptions{Key: strconv.Itoa, ColumnWidth: 200, Gap: 10}, func(i int) { built[i] = true ui.Box(c).Height(float32(40 + i%3*30)).Label("tile " + strconv.Itoa(i)) @@ -68,7 +70,7 @@ func TestMasonryVirtualizesAndMeasures(t *testing.T) { func TestMasonrySnapshot(t *testing.T) { snapshot(t, "masonry", 520, 300, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) Masonry(c, 9, MasonryOptions{Key: strconv.Itoa, ColumnWidth: 150}, func(i int) { ui.Column(c).Height(float32(50+i%4*25)).Padding(8).Background(k.Surface).Border(1, k.Border).Radius(4).Children(func() { ui.Text(c, "Card "+strconv.Itoa(i+1)) diff --git a/mention_input.go b/input/mention_input.go similarity index 93% rename from mention_input.go rename to input/mention_input.go index e286fb0..1ff150e 100644 --- a/mention_input.go +++ b/input/mention_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MentionCandidate is someone a MentionInput may mention. @@ -53,11 +54,11 @@ func MentionInput(c *ui.Context, value *string, mentions *[]Mention, opts Mentio panic(fmt.Sprintf("mujica: MentionInput mention %+v does not match its text", m)) } } - k := Tokens(c) + k := core.Tokens(c) var r InputResult look := &textInputLook{disabled: opts.Disabled, err: opts.Error} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { - in := ui.Box(c).Grow(1).MinWidth(Density(c).Unit() * 20).Focusable().Clip().Role(ui.RoleTextField).Label(opts.Label).Cursor(ui.CursorText) + in := ui.Box(c).Grow(1).MinWidth(core.Density(c).Unit() * 20).Focusable().Clip().Role(ui.RoleTextField).Label(opts.Label).Cursor(ui.CursorText) s := *ui.Local(in, "mention_input", func() *mentionInputState { return &mentionInputState{at: -1} }) if s.doc.text == nil || s.synced != *value || !slices.Equal(s.syncedMentions, *mentions) { // Set from outside: start over at the end. @@ -215,7 +216,7 @@ func mentionInputHit(c *ui.Context, s *mentionInputState, x float32) int { x += s.offset best, bestD := 0, float32(-1) for i := 0; i <= len(s.doc.text); i++ { - w, _ := c.MeasureText(0, ui.Span{Text: string(s.doc.text[:i]), Size: FontSize(c, theme.BodySize)}) + w, _ := c.MeasureText(0, ui.Span{Text: string(s.doc.text[:i]), Size: core.FontSize(c, theme.BodySize)}) if d := max(w-x, x-w); bestD < 0 || d < bestD { best, bestD = i, d } @@ -230,7 +231,7 @@ func mentionInputHit(c *ui.Context, s *mentionInputState, x float32) int { // mentionInputView draws the text with mention chips, the selection and the caret, scrolled to keep the caret in view. func mentionInputView(c *ui.Context, k theme.Tokens, s *mentionInputState, in *ui.Element, placeholder string) { - size := FontSize(c, theme.BodySize) + size := core.FontSize(c, theme.BodySize) measure := func(i int) float32 { w, _ := c.MeasureText(0, ui.Span{Text: string(s.doc.text[:i]), Size: size}) return w @@ -299,18 +300,18 @@ func mentionInputSpans(k theme.Tokens, s *mentionInputState, size float32, place // mentionInputPopup lists the candidates under the field; a click or Enter picks one. func mentionInputPopup(c *ui.Context, k theme.Tokens, s *mentionInputState, frame, in *ui.Element) { b := frame.Bounds() - u := Density(c).Unit() + u := core.Density(c).Unit() ui.Overlay(c, func() { ww, wh := c.Size() - h := float32(len(s.found)) * Density(c).ControlHeight() - x, y := overlayPlace(c, b, min(b.W, 320), min(h+2*u, wh/2), 4) + h := float32(len(s.found)) * core.Density(c).ControlHeight() + x, y := core.PlaceOverlay(c, b, min(b.W, 320), min(h+2*u, wh/2), 4) panel := ui.Column(c).Absolute().Left(x).Top(y).Width(min(max(b.W, 200), ww-8)).MaxHeight(wh / 2).Padding(u).Role(ui.RoleList) - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) s.panel = panel.Bounds() panel.Children(func() { ui.Scroll(c).Shrink(1).Children(func() { for i, m := range s.found { - it := ui.Row(c).Key(m.ID).MinHeight(Density(c).ControlHeight()).AlignItems(ui.Center).Padding(0, u*2). + it := ui.Row(c).Key(m.ID).MinHeight(core.Density(c).ControlHeight()).AlignItems(ui.Center).Padding(0, u*2). Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(m.Name).Cursor(ui.CursorPointer) if i == s.pick { it.Selected(true).Background(k.Selection).TextColor(k.AccentText) diff --git a/mention_input_model.go b/input/mention_input_model.go similarity index 99% rename from mention_input_model.go rename to input/mention_input_model.go index 2384235..9976166 100644 --- a/mention_input_model.go +++ b/input/mention_input_model.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "slices" diff --git a/mention_input_test.go b/input/mention_input_test.go similarity index 97% rename from mention_input_test.go rename to input/mention_input_test.go index d9b87eb..4ad43ca 100644 --- a/mention_input_test.go +++ b/input/mention_input_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "slices" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var mentionPeople = []MentionCandidate{{ID: "u1", Name: "Alice"}, {ID: "u2", Name: "Albert"}, {ID: "u3", Name: "Bob"}} @@ -49,7 +51,7 @@ func TestMentionDocWholeTokens(t *testing.T) { func mentionTester(v *string, ms *[]Mention, dis *bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(400).Children(func() { if MentionInput(c, v, ms, MentionInputOptions{Candidates: mentionFind, Label: "Comment", Disabled: *dis}).Changed() { *n++ @@ -132,7 +134,7 @@ func TestMentionInputRejectsBadMentions(t *testing.T) { }() v, ms := "hi Bob", []Mention{{ID: "u3", Name: "Bob", Start: 3, End: 6}} ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MentionInput(c, &v, &ms, MentionInputOptions{Candidates: mentionFind}) }, 300, 100) } diff --git a/menubar_test.go b/input/menubar_test.go similarity index 74% rename from menubar_test.go rename to input/menubar_test.go index 23ed84a..41d1f44 100644 --- a/menubar_test.go +++ b/input/menubar_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -6,6 +6,9 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" ) type menubarApp struct { @@ -14,33 +17,33 @@ type menubarApp struct { chosen []string } -func (a *menubarApp) menus() []MenubarMenu { - return []MenubarMenu{ - {Label: "File", Items: []MenubarItem{ - {ID: "new", Label: "New", Shortcut: MenubarShortcut{ui.Cmd, ui.KeyN}}, - {ID: "open", Label: "Open…", Shortcut: MenubarShortcut{ui.Cmd, ui.KeyO}}, - {Label: "Export", Items: []MenubarItem{ +func (a *menubarApp) menus() []navigation.MenubarMenu { + return []navigation.MenubarMenu{ + {Label: "File", Items: []navigation.MenubarItem{ + {ID: "new", Label: "New", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {ID: "open", Label: "Open…", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyO}}, + {Label: "Export", Items: []navigation.MenubarItem{ {ID: "pdf", Label: "PDF"}, {ID: "png", Label: "Image", Disabled: true}, {ID: "svg", Label: "Vector"}, }}, - {Kind: MenubarSeparator}, + {Kind: navigation.MenubarSeparator}, {ID: "quit", Label: "Quit"}, }}, - {Label: "View", Items: []MenubarItem{ - {ID: "wrap", Label: "Word wrap", Kind: MenubarCheck, Checked: a.wrap}, - {Kind: MenubarSeparator}, - {ID: "zoom-fit", Label: "Fit", Kind: MenubarRadio, Checked: a.zoom == "zoom-fit"}, - {ID: "zoom-100", Label: "Actual size", Kind: MenubarRadio, Checked: a.zoom == "zoom-100"}, + {Label: "View", Items: []navigation.MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: navigation.MenubarCheck, Checked: a.wrap}, + {Kind: navigation.MenubarSeparator}, + {ID: "zoom-fit", Label: "Fit", Kind: navigation.MenubarRadio, Checked: a.zoom == "zoom-fit"}, + {ID: "zoom-100", Label: "Actual size", Kind: navigation.MenubarRadio, Checked: a.zoom == "zoom-100"}, }}, - {Label: "Help", Items: []MenubarItem{{ID: "about", Label: "About"}}}, + {Label: "Help", Items: []navigation.MenubarItem{{ID: "about", Label: "About"}}}, } } func (a *menubarApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Children(func() { - if id, ok := Menubar(c, a.menus(), MenubarOptions{}).Chosen(); ok { + if id, ok := navigation.Menubar(c, a.menus(), navigation.MenubarOptions{}).Chosen(); ok { a.chosen = append(a.chosen, id) switch id { case "wrap": @@ -182,10 +185,10 @@ func TestMenubarTabOutClosesMenu(t *testing.T) { for _, back := range []bool{false, true} { a := &menubarApp{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Children(func() { Button(c, "Before", ButtonOptions{}) - Menubar(c, a.menus(), MenubarOptions{}) + navigation.Menubar(c, a.menus(), navigation.MenubarOptions{}) Button(c, "After", ButtonOptions{}) }) }, 700, 500) @@ -211,13 +214,13 @@ func TestMenubarTabOutClosesMenu(t *testing.T) { } func TestMenubarLongMenuScrolls(t *testing.T) { - var items []MenubarItem + var items []navigation.MenubarItem for i := range 30 { - items = append(items, MenubarItem{ID: fmt.Sprint(i), Label: fmt.Sprintf("Entry %02d", i)}) + items = append(items, navigation.MenubarItem{ID: fmt.Sprint(i), Label: fmt.Sprintf("Entry %02d", i)}) } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - Menubar(c, []MenubarMenu{{Label: "Long", Items: items}}, MenubarOptions{}) + core.Use(c, core.Settings{}) + navigation.Menubar(c, []navigation.MenubarMenu{{Label: "Long", Items: items}}, navigation.MenubarOptions{}) }, 400, 300) tt.Key(0, ui.KeyTab) tt.Key(0, ui.KeyDown) @@ -243,7 +246,7 @@ func TestMenubarAnnouncesHighlight(t *testing.T) { } func TestMenubarShortcutText(t *testing.T) { - got := menubarShortcutText(MenubarShortcut{ui.Ctrl | ui.Shift, ui.KeyF5}) + got := navigation.MenubarShortcutText(navigation.MenubarShortcut{Mods: ui.Ctrl | ui.Shift, Key: ui.KeyF5}) want := "Ctrl+Shift+F5" if runtime.GOOS == "darwin" { want = "⌃⇧F5" @@ -254,11 +257,11 @@ func TestMenubarShortcutText(t *testing.T) { } func TestMenubarInvalid(t *testing.T) { - for name, menus := range map[string][]MenubarMenu{ + for name, menus := range map[string][]navigation.MenubarMenu{ "empty": nil, - "title": {{Items: []MenubarItem{{ID: "a", Label: "A"}}}}, - "id": {{Label: "M", Items: []MenubarItem{{Label: "A"}}}}, - "submenu": {{Label: "M", Items: []MenubarItem{{Label: "A", Kind: MenubarCheck, Items: []MenubarItem{}}}}}, + "title": {{Items: []navigation.MenubarItem{{ID: "a", Label: "A"}}}}, + "id": {{Label: "M", Items: []navigation.MenubarItem{{Label: "A"}}}}, + "submenu": {{Label: "M", Items: []navigation.MenubarItem{{Label: "A", Kind: navigation.MenubarCheck, Items: []navigation.MenubarItem{}}}}}, } { t.Run(name, func(t *testing.T) { defer func() { @@ -267,8 +270,8 @@ func TestMenubarInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) - Menubar(c, menus, MenubarOptions{}) + core.Use(c, core.Settings{}) + navigation.Menubar(c, menus, navigation.MenubarOptions{}) }, 300, 100, 1) }) } @@ -278,7 +281,7 @@ func TestMenubarSnapshot(t *testing.T) { a := &menubarApp{wrap: true, zoom: "zoom-fit"} snapshot(t, "menubar", 480, 100, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { - Menubar(c, a.menus(), MenubarOptions{}) + navigation.Menubar(c, a.menus(), navigation.MenubarOptions{}) }) }) } diff --git a/multi_select.go b/input/multi_select.go similarity index 72% rename from multi_select.go rename to input/multi_select.go index 7bd4af6..291949c 100644 --- a/multi_select.go +++ b/input/multi_select.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MultiSelectOptions configure a MultiSelect. @@ -29,11 +30,11 @@ type MultiSelectOptions struct { type multiSelectKey struct{ name string } // MultiSelect chooses several *values from options in a popup with search and "Select all". -func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOption[T], opts MultiSelectOptions) ChoiceResult { +func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOption[T], opts MultiSelectOptions) core.ChoiceResult { if opts.Max < 0 || opts.MaxTags < 0 { panic("mujica: MultiSelect Max and MaxTags must not be negative") } - k := Tokens(c) + k := core.Tokens(c) before := slices.Clone(*values) tr := ui.ButtonBase(c).Disabled(opts.Disabled).Role(ui.RolePopUpButton) if opts.Label != "" { @@ -51,11 +52,11 @@ func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOptio if n := len(*values); n > 0 && tr.Shortcut(0, ui.KeyBackspace) && !tr.IsDisabled() { *values = slices.Clone((*values)[:n-1]) } - selectField(c, tr, *open, func() { - multiSelectTags(c, values, options, opts) + SelectField(c, tr, *open, func() { + MultiSelectTags(c, values, options, opts) }) ui.PopoverBase(c, tr, open, func(panel *ui.Element) { - selectPanel(c, panel) + SelectPanel(c, panel) panel.MinWidth(tr.Bounds().W).MaxWidth(max(tr.Bounds().W, 320)) multiSelectPopup(c, k, values, options, query, opts) }) @@ -63,14 +64,14 @@ func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOptio if changed { slog.Debug("mujica: multi select changed", "label", opts.Label, "count", len(*values)) } - return ChoiceResult{tr, changed} + return core.NewChoiceResult(tr, changed) } -// multiSelectTags shows the chosen values as removable tags, collapsing the rest into "+N". -func multiSelectTags[T comparable](c *ui.Context, values *[]T, options []SelectOption[T], opts MultiSelectOptions) { - k := Tokens(c) +// MultiSelectTags shows the chosen values as removable tags, collapsing the rest into "+N". +func MultiSelectTags[T comparable](c *ui.Context, values *[]T, options []SelectOption[T], opts MultiSelectOptions) { + k := core.Tokens(c) if len(*values) == 0 { - ui.Text(c, selectOr(opts.Placeholder, msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) + ui.Text(c, selectOr(opts.Placeholder, core.Msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) return } limit := opts.MaxTags @@ -82,7 +83,7 @@ func multiSelectTags[T comparable](c *ui.Context, values *[]T, options []SelectO if i == limit { break } - label, _ := selectLabel(options, v) + label, _ := SelectLabel(options, v) ui.Row(c).Key(v).Shrink(0).Children(func() { if multiSelectTag(c, label, opts.Disabled) { *values = slices.Delete(slices.Clone(*values), i, i+1) @@ -90,27 +91,27 @@ func multiSelectTags[T comparable](c *ui.Context, values *[]T, options []SelectO }) } if n := len(*values) - limit; n > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "multi_select.more"), n)).TextColor(k.TextMuted).SingleLine().Shrink(0) + ui.Text(c, fmt.Sprintf(core.Msg(c, "multi_select.more"), n)).TextColor(k.TextMuted).SingleLine().Shrink(0) } }) } // multiSelectTag draws a chosen value with a remove cross outside the Tab order; it reports a removal. func multiSelectTag(c *ui.Context, label string, disabled bool) bool { - k := Tokens(c) + k := core.Tokens(c) removed := false ui.Row(c).Gap(2).Padding(1, 2, 1, 6).Radius(theme.ControlRadius).Background(k.Selection). Border(theme.BorderWidth, k.Border).Shrink(0).Children(func() { - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() if !disabled { - removed = selectClear(c, k, fmt.Sprintf(msg(c, "multi_select.remove"), label)) + removed = selectClear(c, k, fmt.Sprintf(core.Msg(c, "multi_select.remove"), label)) } }) return removed } func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, options []SelectOption[T], query *string, opts MultiSelectOptions) { - d := Density(c) + d := core.Density(c) var shown []SelectOption[T] q := strings.ToLower(strings.TrimSpace(*query)) for _, o := range options { @@ -121,10 +122,10 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, full := opts.Max > 0 && len(*values) >= opts.Max into := false ui.Column(c).Gap(4).Children(func() { - search := selectSearch(c, query, msg(c, "multi_select.search")) + search := SelectSearch(c, query, core.Msg(c, "multi_select.search")) into = search.Shortcut(0, ui.KeyDown) if len(shown) == 0 { - selectNote(c, msg(c, "multi_select.no_results")) + SelectNote(c, core.Msg(c, "multi_select.no_results")) return } list := ui.Column(c).FocusGroup(ui.Vertical) @@ -143,12 +144,12 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, } tick := state == internal.Checked ui.Column(c).PaddingX(4).Children(func() { - b := multiSelectCheck(c, k, &tick, state, msg(c, "multi_select.all"), opts.Max > 0 && !some && full) + b := MultiSelectCheck(c, k, &tick, state, core.Msg(c, "multi_select.all"), opts.Max > 0 && !some && full) if into { b.Focus() } if b.Changed() { - *values = multiSelectAll(*values, shown, tick, opts.Max) + *values = MultiSelectAll(*values, shown, tick, opts.Max) } }) ui.Box(c).Height(1).Background(k.Border).MarginY(2) @@ -161,24 +162,24 @@ func multiSelectPopup[T comparable](c *ui.Context, k theme.Tokens, values *[]T, } row := ui.Row(c).Key(o.Value) row.Children(func() { - cb := multiSelectCheck(c, k, &on, st, o.Label, o.Disabled || (full && !on)) + cb := MultiSelectCheck(c, k, &on, st, o.Label, o.Disabled || (full && !on)) if cb.Changed() { - *values = multiSelectSet(*values, o.Value, on) + *values = MultiSelectSet(*values, o.Value, on) } }) } }) }) if opts.Max > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "multi_select.limit"), len(*values), opts.Max)). - FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(2, d.Unit()*2) + ui.Text(c, fmt.Sprintf(core.Msg(c, "multi_select.limit"), len(*values), opts.Max)). + FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(2, d.Unit()*2) } }) } -// multiSelectCheck is a CheckboxBase row drawing its own box. -func multiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.CheckState, label string, disabled bool) *ui.Element { - d := Density(c) +// MultiSelectCheck is a CheckboxBase row drawing its own box. +func MultiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.CheckState, label string, disabled bool) *ui.Element { + d := core.Density(c) var cb *ui.Element // A disabled wrapper blocks the base, which toggles as it is built. ui.Row(c).Grow(1).Disabled(disabled).Children(func() { @@ -187,7 +188,7 @@ func multiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.Check }) if st == internal.Mixed { // MyGo sets the mixed state only inside its own widgets (ui/element.go:282). - cb.Description(msg(c, "multi_select.mixed")) + cb.Description(core.Msg(c, "multi_select.mixed")) } internal.Ring(cb, k.Focus, theme.ControlRadius) hover := cb.Hovered() @@ -203,16 +204,16 @@ func multiSelectCheck(c *ui.Context, k theme.Tokens, on *bool, st internal.Check return cb } -// multiSelectSet adds or removes v. -func multiSelectSet[T comparable](values []T, v T, on bool) []T { +// MultiSelectSet adds or removes v. +func MultiSelectSet[T comparable](values []T, v T, on bool) []T { if on { return append(slices.Clone(values), v) } return slices.DeleteFunc(slices.Clone(values), func(x T) bool { return x == v }) } -// multiSelectAll checks every enabled shown option up to max, or unchecks them all. -func multiSelectAll[T comparable](values []T, shown []SelectOption[T], on bool, limit int) []T { +// MultiSelectAll checks every enabled shown option up to max, or unchecks them all. +func MultiSelectAll[T comparable](values []T, shown []SelectOption[T], on bool, limit int) []T { for _, o := range shown { if o.Disabled || slices.Contains(values, o.Value) == on { continue @@ -220,15 +221,15 @@ func multiSelectAll[T comparable](values []T, shown []SelectOption[T], on bool, if on && limit > 0 && len(values) >= limit { break } - values = multiSelectSet(values, o.Value, on) + values = MultiSelectSet(values, o.Value, on) } return values } -// selectSearch is the search field at the top of a popup; it takes the focus as the popup opens. -func selectSearch(c *ui.Context, query *string, placeholder string) *ui.Element { - k := Tokens(c) - d := Density(c) +// SelectSearch is the search field at the top of a popup; it takes the focus as the popup opens. +func SelectSearch(c *ui.Context, query *string, placeholder string) *ui.Element { + k := core.Tokens(c) + d := core.Density(c) f := ui.Row(c).Gap(d.Unit()*2).Padding(0, d.Unit()*2).MinHeight(d.ControlHeight()-4).AlignItems(ui.Center). Radius(theme.ControlRadius).Background(k.Background).Border(theme.BorderWidth, k.ControlBorder) var in *ui.Element diff --git a/multi_select_test.go b/input/multi_select_test.go similarity index 97% rename from multi_select_test.go rename to input/multi_select_test.go index db8a572..1d4da3f 100644 --- a/multi_select_test.go +++ b/input/multi_select_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var multiSelectTestOptions = []SelectOption[int]{ @@ -17,7 +19,7 @@ var multiSelectTestOptions = []SelectOption[int]{ func multiSelectTester(values *[]int, opts MultiSelectOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if MultiSelect(c, values, multiSelectTestOptions, opts).Changed() { *changes++ diff --git a/number_input.go b/input/number_input.go similarity index 70% rename from number_input.go rename to input/number_input.go index d9d7e81..5dc17a5 100644 --- a/number_input.go +++ b/input/number_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -9,9 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // NumberInputOptions configure a NumberInput. @@ -34,43 +35,43 @@ type NumberInputOptions struct { Error string } -// numberInputSpec is NumberInputOptions resolved. -type numberInputSpec struct { +// NumberInputSpec is NumberInputOptions resolved. +type NumberInputSpec struct { // lo and hi are the bounds on the precision grid; min and max the original ones. - lo, hi, min, max, step float64 - decimals int + Lo, Hi, min, max, Step float64 + Decimals int } -func (s numberInputSpec) format(v float64) string { +func (s NumberInputSpec) format(v float64) string { if math.IsNaN(v) { return "" } - return strconv.FormatFloat(v, 'f', s.decimals, 64) + return strconv.FormatFloat(v, 'f', s.Decimals, 64) } -// round rounds v to the precision. -func (s numberInputSpec) round(v float64) float64 { - p := math.Pow(10, float64(s.decimals)) +// Round rounds v to the precision. +func (s NumberInputSpec) Round(v float64) float64 { + p := math.Pow(10, float64(s.Decimals)) return math.Round(v*p)/p + 0 // + 0 turns -0 into 0 } // clamp rounds v to the precision, then bounds it; the bounds sit on that grid. -func (s numberInputSpec) clamp(v float64) float64 { - return math.Max(s.lo, math.Min(s.hi, s.round(v))) +func (s NumberInputSpec) clamp(v float64) float64 { + return math.Max(s.Lo, math.Min(s.Hi, s.Round(v))) } -// stepBy moves v n grid steps from Min, exactly checked against the original bounds. -func (s numberInputSpec) stepBy(v float64, n int) float64 { +// StepBy moves v n grid steps from Min, exactly checked against the original bounds. +func (s NumberInputSpec) StepBy(v float64, n int) float64 { if math.IsNaN(v) { // From empty, the first step lands next to zero. v = s.clamp(0) } - base := s.lo + base := s.Lo if math.IsInf(base, 0) { base = 0 } - at := func(k float64) float64 { return s.round(base + k*s.step) } - k := math.Round((v - base) / s.step) + at := func(k float64) float64 { return s.Round(base + k*s.Step) } + k := math.Round((v - base) / s.Step) // Off the grid, the nearest point in the direction is the first step. if n > 0 && at(k) <= v || n < 0 && at(k) >= v { k += float64(n) @@ -91,38 +92,38 @@ func (s numberInputSpec) stepBy(v float64, n int) float64 { // numberInputSame reports equal values, counting NaN (empty) as equal. func numberInputSame(a, b float64) bool { return a == b || math.IsNaN(a) && math.IsNaN(b) } -func numberInputResolve(opts NumberInputOptions) numberInputSpec { - s := numberInputSpec{lo: opts.Min, hi: opts.Max, step: opts.Step, decimals: opts.Precision} - if s.lo > s.hi { +func NumberInputResolve(opts NumberInputOptions) NumberInputSpec { + s := NumberInputSpec{Lo: opts.Min, Hi: opts.Max, Step: opts.Step, Decimals: opts.Precision} + if s.Lo > s.Hi { panic("mujica: NumberInput Min is above Max") } - if s.step < 0 || opts.Precision < 0 { + if s.Step < 0 || opts.Precision < 0 { panic("mujica: NumberInput Step and Precision cannot be negative") } - if s.lo == s.hi { + if s.Lo == s.Hi { if opts.StepperOnly { panic("mujica: NumberInput StepperOnly needs Min < Max") } - s.lo, s.hi = math.Inf(-1), math.Inf(1) + s.Lo, s.Hi = math.Inf(-1), math.Inf(1) } - if s.step == 0 { - s.step = 1 + if s.Step == 0 { + s.Step = 1 } - if s.decimals == 0 { - if t := strconv.FormatFloat(s.step, 'f', -1, 64); strings.Contains(t, ".") { - s.decimals = len(t) - strings.IndexByte(t, '.') - 1 + if s.Decimals == 0 { + if t := strconv.FormatFloat(s.Step, 'f', -1, 64); strings.Contains(t, ".") { + s.Decimals = len(t) - strings.IndexByte(t, '.') - 1 } } // Bounds move inward onto the precision grid, compared exactly with the originals. - s.min, s.max = s.lo, s.hi - p := math.Pow(10, float64(s.decimals)) - if s.lo = s.round(s.lo); s.lo < s.min { - s.lo = s.round(s.lo + 1/p) + s.min, s.max = s.Lo, s.Hi + p := math.Pow(10, float64(s.Decimals)) + if s.Lo = s.Round(s.Lo); s.Lo < s.min { + s.Lo = s.Round(s.Lo + 1/p) } - if s.hi = s.round(s.hi); s.hi > s.max { - s.hi = s.round(s.hi - 1/p) + if s.Hi = s.Round(s.Hi); s.Hi > s.max { + s.Hi = s.Round(s.Hi - 1/p) } - if s.lo > s.hi { + if s.Lo > s.Hi { panic("mujica: NumberInput has no value in range at this precision") } return s @@ -139,7 +140,7 @@ func NumberInput(c *ui.Context, value *float64, opts NumberInputOptions) InputRe if math.IsNaN(*value) && !opts.AllowEmpty { panic("mujica: NumberInput value is NaN without AllowEmpty") } - s := numberInputResolve(opts) + s := NumberInputResolve(opts) if opts.StepperOnly { return numberInputStepper(c, value, s, opts) } @@ -153,7 +154,7 @@ func NumberInput(c *ui.Context, value *float64, opts NumberInputOptions) InputRe r.changed = true } } - stepBy := func(n int) { set(s.stepBy(*value, n)) } + stepBy := func(n int) { set(s.StepBy(*value, n)) } look := &textInputLook{disabled: opts.Disabled, err: opts.Error} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { text := ui.Local(frame, "text", func() string { return s.format(*value) }) @@ -163,7 +164,7 @@ func NumberInput(c *ui.Context, value *float64, opts NumberInputOptions) InputRe // Set from outside: the text follows, and a typing error goes. *text, *invalid = s.format(*value), "" } - in := textInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 12). + in := TextInputEditor(c, frame, text, look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 12). Placeholder(opts.Placeholder).Label(opts.Label) if in.Changed() && !look.disabled { *invalid = numberInputParse(c, s, *text, opts.AllowEmpty, set) @@ -188,10 +189,10 @@ func NumberInput(c *ui.Context, value *float64, opts NumberInputOptions) InputRe delta int can bool }{ - {msg(c, "number_input.decrease"), "minus", -1, math.IsNaN(*value) || *value > s.lo}, - {msg(c, "number_input.increase"), "plus", 1, math.IsNaN(*value) || *value < s.hi}, + {core.Msg(c, "number_input.decrease"), "minus", -1, math.IsNaN(*value) || *value > s.Lo}, + {core.Msg(c, "number_input.increase"), "plus", 1, math.IsNaN(*value) || *value < s.Hi}, } { - if numberInputButton(c, b.icon, b.name, b.can) { + if NumberInputButton(c, b.icon, b.name, b.can) { stepBy(b.delta) *text, *invalid = s.format(*value), "" in.Focus() @@ -209,7 +210,7 @@ func NumberInput(c *ui.Context, value *float64, opts NumberInputOptions) InputRe } // numberInputParse applies typed text and returns why it is invalid, if it is. -func numberInputParse(c *ui.Context, s numberInputSpec, text string, allowEmpty bool, set func(float64)) string { +func numberInputParse(c *ui.Context, s NumberInputSpec, text string, allowEmpty bool, set func(float64)) string { t := strings.TrimSpace(text) if t == "" && allowEmpty { set(math.NaN()) @@ -217,18 +218,18 @@ func numberInputParse(c *ui.Context, s numberInputSpec, text string, allowEmpty } v, err := strconv.ParseFloat(t, 64) if err != nil || math.IsNaN(v) || math.IsInf(v, 0) { - return msg(c, "number_input.invalid") + return core.Msg(c, "number_input.invalid") } - if v < s.lo || v > s.hi { - return fmt.Sprintf(msg(c, "number_input.range"), s.format(s.lo), s.format(s.hi)) + if v < s.Lo || v > s.Hi { + return fmt.Sprintf(core.Msg(c, "number_input.range"), s.format(s.Lo), s.format(s.Hi)) } set(v) return "" } -// numberInputButton builds a pointer-only step button; Up and Down are its keys. -func numberInputButton(c *ui.Context, icon, name string, can bool) bool { - k := Tokens(c) +// NumberInputButton builds a pointer-only step button; Up and Down are its keys. +func NumberInputButton(c *ui.Context, icon, name string, can bool) bool { + k := core.Tokens(c) b := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Center().Radius(theme.ControlRadius). Role(ui.RoleButton).Label(name).Cursor(ui.CursorPointer).Disabled(!can) fg := k.TextMuted @@ -244,9 +245,9 @@ func numberInputButton(c *ui.Context, icon, name string, can bool) bool { } // numberInputStepper shows the value beside up and down arrows; it steps itself. -func numberInputStepper(c *ui.Context, value *float64, s numberInputSpec, opts NumberInputOptions) InputResult { - k := Tokens(c) - d := Density(c) +func numberInputStepper(c *ui.Context, value *float64, s NumberInputSpec, opts NumberInputOptions) InputResult { + k := core.Tokens(c) + d := core.Density(c) var r InputResult r.Element = ui.Column(c).Gap(d.Unit()).Shrink(0).Disabled(opts.Disabled) disabled := r.Element.IsDisabled() @@ -269,28 +270,28 @@ func numberInputStepper(c *ui.Context, value *float64, s numberInputSpec, opts N internal.Ring(st, k.Focus, theme.ControlRadius) switch { case st.Shortcut(0, ui.KeyUp), st.Shortcut(0, ui.KeyRight): - set(s.stepBy(*value, 1)) + set(s.StepBy(*value, 1)) case st.Shortcut(0, ui.KeyDown), st.Shortcut(0, ui.KeyLeft): - set(s.stepBy(*value, -1)) + set(s.StepBy(*value, -1)) case st.Shortcut(0, ui.KeyHome): - set(s.lo) + set(s.Lo) case st.Shortcut(0, ui.KeyEnd): - set(s.hi) + set(s.Hi) } st.Children(func() { - if numberInputArrow(c, "chevron-up", *value < s.hi, false) { - set(s.stepBy(*value, 1)) + if numberInputArrow(c, "chevron-up", *value < s.Hi, false) { + set(s.StepBy(*value, 1)) st.Focus() } - if numberInputArrow(c, "chevron-down", *value > s.lo, true) { - set(s.stepBy(*value, -1)) + if numberInputArrow(c, "chevron-down", *value > s.Lo, true) { + set(s.StepBy(*value, -1)) st.Focus() } }) r.Input = st }) if opts.Error != "" { - ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, opts.Error).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)) } }) if r.changed { @@ -301,8 +302,8 @@ func numberInputStepper(c *ui.Context, value *float64, s numberInputSpec, opts N // numberInputArrow builds one half of the stepper and reports a click. func numberInputArrow(c *ui.Context, icon string, can, lower bool) bool { - k := Tokens(c) - h := Density(c).ControlHeight()/2 - theme.BorderWidth + k := core.Tokens(c) + h := core.Density(c).ControlHeight()/2 - theme.BorderWidth a := ui.Box(c).Height(h).Center().Role(ui.RoleNone).Cursor(ui.CursorPointer).Disabled(!can) if lower { a.BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border) diff --git a/number_input_test.go b/input/number_input_test.go similarity index 97% rename from number_input_test.go rename to input/number_input_test.go index 1109778..89599f6 100644 --- a/number_input_test.go +++ b/input/number_input_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func numberInputTester(v *float64, changes *int, opts NumberInputOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if NumberInput(c, v, opts).Changed() { *changes++ } @@ -124,7 +126,7 @@ func TestNumberInputRangeOnPrecisionGrid(t *testing.T) { func TestNumberInputDisabledWhileFocused(t *testing.T) { v, n, off, focused := 5.0, 0, false, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := NumberInput(c, &v, NumberInputOptions{Label: "Live", Disabled: off}) if r.Changed() { n++ @@ -194,7 +196,7 @@ func TestNumberInputBadOptionsPanic(t *testing.T) { }() v := 0.0 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NumberInput(c, &v, opts) }, 200, 100) }() diff --git a/password_input.go b/input/password_input.go similarity index 78% rename from password_input.go rename to input/password_input.go index 2c2e585..c9cd991 100644 --- a/password_input.go +++ b/input/password_input.go @@ -1,11 +1,12 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) // PasswordInputOptions configure a PasswordInput. @@ -34,7 +35,7 @@ func PasswordInput(c *ui.Context, value *string, opts PasswordInputOptions) Inpu mode = "shown" } ui.Row(c).Key(mode).Grow(1).Children(func() { - in = textInputEditor(c, frame, value, look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 10). + in = TextInputEditor(c, frame, value, look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 10). Placeholder(opts.Placeholder).Label(opts.Label) if !*shown { in.Password() @@ -53,9 +54,9 @@ func PasswordInput(c *ui.Context, value *string, opts PasswordInputOptions) Inpu // passwordInputToggle builds the show or hide button and reports a click. func passwordInputToggle(c *ui.Context, shown bool) bool { - name, icon := msg(c, "password_input.show"), "eye" + name, icon := core.Msg(c, "password_input.show"), "eye" if shown { - name, icon = msg(c, "password_input.hide"), "eye-off" + name, icon = core.Msg(c, "password_input.hide"), "eye-off" } - return iconAction(c, icons.Must(icon), name).Tooltip(name).Clicked() + return core.IconAction(c, icons.Must(icon), name).Tooltip(name).Clicked() } diff --git a/password_input_test.go b/input/password_input_test.go similarity index 94% rename from password_input_test.go rename to input/password_input_test.go index 8557a7e..117898d 100644 --- a/password_input_test.go +++ b/input/password_input_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPasswordInputMaskPasteAndReveal(t *testing.T) { pw, changes, submits := "", 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := PasswordInput(c, &pw, PasswordInputOptions{Label: "Password"}) if r.Changed() { changes++ @@ -61,7 +63,7 @@ func TestPasswordInputMaskPasteAndReveal(t *testing.T) { func TestPasswordInputToggleByKeyboard(t *testing.T) { pw := "x" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PasswordInput(c, &pw, PasswordInputOptions{Label: "PIN"}) }, 400, 200) tt.Key(0, ui.KeyTab) @@ -75,7 +77,7 @@ func TestPasswordInputToggleByKeyboard(t *testing.T) { func TestPasswordInputDisabled(t *testing.T) { pw := "locked" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) PasswordInput(c, &pw, PasswordInputOptions{Label: "Off", Disabled: true}) }, 400, 200) _ = tt.Click("Show password") diff --git a/pin_input.go b/input/pin_input.go similarity index 89% rename from pin_input.go rename to input/pin_input.go index 00cc6eb..c88ca78 100644 --- a/pin_input.go +++ b/input/pin_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PinInputOptions configure a PinInput. Length is 6 when zero. @@ -54,11 +55,11 @@ func PinInput(c *ui.Context, value *string, opts PinInputOptions) PinInputParts if n < 0 || len(*value) > n || strings.Trim(*value, "0123456789") != "" { panic(fmt.Sprintf("mujica: PinInput value %q does not fit %d digits", *value, n)) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "pin_input.label") + label = core.Msg(c, "pin_input.label") } parts := PinInputParts{} e := ui.Row(c).Gap(d.Unit() * 2).Shrink(0).Role(ui.RoleTextField).Label(label).Disabled(opts.Disabled) @@ -124,7 +125,7 @@ func PinInput(c *ui.Context, value *string, opts PinInputOptions) PinInputParts active := min(len(*value), n-1) focused := e.Focused() compose := st.compose - e.Description(fmt.Sprintf(msg(c, "pin_input.filled"), len(*value), n)) + e.Description(fmt.Sprintf(core.Msg(c, "pin_input.filled"), len(*value), n)) e.TextCaret(ui.Rect{X: float32(active) * (size + d.Unit()*2), Y: 0, W: size, H: size}) e.Children(func() { for i := range n { @@ -145,9 +146,9 @@ func PinInput(c *ui.Context, value *string, opts PinInputOptions) PinInputParts cell.Children(func() { switch { case showCompose: - ui.Text(c, compose).FontSize(FontSize(c, theme.H3Size)).Underline().SingleLine() + ui.Text(c, compose).FontSize(core.FontSize(c, theme.H3Size)).Underline().SingleLine() case ch != "": - ui.Text(c, ch).FontSize(FontSize(c, theme.H3Size)).FontFeatures("tnum").SingleLine() + ui.Text(c, ch).FontSize(core.FontSize(c, theme.H3Size)).FontFeatures("tnum").SingleLine() } }) caret := focused && i == active && len(*value) < n && !showCompose diff --git a/pin_input_test.go b/input/pin_input_test.go similarity index 96% rename from pin_input_test.go rename to input/pin_input_test.go index 9fc79f5..41fb74e 100644 --- a/pin_input_test.go +++ b/input/pin_input_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type pinInputProbe struct { @@ -13,7 +15,7 @@ type pinInputProbe struct { func (p *pinInputProbe) tester(opts PinInputOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := PinInput(c, &p.value, opts) if r.Changed() { p.changes++ @@ -107,7 +109,7 @@ func TestPinInputDisabledAfterFocus(t *testing.T) { p := &pinInputProbe{value: "12"} opts := PinInputOptions{Label: "Code"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := PinInput(c, &p.value, opts) if r.Changed() { p.changes++ diff --git a/qr_code.go b/input/qr_code.go similarity index 82% rename from qr_code.go rename to input/qr_code.go index 09a90b6..4a354c0 100644 --- a/qr_code.go +++ b/input/qr_code.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QRCodeOptions configure a QRCode. @@ -38,8 +39,8 @@ func QRCode(c *ui.Context, text string, opts QRCodeOptions) *ui.Element { if size == 0 { size = 160 } - k := Tokens(c) - e := ui.Box(c).Size(size, size).Shrink(0).Role(ui.RoleImage).Label(cmp.Or(opts.Label, msg(c, "qr_code.label"))) + k := core.Tokens(c) + e := ui.Box(c).Size(size, size).Shrink(0).Role(ui.RoleImage).Label(cmp.Or(opts.Label, core.Msg(c, "qr_code.label"))) cache := ui.Local(e, "qr_code", func() qrCodeCache { return qrCodeCache{} }) if !cache.done || cache.text != text || cache.level != opts.Level { m, err := EncodeQR([]byte(text), opts.Level) @@ -47,10 +48,10 @@ func QRCode(c *ui.Context, text string, opts QRCodeOptions) *ui.Element { slog.Debug("mujica: qr_code encoded", "version", m.Version, "level", opts.Level, "err", err) } if cache.err != nil { - e.Center().Padding(Density(c).Unit()*2).Background(k.Surface).Border(theme.BorderWidth, k.Danger).Radius(theme.CardRadius). + e.Center().Padding(core.Density(c).Unit()*2).Background(k.Surface).Border(theme.BorderWidth, k.Danger).Radius(theme.CardRadius). Description(cache.err.Error()) e.Children(func() { - ui.Text(c, msg(c, "qr_code.too_long")).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).TextAlign(ui.Center) + ui.Text(c, core.Msg(c, "qr_code.too_long")).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).TextAlign(ui.Center) }) return e } diff --git a/qr_code_encoder.go b/input/qr_code_encoder.go similarity index 99% rename from qr_code_encoder.go rename to input/qr_code_encoder.go index 4408273..ea7efaf 100644 --- a/qr_code_encoder.go +++ b/input/qr_code_encoder.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" diff --git a/qr_code_encoder_test.go b/input/qr_code_encoder_test.go similarity index 99% rename from qr_code_encoder_test.go rename to input/qr_code_encoder_test.go index 49f0a26..9a57d0b 100644 --- a/qr_code_encoder_test.go +++ b/input/qr_code_encoder_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "encoding/hex" diff --git a/qr_code_grid.go b/input/qr_code_grid.go similarity index 99% rename from qr_code_grid.go rename to input/qr_code_grid.go index 39dbade..b82c08b 100644 --- a/qr_code_grid.go +++ b/input/qr_code_grid.go @@ -1,4 +1,4 @@ -package mujica +package input // qrCodeGrid is a symbol under construction; fixed marks function modules. type qrCodeGrid struct { diff --git a/qr_code_test.go b/input/qr_code_test.go similarity index 87% rename from qr_code_test.go rename to input/qr_code_test.go index 19bf83c..0ddfb94 100644 --- a/qr_code_test.go +++ b/input/qr_code_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "strings" @@ -6,14 +6,15 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestQRCodeDrawsModulesAndCaches(t *testing.T) { text := "HELLO" var box ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Dark}) + core.Use(c, core.Settings{Mode: core.Dark}) ui.Row(c).Children(func() { box = QRCode(c, text, QRCodeOptions{Level: QRLow, Size: 116}).Bounds() }) }, 200, 200) tt.Frame() @@ -42,7 +43,7 @@ func TestQRCodeDrawsModulesAndCaches(t *testing.T) { func TestQRCodeTooLong(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) QRCode(c, strings.Repeat("x", 1300), QRCodeOptions{Level: QRHigh}) }, 300, 300) if !tt.HasText("Text too long for a QR code") { @@ -58,7 +59,7 @@ func TestQRCodeRejectsBadOptions(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); QRCode(c, "x", o) }, 200, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); QRCode(c, "x", o) }, 200, 200) }() } } diff --git a/radio_group.go b/input/radio_group.go similarity index 89% rename from radio_group.go rename to input/radio_group.go index 0cfca88..a2f0a0d 100644 --- a/radio_group.go +++ b/input/radio_group.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RadioItem is one choice of a RadioGroup; Value is its stable identity. @@ -27,7 +28,7 @@ type RadioGroupOptions struct { } // RadioGroup chooses one item's Value into *value; arrows move the choice. -func RadioGroup[T comparable](c *ui.Context, value *T, items []RadioItem[T], opts RadioGroupOptions) ChoiceResult { +func RadioGroup[T comparable](c *ui.Context, value *T, items []RadioItem[T], opts RadioGroupOptions) core.ChoiceResult { if value == nil { panic("mujica: RadioGroup needs a value") } @@ -41,9 +42,9 @@ func RadioGroup[T comparable](c *ui.Context, value *T, items []RadioItem[T], opt } seen[it.Value] = true } - k := Tokens(c) - d := Density(c) - var r ChoiceResult + k := core.Tokens(c) + d := core.Density(c) + var r core.ChoiceResult // Disabled on a wrapper blocks the bases' clicks before they choose. wrap := ui.Box(c).Shrink(0).Disabled(opts.Disabled) // Keys by first appearance of each value: fmt keys would merge equal-looking values. @@ -52,7 +53,7 @@ func RadioGroup[T comparable](c *ui.Context, value *T, items []RadioItem[T], opt r.Element = ui.RadioGroup(c, func() { for _, it := range items { if radioGroupItem(c, k, d, value, it, stableKey(known, it.Value)) { - r.changed = true + r = core.NewChoiceResult(r.Element, true) slog.Debug("mujica: radio_group choose", "label", it.Label) } } @@ -84,7 +85,7 @@ func radioGroupItem[T comparable](c *ui.Context, k theme.Tokens, d theme.Density bg, border := checkboxColors(k, on, hover, pressed) dot := ui.Box(c).Size(checkboxSize, checkboxSize).Shrink(0).Radius(checkboxSize/2). Background(bg).Border(theme.BorderWidth, border) - dot.Transition(colorFade) + dot.Transition(core.ColorFade) dot.DrawOver(func(p *ui.Painter, r ui.Rect) { if on { s := r.W * 0.375 diff --git a/radio_group_test.go b/input/radio_group_test.go similarity index 95% rename from radio_group_test.go rename to input/radio_group_test.go index e4a2099..1765ccb 100644 --- a/radio_group_test.go +++ b/input/radio_group_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var radioGroupTestItems = []RadioItem[string]{ @@ -14,7 +16,7 @@ var radioGroupTestItems = []RadioItem[string]{ func radioGroupTester(v *string, changes *int, opts RadioGroupOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if RadioGroup(c, v, radioGroupTestItems, opts).Changed() { *changes++ } @@ -83,7 +85,7 @@ func TestRadioGroupDuplicateValuePanics(t *testing.T) { }() v := 1 ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RadioGroup(c, &v, []RadioItem[int]{{Value: 1, Label: "A"}, {Value: 1, Label: "B"}}, RadioGroupOptions{}) }, 200, 100) } diff --git a/range_slider.go b/input/range_slider.go similarity index 74% rename from range_slider.go rename to input/range_slider.go index 7a3b088..2d70a24 100644 --- a/range_slider.go +++ b/input/range_slider.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RangeSliderOptions configure a RangeSlider. Min and Max both zero mean 0 to 100. @@ -23,11 +24,11 @@ type RangeSliderOptions struct { } // RangeSlider sets *low and *high, which never cross; a press moves the nearer thumb, each takes the keys. -func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) ChoiceResult { - g, h := sliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) +func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) core.ChoiceResult { + g, h := SliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) format := sliderFormat(opts.Format, g) - size := FontSize(c, theme.CaptionSize) - k := Tokens(c) + size := core.FontSize(c, theme.CaptionSize) + k := core.Tokens(c) *low, *high = g.snap(math.Min(*low, *high)), g.snap(math.Max(*low, *high)) oldLow, oldHigh := *low, *high e := ui.Box(c).Height(h).MinWidth(120).Shrink(0).Role(ui.RoleGroup).Disabled(opts.Disabled).Cursor(ui.CursorPointer) @@ -37,7 +38,7 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho drag := ui.Local(e, rangeSliderDrag{}, func() int { return -1 }) px, _, _ := e.PointerPosition() r := e.Bounds() - at := g.snap(g.lo + float64((px-sliderThumb/2)/max(r.W-sliderThumb, 1))*(g.hi-g.lo)) + at := g.snap(g.lo + float64((px-SliderThumb/2)/max(r.W-SliderThumb, 1))*(g.hi-g.lo)) var knobs [2]*ui.Element pressed := false e.Children(func() { @@ -46,16 +47,16 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho if i == 1 { from, to = *low, g.hi } - x := g.x(ui.Rect{W: r.W}, *v) - sliderThumb/2 + x := g.X(ui.Rect{W: r.W}, *v) - SliderThumb/2 var kb *ui.Element var next float64 var held bool // Each thumb is a SliderBase, which reports the range and value to assistive technology. - ui.Box(c).Key(i).Absolute().Left(x).Top(g.trackY-sliderThumb/2).Size(sliderThumb, sliderThumb).Children(func() { - kb, next, held = sliderControl(c, *v, g, from, to) + ui.Box(c).Key(i).Absolute().Left(x).Top(g.TrackY-SliderThumb/2).Size(SliderThumb, SliderThumb).Children(func() { + kb, next, held = SliderControl(c, *v, g, from, to) }) - kb.Size(sliderThumb, sliderThumb).FocusRing(false) - name := msg(c, []string{"range_slider.min", "range_slider.max"}[i]) + kb.Size(SliderThumb, SliderThumb).FocusRing(false) + name := core.Msg(c, []string{"range_slider.min", "range_slider.max"}[i]) if opts.Label != "" { name = opts.Label + " " + name } @@ -95,17 +96,17 @@ func RangeSlider(c *ui.Context, low, high *float64, opts RangeSliderOptions) Cho lo, hi, which := *low, *high, *drag hover := e.Hovered() e.Draw(func(p *ui.Painter, r ui.Rect) { - x0, x1 := g.x(r, lo), g.x(r, hi) - sliderTrackPaint(p, r, g, k, opts.Marks, size, x0, x1) + x0, x1 := g.X(r, lo), g.X(r, hi) + SliderTrackPaint(p, r, g, k, opts.Marks, size, x0, x1) for i, x := range []float32{x0, x1} { hot := which == i || knobs[i].Hovered() - sliderThumbPaint(p, k, x, r.Y+g.trackY, hot, knobs[i].FocusVisible()) + SliderThumbPaint(p, k, x, r.Y+g.TrackY, hot, knobs[i].FocusVisible()) if opts.ShowValue && (hot || hover || knobs[i].Focused()) { - rangeSliderBubble(p, k, x, r.Y+g.trackY, format([]float64{lo, hi}[i]), i, size) + rangeSliderBubble(p, k, x, r.Y+g.TrackY, format([]float64{lo, hi}[i]), i, size) } } }) - return ChoiceResult{e, changed} + return core.NewChoiceResult(e, changed) } // rangeSliderDrag keys which thumb the pointer drags, -1 for none. @@ -114,7 +115,7 @@ type rangeSliderDrag struct{} // rangeSliderBubble keeps the two tips apart: the low one leans left, the high one right. func rangeSliderBubble(p *ui.Painter, k theme.Tokens, x, y float32, s string, i int, size float32) { w, _ := p.MeasureText(1000, ui.Span{Text: s, Size: size}) - shift := (w+12)/2 - sliderThumb/2 + shift := (w+12)/2 - SliderThumb/2 if i == 0 { shift = -shift } diff --git a/range_slider_test.go b/input/range_slider_test.go similarity index 96% rename from range_slider_test.go rename to input/range_slider_test.go index d82bbf6..4cc577d 100644 --- a/range_slider_test.go +++ b/input/range_slider_test.go @@ -1,14 +1,16 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func rangeTester(low, high *float64, opts RangeSliderOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(216).Children(func() { if RangeSlider(c, low, high, opts).Changed() { *changes++ diff --git a/rating.go b/input/rating.go similarity index 84% rename from rating.go rename to input/rating.go index 1f3dea4..200a1bf 100644 --- a/rating.go +++ b/input/rating.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RatingOptions configure a Rating. Max is 5 and Step 1 when zero. @@ -26,7 +27,7 @@ type RatingOptions struct { } // Rating shows *value as stars and lets the pointer and arrows set it in steps. -func Rating(c *ui.Context, value *float64, opts RatingOptions) ChoiceResult { +func Rating(c *ui.Context, value *float64, opts RatingOptions) core.ChoiceResult { n, step := opts.Max, opts.Step if n == 0 { n = 5 @@ -37,24 +38,24 @@ func Rating(c *ui.Context, value *float64, opts RatingOptions) ChoiceResult { if n < 0 || step < 0 || step > 1 || math.Abs(1/step-math.Round(1/step)) > 1e-9 { panic(fmt.Sprintf("mujica: Rating Max %d, Step %v: Max must be positive and Step divide 1", opts.Max, opts.Step)) } - k := Tokens(c) - size := Density(c).ControlHeight() * 0.6 + k := core.Tokens(c) + size := core.Density(c).ControlHeight() * 0.6 // Each star sits centered in a hit area of at least the minimum size. hit := max(size, theme.MinHitSize) label := opts.Label if label == "" { - label = msg(c, "rating.label") + label = core.Msg(c, "rating.label") } - parts := ChoiceResult{} + parts := core.ChoiceResult{} set := func(v float64) { v = math.Max(0, math.Min(float64(n), math.Round(v/step)*step)) if v != *value { slog.Debug("mujica: rating changed", "from", *value, "to", v) *value = v - parts.changed = true + parts = core.NewChoiceResult(parts.Element, true) } } - e := ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit() * 2).Shrink(0) + e := ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit() * 2).Shrink(0) parts.Element = e e.Label(label) e.Disabled(opts.Disabled) @@ -109,7 +110,7 @@ func Rating(c *ui.Context, value *float64, opts RatingOptions) ChoiceResult { TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() } }) - e.Description(fmt.Sprintf(msg(c, "rating.value"), strconv.FormatFloat(*value, 'f', -1, 64), n)) + e.Description(fmt.Sprintf(core.Msg(c, "rating.value"), strconv.FormatFloat(*value, 'f', -1, 64), n)) return parts } diff --git a/rating_test.go b/input/rating_test.go similarity index 93% rename from rating_test.go rename to input/rating_test.go index 57d90e5..dbe4570 100644 --- a/rating_test.go +++ b/input/rating_test.go @@ -1,16 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func ratingTester(v *float64, opts RatingOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).AlignItems(ui.Start).Children(func() { if Rating(c, v, opts).Changed() { *changes++ @@ -105,9 +106,9 @@ func TestRatingSnapshot(t *testing.T) { func TestRatingHitAreaCompact(t *testing.T) { v := 2.0 - var r ChoiceResult + var r core.ChoiceResult tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) ui.Column(c).AlignItems(ui.Start).Children(func() { r = Rating(c, &v, RatingOptions{}) }) diff --git a/resizable_panel_group.go b/input/resizable_panel_group.go similarity index 95% rename from resizable_panel_group.go rename to input/resizable_panel_group.go index 8e273b6..cd6a714 100644 --- a/resizable_panel_group.go +++ b/input/resizable_panel_group.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ResizablePanel is one panel of a ResizablePanelGroup. @@ -67,8 +68,8 @@ func ResizablePanelGroup(c *ui.Context, sizes *[]float32, panels []ResizablePane if math.Abs(float64(sum-1)) > 1e-3 { panic(fmt.Sprintf("mujica: ResizablePanelGroup sizes sum to %v, not 1", sum)) } - u := Density(c).Unit() - k := Tokens(c) + u := core.Density(c).Unit() + k := core.Tokens(c) var res ResizablePanelGroupResult box := ui.Row(c).AlignItems(ui.Stretch).Disabled(opts.Disabled) if opts.Vertical { @@ -128,7 +129,7 @@ func ResizablePanelGroup(c *ui.Context, sizes *[]float32, panels []ResizablePane func resizablePanelDivider(c *ui.Context, k theme.Tokens, u float32, st *resizablePanelState, sizes *[]float32, panels []ResizablePanel, mins []float32, avail float32, i int, vertical, disabled bool, res *ResizablePanelGroupResult) func() { s := *sizes div := ui.Box(c).Key(fmt.Sprint("div", i)).Shrink(0).Focusable().FocusRing(false).Role(ui.RoleSplitter). - Label(fmt.Sprintf(msg(c, "resizable_panel_group.divider"), panels[i].Label, panels[i+1].Label)) + Label(fmt.Sprintf(core.Msg(c, "resizable_panel_group.divider"), panels[i].Label, panels[i+1].Label)) if vertical { div.Height(theme.BorderWidth).FillWidth() } else { @@ -198,7 +199,7 @@ func resizablePanelDivider(c *ui.Context, k theme.Tokens, u float32, st *resizab if s[i] > 0 && s[i+1] > 0 { div.Description(fmt.Sprintf("%.0f%%", 100*s[i]/(s[i]+s[i+1]))) } else { - div.Description(msg(c, "split_pane.collapsed")) + div.Description(core.Msg(c, "split_pane.collapsed")) } return func() { grip := ui.Box(c).Key(fmt.Sprint("grip", i)).Absolute().Cursor(ui.CursorResizeEW) diff --git a/resizable_panel_group_test.go b/input/resizable_panel_group_test.go similarity index 96% rename from resizable_panel_group_test.go rename to input/resizable_panel_group_test.go index 6c56463..92d2160 100644 --- a/resizable_panel_group_test.go +++ b/input/resizable_panel_group_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func panelTester(sizes *[]float32, opts *ResizablePanelGroupOptions, commits *int) *ui.Tester { @@ -14,7 +16,7 @@ func panelTester(sizes *[]float32, opts *ResizablePanelGroupOptions, commits *in {Label: "Outline", Min: 80, Build: func() {}}, } return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := ResizablePanelGroup(c, sizes, panels, *opts) r.Element.Size(602, 300) if r.Committed { @@ -107,7 +109,7 @@ func TestResizablePanelGroupRejects(t *testing.T) { }() s := tc.sizes ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ResizablePanelGroup(c, &s, tc.panels, ResizablePanelGroupOptions{}) }, 300, 200) }() @@ -118,7 +120,7 @@ func TestResizablePanelGroupSnapshot(t *testing.T) { sizes := []float32{0.25, 0.5, 0.25} vs := []float32{0.6, 0.4} snapshot(t, "resizable_panel_group", 560, 260, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) pane := func(s string) func() { return func() { ui.Column(c).Fill().Padding(12).Background(k.Surface).Children(func() { ui.Text(c, s) }) diff --git a/scrub_input.go b/input/scrub_input.go similarity index 85% rename from scrub_input.go rename to input/scrub_input.go index 387bc67..d4866f4 100644 --- a/scrub_input.go +++ b/input/scrub_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ScrubInputOptions configure a ScrubInput; the range and step rules are NumberInput's. @@ -26,10 +27,10 @@ func ScrubInput(c *ui.Context, label string, value *float64, opts ScrubInputOpti panic("mujica: ScrubInput needs a label and a value") } nopts := NumberInputOptions{Min: opts.Min, Max: opts.Max, Step: opts.Step, Precision: opts.Precision, Label: label, Disabled: opts.Disabled} - s := numberInputResolve(nopts) - k := Tokens(c) + s := NumberInputResolve(nopts) + k := core.Tokens(c) var r InputResult - row := ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit() * 2).Disabled(opts.Disabled) + row := ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit() * 2).Disabled(opts.Disabled) row.Children(func() { h := ui.Box(c).Shrink(0).MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize).Center().Cursor(ui.CursorResizeEW).Role(ui.RoleNone) drag := ui.Local(h, "scrub_input.drag", func() float64 { return math.NaN() }) @@ -49,7 +50,7 @@ func ScrubInput(c *ui.Context, label string, value *float64, opts ScrubInputOpti // One grid step at a time, so a long drag stops at the bound. v := start for range max(n, -n) { - v = s.stepBy(v, max(-1, min(1, n))) + v = s.StepBy(v, max(-1, min(1, n))) } if v != *value { *value, scrubbed = v, true @@ -64,7 +65,7 @@ func ScrubInput(c *ui.Context, label string, value *float64, opts ScrubInputOpti ui.Text(c, label).TextColor(fg).Underline().DecorationColor(k.Border).SingleLine() }) r = NumberInput(c, value, nopts) - r.Element.Width(Density(c).Unit() * 36) + r.Element.Width(core.Density(c).Unit() * 36) r.changed = r.changed || scrubbed if scrubbed { slog.Debug("mujica: scrub_input scrubbed", "label", label, "value", *value) diff --git a/scrub_input_test.go b/input/scrub_input_test.go similarity index 95% rename from scrub_input_test.go rename to input/scrub_input_test.go index 2e9502b..6b89e63 100644 --- a/scrub_input_test.go +++ b/input/scrub_input_test.go @@ -1,14 +1,16 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func scrubTester(v *float64, opts *ScrubInputOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).Children(func() { if ScrubInput(c, "Opacity", v, *opts).Changed() { *n++ diff --git a/search_input.go b/input/search_input.go similarity index 77% rename from search_input.go rename to input/search_input.go index ead6c59..8e7eb5f 100644 --- a/search_input.go +++ b/input/search_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SearchInputOptions configure a SearchInput. @@ -46,12 +47,12 @@ func SearchInput(c *ui.Context, query *string, opts SearchInputOptions) SearchIn if opts.Matches < 0 { panic("mujica: SearchInput Matches is negative") } - k := Tokens(c) + k := core.Tokens(c) place := opts.Placeholder if place == "" { - place = msg(c, "search_input.search") + place = core.Msg(c, "search_input.search") if opts.Find { - place = msg(c, "search_input.find") + place = core.Msg(c, "search_input.find") } } name := opts.Label @@ -84,7 +85,7 @@ func SearchInput(c *ui.Context, query *string, opts SearchInputOptions) SearchIn } ui.Icon(c, icons.Must("search")).Size(16, 16).Shrink(0).TextColor(k.TextMuted) back := ui.Local(frame, "back", func() bool { return false }) - in := textInputEditor(c, frame, query, look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 10).Placeholder(place).Label(name) + in := TextInputEditor(c, frame, query, look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 10).Placeholder(place).Label(name) r.changed = in.Changed() && !look.disabled if opts.Find { // Shift+Enter would reach the editor as a plain submit. @@ -120,22 +121,22 @@ func SearchInput(c *ui.Context, query *string, opts SearchInputOptions) SearchIn // searchInputFindControls shows the match count with previous and next buttons. func searchInputFindControls(c *ui.Context, query string, matches, current int, step func(int)) { - k := Tokens(c) - status := msg(c, "search_input.none") + k := core.Tokens(c) + status := core.Msg(c, "search_input.none") switch { case query == "": status = "" case matches > 0: - status = fmt.Sprintf(msg(c, "search_input.count"), current+1, matches) + status = fmt.Sprintf(core.Msg(c, "search_input.count"), current+1, matches) } - ui.Text(c, status).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). + ui.Text(c, status).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). SingleLine().Shrink(0).Role(ui.RoleStatus) ui.Box(c).Width(theme.BorderWidth).Height(16).Background(k.Border).Shrink(0) opts := IconButtonOptions{Variant: Ghost, Disabled: matches == 0} - if IconButton(c, icons.Must("chevron-up"), msg(c, "search_input.previous"), opts).Size(theme.MinHitSize, theme.MinHitSize).MinHeight(theme.MinHitSize).Clicked() { + if IconButton(c, icons.Must("chevron-up"), core.Msg(c, "search_input.previous"), opts).Size(theme.MinHitSize, theme.MinHitSize).MinHeight(theme.MinHitSize).Clicked() { step(-1) } - if IconButton(c, icons.Must("chevron-down"), msg(c, "search_input.next"), opts).Size(theme.MinHitSize, theme.MinHitSize).MinHeight(theme.MinHitSize).Clicked() { + if IconButton(c, icons.Must("chevron-down"), core.Msg(c, "search_input.next"), opts).Size(theme.MinHitSize, theme.MinHitSize).MinHeight(theme.MinHitSize).Clicked() { step(1) } } diff --git a/search_input_test.go b/input/search_input_test.go similarity index 90% rename from search_input_test.go rename to input/search_input_test.go index dc6cfb3..cac9c99 100644 --- a/search_input_test.go +++ b/input/search_input_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "runtime" @@ -6,12 +6,14 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSearchInputSearchClearSubmit(t *testing.T) { q, changes, submits := "", 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SearchInput(c, &q, SearchInputOptions{}) if r.Changed() { changes++ @@ -38,10 +40,10 @@ func TestSearchInputSearchClearSubmit(t *testing.T) { } // searchInputFindTester counts matches of the query in a fixed text. -func searchInputFindTester(q *string, cur, moves *int, locale Locale) *ui.Tester { +func searchInputFindTester(q *string, cur, moves *int, locale core.Locale) *ui.Tester { const text = "rose lys rose crown rose" return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: locale}) + core.Use(c, core.Settings{Locale: locale}) n := 0 if *q != "" { n = strings.Count(text, *q) @@ -91,7 +93,7 @@ func TestSearchInputFindBarNavigation(t *testing.T) { func TestSearchInputFindNoMatches(t *testing.T) { q, cur, moves := "", 0, 0 - tt := searchInputFindTester(&q, &cur, &moves, ZhCN) + tt := searchInputFindTester(&q, &cur, &moves, core.ZhCN) _ = tt.Click("Find in page") tt.Type("tulip") if !tt.HasText("无匹配") { @@ -112,7 +114,7 @@ func TestSearchInputFindNeedsCurrent(t *testing.T) { }() q := "" ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SearchInput(c, &q, SearchInputOptions{Find: true}) }, 200, 100) } diff --git a/select.go b/input/select.go similarity index 79% rename from select.go rename to input/select.go index 6f3de2a..a1d56b4 100644 --- a/select.go +++ b/input/select.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SelectOption is one choice of a Select or MultiSelect. @@ -47,8 +48,8 @@ type selectLifetime struct { } // Select chooses *value from options in a popup; typing on the closed select jumps to a match. -func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], opts SelectOptions) ChoiceResult { - k := Tokens(c) +func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], opts SelectOptions) core.ChoiceResult { + k := core.Tokens(c) old := *value // SelectBase chooses into a copy as it is built; a choice counts once the option is known enabled. pick := old @@ -74,7 +75,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op tr.Label(opts.Label) } ta := ui.Local(tr, selectKey{"typeahead"}, func() selectTypeahead { return selectTypeahead{} }) - tr.TextCaret(ui.Rect{W: 1, H: Density(c).ControlHeight()}).HandleInput(func(ev ui.InputEvent) bool { + tr.TextCaret(ui.Rect{W: 1, H: core.Density(c).ControlHeight()}).HandleInput(func(ev ui.InputEvent) bool { if tr.IsDisabled() { return false } @@ -97,34 +98,34 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op } } var zero T - label, has := selectLabel(options, *value) + label, has := SelectLabel(options, *value) if opts.Clearable && has && (tr.Shortcut(0, ui.KeyBackspace) || tr.Shortcut(0, ui.KeyDelete)) && !tr.IsDisabled() { *value, has = zero, false } - selectField(c, tr, s.Open(), func() { + SelectField(c, tr, s.Open(), func() { if has { ui.Text(c, label).SingleLine().Grow(1).Shrink(1) } else { - ui.Text(c, selectOr(opts.Placeholder, msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) + ui.Text(c, selectOr(opts.Placeholder, core.Msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1).Shrink(1) } - if opts.Clearable && has && !opts.Disabled && selectClear(c, k, msg(c, "select.clear")) { + if opts.Clearable && has && !opts.Disabled && selectClear(c, k, core.Msg(c, "select.clear")) { *value = zero } }) pick = *value lastHi := ui.Local(tr, selectKey{"shown"}, func() int { return -1 }) if tr.IsDisabled() { - return ChoiceResult{tr, false} + return core.NewChoiceResult(tr, false) } s.Popup(func(panel *ui.Element) { - selectPanel(c, panel) + SelectPanel(c, panel) ui.Scroll(c).MaxHeight(280).Children(func() { if len(options) == 0 { - selectNote(c, selectOr(opts.Empty, msg(c, "select.empty"))) + SelectNote(c, selectOr(opts.Empty, core.Msg(c, "select.empty"))) } for i, o := range options { if o.Disabled { - selectRow(c, ui.Row(c).Disabled(true), false, false, o.Label) + SelectRow(c, ui.Row(c).Disabled(true), false, false, o.Label) continue } it := s.Item(o.Value) @@ -132,7 +133,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op *lastHi = i it.ScrollIntoView() } - selectRow(c, it, it.Highlighted(), o.Value == *value, o.Label) + SelectRow(c, it, it.Highlighted(), o.Value == *value, o.Label) } }) }) @@ -144,7 +145,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op if changed { slog.Debug("mujica: select changed", "label", opts.Label, "value", *value) } - return ChoiceResult{tr, changed} + return core.NewChoiceResult(tr, changed) } // selectEnabled reports whether v is an option that is not disabled. @@ -178,7 +179,7 @@ func selectFind[T comparable](options []SelectOption[T], current T, prefix strin return -1 } -func selectLabel[T comparable](options []SelectOption[T], v T) (string, bool) { +func SelectLabel[T comparable](options []SelectOption[T], v T) (string, bool) { for _, o := range options { if o.Value == v { return o.Label, true @@ -194,10 +195,10 @@ func selectOr(s, fallback string) string { return fallback } -// selectField styles tr as a field that opens a popup, with content and a chevron. -func selectField(c *ui.Context, tr *ui.Element, open bool, content func()) { - k := Tokens(c) - d := Density(c) +// SelectField styles tr as a field that opens a popup, with content and a chevron. +func SelectField(c *ui.Context, tr *ui.Element, open bool, content func()) { + k := core.Tokens(c) + d := core.Density(c) tr.MinHeight(d.ControlHeight()).MinWidth(180).Padding(0, d.Unit()*2, 0, d.Unit()*3).Gap(d.Unit() * 2). Justify(ui.Start).Radius(theme.ControlRadius).Background(k.Surface) internal.Ring(tr, k.Focus, theme.ControlRadius) @@ -242,9 +243,9 @@ func selectCenter(r ui.Rect, size float32) ui.Rect { return ui.Rect{X: r.X + (r.W-size)/2, Y: r.Y + (r.H-size)/2, W: size, H: size} } -// selectCheck reserves room for the chosen mark and draws it when on. -func selectCheck(c *ui.Context, on bool) { - k := Tokens(c) +// SelectCheck reserves room for the chosen mark and draws it when on. +func SelectCheck(c *ui.Context, on bool) { + k := core.Tokens(c) ui.Box(c).Size(16, 16).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { if on { internal.Mark(p, r, "check", k.AccentText) @@ -252,16 +253,16 @@ func selectCheck(c *ui.Context, on bool) { }) } -// selectPanel styles a popup panel. -func selectPanel(c *ui.Context, panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) +// SelectPanel styles a popup panel. +func SelectPanel(c *ui.Context, panel *ui.Element) { + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Margin(4, 0, 0, 0).Padding(4) } -// selectRow styles an option row: chosen has the selection fill and a check, highlighted a side bar. -func selectRow(c *ui.Context, row *ui.Element, highlighted, chosen bool, label string) { - k := Tokens(c) - d := Density(c) +// SelectRow styles an option row: chosen has the selection fill and a check, highlighted a side bar. +func SelectRow(c *ui.Context, row *ui.Element, highlighted, chosen bool, label string) { + k := core.Tokens(c) + d := core.Density(c) row.MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3, 0, d.Unit()*2).Gap(d.Unit() * 2). AlignItems(ui.Center).Radius(theme.ControlRadius).Role(ui.RoleListItem) switch { @@ -276,15 +277,15 @@ func selectRow(c *ui.Context, row *ui.Element, highlighted, chosen bool, label s }) } row.Children(func() { - selectCheck(c, chosen) + SelectCheck(c, chosen) ui.Text(c, label).SingleLine() }) } -// selectNote shows a muted line in a popup, as an empty state. -func selectNote(c *ui.Context, s string) { - d := Density(c) +// SelectNote shows a muted line in a popup, as an empty state. +func SelectNote(c *ui.Context, s string) { + d := core.Density(c) ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).AlignItems(ui.Center).Children(func() { - ui.Text(c, s).TextColor(Tokens(c).TextMuted) + ui.Text(c, s).TextColor(core.Tokens(c).TextMuted) }) } diff --git a/select_test.go b/input/select_test.go similarity index 97% rename from select_test.go rename to input/select_test.go index 1978715..944d8a0 100644 --- a/select_test.go +++ b/input/select_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var selectTestOptions = []SelectOption[string]{ @@ -16,7 +18,7 @@ var selectTestOptions = []SelectOption[string]{ func selectTester(value *string, options []SelectOption[string], opts SelectOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if Select(c, value, options, opts).Changed() { *changes++ diff --git a/signature_pad.go b/input/signature_pad.go similarity index 82% rename from signature_pad.go rename to input/signature_pad.go index 226162f..cb5c294 100644 --- a/signature_pad.go +++ b/input/signature_pad.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -9,9 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SignaturePoint is a point of a stroke, in DIPs from the pad's top left. @@ -52,23 +53,23 @@ type signaturePadDrawing struct { } // SignaturePad records pointer strokes into *strokes; Changed reports a finished stroke or a clear. -func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadOptions) ChoiceResult { +func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadOptions) core.ChoiceResult { if strokes == nil { panic("mujica: SignaturePad needs strokes") } if !(opts.Height >= 0) || math.IsInf(float64(opts.Height), 1) { panic("mujica: SignaturePad Height must be finite and >= 0") } - k := Tokens(c) - u := Density(c).Unit() - name := cmp.Or(opts.Label, msg(c, "signature_pad.label")) + k := core.Tokens(c) + u := core.Density(c).Unit() + name := cmp.Or(opts.Label, core.Msg(c, "signature_pad.label")) col := ui.Column(c).FillWidth().Gap(u * 2).Disabled(opts.Disabled) disabled := col.IsDisabled() changed := false col.Children(func() { pad := ui.Box(c).FillWidth().Height(cmp.Or(opts.Height, 160)).Focusable().Radius(theme.CardRadius).Clip(). Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Cursor(ui.CursorCrosshair). - Role(ui.RoleImage).Label(name).Description(msg(c, "signature_pad.hint")) + Role(ui.RoleImage).Label(name).Description(core.Msg(c, "signature_pad.hint")) internal.Ring(pad, k.Focus, theme.CardRadius) d := ui.Local(pad, "signature_pad", func() *signaturePadDrawing { return &signaturePadDrawing{} }) s := *d @@ -109,14 +110,14 @@ func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadO } ink := signaturePadClone(*strokes) empty := len(ink) == 0 - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) pad.Draw(func(p *ui.Painter, r ui.Rect) { line := r.Y + r.H - 28 p.Fill(ui.Rect{X: r.X + 16, Y: line, W: r.W - 32, H: 1}, k.Border, 0) p.Text(r.X+16, line+4, "×", size, k.TextMuted) if empty { - w, _ := p.MeasureText(0, ui.Span{Text: msg(c, "signature_pad.empty"), Size: size}) - p.Text(r.X+(r.W-w)/2, r.Y+r.H/2-size, msg(c, "signature_pad.empty"), size, k.TextMuted) + w, _ := p.MeasureText(0, ui.Span{Text: core.Msg(c, "signature_pad.empty"), Size: size}) + p.Text(r.X+(r.W-w)/2, r.Y+r.H/2-size, core.Msg(c, "signature_pad.empty"), size, k.TextMuted) } for _, st := range ink { var path ui.Path @@ -135,12 +136,12 @@ func SignaturePad(c *ui.Context, strokes *[][]SignaturePoint, opts SignaturePadO } }) ui.Row(c).Children(func() { - if Button(c, msg(c, "signature_pad.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("trash-2"), Disabled: empty}).Clicked() && !disabled { + if Button(c, core.Msg(c, "signature_pad.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("trash-2"), Disabled: empty}).Clicked() && !disabled { clear() } }) }) - return ChoiceResult{col, changed} + return core.NewChoiceResult(col, changed) } // signaturePadClone copies strokes so painting never sees later edits. diff --git a/signature_pad_test.go b/input/signature_pad_test.go similarity index 95% rename from signature_pad_test.go rename to input/signature_pad_test.go index 5aaf1c8..65943d8 100644 --- a/signature_pad_test.go +++ b/input/signature_pad_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSignaturePath(t *testing.T) { @@ -21,7 +23,7 @@ func TestSignaturePadStrokesAndClear(t *testing.T) { dis := false n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(400).Children(func() { if SignaturePad(c, &strokes, SignaturePadOptions{Disabled: dis}).Changed() { n++ diff --git a/slider.go b/input/slider.go similarity index 64% rename from slider.go rename to input/slider.go index 1163a64..682caff 100644 --- a/slider.go +++ b/input/slider.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SliderMark is a tick on a slider's track, with an optional caption. @@ -33,39 +34,39 @@ type SliderOptions struct { } const ( - sliderThumb float32 = 16 - sliderTrack float32 = 4 + SliderThumb float32 = 16 + SliderTrack float32 = 4 sliderBubble float32 = 26 sliderMarkH float32 = 18 ) -// sliderGeom maps values to x positions across a slider's box; grid holds the step points. -type sliderGeom struct { +// SliderGeom maps values to x positions across a slider's box; grid holds the step points. +type SliderGeom struct { lo, hi, step float64 - grid numberInputSpec - trackY float32 + grid NumberInputSpec + TrackY float32 } -func (g sliderGeom) x(r ui.Rect, v float64) float32 { +func (g SliderGeom) X(r ui.Rect, v float64) float32 { f := float32((v - g.lo) / (g.hi - g.lo)) - return r.X + sliderThumb/2 + (r.W-sliderThumb)*max(0, min(1, f)) + return r.X + SliderThumb/2 + (r.W-SliderThumb)*max(0, min(1, f)) } // snap moves v to the nearest step point inside the range. -func (g sliderGeom) snap(v float64) float64 { +func (g SliderGeom) snap(v float64) float64 { v = max(g.lo, min(g.hi, v)) if g.step <= 0 { return v } - return g.grid.stepBy(v, 0) + return g.grid.StepBy(v, 0) } // move goes n steps from v, 1% of the range each when continuous, staying in range. -func (g sliderGeom) move(v float64, n int) float64 { +func (g SliderGeom) move(v float64, n int) float64 { if g.step <= 0 { return max(g.lo, min(g.hi, v+float64(n)*(g.hi-g.lo)/100)) } - return g.grid.stepBy(v, n) + return g.grid.StepBy(v, n) } // sliderKey is the last key released on a slider and how many presses MyGo saw before it. @@ -76,8 +77,8 @@ type sliderKey struct { type sliderKeyID struct{} -// sliderControl builds a SliderBase on a copy of v and returns where the pointer or a key moves v. -func sliderControl(c *ui.Context, v float64, g sliderGeom, from, to float64) (s *ui.Element, next float64, dragged bool) { +// SliderControl builds a SliderBase on a copy of v and returns where the pointer or a key moves v. +func SliderControl(c *ui.Context, v float64, g SliderGeom, from, to float64) (s *ui.Element, next float64, dragged bool) { raw := v s = ui.SliderBase(c, &raw, g.lo, g.hi) ks := *ui.Local(s, sliderKeyID{}, func() *sliderKey { return &sliderKey{} }) @@ -117,22 +118,22 @@ func sliderControl(c *ui.Context, v float64, g sliderGeom, from, to float64) (s return s, v, false } -func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c *ui.Context) (sliderGeom, float32) { +func SliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c *ui.Context) (SliderGeom, float32) { if min == 0 && max == 0 { max = 100 } if max <= min || step < 0 { panic("mujica: slider needs Min < Max and Step >= 0") } - g := sliderGeom{lo: min, hi: max, step: step} + g := SliderGeom{lo: min, hi: max, step: step} if step > 0 { // NumberInput's grid: exact bounds and integer step indexes at the decimals Min and Step need. - g.grid = numberInputResolve(NumberInputOptions{Min: min, Max: max, Step: step, Precision: sliderDecimals(min, step)}) + g.grid = NumberInputResolve(NumberInputOptions{Min: min, Max: max, Step: step, Precision: SliderDecimals(min, step)}) } - h := Density(c).ControlHeight() - g.trackY = h / 2 + h := core.Density(c).ControlHeight() + g.TrackY = h / 2 if showValue { - g.trackY += sliderBubble + g.TrackY += sliderBubble h += sliderBubble } for _, m := range marks { @@ -143,8 +144,8 @@ func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c * return g, h } -// sliderDecimals counts the decimals the shortest form of each value needs. -func sliderDecimals(vs ...float64) int { +// SliderDecimals counts the decimals the shortest form of each value needs. +func SliderDecimals(vs ...float64) int { n := 0 for _, v := range vs { s := strconv.FormatFloat(v, 'f', -1, 64) @@ -155,11 +156,11 @@ func sliderDecimals(vs ...float64) int { return n } -func sliderFormat(format func(float64) string, g sliderGeom) func(float64) string { +func sliderFormat(format func(float64) string, g SliderGeom) func(float64) string { if format != nil { return format } - prec := sliderDecimals(g.lo, g.step) + prec := SliderDecimals(g.lo, g.step) if g.step == 0 { prec = max(prec, 1) } @@ -167,11 +168,11 @@ func sliderFormat(format func(float64) string, g sliderGeom) func(float64) strin } // Slider sets *value by dragging anywhere on its box, the arrows (one Step), Home and End. -func Slider(c *ui.Context, value *float64, opts SliderOptions) ChoiceResult { - g, h := sliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) +func Slider(c *ui.Context, value *float64, opts SliderOptions) core.ChoiceResult { + g, h := SliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, opts.ShowValue, c) format := sliderFormat(opts.Format, g) - size := FontSize(c, theme.CaptionSize) - k := Tokens(c) + size := core.FontSize(c, theme.CaptionSize) + k := core.Tokens(c) old := g.snap(*value) *value = old var s *ui.Element @@ -179,9 +180,9 @@ func Slider(c *ui.Context, value *float64, opts SliderOptions) ChoiceResult { var held bool // Disabled on a wrapper blocks the base before it takes input. ui.Column(c).Disabled(opts.Disabled).Children(func() { - s, next, held = sliderControl(c, old, g, g.lo, g.hi) + s, next, held = SliderControl(c, old, g, g.lo, g.hi) }) - s.Height(h).MinWidth(120).PaddingX(sliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer) + s.Height(h).MinWidth(120).PaddingX(SliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer) if opts.Label != "" { s.Label(opts.Label) } @@ -195,37 +196,37 @@ func Slider(c *ui.Context, value *float64, opts SliderOptions) ChoiceResult { hover := s.Hovered() v := *value s.Draw(func(p *ui.Painter, r ui.Rect) { - sliderTrackPaint(p, r, g, k, opts.Marks, size, g.x(r, g.lo), g.x(r, v)) - x := g.x(r, v) - sliderThumbPaint(p, k, x, r.Y+g.trackY, hover || held, s.FocusVisible()) + SliderTrackPaint(p, r, g, k, opts.Marks, size, g.X(r, g.lo), g.X(r, v)) + x := g.X(r, v) + SliderThumbPaint(p, k, x, r.Y+g.TrackY, hover || held, s.FocusVisible()) if opts.ShowValue && (hover || held || s.Focused()) { - sliderBubblePaint(p, k, x, r.Y+g.trackY, format(v), size) + sliderBubblePaint(p, k, x, r.Y+g.TrackY, format(v), size) } }) - return ChoiceResult{s, changed} + return core.NewChoiceResult(s, changed) } -// sliderTrackPaint draws the rail, the filled part from x0 to x1, and the marks. -func sliderTrackPaint(p *ui.Painter, r ui.Rect, g sliderGeom, k theme.Tokens, marks []SliderMark, size, x0, x1 float32) { - y := r.Y + g.trackY - left, right := g.x(r, g.lo), g.x(r, g.hi) - p.Fill(ui.Rect{X: left, Y: y - sliderTrack/2, W: right - left, H: sliderTrack}, k.Border, 0) - p.Stroke(ui.Rect{X: left, Y: y - sliderTrack/2, W: right - left, H: sliderTrack}, k.ControlBorder, 0, 1) - p.Fill(ui.Rect{X: x0, Y: y - sliderTrack/2, W: x1 - x0, H: sliderTrack}, k.AccentText, 0) +// SliderTrackPaint draws the rail, the filled part from x0 to x1, and the marks. +func SliderTrackPaint(p *ui.Painter, r ui.Rect, g SliderGeom, k theme.Tokens, marks []SliderMark, size, x0, x1 float32) { + y := r.Y + g.TrackY + left, right := g.X(r, g.lo), g.X(r, g.hi) + p.Fill(ui.Rect{X: left, Y: y - SliderTrack/2, W: right - left, H: SliderTrack}, k.Border, 0) + p.Stroke(ui.Rect{X: left, Y: y - SliderTrack/2, W: right - left, H: SliderTrack}, k.ControlBorder, 0, 1) + p.Fill(ui.Rect{X: x0, Y: y - SliderTrack/2, W: x1 - x0, H: SliderTrack}, k.AccentText, 0) for _, m := range marks { - x := g.x(r, m.Value) - p.Fill(ui.Rect{X: x - 0.5, Y: y + sliderTrack/2 + 2, W: 1, H: 5}, k.TextMuted, 0) + x := g.X(r, m.Value) + p.Fill(ui.Rect{X: x - 0.5, Y: y + SliderTrack/2 + 2, W: 1, H: 5}, k.TextMuted, 0) if m.Label != "" { w, _ := p.MeasureText(1000, ui.Span{Text: m.Label, Size: size}) - p.Text(x-w/2, y+sliderTrack/2+8, m.Label, size, k.TextMuted) + p.Text(x-w/2, y+SliderTrack/2+8, m.Label, size, k.TextMuted) } } } -// sliderThumbPaint draws the diamond thumb at (x, y), with its focus ring. -func sliderThumbPaint(p *ui.Painter, k theme.Tokens, x, y float32, hot, focus bool) { +// SliderThumbPaint draws the diamond thumb at (x, y), with its focus ring. +func SliderThumbPaint(p *ui.Painter, k theme.Tokens, x, y float32, hot, focus bool) { x, y = internal.Snap(p, x), internal.Snap(p, y) - size := sliderThumb + size := SliderThumb if hot { size += 2 } @@ -239,7 +240,7 @@ func sliderThumbPaint(p *ui.Painter, k theme.Tokens, x, y float32, hot, focus bo // sliderBubblePaint draws the value tip above the thumb at (x, y). func sliderBubblePaint(p *ui.Painter, k theme.Tokens, x, y float32, s string, size float32) { w, h := p.MeasureText(1000, ui.Span{Text: s, Size: size}) - b := ui.Rect{X: x - w/2 - 6, Y: y - sliderThumb/2 - h - 10, W: w + 12, H: h + 4} + b := ui.Rect{X: x - w/2 - 6, Y: y - SliderThumb/2 - h - 10, W: w + 12, H: h + 4} p.Fill(b, k.Surface, theme.ControlRadius) p.Stroke(b, k.ControlBorder, theme.ControlRadius, theme.BorderWidth) p.Text(b.X+6, b.Y+2, s, size, k.Text) diff --git a/slider_direction_test.go b/input/slider_direction_test.go similarity index 98% rename from slider_direction_test.go rename to input/slider_direction_test.go index c3e60e4..8f45217 100644 --- a/slider_direction_test.go +++ b/input/slider_direction_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "testing" diff --git a/slider_grid_test.go b/input/slider_grid_test.go similarity index 95% rename from slider_grid_test.go rename to input/slider_grid_test.go index 8915830..99d750f 100644 --- a/slider_grid_test.go +++ b/input/slider_grid_test.go @@ -1,9 +1,11 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // sliderRecord builds a Slider and a RangeSlider and records the values each Changed reported. @@ -14,7 +16,7 @@ type sliderRecord struct { func sliderGridTester(r *sliderRecord, min, max, step float64) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(216).Gap(10).Children(func() { if Slider(c, &r.v, SliderOptions{Min: min, Max: max, Step: step, Label: "One"}).Changed() { r.single = append(r.single, r.v) diff --git a/slider_keys_test.go b/input/slider_keys_test.go similarity index 87% rename from slider_keys_test.go rename to input/slider_keys_test.go index 0b27069..6d4175c 100644 --- a/slider_keys_test.go +++ b/input/slider_keys_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "bytes" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSliderFineStepNearEnds(t *testing.T) { @@ -48,13 +50,13 @@ func TestRangeSliderFineStepNearEnds(t *testing.T) { func TestSliderFormatKeepsStepDecimals(t *testing.T) { for _, tc := range []struct { - g sliderGeom + g SliderGeom v float64 want string }{ - {sliderGeom{lo: 0, hi: 1, step: 0.25}, 0.75, "0.75"}, - {sliderGeom{lo: 0.5, hi: 10, step: 1}, 2.5, "2.5"}, - {sliderGeom{lo: 0, hi: 100, step: 5}, 35, "35"}, + {SliderGeom{lo: 0, hi: 1, step: 0.25}, 0.75, "0.75"}, + {SliderGeom{lo: 0.5, hi: 10, step: 1}, 2.5, "2.5"}, + {SliderGeom{lo: 0, hi: 100, step: 5}, 35, "35"}, } { if got := sliderFormat(nil, tc.g)(tc.v); got != tc.want { t.Errorf("%+v: %q, want %q", tc.g, got, tc.want) @@ -67,7 +69,7 @@ func TestSliderMarksFollowTextScale(t *testing.T) { v, low, high := 50.0, 20.0, 80.0 marks := []SliderMark{{Value: 0, Label: "Low"}, {Value: 100, Label: "High"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(300).Children(func() { Slider(c, &v, SliderOptions{Marks: marks}) RangeSlider(c, &low, &high, RangeSliderOptions{Marks: marks}) diff --git a/slider_test.go b/input/slider_test.go similarity index 95% rename from slider_test.go rename to input/slider_test.go index 284606f..dfb5712 100644 --- a/slider_test.go +++ b/input/slider_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func sliderTester(value *float64, opts SliderOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(0, 0).Width(216).Children(func() { if Slider(c, value, opts).Changed() { *changes++ @@ -78,7 +80,7 @@ func TestSliderBadRange(t *testing.T) { }() v := 0.0 ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Slider(c, &v, SliderOptions{Min: 5, Max: 1}) }, 100, 100, 1) } diff --git a/spinner_test.go b/input/spinner_test.go similarity index 76% rename from spinner_test.go rename to input/spinner_test.go index 436ad18..d173db6 100644 --- a/spinner_test.go +++ b/input/spinner_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "bytes" @@ -7,14 +7,16 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSpinnerLabelAndSize(t *testing.T) { var mark ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) - Spinner(c, SpinnerOptions{}) - Spinner(c, SpinnerOptions{Size: 32, Label: "Fetching mail", ShowLabel: true}) + core.Use(c, core.Settings{Locale: core.ZhCN}) + core.Spinner(c, core.SpinnerOptions{}) + core.Spinner(c, core.SpinnerOptions{Size: 32, Label: "Fetching mail", ShowLabel: true}) }, 400, 300) r, ok := tt.Find("加载中") if !ok || r.W != 16 || r.H != 16 { @@ -41,9 +43,9 @@ func TestSpinnerLabelAndSize(t *testing.T) { func TestSpinnerMotion(t *testing.T) { show := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if show { - Spinner(c, SpinnerOptions{Size: 40}) + core.Spinner(c, core.SpinnerOptions{Size: 40}) } }, 100, 100) a := encode(t, tt.Image()) @@ -69,9 +71,9 @@ func TestSpinnerMotion(t *testing.T) { func TestSpinnerSnapshot(t *testing.T) { snapshot(t, "spinner", 360, 100, func(c *ui.Context) { ui.Row(c).Padding(24).Gap(24).AlignItems(ui.Center).Children(func() { - Spinner(c, SpinnerOptions{}) - Spinner(c, SpinnerOptions{Size: 24}) - Spinner(c, SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) + core.Spinner(c, core.SpinnerOptions{}) + core.Spinner(c, core.SpinnerOptions{Size: 24}) + core.Spinner(c, core.SpinnerOptions{Size: 20, Label: "Syncing", ShowLabel: true}) Button(c, "Saving", ButtonOptions{Loading: true}) }) }) diff --git a/stable_keys_test.go b/input/stable_keys_test.go similarity index 92% rename from stable_keys_test.go rename to input/stable_keys_test.go index 8cc1d50..786a8d6 100644 --- a/stable_keys_test.go +++ b/input/stable_keys_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestFormFocusFollowsFieldAfterReorder(t *testing.T) { @@ -12,7 +14,7 @@ func TestFormFocusFollowsFieldAfterReorder(t *testing.T) { first, second := "", "" swapped := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Form(c, &st, FormOptions{}, func(f *FormScope) { fields := []func(){ func() { @@ -50,7 +52,7 @@ func TestToggleGroupFocusFollowsValueAfterReorder(t *testing.T) { var on []string items := []ToggleItem[string]{{Value: "left", Label: "Left"}, {Value: "right", Label: "Right"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToggleGroup(c, &on, items, ToggleGroupOptions{Multiple: true, Label: "Align"}) }, 400, 200) tt.Key(0, ui.KeyTab) diff --git a/switch.go b/input/switch.go similarity index 86% rename from switch.go rename to input/switch.go index 40531cd..9e16114 100644 --- a/switch.go +++ b/input/switch.go @@ -1,12 +1,13 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SwitchOptions configure a Switch. @@ -26,8 +27,8 @@ const ( // Switch turns *on over by click or Space; Changed reports it, the caller applies it. func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) blocked := opts.Disabled || opts.Loading var sw *ui.Element // Disabled on a wrapper blocks the base before it takes input. @@ -42,7 +43,7 @@ func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Eleme slog.Debug("mujica: switch changed", "label", label, "on", *on) } hover := sw.Hovered() - pos := sw.Animate("on", switchFloat(*on), Motion(c, theme.StateDuration)) + pos := sw.Animate("on", switchFloat(*on), core.Motion(c, theme.StateDuration)) sw.Children(func() { track := ui.Box(c).Size(switchW, switchH).Shrink(0).Radius(theme.CardRadius) bg, border := k.Surface, k.ControlBorder @@ -72,7 +73,7 @@ func Switch(c *ui.Context, on *bool, label string, opts SwitchOptions) *ui.Eleme track.Children(func() { t := ui.Box(c).Absolute().Left(x).Top(inset).Size(switchK, switchK).Radius(theme.ControlRadius).Background(thumb).Center() if opts.Loading { - t.Children(func() { Spinner(c, SpinnerOptions{Size: 10, Color: bg}) }) + t.Children(func() { core.Spinner(c, core.SpinnerOptions{Size: 10, Color: bg}) }) } }) if label != "" { diff --git a/switch_test.go b/input/switch_test.go similarity index 92% rename from switch_test.go rename to input/switch_test.go index 8b90bfb..f4f95ea 100644 --- a/switch_test.go +++ b/input/switch_test.go @@ -1,15 +1,17 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSwitchToggles(t *testing.T) { on, changes := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Switch(c, &on, "Wi-Fi", SwitchOptions{}).Changed() { changes++ } @@ -29,7 +31,7 @@ func TestSwitchToggles(t *testing.T) { func TestSwitchKeyboardFocus(t *testing.T) { on := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Switch(c, &on, "Sync", SwitchOptions{}) }, 300, 120) tt.Key(0, ui.KeyTab) @@ -46,7 +48,7 @@ func TestSwitchBlockedStates(t *testing.T) { for _, opts := range []SwitchOptions{{Disabled: true}, {Loading: true}} { on := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Switch(c, &on, "Backup", opts) }, 300, 120) _ = tt.Click("Backup") diff --git a/tags_input.go b/input/tags_input.go similarity index 87% rename from tags_input.go rename to input/tags_input.go index b1b2944..e289ef7 100644 --- a/tags_input.go +++ b/input/tags_input.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TagsInputOptions configure a TagsInput. @@ -42,6 +43,11 @@ type TagsInputParts struct { // Changed reports whether tags were added or removed this frame. func (p TagsInputParts) Changed() bool { return p.changed } +// NewTagsInputParts reports a tag field built from parts in another component. +func NewTagsInputParts(e, input *ui.Element, changed bool) TagsInputParts { + return TagsInputParts{e, input, changed} +} + type tagsInputState struct { query string q *string @@ -54,8 +60,8 @@ func TagsInput(c *ui.Context, tags *[]string, opts TagsInputOptions) TagsInputPa if opts.Max < 0 { panic("mujica: TagsInput Max must not be negative") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) seps := opts.Separators if seps == "" { seps = ",;,;\n\r\t" @@ -74,10 +80,10 @@ func TagsInput(c *ui.Context, tags *[]string, opts TagsInputOptions) TagsInputPa case s == "": return true case slices.Contains(*tags, s): - st.err = fmt.Sprintf(msg(c, "tags_input.duplicate"), s) + st.err = fmt.Sprintf(core.Msg(c, "tags_input.duplicate"), s) return false case opts.Max > 0 && len(*tags) >= opts.Max: - st.err = fmt.Sprintf(msg(c, "tags_input.max"), opts.Max) + st.err = fmt.Sprintf(core.Msg(c, "tags_input.max"), opts.Max) return false } if opts.Validate != nil { @@ -113,7 +119,7 @@ func TagsInput(c *ui.Context, tags *[]string, opts TagsInputOptions) TagsInputPa chip.Children(func() { ui.Text(c, tag).SingleLine() x := ui.ButtonBase(c).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).TextColor(k.TextMuted). - Label(fmt.Sprintf(msg(c, "tags_input.remove"), tag)) + Label(fmt.Sprintf(core.Msg(c, "tags_input.remove"), tag)) internal.Ring(x, k.Focus, theme.ControlRadius) if x.Hovered() { x.Background(k.SurfaceHover).TextColor(k.Text) @@ -142,7 +148,7 @@ func TagsInput(c *ui.Context, tags *[]string, opts TagsInputOptions) TagsInputPa }) }) if st.err != "" { - ui.Text(c, st.err).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) + ui.Text(c, st.err).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Role(ui.RoleStatus) } }) in := parts.Input diff --git a/tags_input_test.go b/input/tags_input_test.go similarity index 96% rename from tags_input_test.go rename to input/tags_input_test.go index 3028c5d..6ee5652 100644 --- a/tags_input_test.go +++ b/input/tags_input_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "slices" @@ -6,11 +6,13 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func tagsInputTester(tags *[]string, opts TagsInputOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if TagsInput(c, tags, opts).Changed() { *changes++ @@ -129,7 +131,7 @@ func TestTagsInputDisabledAfterFocus(t *testing.T) { tags, n := []string{"rose"}, 0 opts := TagsInputOptions{Label: "Tags"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TagsInput(c, &tags, opts).Changed() { n++ } @@ -150,7 +152,7 @@ func TestTagsInputDisabledEditCommands(t *testing.T) { tags, n, seen := []string{}, 0, "" opts := TagsInputOptions{Label: "Tags", Validate: func(s string) string { seen = s; return "" }} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TagsInput(c, &tags, opts).Changed() { n++ } diff --git a/testdata/qr/hello-l.txt b/input/testdata/qr/hello-l.txt similarity index 100% rename from testdata/qr/hello-l.txt rename to input/testdata/qr/hello-l.txt diff --git a/testdata/qr/long-h.txt b/input/testdata/qr/long-h.txt similarity index 100% rename from testdata/qr/long-h.txt rename to input/testdata/qr/long-h.txt diff --git a/testdata/qr/text-q.txt b/input/testdata/qr/text-q.txt similarity index 100% rename from testdata/qr/text-q.txt rename to input/testdata/qr/text-q.txt diff --git a/testdata/qr/url-m.txt b/input/testdata/qr/url-m.txt similarity index 100% rename from testdata/qr/url-m.txt rename to input/testdata/qr/url-m.txt diff --git a/testdata/qr/v8-l.txt b/input/testdata/qr/v8-l.txt similarity index 100% rename from testdata/qr/v8-l.txt rename to input/testdata/qr/v8-l.txt diff --git a/testdata/qr/zh-m.txt b/input/testdata/qr/zh-m.txt similarity index 100% rename from testdata/qr/zh-m.txt rename to input/testdata/qr/zh-m.txt diff --git a/text_area.go b/input/text_area.go similarity index 82% rename from text_area.go rename to input/text_area.go index c5f0fff..3d007e9 100644 --- a/text_area.go +++ b/input/text_area.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TextAreaOptions configure a TextArea. @@ -27,8 +28,8 @@ type TextAreaOptions struct { ReadOnly bool // Error marks the value invalid and shows the message below. Error string - // keys sees keys before the editor, for PromptComposer's menus. - keys func(ui.InputEvent) bool + // Keys sees keys before the editor, for PromptComposer's menus. + Keys func(ui.InputEvent) bool } // TextArea is a multi-line field editing *value. @@ -46,18 +47,18 @@ func TextArea(c *ui.Context, value *string, opts TextAreaOptions) InputResult { if key == 0 { key, mods = ui.KeyEnter, ui.Cmd } - d := Density(c) + d := core.Density(c) n := utf8.RuneCountInString(*value) over := opts.MaxLength > 0 && n > opts.MaxLength errMsg := opts.Error if errMsg == "" && over { - errMsg = fmt.Sprintf(msg(c, "text_area.over"), n-opts.MaxLength) + errMsg = fmt.Sprintf(core.Msg(c, "text_area.over"), n-opts.MaxLength) } count := textAreaCount(c, n, opts.MaxLength) var r InputResult look := &textInputLook{disabled: opts.Disabled, readOnly: opts.ReadOnly, err: errMsg, area: true} r.Element, r.Input = textInputFrame(c, look, func(frame *ui.Element) *ui.Element { - in := textInputEditor(c, frame, value, opts.ReadOnly || look.disabled, ui.TextAreaBase).Grow(1).Padding(d.Unit()*2, d.Unit()*3). + in := TextInputEditor(c, frame, value, opts.ReadOnly || look.disabled, ui.TextAreaBase).Grow(1).Padding(d.Unit()*2, d.Unit()*3). Placeholder(opts.Placeholder).Label(opts.Label) if opts.MinHeight > 0 { in.MinHeight(opts.MinHeight) @@ -69,7 +70,7 @@ func TextArea(c *ui.Context, value *string, opts TextAreaOptions) InputResult { // Taken before the editor, which would make a plain Enter a newline. submit := ui.Local(frame, "submit", func() bool { return false }) in.HandleInput(func(ev ui.InputEvent) bool { - if opts.keys != nil && opts.keys(ev) { + if opts.Keys != nil && opts.Keys(ev) { return true } if ev.Kind == ui.InputKeyDown && ev.Key == key && ev.Mods == mods { @@ -91,13 +92,13 @@ func textAreaCount(c *ui.Context, n, max int) func() { return nil } return func() { - k := Tokens(c) + k := core.Tokens(c) color := k.TextMuted over := n > max if over { color = k.Danger } - ui.Text(c, fmt.Sprintf("%d / %d", n, max)).TextColor(color).FontSize(FontSize(c, theme.CaptionSize)). - FontFeatures("tnum").Shrink(0).Label(fmt.Sprintf(msg(c, "text_area.count"), n, max)) + ui.Text(c, fmt.Sprintf("%d / %d", n, max)).TextColor(color).FontSize(core.FontSize(c, theme.CaptionSize)). + FontFeatures("tnum").Shrink(0).Label(fmt.Sprintf(core.Msg(c, "text_area.count"), n, max)) } } diff --git a/text_area_test.go b/input/text_area_test.go similarity index 93% rename from text_area_test.go rename to input/text_area_test.go index 4e51b59..1ec36ce 100644 --- a/text_area_test.go +++ b/input/text_area_test.go @@ -1,16 +1,18 @@ -package mujica +package input import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTextAreaNewlinesAndSubmit(t *testing.T) { s, submits := "", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TextArea(c, &s, TextAreaOptions{Label: "Notes"}).Submitted() { submits++ } @@ -31,7 +33,7 @@ func TestTextAreaNewlinesAndSubmit(t *testing.T) { func TestTextAreaCustomSubmitKey(t *testing.T) { s, submits := "", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TextArea(c, &s, TextAreaOptions{Label: "Chat", SubmitKey: ui.KeyEnter, SubmitMods: ui.Ctrl | ui.Shift}).Submitted() { submits++ } @@ -49,7 +51,7 @@ func TestTextAreaHeightsAndCount(t *testing.T) { s := strings.Repeat("line\n", 40) + "end" var h float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Width(300).Children(func() { r := TextArea(c, &s, TextAreaOptions{Label: "Log", MinHeight: 60, MaxHeight: 120, MaxLength: 100}) h = r.Input.Bounds().H @@ -77,7 +79,7 @@ func TestTextAreaBadOptionsPanic(t *testing.T) { }() s := "" ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TextArea(c, &s, TextAreaOptions{MinHeight: 100, MaxHeight: 50}) }, 300, 200) } diff --git a/text_input.go b/input/text_input.go similarity index 83% rename from text_input.go rename to input/text_input.go index 9630d82..742d0da 100644 --- a/text_input.go +++ b/input/text_input.go @@ -1,13 +1,14 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // InputResult is a value input's outer element and its events. @@ -25,6 +26,11 @@ func (r InputResult) Changed() bool { return r.changed } // Submitted reports Enter, or the submit shortcut of a TextArea. func (r InputResult) Submitted() bool { return r.submitted } +// NewInputResult reports a text control built from parts in another component. +func NewInputResult(e, input *ui.Element, changed, submitted bool) InputResult { + return InputResult{e, input, changed, submitted} +} + // TextInputOptions configure a TextInput. type TextInputOptions struct { Placeholder string @@ -53,7 +59,7 @@ func TextInput(c *ui.Context, value *string, opts TextInputOptions) InputResult if opts.Leading != nil { opts.Leading() } - in := textInputEditor(c, frame, value, opts.ReadOnly || look.disabled, ui.TextInputBase).Grow(1).MinWidth(Density(c).Unit() * 10). + in := TextInputEditor(c, frame, value, opts.ReadOnly || look.disabled, ui.TextInputBase).Grow(1).MinWidth(core.Density(c).Unit() * 10). Placeholder(opts.Placeholder).Label(opts.Label) r.changed = in.Changed() && !opts.ReadOnly && !look.disabled r.submitted = in.Submitted() && !look.disabled @@ -80,8 +86,8 @@ type textInputLook struct { // textInputFrame builds the bordered field around what build returns as the input. func textInputFrame(c *ui.Context, look *textInputLook, build func(frame *ui.Element) *ui.Element, footer func()) (outer, in *ui.Element) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) outer = ui.Column(c).Gap(d.Unit()).Shrink(0).Disabled(look.disabled) // One disabled state, the field's own or an ancestor's, for build to obey. look.disabled = outer.IsDisabled() @@ -117,7 +123,7 @@ func textInputFrame(c *ui.Context, look *textInputLook, build func(frame *ui.Ele case in.Focused() && !look.readOnly: border = k.Focus } - frame.Background(bg).Border(theme.BorderWidth, border).Transition(colorFade) + frame.Background(bg).Border(theme.BorderWidth, border).Transition(core.ColorFade) frame.DrawOver(func(p *ui.Painter, r ui.Rect) { if in.FocusVisible() { internal.FocusRing(p, r, theme.ControlRadius, k.Focus) @@ -128,7 +134,7 @@ func textInputFrame(c *ui.Context, look *textInputLook, build func(frame *ui.Ele } ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Start).Children(func() { if look.err != "" { - ui.Text(c, look.err).TextColor(k.Danger).FontSize(FontSize(c, theme.CaptionSize)).Grow(1) + ui.Text(c, look.err).TextColor(k.Danger).FontSize(core.FontSize(c, theme.CaptionSize)).Grow(1) } else { ui.Spacer(c) } @@ -140,8 +146,8 @@ func textInputFrame(c *ui.Context, look *textInputLook, build func(frame *ui.Ele return outer, in } -// textInputEditor builds the MyGo editor; a locked one edits a copy, undone at once. -func textInputEditor(c *ui.Context, frame *ui.Element, value *string, locked bool, base func(*ui.Context, *string) *ui.Element) *ui.Element { +// TextInputEditor builds the MyGo editor; a locked one edits a copy, undone at once. +func TextInputEditor(c *ui.Context, frame *ui.Element, value *string, locked bool, base func(*ui.Context, *string) *ui.Element) *ui.Element { if !locked { return base(c, value) } @@ -154,9 +160,9 @@ func textInputEditor(c *ui.Context, frame *ui.Element, value *string, locked boo // textInputClear builds the clear button and reports a clear by click or Escape. func textInputClear(c *ui.Context, in *ui.Element) bool { - k := Tokens(c) + k := core.Tokens(c) b := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Center().Radius(theme.ControlRadius). - Role(ui.RoleButton).Label(msg(c, "text_input.clear")).Cursor(ui.CursorPointer) + Role(ui.RoleButton).Label(core.Msg(c, "text_input.clear")).Cursor(ui.CursorPointer) fg := k.TextMuted if b.Hovered() { b.Background(k.SurfaceHover) diff --git a/text_input_test.go b/input/text_input_test.go similarity index 91% rename from text_input_test.go rename to input/text_input_test.go index 8758dbf..669c674 100644 --- a/text_input_test.go +++ b/input/text_input_test.go @@ -1,17 +1,18 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestTextInputTypingAndSubmit(t *testing.T) { name, changes, submits := "", 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := TextInput(c, &name, TextInputOptions{Label: "Name", Placeholder: "Your name"}) if r.Changed() { changes++ @@ -33,7 +34,7 @@ func TestTextInputTypingAndSubmit(t *testing.T) { func TestTextInputUndoClipboardIME(t *testing.T) { s := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TextInput(c, &s, TextInputOptions{Label: "Note"}) }, 400, 200) _ = tt.Click("Note") @@ -57,7 +58,7 @@ func TestTextInputUndoClipboardIME(t *testing.T) { func TestTextInputClear(t *testing.T) { s, changes := "draft", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TextInput(c, &s, TextInputOptions{Label: "Draft", Clearable: true}).Changed() { changes++ } @@ -81,7 +82,7 @@ func TestTextInputClear(t *testing.T) { func TestTextInputReadOnlyAndDisabled(t *testing.T) { ro, off := "fixed", "locked" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(8).Children(func() { TextInput(c, &ro, TextInputOptions{Label: "RO", ReadOnly: true, Clearable: true}) TextInput(c, &off, TextInputOptions{Label: "Off", Disabled: true}) @@ -108,7 +109,7 @@ func TestTextInputReadOnlyAndDisabled(t *testing.T) { func TestTextInputSlotsAndError(t *testing.T) { s := "x" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TextInput(c, &s, TextInputOptions{ Label: "Price", Leading: func() { ui.Text(c, "¥") }, @@ -129,7 +130,7 @@ func TestTextInputSnapshot(t *testing.T) { ui.Column(c).Padding(24).Gap(12).Width(360).Children(func() { TextInput(c, &a, TextInputOptions{Placeholder: "Placeholder", Label: "A"}) TextInput(c, &b, TextInputOptions{Label: "B", Clearable: true, Leading: func() { - ui.Icon(c, icons.Must("user")).Size(16, 16).TextColor(Tokens(c).TextMuted) + ui.Icon(c, icons.Must("user")).Size(16, 16).TextColor(core.Tokens(c).TextMuted) }}) TextInput(c, &e, TextInputOptions{Label: "E", Error: "No such date"}) TextInput(c, &ro, TextInputOptions{Label: "R", ReadOnly: true}) diff --git a/toggle.go b/input/toggle.go similarity index 90% rename from toggle.go rename to input/toggle.go index 4bd8846..5ce171c 100644 --- a/toggle.go +++ b/input/toggle.go @@ -1,12 +1,13 @@ -package mujica +package input import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToggleOptions configure a Toggle. @@ -39,8 +40,8 @@ func Toggle(c *ui.Context, on *bool, label string, opts ToggleOptions) *ui.Eleme // toggleLook styles a pressed-or-not button and builds its content. func toggleLook(c *ui.Context, b *ui.Element, on bool, label string, icon *ui.SVG) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b.MinHeight(d.ControlHeight()).MinWidth(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2). Radius(theme.ControlRadius) internal.Ring(b, k.Focus, theme.ControlRadius) diff --git a/toggle_group.go b/input/toggle_group.go similarity index 87% rename from toggle_group.go rename to input/toggle_group.go index 1c87cc3..7fc94af 100644 --- a/toggle_group.go +++ b/input/toggle_group.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "log/slog" @@ -6,19 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) -// ChoiceResult is a choice control's element and whether its value changed this frame. -type ChoiceResult struct { - Element *ui.Element - changed bool -} - -// Changed reports whether the user changed the value in this frame. -func (r ChoiceResult) Changed() bool { return r.changed } - // ToggleItem is one button of a ToggleGroup. type ToggleItem[T comparable] struct { Value T @@ -42,11 +34,11 @@ type ToggleGroupOptions struct { } // ToggleGroup is a joined row of toggles choosing one item of *value, or several with Multiple. -func ToggleGroup[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], opts ToggleGroupOptions) ChoiceResult { +func ToggleGroup[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], opts ToggleGroupOptions) core.ChoiceResult { if !opts.Multiple && len(*value) > 1 { panic("mujica: single ToggleGroup holds more than one value") } - k := Tokens(c) + k := core.Tokens(c) g := ui.Row(c).Shrink(0).AlignSelf(ui.Start).AlignItems(ui.Stretch).Radius(theme.ControlRadius). Border(theme.BorderWidth, k.ControlBorder).Background(k.Surface).Disabled(opts.Disabled).Role(ui.RoleGroup) if opts.Vertical { @@ -79,7 +71,7 @@ func ToggleGroup[T comparable](c *ui.Context, value *[]T, items []ToggleItem[T], if changed { slog.Debug("mujica: toggle group changed", "label", opts.Label, "value", *value) } - return ChoiceResult{g, changed} + return core.NewChoiceResult(g, changed) } // toggleGroupApply presses or releases v by the group's rules and reports a change. @@ -99,7 +91,7 @@ func toggleGroupApply[T comparable](value *[]T, v T, pressed bool, opts ToggleGr } func toggleGroupLook[T comparable](c *ui.Context, k theme.Tokens, b *ui.Element, on bool, it ToggleItem[T]) { - d := Density(c) + d := core.Density(c) b.MinHeight(d.ControlHeight()-2).MinWidth(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2) internal.Ring(b, k.Focus, theme.ControlRadius) bg, fg := ui.Transparent, k.Text diff --git a/toggle_group_test.go b/input/toggle_group_test.go similarity index 96% rename from toggle_group_test.go rename to input/toggle_group_test.go index 75cfed0..ee787ac 100644 --- a/toggle_group_test.go +++ b/input/toggle_group_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var toggleGroupTestItems = []ToggleItem[string]{ @@ -16,7 +18,7 @@ var toggleGroupTestItems = []ToggleItem[string]{ func toggleGroupTester(value *[]string, opts ToggleGroupOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ToggleGroup(c, value, toggleGroupTestItems, opts).Changed() { *changes++ } @@ -94,7 +96,7 @@ func TestToggleGroupRejectsTwoValuesInSingleMode(t *testing.T) { }() value := []string{"left", "right"} ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToggleGroup(c, &value, toggleGroupTestItems, ToggleGroupOptions{}) }, 300, 100, 1) } diff --git a/toggle_test.go b/input/toggle_test.go similarity index 88% rename from toggle_test.go rename to input/toggle_test.go index 7186775..bd1e2eb 100644 --- a/toggle_test.go +++ b/input/toggle_test.go @@ -1,17 +1,18 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestTogglePressAndRelease(t *testing.T) { on, changes := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Toggle(c, &on, "Bold", ToggleOptions{}).Changed() { changes++ } @@ -31,7 +32,7 @@ func TestTogglePressAndRelease(t *testing.T) { func TestToggleIconOnlyNamed(t *testing.T) { on := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Toggle(c, &on, "", ToggleOptions{Icon: icons.Must("bold"), Label: "Bold text"}) }, 300, 120) if err := tt.Click("Bold text"); err != nil { @@ -50,7 +51,7 @@ func TestToggleNeedsName(t *testing.T) { }() on := false ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Toggle(c, &on, "", ToggleOptions{}) }, 100, 100, 1) } diff --git a/transfer.go b/input/transfer.go similarity index 84% rename from transfer.go rename to input/transfer.go index 4292457..ec6c349 100644 --- a/transfer.go +++ b/input/transfer.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TransferItem is one entry of a Transfer; ID must be unique and stable. @@ -35,7 +36,7 @@ type transferState struct { } // Transfer moves items between a source list and *target, the chosen IDs in order. -func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts TransferOptions) ChoiceResult { +func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts TransferOptions) core.ChoiceResult { byID := make(map[string]TransferItem, len(items)) for _, it := range items { if _, dup := byID[it.ID]; dup || it.ID == "" { @@ -48,8 +49,8 @@ func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts Transf panic(fmt.Sprintf("mujica: Transfer target ID %q is not an item", id)) } } - d := Density(c) - parts := ChoiceResult{} + d := core.Density(c) + parts := core.ChoiceResult{} row := ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Disabled(opts.Disabled) parts.Element = row st := ui.Local(row, "transfer", func() transferState { return transferState{checked: map[string]bool{}} }) @@ -83,7 +84,7 @@ func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts Transf } else { *target = slices.DeleteFunc(slices.Clone(*target), func(id string) bool { return slices.Contains(ids, id) }) } - parts.changed = true + parts = core.NewChoiceResult(parts.Element, true) slog.Debug("mujica: transfer moved", "count", len(ids), "right", right) } height := opts.Height @@ -92,20 +93,20 @@ func Transfer(c *ui.Context, target *[]string, items []TransferItem, opts Transf } srcTitle, dstTitle := opts.SourceTitle, opts.TargetTitle if srcTitle == "" { - srcTitle = msg(c, "transfer.source") + srcTitle = core.Msg(c, "transfer.source") } if dstTitle == "" { - dstTitle = msg(c, "transfer.target") + dstTitle = core.Msg(c, "transfer.target") } row.Children(func() { transferPanel(c, st, 0, srcTitle, source, height) ui.Column(c).Gap(d.Unit() * 2).Children(func() { right := transferCount(st, source) > 0 left := transferCount(st, chosen) > 0 - if Button(c, "", ButtonOptions{Variant: Secondary, Icon: icons.Must("chevron-right"), Label: fmt.Sprintf(msg(c, "transfer.to"), dstTitle), Disabled: !right}).Clicked() { + if Button(c, "", ButtonOptions{Variant: Secondary, Icon: icons.Must("chevron-right"), Label: fmt.Sprintf(core.Msg(c, "transfer.to"), dstTitle), Disabled: !right}).Clicked() { move(source, true) } - if Button(c, "", ButtonOptions{Variant: Secondary, Icon: icons.Must("chevron-left"), Label: fmt.Sprintf(msg(c, "transfer.to"), srcTitle), Disabled: !left}).Clicked() { + if Button(c, "", ButtonOptions{Variant: Secondary, Icon: icons.Must("chevron-left"), Label: fmt.Sprintf(core.Msg(c, "transfer.to"), srcTitle), Disabled: !left}).Clicked() { move(chosen, false) } }) @@ -127,8 +128,8 @@ func transferCount(st *transferState, items []TransferItem) int { // transferPanel is one side: header with select-all, search, and the list. func transferPanel(c *ui.Context, st *transferState, side int, title string, items []TransferItem, height float32) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) q := strings.ToLower(strings.TrimSpace(st.query[side])) var shown []TransferItem enabled := 0 @@ -146,7 +147,7 @@ func transferPanel(c *ui.Context, st *transferState, side int, title string, ite BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border) head.Children(func() { all := checked > 0 && checked == enabled - box := ui.CheckboxBase(c, &all).Size(theme.MinHitSize, theme.MinHitSize).Justify(ui.Center).Label(fmt.Sprintf(msg(c, "transfer.all"), title)).Disabled(enabled == 0) + box := ui.CheckboxBase(c, &all).Size(theme.MinHitSize, theme.MinHitSize).Justify(ui.Center).Label(fmt.Sprintf(core.Msg(c, "transfer.all"), title)).Disabled(enabled == 0) transferMark(c, box, all, checked > 0 && !all) box.Description(transferPartial(c, checked, enabled)) if box.Changed() { @@ -157,17 +158,17 @@ func transferPanel(c *ui.Context, st *transferState, side int, title string, ite } } ui.Text(c, title).Font(theme.SerifFont).FontWeight(600).TextColor(k.Text).Grow(1) - ui.Text(c, fmt.Sprintf("%d / %d", checked, len(items))).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum") + ui.Text(c, fmt.Sprintf("%d / %d", checked, len(items))).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum") }) ui.Box(c).Padding(d.Unit() * 2).Children(func() { - InputGroup(c, &st.query[side], InputGroupOptions{Placeholder: msg(c, "transfer.search"), Label: fmt.Sprintf(msg(c, "transfer.search_in"), title)}) + InputGroup(c, &st.query[side], InputGroupOptions{Placeholder: core.Msg(c, "transfer.search"), Label: fmt.Sprintf(core.Msg(c, "transfer.search_in"), title)}) }) list := ui.Scroll(c).Height(height).Padding(0, d.Unit(), d.Unit(), d.Unit()).FocusGroup(ui.Vertical).Role(ui.RoleList).Label(title) list.Children(func() { if len(shown) == 0 { - empty := msg(c, "transfer.empty") + empty := core.Msg(c, "transfer.empty") if q != "" { - empty = msg(c, "transfer.no_match") + empty = core.Msg(c, "transfer.no_match") } ui.Text(c, empty).TextColor(k.TextMuted).Padding(d.Unit() * 3).TextAlign(ui.Center).FillWidth() return @@ -198,12 +199,12 @@ func transferPartial(c *ui.Context, checked, enabled int) string { if checked == 0 || checked == enabled { return "" } - return fmt.Sprintf(msg(c, "transfer.partial"), checked, enabled) + return fmt.Sprintf(core.Msg(c, "transfer.partial"), checked, enabled) } // transferMark draws a check box's square inside box, which takes the focus ring. func transferMark(c *ui.Context, box *ui.Element, on, mixed bool) { - k := Tokens(c) + k := core.Tokens(c) internal.Ring(box, k.Focus, theme.ControlRadius) box.Children(func() { sq := ui.Box(c).Size(16, 16).Shrink(0).Center().Radius(theme.ControlRadius).Role(ui.RoleNone) diff --git a/transfer_test.go b/input/transfer_test.go similarity index 94% rename from transfer_test.go rename to input/transfer_test.go index 13e1d69..02237bd 100644 --- a/transfer_test.go +++ b/input/transfer_test.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "slices" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) var transferCast = []TransferItem{ @@ -16,7 +17,7 @@ var transferCast = []TransferItem{ func transferTester(target *[]string, items *[]TransferItem, opts TransferOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Transfer(c, target, *items, opts).Changed() { *changes++ } @@ -111,7 +112,7 @@ func TestTransferSnapshot(t *testing.T) { func TestTransferPartialDescription(t *testing.T) { var got [3]string ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) got = [3]string{transferPartial(c, 2, 3), transferPartial(c, 3, 3), transferPartial(c, 0, 3)} }, 100, 100, 1) if got != [3]string{"2 of 3 selected", "", ""} { @@ -122,7 +123,7 @@ func TestTransferPartialDescription(t *testing.T) { func TestTransferSelectAllHitAreaCompact(t *testing.T) { target := []string{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) Transfer(c, &target, transferCast, TransferOptions{}) }, 640, 420) if r, ok := tt.Find("Select all in Available"); !ok || r.W < 28 || r.H < 28 { diff --git a/tree_select.go b/input/tree_select.go similarity index 87% rename from tree_select.go rename to input/tree_select.go index e1e790c..2db7370 100644 --- a/tree_select.go +++ b/input/tree_select.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TreeSelectNode is one node of a TreeSelect. IDs must be unique in the tree. @@ -60,11 +61,11 @@ type treeSelectRow struct { } // TreeSelect chooses one node, or several with Multiple, from a searchable tree in a popup. -func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts TreeSelectOptions) ChoiceResult { +func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts TreeSelectOptions) core.ChoiceResult { if !opts.Multiple && len(*selected) > 1 { panic("mujica: single TreeSelect holds more than one ID") } - k := Tokens(c) + k := core.Tokens(c) before := slices.Clone(*selected) tr := ui.ButtonBase(c).Disabled(opts.Disabled).Role(ui.RolePopUpButton) if opts.Label != "" { @@ -79,10 +80,10 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts } labels := map[string]string{} treeSelectWalk(nodes, func(n *TreeSelectNode) { labels[n.ID] = n.Label }) - selectField(c, tr, st.open, func() { + SelectField(c, tr, st.open, func() { switch { case len(*selected) == 0: - ui.Text(c, selectOr(opts.Placeholder, msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1) + ui.Text(c, selectOr(opts.Placeholder, core.Msg(c, "select.placeholder"))).SingleLine().TextColor(k.TextMuted).Grow(1) case !opts.Multiple: ui.Text(c, labels[(*selected)[0]]).SingleLine().Grow(1).Shrink(1) default: @@ -90,19 +91,19 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts for _, id := range *selected { options = append(options, SelectOption[string]{Value: id, Label: labels[id]}) } - multiSelectTags(c, selected, options, MultiSelectOptions{MaxTags: opts.MaxTags, Disabled: opts.Disabled}) + MultiSelectTags(c, selected, options, MultiSelectOptions{MaxTags: opts.MaxTags, Disabled: opts.Disabled}) } }) ui.PopoverBase(c, tr, &st.open, func(panel *ui.Element) { - selectPanel(c, panel) + SelectPanel(c, panel) panel.MinWidth(max(tr.Bounds().W, 260)) ui.Column(c).Gap(4).Children(func() { - if selectSearch(c, &st.query, msg(c, "tree_select.search")).Shortcut(0, ui.KeyDown) { + if SelectSearch(c, &st.query, core.Msg(c, "tree_select.search")).Shortcut(0, ui.KeyDown) { st.focus = "\x00" } rows := treeSelectVisible(nodes, st) if len(rows) == 0 { - selectNote(c, msg(c, "tree_select.no_results")) + SelectNote(c, core.Msg(c, "tree_select.no_results")) return } shown := make(map[string]bool, len(rows)) @@ -123,7 +124,7 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts if changed { slog.Debug("mujica: tree select changed", "label", opts.Label, "selected", *selected) } - return ChoiceResult{tr, changed} + return core.NewChoiceResult(tr, changed) } func treeSelectWalk(nodes []TreeSelectNode, fn func(n *TreeSelectNode)) { @@ -199,7 +200,7 @@ func treeSelectToggle(n *TreeSelectNode, selected []string, on bool, opts TreeSe }) } for _, id := range ids { - selected = multiSelectSet(selected, id, false) + selected = MultiSelectSet(selected, id, false) if on { selected = append(selected, id) } @@ -221,10 +222,10 @@ func treeSelectFirstChild(n *TreeSelectNode, shown map[string]bool) string { func treeSelectDescribe(c *ui.Context, b *ui.Element, branch, open bool, state internal.CheckState) { var parts []string if branch { - parts = append(parts, msg(c, map[bool]string{true: "tree_select.expanded", false: "tree_select.collapsed"}[open])) + parts = append(parts, core.Msg(c, map[bool]string{true: "tree_select.expanded", false: "tree_select.collapsed"}[open])) } if state == internal.Mixed { - parts = append(parts, msg(c, "tree_select.mixed")) + parts = append(parts, core.Msg(c, "tree_select.mixed")) } if len(parts) > 0 { b.Description(strings.Join(parts, ", ")) @@ -232,15 +233,15 @@ func treeSelectDescribe(c *ui.Context, b *ui.Element, branch, open bool, state i } func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r treeSelectRow, first bool, shown map[string]bool, opts TreeSelectOptions) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) n := r.node branch := len(n.Children) > 0 open := branch && (st.expanded[n.ID] || strings.TrimSpace(st.query) != "") ui.Row(c).Key(n.ID).AlignItems(ui.Center).Gap(2).Padding(0, 0, 0, float32(r.depth)*18).Children(func() { arrow := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0).Radius(theme.ControlRadius) if branch { - arrow.Role(ui.RoleButton).Label(fmt.Sprintf(msg(c, map[bool]string{true: "tree_select.collapse", false: "tree_select.expand"}[open]), n.Label)) + arrow.Role(ui.RoleButton).Label(fmt.Sprintf(core.Msg(c, map[bool]string{true: "tree_select.collapse", false: "tree_select.expand"}[open]), n.Label)) if arrow.Clicked() { st.expanded[n.ID] = !st.expanded[n.ID] } @@ -257,7 +258,7 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r var b *ui.Element if opts.Multiple { on := state == internal.Checked - b = multiSelectCheck(c, k, &on, state, n.Label, n.Disabled) + b = MultiSelectCheck(c, k, &on, state, n.Label, n.Disabled) if b.Changed() { *selected = treeSelectToggle(n, *selected, on, opts) } @@ -280,7 +281,7 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r } b.Children(func() { ui.Text(c, n.Label).SingleLine().Grow(1) - selectCheck(c, chosen) + SelectCheck(c, chosen) }) } treeSelectDescribe(c, b, branch, open, state) diff --git a/tree_select_test.go b/input/tree_select_test.go similarity index 97% rename from tree_select_test.go rename to input/tree_select_test.go index 7e7e141..a6d0c1a 100644 --- a/tree_select_test.go +++ b/input/tree_select_test.go @@ -1,10 +1,12 @@ -package mujica +package input import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var treeSelectTestNodes = []TreeSelectNode{ @@ -21,7 +23,7 @@ var treeSelectTestNodes = []TreeSelectNode{ func treeSelectTester(sel *[]string, opts TreeSelectOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(10).Children(func() { if TreeSelect(c, sel, treeSelectTestNodes, opts).Changed() { *changes++ diff --git a/vertical_slider.go b/input/vertical_slider.go similarity index 66% rename from vertical_slider.go rename to input/vertical_slider.go index 7e25cb9..8a5ac6c 100644 --- a/vertical_slider.go +++ b/input/vertical_slider.go @@ -1,4 +1,4 @@ -package mujica +package input import ( "cmp" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // VerticalSliderOptions configure a VerticalSlider. Min and Max both zero mean 0 to 100. @@ -26,17 +27,17 @@ type VerticalSliderOptions struct { } // VerticalSlider is a Slider standing up: max at the top, Up and Right raise it. -func VerticalSlider(c *ui.Context, value *float64, opts VerticalSliderOptions) ChoiceResult { +func VerticalSlider(c *ui.Context, value *float64, opts VerticalSliderOptions) core.ChoiceResult { if value == nil { panic("mujica: VerticalSlider needs a value") } if !(opts.Height >= 0) || math.IsInf(float64(opts.Height), 1) { panic("mujica: VerticalSlider Height must be finite and >= 0") } - g, _ := sliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, false, c) + g, _ := SliderSetup(opts.Min, opts.Max, opts.Step, opts.Marks, false, c) format := sliderFormat(opts.Format, g) - k := Tokens(c) - size := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + size := core.FontSize(c, theme.CaptionSize) h := cmp.Or(opts.Height, 160) old := g.snap(*value) *value = old @@ -44,25 +45,25 @@ func VerticalSlider(c *ui.Context, value *float64, opts VerticalSliderOptions) C var next float64 var held bool wrap := ui.Column(c).Disabled(opts.Disabled) - wrap.Children(func() { s, next, held = sliderControl(c, old, g, g.lo, g.hi) }) + wrap.Children(func() { s, next, held = SliderControl(c, old, g, g.lo, g.hi) }) // The base maps the pointer across; here the height decides. y := func(r ui.Rect, v float64) float32 { - return r.Y + r.H - sliderThumb/2 - (r.H-sliderThumb)*float32((v-g.lo)/(g.hi-g.lo)) + return r.Y + r.H - SliderThumb/2 - (r.H-SliderThumb)*float32((v-g.lo)/(g.hi-g.lo)) } if held { - if _, py, _ := s.PointerPosition(); s.Bounds().H > sliderThumb { - f := 1 - float64((py-sliderThumb/2)/(s.Bounds().H-sliderThumb)) + if _, py, _ := s.PointerPosition(); s.Bounds().H > SliderThumb { + f := 1 - float64((py-SliderThumb/2)/(s.Bounds().H-SliderThumb)) next = g.snap(g.lo + max(0, min(1, f))*(g.hi-g.lo)) } } - w := Density(c).ControlHeight() + w := core.Density(c).ControlHeight() for _, m := range opts.Marks { if m.Label != "" { w += 40 break } } - s.Size(w, h).PaddingY(sliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer) + s.Size(w, h).PaddingY(SliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer) if opts.Label != "" { s.Label(opts.Label) } @@ -76,23 +77,23 @@ func VerticalSlider(c *ui.Context, value *float64, opts VerticalSliderOptions) C hot := s.Hovered() || held v := *value s.Draw(func(p *ui.Painter, r ui.Rect) { - x := r.X + Density(c).ControlHeight()/2 + x := r.X + core.Density(c).ControlHeight()/2 top, bot := y(r, g.hi), y(r, g.lo) - p.Fill(ui.Rect{X: x - sliderTrack/2, Y: top, W: sliderTrack, H: bot - top}, k.Border, 0) - p.Stroke(ui.Rect{X: x - sliderTrack/2, Y: top, W: sliderTrack, H: bot - top}, k.ControlBorder, 0, 1) - p.Fill(ui.Rect{X: x - sliderTrack/2, Y: y(r, v), W: sliderTrack, H: bot - y(r, v)}, k.AccentText, 0) + p.Fill(ui.Rect{X: x - SliderTrack/2, Y: top, W: SliderTrack, H: bot - top}, k.Border, 0) + p.Stroke(ui.Rect{X: x - SliderTrack/2, Y: top, W: SliderTrack, H: bot - top}, k.ControlBorder, 0, 1) + p.Fill(ui.Rect{X: x - SliderTrack/2, Y: y(r, v), W: SliderTrack, H: bot - y(r, v)}, k.AccentText, 0) for _, m := range opts.Marks { my := y(r, m.Value) - p.Fill(ui.Rect{X: x + sliderTrack/2 + 2, Y: my - 0.5, W: 5, H: 1}, k.TextMuted, 0) + p.Fill(ui.Rect{X: x + SliderTrack/2 + 2, Y: my - 0.5, W: 5, H: 1}, k.TextMuted, 0) if m.Label != "" { _, th := p.MeasureText(1000, ui.Span{Text: m.Label, Size: size}) - p.Text(x+sliderTrack/2+10, my-th/2, m.Label, size, k.TextMuted) + p.Text(x+SliderTrack/2+10, my-th/2, m.Label, size, k.TextMuted) } } - sliderThumbPaint(p, k, x, y(r, v), hot, s.FocusVisible()) + SliderThumbPaint(p, k, x, y(r, v), hot, s.FocusVisible()) if opts.ShowValue && (hot || s.Focused()) { sliderBubblePaint(p, k, x, y(r, v), format(v), size) } }) - return ChoiceResult{wrap, changed} + return core.NewChoiceResult(wrap, changed) } diff --git a/vertical_slider_test.go b/input/vertical_slider_test.go similarity index 90% rename from vertical_slider_test.go rename to input/vertical_slider_test.go index 45d0347..10c2c42 100644 --- a/vertical_slider_test.go +++ b/input/vertical_slider_test.go @@ -1,14 +1,16 @@ -package mujica +package input import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func verticalTester(v *float64, opts *VerticalSliderOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).Children(func() { if VerticalSlider(c, v, *opts).Changed() { *n++ @@ -67,7 +69,7 @@ func TestVerticalSliderRejects(t *testing.T) { t.Errorf("%s: no panic", name) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); VerticalSlider(c, &v, o) }, 200, 200) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); VerticalSlider(c, &v, o) }, 200, 200) }() } } diff --git a/internal/chart.go b/internal/chart.go index 9598c7b..cde4f94 100644 --- a/internal/chart.go +++ b/internal/chart.go @@ -7,7 +7,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Linear maps domain D0..D1 onto pixels R0..R1. diff --git a/internal/chart_test.go b/internal/chart_test.go index f8eca08..7b3f6ee 100644 --- a/internal/chart_test.go +++ b/internal/chart_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestNiceAndTicks(t *testing.T) { diff --git a/internal/choice.go b/internal/choice.go index e6dcd59..4b2d829 100644 --- a/internal/choice.go +++ b/internal/choice.go @@ -3,8 +3,8 @@ package internal import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CheckState is what a drawn check box shows. diff --git a/internal/git.go b/internal/git.go index 6d0f166..438ce68 100644 --- a/internal/git.go +++ b/internal/git.go @@ -5,7 +5,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CodeSpans colors a code line; inBlock carries block comments. diff --git a/internal/git_test.go b/internal/git_test.go index d5831b8..9c8446b 100644 --- a/internal/git_test.go +++ b/internal/git_test.go @@ -3,7 +3,7 @@ package internal import ( "testing" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestAnsiPaletteReadable(t *testing.T) { diff --git a/internal/paint.go b/internal/paint.go index f1cd9f4..d843e7f 100644 --- a/internal/paint.go +++ b/internal/paint.go @@ -6,7 +6,7 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // FocusRing strokes the keyboard ring outside r, leaving the focus gap. diff --git a/snapshot_test.go b/internal/snaptest/snaptest.go similarity index 53% rename from snapshot_test.go rename to internal/snaptest/snaptest.go index 49940ac..fe6558d 100644 --- a/snapshot_test.go +++ b/internal/snaptest/snaptest.go @@ -1,4 +1,5 @@ -package mujica +// Package snaptest compares rendered views with the PNG goldens in testdata/snapshots. +package snaptest import ( "bytes" @@ -19,28 +20,35 @@ import ( var update = flag.Bool("update", false, "rewrite snapshots") -// snapshot compares view in both themes with testdata/snapshots//-.png. -func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { +// Modes names the two themes a snapshot covers, in capture order. +var Modes = []string{"light", "dark"} + +// child marks a process that renders snapshots itself. +const child = "MUJICA_SNAPSHOT_CHILD" + +var ran sync.Map + +// dir is the module's testdata/snapshots/, whatever package runs the test. +var dir = func() string { + _, file, _, ok := runtime.Caller(0) + if !ok { + panic("snaptest: no caller") + } + return filepath.Join(filepath.Dir(file), "..", "..", "testdata", "snapshots", runtime.GOOS) +}() + +// Run compares view, themed by theme for each of Modes, with testdata/snapshots//-.png. +// drive, when non-nil, sends input before the capture. +func Run(t *testing.T, name string, w, h int, theme func(c *ui.Context, mode string), view func(c *ui.Context), drive func(tt *ui.Tester)) { t.Helper() - snapshotAfter(t, name, w, h, view, nil) -} - -// snapshotChild marks a process that renders snapshots itself. -const snapshotChild = "MUJICA_SNAPSHOT_CHILD" - -var snapshotRan sync.Map - -// snapshotAfter is snapshot with drive sending input before the capture. -func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { - t.Helper() - if os.Getenv(snapshotChild) == "" { - snapshotInChild(t) + if os.Getenv(child) == "" { + inChild(t) return } - for _, m := range []Mode{Light, Dark} { + for _, m := range Modes { ready := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: m}) + theme(c, m) if ready { view(c) } @@ -52,8 +60,8 @@ func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context) if drive != nil { drive(tt) } - got := encode(t, tt.Image()) - path := filepath.Join("testdata", "snapshots", runtime.GOOS, name+"-"+strings.ToLower(m.String())+".png") + got := Encode(t, tt.Image()) + path := filepath.Join(dir, name+"-"+m+".png") if *update { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) @@ -76,10 +84,10 @@ func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context) } } -// snapshotInChild reruns the test alone in a fresh process; MyGo's path cache is process-wide. -func snapshotInChild(t *testing.T) { +// inChild reruns the test alone in a fresh process; MyGo's path cache is process-wide. +func inChild(t *testing.T) { t.Helper() - if _, done := snapshotRan.LoadOrStore(t.Name(), true); done { + if _, done := ran.LoadOrStore(t.Name(), true); done { return } parts := strings.Split(t.Name(), "/") @@ -91,13 +99,14 @@ func snapshotInChild(t *testing.T) { args = append(args, "-update") } cmd := exec.Command(os.Args[0], args...) - cmd.Env = append(os.Environ(), snapshotChild+"=1") + cmd.Env = append(os.Environ(), child+"=1") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("snapshot child failed: %v\n%s", err, out) } } -func encode(t *testing.T, img image.Image) []byte { +// Encode returns img as PNG bytes. +func Encode(t *testing.T, img image.Image) []byte { t.Helper() var b bytes.Buffer if err := png.Encode(&b, img); err != nil { diff --git a/app_shell.go b/layout/app_shell.go similarity index 84% rename from app_shell.go rename to layout/app_shell.go index bd3476d..07cd565 100644 --- a/app_shell.go +++ b/layout/app_shell.go @@ -1,12 +1,13 @@ -package mujica +package layout import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AppShellOptions are the slots of an AppShell. Content is required; the others are optional. @@ -43,7 +44,7 @@ func AppShell(c *ui.Context, collapsed *bool, opts AppShellOptions) AppShellResu if opts.SidebarWidth < 0 { panic("mujica: AppShell SidebarWidth must not be negative") } - k := Tokens(c) + k := core.Tokens(c) var res AppShellResult toggle := func() { *collapsed = !*collapsed @@ -74,8 +75,8 @@ func AppShell(c *ui.Context, collapsed *bool, opts AppShellOptions) AppShellResu // appShellSidebar builds the sidebar or its rail and reports whether its toggle was pressed. func appShellSidebar(c *ui.Context, collapsed bool, opts AppShellOptions) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() open := opts.SidebarWidth if open == 0 { open = 240 @@ -87,7 +88,7 @@ func appShellSidebar(c *ui.Context, collapsed bool, opts AppShellOptions) bool { } side := ui.Column(c).Shrink(0).Clip().AlignItems(ui.Stretch).Background(k.Surface). BorderWidth(0, theme.BorderWidth, 0, 0).BorderColor(k.Border) - side.Width(side.AnimateWith("app_shell.width", target, Motion(c, theme.StateDuration), ui.EaseOut)) + side.Width(side.AnimateWith("app_shell.width", target, core.Motion(c, theme.StateDuration), ui.EaseOut)) var pressed bool side.Children(func() { head := ui.Row(c).Shrink(0).Padding(u * 2).Justify(ui.End) @@ -95,11 +96,11 @@ func appShellSidebar(c *ui.Context, collapsed bool, opts AppShellOptions) bool { head.Justify(ui.Center) } head.Children(func() { - label := msg(c, "app_shell.collapse") + label := core.Msg(c, "app_shell.collapse") if collapsed { - label = msg(c, "app_shell.expand") + label = core.Msg(c, "app_shell.expand") } - pressed = iconAction(c, icons.Must("panel-left"), label).Tooltip(label).Clicked() + pressed = core.IconAction(c, icons.Must("panel-left"), label).Tooltip(label).Clicked() }) if !collapsed { ScrollArea(c, nil, ScrollAreaOptions{}, opts.Sidebar).Grow(1).MinHeight(0) diff --git a/app_shell_test.go b/layout/app_shell_test.go similarity index 97% rename from app_shell_test.go rename to layout/app_shell_test.go index 3d53652..2c8999c 100644 --- a/app_shell_test.go +++ b/layout/app_shell_test.go @@ -1,10 +1,12 @@ -package mujica +package layout import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type appShellProbe struct { @@ -14,7 +16,7 @@ type appShellProbe struct { } func (p *appShellProbe) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := AppShell(c, &p.collapsed, AppShellOptions{ TitleBar: func() { TitleBar(c, "Court Records", TitleBarOptions{}) }, Sidebar: func() { @@ -118,7 +120,7 @@ func TestAppShellNeedsContent(t *testing.T) { }() var collapsed bool ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AppShell(c, &collapsed, AppShellOptions{}) }, 100, 100) } diff --git a/aspect_ratio.go b/layout/aspect_ratio.go similarity index 95% rename from aspect_ratio.go rename to layout/aspect_ratio.go index 139b940..70f2c53 100644 --- a/aspect_ratio.go +++ b/layout/aspect_ratio.go @@ -1,9 +1,9 @@ -package mujica +package layout import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" + "github.com/ZacharyZhang-NY/MujicaUI/internal" ) // AspectRatioOptions configure an AspectRatio. diff --git a/aspect_ratio_test.go b/layout/aspect_ratio_test.go similarity index 94% rename from aspect_ratio_test.go rename to layout/aspect_ratio_test.go index 8224068..4a2f691 100644 --- a/aspect_ratio_test.go +++ b/layout/aspect_ratio_test.go @@ -1,16 +1,18 @@ -package mujica +package layout import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // aspectRatioBounds returns the frame (via a filling child) and a fixed 40x20 child. func aspectRatioBounds(t *testing.T, ratio float32, w, h int) (box, child ui.Rect) { t.Helper() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Size(float32(w), float32(h)).Children(func() { AspectRatio(c, AspectRatioOptions{Ratio: ratio}, func() { box = ui.Box(c).Fill().Absolute().Bounds() @@ -51,7 +53,7 @@ func TestAspectRatioDefaultSquareAndUnstretchedChild(t *testing.T) { func TestAspectRatioFollowsWindowResize(t *testing.T) { var box ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Children(func() { AspectRatio(c, AspectRatioOptions{Ratio: 2}, func() { box = ui.Box(c).Fill().Absolute().Bounds() @@ -78,14 +80,14 @@ func TestAspectRatioRejectsNegative(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AspectRatio(c, AspectRatioOptions{Ratio: -1}, func() {}) }, 100, 100) } func TestAspectRatioSnapshot(t *testing.T) { snapshot(t, "aspect_ratio", 420, 160, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) ui.Row(c).Padding(16).Gap(16).Height(160).AlignItems(ui.Stretch).Children(func() { for _, r := range []float32{1, 16.0 / 9, 3.0 / 4} { ui.Box(c).Width(120).Children(func() { diff --git a/container.go b/layout/container.go similarity index 90% rename from container.go rename to layout/container.go index 7007853..8f4b2ae 100644 --- a/container.go +++ b/layout/container.go @@ -1,9 +1,10 @@ -package mujica +package layout import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Space is a spacing step scaled by the window's density. Zero picks the component's default. @@ -68,7 +69,7 @@ func Container(c *ui.Context, opts ContainerOptions, children func()) *ui.Elemen if children == nil { panic("mujica: Container needs children") } - e := ui.Column(c).Padding(containerSpace(Density(c), opts.Padding, SpaceL)) + e := ui.Column(c).Padding(containerSpace(core.Density(c), opts.Padding, SpaceL)) if opts.MaxWidth > 0 { e.MaxWidth(opts.MaxWidth) } diff --git a/container_test.go b/layout/container_test.go similarity index 88% rename from container_test.go rename to layout/container_test.go index b3bceaa..0cd1016 100644 --- a/container_test.go +++ b/layout/container_test.go @@ -1,4 +1,4 @@ -package mujica +package layout import ( "image/color" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestContainerPaddingByDensity(t *testing.T) { @@ -17,7 +18,7 @@ func TestContainerPaddingByDensity(t *testing.T) { }{{theme.Default, SpaceDefault, 16}, {theme.Compact, SpaceDefault, 12}, {theme.Comfortable, SpaceS, 10}, {theme.Default, SpaceNone, 0}} { var outer, inner ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: tc.density}) + core.Use(c, core.Settings{Density: tc.density}) outer = Container(c, ContainerOptions{Padding: tc.pad}, func() { inner = ui.Box(c).Size(10, 10).Bounds() }).Bounds() @@ -31,7 +32,7 @@ func TestContainerPaddingByDensity(t *testing.T) { func TestContainerMaxWidthCentered(t *testing.T) { var r ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(600).Children(func() { r = Container(c, ContainerOptions{MaxWidth: 300, Center: true}, func() { ui.Text(c, "Body") }).Bounds() }) @@ -44,7 +45,7 @@ func TestContainerMaxWidthCentered(t *testing.T) { func TestContainerFill(t *testing.T) { var r ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Size(320, 200).Children(func() { r = Container(c, ContainerOptions{Fill: true}, func() { ui.Text(c, "x") }).Bounds() }) @@ -58,8 +59,8 @@ func TestContainerClip(t *testing.T) { for _, clip := range []bool{false, true} { var bg ui.Color tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - bg = Tokens(c).Background + core.Use(c, core.Settings{Mode: core.Light}) + bg = core.Tokens(c).Background Container(c, ContainerOptions{Padding: SpaceNone, Clip: clip}, func() { ui.Box(c).Size(200, 200).Background(ui.RGB(255, 0, 0)) }).Size(100, 100) @@ -79,14 +80,14 @@ func TestContainerInvalidSpacePanics(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Container(c, ContainerOptions{Padding: Space(99)}, func() {}) }, 100, 100) } func TestContainerSnapshot(t *testing.T) { snapshot(t, "container", 420, 150, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) ui.Column(c).Padding(16).Children(func() { Container(c, ContainerOptions{MaxWidth: 280, Center: true}, func() { ui.Text(c, "Of the Court").Font(theme.SerifFont).FontSize(theme.H3Size) diff --git a/divider.go b/layout/divider.go similarity index 81% rename from divider.go rename to layout/divider.go index 2d0ed68..ab5bd5a 100644 --- a/divider.go +++ b/layout/divider.go @@ -1,10 +1,11 @@ -package mujica +package layout import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // dividerDiamond is the ornament's size in DIPs. @@ -25,7 +26,7 @@ func Divider(c *ui.Context, opts DividerOptions) *ui.Element { if opts.Vertical && opts.Label != "" { panic("mujica: a vertical Divider cannot have a Label") } - k := Tokens(c) + k := core.Tokens(c) line := k.Border if opts.Ornament { line = k.Ornament @@ -38,12 +39,12 @@ func Divider(c *ui.Context, opts DividerOptions) *ui.Element { return e.Height(theme.BorderWidth) } if opts.Label == "" && !opts.Vertical { - gap := Density(c).Unit()*2 + dividerDiamond/2 + gap := core.Density(c).Unit()*2 + dividerDiamond/2 return ui.Box(c).Height(dividerDiamond).Shrink(0).AlignSelf(ui.Stretch).Draw(func(p *ui.Painter, r ui.Rect) { internal.RuleDiamond(p, r, gap, dividerDiamond, line) }) } - e := ui.Row(c).Gap(Density(c).Unit() * 2).Shrink(0).AlignSelf(ui.Stretch) + e := ui.Row(c).Gap(core.Density(c).Unit() * 2).Shrink(0).AlignSelf(ui.Stretch) if opts.Vertical { e.Column().AlignItems(ui.Center) } @@ -58,7 +59,7 @@ func Divider(c *ui.Context, opts DividerOptions) *ui.Element { return e.Children(func() { segment() if opts.Label != "" { - ui.Text(c, opts.Label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, opts.Label).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).SingleLine().Shrink(0) } else { ui.Box(c).Size(dividerDiamond, dividerDiamond).Shrink(0).Role(ui.RoleNone). diff --git a/divider_test.go b/layout/divider_test.go similarity index 88% rename from divider_test.go rename to layout/divider_test.go index 709a78b..ca2cad9 100644 --- a/divider_test.go +++ b/layout/divider_test.go @@ -1,16 +1,19 @@ -package mujica +package layout import ( "image/color" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestDividerSizes(t *testing.T) { var h, v ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(300).Children(func() { h = Divider(c, DividerOptions{}).Bounds() ui.Row(c).Height(80).Children(func() { @@ -28,11 +31,11 @@ func TestDividerSizes(t *testing.T) { func TestDividerNeverFocused(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { Divider(c, DividerOptions{Label: "Section", Ornament: true}) Divider(c, DividerOptions{Ornament: true}) - Button(c, "OK", ButtonOptions{}) + input.Button(c, "OK", input.ButtonOptions{}) }) }, 400, 300) tt.Key(0, ui.KeyTab) @@ -46,7 +49,7 @@ func TestDividerNeverFocused(t *testing.T) { func TestDividerLabelCentered(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(400).Children(func() { Divider(c, DividerOptions{Label: "Chapter II"}) }) @@ -63,8 +66,8 @@ func TestDividerLabelCentered(t *testing.T) { func TestDividerOrnamentColor(t *testing.T) { var want ui.Color tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - want = Tokens(c).Ornament + core.Use(c, core.Settings{Mode: core.Light}) + want = core.Tokens(c).Ornament ui.Column(c).Width(201).Padding(10, 0).Children(func() { Divider(c, DividerOptions{Ornament: true}) }) @@ -83,7 +86,7 @@ func TestDividerVerticalLabelPanics(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Divider(c, DividerOptions{Vertical: true, Label: "x"}) }, 100, 100) } @@ -108,7 +111,7 @@ func TestDividerSnapshot(t *testing.T) { func TestDividerLabelTextScale(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(300).Children(func() { Divider(c, DividerOptions{Label: "Chapter II"}) }) }, 400, 200) plain, _ := tt.Find("Chapter II") diff --git a/layout/doc.go b/layout/doc.go new file mode 100644 index 0000000..52efa82 --- /dev/null +++ b/layout/doc.go @@ -0,0 +1,2 @@ +// Package layout holds MujicaUI app shells, panes, toolbars and other desktop layout. +package layout diff --git a/example_layout_test.go b/layout/example_layout_test.go similarity index 50% rename from example_layout_test.go rename to layout/example_layout_test.go index 0f01e58..41fc4d1 100644 --- a/example_layout_test.go +++ b/layout/example_layout_test.go @@ -1,18 +1,20 @@ -package mujica_test +package layout_test import ( "github.com/egoist/mygo/ui" - mujica "mujicaui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/layout" ) func ExampleAppShell() { var collapsed bool view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.AppShell(c, &collapsed, mujica.AppShellOptions{ - TitleBar: func() { mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{}) }, + core.Use(c, core.Settings{}) + layout.AppShell(c, &collapsed, layout.AppShellOptions{ + TitleBar: func() { layout.TitleBar(c, "Court Records", layout.TitleBarOptions{}) }, Sidebar: func() { ui.Text(c, "Chronicles") }, Content: func() { ui.Text(c, "The Grand Hall") }, }) @@ -22,9 +24,9 @@ func ExampleAppShell() { func ExampleTitleBar() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.TitleBar(c, "Court Records", mujica.TitleBarOptions{ - Trailing: func() { mujica.Button(c, "Share", mujica.ButtonOptions{Variant: mujica.Ghost}) }, + core.Use(c, core.Settings{}) + layout.TitleBar(c, "Court Records", layout.TitleBarOptions{ + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, }) } ui.Render(view, 400, 300, 1) @@ -33,10 +35,10 @@ func ExampleTitleBar() { func ExampleStatusBar() { opened := 0 view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - bar := mujica.StatusBar(c, mujica.StatusBarOptions{ - Left: []mujica.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, - Right: []mujica.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, + core.Use(c, core.Settings{}) + bar := layout.StatusBar(c, layout.StatusBarOptions{ + Left: []layout.StatusItem{{ID: "sync", Text: "Syncing", ShowProgress: true, Progress: 0.4}}, + Right: []layout.StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, }) if bar.Clicked == "bell" { opened++ @@ -47,8 +49,8 @@ func ExampleStatusBar() { func ExampleContainer() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Container(c, mujica.ContainerOptions{MaxWidth: 320, Center: true}, func() { + core.Use(c, core.Settings{}) + layout.Container(c, layout.ContainerOptions{MaxWidth: 320, Center: true}, func() { ui.Text(c, "A centered column of prose.") }) } @@ -58,13 +60,13 @@ func ExampleContainer() { func ExampleStack() { showDraft := false view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Stack(c, mujica.StackOptions{Horizontal: true, Gap: mujica.SpaceM}, func() { - mujica.Button(c, "Save", mujica.ButtonOptions{}) + core.Use(c, core.Settings{}) + layout.Stack(c, layout.StackOptions{Horizontal: true, Gap: layout.SpaceM}, func() { + input.Button(c, "Save", input.ButtonOptions{}) if showDraft { - mujica.Button(c, "Draft", mujica.ButtonOptions{Variant: mujica.Secondary}) + input.Button(c, "Draft", input.ButtonOptions{Variant: input.Secondary}) } - mujica.Button(c, "Cancel", mujica.ButtonOptions{Variant: mujica.Ghost}) + input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Ghost}) }) } ui.Render(view, 400, 300, 1) @@ -72,9 +74,9 @@ func ExampleStack() { func ExampleGrid() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.Grid(c, mujica.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { - mujica.GridCell(c, mujica.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) + core.Use(c, core.Settings{}) + layout.Grid(c, layout.GridOptions{Columns: []ui.Track{ui.Fixed(96), ui.Fr(1)}}, func() { + layout.GridCell(c, layout.GridCellOptions{ColumnSpan: -1}, func() { ui.Text(c, "Header") }) ui.Text(c, "Name") ui.Text(c, "Ada") }) @@ -85,8 +87,8 @@ func ExampleGrid() { func ExampleScrollArea() { var offset ui.ScrollState view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.ScrollArea(c, &offset, mujica.ScrollAreaOptions{Label: "Log"}, func() { + core.Use(c, core.Settings{}) + layout.ScrollArea(c, &offset, layout.ScrollAreaOptions{Label: "Log"}, func() { for range 50 { ui.Text(c, "A line of the log") } @@ -96,10 +98,10 @@ func ExampleScrollArea() { } func ExampleSplitPane() { - split := mujica.SplitPaneState{Size: 160} + split := layout.SplitPaneState{Size: 160} view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) - mujica.SplitPane(c, &split, mujica.SplitPaneOptions{MinFirst: 120, Collapsible: mujica.SplitPaneFirst}, + core.Use(c, core.Settings{}) + layout.SplitPane(c, &split, layout.SplitPaneOptions{MinFirst: 120, Collapsible: layout.SplitPaneFirst}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }, ).Element.Fill() @@ -109,9 +111,9 @@ func ExampleSplitPane() { func ExampleAspectRatio() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Size(320, 240).Children(func() { - mujica.AspectRatio(c, mujica.AspectRatioOptions{Ratio: 16.0 / 9}, func() { + layout.AspectRatio(c, layout.AspectRatioOptions{Ratio: 16.0 / 9}, func() { ui.Text(c, "16 : 9") }) }) @@ -121,10 +123,10 @@ func ExampleAspectRatio() { func ExampleDivider() { view := func(c *ui.Context) { - mujica.Use(c, mujica.Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Gap(12).Children(func() { - mujica.Divider(c, mujica.DividerOptions{Ornament: true}) - mujica.Divider(c, mujica.DividerOptions{Label: "Chapter II"}) + layout.Divider(c, layout.DividerOptions{Ornament: true}) + layout.Divider(c, layout.DividerOptions{Label: "Chapter II"}) }) } ui.Render(view, 400, 300, 1) diff --git a/grid.go b/layout/grid.go similarity index 94% rename from grid.go rename to layout/grid.go index 9de4fe2..935c4a8 100644 --- a/grid.go +++ b/layout/grid.go @@ -1,6 +1,10 @@ -package mujica +package layout -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" +) // GridOptions configure a Grid. type GridOptions struct { @@ -19,7 +23,7 @@ func Grid(c *ui.Context, opts GridOptions, children func()) *ui.Element { if children == nil { panic("mujica: Grid needs children") } - d := Density(c) + d := core.Density(c) gap := containerSpace(d, opts.Gap, SpaceM) rowGap := gap if opts.RowGap != SpaceDefault { diff --git a/grid_test.go b/layout/grid_test.go similarity index 93% rename from grid_test.go rename to layout/grid_test.go index a04dce3..30866d2 100644 --- a/grid_test.go +++ b/layout/grid_test.go @@ -1,15 +1,17 @@ -package mujica +package layout import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestGridFixedAndFrTracks(t *testing.T) { var a, b ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Grid(c, GridOptions{Columns: []ui.Track{ui.Fixed(100), ui.Fr(1)}}, func() { a = ui.Box(c).Height(20).Bounds() b = ui.Box(c).Height(20).Bounds() @@ -23,7 +25,7 @@ func TestGridFixedAndFrTracks(t *testing.T) { func TestGridSpans(t *testing.T) { var head, tall, side ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Grid(c, GridOptions{Columns: []ui.Track{ui.Fr(1), ui.Fr(1), ui.Fr(1)}, Gap: SpaceS, RowGap: SpaceL}, func() { head = GridCell(c, GridCellOptions{ColumnSpan: -1}, func() { ui.Box(c).Height(20) }).Bounds() tall = GridCell(c, GridCellOptions{RowSpan: 2}, func() { ui.Box(c).Height(10) }).Bounds() @@ -46,7 +48,7 @@ func TestGridSpans(t *testing.T) { func TestGridRowTracksShareHeight(t *testing.T) { var a, b ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Grid(c, GridOptions{Rows: []ui.Track{ui.Fixed(40), ui.Fr(1)}, Gap: SpaceNone}, func() { a = ui.Box(c).Bounds() b = ui.Box(c).Bounds() @@ -66,7 +68,7 @@ func TestGridCellRejectsBadPlacement(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Grid(c, GridOptions{}, func() { GridCell(c, o, func() {}) }) }, 100, 100) }() @@ -75,7 +77,7 @@ func TestGridCellRejectsBadPlacement(t *testing.T) { func TestGridSnapshot(t *testing.T) { snapshot(t, "grid", 420, 200, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) cell := func(s string) { ui.Box(c).Center().Padding(8).Background(k.Surface).Border(1, k.Border).Radius(4). Children(func() { ui.Text(c, s).TextColor(k.TextMuted) }) diff --git a/layout/helpers_test.go b/layout/helpers_test.go new file mode 100644 index 0000000..cf4d0a4 --- /dev/null +++ b/layout/helpers_test.go @@ -0,0 +1,24 @@ +package layout + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/scroll_area.go b/layout/scroll_area.go similarity index 92% rename from scroll_area.go rename to layout/scroll_area.go index 83046fd..896003e 100644 --- a/scroll_area.go +++ b/layout/scroll_area.go @@ -1,9 +1,10 @@ -package mujica +package layout import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ScrollDirection is the way a ScrollArea scrolls. The zero value is vertical. @@ -46,7 +47,7 @@ func ScrollArea(c *ui.Context, state *ui.ScrollState, opts ScrollAreaOptions, ch if overflow { e.Focusable() } - focus := Tokens(c).Focus + focus := core.Tokens(c).Focus // The ring goes inside, past the focus gap: areas often fill a clipped pane. e.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) { if overflow != (state.MaxX > 0 || state.MaxY > 0) { diff --git a/scroll_area_test.go b/layout/scroll_area_test.go similarity index 93% rename from scroll_area_test.go rename to layout/scroll_area_test.go index 27e61e8..daf154f 100644 --- a/scroll_area_test.go +++ b/layout/scroll_area_test.go @@ -1,4 +1,4 @@ -package mujica +package layout import ( "fmt" @@ -7,11 +7,13 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func scrollAreaView(st *ui.ScrollState, opts ScrollAreaOptions, rows int) func(c *ui.Context) { return func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ScrollArea(c, st, opts, func() { for i := range rows { ui.Text(c, fmt.Sprintf("Row %d", i)).Height(20).Width(float32(100 + 40*i)) @@ -74,7 +76,7 @@ func TestScrollAreaNoOverflowSkipsFocus(t *testing.T) { func TestScrollAreaHorizontal(t *testing.T) { st := &ui.ScrollState{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ScrollArea(c, st, ScrollAreaOptions{Direction: ScrollHorizontal}, func() { ui.Box(c).Size(500, 50) }).Size(200, 60) @@ -106,7 +108,7 @@ func TestScrollAreaInvalidDirectionPanics(t *testing.T) { func TestScrollAreaSnapshot(t *testing.T) { snapshot(t, "scroll_area", 300, 160, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) st := ui.Local(c.Root(), "snap", func() ui.ScrollState { return ui.ScrollState{Y: 30} }) ui.Column(c).Padding(16).Children(func() { ScrollArea(c, st, ScrollAreaOptions{}, func() { @@ -121,8 +123,8 @@ func TestScrollAreaSnapshot(t *testing.T) { func TestScrollAreaFocusRingKeepsGap(t *testing.T) { var focus, bg ui.Color tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) - focus, bg = Tokens(c).Focus, Tokens(c).Background + core.Use(c, core.Settings{Mode: core.Light}) + focus, bg = core.Tokens(c).Focus, core.Tokens(c).Background ScrollArea(c, nil, ScrollAreaOptions{Label: "Log"}, func() { ui.Box(c).Height(400).Shrink(0) }).Size(100, 100) }, 200, 200) tt.Key(0, ui.KeyTab) diff --git a/split_pane.go b/layout/split_pane.go similarity index 95% rename from split_pane.go rename to layout/split_pane.go index 0b90209..b382107 100644 --- a/split_pane.go +++ b/layout/split_pane.go @@ -1,4 +1,4 @@ -package mujica +package layout import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SplitPaneSide names a pane of a SplitPane. @@ -64,7 +65,7 @@ func SplitPane(c *ui.Context, state *SplitPaneState, opts SplitPaneOptions, firs if state.Collapsed && opts.Collapsible == SplitPaneNeither { panic("mujica: SplitPane collapsed without a Collapsible pane") } - u := Density(c).Unit() + u := core.Density(c).Unit() lo, minSecond := opts.MinFirst, opts.MinSecond if lo == 0 { lo = 10 * u @@ -94,8 +95,8 @@ func SplitPane(c *ui.Context, state *SplitPaneState, opts SplitPaneOptions, firs // splitPaneBuild lays the panes, divider and drag handle out in the inner box in. func splitPaneBuild(c *ui.Context, in *ui.Element, state *SplitPaneState, opts SplitPaneOptions, lo, minSecond float32, res *SplitPaneResult, first, second func()) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() was := in.Bounds() internal.Remeasure(c, in, was.W, was.H) total := was.W @@ -144,10 +145,10 @@ func splitPaneBuild(c *ui.Context, in *ui.Element, state *SplitPaneState, opts S } a.Children(first) } - div := ui.Box(c).Key("divider").Shrink(0).Focusable().FocusRing(false).Role(ui.RoleSplitter).Label(msg(c, "split_pane.divider")) + div := ui.Box(c).Key("divider").Shrink(0).Focusable().FocusRing(false).Role(ui.RoleSplitter).Label(core.Msg(c, "split_pane.divider")) along(div, theme.BorderWidth) if state.Collapsed { - div.Description(msg(c, "split_pane.collapsed")) + div.Description(core.Msg(c, "split_pane.collapsed")) } else if total > 0 { div.Description(fmt.Sprintf("%.0f%%", 100*state.Size/total)) } diff --git a/split_pane_test.go b/layout/split_pane_test.go similarity index 97% rename from split_pane_test.go rename to layout/split_pane_test.go index f263d8f..c598b99 100644 --- a/split_pane_test.go +++ b/layout/split_pane_test.go @@ -1,9 +1,11 @@ -package mujica +package layout import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type splitPaneProbe struct { @@ -12,7 +14,7 @@ type splitPaneProbe struct { func splitPaneTester(st *SplitPaneState, opts SplitPaneOptions, p *splitPaneProbe) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := SplitPane(c, st, opts, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }) r.Element.Fill() if r.Resized { @@ -134,7 +136,7 @@ func TestSplitPaneCollapseFirst(t *testing.T) { func TestSplitPaneCollapseSecondVertical(t *testing.T) { st := &SplitPaneState{Size: 100} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SplitPane(c, st, SplitPaneOptions{Vertical: true, MinSecond: 60, Collapsible: SplitPaneSecond}, func() { ui.Text(c, "Editor") }, func() { ui.Text(c, "Console") }).Element.Fill() }, 300, 201) @@ -175,7 +177,7 @@ func TestSplitPaneRejectsCollapsedWithoutSide(t *testing.T) { func TestSplitPaneSnapshot(t *testing.T) { snapshot(t, "split_pane", 420, 160, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) st := ui.Local(c.Root(), "snap", func() SplitPaneState { return SplitPaneState{Size: 140} }) pane := func(s string, bg ui.Color) func() { return func() { @@ -193,7 +195,7 @@ func TestSplitPaneSizesInsideBorder(t *testing.T) { st := &SplitPaneState{Size: 150} var second ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SplitPane(c, st, SplitPaneOptions{MinSecond: 120}, func() { ui.Text(c, "Files") }, func() { second = ui.Box(c).Fill().Absolute().Bounds() }).Element.Fill().Border(1, ui.RGB(0, 0, 0)) @@ -215,7 +217,7 @@ func TestSplitPaneSizesInsideBorder(t *testing.T) { func TestSplitPaneClampsOnResizeAlone(t *testing.T) { st := &SplitPaneState{Size: 299} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SplitPane(c, st, SplitPaneOptions{}, func() { ui.Text(c, "Files") }, func() { ui.Text(c, "Editor") }).Element.Fill() }, 400, 200) tt.SetSize(250, 200) diff --git a/stack.go b/layout/stack.go similarity index 88% rename from stack.go rename to layout/stack.go index 6e4b593..9e40a63 100644 --- a/stack.go +++ b/layout/stack.go @@ -1,6 +1,10 @@ -package mujica +package layout -import "github.com/egoist/mygo/ui" +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" +) // StackAlign places children across a Stack's direction. Zero stretches a vertical stack and centers a horizontal one. type StackAlign int @@ -38,7 +42,7 @@ func Stack(c *ui.Context, opts StackOptions, children func()) *ui.Element { if opts.Horizontal { e.Row() } - e.Gap(containerSpace(Density(c), opts.Gap, SpaceS)).Justify(opts.Justify) + e.Gap(containerSpace(core.Density(c), opts.Gap, SpaceS)).Justify(opts.Justify) switch opts.Align { case StackAlignAuto: case StackAlignStart: diff --git a/stack_test.go b/layout/stack_test.go similarity index 92% rename from stack_test.go rename to layout/stack_test.go index e208555..6a77166 100644 --- a/stack_test.go +++ b/layout/stack_test.go @@ -1,15 +1,17 @@ -package mujica +package layout import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStackVerticalGap(t *testing.T) { var a, b ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Stack(c, StackOptions{}, func() { a = ui.Box(c).Height(20).Bounds() b = ui.Box(c).Height(20).Bounds() @@ -27,7 +29,7 @@ func TestStackHiddenItemTakesNoGap(t *testing.T) { for _, hide := range []bool{false, true} { var a, last ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Stack(c, StackOptions{Horizontal: true, Gap: SpaceL}, func() { a = ui.Box(c).Size(30, 10).Bounds() if !hide { @@ -49,7 +51,7 @@ func TestStackHiddenItemTakesNoGap(t *testing.T) { func TestStackAlignEnd(t *testing.T) { var r ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(200).Children(func() { Stack(c, StackOptions{Align: StackAlignEnd}, func() { r = ui.Box(c).Size(50, 10).Bounds() @@ -64,7 +66,7 @@ func TestStackAlignEnd(t *testing.T) { func TestStackJustifyAndWrap(t *testing.T) { var first, third ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(250).Children(func() { Stack(c, StackOptions{Horizontal: true, Wrap: true, Gap: SpaceNone, Justify: ui.End}, func() { first = ui.Box(c).Size(100, 20).Bounds() @@ -88,14 +90,14 @@ func TestStackWrapNeedsHorizontal(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Stack(c, StackOptions{Wrap: true}, func() {}) }, 100, 100) } func TestStackSnapshot(t *testing.T) { snapshot(t, "stack", 420, 150, func(c *ui.Context) { - k := Tokens(c) + k := core.Tokens(c) chip := func(s string) { ui.Box(c).Padding(6, 12).Background(k.Surface).Border(1, k.Border).Radius(2).Children(func() { ui.Text(c, s) }) } diff --git a/status_bar.go b/layout/status_bar.go similarity index 89% rename from status_bar.go rename to layout/status_bar.go index 18f315c..7929652 100644 --- a/status_bar.go +++ b/layout/status_bar.go @@ -1,4 +1,4 @@ -package mujica +package layout import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StatusItemTone colors a status item's text and icon. The zero value is muted text. @@ -58,13 +59,13 @@ const ( // StatusBar is a window's bottom strip with left and right groups of status items. func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) statusBarCheck(opts) gap := d.Unit() * 2 e := ui.Row(c).MinHeight(max(theme.MinHitSize, d.ControlHeight()-8)).Shrink(0).PaddingX(d.Unit()*2).Gap(gap). Background(k.Surface).BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border). - Label(msg(c, "status_bar.label")) + Label(core.Msg(c, "status_bar.label")) was := e.Bounds() internal.Remeasure(c, e, was.W, was.H) hidden := statusBarHide(c, opts, was.W-d.Unit()*4, gap) @@ -108,7 +109,7 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult { } if *open { ui.PopoverBase(c, more, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Margin(d.Unit(), 0, 0, 0).Padding(d.Unit()).Gap(d.Unit()).AlignItems(ui.Stretch) for _, it := range statusBarAll(opts) { if hidden[it.ID] { @@ -126,10 +127,10 @@ func StatusBar(c *ui.Context, opts StatusBarOptions) StatusBarResult { // statusBarMore is the "+N" button that opens the hidden items. func statusBarMore(c *ui.Context, n int) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) b := ui.ButtonBase(c).Key("status_bar.more").MinHeight(theme.MinHitSize).MinWidth(theme.MinHitSize). - PaddingX(Density(c).Unit()).Radius(theme.ControlRadius).TextColor(k.Text). - Label(fmt.Sprintf(msg(c, "status_bar.hidden"), n)) + PaddingX(core.Density(c).Unit()).Radius(theme.ControlRadius).TextColor(k.Text). + Label(fmt.Sprintf(core.Msg(c, "status_bar.hidden"), n)) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Pressed() { b.Background(k.Selection) @@ -137,7 +138,7 @@ func statusBarMore(c *ui.Context, n int) *ui.Element { b.Background(k.SurfaceHover) } return b.Children(func() { - ui.Text(c, fmt.Sprintf("+%d", n)).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, fmt.Sprintf("+%d", n)).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) } @@ -169,7 +170,7 @@ func statusBarHide(c *ui.Context, opts StatusBarOptions, room, gap float32) map[ // Not laid out yet: show all; Remeasure rebuilds once measured. return hidden } - u := Density(c).Unit() + u := core.Density(c).Unit() group := func(items []StatusItem) float32 { var w float32 n := 0 @@ -185,7 +186,7 @@ func statusBarHide(c *ui.Context, opts StatusBarOptions, room, gap float32) map[ need := func() float32 { w := group(opts.Left) + group(opts.Right) + 2*gap if len(hidden) > 0 { - tw, _ := c.MeasureText(0, ui.Span{Text: fmt.Sprintf("+%d", len(hidden)), Size: FontSize(c, theme.CaptionSize)}) + tw, _ := c.MeasureText(0, ui.Span{Text: fmt.Sprintf("+%d", len(hidden)), Size: core.FontSize(c, theme.CaptionSize)}) w += max(theme.MinHitSize, tw+2*u) + gap } return w @@ -201,13 +202,13 @@ func statusBarHide(c *ui.Context, opts StatusBarOptions, room, gap float32) map[ // statusBarWidth is the width an item takes, as statusBarItem lays it out. func statusBarWidth(c *ui.Context, it StatusItem) float32 { - u := Density(c).Unit() + u := core.Density(c).Unit() var parts []float32 if it.Icon != nil { parts = append(parts, statusBarIcon) } if it.Text != "" { - tw, _ := c.MeasureText(0, ui.Span{Text: it.Text, Size: FontSize(c, theme.CaptionSize)}) + tw, _ := c.MeasureText(0, ui.Span{Text: it.Text, Size: core.FontSize(c, theme.CaptionSize)}) parts = append(parts, tw) } if it.ShowProgress { @@ -228,8 +229,8 @@ func statusBarWidth(c *ui.Context, it StatusItem) float32 { // statusBarItem builds one item and reports whether its action was clicked and whether it has the focus. func statusBarItem(c *ui.Context, it StatusItem) (clicked, focused bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) fg := k.TextMuted if it.Action { fg = k.Text @@ -268,7 +269,7 @@ func statusBarItem(c *ui.Context, it StatusItem) (clicked, focused bool) { ui.Icon(c, it.Icon).Size(statusBarIcon, statusBarIcon) } if it.Text != "" { - ui.Text(c, it.Text).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, it.Text).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() } if it.ShowProgress { statusBarProgress(c, it.Progress) @@ -278,7 +279,7 @@ func statusBarItem(c *ui.Context, it StatusItem) (clicked, focused bool) { } func statusBarProgress(c *ui.Context, v float32) { - k := Tokens(c) + k := core.Tokens(c) ui.Box(c).Size(statusBarProgressWidth, 4).Shrink(0).Role(ui.RoleProgress). Description(fmt.Sprintf("%.0f%%", v*100)). Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/status_bar_test.go b/layout/status_bar_test.go similarity index 94% rename from status_bar_test.go rename to layout/status_bar_test.go index 541f099..44e9465 100644 --- a/status_bar_test.go +++ b/layout/status_bar_test.go @@ -1,4 +1,4 @@ -package mujica +package layout import ( "fmt" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func statusBarSample() StatusBarOptions { @@ -28,7 +29,7 @@ func statusBarSample() StatusBarOptions { func TestStatusBarActionClick(t *testing.T) { var clicked []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := StatusBar(c, statusBarSample()); r.Clicked != "" { clicked = append(clicked, r.Clicked) } @@ -45,7 +46,7 @@ func TestStatusBarActionClick(t *testing.T) { func TestStatusBarActionKeyboard(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if StatusBar(c, statusBarSample()).Clicked == "bell" { n++ } @@ -63,7 +64,7 @@ func TestStatusBarActionKeyboard(t *testing.T) { func TestStatusBarCollapsesSecondary(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, statusBarSample()) }, 800, 100) for _, s := range []string{"main", "Syncing", "2 warnings", "UTF-8", "Ln 12, Col 4"} { @@ -109,7 +110,7 @@ func TestStatusBarManyActionsKeepMinimumWidth(t *testing.T) { Icon: icons.Must("bell"), Action: true, Secondary: i >= 8}) } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, o) }, 560, 100) shown := 0 @@ -134,7 +135,7 @@ func TestStatusBarManyActionsKeepMinimumWidth(t *testing.T) { func TestStatusBarHiddenActionsByKeyboard(t *testing.T) { var clicked []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if id := StatusBar(c, StatusBarOptions{ Left: []StatusItem{{ID: "main", Text: "A long primary status text that fills the bar"}}, Right: []StatusItem{{ID: "fmt", Text: "Format", Action: true, Secondary: true}}, @@ -173,7 +174,7 @@ func TestStatusBarHiddenActionsByKeyboard(t *testing.T) { func TestStatusBarIconSize(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, StatusBarOptions{Left: []StatusItem{{ID: "i", Label: "Online", Icon: icons.Must("wifi-off")}}}) }, 400, 100) // Padding of one unit each side around a 16 DIP icon. @@ -184,7 +185,7 @@ func TestStatusBarIconSize(t *testing.T) { func TestStatusBarTextScale(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, StatusBarOptions{Left: []StatusItem{{ID: "r", Text: "Ready"}}}) }, 400, 200) plain, _ := tt.Find("Ready") @@ -214,7 +215,7 @@ func TestStatusBarRejectsBadItems(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, o) }, 400, 100) }() @@ -233,7 +234,7 @@ func TestStatusBarSnapshot(t *testing.T) { func TestStatusBarFoldsOnResizeAlone(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) StatusBar(c, statusBarSample()) }, 400, 100) tt.SetSize(160, 100) @@ -256,7 +257,7 @@ func TestStatusBarKeepsFocusAcrossDensity(t *testing.T) { Right: []StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: density}) + core.Use(c, core.Settings{Density: density}) if StatusBar(c, o).Clicked == "bell" { n++ } @@ -295,7 +296,7 @@ func TestStatusBarFocusMovesToMoreWhenFocusedItemFolds(t *testing.T) { Right: []StatusItem{{ID: "bell", Label: "Notifications", Icon: icons.Must("bell"), Action: true}}, } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: density}) + core.Use(c, core.Settings{Density: density}) if id := StatusBar(c, o).Clicked; id != "" { clicked = append(clicked, id) } diff --git a/title_bar.go b/layout/title_bar.go similarity index 79% rename from title_bar.go rename to layout/title_bar.go index ee83d64..3537773 100644 --- a/title_bar.go +++ b/layout/title_bar.go @@ -1,9 +1,10 @@ -package mujica +package layout import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TitleBarOptions configure a TitleBar. @@ -16,8 +17,8 @@ type TitleBarOptions struct { // TitleBar is a window title row that drags the window and clears the system buttons. func TitleBar(c *ui.Context, title string, opts TitleBarOptions) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) bar := c.TitleBar() pad := d.Unit() * 3 e := ui.Row(c).MinHeight(max(bar.Height, d.ControlHeight()+d.Unit()*2)).Shrink(0). @@ -28,7 +29,7 @@ func TitleBar(c *ui.Context, title string, opts TitleBarOptions) *ui.Element { if opts.Leading != nil { ui.Row(c).Gap(d.Unit()).Shrink(0).Children(opts.Leading) } - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.BodySize+1)).FontWeight(600). + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.BodySize+1)).FontWeight(600). SingleLine().Grow(1).Shrink(1).MinWidth(0) if opts.Trailing != nil { ui.Row(c).Gap(d.Unit()).Shrink(0).Children(opts.Trailing) diff --git a/title_bar_test.go b/layout/title_bar_test.go similarity index 78% rename from title_bar_test.go rename to layout/title_bar_test.go index 6268419..42aba65 100644 --- a/title_bar_test.go +++ b/layout/title_bar_test.go @@ -1,15 +1,18 @@ -package mujica +package layout import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestTitleBarDefaultHeight(t *testing.T) { var r ui.Rect ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r = TitleBar(c, "Archive", TitleBarOptions{}).Bounds() }, 400, 300).Frame() if r.H != 44 || r.W != 400 { @@ -20,9 +23,9 @@ func TestTitleBarDefaultHeight(t *testing.T) { func TestTitleBarClearsWindowControls(t *testing.T) { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r = TitleBar(c, "Archive", TitleBarOptions{ - Trailing: func() { Button(c, "Share", ButtonOptions{Variant: Ghost}) }, + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, }).Bounds() }, 400, 300) tt.SetTitleBar(ui.TitleBar{Height: 52, Left: 78, Right: 40}) @@ -43,10 +46,10 @@ func TestTitleBarClearsWindowControls(t *testing.T) { func TestTitleBarSlotsStayClickable(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TitleBar(c, "Archive", TitleBarOptions{ Leading: func() { - if Button(c, "Menu", ButtonOptions{Variant: Ghost}).Clicked() { + if input.Button(c, "Menu", input.ButtonOptions{Variant: input.Ghost}).Clicked() { n++ } }, @@ -62,9 +65,9 @@ func TestTitleBarSlotsStayClickable(t *testing.T) { func TestTitleBarLongTitleKeepsTools(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) TitleBar(c, "A very long window title that cannot possibly fit in the bar", TitleBarOptions{ - Trailing: func() { Button(c, "Share", ButtonOptions{Variant: Ghost}) }, + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, }) }, 240, 100) share, ok := tt.Find("Share") @@ -76,7 +79,7 @@ func TestTitleBarLongTitleKeepsTools(t *testing.T) { func TestTitleBarSnapshot(t *testing.T) { snapshot(t, "title_bar", 420, 60, func(c *ui.Context) { TitleBar(c, "Chronicle of the Court", TitleBarOptions{ - Trailing: func() { Button(c, "Share", ButtonOptions{Variant: Ghost}) }, + Trailing: func() { input.Button(c, "Share", input.ButtonOptions{Variant: input.Ghost}) }, }) }) } @@ -84,7 +87,7 @@ func TestTitleBarSnapshot(t *testing.T) { func TestTitleBarTextScale(t *testing.T) { var bar ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) bar = TitleBar(c, "Archive", TitleBarOptions{}).Bounds() }, 400, 300) plain, _ := tt.Find("Archive") diff --git a/audio_player.go b/media/audio_player.go similarity index 75% rename from audio_player.go rename to media/audio_player.go index 144976a..8b38bdc 100644 --- a/audio_player.go +++ b/media/audio_player.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AudioState is what the caller's playback reports. @@ -29,9 +31,9 @@ func AudioPlayer(c *ui.Context, st AudioState, opts AudioPlayerOptions) *ui.Elem if opts.OnPlay == nil || opts.OnPause == nil || opts.OnSeek == nil { panic("mujica: AudioPlayer needs OnPlay, OnPause and OnSeek") } - k := Tokens(c) - u := Density(c).Unit() - row := ui.Row(c).Gap(u*3).AlignItems(ui.Center).MinHeight(Density(c).ControlHeight()).Padding(u*2, u*3). + k := core.Tokens(c) + u := core.Density(c).Unit() + row := ui.Row(c).Gap(u*3).AlignItems(ui.Center).MinHeight(core.Density(c).ControlHeight()).Padding(u*2, u*3). Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup). Disabled(opts.Disabled) if st.Title != "" { @@ -50,7 +52,7 @@ func AudioPlayer(c *ui.Context, st AudioState, opts AudioPlayerOptions) *ui.Elem } } }) - ui.Text(c, mediaClock(st.Position)+" / "+mediaClock(st.Duration)).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, core.MediaClock(st.Position)+" / "+core.MediaClock(st.Duration)).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() }) // Space plays or pauses wherever the focus is inside. @@ -62,11 +64,11 @@ func AudioPlayer(c *ui.Context, st AudioState, opts AudioPlayerOptions) *ui.Elem // mediaPlayToggle is the play/pause button; it calls the matching callback. func mediaPlayToggle(c *ui.Context, playing bool, onPlay, onPause func()) *ui.Element { - icon, label := mediaPlay, msg(c, "media_controls.play") + icon, label := core.MediaPlay, core.Msg(c, "media_controls.play") if playing { - icon, label = mediaPause, msg(c, "media_controls.pause") + icon, label = core.MediaPause, core.Msg(c, "media_controls.pause") } - b := IconButton(c, icon, label, IconButtonOptions{}) + b := input.IconButton(c, icon, label, input.IconButtonOptions{}) if b.Clicked() && !b.IsDisabled() { mediaToggle(playing, onPlay, onPause) } diff --git a/audio_player_test.go b/media/audio_player_test.go similarity index 96% rename from audio_player_test.go rename to media/audio_player_test.go index 1f90d57..b284cee 100644 --- a/audio_player_test.go +++ b/media/audio_player_test.go @@ -1,10 +1,12 @@ -package mujica +package media import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type mediaCalls struct { @@ -18,7 +20,7 @@ func (m *mediaCalls) opts() AudioPlayerOptions { func audioTester(st *AudioState, m *mediaCalls, disabled bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := m.opts() o.Disabled = disabled ui.Column(c).Padding(20).Children(func() { AudioPlayer(c, *st, o) }) diff --git a/audio_spectrum.go b/media/audio_spectrum.go similarity index 79% rename from audio_spectrum.go rename to media/audio_spectrum.go index 13ebd33..492c2b4 100644 --- a/audio_spectrum.go +++ b/media/audio_spectrum.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AudioSpectrumOptions configure an AudioSpectrum. @@ -18,7 +20,7 @@ type AudioSpectrumOptions struct { // BandLabel names band i in the readout; nil numbers them. BandLabel func(i int) string // Status and Error show loading and failure at one size. - Status DataStatus + Status data.DataStatus Error string // Label names the chart for assistive technology. Label string @@ -40,10 +42,10 @@ func AudioSpectrum(c *ui.Context, bands []float32, opts AudioSpectrumOptions) *u if h == 0 { h = 120 } - k := Tokens(c) + k := core.Tokens(c) label := opts.Label if label == "" { - label = msg(c, "audio_spectrum.label") + label = core.Msg(c, "audio_spectrum.label") } e := ui.Box(c).Height(h).MinWidth(120).Radius(theme.ControlRadius).Background(k.Surface). Border(theme.BorderWidth, k.Border).Focusable().Label(label) @@ -57,7 +59,7 @@ func AudioSpectrum(c *ui.Context, bands []float32, opts AudioSpectrumOptions) *u if opts.BandLabel != nil { return opts.BandLabel(i) } - return fmt.Sprintf(msg(c, "audio_spectrum.band"), i+1) + return fmt.Sprintf(core.Msg(c, "audio_spectrum.band"), i+1) } if moved, ok := chartCursor(e, cur, len(bands)); ok && moved { c.Announce(spectrumReadout(name(*cur), bands[*cur])) @@ -67,7 +69,7 @@ func AudioSpectrum(c *ui.Context, bands []float32, opts AudioSpectrumOptions) *u readout = spectrumReadout(name(*cur), bands[*cur]) e.Description(readout) } - sel, size := *cur, FontSize(c, theme.CaptionSize) + sel, size := *cur, core.FontSize(c, theme.CaptionSize) e.Draw(func(p *ui.Painter, r ui.Rect) { pad := float32(8) n := float32(len(bands)) @@ -118,12 +120,12 @@ func chartCursor(e *ui.Element, cur *int, n int) (moved, ok bool) { } // mediaChartState fills e with a loading, error or empty state. -func mediaChartState(c *ui.Context, e *ui.Element, status DataStatus, errText string, empty bool, emptyKey string) bool { - k := Tokens(c) +func mediaChartState(c *ui.Context, e *ui.Element, status data.DataStatus, errText string, empty bool, emptyKey string) bool { + k := core.Tokens(c) switch { - case status == DataLoading: - e.Center().Children(func() { Spinner(c, SpinnerOptions{ShowLabel: true}) }) - case status == DataFailed: + case status == data.DataLoading: + e.Center().Children(func() { core.Spinner(c, core.SpinnerOptions{ShowLabel: true}) }) + case status == data.DataFailed: if errText == "" { panic("mujica: a failed media chart needs Error") } @@ -133,11 +135,11 @@ func mediaChartState(c *ui.Context, e *ui.Element, status DataStatus, errText st ui.Text(c, errText).TextColor(k.Danger).MaxLines(2) }) }) - case status != DataReady: + case status != data.DataReady: panic(fmt.Sprintf("mujica: media chart Status %d is not Ready, Loading or Failed", status)) case empty: - e.Center().Description(msg(c, emptyKey)).Children(func() { - ui.Text(c, msg(c, emptyKey)).TextColor(k.TextMuted) + e.Center().Description(core.Msg(c, emptyKey)).Children(func() { + ui.Text(c, core.Msg(c, emptyKey)).TextColor(k.TextMuted) }) default: return false diff --git a/audio_spectrum_test.go b/media/audio_spectrum_test.go similarity index 78% rename from audio_spectrum_test.go rename to media/audio_spectrum_test.go index fb1d21b..9820e7b 100644 --- a/audio_spectrum_test.go +++ b/media/audio_spectrum_test.go @@ -1,16 +1,19 @@ -package mujica +package media import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestAudioSpectrumKeyboardReadout(t *testing.T) { bands := []float32{0.2, 0.9, 0.5} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(300).Children(func() { AudioSpectrum(c, bands, AudioSpectrumOptions{BandLabel: func(i int) string { return []string{"63 Hz", "1 kHz", "8 kHz"}[i] }}) }) @@ -28,7 +31,7 @@ func TestAudioSpectrumNeverAnimates(t *testing.T) { builds := 0 bands := []float32{0.3, 0.6} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) builds++ AudioSpectrum(c, bands, AudioSpectrumOptions{}) }, 300, 200) @@ -41,18 +44,18 @@ func TestAudioSpectrumNeverAnimates(t *testing.T) { func TestAudioSpectrumStates(t *testing.T) { var got []ui.Rect - for _, o := range []AudioSpectrumOptions{{}, {Status: DataLoading}, {Status: DataFailed, Error: "Device lost"}} { + for _, o := range []AudioSpectrumOptions{{}, {Status: data.DataLoading}, {Status: data.DataFailed, Error: "Device lost"}} { var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(300).Children(func() { r = AudioSpectrum(c, nil, o).Bounds() }) }, 400, 200) tt.Frame() got = append(got, r) - if o.Status == DataFailed && !tt.HasText("Device lost") { + if o.Status == data.DataFailed && !tt.HasText("Device lost") { t.Fatal("error not shown") } - if o.Status == DataReady && !tt.HasText("No signal") { + if o.Status == data.DataReady && !tt.HasText("No signal") { t.Fatal("empty state not shown") } } @@ -60,7 +63,7 @@ func TestAudioSpectrumStates(t *testing.T) { t.Fatalf("state sizes %v", got) } mediaPanics(t, "band over 1", func(c *ui.Context) { AudioSpectrum(c, []float32{2}, AudioSpectrumOptions{}) }) - mediaPanics(t, "failed without error", func(c *ui.Context) { AudioSpectrum(c, nil, AudioSpectrumOptions{Status: DataFailed}) }) + mediaPanics(t, "failed without error", func(c *ui.Context) { AudioSpectrum(c, nil, AudioSpectrumOptions{Status: data.DataFailed}) }) } func TestAudioSpectrumSnapshot(t *testing.T) { diff --git a/audio_waveform.go b/media/audio_waveform.go similarity index 91% rename from audio_waveform.go rename to media/audio_waveform.go index f22a24c..db090c1 100644 --- a/audio_waveform.go +++ b/media/audio_waveform.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AudioWaveformOptions configure an AudioWaveform. @@ -22,7 +24,7 @@ type AudioWaveformOptions struct { Selection *MediaRange // Height is the chart's height; zero means 96. Height float32 - Status DataStatus + Status data.DataStatus Error string Label string } @@ -46,7 +48,7 @@ type waveformPress struct { var waveformBuckets int // AudioWaveform draws samples with played part and selectable range. -func AudioWaveform(c *ui.Context, samples []float32, opts AudioWaveformOptions) ChoiceResult { +func AudioWaveform(c *ui.Context, samples []float32, opts AudioWaveformOptions) core.ChoiceResult { h := opts.Height if h < 0 || opts.Position < 0 { panic("mujica: AudioWaveform Height and Position must not be negative") @@ -54,15 +56,15 @@ func AudioWaveform(c *ui.Context, samples []float32, opts AudioWaveformOptions) if h == 0 { h = 96 } - k := Tokens(c) + k := core.Tokens(c) label := opts.Label if label == "" { - label = msg(c, "audio_waveform.label") + label = core.Msg(c, "audio_waveform.label") } e := ui.Box(c).Height(h).MinWidth(120).Radius(theme.ControlRadius).Background(k.Surface). Border(theme.BorderWidth, k.Border).Focusable().Label(label).Cursor(ui.CursorText) internal.Ring(e, k.Focus, theme.ControlRadius) - v := ChoiceResult{Element: e} + v := core.ChoiceResult{Element: e} if mediaChartState(c, e, opts.Status, opts.Error, len(samples) == 0, "audio_waveform.empty") { return v } @@ -82,9 +84,9 @@ func AudioWaveform(c *ui.Context, samples []float32, opts AudioWaveformOptions) *cache = waveformPeaks{&samples[0], len(samples), cols, waveformBucket(samples, cols)} } if !e.IsDisabled() { - v.changed = waveformInput(c, e, r, opts) + v = core.NewChoiceResult(v.Element, waveformInput(c, e, r, opts)) } - e.Description(mediaClock(opts.Position) + " / " + mediaClock(opts.Duration)) + e.Description(core.MediaClock(opts.Position) + " / " + core.MediaClock(opts.Duration)) peaks, pos, sel := cache.peaks, float32(opts.Position)/float32(opts.Duration), opts.Selection dur := float32(opts.Duration) e.Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/audio_waveform_test.go b/media/audio_waveform_test.go similarity index 89% rename from audio_waveform_test.go rename to media/audio_waveform_test.go index 9ccb2c0..ed9a405 100644 --- a/audio_waveform_test.go +++ b/media/audio_waveform_test.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "math" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func waveSamples(n int) []float32 { @@ -18,7 +21,7 @@ func waveSamples(n int) []float32 { func waveTester(samples []float32, opts *AudioWaveformOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(400).Children(func() { if AudioWaveform(c, samples, *opts).Changed() { *n++ @@ -84,15 +87,15 @@ func TestAudioWaveformComputesPeaksOnce(t *testing.T) { func TestAudioWaveformStatesKeepSize(t *testing.T) { var got []ui.Rect seek := func(time.Duration) {} - for _, o := range []AudioWaveformOptions{{Duration: time.Second, OnSeek: seek}, {Status: DataLoading}, {Status: DataFailed, Error: "Decode failed"}} { + for _, o := range []AudioWaveformOptions{{Duration: time.Second, OnSeek: seek}, {Status: data.DataLoading}, {Status: data.DataFailed, Error: "Decode failed"}} { var r ui.Rect - data := waveSamples(100) - if o.Status != DataReady { - data = nil + samples := waveSamples(100) + if o.Status != data.DataReady { + samples = nil } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - ui.Column(c).Width(300).Children(func() { r = AudioWaveform(c, data, o).Element.Bounds() }) + core.Use(c, core.Settings{}) + ui.Column(c).Width(300).Children(func() { r = AudioWaveform(c, samples, o).Element.Bounds() }) }, 400, 200) tt.Frame() got = append(got, r) @@ -107,7 +110,7 @@ func TestAudioWaveformDisabled(t *testing.T) { opts := AudioWaveformOptions{Duration: time.Minute, OnSeek: func(d time.Duration) { seeks = append(seeks, d) }} n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Width(300).Children(func() { AudioWaveform(c, waveSamples(500), opts) }) }, 400, 200) r, _ := tt.Find("Waveform") diff --git a/camera_preview.go b/media/camera_preview.go similarity index 76% rename from camera_preview.go rename to media/camera_preview.go index 599365b..732d29a 100644 --- a/camera_preview.go +++ b/media/camera_preview.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "image" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CameraFrame is the caller's latest camera picture. @@ -35,23 +36,23 @@ type cameraCache struct { type cameraKey struct{} // CameraPreview shows frame at 16:9, mirrored while *mirror. -func CameraPreview(c *ui.Context, frame CameraFrame, mirror *bool, opts CameraPreviewOptions) ChoiceResult { - k := Tokens(c) +func CameraPreview(c *ui.Context, frame CameraFrame, mirror *bool, opts CameraPreviewOptions) core.ChoiceResult { + k := core.Tokens(c) old := *mirror label := opts.Label if label == "" { - label = msg(c, "camera_preview.label") + label = core.Msg(c, "camera_preview.label") } - box := ui.Box(c).FillWidth().AspectRatio(16.0 / 9).Background(videoBlack).Radius(theme.CardRadius).Clip(). + box := ui.Box(c).FillWidth().AspectRatio(16.0 / 9).Background(VideoBlack).Radius(theme.CardRadius).Clip(). Center().Focusable().Role(ui.RoleImage).Label(label).Disabled(opts.Disabled) internal.Ring(box, k.Focus, theme.CardRadius) cache := ui.Local(box, cameraKey{}, func() cameraCache { return cameraCache{} }) box.Children(func() { if frame.Image == nil { - box.Description(msg(c, "camera_preview.noSignal")) + box.Description(core.Msg(c, "camera_preview.noSignal")) ui.Column(c).Gap(8).AlignItems(ui.Center).Children(func() { - ui.Icon(c, mediaCamera).Size(28, 28).TextColor(ui.Hex("#BCAEAA")) - ui.Text(c, msg(c, "camera_preview.noSignal")).TextColor(ui.Hex("#BCAEAA")) + ui.Icon(c, core.MediaCamera).Size(28, 28).TextColor(ui.Hex("#BCAEAA")) + ui.Text(c, core.Msg(c, "camera_preview.noSignal")).TextColor(ui.Hex("#BCAEAA")) }) } else { // Copies are made once per frame number, not per paint. @@ -61,7 +62,7 @@ func CameraPreview(c *ui.Context, frame CameraFrame, mirror *bool, opts CameraPr ui.Image(c, cache.bmp).Fit(ui.Contain).Fill() } ui.Box(c).Absolute().Top(8).Right(8).Background(ui.RGBA(0, 0, 0, 0.5)).Radius(theme.ControlRadius).Children(func() { - b := mediaToggleAction(c, mediaMirror, msg(c, "camera_preview.mirror"), *mirror) + b := MediaToggleAction(c, core.MediaMirror, core.Msg(c, "camera_preview.mirror"), *mirror) if b.Clicked() && !b.IsDisabled() { *mirror = !*mirror } @@ -74,7 +75,7 @@ func CameraPreview(c *ui.Context, frame CameraFrame, mirror *bool, opts CameraPr if changed { slog.Debug("mujica: camera_preview mirror", "on", *mirror) } - return ChoiceResult{Element: box, changed: changed} + return core.NewChoiceResult(box, changed) } // cameraBitmap copies img into a bitmap, flipped when mirror. diff --git a/camera_preview_test.go b/media/camera_preview_test.go similarity index 96% rename from camera_preview_test.go rename to media/camera_preview_test.go index cde85b9..f03c1bb 100644 --- a/camera_preview_test.go +++ b/media/camera_preview_test.go @@ -1,15 +1,17 @@ -package mujica +package media import ( "image" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func cameraTester(frame *CameraFrame, mirror *bool, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(320).Children(func() { if CameraPreview(c, *frame, mirror, CameraPreviewOptions{Disabled: disabled}).Changed() { *n++ diff --git a/media/device_selector.go b/media/device_selector.go new file mode 100644 index 0000000..6ea83f6 --- /dev/null +++ b/media/device_selector.go @@ -0,0 +1,80 @@ +package media + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" +) + +// MediaDevice is one device; IDs are unique within a kind. +type MediaDevice struct { + ID, Name string +} + +// MediaDevices are the caller's lists; nil hides that kind. +type MediaDevices struct { + Microphones, Cameras, Speakers []MediaDevice +} + +// DeviceSelection holds each kind's chosen ID; empty is none. +type DeviceSelection struct { + Microphone, Camera, Speaker string +} + +// DeviceSelectorOptions configure a DeviceSelector. +type DeviceSelectorOptions struct { + Disabled bool +} + +// DeviceSelector chooses a microphone, camera and speaker. +func DeviceSelector(c *ui.Context, sel *DeviceSelection, devices MediaDevices, opts DeviceSelectorOptions) core.ChoiceResult { + old := *sel + k := core.Tokens(c) + u := core.Density(c).Unit() + col := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(core.Msg(c, "device_selector.label")).Disabled(opts.Disabled) + col.Children(func() { + deviceRow(c, k, core.MediaMic, "device_selector.microphone", &sel.Microphone, devices.Microphones, opts.Disabled) + deviceRow(c, k, core.MediaCamera, "device_selector.camera", &sel.Camera, devices.Cameras, opts.Disabled) + deviceRow(c, k, core.MediaSpeaker, "device_selector.speaker", &sel.Speaker, devices.Speakers, opts.Disabled) + }) + changed := *sel != old + if changed { + slog.Debug("mujica: device_selector changed", "selection", *sel) + } + return core.NewChoiceResult(col, changed) +} + +func deviceRow(c *ui.Context, k theme.Tokens, icon *ui.SVG, key string, id *string, list []MediaDevice, disabled bool) { + if list == nil { + return + } + options := make([]input.SelectOption[string], len(list)) + for i, d := range list { + if d.ID == "" { + panic("mujica: DeviceSelector device without an ID") + } + for _, o := range options[:i] { + if o.Value == d.ID { + panic("mujica: DeviceSelector duplicate device ID " + d.ID) + } + } + options[i] = input.SelectOption[string]{Value: d.ID, Label: d.Name} + } + if _, ok := input.SelectLabel(options, *id); *id != "" && !ok { + panic("mujica: DeviceSelector selection " + *id + " is not in its list") + } + ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { + ui.Icon(c, icon).Size(16, 16).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, key)).Width(96).SingleLine() + hint := core.Msg(c, "device_selector.choose") + if len(list) == 0 { + hint = core.Msg(c, "device_selector.none") + } + input.Select(c, id, options, input.SelectOptions{Disabled: disabled || len(list) == 0, Label: core.Msg(c, key), + Placeholder: hint, Empty: core.Msg(c, "device_selector.none")}).Element.Grow(1) + }) +} diff --git a/device_selector_test.go b/media/device_selector_test.go similarity index 96% rename from device_selector_test.go rename to media/device_selector_test.go index 74c8b79..e5a85d1 100644 --- a/device_selector_test.go +++ b/media/device_selector_test.go @@ -1,9 +1,11 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var deviceTestList = MediaDevices{ @@ -14,7 +16,7 @@ var deviceTestList = MediaDevices{ func deviceTester(sel *DeviceSelection, devices MediaDevices, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(420).Children(func() { if DeviceSelector(c, sel, devices, DeviceSelectorOptions{Disabled: disabled}).Changed() { *n++ diff --git a/media/doc.go b/media/doc.go new file mode 100644 index 0000000..aefc596 --- /dev/null +++ b/media/doc.go @@ -0,0 +1,2 @@ +// Package media holds MujicaUI audio, video, image and device controls. +package media diff --git a/media/example_helpers_test.go b/media/example_helpers_test.go new file mode 100644 index 0000000..b7c468a --- /dev/null +++ b/media/example_helpers_test.go @@ -0,0 +1,8 @@ +package media_test + +import ( + "image" +) + +// examplePhoto stands in for a decoded picture. +var examplePhoto = image.NewRGBA(image.Rect(0, 0, 320, 180)) diff --git a/media/example_media_test.go b/media/example_media_test.go new file mode 100644 index 0000000..f216d2b --- /dev/null +++ b/media/example_media_test.go @@ -0,0 +1,245 @@ +package media_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/media" +) + +func ExampleImageCropper() { + crop := media.ImageCrop{} // empty: starts fitted to the image + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if media.ImageCropper(c, examplePhoto, &crop, media.ImageCropperOptions{Aspect: 1}).Changed() { + _ = crop.Rect // in pixels of the photo turned by crop.Rotation + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImageAnnotator() { + photo := ui.NewBitmap(examplePhoto) + var notes []media.Annotation + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if media.ImageAnnotator(c, photo, ¬es, media.ImageAnnotatorOptions{}).Changed() { + data, _ := media.ExportAnnotations(notes) + _ = data // save the JSON + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImageThumbnail() { + photo := ui.NewBitmap(examplePhoto) + selected := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if media.ImageThumbnail(c, photo, media.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() { + selected = !selected + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLightbox() { + images := []data.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}} + open, index := false, 0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "View photos", input.ButtonOptions{}).Clicked() { + open = true + } + media.Lightbox(c, &open, &index, images) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVideoPlayer() { + st := media.VideoState{Duration: 4 * time.Minute, Rate: 1} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.VideoPlayer(c, st, media.VideoPlayerOptions{ + OnPlay: func() { st.Playing = true }, // start your decoder + OnPause: func() { st.Playing = false }, // pause it + OnSeek: func(d time.Duration) { st.Position = d }, + OnRate: func(r float64) { st.Rate = r }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVideoScrubber() { + pos := 30 * time.Second + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.VideoScrubber(c, &pos, media.VideoScrubberOptions{ + Duration: 2 * time.Minute, + Buffered: []media.MediaRange{{Start: 0, End: time.Minute}}, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVideoThumbnailStrip() { + frames := []media.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}} + current := 0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if media.VideoThumbnailStrip(c, ¤t, frames, media.VideoThumbnailStripOptions{}).Changed() { + _ = frames[current].At // seek the player there + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAudioPlayer() { + st := media.AudioState{Title: "Overture", Duration: 3 * time.Minute} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.AudioPlayer(c, st, media.AudioPlayerOptions{ + OnPlay: func() { st.Playing = true }, + OnPause: func() { st.Playing = false }, + OnSeek: func(d time.Duration) { st.Position = d }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAudioWaveform() { + samples := make([]float32, 48000) // from your decoder, -1 to 1 + pos, sel := time.Duration(0), media.MediaRange{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.AudioWaveform(c, samples, media.AudioWaveformOptions{ + Duration: time.Second, Position: pos, Selection: &sel, + OnSeek: func(d time.Duration) { pos = d }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAudioSpectrum() { + bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate() + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.AudioSpectrum(c, bands, media.AudioSpectrumOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVolumeControl() { + volume, muted := 0.8, false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.VolumeControl(c, &volume, &muted, media.VolumeControlOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePlaybackSpeedControl() { + rate := 1.0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.PlaybackSpeedControl(c, &rate, media.PlaybackSpeedControlOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMediaControls() { + st := media.MediaControlsState{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.MediaControls(c, st, media.MediaControlsOptions{ + OnPrevious: func() {}, OnNext: func() {}, + OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false }, + OnLoop: func(m media.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePlaylist() { + items := []media.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}} + current := "a" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := media.Playlist(c, &items, media.PlaylistOptions{Current: current}); r.Played != "" { + current = r.Played + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSubtitleEditor() { + cues := []media.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.SubtitleEditor(c, &cues, media.SubtitleEditorOptions{}) + if len(media.CheckSubtitles(cues)) > 0 { + _ = cues // block saving until the issues are fixed + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleScreenRecorderControls() { + st := media.RecorderState{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.ScreenRecorderControls(c, st, media.ScreenRecorderControlsOptions{ + OnStart: func() { st.Status = media.RecorderRecording }, + OnPause: func() { st.Status = media.RecorderPaused }, + OnResume: func() { st.Status = media.RecorderRecording }, + OnStop: func() { st = media.RecorderState{} }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCameraPreview() { + frame := media.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame + mirror := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.CameraPreview(c, frame, &mirror, media.CameraPreviewOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMicLevelMeter() { + level := float32(0.4) // the input's peak, 0 to 1 of full scale + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.MicLevelMeter(c, level, media.MicLevelMeterOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDeviceSelector() { + devices := media.MediaDevices{ + Microphones: []media.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}}, + Speakers: []media.MediaDevice{{ID: "out-1", Name: "Headphones"}}, + } + sel := media.DeviceSelection{Microphone: "mic-1"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.DeviceSelector(c, &sel, devices, media.DeviceSelectorOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleImageCompare() { + before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto) + split := 0.5 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + media.ImageCompare(c, before, after, &split, media.ImageCompareOptions{}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/media/helpers_test.go b/media/helpers_test.go new file mode 100644 index 0000000..e7da9b3 --- /dev/null +++ b/media/helpers_test.go @@ -0,0 +1,61 @@ +package media + +import ( + "image" + "image/color" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// mediaPanics fails t unless fn panics. +func mediaPanics(t *testing.T, name string, fn func(c *ui.Context)) { + t.Helper() + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + fn(c) + }, 400, 300) + tt.Frame() +} + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// mediaTestImage is a w×h picture whose left half is wine and right half ivory. +func mediaTestImage(w, h int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := range h { + for x := range w { + c := color.RGBA{125, 32, 52, 255} + if x >= w/2 { + c = color.RGBA{242, 235, 221, 255} + } + if y < h/8 { + c = color.RGBA{173, 145, 100, 255} + } + img.Set(x, y, c) + } + } + return img +} + +func mediaTestBitmap(w, h int) *ui.Bitmap { return ui.NewBitmap(mediaTestImage(w, h)) } + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/image_annotator.go b/media/image_annotator.go similarity index 79% rename from image_annotator.go rename to media/image_annotator.go index 90461d2..9ce6b9a 100644 --- a/image_annotator.go +++ b/media/image_annotator.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "encoding/json" @@ -10,9 +10,13 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AnnotationKind is the shape of an Annotation. @@ -70,8 +74,8 @@ type annotatorState struct { type annotatorKey struct{} // ImageAnnotator draws, selects, moves and deletes marks over src. -func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts ImageAnnotatorOptions) ChoiceResult { - iw, ih := viewerSize(src) +func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts ImageAnnotatorOptions) core.ChoiceResult { + iw, ih := data.ViewerSize(src) size := image.Pt(int(iw), int(ih)) for _, n := range *notes { if n.Kind < AnnotateRect || n.Kind > AnnotateText || (n.Kind == AnnotateText && n.Text == "") { @@ -85,11 +89,13 @@ func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts if h == 0 { h = 320 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() before := slices.Clone(*notes) col := ui.Column(c).Gap(u * 2).Disabled(opts.Disabled) - st := ui.Local(col, annotatorKey{}, func() *annotatorState { return &annotatorState{selected: -1, draft: msg(c, "image_annotator.note")} }) + st := ui.Local(col, annotatorKey{}, func() *annotatorState { + return &annotatorState{selected: -1, draft: core.Msg(c, "image_annotator.note")} + }) s := *st if s.selected >= len(*notes) { s.selected = -1 @@ -97,7 +103,7 @@ func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts col.Children(func() { annotatorToolbar(c, k, s, notes) stage := ui.Box(c).FillWidth().Height(h).Focusable().Clip().Radius(theme.ControlRadius). - Background(videoBlack).Label(annotatorLabel(c, opts.Label)).Cursor(ui.CursorCrosshair) + Background(VideoBlack).Label(annotatorLabel(c, opts.Label)).Cursor(ui.CursorCrosshair) internal.Ring(stage, k.Focus, theme.ControlRadius) r := stage.Bounds() scale, ox, oy := cropperPlace(r, size) @@ -107,10 +113,10 @@ func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts } if s.selected >= 0 { n := (*notes)[s.selected] - stage.Description(fmt.Sprintf(msg(c, "image_annotator.selected"), s.selected+1, len(*notes), annotatorKind(c, n.Kind))) + stage.Description(fmt.Sprintf(core.Msg(c, "image_annotator.selected"), s.selected+1, len(*notes), annotatorKind(c, n.Kind))) } list, sel, preview := slices.Clone(*notes), s.selected, annotatorPreview(s) - size := FontSize(c, theme.BodySize) + size := core.FontSize(c, theme.BodySize) stage.Draw(func(p *ui.Painter, r ui.Rect) { scale, ox, oy := cropperPlace(r, image.Pt(int(iw), int(ih))) p.Image(src, ui.Rect{X: ox, Y: oy, W: iw * scale, H: ih * scale}, ui.FillBox) @@ -126,47 +132,47 @@ func ImageAnnotator(c *ui.Context, src ui.ImageSource, notes *[]Annotation, opts if changed { slog.Debug("mujica: image_annotator changed", "count", len(*notes)) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func annotatorLabel(c *ui.Context, l string) string { if l != "" { return l } - return msg(c, "image_annotator.label") + return core.Msg(c, "image_annotator.label") } func annotatorKind(c *ui.Context, k AnnotationKind) string { - return msg(c, []string{"image_annotator.rect", "image_annotator.arrow", "image_annotator.text"}[k]) + return core.Msg(c, []string{"image_annotator.rect", "image_annotator.arrow", "image_annotator.text"}[k]) } // annotatorToolbar picks tools, edits text and deletes the selection. func annotatorToolbar(c *ui.Context, k theme.Tokens, s *annotatorState, notes *[]Annotation) { - u := Density(c).Unit() + u := core.Density(c).Unit() ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Wrap().Children(func() { - SegmentedControl(c, &s.tool, []Segment{ - {Label: msg(c, "image_annotator.select"), Icon: mediaPointer, HideLabel: true}, - {Label: msg(c, "image_annotator.rect"), Icon: mediaRect, HideLabel: true}, - {Label: msg(c, "image_annotator.arrow"), Icon: mediaArrow, HideLabel: true}, - {Label: msg(c, "image_annotator.text"), Icon: mediaText, HideLabel: true}, - }, SegmentedControlOptions{Label: msg(c, "image_annotator.tool")}) + navigation.SegmentedControl(c, &s.tool, []navigation.Segment{ + {Label: core.Msg(c, "image_annotator.select"), Icon: core.MediaPointer, HideLabel: true}, + {Label: core.Msg(c, "image_annotator.rect"), Icon: core.MediaRect, HideLabel: true}, + {Label: core.Msg(c, "image_annotator.arrow"), Icon: core.MediaArrow, HideLabel: true}, + {Label: core.Msg(c, "image_annotator.text"), Icon: core.MediaText, HideLabel: true}, + }, navigation.SegmentedControlOptions{Label: core.Msg(c, "image_annotator.tool")}) editing := s.selected >= 0 && (*notes)[s.selected].Kind == AnnotateText if editing { s.draft = (*notes)[s.selected].Text } if s.tool == 3 || editing { - in := TextInput(c, &s.draft, TextInputOptions{Label: msg(c, "image_annotator.textLabel")}) + in := input.TextInput(c, &s.draft, input.TextInputOptions{Label: core.Msg(c, "image_annotator.textLabel")}) in.Element.Width(180) if editing && in.Changed() && s.draft != "" { (*notes)[s.selected].Text = s.draft } } - del := iconAction(c, icons.Must("trash-2"), msg(c, "image_annotator.delete")).Disabled(s.selected < 0) + del := core.IconAction(c, icons.Must("trash-2"), core.Msg(c, "image_annotator.delete")).Disabled(s.selected < 0) if del.Clicked() && !del.IsDisabled() { *notes = slices.Delete(*notes, s.selected, s.selected+1) s.selected = -1 } - ui.Textf(c, msg(c, "image_annotator.count"), len(*notes)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Textf(c, core.Msg(c, "image_annotator.count"), len(*notes)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) } @@ -266,7 +272,7 @@ func annotatorPaint(p *ui.Painter, k theme.Tokens, n Annotation, at func(image.P halo, ink := ui.Hex("#FBF7EF"), ui.Hex("#A12635") switch n.Kind { case AnnotateRect: - b := ui.Rect{X: min(x0, x1), Y: min(y0, y1), W: abs32(x1 - x0), H: abs32(y1 - y0)} + b := ui.Rect{X: min(x0, x1), Y: min(y0, y1), W: core.Abs32(x1 - x0), H: core.Abs32(y1 - y0)} p.Stroke(b, halo, 0, 4) p.Stroke(b, ink, 0, 2) case AnnotateArrow: @@ -285,7 +291,7 @@ func annotatorPaint(p *ui.Painter, k theme.Tokens, n Annotation, at func(image.P x1, y1 = x0+w+8, y0+h+4 } if selected { - b := ui.Rect{X: min(x0, x1) - 4, Y: min(y0, y1) - 4, W: abs32(x1-x0) + 8, H: abs32(y1-y0) + 8} + b := ui.Rect{X: min(x0, x1) - 4, Y: min(y0, y1) - 4, W: core.Abs32(x1-x0) + 8, H: core.Abs32(y1-y0) + 8} p.StrokeDashed(b, k.Focus, 0, 1) p.FillPath(internal.Diamond(internal.Snap(p, b.X), internal.Snap(p, b.Y), 8), k.Focus) } diff --git a/image_annotator_test.go b/media/image_annotator_test.go similarity index 97% rename from image_annotator_test.go rename to media/image_annotator_test.go index 2229d6a..c043b29 100644 --- a/image_annotator_test.go +++ b/media/image_annotator_test.go @@ -1,16 +1,18 @@ -package mujica +package media import ( "image" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func annotatorTester(notes *[]Annotation, disabled bool, n *int) *ui.Tester { src := mediaTestBitmap(400, 200) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(440).Children(func() { if ImageAnnotator(c, src, notes, ImageAnnotatorOptions{Height: 200, Disabled: disabled}).Changed() { *n++ diff --git a/image_compare.go b/media/image_compare.go similarity index 74% rename from image_compare.go rename to media/image_compare.go index d621e6f..026f472 100644 --- a/image_compare.go +++ b/media/image_compare.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ImageCompareOptions configure an ImageCompare. @@ -18,26 +21,26 @@ type ImageCompareOptions struct { } // ImageCompare shows before left of *split, after right. -func ImageCompare(c *ui.Context, before, after ui.ImageSource, split *float64, opts ImageCompareOptions) ChoiceResult { +func ImageCompare(c *ui.Context, before, after ui.ImageSource, split *float64, opts ImageCompareOptions) core.ChoiceResult { if *split < 0 || *split > 1 || math.IsNaN(*split) { panic("mujica: ImageCompare split outside 0..1") } - bw, bh := viewerSize(before) - viewerSize(after) - k := Tokens(c) + bw, bh := data.ViewerSize(before) + data.ViewerSize(after) + k := core.Tokens(c) bl, al := opts.BeforeLabel, opts.AfterLabel if bl == "" { - bl = msg(c, "image_compare.before") + bl = core.Msg(c, "image_compare.before") } if al == "" { - al = msg(c, "image_compare.after") + al = core.Msg(c, "image_compare.after") } - g, _ := sliderSetup(0, 1, 0.01, nil, false, c) + g, _ := input.SliderSetup(0, 1, 0.01, nil, false, c) old := *split var s *ui.Element var next float64 ui.Box(c).Disabled(opts.Disabled).Children(func() { - s, next, _ = sliderControl(c, old, g, 0, 1) + s, next, _ = input.SliderControl(c, old, g, 0, 1) }) s.FillWidth().AspectRatio(bw / bh).FocusRing(false).Cursor(ui.CursorResizeEW).Clip().Radius(theme.ControlRadius). Label(bl + " / " + al) @@ -49,7 +52,7 @@ func ImageCompare(c *ui.Context, before, after ui.ImageSource, split *float64, o if changed { slog.Debug("mujica: image_compare split", "split", *split) } - v, size := float32(*split), FontSize(c, theme.CaptionSize) + v, size := float32(*split), core.FontSize(c, theme.CaptionSize) s.Draw(func(p *ui.Painter, r ui.Rect) { x := internal.Snap(p, r.X+r.W*v) p.Image(after, r, ui.Cover) @@ -62,7 +65,7 @@ func ImageCompare(c *ui.Context, before, after ui.ImageSource, split *float64, o p.FillPath(internal.Diamond(x, cy, 22), k.Surface) p.FillPath(internal.Diamond(x, cy, 16), k.AccentText) }) - return ChoiceResult{Element: s, changed: changed} + return core.NewChoiceResult(s, changed) } // compareTag draws a caption chip at (x, y). diff --git a/image_compare_test.go b/media/image_compare_test.go similarity index 95% rename from image_compare_test.go rename to media/image_compare_test.go index 89c476b..e2006e5 100644 --- a/image_compare_test.go +++ b/media/image_compare_test.go @@ -1,15 +1,17 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func compareTester(split *float64, disabled bool, n *int) *ui.Tester { before, after := mediaTestBitmap(160, 90), mediaTestBitmap(160, 90) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(320).Children(func() { if ImageCompare(c, before, after, split, ImageCompareOptions{Disabled: disabled}).Changed() { *n++ diff --git a/image_cropper.go b/media/image_cropper.go similarity index 90% rename from image_cropper.go rename to media/image_cropper.go index aac97bb..492f33f 100644 --- a/image_cropper.go +++ b/media/image_cropper.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ImageCrop is Rect in pixels of the turned image. @@ -44,7 +45,7 @@ type cropperDrag struct { type cropperKey struct{} // ImageCropper edits *crop over src; empty Rect starts fitted. -func ImageCropper(c *ui.Context, src image.Image, crop *ImageCrop, opts ImageCropperOptions) ChoiceResult { +func ImageCropper(c *ui.Context, src image.Image, crop *ImageCrop, opts ImageCropperOptions) core.ChoiceResult { if src == nil || src.Bounds().Empty() || opts.Aspect < 0 || opts.Height < 0 { panic("mujica: ImageCropper needs an image, Aspect >= 0 and Height >= 0") } @@ -65,17 +66,17 @@ func ImageCropper(c *ui.Context, src image.Image, crop *ImageCrop, opts ImageCro if h == 0 { h = 320 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() old := *crop label := opts.Label if label == "" { - label = msg(c, "image_cropper.label") + label = core.Msg(c, "image_cropper.label") } col := ui.Column(c).Gap(u * 2).Disabled(opts.Disabled) col.Children(func() { stage := ui.Box(c).FillWidth().Height(h).Focusable().Clip().Radius(theme.ControlRadius). - Background(videoBlack).Label(label).Cursor(ui.CursorMove) + Background(VideoBlack).Label(label).Cursor(ui.CursorMove) internal.Ring(stage, k.Focus, theme.ControlRadius) r := stage.Bounds() scale, ox, oy := cropperPlace(r, size) @@ -85,15 +86,15 @@ func ImageCropper(c *ui.Context, src image.Image, crop *ImageCrop, opts ImageCro cropperKeys(stage, crop, size, opts.Aspect) } ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - left := iconAction(c, mediaRotateLeft, msg(c, "image_cropper.rotateLeft")) - right := iconAction(c, mediaRotate, msg(c, "image_cropper.rotateRight")) + left := core.IconAction(c, core.MediaRotateLeft, core.Msg(c, "image_cropper.rotateLeft")) + right := core.IconAction(c, core.MediaRotate, core.Msg(c, "image_cropper.rotateRight")) switch { case live && (right.Clicked() || stage.Shortcut(0, ui.KeyR)): cropperRotate(crop, size, 90, opts.Aspect) case live && (left.Clicked() || stage.Shortcut(ui.Shift, ui.KeyR)): cropperRotate(crop, size, 270, opts.Aspect) } - ui.Text(c, cropperReadout(*crop)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, cropperReadout(*crop)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") }) stage.Description(cropperReadout(*crop)) // Turned pixels are copied only when image or rotation change. @@ -129,7 +130,7 @@ func ImageCropper(c *ui.Context, src image.Image, crop *ImageCrop, opts ImageCro if changed { slog.Debug("mujica: image_cropper changed", "rect", crop.Rect, "rotation", crop.Rotation) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func cropperReadout(cr ImageCrop) string { @@ -232,7 +233,7 @@ func cropperPointer(stage *ui.Element, cr *ImageCrop, r ui.Rect, size image.Poin func cropperHit(r image.Rectangle, x, y, slop float32) int { corners := [4][2]int{{r.Min.X, r.Min.Y}, {r.Max.X, r.Min.Y}, {r.Min.X, r.Max.Y}, {r.Max.X, r.Max.Y}} for i, c := range corners { - if abs32(x-float32(c[0])) <= slop && abs32(y-float32(c[1])) <= slop { + if core.Abs32(x-float32(c[0])) <= slop && core.Abs32(y-float32(c[1])) <= slop { return 2 + i } } @@ -241,5 +242,3 @@ func cropperHit(r image.Rectangle, x, y, slop float32) int { } return 0 } - -func abs32(v float32) float32 { return float32(math.Abs(float64(v))) } diff --git a/image_cropper_geom.go b/media/image_cropper_geom.go similarity index 99% rename from image_cropper_geom.go rename to media/image_cropper_geom.go index 7d39e42..78ec86b 100644 --- a/image_cropper_geom.go +++ b/media/image_cropper_geom.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "image" diff --git a/image_cropper_test.go b/media/image_cropper_test.go similarity index 97% rename from image_cropper_test.go rename to media/image_cropper_test.go index 9ba67a5..fa3d432 100644 --- a/image_cropper_test.go +++ b/media/image_cropper_test.go @@ -1,15 +1,17 @@ -package mujica +package media import ( "image" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func cropperTester(src image.Image, crop *ImageCrop, opts ImageCropperOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(440).Children(func() { if ImageCropper(c, src, crop, opts).Changed() { *n++ diff --git a/image_thumbnail.go b/media/image_thumbnail.go similarity index 78% rename from image_thumbnail.go rename to media/image_thumbnail.go index 27d0c71..e8206c9 100644 --- a/image_thumbnail.go +++ b/media/image_thumbnail.go @@ -1,13 +1,14 @@ -package mujica +package media import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ImageThumbnailOptions configure an ImageThumbnail. @@ -26,7 +27,7 @@ type ImageThumbnailOptions struct { } // ImageThumbnail is a cropped picture; Changed reports activation. -func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOptions) ChoiceResult { +func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOptions) core.ChoiceResult { if opts.Width < 0 || opts.Aspect < 0 { panic("mujica: ImageThumbnail needs Width and Aspect >= 0") } @@ -40,7 +41,7 @@ func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOption if aspect == 0 { aspect = 1 } - k := Tokens(c) + k := core.Tokens(c) h := w / aspect b := ui.ButtonBase(c).Size(w, h).Radius(theme.ControlRadius).Clip(). Disabled(opts.Disabled).Label(opts.Alt).Selected(opts.Selected).Background(k.SurfaceHover) @@ -52,7 +53,7 @@ func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOption case b.Hovered(): edge = k.ControlBorder } - b.Border(width, edge).Transition(colorFade) + b.Border(width, edge).Transition(core.ColorFade) b.Children(func() { switch { case opts.Err != nil: @@ -60,11 +61,11 @@ func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOption ui.Column(c).Gap(4).AlignItems(ui.Center).Children(func() { ui.Icon(c, icons.Must("circle-alert")).Size(20, 20).TextColor(k.Danger) if h >= 64 { - ui.Text(c, msg(c, "image_thumbnail.failed")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() + ui.Text(c, core.Msg(c, "image_thumbnail.failed")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() } }) case opts.Loading: - Spinner(c, SpinnerOptions{Size: min(20, h/2), Label: msg(c, "image_thumbnail.loading")}) + core.Spinner(c, core.SpinnerOptions{Size: min(20, h/2), Label: core.Msg(c, "image_thumbnail.loading")}) case src == nil: ui.Icon(c, icons.Must("image")).Size(20, 20).TextColor(k.TextMuted) default: @@ -83,5 +84,5 @@ func ImageThumbnail(c *ui.Context, src ui.ImageSource, opts ImageThumbnailOption if clicked { slog.Debug("mujica: image_thumbnail click", "alt", opts.Alt) } - return ChoiceResult{Element: b, changed: clicked} + return core.NewChoiceResult(b, clicked) } diff --git a/image_thumbnail_test.go b/media/image_thumbnail_test.go similarity index 93% rename from image_thumbnail_test.go rename to media/image_thumbnail_test.go index fe83abf..7acda73 100644 --- a/image_thumbnail_test.go +++ b/media/image_thumbnail_test.go @@ -1,17 +1,19 @@ -package mujica +package media import ( "errors" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestImageThumbnailClickAndKeys(t *testing.T) { n := 0 bmp := mediaTestBitmap(120, 80) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).AlignItems(ui.Start).Children(func() { if ImageThumbnail(c, bmp, ImageThumbnailOptions{Alt: "Hall", Aspect: 4.0 / 3}).Changed() { n++ @@ -36,7 +38,7 @@ func TestImageThumbnailStatesKeepSize(t *testing.T) { o.Alt, o.Width = "Hall", 120 var r ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).AlignItems(ui.Start).Children(func() { r = ImageThumbnail(c, mediaTestBitmap(40, 40), o).Element.Bounds() }) @@ -54,7 +56,7 @@ func TestImageThumbnailStatesKeepSize(t *testing.T) { func TestImageThumbnailDisabledAndBad(t *testing.T) { n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if ImageThumbnail(c, nil, ImageThumbnailOptions{Alt: "Hall", Disabled: true}).Changed() { n++ } diff --git a/lightbox.go b/media/lightbox.go similarity index 71% rename from lightbox.go rename to media/lightbox.go index 4b19858..52a4b33 100644 --- a/lightbox.go +++ b/media/lightbox.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -6,14 +6,16 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Lightbox fills the window with images[*index] while *open. -func Lightbox(c *ui.Context, open *bool, index *int, images []ViewerImage) ChoiceResult { - var v ChoiceResult +func Lightbox(c *ui.Context, open *bool, index *int, images []data.ViewerImage) core.ChoiceResult { + var v core.ChoiceResult if !*open { return v } @@ -21,21 +23,21 @@ func Lightbox(c *ui.Context, open *bool, index *int, images []ViewerImage) Choic panic("mujica: Lightbox index out of range") } for _, im := range images { - viewerSize(im.Source) + data.ViewerSize(im.Source) } - u := Density(c).Unit() + u := core.Density(c).Unit() w, h := c.Size() light := ui.Hex("#F2EBDD") v.Element = ui.DialogBase(c, open, func(back, panel *ui.Element) { back.Background(ui.RGBA(0, 0, 0, 0.92)) - panel.Size(w, h).Padding(u * 4).Gap(u * 3).TextColor(light).Label(msg(c, "lightbox.title")). - Opacity(overlayEnter(c, panel)) + panel.Size(w, h).Padding(u * 4).Gap(u * 3).TextColor(light).Label(core.Msg(c, "lightbox.title")). + Opacity(core.OverlayEnter(c, panel)) goTo := func(i int) { if i < 0 || i >= len(images) || i == *index { return } - *index, v.changed = i, true - c.Announce(fmt.Sprintf(msg(c, "lightbox.position"), i+1, len(images))) + *index, v = i, core.NewChoiceResult(v.Element, true) + c.Announce(fmt.Sprintf(core.Msg(c, "lightbox.position"), i+1, len(images))) slog.Debug("mujica: lightbox image", "index", i) } switch { @@ -50,7 +52,7 @@ func Lightbox(c *ui.Context, open *bool, index *int, images []ViewerImage) Choic } img := images[*index] ui.Row(c).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "lightbox.position"), *index+1, len(images))).FontFeatures("tnum").Grow(1) + ui.Text(c, fmt.Sprintf(core.Msg(c, "lightbox.position"), *index+1, len(images))).FontFeatures("tnum").Grow(1) if lightboxButton(c, "x", "lightbox.close", false).Clicked() { *open = false } @@ -79,7 +81,7 @@ func Lightbox(c *ui.Context, open *bool, index *int, images []ViewerImage) Choic // lightboxButton is a 40 DIP icon button on the backdrop. func lightboxButton(c *ui.Context, icon, key string, disabled bool) *ui.Element { light := ui.Hex("#F2EBDD") - b := iconAction(c, icons.Must(icon), msg(c, key)).Size(40, 40).TextColor(light).Disabled(disabled) + b := core.IconAction(c, icons.Must(icon), core.Msg(c, key)).Size(40, 40).TextColor(light).Disabled(disabled) // The theme's focus color is too dark on the backdrop. internal.Ring(b, light, theme.ControlRadius) if b.Hovered() { diff --git a/lightbox_test.go b/media/lightbox_test.go similarity index 81% rename from lightbox_test.go rename to media/lightbox_test.go index d50e70c..3cb9691 100644 --- a/lightbox_test.go +++ b/media/lightbox_test.go @@ -1,13 +1,17 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) -func lightboxImages() []ViewerImage { - return []ViewerImage{ +func lightboxImages() []data.ViewerImage { + return []data.ViewerImage{ {Source: mediaTestBitmap(160, 90), Alt: "The grand hall"}, {Source: mediaTestBitmap(90, 160), Alt: "The east tower"}, {Source: mediaTestBitmap(120, 120), Alt: "The garden"}, @@ -18,8 +22,8 @@ func TestLightboxBrowseAndClose(t *testing.T) { open, index, n := false, 0, 0 images := lightboxImages() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Open gallery", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Open gallery", input.ButtonOptions{}).Clicked() { open = true } if Lightbox(c, &open, &index, images).Changed() { diff --git a/media_controls.go b/media/media_controls.go similarity index 71% rename from media_controls.go rename to media/media_controls.go index a0913d1..7ef17d1 100644 --- a/media_controls.go +++ b/media/media_controls.go @@ -1,10 +1,12 @@ -package mujica +package media import ( "fmt" "log/slog" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // LoopMode is how playback repeats. The zero value repeats nothing. @@ -44,30 +46,30 @@ func MediaControls(c *ui.Context, st MediaControlsState, opts MediaControlsOptio if st.Loop < LoopOff || st.Loop > LoopOne { panic(fmt.Sprintf("mujica: unknown LoopMode %d", st.Loop)) } - row := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar). - Label(msg(c, "media_controls.label")).Disabled(opts.Disabled) + row := ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar). + Label(core.Msg(c, "media_controls.label")).Disabled(opts.Disabled) row.Children(func() { - shuffle := mediaToggleAction(c, mediaShuffle, msg(c, "media_controls.shuffle"), st.Shuffle) + shuffle := MediaToggleAction(c, core.MediaShuffle, core.Msg(c, "media_controls.shuffle"), st.Shuffle) if shuffle.Clicked() && !shuffle.IsDisabled() { slog.Debug("mujica: media_controls shuffle", "on", !st.Shuffle) opts.OnShuffle(!st.Shuffle) } - prev := iconAction(c, mediaPrev, msg(c, "media_controls.previous")).Disabled(st.NoPrevious) + prev := core.IconAction(c, core.MediaPrev, core.Msg(c, "media_controls.previous")).Disabled(st.NoPrevious) if prev.Clicked() && !prev.IsDisabled() { slog.Debug("mujica: media_controls previous") opts.OnPrevious() } mediaPlayToggle(c, st.Playing, opts.OnPlay, opts.OnPause) - next := iconAction(c, mediaNext, msg(c, "media_controls.next")).Disabled(st.NoNext) + next := core.IconAction(c, core.MediaNext, core.Msg(c, "media_controls.next")).Disabled(st.NoNext) if next.Clicked() && !next.IsDisabled() { slog.Debug("mujica: media_controls next") opts.OnNext() } - icon := mediaRepeat + icon := core.MediaRepeat if st.Loop == LoopOne { - icon = mediaRepeatOne + icon = core.MediaRepeatOne } - loop := mediaToggleAction(c, icon, msg(c, []string{"media_controls.loopOff", "media_controls.loopAll", "media_controls.loopOne"}[st.Loop]), st.Loop != LoopOff) + loop := MediaToggleAction(c, icon, core.Msg(c, []string{"media_controls.loopOff", "media_controls.loopAll", "media_controls.loopOne"}[st.Loop]), st.Loop != LoopOff) if loop.Clicked() && !loop.IsDisabled() { slog.Debug("mujica: media_controls loop", "mode", st.Loop.next()) opts.OnLoop(st.Loop.next()) diff --git a/media_controls_test.go b/media/media_controls_test.go similarity index 96% rename from media_controls_test.go rename to media/media_controls_test.go index 9b2e220..961069b 100644 --- a/media_controls_test.go +++ b/media/media_controls_test.go @@ -1,9 +1,11 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type controlsCalls struct { @@ -20,7 +22,7 @@ func (m *controlsCalls) opts() MediaControlsOptions { func controlsTester(st *MediaControlsState, o MediaControlsOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { MediaControls(c, *st, o) }) }, 400, 120) } diff --git a/media_palette_test.go b/media/media_palette_test.go similarity index 91% rename from media_palette_test.go rename to media/media_palette_test.go index 3deed2a..54437a4 100644 --- a/media_palette_test.go +++ b/media/media_palette_test.go @@ -1,11 +1,11 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TestMediaChartInksContrast keeps every chart ink at 3:1 against Surface in both themes. diff --git a/mic_level_meter.go b/media/mic_level_meter.go similarity index 84% rename from mic_level_meter.go rename to media/mic_level_meter.go index d3aef5c..37fa579 100644 --- a/mic_level_meter.go +++ b/media/mic_level_meter.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MicLevelMeterOptions configure a MicLevelMeter. @@ -34,12 +35,12 @@ func MicLevelMeter(c *ui.Context, level float32, opts MicLevelMeterOptions) *ui. if hold == 0 { hold = 1500 * time.Millisecond } - k := Tokens(c) + k := core.Tokens(c) label := opts.Label if label == "" { - label = msg(c, "mic_level_meter.label") + label = core.Msg(c, "mic_level_meter.label") } - row := ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize) + row := ui.Row(c).Gap(core.Density(c).Unit() * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize) pk := ui.Local(row, micPeak{}, func() micPeak { return micPeak{} }) now := c.Now() switch { @@ -52,7 +53,7 @@ func MicLevelMeter(c *ui.Context, level float32, opts MicLevelMeterOptions) *ui. c.After(hold - now.Sub(pk.at)) } peak := pk.level - row.Role(ui.RoleMeter).Label(label).Description(micDB(level) + ", " + msg(c, "mic_level_meter.peak") + " " + micDB(peak)) + row.Role(ui.RoleMeter).Label(label).Description(micDB(level) + ", " + core.Msg(c, "mic_level_meter.peak") + " " + micDB(peak)) row.Children(func() { ui.Box(c).Height(12).MinWidth(120).Grow(1).Draw(func(p *ui.Painter, r ui.Rect) { gap := float32(2) @@ -74,7 +75,7 @@ func MicLevelMeter(c *ui.Context, level float32, opts MicLevelMeterOptions) *ui. } } }) - ui.Text(c, micDB(level)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). + ui.Text(c, micDB(level)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). TextColor(k.TextMuted).MinWidth(56).TextAlign(ui.End).SingleLine() }) return row diff --git a/mic_level_meter_test.go b/media/mic_level_meter_test.go similarity index 94% rename from mic_level_meter_test.go rename to media/mic_level_meter_test.go index 1e9736b..1e6bb81 100644 --- a/mic_level_meter_test.go +++ b/media/mic_level_meter_test.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "strings" @@ -6,13 +6,15 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMicLevelMeterPeakHold(t *testing.T) { level := float32(1) var e *ui.Element tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) e = MicLevelMeter(c, level, MicLevelMeterOptions{Hold: 40 * time.Millisecond}) }, 400, 100) if !tt.HasText("0 dB") { diff --git a/playback_speed_control.go b/media/playback_speed_control.go similarity index 72% rename from playback_speed_control.go rename to media/playback_speed_control.go index e3fe9d0..87c1314 100644 --- a/playback_speed_control.go +++ b/media/playback_speed_control.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -6,6 +6,9 @@ import ( "strconv" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // playbackRates are the default choices, 0.5× to 2×. @@ -19,7 +22,7 @@ type PlaybackSpeedControlOptions struct { } // PlaybackSpeedControl chooses *rate from Rates in a drop-down. -func PlaybackSpeedControl(c *ui.Context, rate *float64, opts PlaybackSpeedControlOptions) ChoiceResult { +func PlaybackSpeedControl(c *ui.Context, rate *float64, opts PlaybackSpeedControlOptions) core.ChoiceResult { rates := opts.Rates if rates == nil { rates = playbackRates @@ -30,11 +33,11 @@ func PlaybackSpeedControl(c *ui.Context, rate *float64, opts PlaybackSpeedContro if !slices.Contains(rates, *rate) { panic("mujica: PlaybackSpeedControl rate is not one of Rates") } - options := make([]SelectOption[float64], len(rates)) + options := make([]input.SelectOption[float64], len(rates)) for i, r := range rates { - options[i] = SelectOption[float64]{Value: r, Label: playbackLabel(r)} + options[i] = input.SelectOption[float64]{Value: r, Label: playbackLabel(r)} } - res := Select(c, rate, options, SelectOptions{Disabled: opts.Disabled, Label: msg(c, "playback_speed_control.label")}) + res := input.Select(c, rate, options, input.SelectOptions{Disabled: opts.Disabled, Label: core.Msg(c, "playback_speed_control.label")}) res.Element.MinWidth(88).Width(96) if res.Changed() { slog.Debug("mujica: playback_speed changed", "rate", *rate) diff --git a/playback_speed_control_test.go b/media/playback_speed_control_test.go similarity index 95% rename from playback_speed_control_test.go rename to media/playback_speed_control_test.go index 8d0dee2..2ed6223 100644 --- a/playback_speed_control_test.go +++ b/media/playback_speed_control_test.go @@ -1,14 +1,16 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func speedTester(rate *float64, opts PlaybackSpeedControlOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { if PlaybackSpeedControl(c, rate, opts).Changed() { *n++ diff --git a/playlist.go b/media/playlist.go similarity index 77% rename from playlist.go rename to media/playlist.go index c077165..d36531b 100644 --- a/playlist.go +++ b/media/playlist.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PlaylistItem is one track; ID must be unique and stable. @@ -55,30 +57,30 @@ func Playlist(c *ui.Context, items *[]PlaylistItem, opts PlaylistOptions) Playli } seen[it.ID] = true } - k := Tokens(c) + k := core.Tokens(c) var r PlaylistResult box := ui.Column(c).Height(h).Disabled(opts.Disabled) r.Element = box - st := ui.Local(box, playlistKey{}, func() *ListState[string] { return &ListState[string]{} }) + st := ui.Local(box, playlistKey{}, func() *data.ListState[string] { return &data.ListState[string]{} }) s := *st list := *items live := !box.IsDisabled() - s.rows.Reorder = nil + s.Rows.Reorder = nil if live { - s.rows.Reorder = func(rows []int, to int) { + s.Rows.Reorder = func(rows []int, to int) { *items = playlistMove(*items, rows, to) r.Moved = true } } removeAt := -1 box.Children(func() { - v := List(c, s, len(list), ListOptions[string]{ + v := data.List(c, s, len(list), data.ListOptions[string]{ Key: func(i int) string { return list[i].ID }, Label: func(i int) string { return list[i].Title }, - Empty: msg(c, "playlist.empty"), + Empty: core.Msg(c, "playlist.empty"), }, func(i int) { playlistRow(c, k, list[i], i, list[i].ID == opts.Current) - if iconAction(c, icons.Must("x"), msg(c, "playlist.remove")+" "+list[i].Title).Clicked() && live { + if core.IconAction(c, icons.Must("x"), core.Msg(c, "playlist.remove")+" "+list[i].Title).Clicked() && live { removeAt = i } }) @@ -121,9 +123,9 @@ func playlistRow(c *ui.Context, k theme.Tokens, it PlaylistItem, i int, playing ui.Icon(c, icons.Must("grip-vertical")).Size(14, 14).TextColor(k.TextMuted).Shrink(0) ui.Box(c).Width(20).Center().Shrink(0).Children(func() { if playing { - ui.Icon(c, mediaPlay).Size(14, 14).TextColor(k.AccentText).Label(msg(c, "playlist.playing")) + ui.Icon(c, core.MediaPlay).Size(14, 14).TextColor(k.AccentText).Label(core.Msg(c, "playlist.playing")) } else { - ui.Textf(c, "%d", i+1).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Textf(c, "%d", i+1).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") } }) ui.Column(c).Grow(1).MinWidth(0).Children(func() { @@ -132,11 +134,11 @@ func playlistRow(c *ui.Context, k theme.Tokens, it PlaylistItem, i int, playing t.TextColor(k.AccentText).FontWeight(600) } if it.Subtitle != "" { - ui.Text(c, it.Subtitle).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, it.Subtitle).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if it.Duration > 0 { - ui.Text(c, mediaClock(it.Duration)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") + ui.Text(c, core.MediaClock(it.Duration)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum") } } diff --git a/playlist_test.go b/media/playlist_test.go similarity index 97% rename from playlist_test.go rename to media/playlist_test.go index fb3cb2f..cf9d525 100644 --- a/playlist_test.go +++ b/media/playlist_test.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -7,6 +7,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func playlistItems(n int) []PlaylistItem { @@ -27,7 +29,7 @@ func playlistIDs(items []PlaylistItem) []string { func playlistTester(items *[]PlaylistItem, disabled bool, got *[]PlaylistResult) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(420).Children(func() { r := Playlist(c, items, PlaylistOptions{Current: "t1", Disabled: disabled}) if r.Played != "" || r.Removed != "" || r.Moved { diff --git a/screen_recorder_controls.go b/media/screen_recorder_controls.go similarity index 60% rename from screen_recorder_controls.go rename to media/screen_recorder_controls.go index d23ce6f..6c81c3b 100644 --- a/screen_recorder_controls.go +++ b/media/screen_recorder_controls.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -7,7 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // RecorderStatus is where a recording is; zero is idle. @@ -39,37 +41,37 @@ func ScreenRecorderControls(c *ui.Context, st RecorderState, opts ScreenRecorder if st.Status < RecorderIdle || st.Status > RecorderPaused || st.Elapsed < 0 { panic(fmt.Sprintf("mujica: bad RecorderState %+v", st)) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() row := ui.Row(c).Gap(u*2).Padding(u, u*2).AlignItems(ui.Center).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border).Role(ui.RoleToolbar). - Label(msg(c, "screen_recorder_controls.label")).Disabled(opts.Disabled) - act := func(icon *ui.SVG, key string, variant ButtonVariant, fn func()) { - if Button(c, msg(c, key), ButtonOptions{Variant: variant, Icon: icon}).Clicked() && !row.IsDisabled() { + Label(core.Msg(c, "screen_recorder_controls.label")).Disabled(opts.Disabled) + act := func(icon *ui.SVG, key string, variant input.ButtonVariant, fn func()) { + if input.Button(c, core.Msg(c, key), input.ButtonOptions{Variant: variant, Icon: icon}).Clicked() && !row.IsDisabled() { slog.Debug("mujica: recorder " + key) fn() } } row.Children(func() { status := []string{"screen_recorder_controls.idle", "screen_recorder_controls.recording", "screen_recorder_controls.paused"}[st.Status] - ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Role(ui.RoleStatus).Label(msg(c, status) + " " + mediaClock(st.Elapsed)).Children(func() { + ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Role(ui.RoleStatus).Label(core.Msg(c, status) + " " + core.MediaClock(st.Elapsed)).Children(func() { dot := k.TextMuted if st.Status == RecorderRecording { dot = k.Danger } ui.Box(c).Size(10, 10).Radius(5).Background(dot) - ui.Text(c, msg(c, status)).TextColor(k.TextMuted).SingleLine() - ui.Text(c, mediaClock(st.Elapsed)).FontFeatures("tnum").SingleLine().MinWidth(48) + ui.Text(c, core.Msg(c, status)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, core.MediaClock(st.Elapsed)).FontFeatures("tnum").SingleLine().MinWidth(48) }) switch st.Status { case RecorderIdle: - act(mediaRecord, "screen_recorder_controls.start", Danger, opts.OnStart) + act(core.MediaRecord, "screen_recorder_controls.start", input.Danger, opts.OnStart) case RecorderRecording: - act(mediaPause, "screen_recorder_controls.pause", Secondary, opts.OnPause) - act(mediaStop, "screen_recorder_controls.stop", Danger, opts.OnStop) + act(core.MediaPause, "screen_recorder_controls.pause", input.Secondary, opts.OnPause) + act(core.MediaStop, "screen_recorder_controls.stop", input.Danger, opts.OnStop) case RecorderPaused: - act(mediaRecord, "screen_recorder_controls.resume", Secondary, opts.OnResume) - act(mediaStop, "screen_recorder_controls.stop", Danger, opts.OnStop) + act(core.MediaRecord, "screen_recorder_controls.resume", input.Secondary, opts.OnResume) + act(core.MediaStop, "screen_recorder_controls.stop", input.Danger, opts.OnStop) } }) return row diff --git a/screen_recorder_controls_test.go b/media/screen_recorder_controls_test.go similarity index 94% rename from screen_recorder_controls_test.go rename to media/screen_recorder_controls_test.go index f83ba6d..3772e1f 100644 --- a/screen_recorder_controls_test.go +++ b/media/screen_recorder_controls_test.go @@ -1,10 +1,12 @@ -package mujica +package media import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type recorderCalls struct{ start, pause, resume, stop int } @@ -18,7 +20,7 @@ func TestScreenRecorderControlsFlow(t *testing.T) { st := RecorderState{} var r recorderCalls tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { ScreenRecorderControls(c, st, r.opts()) }) }, 500, 120) if !tt.HasText("Ready") || tt.HasText("Stop") { @@ -51,7 +53,7 @@ func TestScreenRecorderControlsDisabledAndBad(t *testing.T) { o := r.opts() o.Disabled = true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ScreenRecorderControls(c, RecorderState{}, o) }, 500, 120) _ = tt.Click("Record") diff --git a/subtitle_editor.go b/media/subtitle_editor.go similarity index 79% rename from subtitle_editor.go rename to media/subtitle_editor.go index 8a1a5fa..bbcb33b 100644 --- a/subtitle_editor.go +++ b/media/subtitle_editor.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -10,8 +10,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SubtitleCue is one subtitle: shown from Start until End. @@ -67,7 +69,7 @@ type subtitleField struct{ row, part int } type subtitleKey struct{} // SubtitleEditor edits cue times and text, building visible rows. -func SubtitleEditor(c *ui.Context, cues *[]SubtitleCue, opts SubtitleEditorOptions) ChoiceResult { +func SubtitleEditor(c *ui.Context, cues *[]SubtitleCue, opts SubtitleEditorOptions) core.ChoiceResult { if opts.Height < 0 { panic("mujica: SubtitleEditor Height must not be negative") } @@ -75,18 +77,18 @@ func SubtitleEditor(c *ui.Context, cues *[]SubtitleCue, opts SubtitleEditorOptio if h == 0 { h = 360 } - k := Tokens(c) - u := Density(c).Unit() - rowH := Density(c).ControlHeight() + FontSize(c, theme.CaptionSize)*theme.LineHeight + u*3 + k := core.Tokens(c) + u := core.Density(c).Unit() + rowH := core.Density(c).ControlHeight() + core.FontSize(c, theme.CaptionSize)*theme.LineHeight + u*3 col := ui.Column(c).Gap(u * 2).Disabled(opts.Disabled) st := ui.Local(col, subtitleKey{}, func() *subtitleState { return &subtitleState{drafts: map[subtitleField]string{}} }) s := *st issues := map[int]string{} for _, is := range CheckSubtitles(*cues) { if is.Kind == SubtitleOverlap { - issues[is.Index] = fmt.Sprintf(msg(c, "subtitle_editor.overlap"), is.Index) + issues[is.Index] = fmt.Sprintf(core.Msg(c, "subtitle_editor.overlap"), is.Index) } else { - issues[is.Index] = msg(c, "subtitle_editor.order") + issues[is.Index] = core.Msg(c, "subtitle_editor.order") } } changed := false @@ -113,10 +115,10 @@ func SubtitleEditor(c *ui.Context, cues *[]SubtitleCue, opts SubtitleEditorOptio } }) if n == 0 { - ui.Text(c, msg(c, "subtitle_editor.empty")).TextColor(k.TextMuted).Padding(u * 3) + ui.Text(c, core.Msg(c, "subtitle_editor.empty")).TextColor(k.TextMuted).Padding(u * 3) } }) - if Button(c, msg(c, "subtitle_editor.add"), ButtonOptions{Variant: Secondary, Icon: icons.Must("plus")}).Clicked() && !col.IsDisabled() { + if input.Button(c, core.Msg(c, "subtitle_editor.add"), input.ButtonOptions{Variant: input.Secondary, Icon: icons.Must("plus")}).Clicked() && !col.IsDisabled() { var at time.Duration if n := len(*cues); n > 0 { at = (*cues)[n-1].End @@ -129,12 +131,12 @@ func SubtitleEditor(c *ui.Context, cues *[]SubtitleCue, opts SubtitleEditorOptio if changed { slog.Debug("mujica: subtitle_editor changed", "cues", len(*cues)) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } // subtitleRow edits cue i; true means changed or removed. func subtitleRow(c *ui.Context, k theme.Tokens, cues *[]SubtitleCue, s *subtitleState, i int, rowH float32, issue string) bool { - u := Density(c).Unit() + u := core.Density(c).Unit() q := &(*cues)[i] changed, removed := false, false row := ui.Column(c).Key(i).Absolute().Left(0).Right(0).Top(float32(i)*rowH).Height(rowH).Padding(u*1.5, u*2).Gap(2). @@ -148,8 +150,8 @@ func subtitleRow(c *ui.Context, k theme.Tokens, cues *[]SubtitleCue, s *subtitle if !editing { text = subtitleClock(*t) } - name := msg(c, []string{"subtitle_editor.start", "subtitle_editor.end"}[part]) - in := TextInput(c, &text, TextInputOptions{Label: fmt.Sprintf("%s %d", name, i+1)}) + name := core.Msg(c, []string{"subtitle_editor.start", "subtitle_editor.end"}[part]) + in := input.TextInput(c, &text, input.TextInputOptions{Label: fmt.Sprintf("%s %d", name, i+1)}) in.Element.Width(112) bad := false if in.Changed() { @@ -164,17 +166,17 @@ func subtitleRow(c *ui.Context, k theme.Tokens, cues *[]SubtitleCue, s *subtitle bad = !ok } if bad { - in.Input.Error(msg(c, "subtitle_editor.badTime")) + in.Input.Error(core.Msg(c, "subtitle_editor.badTime")) } else if issue != "" { in.Input.Error(issue) } } - txt := TextInput(c, &q.Text, TextInputOptions{Label: fmt.Sprintf("%s %d", msg(c, "subtitle_editor.text"), i+1)}) + txt := input.TextInput(c, &q.Text, input.TextInputOptions{Label: fmt.Sprintf("%s %d", core.Msg(c, "subtitle_editor.text"), i+1)}) txt.Element.Grow(1) if txt.Changed() { changed = true } - if b := iconAction(c, icons.Must("trash-2"), fmt.Sprintf("%s %d", msg(c, "subtitle_editor.remove"), i+1)); b.Clicked() && !b.IsDisabled() { + if b := core.IconAction(c, icons.Must("trash-2"), fmt.Sprintf("%s %d", core.Msg(c, "subtitle_editor.remove"), i+1)); b.Clicked() && !b.IsDisabled() { removed = true } }) @@ -183,12 +185,12 @@ func subtitleRow(c *ui.Context, k theme.Tokens, cues *[]SubtitleCue, s *subtitle for part := range 2 { if d, ok := s.drafts[subtitleField{i, part}]; ok { if _, valid := subtitleParse(d); !valid { - note = msg(c, "subtitle_editor.badTime") + note = core.Msg(c, "subtitle_editor.badTime") } } } if note != "" { - ui.Text(c, note).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() + ui.Text(c, note).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).SingleLine() } }) }) diff --git a/subtitle_editor_test.go b/media/subtitle_editor_test.go similarity index 97% rename from subtitle_editor_test.go rename to media/subtitle_editor_test.go index 9c2b4c7..84bbae9 100644 --- a/subtitle_editor_test.go +++ b/media/subtitle_editor_test.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "slices" @@ -7,11 +7,13 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func subtitleTester(cues *[]SubtitleCue, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(620).Children(func() { if SubtitleEditor(c, cues, SubtitleEditorOptions{Height: 260, Disabled: disabled}).Changed() { *n++ diff --git a/video_player.go b/media/video_player.go similarity index 79% rename from video_player.go rename to media/video_player.go index 1c115b0..4be7459 100644 --- a/video_player.go +++ b/media/video_player.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -6,8 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // VideoState is what the caller's playback reports. @@ -37,8 +38,8 @@ type VideoPlayerOptions struct { Label string } -// videoBlack is the stage behind letterboxed frames in both themes. -var videoBlack = ui.Hex("#0E0C0D") +// VideoBlack is the stage behind letterboxed frames in both themes. +var VideoBlack = ui.Hex("#0E0C0D") // VideoPlayer draws the caller's frame and reports controls through callbacks. func VideoPlayer(c *ui.Context, st VideoState, opts VideoPlayerOptions) *ui.Element { @@ -48,8 +49,8 @@ func VideoPlayer(c *ui.Context, st VideoState, opts VideoPlayerOptions) *ui.Elem if st.Duration < 0 || st.Rate < 0 { panic("mujica: VideoPlayer needs Duration and Rate >= 0") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() col := ui.Column(c).Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface). Clip().Role(ui.RoleGroup).Label(videoLabel(c, opts.Label)).Disabled(opts.Disabled) col.Children(func() { @@ -63,7 +64,7 @@ func VideoPlayer(c *ui.Context, st VideoState, opts VideoPlayerOptions) *ui.Elem } ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { mediaPlayToggle(c, st.Playing, opts.OnPlay, opts.OnPause) - ui.Text(c, mediaClock(st.Position)+" / "+mediaClock(st.Duration)).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, core.MediaClock(st.Position)+" / "+core.MediaClock(st.Duration)).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).FontFeatures("tnum").SingleLine().Grow(1) videoButtons(c, st, opts) }) @@ -76,12 +77,12 @@ func videoLabel(c *ui.Context, label string) string { if label != "" { return label } - return msg(c, "video_player.label") + return core.Msg(c, "video_player.label") } // videoStage is the focusable picture with playback keys. func videoStage(c *ui.Context, k theme.Tokens, st VideoState, opts VideoPlayerOptions) { - stage := ui.Box(c).FillWidth().AspectRatio(16.0 / 9).Background(videoBlack).Center().Focusable(). + stage := ui.Box(c).FillWidth().AspectRatio(16.0 / 9).Background(VideoBlack).Center().Focusable(). Role(ui.RoleImage).Label(videoLabel(c, opts.Label)) internal.Ring(stage, k.Focus, 0) if !stage.IsDisabled() { @@ -92,7 +93,7 @@ func videoStage(c *ui.Context, k theme.Tokens, st VideoState, opts VideoPlayerOp } stage.Children(func() { if st.Frame == nil { - ui.Text(c, msg(c, "video_player.empty")).TextColor(ui.Hex("#BCAEAA")) + ui.Text(c, core.Msg(c, "video_player.empty")).TextColor(ui.Hex("#BCAEAA")) } else { ui.Image(c, st.Frame).Fill() } @@ -140,21 +141,21 @@ func videoButtons(c *ui.Context, st VideoState, opts VideoPlayerOptions) { if fn == nil { return } - b := mediaToggleAction(c, icon, msg(c, key), on) + b := MediaToggleAction(c, icon, core.Msg(c, key), on) if b.Clicked() && !b.IsDisabled() { slog.Debug("mujica: video_player toggle", "control", key, "on", !on) fn(!on) } } - toggle(mediaCaptions, "video_player.subtitles", st.SubtitlesOn, opts.OnSubtitles) - toggle(mediaPiP, "video_player.pip", st.PiP, opts.OnPiP) - toggle(mediaFullscreen, "video_player.fullscreen", st.Fullscreen, opts.OnFullscreen) + toggle(core.MediaCaptions, "video_player.subtitles", st.SubtitlesOn, opts.OnSubtitles) + toggle(core.MediaPiP, "video_player.pip", st.PiP, opts.OnPiP) + toggle(core.MediaFullscreen, "video_player.fullscreen", st.Fullscreen, opts.OnFullscreen) } -// mediaToggleAction is an iconAction that shows when it is on. -func mediaToggleAction(c *ui.Context, icon *ui.SVG, label string, on bool) *ui.Element { - k := Tokens(c) - b := iconAction(c, icon, label).Role(ui.RoleToggleButton).Selected(on) +// MediaToggleAction is an iconAction that shows when it is on. +func MediaToggleAction(c *ui.Context, icon *ui.SVG, label string, on bool) *ui.Element { + k := core.Tokens(c) + b := core.IconAction(c, icon, label).Role(ui.RoleToggleButton).Selected(on) if on { b.Background(k.Selection).TextColor(k.AccentText).BorderWidth(0, 0, 2, 0).BorderColor(k.AccentText) } diff --git a/video_player_test.go b/media/video_player_test.go similarity index 97% rename from video_player_test.go rename to media/video_player_test.go index 006b3dd..3d7dc76 100644 --- a/video_player_test.go +++ b/media/video_player_test.go @@ -1,10 +1,12 @@ -package mujica +package media import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type videoCalls struct { @@ -24,7 +26,7 @@ func (v *videoCalls) opts() VideoPlayerOptions { func videoTester(st *VideoState, o *VideoPlayerOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(640).Children(func() { VideoPlayer(c, *st, *o) }) }, 700, 520) } diff --git a/video_scrubber.go b/media/video_scrubber.go similarity index 64% rename from video_scrubber.go rename to media/video_scrubber.go index 156e06b..d6e993d 100644 --- a/video_scrubber.go +++ b/media/video_scrubber.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "log/slog" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MediaRange is a span of a media timeline. @@ -30,7 +32,7 @@ type VideoScrubberOptions struct { } // VideoScrubber sets *pos by dragging, clicking, arrows, Home and End. -func VideoScrubber(c *ui.Context, pos *time.Duration, opts VideoScrubberOptions) ChoiceResult { +func VideoScrubber(c *ui.Context, pos *time.Duration, opts VideoScrubberOptions) core.ChoiceResult { if opts.Duration <= 0 || opts.Step < 0 || opts.PreviewWidth < 0 || opts.PreviewHeight < 0 { panic("mujica: VideoScrubber needs Duration > 0 and no negative Step or preview size") } @@ -45,20 +47,20 @@ func VideoScrubber(c *ui.Context, pos *time.Duration, opts VideoScrubberOptions) if pw == 0 { pw, ph = 160, 90 } - k := Tokens(c) + k := core.Tokens(c) dur := opts.Duration.Seconds() - g, h := sliderSetup(0, dur, min(step.Seconds(), dur), nil, false, c) + g, h := input.SliderSetup(0, dur, min(step.Seconds(), dur), nil, false, c) old := *pos var s *ui.Element var next float64 wrap := ui.Box(c).MinWidth(120).Shrink(0).Disabled(opts.Disabled) wrap.Children(func() { - s, next, _ = sliderControl(c, old.Seconds(), g, 0, dur) - s.Height(h).PaddingX(sliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer). + s, next, _ = input.SliderControl(c, old.Seconds(), g, 0, dur) + s.Height(h).PaddingX(input.SliderThumb / 2).FocusRing(false).Cursor(ui.CursorPointer). Label(scrubberLabel(c, opts.Label)) px, _, over := s.PointerPosition() r := s.Bounds() - at := time.Duration(float64((px-sliderThumb/2)/max(r.W-sliderThumb, 1)) * float64(opts.Duration)) + at := time.Duration(float64((px-input.SliderThumb/2)/max(r.W-input.SliderThumb, 1)) * float64(opts.Duration)) at = max(0, min(opts.Duration, at)) if over && !s.IsDisabled() { scrubberHover(c, k, r, g, at, opts, pw, ph) @@ -76,43 +78,43 @@ func VideoScrubber(c *ui.Context, pos *time.Duration, opts VideoScrubberOptions) } v, buffered, focus := pos.Seconds(), opts.Buffered, s s.Draw(func(p *ui.Painter, r ui.Rect) { - y := r.Y + g.trackY - x0 := g.x(r, 0) - sliderTrackPaint(p, r, g, k, nil, 0, x0, x0) + y := r.Y + g.TrackY + x0 := g.X(r, 0) + input.SliderTrackPaint(p, r, g, k, nil, 0, x0, x0) // Buffered spans fill the rail under the played part. for _, b := range buffered { - b0, b1 := g.x(r, b.Start.Seconds()), g.x(r, b.End.Seconds()) - p.Fill(ui.Rect{X: b0, Y: y - sliderTrack/2, W: b1 - b0, H: sliderTrack}, k.TextMuted.Alpha(0.45), 0) + b0, b1 := g.X(r, b.Start.Seconds()), g.X(r, b.End.Seconds()) + p.Fill(ui.Rect{X: b0, Y: y - input.SliderTrack/2, W: b1 - b0, H: input.SliderTrack}, k.TextMuted.Alpha(0.45), 0) } - p.Fill(ui.Rect{X: x0, Y: y - sliderTrack/2, W: g.x(r, v) - x0, H: sliderTrack}, k.AccentText, 0) - sliderThumbPaint(p, k, g.x(r, v), y, focus.Hovered(), focus.FocusVisible()) + p.Fill(ui.Rect{X: x0, Y: y - input.SliderTrack/2, W: g.X(r, v) - x0, H: input.SliderTrack}, k.AccentText, 0) + input.SliderThumbPaint(p, k, g.X(r, v), y, focus.Hovered(), focus.FocusVisible()) }) - return ChoiceResult{Element: s, changed: changed} + return core.NewChoiceResult(s, changed) } func scrubberLabel(c *ui.Context, label string) string { if label != "" { return label } - return msg(c, "video_scrubber.label") + return core.Msg(c, "video_scrubber.label") } // scrubberHover shows hover time and preview slot above the track. -func scrubberHover(c *ui.Context, k theme.Tokens, r ui.Rect, g sliderGeom, at time.Duration, opts VideoScrubberOptions, pw, ph float32) { - x := g.x(ui.Rect{W: r.W}, at.Seconds()) +func scrubberHover(c *ui.Context, k theme.Tokens, r ui.Rect, g input.SliderGeom, at time.Duration, opts VideoScrubberOptions, pw, ph float32) { + x := g.X(ui.Rect{W: r.W}, at.Seconds()) w := pw if opts.Preview == nil { w = 64 } - panel := ui.Column(c).Absolute().Left(max(0, min(r.W-w, x-w/2))).Bottom(r.H - g.trackY + sliderThumb). + panel := ui.Column(c).Absolute().Left(max(0, min(r.W-w, x-w/2))).Bottom(r.H - g.TrackY + input.SliderThumb). Width(w).Padding(4).Gap(4).AlignItems(ui.Center).PassThrough() - overlayStyle(c, panel, 1) + core.OverlayStyle(c, panel, 1) panel.Children(func() { if opts.Preview != nil { ui.Box(c).Size(pw-8, ph).Clip().Radius(theme.ControlRadius).Background(k.Background).Children(func() { opts.Preview(at) }) } - ui.Text(c, mediaClock(at)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() + ui.Text(c, core.MediaClock(at)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum").SingleLine() }) } diff --git a/video_scrubber_test.go b/media/video_scrubber_test.go similarity index 95% rename from video_scrubber_test.go rename to media/video_scrubber_test.go index 7805684..32ebae2 100644 --- a/video_scrubber_test.go +++ b/media/video_scrubber_test.go @@ -1,15 +1,17 @@ -package mujica +package media import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func scrubberTester(pos *time.Duration, opts VideoScrubberOptions, changes *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(40).Children(func() { if VideoScrubber(c, pos, opts).Changed() { *changes++ @@ -52,7 +54,7 @@ func TestVideoScrubberClickAndHover(t *testing.T) { } mid := r.X + r.W/2 tt.Move(mid, r.Y+r.H/2) - if asked < 45*time.Second || asked > 55*time.Second || !tt.HasText(mediaClock(asked)) { + if asked < 45*time.Second || asked > 55*time.Second || !tt.HasText(core.MediaClock(asked)) { t.Fatalf("hover preview at %v, texts %v", asked, tt.Texts()) } tt.ClickAt(mid, r.Y+r.H/2) diff --git a/video_thumbnail_strip.go b/media/video_thumbnail_strip.go similarity index 74% rename from video_thumbnail_strip.go rename to media/video_thumbnail_strip.go index af92ed4..dc23f6b 100644 --- a/video_thumbnail_strip.go +++ b/media/video_thumbnail_strip.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StripFrame is one thumbnail of a VideoThumbnailStrip. @@ -31,7 +32,7 @@ const stripGap = 4 type stripKey struct{} // VideoThumbnailStrip shows the frames fitting around *current. -func VideoThumbnailStrip(c *ui.Context, current *int, frames []StripFrame, opts VideoThumbnailStripOptions) ChoiceResult { +func VideoThumbnailStrip(c *ui.Context, current *int, frames []StripFrame, opts VideoThumbnailStripOptions) core.ChoiceResult { if len(frames) == 0 || *current < 0 || *current >= len(frames) { panic("mujica: VideoThumbnailStrip current out of range") } @@ -43,16 +44,16 @@ func VideoThumbnailStrip(c *ui.Context, current *int, frames []StripFrame, opts fw = 96 } fh := fw * 9 / 16 - k := Tokens(c) + k := core.Tokens(c) old := *current - row := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Disabled(opts.Disabled) + row := ui.Row(c).Gap(core.Density(c).Unit()).AlignItems(ui.Center).Disabled(opts.Disabled) row.Children(func() { - prev := iconAction(c, icons.Must("chevron-left"), msg(c, "video_thumbnail_strip.previous")).Disabled(*current == 0) + prev := core.IconAction(c, icons.Must("chevron-left"), core.Msg(c, "video_thumbnail_strip.previous")).Disabled(*current == 0) view := ui.Row(c).Grow(1).MinWidth(fw).Height(fh + 4).Gap(stripGap).Clip().Focusable(). - Role(ui.RoleGroup).Label(msg(c, "video_thumbnail_strip.label")). - Description(fmt.Sprintf(msg(c, "video_thumbnail_strip.position"), *current+1, len(frames), mediaClock(frames[*current].At))) + Role(ui.RoleGroup).Label(core.Msg(c, "video_thumbnail_strip.label")). + Description(fmt.Sprintf(core.Msg(c, "video_thumbnail_strip.position"), *current+1, len(frames), core.MediaClock(frames[*current].At))) internal.Ring(view, k.Focus, theme.ControlRadius) - next := iconAction(c, icons.Must("chevron-right"), msg(c, "video_thumbnail_strip.next")).Disabled(*current == len(frames)-1) + next := core.IconAction(c, icons.Must("chevron-right"), core.Msg(c, "video_thumbnail_strip.next")).Disabled(*current == len(frames)-1) live := !row.IsDisabled() switch { case live && (prev.Clicked() || view.Shortcut(0, ui.KeyLeft)): @@ -88,12 +89,12 @@ func VideoThumbnailStrip(c *ui.Context, current *int, frames []StripFrame, opts if changed { slog.Debug("mujica: video_strip jump", "index", *current) } - return ChoiceResult{Element: row, changed: changed} + return core.NewChoiceResult(row, changed) } // stripCell draws one frame and reports a click on it. func stripCell(c *ui.Context, k theme.Tokens, f StripFrame, i int, on bool, fw, fh float32) bool { - cell := ui.Box(c).Key(i).Size(fw, fh).Shrink(0).Radius(theme.ControlRadius).Clip().Background(videoBlack). + cell := ui.Box(c).Key(i).Size(fw, fh).Shrink(0).Radius(theme.ControlRadius).Clip().Background(VideoBlack). Cursor(ui.CursorPointer).AlignSelf(ui.Center) edge, width := k.Border, theme.BorderWidth if on { @@ -104,7 +105,7 @@ func stripCell(c *ui.Context, k theme.Tokens, f StripFrame, i int, on bool, fw, if f.Image != nil { ui.Image(c, f.Image).Fit(ui.Cover).Size(fw, fh) } - ui.Text(c, mediaClock(f.At)).Absolute().Left(4).Bottom(3).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, core.MediaClock(f.At)).Absolute().Left(4).Bottom(3).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(ui.Hex("#F2EBDD")).Background(ui.RGBA(0, 0, 0, 0.6)).FontFeatures("tnum") if on { ui.Box(c).Absolute().Top(3).Right(3).Size(10, 10).Draw(func(p *ui.Painter, r ui.Rect) { diff --git a/video_thumbnail_strip_test.go b/media/video_thumbnail_strip_test.go similarity index 94% rename from video_thumbnail_strip_test.go rename to media/video_thumbnail_strip_test.go index 6cc95fe..98e1a44 100644 --- a/video_thumbnail_strip_test.go +++ b/media/video_thumbnail_strip_test.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "strings" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func stripFrames(n int) []StripFrame { @@ -18,7 +20,7 @@ func stripFrames(n int) []StripFrame { func stripTester(cur *int, frames []StripFrame, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(520).Children(func() { if VideoThumbnailStrip(c, cur, frames, VideoThumbnailStripOptions{Disabled: disabled}).Changed() { *n++ @@ -68,7 +70,7 @@ func TestVideoThumbnailStripBuildsOnlyVisible(t *testing.T) { if b := stripBuilt(tt); b < 3 || b > 10 { t.Fatalf("built %d frames of 100000", b) } - if !tt.HasText(mediaClock(50000 * time.Second)) { + if !tt.HasText(core.MediaClock(50000 * time.Second)) { t.Fatal("current frame not built") } } diff --git a/volume_control.go b/media/volume_control.go similarity index 58% rename from volume_control.go rename to media/volume_control.go index 7fac647..d3e604c 100644 --- a/volume_control.go +++ b/media/volume_control.go @@ -1,4 +1,4 @@ -package mujica +package media import ( "fmt" @@ -6,6 +6,9 @@ import ( "math" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // VolumeControlOptions configure a VolumeControl. @@ -14,24 +17,24 @@ type VolumeControlOptions struct { } // VolumeControl sets *volume in 5% steps and toggles *muted. -func VolumeControl(c *ui.Context, volume *float64, muted *bool, opts VolumeControlOptions) ChoiceResult { +func VolumeControl(c *ui.Context, volume *float64, muted *bool, opts VolumeControlOptions) core.ChoiceResult { if *volume < 0 || *volume > 1 || math.IsNaN(*volume) { panic("mujica: VolumeControl volume outside 0..1") } oldV, oldM := *volume, *muted - u := Density(c).Unit() - row := ui.Row(c).Gap(u).AlignItems(ui.Center).Role(ui.RoleGroup).Label(msg(c, "volume_control.label")).Disabled(opts.Disabled) + u := core.Density(c).Unit() + row := ui.Row(c).Gap(u).AlignItems(ui.Center).Role(ui.RoleGroup).Label(core.Msg(c, "volume_control.label")).Disabled(opts.Disabled) row.Children(func() { - icon, key := mediaVolume, "volume_control.mute" + icon, key := core.MediaVolume, "volume_control.mute" if *muted || *volume == 0 { - icon, key = mediaMute, "volume_control.unmute" + icon, key = core.MediaMute, "volume_control.unmute" } - b := mediaToggleAction(c, icon, msg(c, key), *muted) + b := MediaToggleAction(c, icon, core.Msg(c, key), *muted) if b.Clicked() && !b.IsDisabled() { *muted = !*muted } - s := Slider(c, volume, SliderOptions{Min: 0, Max: 1, Step: 0.05, Disabled: opts.Disabled, - Label: msg(c, "volume_control.label"), ShowValue: true, Format: func(v float64) string { return fmt.Sprintf("%d%%", int(math.Round(v*100))) }}) + s := input.Slider(c, volume, input.SliderOptions{Min: 0, Max: 1, Step: 0.05, Disabled: opts.Disabled, + Label: core.Msg(c, "volume_control.label"), ShowValue: true, Format: func(v float64) string { return fmt.Sprintf("%d%%", int(math.Round(v*100))) }}) s.Element.Width(120) // Raising the volume while muted unmutes, as players do. if s.Changed() && *muted && *volume > oldV { @@ -45,5 +48,5 @@ func VolumeControl(c *ui.Context, volume *float64, muted *bool, opts VolumeContr if changed { slog.Debug("mujica: volume changed", "volume", *volume, "muted", *muted) } - return ChoiceResult{Element: row, changed: changed} + return core.NewChoiceResult(row, changed) } diff --git a/volume_control_test.go b/media/volume_control_test.go similarity index 95% rename from volume_control_test.go rename to media/volume_control_test.go index 94a6b71..d58bb53 100644 --- a/volume_control_test.go +++ b/media/volume_control_test.go @@ -1,14 +1,16 @@ -package mujica +package media import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func volumeTester(v *float64, muted *bool, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(40).AlignItems(ui.Start).Children(func() { if VolumeControl(c, v, muted, VolumeControlOptions{Disabled: disabled}).Changed() { *n++ diff --git a/media_icons.go b/media_icons.go deleted file mode 100644 index ed5de5f..0000000 --- a/media_icons.go +++ /dev/null @@ -1,53 +0,0 @@ -package mujica - -import ( - "fmt" - "time" - - "github.com/egoist/mygo/ui" -) - -// mediaSVG wraps stroked 24×24 path data as an icon. -func mediaSVG(body string) *ui.SVG { - return ui.MustParseSVG([]byte(`` + body + ``)) -} - -// Media glyphs drawn for this group. -var ( - mediaPlay = mediaSVG(``) - mediaPause = mediaSVG(``) - mediaPrev = mediaSVG(``) - mediaNext = mediaSVG(``) - mediaRepeat = mediaSVG(``) - mediaRepeatOne = mediaSVG(``) - mediaShuffle = mediaSVG(``) - mediaVolume = mediaSVG(``) - mediaMute = mediaSVG(``) - mediaRotate = mediaSVG(``) - mediaRotateLeft = mediaSVG(``) - mediaMirror = mediaSVG(``) - mediaPiP = mediaSVG(``) - mediaFullscreen = mediaSVG(``) - mediaCaptions = mediaSVG(``) - mediaRecord = mediaSVG(``) - mediaStop = mediaSVG(``) - mediaMic = mediaSVG(``) - mediaCamera = mediaSVG(``) - mediaSpeaker = mediaSVG(``) - mediaRect = mediaSVG(``) - mediaArrow = mediaSVG(``) - mediaText = mediaSVG(``) - mediaPointer = mediaSVG(``) -) - -// mediaClock writes d as m:ss, or h:mm:ss past an hour. -func mediaClock(d time.Duration) string { - if d < 0 { - d = 0 - } - s := int(d / time.Second) - if s >= 3600 { - return fmt.Sprintf("%d:%02d:%02d", s/3600, s/60%60, s%60) - } - return fmt.Sprintf("%d:%02d", s/60, s%60) -} diff --git a/call_controls.go b/messaging/call_controls.go similarity index 70% rename from call_controls.go rename to messaging/call_controls.go index 4d12d1a..e4ec706 100644 --- a/call_controls.go +++ b/messaging/call_controls.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "log/slog" @@ -6,7 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/media" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CallState is what the caller's call reports. @@ -52,7 +56,7 @@ func (k callKey) String() string { } out := "" for i, name := range append(keys, k.letter) { - face, _ := kbdKey(name, mac) + face, _ := display.KbdKey(name, mac) if i > 0 && !mac { out += "+" } @@ -66,11 +70,11 @@ func CallControls(c *ui.Context, st CallState, opts CallControlsOptions) *ui.Ele if opts.OnMute == nil || opts.OnCamera == nil || opts.OnShare == nil || opts.OnHangUp == nil { panic("mujica: CallControls needs every callback") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() row := ui.Row(c).Gap(u*2).Padding(u*2, u*3).AlignItems(ui.Center).Radius(theme.CardRadius). Background(k.Surface).Border(theme.BorderWidth, k.Border).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar). - Label(msg(c, "call_controls.label")).Disabled(opts.Disabled) + Label(core.Msg(c, "call_controls.label")).Disabled(opts.Disabled) live := !row.IsDisabled() report := func(name string, on bool, fn func(bool)) { slog.Debug("mujica: call_controls "+name, "on", on) @@ -78,20 +82,20 @@ func CallControls(c *ui.Context, st CallState, opts CallControlsOptions) *ui.Ele } row.Children(func() { toggle := func(off, on *ui.SVG, offKey, onKey string, state bool, key callKey, name string, fn func(bool)) { - icon, label := off, msg(c, offKey) + icon, label := off, core.Msg(c, offKey) if state { - icon, label = on, msg(c, onKey) + icon, label = on, core.Msg(c, onKey) } - b := mediaToggleAction(c, icon, label, state).Size(40, 40).Tooltip(label + " (" + key.String() + ")") + b := media.MediaToggleAction(c, icon, label, state).Size(40, 40).Tooltip(label + " (" + key.String() + ")") if live && b.Clicked() { report(name, !state, fn) } } - toggle(mediaMic, msgMicOff, "call_controls.mute", "call_controls.unmute", st.Muted, callMute, "mute", opts.OnMute) - toggle(mediaCamera, msgVideoOff, "call_controls.cameraOff", "call_controls.cameraOn", st.CameraOff, callCamera, "camera", opts.OnCamera) + toggle(core.MediaMic, msgMicOff, "call_controls.mute", "call_controls.unmute", st.Muted, callMute, "mute", opts.OnMute) + toggle(core.MediaCamera, msgVideoOff, "call_controls.cameraOff", "call_controls.cameraOn", st.CameraOff, callCamera, "camera", opts.OnCamera) toggle(msgShare, msgShare, "call_controls.share", "call_controls.stopShare", st.Sharing, callShare, "share", opts.OnShare) - hang := Button(c, msg(c, "call_controls.hangUp"), ButtonOptions{Variant: Danger, Icon: msgHangUp}). - Tooltip(msg(c, "call_controls.hangUp") + " (" + callHangUp.String() + ")") + hang := input.Button(c, core.Msg(c, "call_controls.hangUp"), input.ButtonOptions{Variant: input.Danger, Icon: msgHangUp}). + Tooltip(core.Msg(c, "call_controls.hangUp") + " (" + callHangUp.String() + ")") if live && hang.Clicked() { slog.Debug("mujica: call_controls hang up") opts.OnHangUp() diff --git a/call_controls_test.go b/messaging/call_controls_test.go similarity index 88% rename from call_controls_test.go rename to messaging/call_controls_test.go index 4b2fa45..fff05ce 100644 --- a/call_controls_test.go +++ b/messaging/call_controls_test.go @@ -1,19 +1,22 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestCallControlsClicksAndShortcuts(t *testing.T) { st := CallState{Muted: true} var k callCalls tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).AlignItems(ui.Start).Children(func() { CallControls(c, st, k.opts()) - Button(c, "Elsewhere", ButtonOptions{}) + input.Button(c, "Elsewhere", input.ButtonOptions{}) }) }, 500, 200) for _, name := range []string{"Unmute", "Turn camera off", "Share screen", "Leave"} { @@ -41,7 +44,7 @@ func TestCallControlsDisabled(t *testing.T) { o := k.opts() o.Disabled = true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CallControls(c, CallState{}, o) }, 500, 200) _ = tt.Click("Mute") diff --git a/channel_list.go b/messaging/channel_list.go similarity index 82% rename from channel_list.go rename to messaging/channel_list.go index d56e090..c8bc8ee 100644 --- a/channel_list.go +++ b/messaging/channel_list.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Channel supplies channel or direct-conversation data. @@ -50,12 +52,12 @@ type channelListState struct { } // ChannelList chooses a channel from collapsible, virtualized groups. -func ChannelList(c *ui.Context, selected *string, groups []ChannelGroup, opts ChannelListOptions) ChoiceResult { +func ChannelList(c *ui.Context, selected *string, groups []ChannelGroup, opts ChannelListOptions) core.ChoiceResult { if opts.Height < 0 { panic("mujica: ChannelList Height must be nonnegative") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := ui.Column(c).Disabled(opts.Disabled) st := ui.Local(col, channelListKey{}, func() channelListState { return channelListState{folded: map[string]bool{}} @@ -100,13 +102,13 @@ func ChannelList(c *ui.Context, selected *string, groups []ChannelGroup, opts Ch if channelRow(c, k, ch, ch.ID == *selected) && !col.IsDisabled() { *selected = ch.ID } - }).Height(h).FocusGroup(ui.Vertical).Role(ui.RoleTree).Label(msg(c, "channel_list.label")) + }).Height(h).FocusGroup(ui.Vertical).Role(ui.RoleTree).Label(core.Msg(c, "channel_list.label")) }) changed := *selected != old if changed { slog.Debug("mujica: channel_list select", "id", *selected) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func channelRows(st *channelListState, groups []ChannelGroup) { @@ -140,9 +142,9 @@ func channelRows(st *channelListState, groups []ChannelGroup) { } func channelHeader(c *ui.Context, k theme.Tokens, g ChannelGroup, open bool) bool { - u := Density(c).Unit() - b := ui.ButtonBase(c).MinHeight(Density(c).ControlHeight()).Justify(ui.Start).Gap(u).Padding(0, u*2).Radius(theme.ControlRadius). - Label(g.Title).Role(ui.RoleTreeItem).TextColor(k.TextMuted).Transition(colorFade) + u := core.Density(c).Unit() + b := ui.ButtonBase(c).MinHeight(core.Density(c).ControlHeight()).Justify(ui.Start).Gap(u).Padding(0, u*2).Radius(theme.ControlRadius). + Label(g.Title).Role(ui.RoleTreeItem).TextColor(k.TextMuted).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) @@ -153,7 +155,7 @@ func channelHeader(c *ui.Context, k theme.Tokens, g ChannelGroup, open bool) boo } b.Children(func() { ui.Icon(c, icons.Must(arrow)).Size(14, 14) - ui.Text(c, g.Title).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() + ui.Text(c, g.Title).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine() }) if b.IsDisabled() { return false @@ -167,15 +169,15 @@ func channelHeader(c *ui.Context, k theme.Tokens, g ChannelGroup, open bool) boo // channelRow is one channel button; true means it was chosen. func channelRow(c *ui.Context, k theme.Tokens, ch Channel, on bool) bool { - d := Density(c) + d := core.Density(c) u := d.Unit() label := ch.Name if ch.Unread > 0 { - label = fmt.Sprintf(msg(c, "channel_list.unread"), ch.Name, ch.Unread) + label = fmt.Sprintf(core.Msg(c, "channel_list.unread"), ch.Name, ch.Unread) } b := ui.ButtonBase(c).Key(ch.ID).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(u*2).Padding(0, u*2, 0, u*5). Radius(theme.ControlRadius).Label(label).Role(ui.RoleTreeItem).Selected(on).Background(ui.Transparent). - TextColor(k.Text).Transition(colorFade) + TextColor(k.Text).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) switch { case on: @@ -199,17 +201,17 @@ func channelRow(c *ui.Context, k theme.Tokens, ch Channel, on bool) bool { name.FontWeight(600) } if ch.Pinned { - ui.Icon(c, msgPin).Size(13, 13).TextColor(k.TextMuted).Label(msg(c, "channel_list.pinned")) + ui.Icon(c, msgPin).Size(13, 13).TextColor(k.TextMuted).Label(core.Msg(c, "channel_list.pinned")) } if ch.Muted { - ui.Icon(c, msgBellOff).Size(13, 13).TextColor(k.TextMuted).Label(msg(c, "channel_list.muted")) + ui.Icon(c, msgBellOff).Size(13, 13).TextColor(k.TextMuted).Label(core.Msg(c, "channel_list.muted")) } if ch.Unread > 0 { - tone := BadgeAccent + tone := display.BadgeAccent if ch.Muted { - tone = BadgeNeutral + tone = display.BadgeNeutral } - Badge(c, BadgeOptions{Count: ch.Unread, Tone: tone}, nil) + display.Badge(c, display.BadgeOptions{Count: ch.Unread, Tone: tone}, nil) } }) return b.Clicked() diff --git a/channel_list_test.go b/messaging/channel_list_test.go similarity index 98% rename from channel_list_test.go rename to messaging/channel_list_test.go index 166875a..223bc4e 100644 --- a/channel_list_test.go +++ b/messaging/channel_list_test.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "testing" diff --git a/chat_message.go b/messaging/chat_message.go similarity index 71% rename from chat_message.go rename to messaging/chat_message.go index 1d24bc8..03f3bc1 100644 --- a/chat_message.go +++ b/messaging/chat_message.go @@ -1,11 +1,14 @@ -package mujica +package messaging import ( "time" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChatEntry is one message of a person-to-person chat. @@ -44,32 +47,32 @@ func ChatMessage(c *ui.Context, m ChatEntry, opts ChatMessageOptions) *ui.Elemen if m.Author == "" { panic("mujica: ChatMessage needs an Author") } - k := Tokens(c) - u := Density(c).Unit() - role := MessageAssistant + k := core.Tokens(c) + u := core.Density(c).Unit() + role := chat.MessageAssistant if m.Own { - role = MessageUser + role = chat.MessageUser } - bo := MessageBubbleOptions{} + bo := chat.MessageBubbleOptions{} const avatar = 32 if !opts.Continued { bo.Name = m.Author - bo.Avatar = func() { Avatar(c, m.Author, AvatarOptions{Image: m.Image, Size: avatar}) } + bo.Avatar = func() { display.Avatar(c, m.Author, display.AvatarOptions{Image: m.Image, Size: avatar}) } } else { bo.Avatar = func() { ui.Box(c).Width(avatar).Shrink(0) } } - e := MessageBubble(c, role, bo, func() { + e := chat.MessageBubble(c, role, bo, func() { if opts.Reply != nil { opts.Reply() } ui.Text(c, m.Text).Selectable().FocusRing(false).LineHeight(theme.LineHeight) ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { if !m.Time.IsZero() { - ui.Text(c, m.Time.Format(msg(c, "chat_message.time"))).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, m.Time.Format(core.Msg(c, "chat_message.time"))).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() } if m.Edited { - ui.Text(c, msg(c, "chat_message.edited")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). + ui.Text(c, core.Msg(c, "chat_message.edited")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). Italic().SingleLine() } }) diff --git a/chat_message_test.go b/messaging/chat_message_test.go similarity index 82% rename from chat_message_test.go rename to messaging/chat_message_test.go index 745c5b0..b4fb165 100644 --- a/chat_message_test.go +++ b/messaging/chat_message_test.go @@ -1,10 +1,14 @@ -package mujica +package messaging import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" ) func TestChatContinues(t *testing.T) { @@ -28,11 +32,11 @@ func TestChatContinues(t *testing.T) { func TestChatMessageSlotsAndMerge(t *testing.T) { at := time.Date(2026, 10, 5, 21, 7, 0, 0, time.UTC) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(500).Children(func() { ChatMessage(c, ChatEntry{ID: "1", Author: "Uika", Time: at, Text: "Rehearsal at nine."}, ChatMessageOptions{ - Reply: func() { QuoteReply(c, "Sakiko", "When do we start?") }, - Reactions: func() { Tag(c, "👍 2", TagOptions{}) }, + Reply: func() { chat.QuoteReply(c, "Sakiko", "When do we start?") }, + Reactions: func() { display.Tag(c, "👍 2", display.TagOptions{}) }, }) ChatMessage(c, ChatEntry{ID: "2", Author: "Uika", Time: at, Text: "Bring the masks.", Edited: true}, ChatMessageOptions{Continued: true}) }) @@ -52,7 +56,7 @@ func TestChatMessageSnapshot(t *testing.T) { snapshot(t, "chat_message", 520, 340, func(c *ui.Context) { ui.Column(c).Padding(20).Gap(8).Width(480).Children(func() { ChatMessage(c, ChatEntry{Author: "Uika Misumi", Time: at, Text: "Rehearsal moves to nine."}, ChatMessageOptions{ - Reply: func() { QuoteReply(c, "Sakiko", "When do we start?") }}) + Reply: func() { chat.QuoteReply(c, "Sakiko", "When do we start?") }}) ChatMessage(c, ChatEntry{Author: "Uika Misumi", Time: at, Text: "Bring the masks.", Edited: true}, ChatMessageOptions{Continued: true}) ChatMessage(c, ChatEntry{Author: "Me", Time: at, Text: "Understood.", Own: true}, ChatMessageOptions{}) }) diff --git a/messaging/doc.go b/messaging/doc.go new file mode 100644 index 0000000..86ff097 --- /dev/null +++ b/messaging/doc.go @@ -0,0 +1,2 @@ +// Package messaging holds MujicaUI messaging, calls and mail. +package messaging diff --git a/emoji_data.go b/messaging/emoji_data.go similarity index 99% rename from emoji_data.go rename to messaging/emoji_data.go index a0e7fc4..7942e02 100644 --- a/emoji_data.go +++ b/messaging/emoji_data.go @@ -1,4 +1,4 @@ -package mujica +package messaging type emoji struct { char, name string diff --git a/emoji_picker.go b/messaging/emoji_picker.go similarity index 77% rename from emoji_picker.go rename to messaging/emoji_picker.go index a88830c..946b174 100644 --- a/emoji_picker.go +++ b/messaging/emoji_picker.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -8,9 +8,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // EmojiPickerOptions configure an EmojiPicker. @@ -45,15 +47,15 @@ func EmojiPicker(c *ui.Context, opts EmojiPickerOptions) EmojiResult { if opts.Tone != nil && (*opts.Tone < 0 || *opts.Tone >= len(emojiTones)) { panic("mujica: EmojiPicker Tone outside 0..5") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() tone := 0 if opts.Tone != nil { tone = *opts.Tone } var r EmojiResult col := ui.Column(c).Width(8*36+u*4).Gap(u*2).Padding(u*2).Radius(theme.PopupRadius).Background(k.Surface). - Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup).Label(msg(c, "emoji_picker.label")).Disabled(opts.Disabled) + Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup).Label(core.Msg(c, "emoji_picker.label")).Disabled(opts.Disabled) r.Element = col recent := opts.Recent != nil && len(*opts.Recent) > 0 st := ui.Local(col, emojiKey{}, func() emojiState { return emojiState{recent: recent} }) @@ -66,7 +68,7 @@ func EmojiPicker(c *ui.Context, opts EmojiPickerOptions) EmojiResult { st.recent = recent } col.Children(func() { - TextInput(c, &st.query, TextInputOptions{Placeholder: msg(c, "emoji_picker.search"), Label: msg(c, "emoji_picker.search"), + input.TextInput(c, &st.query, input.TextInputOptions{Placeholder: core.Msg(c, "emoji_picker.search"), Label: core.Msg(c, "emoji_picker.search"), Leading: func() { ui.Icon(c, icons.Must("search")).Size(14, 14).TextColor(k.TextMuted) }, Clearable: true}) if opts.Tone != nil { emojiToneRow(c, k, opts.Tone) @@ -106,7 +108,7 @@ func emojiShown(c *ui.Context, st *emojiState, recentList *[]string, recent bool } } if len(out) == 0 { - ui.Text(c, msg(c, "emoji_picker.none")).TextColor(Tokens(c).TextMuted).Padding(Density(c).Unit() * 2) + ui.Text(c, core.Msg(c, "emoji_picker.none")).TextColor(core.Tokens(c).TextMuted).Padding(core.Density(c).Unit() * 2) } return out } @@ -137,8 +139,8 @@ func emojiTabs(c *ui.Context, k theme.Tokens, tab *int, recent bool) { *tab = min(*tab, len(tabs)-1) ui.Row(c).FocusGroup(ui.Horizontal).Role(ui.RoleTabList).BorderWidth(0, 0, 1, 0).BorderColor(k.Border).Children(func() { for i, t := range tabs { - b := ui.ButtonBase(c).Key(t.key).Size(32, 32).Radius(theme.ControlRadius).Role(ui.RoleTab).Label(msg(c, t.key)). - Selected(i == *tab).Background(ui.Transparent).Tooltip(msg(c, t.key)) + b := ui.ButtonBase(c).Key(t.key).Size(32, 32).Radius(theme.ControlRadius).Role(ui.RoleTab).Label(core.Msg(c, t.key)). + Selected(i == *tab).Background(ui.Transparent).Tooltip(core.Msg(c, t.key)) internal.Ring(b, k.Focus, theme.ControlRadius) switch { case i == *tab: @@ -146,7 +148,7 @@ func emojiTabs(c *ui.Context, k theme.Tokens, tab *int, recent bool) { case b.Hovered(): b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, t.face).FontSize(FontSize(c, 16)) }) + b.Children(func() { ui.Text(c, t.face).FontSize(core.FontSize(c, 16)) }) if b.Clicked() && !b.IsDisabled() { *tab = i } @@ -155,18 +157,18 @@ func emojiTabs(c *ui.Context, k theme.Tokens, tab *int, recent bool) { } func emojiToneRow(c *ui.Context, k theme.Tokens, tone *int) { - ui.Row(c).Gap(2).AlignItems(ui.Center).FocusGroup(ui.Horizontal).Role(ui.RoleRadioGroup).Label(msg(c, "emoji_picker.tone")).Children(func() { - ui.Text(c, msg(c, "emoji_picker.tone")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(0, 6, 0, 0) + ui.Row(c).Gap(2).AlignItems(ui.Center).FocusGroup(ui.Horizontal).Role(ui.RoleRadioGroup).Label(core.Msg(c, "emoji_picker.tone")).Children(func() { + ui.Text(c, core.Msg(c, "emoji_picker.tone")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Margin(0, 6, 0, 0) for i := range emojiTones { b := ui.ButtonBase(c).Key(i).Size(theme.MinHitSize, theme.MinHitSize).Radius(theme.ControlRadius).Role(ui.RoleRadio). - Label(fmt.Sprintf(msg(c, "emoji_picker.toneN"), i)).Selected(i == *tone).Background(ui.Transparent) + Label(fmt.Sprintf(core.Msg(c, "emoji_picker.toneN"), i)).Selected(i == *tone).Background(ui.Transparent) internal.Ring(b, k.Focus, theme.ControlRadius) if i == *tone { b.Background(k.Selection).Border(theme.BorderWidth, k.AccentText) } else if b.Hovered() { b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, emojiToned(emoji{char: "👋", toned: true}, i)).FontSize(FontSize(c, 15)) }) + b.Children(func() { ui.Text(c, emojiToned(emoji{char: "👋", toned: true}, i)).FontSize(core.FontSize(c, 15)) }) if b.Clicked() && !b.IsDisabled() { *tone = i slog.Debug("mujica: emoji_picker tone", "tone", i) @@ -177,12 +179,12 @@ func emojiToneRow(c *ui.Context, k theme.Tokens, tone *int) { func emojiCell(c *ui.Context, k theme.Tokens, e emoji, tone int) string { s := emojiToned(e, tone) - b := ui.ButtonBase(c).Key(e.char).Size(36, 36).Radius(theme.ControlRadius).Label(emojiLabel(c, e)).Tooltip(emojiLabel(c, e)).Transition(colorFade) + b := ui.ButtonBase(c).Key(e.char).Size(36, 36).Radius(theme.ControlRadius).Label(emojiLabel(c, e)).Tooltip(emojiLabel(c, e)).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() || b.Focused() { b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, s).FontSize(FontSize(c, 20)) }) + b.Children(func() { ui.Text(c, s).FontSize(core.FontSize(c, 20)) }) if b.Clicked() { return s } @@ -193,5 +195,5 @@ func emojiLabel(c *ui.Context, e emoji) string { if e.name == e.char { return e.char } - return msg(c, "emoji_picker.name_"+strings.ReplaceAll(e.name, " ", "_")) + return core.Msg(c, "emoji_picker.name_"+strings.ReplaceAll(e.name, " ", "_")) } diff --git a/emoji_picker_test.go b/messaging/emoji_picker_test.go similarity index 92% rename from emoji_picker_test.go rename to messaging/emoji_picker_test.go index 6a8155a..8feed57 100644 --- a/emoji_picker_test.go +++ b/messaging/emoji_picker_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestEmojiPickerPickSearchToneRecent(t *testing.T) { @@ -12,7 +14,7 @@ func TestEmojiPickerPickSearchToneRecent(t *testing.T) { tone := 0 var picked []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := EmojiPicker(c, EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { picked = append(picked, r.Picked) } @@ -51,7 +53,7 @@ func TestEmojiPickerPickSearchToneRecent(t *testing.T) { func TestEmojiPickerKeyboardAndDisabled(t *testing.T) { var got string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := EmojiPicker(c, EmojiPickerOptions{}); r.Picked != "" { got = r.Picked } @@ -66,7 +68,7 @@ func TestEmojiPickerKeyboardAndDisabled(t *testing.T) { } got = "" off := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := EmojiPicker(c, EmojiPickerOptions{Disabled: true}); r.Picked != "" { got = r.Picked } @@ -90,7 +92,7 @@ func TestEmojiPickerSnapshot(t *testing.T) { func TestEmojiPickerKeepsCategoryAfterFirstPick(t *testing.T) { var recent []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EmojiPicker(c, EmojiPickerOptions{Recent: &recent}) }, 400, 400) if err := tt.Click("People"); err != nil { @@ -113,7 +115,7 @@ func TestEmojiPickerKeepsCategoryAfterFirstPick(t *testing.T) { func TestEmojiPickerChineseSearch(t *testing.T) { picked := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) if r := EmojiPicker(c, EmojiPickerOptions{}); r.Picked != "" { picked = r.Picked } @@ -133,7 +135,7 @@ func TestEmojiPickerChineseSearch(t *testing.T) { func TestEmojiPickerInitialHistory(t *testing.T) { recent := []string{"🌹", "🍷"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) EmojiPicker(c, EmojiPickerOptions{Recent: &recent}) }, 400, 400) if got := mediaTextCount(tt, "🌹"); got != 3 { diff --git a/messaging/example_messaging_test.go b/messaging/example_messaging_test.go new file mode 100644 index 0000000..e477223 --- /dev/null +++ b/messaging/example_messaging_test.go @@ -0,0 +1,247 @@ +package messaging_test + +import ( + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/messaging" +) + +func ExampleChannelList() { + groups := []messaging.ChannelGroup{{ID: "ch", Title: "Channels", Channels: []messaging.Channel{ + {ID: "general", Name: "general", Unread: 3}, + {ID: "rehearsal", Name: "rehearsal", Pinned: true}, + }}} + selected := "general" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if messaging.ChannelList(c, &selected, groups, messaging.ChannelListOptions{}).Changed() { + _ = selected // show that channel + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleChatMessage() { + prev := messaging.ChatEntry{ID: "1", Author: "Uika", Time: time.Now(), Text: "Rehearsal at nine."} + m := messaging.ChatEntry{ID: "2", Author: "Uika", Time: time.Now(), Text: "Bring the masks.", Edited: true} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.ChatMessage(c, prev, messaging.ChatMessageOptions{}) + messaging.ChatMessage(c, m, messaging.ChatMessageOptions{ + Continued: messaging.ChatContinues(prev, m), + Reactions: func() { display.Tag(c, "👍 2", display.TagOptions{}) }, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleUnreadDivider() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.UnreadDivider(c, 3) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleReadReceipt() { + readers := []display.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.ReadReceipt(c, messaging.ReceiptRead, messaging.ReadReceiptOptions{Readers: readers}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMemberList() { + var s messaging.MemberListState + members := []messaging.Member{ + {ID: "1", Name: "Sakiko", Presence: messaging.PresenceOnline, Role: "Leader"}, + {ID: "2", Name: "Umiri", Presence: messaging.PresenceOffline}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := messaging.MemberList(c, &s, members, messaging.MemberListOptions{}); r.Opened != "" { + _ = r.Opened // show that member's profile + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleUserProfileCard() { + profile := messaging.UserProfile{Name: "Sakiko Togawa", Presence: messaging.PresenceBusy, Role: "Keyboard", Status: "In rehearsal"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.UserProfileCard(c, profile, messaging.UserProfileCardOptions{Actions: func() { + input.Button(c, "Message", input.ButtonOptions{}) + }}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOnlineStatus() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.OnlineStatus(c, messaging.PresenceAway, messaging.OnlineStatusOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleStatusSetter() { + status := messaging.CustomStatus{Emoji: "🎻", Text: "Rehearsing"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if messaging.StatusSetter(c, &status, messaging.StatusSetterOptions{}).Changed() { + _ = status // publish it; clear it once status.Until passes + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCallControls() { + st := messaging.CallState{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.CallControls(c, st, messaging.CallControlsOptions{ + OnMute: func(on bool) { st.Muted = on }, + OnCamera: func(off bool) { st.CameraOff = off }, + OnShare: func(on bool) { st.Sharing = on }, + OnHangUp: func() {}, // leave the call + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVideoCallGrid() { + people := []messaging.CallParticipant{ + {ID: "1", Name: "Sakiko", Speaking: true}, // Frame: the latest decoded frame + {ID: "2", Name: "Uika", Muted: true}, + } + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.VideoCallGrid(c, people, messaging.VideoCallGridOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleIncomingCallDialog() { + ringing := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.IncomingCallDialog(c, &ringing, messaging.IncomingCall{Caller: "Sakiko", Video: true}, messaging.IncomingCallOptions{ + OnAccept: func(video bool) {}, // join, with the camera on when video + OnDecline: func() {}, + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleEmojiPicker() { + var recent []string + tone := 0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := messaging.EmojiPicker(c, messaging.EmojiPickerOptions{Recent: &recent, Tone: &tone}); r.Picked != "" { + _ = r.Picked // insert it + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePinnedMessages() { + pins := []messaging.ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := messaging.PinnedMessages(c, pins, messaging.PinnedMessagesOptions{}) + if r.Unpinned != "" { + pins = pins[:0] // unpin it + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleThreadPanel() { + open := true + root := messaging.ChatEntry{ID: "r", Author: "Sakiko", Text: "Set list for Friday?"} + var replies []messaging.ChatEntry + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := messaging.ThreadPanel(c, &open, root, replies, messaging.ThreadPanelOptions{}); r.Sent != "" { + replies = append(replies, messaging.ChatEntry{ID: r.Sent, Author: "Me", Text: r.Sent, Own: true}) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMailList() { + var s data.ListState[string] + mails := []messaging.MailItem{{ID: "1", From: "Sakiko", Subject: "Second act", Unread: true, Attachments: 1}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := messaging.MailList(c, &s, mails, messaging.MailListOptions{}); r.Starred != "" { + mails[0].Starred = !mails[0].Starred + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMailReader() { + m := messaging.Mail{From: "Sakiko", Subject: "Second act", Body: "The score is attached.\n> Can you send it?", + Attachments: []messaging.MailAttachment{{Name: "score.pdf", Size: 2048}}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if r := messaging.MailReader(c, m); r.Link != "" { + c.OpenURL(r.Link) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMailComposer() { + draft := messaging.MailDraft{To: []string{"uika@example.com"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := messaging.MailComposer(c, &draft, messaging.MailComposerOptions{ + OnSaveDraft: func(d messaging.MailDraft) {}, // store d + }) + if r.Sent { + draft = messaging.MailDraft{} + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleRecipientInput() { + var to []string + contacts := []messaging.Contact{{Name: "Uika Misumi", Email: "uika@example.com"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.RecipientInput(c, &to, messaging.RecipientInputOptions{Contacts: contacts, Label: "To"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleQuotedText() { + open := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + messaging.QuotedText(c, "> Can you send the score?", &open) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSnoozePicker() { + var until time.Time + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if messaging.SnoozePicker(c, &until, messaging.SnoozePickerOptions{}).Changed() { + _ = until // hide the mail until then + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/messaging/helpers_test.go b/messaging/helpers_test.go new file mode 100644 index 0000000..a6d02ec --- /dev/null +++ b/messaging/helpers_test.go @@ -0,0 +1,61 @@ +package messaging + +import ( + "image" + "image/color" + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// mediaPanics fails t unless fn panics. +func mediaPanics(t *testing.T, name string, fn func(c *ui.Context)) { + t.Helper() + defer func() { + if recover() == nil { + t.Errorf("%s: no panic", name) + } + }() + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + fn(c) + }, 400, 300) + tt.Frame() +} + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func mediaTestBitmap(w, h int) *ui.Bitmap { return ui.NewBitmap(mediaTestImage(w, h)) } + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} + +// mediaTestImage is a w×h picture whose left half is wine and right half ivory. +func mediaTestImage(w, h int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := range h { + for x := range w { + c := color.RGBA{125, 32, 52, 255} + if x >= w/2 { + c = color.RGBA{242, 235, 221, 255} + } + if y < h/8 { + c = color.RGBA{173, 145, 100, 255} + } + img.Set(x, y, c) + } + } + return img +} diff --git a/incoming_call_dialog.go b/messaging/incoming_call_dialog.go similarity index 56% rename from incoming_call_dialog.go rename to messaging/incoming_call_dialog.go index 93a6958..3ef9932 100644 --- a/incoming_call_dialog.go +++ b/messaging/incoming_call_dialog.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,7 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IncomingCall is who calls; Caller is required. @@ -32,49 +36,49 @@ func IncomingCallDialog(c *ui.Context, open *bool, call IncomingCall, opts Incom if call.Caller == "" || opts.OnAccept == nil || opts.OnDecline == nil { panic("mujica: IncomingCallDialog needs Caller, OnAccept and OnDecline") } - site, focus := overlaySite(c) + site, focus := core.OverlaySite(c) if opts.Disabled || site.IsDisabled() { *open = false } if !*open { - focus.closed(c, site) + focus.Closed(c, site) return } - overlayLayer(c, open) - k := Tokens(c) - u := Density(c).Unit() - title := fmt.Sprintf(msg(c, "incoming_call_dialog.title"), call.Caller) + core.OverlayLayer(c, open) + k := core.Tokens(c) + u := core.Density(c).Unit() + title := fmt.Sprintf(core.Msg(c, "incoming_call_dialog.title"), call.Caller) shown, accepted, video, declined, backClicked := true, false, false, false, false panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) { backClicked = back.Clicked() // a click outside neither answers nor refuses - p := overlayEnter(c, panel) - back.Background(overlayScrim(k).Alpha(p)).Padding(16) - dialogPanel(c, panel, 420, false, p) + p := core.OverlayEnter(c, panel) + back.Background(overlay.OverlayScrim(k).Alpha(p)).Padding(16) + overlay.DialogPanel(c, panel, 420, false, p) panel.Role(ui.RoleAlertDialog).Label(title).AlignItems(ui.Center) ui.Scroll(c).Shrink(1).FillWidth().Gap(u * 3).AlignItems(ui.Center).Children(func() { - Avatar(c, call.Caller, AvatarOptions{Image: call.Image, Size: 72}) - ui.Text(c, call.Caller).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).TextAlign(ui.Center) - kind := msg(c, "incoming_call_dialog.voice") + display.Avatar(c, call.Caller, display.AvatarOptions{Image: call.Image, Size: 72}) + ui.Text(c, call.Caller).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).TextAlign(ui.Center) + kind := core.Msg(c, "incoming_call_dialog.voice") if call.Video { - kind = msg(c, "incoming_call_dialog.video") + kind = core.Msg(c, "incoming_call_dialog.video") } ui.Text(c, kind).TextColor(k.TextMuted) }) ui.Row(c).Shrink(0).Gap(u*3).Wrap().Justify(ui.Center).Margin(u*2, 0, 0, 0).Children(func() { - decline := Button(c, msg(c, "incoming_call_dialog.decline"), ButtonOptions{Variant: Danger, Icon: msgHangUp, Label: msg(c, "incoming_call_dialog.decline")}) - accept := Button(c, msg(c, "incoming_call_dialog.accept"), ButtonOptions{Icon: msgPhone, Label: msg(c, "incoming_call_dialog.accept")}) + decline := input.Button(c, core.Msg(c, "incoming_call_dialog.decline"), input.ButtonOptions{Variant: input.Danger, Icon: msgHangUp, Label: core.Msg(c, "incoming_call_dialog.decline")}) + accept := input.Button(c, core.Msg(c, "incoming_call_dialog.accept"), input.ButtonOptions{Icon: msgPhone, Label: core.Msg(c, "incoming_call_dialog.accept")}) if opts.FocusAccept { accept.AutoFocus() } else { decline.AutoFocus() } declined, accepted = decline.Clicked(), accept.Clicked() - if call.Video && Button(c, msg(c, "incoming_call_dialog.acceptVideo"), ButtonOptions{Variant: Secondary, Icon: mediaCamera, Label: msg(c, "incoming_call_dialog.acceptVideo")}).Clicked() { + if call.Video && input.Button(c, core.Msg(c, "incoming_call_dialog.acceptVideo"), input.ButtonOptions{Variant: input.Secondary, Icon: core.MediaCamera, Label: core.Msg(c, "incoming_call_dialog.acceptVideo")}).Clicked() { accepted, video = true, true } }) }) - focus.shown(panel) + focus.Shown(panel) if !shown && !accepted && !backClicked { declined = true // Escape } diff --git a/incoming_call_dialog_test.go b/messaging/incoming_call_dialog_test.go similarity index 95% rename from incoming_call_dialog_test.go rename to messaging/incoming_call_dialog_test.go index 1f76ec1..7f1baa7 100644 --- a/incoming_call_dialog_test.go +++ b/messaging/incoming_call_dialog_test.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIncomingCallDefaultFocusAndEscape(t *testing.T) { @@ -57,7 +59,7 @@ func TestIncomingCallSnapshot(t *testing.T) { func TestIncomingCallDisabledAncestorCloses(t *testing.T) { open, disabled, events := true, false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { IncomingCallDialog(c, &open, IncomingCall{Caller: "Caller"}, IncomingCallOptions{ OnAccept: func(bool) { events++ }, OnDecline: func() { events++ }, @@ -75,7 +77,7 @@ func TestIncomingCallDisabledAncestorCloses(t *testing.T) { func TestIncomingCallSmallWindowKeyboardActions(t *testing.T) { open, accepted := true, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IncomingCallDialog(c, &open, IncomingCall{Caller: "Caller", Video: true}, IncomingCallOptions{ OnAccept: func(bool) { accepted++ }, OnDecline: func() {}, }) diff --git a/mail_composer.go b/messaging/mail_composer.go similarity index 69% rename from mail_composer.go rename to messaging/mail_composer.go index b0863be..9ea1703 100644 --- a/mail_composer.go +++ b/messaging/mail_composer.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MailDraft is a mail being written. @@ -55,10 +58,10 @@ func MailComposer(c *ui.Context, d *MailDraft, opts MailComposerOptions) MailCom if opts.OnSaveDraft == nil { panic("mujica: MailComposer needs OnSaveDraft") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r MailComposerResult - col := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(msg(c, "mail_composer.label")).Disabled(opts.Disabled) + col := ui.Column(c).Gap(u * 3).Role(ui.RoleGroup).Label(core.Msg(c, "mail_composer.label")).Disabled(opts.Disabled) r.Element = col st := ui.Local(col, composerKey{}, func() *composerState { return &composerState{} }) s := *st @@ -76,8 +79,8 @@ func MailComposer(c *ui.Context, d *MailDraft, opts MailComposerOptions) MailCom col.Children(func() { field := func(key string, list *[]string, extra func()) { ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() { - label := ui.Text(c, msg(c, key)).Width(56).TextColor(k.TextMuted).Margin(u*2, 0, 0, 0).SingleLine() - parts := RecipientInput(c, list, RecipientInputOptions{Contacts: opts.Contacts, Label: msg(c, key), Disabled: opts.Disabled}) + label := ui.Text(c, core.Msg(c, key)).Width(56).TextColor(k.TextMuted).Margin(u*2, 0, 0, 0).SingleLine() + parts := RecipientInput(c, list, RecipientInputOptions{Contacts: opts.Contacts, Label: core.Msg(c, key), Disabled: opts.Disabled}) parts.Element.Grow(1).MinWidth(0) if label.Clicked() && !parts.Input.IsDisabled() { parts.Input.Focus() @@ -88,10 +91,10 @@ func MailComposer(c *ui.Context, d *MailDraft, opts MailComposerOptions) MailCom }) } field("mail_composer.to", &d.To, func() { - if !s.cc && Button(c, msg(c, "mail_composer.cc"), ButtonOptions{Variant: Ghost}).Clicked() { + if !s.cc && input.Button(c, core.Msg(c, "mail_composer.cc"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.cc = true } - if !s.bcc && Button(c, msg(c, "mail_composer.bcc"), ButtonOptions{Variant: Ghost}).Clicked() { + if !s.bcc && input.Button(c, core.Msg(c, "mail_composer.bcc"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { s.bcc = true } }) @@ -101,8 +104,8 @@ func MailComposer(c *ui.Context, d *MailDraft, opts MailComposerOptions) MailCom if s.bcc { field("mail_composer.bcc", &d.Bcc, nil) } - TextInput(c, &d.Subject, TextInputOptions{Placeholder: msg(c, "mail_composer.subject"), Label: msg(c, "mail_composer.subject"), Disabled: opts.Disabled}) - if TextArea(c, &d.Body, TextAreaOptions{Label: msg(c, "mail_composer.body"), MinHeight: 160, Disabled: opts.Disabled}).Submitted() && + input.TextInput(c, &d.Subject, input.TextInputOptions{Placeholder: core.Msg(c, "mail_composer.subject"), Label: core.Msg(c, "mail_composer.subject"), Disabled: opts.Disabled}) + if input.TextArea(c, &d.Body, input.TextAreaOptions{Label: core.Msg(c, "mail_composer.body"), MinHeight: 160, Disabled: opts.Disabled}).Submitted() && composerReady(*d) { r.Sent = true } @@ -111,16 +114,16 @@ func MailComposer(c *ui.Context, d *MailDraft, opts MailComposerOptions) MailCom } ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ready := composerReady(*d) - if Button(c, msg(c, "mail_composer.send"), ButtonOptions{Icon: msgSend, Disabled: !ready}).Clicked() { + if input.Button(c, core.Msg(c, "mail_composer.send"), input.ButtonOptions{Icon: msgSend, Disabled: !ready}).Clicked() { r.Sent = true } - if opts.OnAttach != nil && Button(c, msg(c, "mail_composer.attach"), ButtonOptions{Variant: Secondary, Icon: msgPaperclip}).Clicked() { + if opts.OnAttach != nil && input.Button(c, core.Msg(c, "mail_composer.attach"), input.ButtonOptions{Variant: input.Secondary, Icon: msgPaperclip}).Clicked() { slog.Debug("mujica: mail_composer attach") opts.OnAttach() } ui.Box(c).Grow(1) composerStatus(c, k, s) - if Button(c, msg(c, "mail_composer.discard"), ButtonOptions{Variant: Ghost, Icon: icons.Must("trash-2")}).Clicked() { + if input.Button(c, core.Msg(c, "mail_composer.discard"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("trash-2")}).Clicked() { r.Discarded = true } }) @@ -161,22 +164,22 @@ func composerStatus(c *ui.Context, k theme.Tokens, s *composerState) { text := "" switch { case s.dirty: - text = msg(c, "mail_composer.unsaved") + text = core.Msg(c, "mail_composer.unsaved") case !s.saved.IsZero(): - text = fmt.Sprintf(msg(c, "mail_composer.saved"), s.saved.Format("15:04")) + text = fmt.Sprintf(core.Msg(c, "mail_composer.saved"), s.saved.Format("15:04")) } if text != "" { - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus) } } // composerAttachments lists attached files with remove buttons. func composerAttachments(c *ui.Context, k theme.Tokens, d *MailDraft, disabled bool) { - ui.Row(c).Gap(Density(c).Unit() * 2).Wrap().Children(func() { + ui.Row(c).Gap(core.Density(c).Unit() * 2).Wrap().Children(func() { for i, a := range d.Attachments { - var v TagView + var v display.TagView ui.Box(c).Key(fmt.Sprint(i, a.Name)).Children(func() { - v = Tag(c, a.Name, TagOptions{Icon: "file", Closable: true, Disabled: disabled}) + v = display.Tag(c, a.Name, display.TagOptions{Icon: "file", Closable: true, Disabled: disabled}) }) if v.Closed { d.Attachments = slices.Delete(slices.Clone(d.Attachments), i, i+1) diff --git a/mail_composer_test.go b/messaging/mail_composer_test.go similarity index 93% rename from mail_composer_test.go rename to messaging/mail_composer_test.go index a842e4b..f8f5a71 100644 --- a/mail_composer_test.go +++ b/messaging/mail_composer_test.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "slices" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestMailComposerDisabledDefersSave(t *testing.T) { @@ -13,7 +16,7 @@ func TestMailComposerDisabledDefersSave(t *testing.T) { disabled, saves := false, 0 var state *composerState tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := MailComposer(c, &draft, MailComposerOptions{OnSaveDraft: func(MailDraft) { saves++ }}) state = *ui.Local(r.Element, composerKey{}, func() *composerState { panic("missing state") }) @@ -40,11 +43,11 @@ func TestMailComposerManualSaveAndFocus(t *testing.T) { draft := MailDraft{To: []string{"a@example.com"}} saves, sent := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if MailComposer(c, &draft, MailComposerOptions{OnSaveDraft: func(MailDraft) { saves++ }}).Sent { sent++ } - TextInput(c, new(string), TextInputOptions{Label: "Other"}) + input.TextInput(c, new(string), input.TextInputOptions{Label: "Other"}) }, 600, 700) if err := tt.Click("Subject"); err != nil { t.Fatal(err) @@ -67,7 +70,7 @@ func TestMailComposerAutosaveAndSend(t *testing.T) { var saves []MailDraft sent := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(560).Children(func() { if MailComposer(c, &d, MailComposerOptions{OnSaveDraft: func(m MailDraft) { saves = append(saves, m) }}).Sent { sent++ @@ -102,7 +105,7 @@ func TestMailComposerNeedsRecipientsAndDisabled(t *testing.T) { d := MailDraft{Subject: "x"} sent := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if MailComposer(c, &d, MailComposerOptions{OnSaveDraft: func(MailDraft) {}}).Sent { sent++ } @@ -129,7 +132,7 @@ func TestMailComposerAttachmentsAndAddressFields(t *testing.T) { d := MailDraft{To: []string{"uika@example.com"}} attachments, discards := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MailComposer(c, &d, MailComposerOptions{OnSaveDraft: func(MailDraft) {}, OnAttach: func() { d.Attachments = append(d.Attachments, MailAttachment{Name: "score.pdf", Size: 2048}) attachments++ @@ -176,7 +179,7 @@ func TestMailComposerAttachmentsAndAddressFields(t *testing.T) { func TestMailComposerLoadedAddressFields(t *testing.T) { d := MailDraft{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MailComposer(c, &d, MailComposerOptions{OnSaveDraft: func(MailDraft) {}}) }, 700, 700) d = MailDraft{Cc: []string{"cc@example.com"}, Bcc: []string{"bcc@example.com"}} diff --git a/mail_list.go b/messaging/mail_list.go similarity index 78% rename from mail_list.go rename to messaging/mail_list.go index 71e6f88..842d096 100644 --- a/mail_list.go +++ b/messaging/mail_list.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -7,8 +7,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/media" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MailItem supplies a unique ID and mail preview data. @@ -40,7 +43,7 @@ type MailListResult struct { func (r MailListResult) Changed() bool { return r.changed } // MailList virtualizes previews, selection and star actions. -func MailList(c *ui.Context, s *ListState[string], mails []MailItem, opts MailListOptions) MailListResult { +func MailList(c *ui.Context, s *data.ListState[string], mails []MailItem, opts MailListOptions) MailListResult { if opts.Height < 0 { panic("mujica: MailList Height must not be negative") } @@ -48,18 +51,18 @@ func MailList(c *ui.Context, s *ListState[string], mails []MailItem, opts MailLi if h == 0 { h = 420 } - k := Tokens(c) - u := Density(c).Unit() - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + u := core.Density(c).Unit() + small := core.FontSize(c, theme.CaptionSize) var r MailListResult box := ui.Column(c).Height(h).Disabled(opts.Disabled) r.Element = box box.Children(func() { - v := List(c, s, len(mails), ListOptions[string]{ + v := data.List(c, s, len(mails), data.ListOptions[string]{ Key: func(i int) string { return mails[i].ID }, Label: func(i int) string { return mailLabel(c, mails[i]) }, Multiple: true, - Empty: msg(c, "mail_list.empty"), + Empty: core.Msg(c, "mail_list.empty"), }, func(i int) { m := mails[i] if m.ID == "" { @@ -70,7 +73,7 @@ func MailList(c *ui.Context, s *ListState[string], mails []MailItem, opts MailLi ui.Box(c).Size(8, 8).Radius(4).Background(k.AccentText) } }) - star := mediaToggleAction(c, icons.Must("star"), fmt.Sprintf(msg(c, "mail_list.star"), m.Subject), m.Starred) + star := media.MediaToggleAction(c, icons.Must("star"), fmt.Sprintf(core.Msg(c, "mail_list.star"), m.Subject), m.Starred) if m.Starred { star.TextColor(k.Warning) } @@ -84,7 +87,7 @@ func MailList(c *ui.Context, s *ListState[string], mails []MailItem, opts MailLi from.FontWeight(700) } if m.Attachments > 0 { - ui.Icon(c, msgPaperclip).Size(13, 13).TextColor(k.TextMuted).Label(fmt.Sprintf(msg(c, "mail_list.attachments"), m.Attachments)) + ui.Icon(c, msgPaperclip).Size(13, 13).TextColor(k.TextMuted).Label(fmt.Sprintf(core.Msg(c, "mail_list.attachments"), m.Attachments)) } if !m.Time.IsZero() { ui.Text(c, mailTime(c, m.Time)).FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() @@ -121,10 +124,10 @@ func MailList(c *ui.Context, s *ListState[string], mails []MailItem, opts MailLi func mailLabel(c *ui.Context, m MailItem) string { s := m.From + ", " + m.Subject if m.Starred { - s = msg(c, "mail_list.starred") + ", " + s + s = core.Msg(c, "mail_list.starred") + ", " + s } if m.Unread { - s = msg(c, "mail_list.unread") + ", " + s + s = core.Msg(c, "mail_list.unread") + ", " + s } return s } @@ -135,5 +138,5 @@ func mailTime(c *ui.Context, t time.Time) string { if y, m, d := t.Date(); y == now.Year() && m == now.Month() && d == now.Day() { return t.Format("15:04") } - return t.Format(msg(c, "mail_list.date")) + return t.Format(core.Msg(c, "mail_list.date")) } diff --git a/mail_list_test.go b/messaging/mail_list_test.go similarity index 85% rename from mail_list_test.go rename to messaging/mail_list_test.go index 5d50b59..b3272ce 100644 --- a/mail_list_test.go +++ b/messaging/mail_list_test.go @@ -1,18 +1,21 @@ -package mujica +package messaging import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) func TestMailListSelectStarOpen(t *testing.T) { - var s ListState[string] + var s data.ListState[string] mails := mailTestItems(6) var opened, starred []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) r := MailList(c, &s, mails, MailListOptions{}) if r.Opened != "" { opened = append(opened, r.Opened) @@ -43,10 +46,10 @@ func TestMailListSelectStarOpen(t *testing.T) { } func TestMailListBuildsOnlyVisible(t *testing.T) { - var s ListState[string] + var s data.ListState[string] mails := mailTestItems(100000) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MailList(c, &s, mails, MailListOptions{}) }, 600, 500) tt.Frame() @@ -62,10 +65,10 @@ func TestMailListBuildsOnlyVisible(t *testing.T) { } func TestMailListDisabled(t *testing.T) { - var s ListState[string] + var s data.ListState[string] starred := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if MailList(c, &s, mailTestItems(3), MailListOptions{Disabled: true}).Starred != "" { starred++ } @@ -84,7 +87,7 @@ func TestMailListSnapshot(t *testing.T) { {ID: "3", From: "Umiri Yahata", Subject: "Venue", Snippet: "The hall is booked."}, } snapshot(t, "mail_list", 480, 260, func(c *ui.Context) { - s := ui.Local(c.Root(), "mail", func() *ListState[string] { return &ListState[string]{} }) + s := ui.Local(c.Root(), "mail", func() *data.ListState[string] { return &data.ListState[string]{} }) ui.Column(c).Padding(20).Width(440).Children(func() { MailList(c, *s, mails, MailListOptions{Height: 220}) }) }) } diff --git a/mail_reader.go b/messaging/mail_reader.go similarity index 75% rename from mail_reader.go rename to messaging/mail_reader.go index 7fd0968..2ffdee3 100644 --- a/mail_reader.go +++ b/messaging/mail_reader.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "log/slog" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MailFormat selects plain text or safe Markdown. @@ -56,41 +59,41 @@ func MailReader(c *ui.Context, m Mail) MailReaderResult { if m.Format != MailPlain && m.Format != MailMarkdown { panic("mujica: unknown MailFormat") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() r := MailReaderResult{Opened: -1, Downloaded: -1} col := ui.Column(c).Gap(u * 4).MinWidth(0).Role(ui.RoleGroup).Label(m.Subject) r.Element = col - small := FontSize(c, theme.CaptionSize) + small := core.FontSize(c, theme.CaptionSize) col.Children(func() { - ui.Text(c, m.Subject).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).FontWeight(600) + ui.Text(c, m.Subject).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).FontWeight(600) ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Children(func() { - Avatar(c, m.From, AvatarOptions{Size: 36}) + display.Avatar(c, m.From, display.AvatarOptions{Size: 36}) ui.Column(c).Gap(2).Grow(1).MinWidth(0).Children(func() { ui.Text(c, m.From).FontWeight(600).Selectable().FocusRing(false) for _, line := range [][2]string{{"mail_reader.to", strings.Join(m.To, ", ")}, {"mail_reader.cc", strings.Join(m.Cc, ", ")}} { if line[1] != "" { - ui.Text(c, msg(c, line[0])+" "+line[1]).FontSize(small).TextColor(k.TextMuted).Selectable().FocusRing(false) + ui.Text(c, core.Msg(c, line[0])+" "+line[1]).FontSize(small).TextColor(k.TextMuted).Selectable().FocusRing(false) } } }) if !m.Date.IsZero() { - ui.Text(c, messagingDate(c, m.Date, "mail_reader.date")).FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() + ui.Text(c, core.MessagingDate(c, m.Date, "mail_reader.date")).FontSize(small).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() } }) ui.Box(c).Height(1).Background(k.Border) if m.Format == MailMarkdown { - if url, ok := MarkdownView(c, m.Body).Link(); ok { + if url, ok := chat.MarkdownView(c, m.Body).Link(); ok { r.Link = url } } else { mailPlainBody(c, ui.Local(col, mailQuotesKey{}, func() map[string]bool { return map[string]bool{} }), m.Body) } if len(m.Attachments) > 0 { - ui.Text(c, msg(c, "mail_reader.attachments")).FontSize(small).FontWeight(600).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "mail_reader.attachments")).FontSize(small).FontWeight(600).TextColor(k.TextMuted) ui.Row(c).Gap(u * 2).Wrap().Children(func() { for i, a := range m.Attachments { - f := FileMessage(c, FileMessageOptions(a)) + f := chat.FileMessage(c, chat.FileMessageOptions(a)) f.Element.Key(i) if f.Opened() { r.Opened = i @@ -111,7 +114,7 @@ func MailReader(c *ui.Context, m Mail) MailReaderResult { // mailPlainBody shows plain text, folding each run of quoted lines. func mailPlainBody(c *ui.Context, open *map[string]bool, body string) { lines := strings.Split(body, "\n") - ui.Column(c).Gap(Density(c).Unit() * 2).MinWidth(0).Children(func() { + ui.Column(c).Gap(core.Density(c).Unit() * 2).MinWidth(0).Children(func() { for i := 0; i < len(lines); { j := i quoted := strings.HasPrefix(lines[i], ">") diff --git a/mail_reader_test.go b/messaging/mail_reader_test.go similarity index 94% rename from mail_reader_test.go rename to messaging/mail_reader_test.go index 03ff774..c946248 100644 --- a/mail_reader_test.go +++ b/messaging/mail_reader_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMailReaderPlainQuotesAndAttachments(t *testing.T) { @@ -12,7 +14,7 @@ func TestMailReaderPlainQuotesAndAttachments(t *testing.T) { Attachments: []MailAttachment{{Name: "score.pdf", Size: 2048}}} opened := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(500).Children(func() { if r := MailReader(c, m); r.Opened >= 0 { opened = r.Opened @@ -40,7 +42,7 @@ func TestMailReaderMarkdownIsSafe(t *testing.T) { m := Mail{From: "A", Subject: "B", Format: MailMarkdown, Body: "**Bold** and [the hall](https://example.com)\n\n"} link := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(500).Children(func() { if r := MailReader(c, m); r.Link != "" { link = r.Link diff --git a/member_list.go b/messaging/member_list.go similarity index 85% rename from member_list.go rename to messaging/member_list.go index 3a9597b..247d4c5 100644 --- a/member_list.go +++ b/messaging/member_list.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Member supplies a unique ID, name, presence and role. @@ -67,8 +70,8 @@ func MemberList(c *ui.Context, s *MemberListState, members []Member, opts Member h = 360 } memberGroup(s, members) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) rows := s.cache.rows if s.looked != s.Selected || s.lookedGen != s.cache.gen { s.looked, s.lookedGen, s.cursor = s.Selected, s.cache.gen, -1 @@ -98,21 +101,21 @@ func MemberList(c *ui.Context, s *MemberListState, members []Member, opts Member r.Element = box box.Children(func() { if len(rows) == 0 { - ui.Text(c, msg(c, "member_list.empty")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "member_list.empty")).TextColor(k.TextMuted) return } - restore := dataSelectTheme(c, 0) + restore := data.DataSelectTheme(c, 0) l := ui.List(c, &s.rows, len(rows), func(i int) { if rows[i] < 0 { ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).AlignItems(ui.Center).Background(k.Surface).Children(func() { - ui.Text(c, memberHeader(c, s, rows[i])).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.TextMuted) + ui.Text(c, memberHeader(c, s, rows[i])).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.TextMuted) }) return } m := members[rows[i]] ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { ui.Box(c).Shrink(0).Children(func() { - Avatar(c, m.Name, AvatarOptions{Image: m.Image, Size: 28}) + display.Avatar(c, m.Name, display.AvatarOptions{Image: m.Image, Size: 28}) ui.Box(c).Absolute().Right(-2).Bottom(-2).Padding(1.5).Radius(6).Background(k.Surface).Children(func() { OnlineStatus(c, m.Presence, OnlineStatusOptions{HideText: true, Size: 9}) }) @@ -122,7 +125,7 @@ func MemberList(c *ui.Context, s *MemberListState, members []Member, opts Member t.TextColor(k.TextMuted) } if m.Role != "" { - Badge(c, BadgeOptions{Text: m.Role, Tone: BadgeNeutral}, nil) + display.Badge(c, display.BadgeOptions{Text: m.Role, Tone: display.BadgeNeutral}, nil) } if i == s.cursor { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) @@ -150,9 +153,9 @@ func MemberList(c *ui.Context, s *MemberListState, members []Member, opts Member func memberHeader(c *ui.Context, s *MemberListState, row int) string { if row == -1 { - return fmt.Sprintf(msg(c, "member_list.online"), s.cache.online) + return fmt.Sprintf(core.Msg(c, "member_list.online"), s.cache.online) } - return fmt.Sprintf(msg(c, "member_list.offline"), s.cache.offline) + return fmt.Sprintf(core.Msg(c, "member_list.offline"), s.cache.offline) } // memberGroup rebuilds the sections only when members changed. diff --git a/member_list_test.go b/messaging/member_list_test.go similarity index 94% rename from member_list_test.go rename to messaging/member_list_test.go index f8810cd..81f36e6 100644 --- a/member_list_test.go +++ b/messaging/member_list_test.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMemberListGroupsAndChoose(t *testing.T) { @@ -11,7 +13,7 @@ func TestMemberListGroupsAndChoose(t *testing.T) { members := memberTestList(8) opened := "" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if r := MemberList(c, &s, members, MemberListOptions{}); r.Opened != "" { opened = r.Opened } @@ -41,7 +43,7 @@ func TestMemberListBuildsOnlyVisibleAndGroupsOnce(t *testing.T) { var s MemberListState members := memberTestList(100000) tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) MemberList(c, &s, members, MemberListOptions{}) }, 400, 500) gen := s.cache.gen @@ -72,7 +74,7 @@ func TestMemberListEmptyDisabledAndReplacement(t *testing.T) { var s MemberListState disabled, events := true, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { r := MemberList(c, &s, members, MemberListOptions{}) if r.Changed() || r.Opened != "" { diff --git a/messaging_calls_test.go b/messaging/messaging_calls_test.go similarity index 75% rename from messaging_calls_test.go rename to messaging/messaging_calls_test.go index b2f205b..08d9d18 100644 --- a/messaging_calls_test.go +++ b/messaging/messaging_calls_test.go @@ -1,7 +1,10 @@ -package mujica +package messaging import ( "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type callCalls struct { @@ -16,8 +19,8 @@ func (k *callCalls) opts() CallControlsOptions { func incomingTester(open *bool, opts IncomingCallOptions, call IncomingCall) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Ring", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Ring", input.ButtonOptions{}).Clicked() { *open = true } IncomingCallDialog(c, open, call, opts) diff --git a/messaging/messaging_icons.go b/messaging/messaging_icons.go new file mode 100644 index 0000000..9f8aa64 --- /dev/null +++ b/messaging/messaging_icons.go @@ -0,0 +1,19 @@ +package messaging + +import ( + "github.com/ZacharyZhang-NY/MujicaUI/core" +) + +var ( + msgBellOff = core.MediaSVG(``) + msgPin = core.MediaSVG(``) + msgPaperclip = core.MediaSVG(``) + msgMicOff = core.MediaSVG(``) + msgVideoOff = core.MediaSVG(``) + msgShare = core.MediaSVG(``) + msgPhone = core.MediaSVG(``) + msgHangUp = core.MediaSVG(``) + msgSpeaking = core.MediaSVG(``) + msgSend = core.MediaSVG(``) + msgClose = core.MediaSVG(``) +) diff --git a/messaging_mail_test.go b/messaging/messaging_mail_test.go similarity index 90% rename from messaging_mail_test.go rename to messaging/messaging_mail_test.go index a04aa60..aadbea4 100644 --- a/messaging_mail_test.go +++ b/messaging/messaging_mail_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "fmt" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func mailTestItems(n int) []MailItem { @@ -22,7 +24,7 @@ var recipientContacts = []Contact{{Name: "Uika Misumi", Email: "uika@example.com func recipientTester(list *[]string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(400).Children(func() { RecipientInput(c, list, RecipientInputOptions{Contacts: recipientContacts, Label: "To"}) }) diff --git a/messaging_people_test.go b/messaging/messaging_people_test.go similarity index 91% rename from messaging_people_test.go rename to messaging/messaging_people_test.go index a4a06e3..e668dbc 100644 --- a/messaging_people_test.go +++ b/messaging/messaging_people_test.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "fmt" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var channelTestGroups = []ChannelGroup{ @@ -17,7 +19,7 @@ var channelTestGroups = []ChannelGroup{ func channelTester(sel *string, disabled bool, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(260).Children(func() { if ChannelList(c, sel, channelTestGroups, ChannelListOptions{Disabled: disabled}).Changed() { *n++ diff --git a/messaging_regression_test.go b/messaging/messaging_regression_test.go similarity index 92% rename from messaging_regression_test.go rename to messaging/messaging_regression_test.go index ac25aa3..c6e7be9 100644 --- a/messaging_regression_test.go +++ b/messaging/messaging_regression_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestMessagingLargeListsBounded(t *testing.T) { @@ -20,7 +22,7 @@ func TestMessagingLargeListsBounded(t *testing.T) { selected := "" groups := []ChannelGroup{{ID: "g", Title: "Group", Channels: channels}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(280).Children(func() { if name == "channels" { ChannelList(c, &selected, groups, ChannelListOptions{Height: 240}) diff --git a/messaging_small_test.go b/messaging/messaging_small_test.go similarity index 88% rename from messaging_small_test.go rename to messaging/messaging_small_test.go index 384dae8..982290e 100644 --- a/messaging_small_test.go +++ b/messaging/messaging_small_test.go @@ -1,9 +1,12 @@ -package mujica +package messaging import ( - "github.com/egoist/mygo/ui" "testing" "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // mediaTextCount counts texts equal to s. @@ -22,7 +25,7 @@ func TestMessagingChineseWeekdays(t *testing.T) { st := CustomStatus{Until: date} var until time.Time tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) StatusSetter(c, &st, StatusSetterOptions{Location: time.UTC}) SnoozePicker(c, &until, SnoozePickerOptions{Now: time.Date(2026, 10, 5, 10, 0, 0, 0, time.UTC), Location: time.UTC}) MailReader(c, Mail{Subject: "Test", From: "a@example.com", Date: date}) diff --git a/online_status.go b/messaging/online_status.go similarity index 82% rename from online_status.go rename to messaging/online_status.go index 8f5017d..21c1acc 100644 --- a/online_status.go +++ b/messaging/online_status.go @@ -1,12 +1,13 @@ -package mujica +package messaging import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PresenceStatus is a person's availability. The zero value is offline. @@ -43,7 +44,7 @@ type OnlineStatusOptions struct { // OnlineStatus shows presence as a distinct shape plus its name. func OnlineStatus(c *ui.Context, p PresenceStatus, opts OnlineStatusOptions) *ui.Element { - name := msg(c, p.key()) + name := core.Msg(c, p.key()) if opts.Size < 0 { panic("mujica: OnlineStatus Size must not be negative") } @@ -51,15 +52,15 @@ func OnlineStatus(c *ui.Context, p PresenceStatus, opts OnlineStatusOptions) *ui if size == 0 { size = 10 } - k := Tokens(c) - row := ui.Row(c).Gap(Density(c).Unit() * 1.5).AlignItems(ui.Center).Shrink(0).Role(ui.RoleStatus).Label(name) + k := core.Tokens(c) + row := ui.Row(c).Gap(core.Density(c).Unit() * 1.5).AlignItems(ui.Center).Shrink(0).Role(ui.RoleStatus).Label(name) if opts.HideText { row.Tooltip(name) } row.Children(func() { ui.Box(c).Size(size, size).Shrink(0).Draw(func(p2 *ui.Painter, r ui.Rect) { presencePaint(p2, k, p, r) }) if !opts.HideText { - ui.Text(c, name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) return row diff --git a/online_status_test.go b/messaging/online_status_test.go similarity index 92% rename from online_status_test.go rename to messaging/online_status_test.go index d968776..0a6f7ef 100644 --- a/online_status_test.go +++ b/messaging/online_status_test.go @@ -1,14 +1,16 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestOnlineStatusTextAndShape(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(12).Children(func() { for _, p := range []PresenceStatus{PresenceOnline, PresenceAway, PresenceBusy, PresenceOffline} { OnlineStatus(c, p, OnlineStatusOptions{}) diff --git a/pinned_messages.go b/messaging/pinned_messages.go similarity index 65% rename from pinned_messages.go rename to messaging/pinned_messages.go index 3b167d4..29a57a8 100644 --- a/pinned_messages.go +++ b/messaging/pinned_messages.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PinnedMessagesResult is the list's element and this frame's events. @@ -32,19 +34,19 @@ func PinnedMessages(c *ui.Context, pins []ChatEntry, opts PinnedMessagesOptions) if h == 0 { h = 320 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var r PinnedMessagesResult - col := ui.Column(c).Gap(u * 2).Role(ui.RoleGroup).Label(fmt.Sprintf(msg(c, "pinned_messages.title"), len(pins))). + col := ui.Column(c).Gap(u * 2).Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "pinned_messages.title"), len(pins))). Disabled(opts.Disabled) r.Element = col col.Children(func() { ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { ui.Icon(c, msgPin).Size(16, 16).TextColor(k.Ornament) - ui.Text(c, fmt.Sprintf(msg(c, "pinned_messages.title"), len(pins))).Font(theme.SerifFont).FontWeight(600) + ui.Text(c, fmt.Sprintf(core.Msg(c, "pinned_messages.title"), len(pins))).Font(theme.SerifFont).FontWeight(600) }) if len(pins) == 0 { - ui.Text(c, msg(c, "pinned_messages.empty")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "pinned_messages.empty")).TextColor(k.TextMuted) return } state := ui.Local(col, "pinned.rows", func() ui.ListState { return ui.ListState{} }) @@ -54,16 +56,16 @@ func PinnedMessages(c *ui.Context, pins []ChatEntry, opts PinnedMessagesOptions) if m.ID == "" || m.Author == "" { panic("mujica: PinnedMessages entries need ID and Author") } - ui.Row(c).MinHeight(Density(c).ControlHeight()).Gap(u).AlignItems(ui.Center).Children(func() { - if QuoteReply(c, m.Author, m.Text).Grow(1).MaxWidth(10000).Clicked() && !col.IsDisabled() { + ui.Row(c).MinHeight(core.Density(c).ControlHeight()).Gap(u).AlignItems(ui.Center).Children(func() { + if chat.QuoteReply(c, m.Author, m.Text).Grow(1).MaxWidth(10000).Clicked() && !col.IsDisabled() { r.Jump = m.ID } - b := iconAction(c, msgPin, fmt.Sprintf(msg(c, "pinned_messages.unpin"), m.Author)) + b := core.IconAction(c, msgPin, fmt.Sprintf(core.Msg(c, "pinned_messages.unpin"), m.Author)) if b.Clicked() && !b.IsDisabled() { r.Unpinned = m.ID } }) - }).Height(min(h, float32(len(pins))*Density(c).ControlHeight())).Gap(u * 2).FocusGroup(ui.Vertical) + }).Height(min(h, float32(len(pins))*core.Density(c).ControlHeight())).Gap(u * 2).FocusGroup(ui.Vertical) }) if r.Jump != "" || r.Unpinned != "" { diff --git a/pinned_messages_test.go b/messaging/pinned_messages_test.go similarity index 93% rename from pinned_messages_test.go rename to messaging/pinned_messages_test.go index c35be4d..b5ace0e 100644 --- a/pinned_messages_test.go +++ b/messaging/pinned_messages_test.go @@ -1,16 +1,18 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPinnedMessages(t *testing.T) { pins := []ChatEntry{{ID: "a", Author: "Sakiko", Text: "Rehearsal moves to nine."}, {ID: "b", Author: "Uika", Text: "Masks are in the hall."}} var jumps, unpins []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(420).Children(func() { r := PinnedMessages(c, pins, PinnedMessagesOptions{}) if r.Jump != "" { diff --git a/quoted_text.go b/messaging/quoted_text.go similarity index 57% rename from quoted_text.go rename to messaging/quoted_text.go index 0d94fb2..cb515a9 100644 --- a/quoted_text.go +++ b/messaging/quoted_text.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,23 +6,25 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // QuotedText folds or expands one level of quoted text. -func QuotedText(c *ui.Context, text string, open *bool) ChoiceResult { +func QuotedText(c *ui.Context, text string, open *bool) core.ChoiceResult { body := QuoteStrip(text) n := strings.Count(body, "\n") + 1 - k := Tokens(c) - u := Density(c).Unit() - title := fmt.Sprintf(msg(c, "quoted_text.show"), n) + k := core.Tokens(c) + u := core.Density(c).Unit() + title := fmt.Sprintf(core.Msg(c, "quoted_text.show"), n) if n == 1 { - title = msg(c, "quoted_text.showOne") + title = core.Msg(c, "quoted_text.showOne") } if *open { - title = msg(c, "quoted_text.hide") + title = core.Msg(c, "quoted_text.hide") } - return Collapsible(c, open, CollapsibleOptions{Title: title}, func() { + return data.Collapsible(c, open, data.CollapsibleOptions{Title: title}, func() { ui.Text(c, body).Selectable().FocusRing(false).TextColor(k.TextMuted).LineHeight(theme.LineHeight). Padding(0, 0, 0, u*3).BorderWidth(0, 0, 0, 2).BorderColor(k.Border) }) diff --git a/quoted_text_test.go b/messaging/quoted_text_test.go similarity index 89% rename from quoted_text_test.go rename to messaging/quoted_text_test.go index 5aebed1..b3653cb 100644 --- a/quoted_text_test.go +++ b/messaging/quoted_text_test.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestQuotedTextAndStrip(t *testing.T) { @@ -12,7 +14,7 @@ func TestQuotedTextAndStrip(t *testing.T) { } open := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) QuotedText(c, "> first\n> second", &open) }, 400, 200) tt.Key(0, ui.KeyTab) @@ -39,7 +41,7 @@ func TestQuotedTextSnapshot(t *testing.T) { func TestQuotedTextSingleLineCopy(t *testing.T) { open := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) QuotedText(c, "> One line", &open) }, 400, 200) if !tt.HasText("Show quoted text (1 line)") { diff --git a/read_receipt.go b/messaging/read_receipt.go similarity index 73% rename from read_receipt.go rename to messaging/read_receipt.go index 97885af..97f0ede 100644 --- a/read_receipt.go +++ b/messaging/read_receipt.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -6,8 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ReceiptState tracks delivery; its zero value means sent. @@ -22,7 +24,7 @@ const ( // ReadReceiptOptions configure a ReadReceipt. type ReadReceiptOptions struct { // Readers supplies avatars for the read state. - Readers []AvatarItem + Readers []display.AvatarItem // Max avatars show before "+N"; zero means 3. Max int } @@ -36,16 +38,16 @@ func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) *ui if limit == 0 { limit = 3 } - k := Tokens(c) - u := Density(c).Unit() - word := msg(c, []string{"read_receipt.sent", "read_receipt.delivered", "read_receipt.read"}[state]) + k := core.Tokens(c) + u := core.Density(c).Unit() + word := core.Msg(c, []string{"read_receipt.sent", "read_receipt.delivered", "read_receipt.read"}[state]) label := word if state == ReceiptRead && len(opts.Readers) > 0 { names := make([]string, len(opts.Readers)) for i, r := range opts.Readers { names[i] = r.Name } - label = fmt.Sprintf(msg(c, "read_receipt.readBy"), strings.Join(names, ", ")) + label = fmt.Sprintf(core.Msg(c, "read_receipt.readBy"), strings.Join(names, ", ")) } ink := k.TextMuted if state == ReceiptRead { @@ -54,7 +56,7 @@ func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) *ui row := ui.Row(c).Gap(u * 1.5).AlignItems(ui.Center).Shrink(0).Role(ui.RoleStatus).Label(label).Tooltip(label) row.Children(func() { ui.Box(c).Size(18, 12).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { receiptChecks(p, r, state != ReceiptSent, ink) }) - ui.Text(c, word).FontSize(FontSize(c, theme.CaptionSize)).TextColor(ink).SingleLine() + ui.Text(c, word).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(ink).SingleLine() if state != ReceiptRead || len(opts.Readers) == 0 { return } @@ -64,11 +66,11 @@ func ReadReceipt(c *ui.Context, state ReceiptState, opts ReadReceiptOptions) *ui if i > 0 { cell.Margin(0, 0, 0, -5) } - cell.Children(func() { Avatar(c, r.Name, AvatarOptions{Image: r.Image, Size: 16}) }) + cell.Children(func() { display.Avatar(c, r.Name, display.AvatarOptions{Image: r.Image, Size: 16}) }) } }) if rest := len(opts.Readers) - limit; rest > 0 { - ui.Textf(c, "+%d", rest).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Textf(c, "+%d", rest).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) return row diff --git a/read_receipt_test.go b/messaging/read_receipt_test.go similarity index 71% rename from read_receipt_test.go rename to messaging/read_receipt_test.go index 1039dc8..f814676 100644 --- a/read_receipt_test.go +++ b/messaging/read_receipt_test.go @@ -1,15 +1,18 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" ) func TestReadReceiptStates(t *testing.T) { - readers := []AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}, {Name: "Umiri"}, {Name: "Nyamu"}} + readers := []display.AvatarItem{{Name: "Uika"}, {Name: "Mutsumi"}, {Name: "Umiri"}, {Name: "Nyamu"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Gap(8).AlignItems(ui.Start).Children(func() { ReadReceipt(c, ReceiptSent, ReadReceiptOptions{}) ReadReceipt(c, ReceiptDelivered, ReadReceiptOptions{}) @@ -32,7 +35,7 @@ func TestReadReceiptSnapshot(t *testing.T) { ui.Column(c).Padding(20).Gap(10).AlignItems(ui.Start).Children(func() { ReadReceipt(c, ReceiptSent, ReadReceiptOptions{}) ReadReceipt(c, ReceiptDelivered, ReadReceiptOptions{}) - ReadReceipt(c, ReceiptRead, ReadReceiptOptions{Readers: []AvatarItem{{Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, {Name: "Umiri Yahata"}, {Name: "Nyamu"}}}) + ReadReceipt(c, ReceiptRead, ReadReceiptOptions{Readers: []display.AvatarItem{{Name: "Uika Misumi"}, {Name: "Mutsumi Wakaba"}, {Name: "Umiri Yahata"}, {Name: "Nyamu"}}}) }) }) } diff --git a/recipient_input.go b/messaging/recipient_input.go similarity index 78% rename from recipient_input.go rename to messaging/recipient_input.go index 07dc88e..e345405 100644 --- a/recipient_input.go +++ b/messaging/recipient_input.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -9,8 +9,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Contact is a person RecipientInput can suggest. @@ -45,10 +48,10 @@ type recipientState struct { type recipientKey struct{} // RecipientInput validates addresses and offers contact suggestions. -func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOptions) TagsInputParts { - k := Tokens(c) +func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOptions) input.TagsInputParts { + k := core.Tokens(c) if opts.Label == "" { - opts.Label = msg(c, "recipient_input.label") + opts.Label = core.Msg(c, "recipient_input.label") } wrap := ui.Column(c).Disabled(opts.Disabled) st := ui.Local(wrap, recipientKey{}, func() *recipientState { return &recipientState{hi: -1} }) @@ -63,16 +66,16 @@ func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOpti s.contacts, s.count, s.added = first, len(opts.Contacts), slices.Clone(*recipients) } typed := s.query - var parts TagsInputParts + var parts input.TagsInputParts wrap.Children(func() { - parts = TagsInput(c, recipients, TagsInputOptions{ + parts = input.TagsInput(c, recipients, input.TagsInputOptions{ Placeholder: opts.Placeholder, Label: opts.Label, Disabled: opts.Disabled, Query: &s.query, Validate: func(tag string) string { if s.hi >= 0 && tag == strings.TrimSpace(typed) { return "" // replaced by the highlighted contact below } if _, err := mail.ParseAddress(tag); err != nil { - return fmt.Sprintf(msg(c, "recipient_input.invalid"), tag) + return fmt.Sprintf(core.Msg(c, "recipient_input.invalid"), tag) } return "" }, @@ -106,22 +109,22 @@ func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOpti } if live && in.Submitted() && s.hi >= 0 && s.hi < len(s.matches) { pick(s.matches[s.hi]) - parts = TagsInputParts{Element: wrap, Input: in, changed: true} + parts = input.NewTagsInputParts(wrap, in, true) } if showing { b := wrap.Bounds() ui.Overlay(c, func() { ww, wh := c.Size() w := min(max(240, b.W), max(0, ww-8)) - h := min(float32(len(s.matches))*Density(c).ControlHeight()+16, max(0, wh-8)) - x, y := overlayPlace(c, b, w, h, 4) + h := min(float32(len(s.matches))*core.Density(c).ControlHeight()+16, max(0, wh-8)) + x, y := core.PlaceOverlay(c, b, w, h, 4) panel := ui.Column(c).Absolute().Left(x).Top(y).Width(w).MaxHeight(h).Padding(4).Role(ui.RoleList). - Label(msg(c, "recipient_input.suggestions")) - overlayStyle(c, panel, overlayEnter(c, panel)) + Label(core.Msg(c, "recipient_input.suggestions")) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Children(func() { ui.Scroll(c).Shrink(1).Padding(4).Children(func() { for i, ct := range s.matches { - row := ui.ButtonBase(c).Key(ct.Email).MinHeight(Density(c).ControlHeight()).Justify(ui.Start).Gap(8).Padding(0, 8). + row := ui.ButtonBase(c).Key(ct.Email).MinHeight(core.Density(c).ControlHeight()).Justify(ui.Start).Gap(8).Padding(0, 8). Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(recipientFormat(ct)).FocusRing(false) if i == s.hi || row.Hovered() { row.Background(k.Selection) @@ -130,13 +133,13 @@ func RecipientInput(c *ui.Context, recipients *[]string, opts RecipientInputOpti row.DrawOver(func(p *ui.Painter, r ui.Rect) { internal.FocusRing(p, r, theme.ControlRadius, k.Focus) }) } row.Children(func() { - Avatar(c, ct.Name, AvatarOptions{Size: 22}) + display.Avatar(c, ct.Name, display.AvatarOptions{Size: 22}) ui.Text(c, ct.Name).SingleLine() - ui.Text(c, ct.Email).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, ct.Email).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() }) if row.Clicked() && !row.IsDisabled() { pick(ct) - parts.changed = true + parts = input.NewTagsInputParts(parts.Element, parts.Input, true) } } }) diff --git a/recipient_input_test.go b/messaging/recipient_input_test.go similarity index 95% rename from recipient_input_test.go rename to messaging/recipient_input_test.go index b86b04b..2561adf 100644 --- a/recipient_input_test.go +++ b/messaging/recipient_input_test.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "net/mail" @@ -6,13 +6,15 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestRecipientInputRefreshesContacts(t *testing.T) { var recipients []string contacts := []Contact{{Name: "Alice", Email: "alice@example.com"}} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RecipientInput(c, &recipients, RecipientInputOptions{Contacts: contacts, Label: "To"}) }, 500, 300) if err := tt.Click("To"); err != nil { @@ -29,7 +31,7 @@ func TestRecipientInputRefreshesContacts(t *testing.T) { func TestRecipientInputPopupFitsWindow(t *testing.T) { var recipients []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Absolute().Left(190).Top(220).Width(110).Children(func() { RecipientInput(c, &recipients, RecipientInputOptions{Contacts: recipientContacts, Label: "To"}) }) @@ -49,7 +51,7 @@ func TestRecipientInputPopupFitsWindow(t *testing.T) { func TestRecipientInputDisabledPreservesAddresses(t *testing.T) { recipients := []string{"a@example.com"} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { RecipientInput(c, &recipients, RecipientInputOptions{Contacts: recipientContacts, Label: "To"}) }) @@ -122,7 +124,7 @@ func TestRecipientInputSnapshot(t *testing.T) { func TestRecipientInputQuotesDisplayNames(t *testing.T) { var recipients []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) RecipientInput(c, &recipients, RecipientInputOptions{Label: "To", Contacts: []Contact{{Name: "Doe, Jane", Email: "jane@example.com"}}}) }, 500, 300) if err := tt.Click("To"); err != nil { @@ -145,8 +147,8 @@ func TestRecipientInputTabShowsFocusRing(t *testing.T) { var recipients []string var focus ui.Color tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - focus = Tokens(c).Focus + core.Use(c, core.Settings{}) + focus = core.Tokens(c).Focus ui.Column(c).Padding(20).Children(func() { RecipientInput(c, &recipients, RecipientInputOptions{Contacts: recipientContacts, Label: "To"}) }) diff --git a/snooze_picker.go b/messaging/snooze_picker.go similarity index 76% rename from snooze_picker.go rename to messaging/snooze_picker.go index 49d933c..c23475d 100644 --- a/snooze_picker.go +++ b/messaging/snooze_picker.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "log/slog" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SnoozePickerOptions configure a SnoozePicker. @@ -59,7 +62,7 @@ type snoozeState struct { type snoozeKey struct{} // SnoozePicker offers shortcuts and custom reminder times. -func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) ChoiceResult { +func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) core.ChoiceResult { if opts.MorningHour < 0 || opts.MorningHour > 23 { panic("mujica: SnoozePicker MorningHour outside 0..23") } @@ -71,8 +74,8 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) Cho if morning == 0 { morning = 9 } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() now := opts.Now if now.IsZero() { @@ -80,14 +83,14 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) Cho } now = now.In(loc) old := *until - col := ui.Column(c).Width(300).Padding(u).FocusGroup(ui.Vertical).Role(ui.RoleList).Label(msg(c, "snooze_picker.label")). + col := ui.Column(c).Width(300).Padding(u).FocusGroup(ui.Vertical).Role(ui.RoleList).Label(core.Msg(c, "snooze_picker.label")). Disabled(opts.Disabled) st := ui.Local(col, snoozeKey{}, func() *snoozeState { return &snoozeState{} }) s := *st col.Children(func() { row := func(name, when string, on bool) bool { b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Justify(ui.Start).Gap(u*2).Padding(0, u*3).Radius(theme.ControlRadius). - Role(ui.RoleListItem).Label(name + ", " + when).Selected(on).Background(ui.Transparent).Transition(colorFade) + Role(ui.RoleListItem).Label(name + ", " + when).Selected(on).Background(ui.Transparent).Transition(core.ColorFade) internal.Ring(b, k.Focus, theme.ControlRadius) switch { case on: @@ -97,19 +100,19 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) Cho } b.Children(func() { ui.Text(c, name).Grow(1).SingleLine() - ui.Text(c, when).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() + ui.Text(c, when).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontFeatures("tnum").SingleLine() }) return b.Clicked() && !b.IsDisabled() } for _, ch := range SnoozeChoices(now, morning) { ui.Box(c).Key(ch.Key).Children(func() { - if row(msg(c, ch.Key), snoozeWhen(c, now, ch.At), until.Equal(ch.At)) { + if row(core.Msg(c, ch.Key), snoozeWhen(c, now, ch.At), until.Equal(ch.At)) { *until, s.custom = ch.At, false } }) } ui.Box(c).Height(1).Margin(u, 0).Background(k.Border) - if row(msg(c, "snooze_picker.custom"), "", s.custom) { + if row(core.Msg(c, "snooze_picker.custom"), "", s.custom) { s.custom = !s.custom if s.pick.IsZero() { s.pick = SnoozeChoices(now, morning)[0].At @@ -117,8 +120,8 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) Cho } if s.custom { ui.Column(c).Gap(u * 2).Padding(u * 2).Children(func() { - DateTimePicker(c, &s.pick, DateTimePickerOptions{Location: loc, Min: now, Label: msg(c, "snooze_picker.custom"), Disabled: opts.Disabled}) - if Button(c, msg(c, "snooze_picker.set"), ButtonOptions{Disabled: !s.pick.After(now)}).Clicked() { + datetime.DateTimePicker(c, &s.pick, datetime.DateTimePickerOptions{Location: loc, Min: now, Label: core.Msg(c, "snooze_picker.custom"), Disabled: opts.Disabled}) + if input.Button(c, core.Msg(c, "snooze_picker.set"), input.ButtonOptions{Disabled: !s.pick.After(now)}).Clicked() { *until = s.pick } }) @@ -128,12 +131,12 @@ func SnoozePicker(c *ui.Context, until *time.Time, opts SnoozePickerOptions) Cho if changed { slog.Debug("mujica: snooze_picker set", "until", *until) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func snoozeWhen(c *ui.Context, now, t time.Time) string { if t.Year() == now.Year() && t.YearDay() == now.YearDay() { return t.Format("15:04") } - return messagingDate(c, t, "snooze_picker.when") + return core.MessagingDate(c, t, "snooze_picker.when") } diff --git a/snooze_picker_test.go b/messaging/snooze_picker_test.go similarity index 96% rename from snooze_picker_test.go rename to messaging/snooze_picker_test.go index 22a8d0e..dc2725a 100644 --- a/snooze_picker_test.go +++ b/messaging/snooze_picker_test.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "slices" @@ -6,6 +6,8 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestSnoozeChoicesPreserveWallHour(t *testing.T) { @@ -46,7 +48,7 @@ func TestSnoozePickerChoose(t *testing.T) { var until time.Time n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if SnoozePicker(c, &until, SnoozePickerOptions{Location: time.UTC}).Changed() { n++ } @@ -87,7 +89,7 @@ func TestSnoozePickerCustomCommitAndDisabled(t *testing.T) { var until time.Time disabled, changes := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if SnoozePicker(c, &until, SnoozePickerOptions{Now: now, Location: time.UTC, Disabled: disabled}).Changed() { changes++ } diff --git a/status_setter.go b/messaging/status_setter.go similarity index 61% rename from status_setter.go rename to messaging/status_setter.go index 1fb7b16..068751c 100644 --- a/status_setter.go +++ b/messaging/status_setter.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "log/slog" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CustomStatus stores content and an optional expiry. @@ -40,15 +43,15 @@ type statusSetterState struct { type statusSetterKey struct{} // StatusSetter edits *st's emoji and text, and when it clears. -func StatusSetter(c *ui.Context, st *CustomStatus, opts StatusSetterOptions) ChoiceResult { +func StatusSetter(c *ui.Context, st *CustomStatus, opts StatusSetterOptions) core.ChoiceResult { loc := opts.Location if loc == nil { loc = time.Local } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() old := *st - col := ui.Column(c).Gap(u * 2).Role(ui.RoleGroup).Label(msg(c, "status_setter.label")).Disabled(opts.Disabled) + col := ui.Column(c).Gap(u * 2).Role(ui.RoleGroup).Label(core.Msg(c, "status_setter.label")).Disabled(opts.Disabled) ss := ui.Local(col, statusSetterKey{}, func() *statusSetterState { return &statusSetterState{} }) s := *ss live := !col.IsDisabled() @@ -69,43 +72,43 @@ func StatusSetter(c *ui.Context, st *CustomStatus, opts StatusSetterOptions) Cho if face == "" { face = "🙂" } - b := ui.ButtonBase(c).Size(Density(c).ControlHeight(), Density(c).ControlHeight()).Radius(theme.ControlRadius). - Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(msg(c, "status_setter.emoji")) + b := ui.ButtonBase(c).Size(core.Density(c).ControlHeight(), core.Density(c).ControlHeight()).Radius(theme.ControlRadius). + Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(core.Msg(c, "status_setter.emoji")) internal.Ring(b, k.Focus, theme.ControlRadius) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Children(func() { ui.Text(c, face).FontSize(FontSize(c, 18)) }) + b.Children(func() { ui.Text(c, face).FontSize(core.FontSize(c, 18)) }) if b.Clicked() && !b.IsDisabled() { s.picking = !s.picking } if b.IsDisabled() { s.picking = false } - Popover(c, b, &s.picking, PopoverOptions{Label: msg(c, "emoji_picker.label")}, func() { + overlay.Popover(c, b, &s.picking, overlay.PopoverOptions{Label: core.Msg(c, "emoji_picker.label")}, func() { if e := EmojiPicker(c, EmojiPickerOptions{}); e.Picked != "" { st.Emoji, s.picking = e.Picked, false } }) - TextInput(c, &st.Text, TextInputOptions{Placeholder: msg(c, "status_setter.placeholder"), Label: msg(c, "status_setter.text"), + input.TextInput(c, &st.Text, input.TextInputOptions{Placeholder: core.Msg(c, "status_setter.placeholder"), Label: core.Msg(c, "status_setter.text"), Clearable: true, Disabled: opts.Disabled}).Element.Grow(1) }) ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "status_setter.clearAfter")).TextColor(k.TextMuted).SingleLine().Shrink(0) - options := make([]SelectOption[int], len(statusClear)) + ui.Text(c, core.Msg(c, "status_setter.clearAfter")).TextColor(k.TextMuted).SingleLine().Shrink(0) + options := make([]input.SelectOption[int], len(statusClear)) for i, o := range statusClear { - options[i] = SelectOption[int]{Value: i, Label: msg(c, o.key)} + options[i] = input.SelectOption[int]{Value: i, Label: core.Msg(c, o.key)} } ui.Box(c).Key(live).Children(func() { - if Select(c, &s.clear, options, SelectOptions{Label: msg(c, "status_setter.clearAfter"), Placeholder: messagingDate(c, st.Until.In(loc), "status_setter.until"), Disabled: !live}).Changed() { + if input.Select(c, &s.clear, options, input.SelectOptions{Label: core.Msg(c, "status_setter.clearAfter"), Placeholder: core.MessagingDate(c, st.Until.In(loc), "status_setter.until"), Disabled: !live}).Changed() { st.Until = statusUntil(c.Now().In(loc), statusClear[s.clear].after) } }) if !st.Until.IsZero() { - ui.Text(c, messagingDate(c, st.Until.In(loc), "status_setter.until")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, core.MessagingDate(c, st.Until.In(loc), "status_setter.until")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } ui.Box(c).Grow(1) - if Button(c, msg(c, "status_setter.clear"), ButtonOptions{Variant: Ghost, Disabled: *st == CustomStatus{}}).Clicked() { + if input.Button(c, core.Msg(c, "status_setter.clear"), input.ButtonOptions{Variant: input.Ghost, Disabled: *st == CustomStatus{}}).Clicked() { *st, s.clear = CustomStatus{}, 0 } }) @@ -115,7 +118,7 @@ func StatusSetter(c *ui.Context, st *CustomStatus, opts StatusSetterOptions) Cho if changed { slog.Debug("mujica: status_setter changed", "emoji", st.Emoji, "until", st.Until) } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } func statusUntil(now time.Time, d time.Duration) time.Time { diff --git a/status_setter_test.go b/messaging/status_setter_test.go similarity index 95% rename from status_setter_test.go rename to messaging/status_setter_test.go index 9620f2b..285c183 100644 --- a/status_setter_test.go +++ b/messaging/status_setter_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestStatusUntil(t *testing.T) { @@ -26,7 +28,7 @@ func TestStatusSetterEditAndClear(t *testing.T) { st := CustomStatus{} n := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Width(420).Children(func() { if StatusSetter(c, &st, StatusSetterOptions{Location: time.UTC}).Changed() { n++ @@ -74,7 +76,7 @@ func TestStatusSetterLoadedExpiryAndDisabledPopup(t *testing.T) { st := CustomStatus{Until: time.Date(2026, 10, 6, 15, 0, 0, 0, time.UTC)} disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { StatusSetter(c, &st, StatusSetterOptions{Location: time.UTC}) }) }, 600, 400) if tt.HasText("Don't clear") { diff --git a/thread_panel.go b/messaging/thread_panel.go similarity index 68% rename from thread_panel.go rename to messaging/thread_panel.go index 2752af3..301d7c5 100644 --- a/thread_panel.go +++ b/messaging/thread_panel.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "fmt" @@ -7,7 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chat" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ThreadPanelOptions configure a ThreadPanel. @@ -25,7 +28,7 @@ type ThreadPanelResult struct { } type threadState struct { - list MessageListState + list chat.MessageListState draft string } @@ -43,10 +46,10 @@ func ThreadPanel(c *ui.Context, open *bool, root ChatEntry, replies []ChatEntry, } ww, _ := c.Size() w = min(w, ww) - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() panel := ui.Column(c).Width(w).FillHeight().Shrink(0).Background(k.Surface).BorderWidth(0, 0, 0, 1). - BorderColor(k.Border).Role(ui.RoleGroup).Label(msg(c, "thread_panel.title")).Disabled(opts.Disabled) + BorderColor(k.Border).Role(ui.RoleGroup).Label(core.Msg(c, "thread_panel.title")).Disabled(opts.Disabled) if panel.IsDisabled() { *open = false } @@ -54,34 +57,34 @@ func ThreadPanel(c *ui.Context, open *bool, root ChatEntry, replies []ChatEntry, panel.Invisible() return r } - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.BorderWidth(0, 0, 0, 1).BorderColor(k.Border) r.Element = panel st := ui.Local(panel, threadKey{}, func() *threadState { return &threadState{} }) s := *st panel.Children(func() { ui.Row(c).Padding(u*3, u*3, u*2, u*4).AlignItems(ui.Center).Children(func() { - ui.Text(c, msg(c, "thread_panel.title")).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) - if iconAction(c, msgClose, msg(c, "thread_panel.close")).Clicked() { + ui.Text(c, core.Msg(c, "thread_panel.title")).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + if core.IconAction(c, msgClose, core.Msg(c, "thread_panel.close")).Clicked() { *open = false } }) ui.Scroll(c).MaxHeight(160).Shrink(1).Padding(0, u*2).Children(func() { ChatMessage(c, root, ChatMessageOptions{}) }) ui.Row(c).Gap(u*2).Padding(u*2, u*4).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "thread_panel.replies"), len(replies))).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, fmt.Sprintf(core.Msg(c, "thread_panel.replies"), len(replies))).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).SingleLine() ui.Box(c).Height(1).Grow(1).Background(k.Border) }) - MessageList(c, &s.list, len(replies), MessageListOptions{ + chat.MessageList(c, &s.list, len(replies), chat.MessageListOptions{ ID: func(i int) string { return replies[i].ID }, Label: func(i int) string { return replies[i].Author + ": " + replies[i].Text }, }, func(i int) { ChatMessage(c, replies[i], ChatMessageOptions{Continued: i > 0 && ChatContinues(replies[i-1], replies[i])}) }) ui.Row(c).Gap(u*2).Padding(u*3).AlignItems(ui.Center).BorderWidth(1, 0, 0, 0).BorderColor(k.Border).Children(func() { - in := TextInput(c, &s.draft, TextInputOptions{Placeholder: msg(c, "thread_panel.reply"), Label: msg(c, "thread_panel.reply"), Disabled: opts.Disabled}) + in := input.TextInput(c, &s.draft, input.TextInputOptions{Placeholder: core.Msg(c, "thread_panel.reply"), Label: core.Msg(c, "thread_panel.reply"), Disabled: opts.Disabled}) in.Element.Grow(1) - send := IconButton(c, msgSend, msg(c, "thread_panel.send"), IconButtonOptions{Disabled: strings.TrimSpace(s.draft) == ""}) + send := input.IconButton(c, msgSend, core.Msg(c, "thread_panel.send"), input.IconButtonOptions{Disabled: strings.TrimSpace(s.draft) == ""}) if (in.Submitted() || send.Clicked()) && strings.TrimSpace(s.draft) != "" && !panel.IsDisabled() { r.Sent, s.draft = strings.TrimSpace(s.draft), "" s.list.ScrollToEnd() diff --git a/thread_panel_test.go b/messaging/thread_panel_test.go similarity index 94% rename from thread_panel_test.go rename to messaging/thread_panel_test.go index 9a2174e..ac07705 100644 --- a/thread_panel_test.go +++ b/messaging/thread_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package messaging import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestThreadPanelReplyAndClose(t *testing.T) { @@ -14,7 +16,7 @@ func TestThreadPanelReplyAndClose(t *testing.T) { open := true var sent []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Fill().Children(func() { ui.Box(c).Grow(1) if r := ThreadPanel(c, &open, root, replies, ThreadPanelOptions{}); r.Sent != "" { @@ -55,7 +57,7 @@ func TestThreadPanelFitsWindowAndDisabled(t *testing.T) { root := ChatEntry{ID: "root", Author: "Author", Text: "Question"} sent := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Fill().Children(func() { if ThreadPanel(c, &open, root, nil, ThreadPanelOptions{Disabled: disabled}).Sent != "" { sent++ diff --git a/unread_divider.go b/messaging/unread_divider.go similarity index 61% rename from unread_divider.go rename to messaging/unread_divider.go index 6bca759..b8cd9d6 100644 --- a/unread_divider.go +++ b/messaging/unread_divider.go @@ -1,11 +1,12 @@ -package mujica +package messaging import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // UnreadDivider marks where unread messages begin; count zero shows "New". @@ -13,17 +14,17 @@ func UnreadDivider(c *ui.Context, count int) *ui.Element { if count < 0 { panic("mujica: UnreadDivider count must not be negative") } - k := Tokens(c) - u := Density(c).Unit() - text := msg(c, "unread_divider.new") + k := core.Tokens(c) + u := core.Density(c).Unit() + text := core.Msg(c, "unread_divider.new") if count > 0 { - text = fmt.Sprintf(msg(c, "unread_divider.count"), count) + text = fmt.Sprintf(core.Msg(c, "unread_divider.count"), count) } row := ui.Row(c).FillWidth().Gap(u * 2).AlignItems(ui.Center).MinHeight(theme.MinHitSize).Role(ui.RoleStatus).Label(text) row.Children(func() { line := func() { ui.Box(c).Height(1).Grow(1).Background(k.Danger) } line() - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.Danger).SingleLine(). + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).TextColor(k.Danger).SingleLine(). Padding(0, u*2).Radius(theme.ControlRadius).Border(theme.BorderWidth, k.Danger) line() }) diff --git a/unread_divider_test.go b/messaging/unread_divider_test.go similarity index 86% rename from unread_divider_test.go rename to messaging/unread_divider_test.go index a18ff7f..5d1a88a 100644 --- a/unread_divider_test.go +++ b/messaging/unread_divider_test.go @@ -1,14 +1,16 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestUnreadDivider(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { UnreadDivider(c, 12) UnreadDivider(c, 0) diff --git a/user_profile_card.go b/messaging/user_profile_card.go similarity index 72% rename from user_profile_card.go rename to messaging/user_profile_card.go index f625c9e..61d40c3 100644 --- a/user_profile_card.go +++ b/messaging/user_profile_card.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // UserProfile is the person a UserProfileCard shows. @@ -40,24 +42,24 @@ func UserProfileCard(c *ui.Context, prof UserProfile, opts UserProfileCardOption if w == 0 { w = 300 } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() card := ui.Column(c).Width(w).Padding(u*4).Gap(u*3).Radius(theme.CardRadius).Background(k.Surface). - Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup).Label(msg(c, "user_profile_card.label") + ": " + prof.Name) + Border(theme.BorderWidth, k.Border).Role(ui.RoleGroup).Label(core.Msg(c, "user_profile_card.label") + ": " + prof.Name) card.Children(func() { ui.Row(c).Gap(u * 3).AlignItems(ui.Center).Children(func() { ui.Box(c).Shrink(0).Children(func() { - Avatar(c, prof.Name, AvatarOptions{Image: prof.Image, Size: 56}) + display.Avatar(c, prof.Name, display.AvatarOptions{Image: prof.Image, Size: 56}) ui.Box(c).Absolute().Right(-1).Bottom(-1).Padding(2).Radius(8).Background(k.Surface).Children(func() { OnlineStatus(c, prof.Presence, OnlineStatusOptions{HideText: true, Size: 12}) }) }) ui.Column(c).Gap(u).MinWidth(0).Grow(1).Children(func() { - ui.Text(c, prof.Name).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).SingleLine() + ui.Text(c, prof.Name).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).SingleLine() ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Wrap().Children(func() { OnlineStatus(c, prof.Presence, OnlineStatusOptions{}) if prof.Role != "" { - Badge(c, BadgeOptions{Text: prof.Role, Tone: BadgeNeutral}, nil) + display.Badge(c, display.BadgeOptions{Text: prof.Role, Tone: display.BadgeNeutral}, nil) } }) }) @@ -66,7 +68,7 @@ func UserProfileCard(c *ui.Context, prof UserProfile, opts UserProfileCardOption ui.Text(c, prof.Status).MaxLines(2) } if prof.About != "" { - ui.Text(c, prof.About).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).MaxLines(4) + ui.Text(c, prof.About).TextColor(k.TextMuted).FontSize(core.FontSize(c, theme.CaptionSize)).MaxLines(4) } if opts.Actions != nil { ui.Box(c).Height(1).Background(k.Border) diff --git a/user_profile_card_test.go b/messaging/user_profile_card_test.go similarity index 77% rename from user_profile_card_test.go rename to messaging/user_profile_card_test.go index 5741866..c48be95 100644 --- a/user_profile_card_test.go +++ b/messaging/user_profile_card_test.go @@ -1,18 +1,21 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func TestUserProfileCard(t *testing.T) { clicked := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) UserProfileCard(c, UserProfile{Name: "Sakiko Togawa", Presence: PresenceBusy, Role: "Keyboard", Status: "In rehearsal"}, UserProfileCardOptions{Actions: func() { - if Button(c, "Message", ButtonOptions{}).Clicked() { + if input.Button(c, "Message", input.ButtonOptions{}).Clicked() { clicked++ } }}) @@ -35,8 +38,8 @@ func TestUserProfileCardSnapshot(t *testing.T) { ui.Column(c).Padding(20).Children(func() { UserProfileCard(c, UserProfile{Name: "Sakiko Togawa", Presence: PresenceOnline, Role: "Keyboard", Status: "Composing the second act", About: "Founder of the masquerade."}, UserProfileCardOptions{Actions: func() { - Button(c, "Message", ButtonOptions{}) - Button(c, "Call", ButtonOptions{Variant: Secondary}) + input.Button(c, "Message", input.ButtonOptions{}) + input.Button(c, "Call", input.ButtonOptions{Variant: input.Secondary}) }}) }) }) diff --git a/video_call_grid.go b/messaging/video_call_grid.go similarity index 76% rename from video_call_grid.go rename to messaging/video_call_grid.go index 4acd2c9..225e57c 100644 --- a/video_call_grid.go +++ b/messaging/video_call_grid.go @@ -1,4 +1,4 @@ -package mujica +package messaging import ( "math" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/media" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // CallParticipant supplies identity, frames and call state. @@ -43,14 +46,14 @@ func VideoCallGrid(c *ui.Context, people []CallParticipant, opts VideoCallGridOp if cols == 0 { cols = max(1, int(math.Ceil(math.Sqrt(float64(len(people)))))) } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() tracks := make([]ui.Track, cols) for i := range tracks { tracks[i] = ui.Fr(1) } grid := ui.Box(c).Grid().ColumnTracks(tracks...).Gap(u * 2).FocusGroup(ui.Horizontal | ui.Vertical). - Role(ui.RoleGroup).Label(msg(c, "video_call_grid.label")) + Role(ui.RoleGroup).Label(core.Msg(c, "video_call_grid.label")) grid.Children(func() { for _, p := range people { callTile(c, k, p) @@ -60,22 +63,22 @@ func VideoCallGrid(c *ui.Context, people []CallParticipant, opts VideoCallGridOp } func callTile(c *ui.Context, k theme.Tokens, p CallParticipant) { - u := Density(c).Unit() + u := core.Density(c).Unit() var notes []string if p.Speaking { - notes = append(notes, msg(c, "video_call_grid.speaking")) + notes = append(notes, core.Msg(c, "video_call_grid.speaking")) } if p.Muted { - notes = append(notes, msg(c, "video_call_grid.muted")) + notes = append(notes, core.Msg(c, "video_call_grid.muted")) } if p.CameraOff { - notes = append(notes, msg(c, "video_call_grid.cameraOff")) + notes = append(notes, core.Msg(c, "video_call_grid.cameraOff")) } label := p.Name if len(notes) > 0 { label += ", " + strings.Join(notes, ", ") } - tile := ui.Box(c).Key(p.ID).AspectRatio(16.0 / 9).Radius(theme.CardRadius).Clip().Background(videoBlack).Center(). + tile := ui.Box(c).Key(p.ID).AspectRatio(16.0 / 9).Radius(theme.CardRadius).Clip().Background(media.VideoBlack).Center(). Focusable().Role(ui.RoleImage).Label(label) internal.Ring(tile, k.Focus, theme.CardRadius) light := ui.Hex("#F2EBDD") @@ -83,7 +86,7 @@ func callTile(c *ui.Context, k theme.Tokens, p CallParticipant) { if p.Frame != nil && !p.CameraOff { ui.Image(c, p.Frame).Fit(ui.Cover).Absolute().Left(0).Top(0).Right(0).Bottom(0) } else { - Avatar(c, p.Name, AvatarOptions{Image: p.Image, Size: 56}) + display.Avatar(c, p.Name, display.AvatarOptions{Image: p.Image, Size: 56}) } ui.Row(c).Absolute().Left(u*2).Bottom(u*2).Gap(u).AlignItems(ui.Center).Padding(2, u*1.5). Radius(theme.ControlRadius).Background(ui.RGBA(0, 0, 0, 0.6)).TextColor(light).Children(func() { @@ -93,7 +96,7 @@ func callTile(c *ui.Context, k theme.Tokens, p CallParticipant) { if p.Speaking { ui.Icon(c, msgSpeaking).Size(13, 13) } - ui.Text(c, p.Name).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, p.Name).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() }) }) if p.Speaking { diff --git a/video_call_grid_test.go b/messaging/video_call_grid_test.go similarity index 94% rename from video_call_grid_test.go rename to messaging/video_call_grid_test.go index 0bb961e..2ff4145 100644 --- a/video_call_grid_test.go +++ b/messaging/video_call_grid_test.go @@ -1,9 +1,11 @@ -package mujica +package messaging import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestVideoCallGridTilesAndSpeaker(t *testing.T) { @@ -11,7 +13,7 @@ func TestVideoCallGridTilesAndSpeaker(t *testing.T) { {ID: "3", Name: "Mutsumi", CameraOff: true}, {ID: "4", Name: "Umiri"}, {ID: "5", Name: "Nyamu"}} var rects []ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(600).Children(func() { VideoCallGrid(c, people, VideoCallGridOptions{}) }) }, 700, 600) for _, name := range []string{"Sakiko, speaking", "Uika, muted", "Mutsumi, camera off", "Umiri", "Nyamu"} { diff --git a/messaging_icons.go b/messaging_icons.go deleted file mode 100644 index e569df0..0000000 --- a/messaging_icons.go +++ /dev/null @@ -1,15 +0,0 @@ -package mujica - -var ( - msgBellOff = mediaSVG(``) - msgPin = mediaSVG(``) - msgPaperclip = mediaSVG(``) - msgMicOff = mediaSVG(``) - msgVideoOff = mediaSVG(``) - msgShare = mediaSVG(``) - msgPhone = mediaSVG(``) - msgHangUp = mediaSVG(``) - msgSpeaking = mediaSVG(``) - msgSend = mediaSVG(``) - msgClose = mediaSVG(``) -) diff --git a/breadcrumb.go b/navigation/breadcrumb.go similarity index 82% rename from breadcrumb.go rename to navigation/breadcrumb.go index ada3102..dd6baaf 100644 --- a/breadcrumb.go +++ b/navigation/breadcrumb.go @@ -1,13 +1,14 @@ -package mujica +package navigation import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BreadcrumbItem is one level of a Breadcrumb path. @@ -53,10 +54,10 @@ func Breadcrumb(c *ui.Context, items []BreadcrumbItem, opts BreadcrumbOptions) B } label := opts.Label if label == "" { - label = msg(c, "breadcrumb.label") + label = core.Msg(c, "breadcrumb.label") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() row := ui.Row(c).AlignItems(ui.Center).Gap(u).MinWidth(0).Role(ui.RoleGroup).Label(label) res := BreadcrumbResult{Element: row, chosen: -1} choose := func(i int) { @@ -83,7 +84,7 @@ func Breadcrumb(c *ui.Context, items []BreadcrumbItem, opts BreadcrumbOptions) B } if i == len(items)-1 { cur := ui.Row(c).AlignItems(ui.Center).Gap(u).Padding(u, u*1.5).MinWidth(0). - FontWeight(600).TextColor(k.Text).Tooltip(it.Label).Label(it.Label + ", " + msg(c, "breadcrumb.current")) + FontWeight(600).TextColor(k.Text).Tooltip(it.Label).Label(it.Label + ", " + core.Msg(c, "breadcrumb.current")) cur.Children(func() { breadcrumbContent(c, it, width) }) continue } @@ -95,7 +96,7 @@ func Breadcrumb(c *ui.Context, items []BreadcrumbItem, opts BreadcrumbOptions) B link.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(link, Motion(c, theme.StateDuration), fg) + internal.Ease(link, core.Motion(c, theme.StateDuration), fg) link.Children(func() { breadcrumbContent(c, it, width) }) if link.Clicked() { choose(i) @@ -113,15 +114,15 @@ func breadcrumbContent(c *ui.Context, it BreadcrumbItem, width float32) { } func breadcrumbSeparator(c *ui.Context) { - ui.Icon(c, icons.Must("chevron-right")).Size(12, 12).Shrink(0).TextColor(Tokens(c).Ornament).Role(ui.RoleNone) + ui.Icon(c, icons.Must("chevron-right")).Size(12, 12).Shrink(0).TextColor(core.Tokens(c).Ornament).Role(ui.RoleNone) } // breadcrumbMore is the ellipsis button listing the folded items. func breadcrumbMore(c *ui.Context, hidden []BreadcrumbItem, choose func(int)) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() b := ui.ButtonBase(c).Padding(u, u*1.5).Radius(theme.ControlRadius).MinWidth(theme.MinHitSize). - Label(msg(c, "breadcrumb.more")).Tooltip(msg(c, "breadcrumb.more")).Cursor(ui.CursorPointer) + Label(core.Msg(c, "breadcrumb.more")).Tooltip(core.Msg(c, "breadcrumb.more")).Cursor(ui.CursorPointer) internal.Ring(b, k.Focus, theme.ControlRadius) open := ui.Local(b, "open", func() bool { return false }) fg := k.TextMuted @@ -129,17 +130,17 @@ func breadcrumbMore(c *ui.Context, hidden []BreadcrumbItem, choose func(int)) { b.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(b, Motion(c, theme.StateDuration), fg) + internal.Ease(b, core.Motion(c, theme.StateDuration), fg) if b.Clicked() { *open = !*open } b.Children(func() { ui.Icon(c, icons.Must("ellipsis")).Size(16, 16) }) ui.PopoverBase(c, b, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Margin(u, 0, 0, 0).Padding(u).Gap(u * 0.5).MinWidth(160).MaxWidth(320) for j, it := range hidden { item := ui.ButtonBase(c).Role(ui.RoleLink).Justify(ui.Start).Gap(u*2).Padding(u*1.5, u*2). - Radius(theme.ControlRadius).MinHeight(Density(c).ControlHeight() - u*2).Label(it.Label).TextColor(k.Text) + Radius(theme.ControlRadius).MinHeight(core.Density(c).ControlHeight() - u*2).Label(it.Label).TextColor(k.Text) if j == 0 { item.AutoFocus() } @@ -147,7 +148,7 @@ func breadcrumbMore(c *ui.Context, hidden []BreadcrumbItem, choose func(int)) { if item.Hovered() { item.Background(k.SurfaceHover) } - internal.Ease(item, Motion(c, theme.StateDuration), k.Text) + internal.Ease(item, core.Motion(c, theme.StateDuration), k.Text) item.Children(func() { if it.Icon != nil { ui.Icon(c, it.Icon).Size(14, 14).Shrink(0) diff --git a/breadcrumb_test.go b/navigation/breadcrumb_test.go similarity index 93% rename from breadcrumb_test.go rename to navigation/breadcrumb_test.go index 326abd8..a5b1341 100644 --- a/breadcrumb_test.go +++ b/navigation/breadcrumb_test.go @@ -1,11 +1,12 @@ -package mujica +package navigation import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func breadcrumbPath(names ...string) []BreadcrumbItem { @@ -19,7 +20,7 @@ func breadcrumbPath(names ...string) []BreadcrumbItem { func TestBreadcrumbChoose(t *testing.T) { got := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if i, ok := Breadcrumb(c, breadcrumbPath("Home", "Library", "Poems"), BreadcrumbOptions{}).Chosen(); ok { got = i } @@ -45,7 +46,7 @@ func TestBreadcrumbChoose(t *testing.T) { func TestBreadcrumbFolds(t *testing.T) { got := -1 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if i, ok := Breadcrumb(c, breadcrumbPath("Root", "Alpha", "Beta", "Gamma", "Omega", "Leaf"), BreadcrumbOptions{}).Chosen(); ok { got = i } @@ -93,7 +94,7 @@ func TestBreadcrumbInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Breadcrumb(c, tc.items, tc.opts) }, 300, 100, 1) }) diff --git a/command_palette.go b/navigation/command_palette.go similarity index 86% rename from command_palette.go rename to navigation/command_palette.go index 49f552f..eed9b77 100644 --- a/command_palette.go +++ b/navigation/command_palette.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "log/slog" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Command is an action of a CommandPalette. @@ -75,21 +76,21 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP } st.wasOpen = *open res := CommandPaletteResult{} - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() res.Element = ui.DialogBase(c, open, func(back, panel *ui.Element) { ww, wh := c.Size() - p := overlayEnter(c, panel) + p := core.OverlayEnter(c, panel) back.Background(ui.RGBA(0, 0, 0, 0.4*p)).Justify(ui.Start) - overlayStyle(c, panel, p) + core.OverlayStyle(c, panel, p) panel.Margin(wh*0.12, 0, 0, 0).Width(560).MaxWidth(ww - 32).MaxHeight(wh * 0.76). - AlignItems(ui.Stretch).Label(msg(c, "command_palette.label")) + AlignItems(ui.Stretch).Label(core.Msg(c, "command_palette.label")) found := commandPaletteFilter(commands, st.query) var in *ui.Element ui.Row(c).AlignItems(ui.Center).Gap(u*3).Padding(u*3, u*4).Children(func() { ui.Icon(c, icons.Must("search")).Size(16, 16).TextColor(k.TextMuted) - in = ui.TextInputBase(c, &st.query).FillWidth().AutoFocus().Label(msg(c, "command_palette.search")) + in = ui.TextInputBase(c, &st.query).FillWidth().AutoFocus().Label(core.Msg(c, "command_palette.search")) }) ui.Box(c).Height(1).Background(k.Ornament).Role(ui.RoleNone) if in.Changed() { @@ -111,7 +112,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP c.Announce(found[st.hl].Label) } } else { - in.Description(msg(c, "command_palette.empty")) + in.Description(core.Msg(c, "command_palette.empty")) } run := -1 if in.Submitted() && n > 0 { @@ -120,7 +121,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP list := ui.Scroll(c).Padding(u * 2).Gap(2).MaxHeight(360).Role(ui.RoleList) list.Children(func() { if n == 0 { - ui.Text(c, msg(c, "command_palette.empty")).TextColor(k.TextMuted).Padding(u*6, u*2).TextAlign(ui.Center).FillWidth() + ui.Text(c, core.Msg(c, "command_palette.empty")).TextColor(k.TextMuted).Padding(u*6, u*2).TextAlign(ui.Center).FillWidth() return } gutter := slices.ContainsFunc(found, func(cmd Command) bool { return cmd.Icon != nil }) @@ -129,7 +130,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP if cmd.Group != group { group = cmd.Group if group != "" { - ui.Text(c, group).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, group).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.Ornament.Mix(k.Text, 0.35)).Padding(u*2, u*2, u, u*2).SingleLine() } } @@ -140,7 +141,7 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP }) ui.Row(c).Gap(u*4).Padding(u*2, u*4).Background(k.Background).Children(func() { for _, hint := range []string{"command_palette.hint_move", "command_palette.hint_run", "command_palette.hint_close"} { - ui.Text(c, msg(c, hint)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, core.Msg(c, hint)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } }) if run >= 0 { @@ -157,8 +158,8 @@ func CommandPalette(c *ui.Context, open *bool, commands []Command, opts CommandP // commandPaletteRow builds row i and reports a click on it. func commandPaletteRow(c *ui.Context, cmd Command, i int, st *commandPaletteState, gutter bool) bool { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() row := ui.Row(c).AlignItems(ui.Center).Gap(u*3).MinHeight(d.ControlHeight()).Padding(0, u*3). Radius(theme.ControlRadius).Role(ui.RoleListItem).Label(cmd.Label).TextColor(k.Text).Cursor(ui.CursorPointer) @@ -173,7 +174,7 @@ func commandPaletteRow(c *ui.Context, cmd Command, i int, st *commandPaletteStat st.reveal = false } } - internal.Ease(row, Motion(c, theme.StateDuration), k.Text) + internal.Ease(row, core.Motion(c, theme.StateDuration), k.Text) px, py, over := row.PointerPosition() b := row.Bounds() if at := [2]float32{b.X + px, b.Y + py}; over && at != st.ptr { diff --git a/command_palette_test.go b/navigation/command_palette_test.go similarity index 95% rename from command_palette_test.go rename to navigation/command_palette_test.go index a7278ff..c157f53 100644 --- a/command_palette_test.go +++ b/navigation/command_palette_test.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "fmt" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func commandPaletteSample() []Command { @@ -25,7 +26,7 @@ type paletteApp struct { } func (a *paletteApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().Children(func() { ui.Text(c, "Editor") if id, ok := CommandPalette(c, &a.open, commandPaletteSample(), CommandPaletteOptions{}).Chosen(); ok { @@ -108,7 +109,7 @@ func TestCommandPaletteFilterRevealsHighlight(t *testing.T) { } open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommandPalette(c, &open, cmds, CommandPaletteOptions{}) }, 800, 600) tt.Key(0, ui.KeyUp) // wraps to the last, scrolling to the bottom @@ -161,7 +162,7 @@ func TestCommandPaletteInvalid(t *testing.T) { }() open := true ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) CommandPalette(c, &open, cmds, CommandPaletteOptions{}) }, 300, 100, 1) }) diff --git a/navigation/doc.go b/navigation/doc.go new file mode 100644 index 0000000..8e9cb06 --- /dev/null +++ b/navigation/doc.go @@ -0,0 +1,2 @@ +// Package navigation holds MujicaUI tabs, menus, breadcrumbs, pagination and other navigation. +package navigation diff --git a/navigation/example_navigation_test.go b/navigation/example_navigation_test.go new file mode 100644 index 0000000..fd1d6e0 --- /dev/null +++ b/navigation/example_navigation_test.go @@ -0,0 +1,156 @@ +package navigation_test + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/navigation" +) + +func ExampleTabs() { + tab := 0 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if navigation.Tabs(c, &tab, []navigation.Tab{ + {Label: "General", Panel: func() { ui.Text(c, "General settings") }}, + {Label: "Fonts", Icon: icons.Must("italic"), Panel: func() { ui.Text(c, "Fonts") }}, + {Label: "Archive", Disabled: true}, + }, navigation.TabsOptions{Label: "Settings"}).Changed() { + fmt.Println("tab", tab) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSegmentedControl() { + view := 0 + build := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if navigation.SegmentedControl(c, &view, []navigation.Segment{ + {Label: "List", Icon: icons.Must("list"), HideLabel: true}, + {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true}, + }, navigation.SegmentedControlOptions{Label: "View"}).Changed() { + fmt.Println("view", view) + } + } + ui.Render(build, 400, 300, 1) +} + +func ExampleBreadcrumb() { + path := []navigation.BreadcrumbItem{{Label: "Home", Icon: icons.Must("house")}, {Label: "Library"}, {Label: "Poems"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if i, ok := navigation.Breadcrumb(c, path, navigation.BreadcrumbOptions{}).Chosen(); ok { + path = path[:i+1] + } + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePagination() { + page, size := 1, 20 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if navigation.Pagination(c, &page, &size, navigation.PaginationOptions{Total: 420, PageSizes: []int{10, 20, 50}, Jump: true}).Changed() { + fmt.Println("load page", page, "of size", size) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSteps() { + current := 1 + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + navigation.Steps(c, ¤t, []navigation.Step{ + {Title: "Account", Clickable: true}, + {Title: "Profile", Description: "Name and seal"}, + {Title: "Confirm"}, + }, navigation.StepsOptions{Label: "Sign-up"}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSidebar() { + selected := "inbox" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + navigation.Sidebar(c, &selected, []navigation.SidebarSection{ + {Title: "Mail", Items: []navigation.SidebarItem{ + {ID: "inbox", Label: "Inbox", Icon: icons.Must("inbox"), Count: 3}, + {Label: "Folders", Items: []navigation.SidebarItem{{ID: "work", Label: "Work", Icon: icons.Must("folder")}}}, + }}, + }, navigation.SidebarOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleNavigationMenu() { + router := ui.NewRouter("/") + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + navigation.NavigationMenu(c, router, []navigation.NavigationLink{ + {Label: "Home", Path: "/"}, + {Label: "Library", Items: []navigation.NavigationLink{ + {Label: "Poems", Path: "/library/poems", Description: "Verse of the court"}, + }}, + }, navigation.NavigationMenuOptions{History: true}) + router.View(c, func(r *ui.Route) { ui.Text(c, r.Path()) }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMenubar() { + wrap := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + res := navigation.Menubar(c, []navigation.MenubarMenu{ + {Label: "File", Items: []navigation.MenubarItem{ + {ID: "new", Label: "New", Shortcut: navigation.MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {Label: "Export", Items: []navigation.MenubarItem{{ID: "pdf", Label: "PDF"}}}, + }}, + {Label: "View", Items: []navigation.MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: navigation.MenubarCheck, Checked: wrap}, + }}, + }, navigation.MenubarOptions{}) + if id, ok := res.Chosen(); ok && id == "wrap" { + wrap = !wrap + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleToolbar() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + navigation.Toolbar(c, navigation.ToolbarOptions{Label: "Format"}, func() { + navigation.ToolbarGroup(c, func() { + navigation.ToolbarButton(c, "Bold", navigation.ToolbarButtonOptions{Icon: icons.Must("bold")}) + navigation.ToolbarButton(c, "Italic", navigation.ToolbarButtonOptions{Icon: icons.Must("italic")}) + }) + navigation.ToolbarSeparator(c) + if navigation.ToolbarButton(c, "Copy", navigation.ToolbarButtonOptions{Icon: icons.Must("copy"), ShowLabel: true}).Clicked() { + fmt.Println("copy") + } + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommandPalette() { + open := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + res := navigation.CommandPalette(c, &open, []navigation.Command{ + {ID: "new", Label: "New document", Group: "File"}, + {ID: "theme", Label: "Toggle dark mode", Group: "View", Keywords: []string{"night"}}, + }, navigation.CommandPaletteOptions{}) + if id, ok := res.Chosen(); ok { + fmt.Println("run", id) + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/navigation/helpers_test.go b/navigation/helpers_test.go new file mode 100644 index 0000000..7cc81a1 --- /dev/null +++ b/navigation/helpers_test.go @@ -0,0 +1,59 @@ +package navigation + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +type menubarApp struct { + wrap bool + zoom string + chosen []string +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} + +func (a *menubarApp) menus() []MenubarMenu { + return []MenubarMenu{ + {Label: "File", Items: []MenubarItem{ + {ID: "new", Label: "New", Shortcut: MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyN}}, + {ID: "open", Label: "Open…", Shortcut: MenubarShortcut{Mods: ui.Cmd, Key: ui.KeyO}}, + {Label: "Export", Items: []MenubarItem{ + {ID: "pdf", Label: "PDF"}, + {ID: "png", Label: "Image", Disabled: true}, + {ID: "svg", Label: "Vector"}, + }}, + {Kind: MenubarSeparator}, + {ID: "quit", Label: "Quit"}, + }}, + {Label: "View", Items: []MenubarItem{ + {ID: "wrap", Label: "Word wrap", Kind: MenubarCheck, Checked: a.wrap}, + {Kind: MenubarSeparator}, + {ID: "zoom-fit", Label: "Fit", Kind: MenubarRadio, Checked: a.zoom == "zoom-fit"}, + {ID: "zoom-100", Label: "Actual size", Kind: MenubarRadio, Checked: a.zoom == "zoom-100"}, + }}, + {Label: "Help", Items: []MenubarItem{{ID: "about", Label: "About"}}}, + } +} diff --git a/menubar.go b/navigation/menubar.go similarity index 95% rename from menubar.go rename to navigation/menubar.go index 00ce82d..b6b250b 100644 --- a/menubar.go +++ b/navigation/menubar.go @@ -1,12 +1,13 @@ -package mujica +package navigation import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // MenubarItemKind is what a menu item does. @@ -83,12 +84,12 @@ func Menubar(c *ui.Context, menus []MenubarMenu, opts MenubarOptions) MenubarRes } menubarCheck(m.Items) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() label := opts.Label if label == "" { - label = msg(c, "menubar.label") + label = core.Msg(c, "menubar.label") } bar := ui.Row(c).AlignItems(ui.Center).Gap(u * 0.5).Padding(u).FocusGroup(ui.Horizontal). Role(ui.RoleToolbar).Label(label).Background(k.Surface) @@ -168,8 +169,8 @@ func (m *menubarRun) current() string { func (m *menubarRun) title(i int, label string) *ui.Element { c, st := m.c, m.st - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) open := st.open == i t := ui.ButtonBase(c).MinHeight(d.ControlHeight()-4).Padding(0, d.Unit()*3).Radius(theme.ControlRadius). Role(ui.RoleMenuButton).Label(label).TextColor(k.Text) @@ -180,7 +181,7 @@ func (m *menubarRun) title(i int, label string) *ui.Element { case t.Hovered(): t.Background(k.SurfaceHover) } - internal.Ease(t, Motion(c, theme.StateDuration), k.Text) + internal.Ease(t, core.Motion(c, theme.StateDuration), k.Text) if st.focusTo == i { t.Focus() st.focusTo = -1 diff --git a/menubar_panel.go b/navigation/menubar_panel.go similarity index 87% rename from menubar_panel.go rename to navigation/menubar_panel.go index d75a83a..bc0fdb3 100644 --- a/menubar_panel.go +++ b/navigation/menubar_panel.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "runtime" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // overlay draws the open menu and its submenus above the window. @@ -54,7 +55,7 @@ func (m *menubarRun) overlay(titles []ui.Rect) { } a := anchor.Bounds() prev = panel.Bounds() - x, y = prev.X+prev.W-2, a.Y-Density(c).Unit() + x, y = prev.X+prev.W-2, a.Y-core.Density(c).Unit() } }) } @@ -62,13 +63,13 @@ func (m *menubarRun) overlay(titles []ui.Rect) { // panel builds level l at (x, y) and returns it with its highlighted row. func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *ui.Element) { c, st := m.c, m.st - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() items := m.items(l) ww, wh := c.Size() // Long menus scroll within the window instead of running off it. panel := ui.Scroll(c).Absolute().Padding(u).MinWidth(200).MaxHeight(wh - 8).AlignItems(ui.Stretch).Role(ui.RolePopup) - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) pb := panel.Bounds() if x+pb.W > ww-4 { x = ww - 4 - pb.W @@ -127,8 +128,8 @@ func (m *menubarRun) panel(l int, x, y float32, parent ui.Rect) (*ui.Element, *u func (m *menubarRun) item(it MenubarItem, hl bool) *ui.Element { c := m.c - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() name := menubarName(c, it) row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()-4).Padding(0, u*2). @@ -136,7 +137,7 @@ func (m *menubarRun) item(it MenubarItem, hl bool) *ui.Element { if hl { row.Background(k.Selection) } - internal.Ease(row, Motion(c, theme.StateDuration), k.Text) + internal.Ease(row, core.Motion(c, theme.StateDuration), k.Text) row.Children(func() { mark := ui.Box(c).Size(16, 16).Shrink(0).Center().Role(ui.RoleNone) if it.Checked && it.Kind == MenubarCheck { @@ -150,7 +151,7 @@ func (m *menubarRun) item(it MenubarItem, hl bool) *ui.Element { ui.Text(c, it.Label).SingleLine() ui.Box(c).Grow(1).MinWidth(u * 6) if it.Shortcut.Key != 0 { - ui.Text(c, menubarShortcutText(it.Shortcut)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, MenubarShortcutText(it.Shortcut)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine() } if it.Items != nil { ui.Icon(c, icons.Must("chevron-right")).Size(14, 14).TextColor(k.TextMuted) @@ -162,7 +163,7 @@ func (m *menubarRun) item(it MenubarItem, hl bool) *ui.Element { // menubarName is an item's label, with its checked state for assistive technology. func menubarName(c *ui.Context, it MenubarItem) string { if (it.Kind == MenubarCheck || it.Kind == MenubarRadio) && it.Checked { - return it.Label + ", " + msg(c, "menubar.checked") + return it.Label + ", " + core.Msg(c, "menubar.checked") } return it.Label } @@ -177,8 +178,8 @@ var menubarKeyNames = map[ui.Key]string{ ui.KeyBracketLeft: "[", ui.KeyBracketRight: "]", ui.KeyBackslash: "\\", ui.KeyBackquote: "`", } -// menubarShortcutText spells a shortcut the platform's way: ⌘S on macOS, Ctrl+S elsewhere. -func menubarShortcutText(s MenubarShortcut) string { +// MenubarShortcutText spells a shortcut the platform's way: ⌘S on macOS, Ctrl+S elsewhere. +func MenubarShortcutText(s MenubarShortcut) string { key, ok := menubarKeyNames[s.Key] switch { case ok: diff --git a/navigation_menu.go b/navigation/navigation_menu.go similarity index 83% rename from navigation_menu.go rename to navigation/navigation_menu.go index 25ad752..f41a2ae 100644 --- a/navigation_menu.go +++ b/navigation/navigation_menu.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "log/slog" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // NavigationLink is a page of a NavigationMenu, or with Items a group of pages. @@ -37,12 +38,12 @@ func NavigationMenu(c *ui.Context, router *ui.Router, links []NavigationLink, op if len(links) == 0 { panic("mujica: NavigationMenu needs links") } - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() path := router.Path() label := opts.Label if label == "" { - label = msg(c, "navigation_menu.label") + label = core.Msg(c, "navigation_menu.label") } push := func(p string) { if p != path { @@ -54,8 +55,8 @@ func NavigationMenu(c *ui.Context, router *ui.Router, links []NavigationLink, op row.Children(func() { if opts.History { ui.Row(c).Gap(u).Children(func() { - navigationMenuHistory(c, ui.BackButton(c, router), msg(c, "navigation_menu.back")) - navigationMenuHistory(c, ui.ForwardButton(c, router), msg(c, "navigation_menu.forward")) + navigationMenuHistory(c, ui.BackButton(c, router), core.Msg(c, "navigation_menu.back")) + navigationMenuHistory(c, ui.ForwardButton(c, router), core.Msg(c, "navigation_menu.forward")) }) ui.Box(c).Size(1, 20).Background(k.Ornament).Role(ui.RoleNone) } @@ -69,8 +70,8 @@ func NavigationMenu(c *ui.Context, router *ui.Router, links []NavigationLink, op } func navigationMenuHistory(c *ui.Context, b *ui.Element, name string) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b.Label(name).MinHeight(d.ControlHeight()).MinWidth(d.ControlHeight()).Radius(theme.ControlRadius).Background(k.Surface) internal.Ring(b, k.Focus, theme.ControlRadius) } @@ -96,8 +97,8 @@ func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(s if l.Items == nil && l.Path == "" { panic("mujica: NavigationLink needs a Path or Items") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() on := navigationMenuActive(l, path) b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(0, u*3).Gap(u * 1.5).Radius(theme.ControlRadius). @@ -117,7 +118,7 @@ func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(s b.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(b, Motion(c, theme.StateDuration), fg) + internal.Ease(b, core.Motion(c, theme.StateDuration), fg) b.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) { if on { p.Fill(ui.Rect{X: r.X + u*3, Y: r.Y + r.H - 2, W: r.W - u*6, H: 2}, k.AccentText, 1) @@ -147,7 +148,7 @@ func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(s *open = true } ui.PopoverBase(c, b, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) // Capped to the room beside the anchor; the links scroll inside. _, wh := c.Size() r := b.Bounds() @@ -160,8 +161,8 @@ func navigationMenuTop(c *ui.Context, l NavigationLink, path string, push func(s // navigationMenuPanel lists a group's links, its subgroups under serif headings. func navigationMenuPanel(c *ui.Context, items []NavigationLink, path string, open *bool, push func(string)) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() first := true var list func(items []NavigationLink) list = func(items []NavigationLink) { @@ -170,7 +171,7 @@ func navigationMenuPanel(c *ui.Context, items []NavigationLink, path string, ope panic("mujica: NavigationLink needs a Label") } if it.Items != nil { - ui.Text(c, it.Label).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)). + ui.Text(c, it.Label).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)). Padding(u*2, u*2, u, u*2).SingleLine() list(it.Items) continue @@ -189,8 +190,8 @@ func navigationMenuPanel(c *ui.Context, items []NavigationLink, path string, ope } func navigationMenuItem(c *ui.Context, l NavigationLink, on, first bool) *ui.Element { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() e := ui.ButtonBase(c).Role(ui.RoleLink).Justify(ui.Start).AlignItems(ui.Start).Gap(u*3). Padding(u*2, u*3).Radius(theme.ControlRadius).Label(l.Label).TextColor(k.Text).Cursor(ui.CursorPointer) if first { @@ -206,7 +207,7 @@ func navigationMenuItem(c *ui.Context, l NavigationLink, on, first bool) *ui.Ele case e.Hovered(): e.Background(k.SurfaceHover) } - internal.Ease(e, Motion(c, theme.StateDuration), k.Text) + internal.Ease(e, core.Motion(c, theme.StateDuration), k.Text) e.Children(func() { if l.Icon != nil { ui.Icon(c, l.Icon).Size(16, 16).Margin(2, 0, 0, 0) @@ -214,7 +215,7 @@ func navigationMenuItem(c *ui.Context, l NavigationLink, on, first bool) *ui.Ele ui.Column(c).Gap(2).MinWidth(0).Children(func() { ui.Text(c, l.Label).SingleLine() if l.Description != "" { - ui.Text(c, l.Description).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontWeight(400) + ui.Text(c, l.Description).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).FontWeight(400) } }) }) diff --git a/navigation_menu_test.go b/navigation/navigation_menu_test.go similarity index 94% rename from navigation_menu_test.go rename to navigation/navigation_menu_test.go index 137e42f..8325546 100644 --- a/navigation_menu_test.go +++ b/navigation/navigation_menu_test.go @@ -1,11 +1,12 @@ -package mujica +package navigation import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func navigationMenuSample() []NavigationLink { @@ -25,7 +26,7 @@ func navigationMenuSample() []NavigationLink { func TestNavigationMenuPush(t *testing.T) { r := ui.NewRouter("/") tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NavigationMenu(c, r, navigationMenuSample(), NavigationMenuOptions{History: true}) }, 700, 400) if err := tt.Click("About"); err != nil { @@ -63,7 +64,7 @@ func TestNavigationMenuPush(t *testing.T) { func TestNavigationMenuKeyboard(t *testing.T) { r := ui.NewRouter("/") tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NavigationMenu(c, r, navigationMenuSample(), NavigationMenuOptions{}) }, 700, 400) tt.Key(0, ui.KeyTab) @@ -121,7 +122,7 @@ func TestNavigationMenuInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NavigationMenu(c, ui.NewRouter("/"), links, NavigationMenuOptions{}) }, 300, 100, 1) }) diff --git a/navigation_review_test.go b/navigation/navigation_review_test.go similarity index 93% rename from navigation_review_test.go rename to navigation/navigation_review_test.go index 7a987b9..e1601fb 100644 --- a/navigation_review_test.go +++ b/navigation/navigation_review_test.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "fmt" @@ -7,9 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" - - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TestNavigationPopupsFadeIn checks each popup fades in over PopupDuration instead of appearing at once. @@ -38,7 +38,7 @@ func TestNavigationPopupsFadeIn(t *testing.T) { } { opened := time.Now() tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Fill().Padding(16).AlignItems(ui.Start).Children(func() { tc.view(c) }) }, 700, 500) if tc.open != "" { @@ -69,7 +69,7 @@ func TestNavigationMenuLongPanelScrolls(t *testing.T) { links = append(links, NavigationLink{Label: fmt.Sprintf("Link %02d", i), Path: fmt.Sprintf("/l/%d", i)}) } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) NavigationMenu(c, ui.NewRouter("/"), []NavigationLink{{Label: "Group", Items: links}}, NavigationMenuOptions{}) }, 400, 300) tt.Key(0, ui.KeyTab) @@ -109,7 +109,7 @@ func TestNavigationCaptionsFollowTextScale(t *testing.T) { } { height := func(scale float32) float32 { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Fill().AlignItems(ui.Start).Children(func() { tc.view(c) }) }, 900, 700) tt.SetPreferences(ui.Preferences{TextScale: scale}) diff --git a/pagination.go b/navigation/pagination.go similarity index 71% rename from pagination.go rename to navigation/pagination.go index 8bf3f07..f953087 100644 --- a/pagination.go +++ b/navigation/pagination.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "fmt" @@ -9,9 +9,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PaginationOptions configure a Pagination. @@ -28,7 +29,7 @@ type PaginationOptions struct { } // Pagination pages *page (from 1) through Total records, clamping it when Total shrinks. -func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) ChoiceResult { +func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) core.ChoiceResult { if opts.Total < 0 { panic("mujica: Pagination Total is negative") } @@ -45,26 +46,26 @@ func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) Choi if sib < 0 { panic("mujica: Pagination Siblings is negative") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() - row := ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Wrap().Role(ui.RoleGroup).Label(msg(c, "pagination.label")).Disabled(opts.Disabled) - res := ChoiceResult{Element: row} + row := ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Wrap().Role(ui.RoleGroup).Label(core.Msg(c, "pagination.label")).Disabled(opts.Disabled) + res := core.ChoiceResult{Element: row} pages := paginationPages(opts.Total, *pageSize) if p := max(1, min(*page, pages)); p != *page { slog.Debug("mujica: pagination correct", "from", *page, "to", p, "pages", pages) - *page, res.changed = p, true + *page, res = p, core.NewChoiceResult(res.Element, true) } set := func(p int) { if p != *page { slog.Debug("mujica: pagination page", "from", *page, "to", p) - *page, res.changed = p, true + *page, res = p, core.NewChoiceResult(res.Element, true) } } row.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "pagination.total"), opts.Total)).TextColor(k.TextMuted).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "pagination.total"), opts.Total)).TextColor(k.TextMuted).SingleLine() ui.Row(c).AlignItems(ui.Center).Gap(u).FocusGroup(ui.Horizontal).Children(func() { - if paginationButton(c, "", icons.Must("chevron-left"), msg(c, "pagination.prev"), false, *page <= 1).Clicked() { + if paginationButton(c, "", icons.Must("chevron-left"), core.Msg(c, "pagination.prev"), false, *page <= 1).Clicked() { set(*page - 1) } for _, p := range paginationItems(*page, pages, sib) { @@ -73,15 +74,15 @@ func Pagination(c *ui.Context, page, pageSize *int, opts PaginationOptions) Choi continue } n := strconv.Itoa(p) - name := fmt.Sprintf(msg(c, "pagination.page"), p) + name := fmt.Sprintf(core.Msg(c, "pagination.page"), p) if p == *page { - name += ", " + msg(c, "pagination.current") + name += ", " + core.Msg(c, "pagination.current") } if paginationButton(c, n, nil, name, p == *page, false).Clicked() { set(p) } } - if paginationButton(c, "", icons.Must("chevron-right"), msg(c, "pagination.next"), false, *page >= pages).Clicked() { + if paginationButton(c, "", icons.Must("chevron-right"), core.Msg(c, "pagination.next"), false, *page >= pages).Clicked() { set(*page + 1) } }) @@ -126,9 +127,9 @@ func paginationItems(cur, pages, sib int) []int { } func paginationButton(c *ui.Context, text string, icon *ui.SVG, name string, on, disabled bool) *ui.Element { - k := Tokens(c) - h := Density(c).ControlHeight() - b := ui.ButtonBase(c).MinHeight(h).MinWidth(h).Padding(0, Density(c).Unit()*2).Radius(theme.ControlRadius). + k := core.Tokens(c) + h := core.Density(c).ControlHeight() + b := ui.ButtonBase(c).MinHeight(h).MinWidth(h).Padding(0, core.Density(c).Unit()*2).Radius(theme.ControlRadius). Label(name).Disabled(disabled) internal.Ring(b, k.Focus, theme.ControlRadius) fg := k.Text @@ -143,7 +144,7 @@ func paginationButton(c *ui.Context, text string, icon *ui.SVG, name string, on, default: b.Border(theme.BorderWidth, k.Border) } - internal.Ease(b, Motion(c, theme.StateDuration), fg) + internal.Ease(b, core.Motion(c, theme.StateDuration), fg) if icon != nil { b.Tooltip(name) } @@ -157,34 +158,34 @@ func paginationButton(c *ui.Context, text string, icon *ui.SVG, name string, on, return b } -func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *ChoiceResult) { - k := Tokens(c) - d := Density(c) +func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *core.ChoiceResult) { + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() old := *pageSize sel := ui.SelectBase(c, pageSize) sel.Trigger.MinHeight(d.ControlHeight()).Padding(0, u*3).Gap(u*2).Radius(theme.ControlRadius). - Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(msg(c, "pagination.size")) + Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Label(core.Msg(c, "pagination.size")) internal.Ring(sel.Trigger, k.Focus, theme.ControlRadius) if sel.Trigger.Hovered() { sel.Trigger.Background(k.SurfaceHover) } - internal.Ease(sel.Trigger, Motion(c, theme.StateDuration), k.Text) + internal.Ease(sel.Trigger, core.Motion(c, theme.StateDuration), k.Text) sel.Trigger.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "pagination.per_page"), *pageSize)).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "pagination.per_page"), *pageSize)).SingleLine() ui.Icon(c, icons.Must("chevron-down")).Size(14, 14).TextColor(k.TextMuted) }) sel.Popup(func(panel *ui.Element) { - overlayStyle(c, panel, overlayEnter(c, panel)) + core.OverlayStyle(c, panel, core.OverlayEnter(c, panel)) panel.Margin(u, 0, 0, 0).Padding(u) for _, s := range sizes { it := sel.Item(s).Padding(u*1.5, u*3).Radius(theme.ControlRadius).Gap(u * 2) if it.Highlighted() { it.Background(k.Selection) } - internal.Ease(it, Motion(c, theme.StateDuration), k.Text) + internal.Ease(it, core.Motion(c, theme.StateDuration), k.Text) it.Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "pagination.per_page"), s)).SingleLine().Grow(1) + ui.Text(c, fmt.Sprintf(core.Msg(c, "pagination.per_page"), s)).SingleLine().Grow(1) if s == *pageSize { ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText) } @@ -195,24 +196,24 @@ func paginationSizes(c *ui.Context, page, pageSize *int, sizes []int, res *Choic // Keep the first record of the old page in view. first := (*page-1)*old + 1 *page = (first-1) / *pageSize + 1 - res.changed = true + *res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: pagination size", "from", old, "to", *pageSize, "page", *page) } } func paginationJump(c *ui.Context, pages int, set func(int)) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() { - ui.Text(c, msg(c, "pagination.jump")).TextColor(k.TextMuted).SingleLine() + ui.Text(c, core.Msg(c, "pagination.jump")).TextColor(k.TextMuted).SingleLine() box := ui.Row(c).AlignItems(ui.Center).MinHeight(d.ControlHeight()).Width(64).Padding(0, u*2). Radius(theme.ControlRadius).Background(k.Surface) text := ui.Local(box, "text", func() string { return "" }) bad := ui.Local(box, "bad", func() bool { return false }) var in *ui.Element box.Children(func() { - in = ui.TextInputBase(c, text).FillWidth().Label(msg(c, "pagination.field")) + in = ui.TextInputBase(c, text).FillWidth().Label(core.Msg(c, "pagination.field")) }) if in.Changed() { *bad = false @@ -230,13 +231,13 @@ func paginationJump(c *ui.Context, pages int, set func(int)) { border := k.ControlBorder if *bad { border = k.Danger - in.Error(fmt.Sprintf(msg(c, "pagination.range"), pages)) + in.Error(fmt.Sprintf(core.Msg(c, "pagination.range"), pages)) } else if in.Focused() { border = k.Focus } - internal.Ease(box.Border(theme.BorderWidth, border), Motion(c, theme.StateDuration), k.Text) + internal.Ease(box.Border(theme.BorderWidth, border), core.Motion(c, theme.StateDuration), k.Text) if *bad { - ui.Text(c, fmt.Sprintf(msg(c, "pagination.range"), pages)).TextColor(k.Danger).SingleLine() + ui.Text(c, fmt.Sprintf(core.Msg(c, "pagination.range"), pages)).TextColor(k.Danger).SingleLine() } }) } diff --git a/pagination_test.go b/navigation/pagination_test.go similarity index 93% rename from pagination_test.go rename to navigation/pagination_test.go index 0e7d427..d9e6f3a 100644 --- a/pagination_test.go +++ b/navigation/pagination_test.go @@ -1,10 +1,12 @@ -package mujica +package navigation import ( "slices" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestPaginationItems(t *testing.T) { @@ -27,7 +29,7 @@ func TestPaginationItems(t *testing.T) { func TestPaginationNavigate(t *testing.T) { page, size, changes := 1, 10, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Pagination(c, &page, &size, PaginationOptions{Total: 95}).Changed() { changes++ } @@ -60,7 +62,7 @@ func TestPaginationNavigate(t *testing.T) { func TestPaginationCorrectsOnTotalChange(t *testing.T) { page, size, total, changes := 9, 10, 95, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Pagination(c, &page, &size, PaginationOptions{Total: total}).Changed() { changes++ } @@ -80,7 +82,7 @@ func TestPaginationCorrectsOnTotalChange(t *testing.T) { func TestPaginationPageSizeKeepsRecord(t *testing.T) { page, size := 3, 10 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Pagination(c, &page, &size, PaginationOptions{Total: 200, PageSizes: []int{10, 20, 50}}) }, 800, 300) if err := tt.Click("10 / page"); err != nil { @@ -98,7 +100,7 @@ func TestPaginationPageSizeKeepsRecord(t *testing.T) { func TestPaginationJump(t *testing.T) { page, size := 1, 10 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Locale: ZhCN}) + core.Use(c, core.Settings{Locale: core.ZhCN}) Pagination(c, &page, &size, PaginationOptions{Total: 100, Jump: true}) }, 900, 120) if err := tt.Click("页码"); err != nil { @@ -121,7 +123,7 @@ func TestPaginationJump(t *testing.T) { func TestPaginationDisabled(t *testing.T) { page, size := 2, 10 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Pagination(c, &page, &size, PaginationOptions{Total: 50, Disabled: true}) }, 700, 120) _ = tt.Click("Next page") @@ -148,7 +150,7 @@ func TestPaginationInvalid(t *testing.T) { }() page := 1 ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Pagination(c, &page, &tc.size, tc.opts) }, 300, 100, 1) }) diff --git a/segmented_control.go b/navigation/segmented_control.go similarity index 90% rename from segmented_control.go rename to navigation/segmented_control.go index f3b0b50..d30c42a 100644 --- a/segmented_control.go +++ b/navigation/segmented_control.go @@ -1,12 +1,13 @@ -package mujica +package navigation import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Segment is one choice of a SegmentedControl. @@ -34,8 +35,8 @@ func SegmentedControl(c *ui.Context, selected *int, segments []Segment, opts Seg if *selected < 0 || *selected >= len(segments) { panic("mujica: SegmentedControl selected index out of range") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() inset := float32(2) prev := *selected @@ -75,7 +76,7 @@ func SegmentedControl(c *ui.Context, selected *int, segments []Segment, opts Seg seg.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(seg, Motion(c, theme.StateDuration), fg) + internal.Ease(seg, core.Motion(c, theme.StateDuration), fg) internal.Ring(seg, k.Focus, theme.ControlRadius) if s.HideLabel { seg.Tooltip(s.Label) diff --git a/segmented_control_test.go b/navigation/segmented_control_test.go similarity index 93% rename from segmented_control_test.go rename to navigation/segmented_control_test.go index 56ea8c2..80f66a8 100644 --- a/segmented_control_test.go +++ b/navigation/segmented_control_test.go @@ -1,17 +1,18 @@ -package mujica +package navigation import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func TestSegmentedControlClickAndKeys(t *testing.T) { sel, changes := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if SegmentedControl(c, &sel, []Segment{{Label: "Day"}, {Label: "Week"}, {Label: "Month"}}, SegmentedControlOptions{Label: "Range"}).Changed() { changes++ } @@ -39,7 +40,7 @@ func TestSegmentedControlClickAndKeys(t *testing.T) { func TestSegmentedControlIconsAndDisabled(t *testing.T) { sel := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SegmentedControl(c, &sel, []Segment{ {Label: "List", Icon: icons.Must("list"), HideLabel: true}, {Label: "Grid", Icon: icons.Must("layout-grid"), HideLabel: true}, @@ -59,7 +60,7 @@ func TestSegmentedControlIconsAndDisabled(t *testing.T) { func TestSegmentedControlEqualWidths(t *testing.T) { sel := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).AlignItems(ui.Start).Children(func() { SegmentedControl(c, &sel, []Segment{{Label: "A"}, {Label: "Much longer"}}, SegmentedControlOptions{Equal: true}) }) @@ -87,7 +88,7 @@ func TestSegmentedControlInvalid(t *testing.T) { }() sel := 0 ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) SegmentedControl(c, &sel, segs, SegmentedControlOptions{}) }, 300, 100, 1) }) diff --git a/sidebar.go b/navigation/sidebar.go similarity index 84% rename from sidebar.go rename to navigation/sidebar.go index 5e5a5c5..898b875 100644 --- a/sidebar.go +++ b/navigation/sidebar.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "fmt" @@ -6,9 +6,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SidebarItem is a destination of a Sidebar, or with Items a nested group. @@ -38,8 +39,8 @@ type SidebarOptions struct { } // Sidebar is a source list choosing an item ID into *selected, built on MyGo's Sidebar. -func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) ChoiceResult { - k := Tokens(c) +func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts SidebarOptions) core.ChoiceResult { + k := core.Tokens(c) width := opts.Width if width == 0 { width = 240 @@ -49,7 +50,7 @@ func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts Si } wrap := ui.Column(c).Width(width).FillHeight().AlignItems(ui.Stretch).Shrink(0) closed := ui.Local(wrap, "closed", func() map[string]*bool { return map[string]*bool{} }) - res := ChoiceResult{Element: wrap} + res := core.ChoiceResult{Element: wrap} prev := *selected wrap.Children(func() { // MyGo draws icons in Accent: swap in readable state colors for the sidebar. @@ -80,7 +81,7 @@ func Sidebar(c *ui.Context, selected *string, sections []SidebarSection, opts Si }) c.SetTheme(saved) if sb.Changed() { - res.changed = true + res = core.NewChoiceResult(res.Element, true) slog.Debug("mujica: sidebar choose", "from", prev, "to", *selected) } }) @@ -99,7 +100,7 @@ func sidebarOpen(closed map[string]*bool, key string) *bool { } func sidebarItems(c *ui.Context, selected string, items []SidebarItem, closed map[string]*bool, parent string, depth int) { - u := Density(c).Unit() + u := core.Density(c).Unit() for _, it := range items { if it.Label == "" { panic("mujica: SidebarItem needs a Label") @@ -120,8 +121,8 @@ func sidebarItems(c *ui.Context, selected string, items []SidebarItem, closed ma // sidebarGroup is a section title or nested group that Enter, Space or a click opens and closes. func sidebarGroup(c *ui.Context, title string, open *bool, section bool, depth int, fn func()) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() ui.Column(c).Gap(u*0.5).Shrink(0).Padding(0, 0, 0, float32(depth)*u*4).Children(func() { was := *open @@ -131,7 +132,7 @@ func sidebarGroup(c *ui.Context, title string, open *bool, section bool, depth i } tr.AlignItems(ui.Center).Gap(u*2).Padding(u, u*2).Radius(theme.ControlRadius).Label(title).Cursor(ui.CursorPointer) if section { - tr.Margin(u*2, 0, 0, 0).FontWeight(600).FontSize(FontSize(c, theme.CaptionSize)) + tr.Margin(u*2, 0, 0, 0).FontWeight(600).FontSize(core.FontSize(c, theme.CaptionSize)) } else { tr.MinHeight(d.ControlHeight()) } @@ -143,8 +144,8 @@ func sidebarGroup(c *ui.Context, title string, open *bool, section bool, depth i if section { fg = k.TextMuted } - internal.Ease(tr, Motion(c, theme.StateDuration), fg) - turn := tr.Animate("mujica.turn", sidebarTurn(*open), Motion(c, theme.StateDuration)) + internal.Ease(tr, core.Motion(c, theme.StateDuration), fg) + turn := tr.Animate("mujica.turn", sidebarTurn(*open), core.Motion(c, theme.StateDuration)) tr.Children(func() { ui.Icon(c, icons.Must("chevron-right")).Size(12, 12).Shrink(0).Rotate(turn).TextColor(k.TextMuted) ui.Text(c, title).SingleLine().Grow(1) @@ -180,8 +181,8 @@ func sidebarItem(c *ui.Context, selected string, it SidebarItem, compact bool) { if it.ID == "" { panic("mujica: SidebarItem needs an ID") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) on := it.ID == selected label := it.Label if compact { @@ -196,11 +197,11 @@ func sidebarItem(c *ui.Context, selected string, it SidebarItem, compact bool) { e.Tooltip(name).Label(name) } else if it.Count > 0 { e.Label(name).Children(func() { - ui.Text(c, sidebarCount(it.Count)).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). + ui.Text(c, sidebarCount(it.Count)).FontSize(core.FontSize(c, theme.CaptionSize)).FontFeatures("tnum"). TextColor(k.TextMuted).Padding(0, d.Unit()*1.5).Radius(theme.ControlRadius).Background(k.Background) }) } - internal.Ease(e, Motion(c, theme.StateDuration), k.Text) + internal.Ease(e, core.Motion(c, theme.StateDuration), k.Text) if on { e.Background(k.Selection).FontWeight(600) e.Draw(func(p *ui.Painter, r ui.Rect) { @@ -217,5 +218,5 @@ func sidebarCount(n int) string { } func sidebarRule(c *ui.Context) { - ui.Box(c).Height(1).Margin(Density(c).Unit()*2, 4).Background(Tokens(c).Ornament).Role(ui.RoleNone) + ui.Box(c).Height(1).Margin(core.Density(c).Unit()*2, 4).Background(core.Tokens(c).Ornament).Role(ui.RoleNone) } diff --git a/sidebar_test.go b/navigation/sidebar_test.go similarity index 93% rename from sidebar_test.go rename to navigation/sidebar_test.go index bd55bf4..2e7387d 100644 --- a/sidebar_test.go +++ b/navigation/sidebar_test.go @@ -1,11 +1,12 @@ -package mujica +package navigation import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) func sidebarSample() []SidebarSection { @@ -27,7 +28,7 @@ func sidebarSample() []SidebarSection { func TestSidebarChooseAndKeys(t *testing.T) { sel, changes := "inbox", 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Sidebar(c, &sel, sidebarSample(), SidebarOptions{}).Changed() { changes++ } @@ -55,7 +56,7 @@ func TestSidebarChooseAndKeys(t *testing.T) { func TestSidebarCollapse(t *testing.T) { sel := "inbox" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Sidebar(c, &sel, sidebarSample(), SidebarOptions{}) }, 400, 500) if err := tt.Click("Projects"); err != nil { @@ -80,7 +81,7 @@ func TestSidebarCompact(t *testing.T) { sel := "inbox" var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).FillHeight().Children(func() { w = Sidebar(c, &sel, sidebarSample(), SidebarOptions{Compact: true}).Element.Bounds().W }) @@ -117,7 +118,7 @@ func TestSidebarInvalid(t *testing.T) { }() sel := "" ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Sidebar(c, &sel, tc.s, tc.opts) }, 300, 300, 1) }) @@ -127,7 +128,7 @@ func TestSidebarInvalid(t *testing.T) { func TestSidebarGroupKeyboard(t *testing.T) { sel := "inbox" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Sidebar(c, &sel, sidebarSample(), SidebarOptions{}) }, 400, 500) // Tab stops: the item list, the Library title, the Projects group. @@ -151,7 +152,7 @@ func TestSidebarSameNamedGroups(t *testing.T) { {Title: "Chapel", Items: []SidebarItem{{Label: "Drafts", Items: []SidebarItem{{ID: "b1", Label: "Hymn"}}}}}, } tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Sidebar(c, &sel, sections, SidebarOptions{}) }, 400, 500) if err := tt.Click("Drafts"); err != nil { diff --git a/steps.go b/navigation/steps.go similarity index 81% rename from steps.go rename to navigation/steps.go index 7ffbddb..d613c76 100644 --- a/steps.go +++ b/navigation/steps.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "fmt" @@ -7,9 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Step is one stage of Steps. @@ -39,14 +40,14 @@ const ( ) // Steps shows a process whose step *current is under way: earlier steps are done. -func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceResult { +func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) core.ChoiceResult { if len(steps) == 0 { panic("mujica: Steps needs at least one step") } if *current < 0 || *current > len(steps) { panic("mujica: Steps current out of range") } - u := Density(c).Unit() + u := core.Density(c).Unit() root := ui.Row(c).AlignItems(ui.Start).Gap(u * 2).FocusGroup(ui.Horizontal | ui.Vertical).Role(ui.RoleList) if opts.Vertical { root.Column().AlignItems(ui.Stretch).Gap(0) @@ -54,7 +55,7 @@ func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceR if opts.Label != "" { root.Label(opts.Label) } - res := ChoiceResult{Element: root} + res := core.ChoiceResult{Element: root} root.Children(func() { for i, s := range steps { if s.Title == "" { @@ -74,7 +75,7 @@ func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceR } if stepsItem(c, i, len(steps), s, st, opts.Vertical, i < len(steps)-1 && opts.Vertical, i < *current) && i != *current { slog.Debug("mujica: steps choose", "from", *current, "to", i) - *current, res.changed = i, true + *current, res = i, core.NewChoiceResult(res.Element, true) } } }) @@ -83,8 +84,8 @@ func Steps(c *ui.Context, current *int, steps []Step, opts StepsOptions) ChoiceR // stepsItem builds one step and reports a click on it. func stepsItem(c *ui.Context, i, n int, s Step, st stepsState, vertical, tail, doneLine bool) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() var e *ui.Element if s.Clickable { e = ui.ButtonBase(c).Cursor(ui.CursorPointer) @@ -92,12 +93,12 @@ func stepsItem(c *ui.Context, i, n int, s Step, st stepsState, vertical, tail, d if e.Hovered() { e.Background(k.SurfaceHover) } - internal.Ease(e, Motion(c, theme.StateDuration), k.Text) + internal.Ease(e, core.Motion(c, theme.StateDuration), k.Text) } else { e = ui.Row(c).Shrink(0).Role(ui.RoleListItem) } e.AlignItems(ui.Stretch).Justify(ui.Start).Gap(u * 3).Padding(u).Radius(theme.ControlRadius). - Label(fmt.Sprintf(msg(c, "steps.item"), i+1, n, s.Title, msg(c, stepsStateKey[st]))) + Label(fmt.Sprintf(core.Msg(c, "steps.item"), i+1, n, s.Title, core.Msg(c, stepsStateKey[st]))) e.Children(func() { ui.Column(c).AlignItems(ui.Center).Gap(u).Shrink(0).Children(func() { stepsMarker(c, i, st) @@ -122,7 +123,7 @@ func stepsItem(c *ui.Context, i, n int, s Step, st stepsState, vertical, tail, d title.TextColor(k.TextMuted) } if s.Description != "" { - ui.Text(c, s.Description).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).MaxWidth(240) + ui.Text(c, s.Description).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).MaxWidth(240) } }) }) @@ -134,7 +135,7 @@ var stepsStateKey = map[stepsState]string{ } func stepsMarker(c *ui.Context, i int, st stepsState) { - k := Tokens(c) + k := core.Tokens(c) m := ui.Box(c).Size(24, 24).Radius(12).Center().Shrink(0).Role(ui.RoleNone) var icon string switch st { @@ -153,15 +154,15 @@ func stepsMarker(c *ui.Context, i int, st stepsState) { if icon != "" { ui.Icon(c, icons.Must(icon)).Size(14, 14) } else { - ui.Text(c, strconv.Itoa(i+1)).FontSize(FontSize(c, theme.CaptionSize)).SingleLine() + ui.Text(c, strconv.Itoa(i+1)).FontSize(core.FontSize(c, theme.CaptionSize)).SingleLine() } }) } // stepsLine joins step i to the next in the horizontal layout. func stepsLine(c *ui.Context, steps []Step, i, current int) { - k := Tokens(c) - ui.Box(c).Height(2).Grow(1).MinWidth(24).Margin(Density(c).Unit()+11, 0, 0, 0).Radius(1). + k := core.Tokens(c) + ui.Box(c).Height(2).Grow(1).MinWidth(24).Margin(core.Density(c).Unit()+11, 0, 0, 0).Radius(1). Background(stepsLineColor(k, i < current && !steps[i].Error)).Role(ui.RoleNone) } diff --git a/steps_test.go b/navigation/steps_test.go similarity index 93% rename from steps_test.go rename to navigation/steps_test.go index 201e346..edbe66a 100644 --- a/steps_test.go +++ b/navigation/steps_test.go @@ -1,9 +1,11 @@ -package mujica +package navigation import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func stepsSample() []Step { @@ -18,7 +20,7 @@ func stepsSample() []Step { func TestStepsClickable(t *testing.T) { cur, changes := 2, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if Steps(c, &cur, stepsSample(), StepsOptions{}).Changed() { changes++ } @@ -45,7 +47,7 @@ func TestStepsStatesNamed(t *testing.T) { steps := stepsSample() steps[2].Error = true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Steps(c, &cur, steps, StepsOptions{Vertical: true}) }, 400, 400) for _, name := range []string{ @@ -82,7 +84,7 @@ func TestStepsInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Steps(c, &tc.cur, tc.steps, StepsOptions{}) }, 300, 100, 1) }) diff --git a/tabs.go b/navigation/tabs.go similarity index 88% rename from tabs.go rename to navigation/tabs.go index 9dc5441..d11c65d 100644 --- a/tabs.go +++ b/navigation/tabs.go @@ -1,12 +1,13 @@ -package mujica +package navigation import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Tab is one tab of Tabs. @@ -28,7 +29,7 @@ type TabsOptions struct { } // Tabs shows tabs choosing *selected and the chosen tab's panel. Disabled tabs are skipped by the arrows. -func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResult { +func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) core.ChoiceResult { if len(tabs) == 0 { panic("mujica: Tabs needs at least one tab") } @@ -38,8 +39,8 @@ func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResu if tabs[*selected].Disabled { panic("mujica: Tabs selected tab is disabled") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() pad := theme.FocusGap + theme.FocusWidth // TabsBase only sees enabled tabs, so the arrows skip disabled ones. @@ -62,7 +63,7 @@ func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResu if opts.Vertical { root.Row() } - res := ChoiceResult{Element: root} + res := core.ChoiceResult{Element: root} root.Children(func() { var strip *ui.Element if opts.Vertical { @@ -97,14 +98,14 @@ func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResu tab = base.Tab(n) n++ } - tabsStyle(c, tab, t, i == enabled[at], opts.Vertical) + TabsStyle(c, tab, t, i == enabled[at], opts.Vertical) built[i] = tab } }) if !list.IsDisabled() && list.Changed() { prev := *selected *selected = enabled[at] - res.changed = true + res = core.NewChoiceResult(res.Element, true) built[*selected].ScrollIntoView() slog.Debug("mujica: tabs change", "from", prev, "to", *selected) } @@ -120,9 +121,9 @@ func Tabs(c *ui.Context, selected *int, tabs []Tab, opts TabsOptions) ChoiceResu return res } -func tabsStyle(c *ui.Context, tab *ui.Element, t Tab, on, vertical bool) { - k := Tokens(c) - d := Density(c) +func TabsStyle(c *ui.Context, tab *ui.Element, t Tab, on, vertical bool) { + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() tab.MinHeight(d.ControlHeight()).Padding(0, u*4).Gap(u * 2).AlignItems(ui.Center).Cursor(ui.CursorPointer) if vertical { @@ -148,7 +149,7 @@ func tabsStyle(c *ui.Context, tab *ui.Element, t Tab, on, vertical bool) { tab.Background(k.SurfaceHover) fg = k.Text } - internal.Ease(tab, Motion(c, theme.StateDuration), fg) + internal.Ease(tab, core.Motion(c, theme.StateDuration), fg) tab.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) { if on { if vertical { diff --git a/tabs_test.go b/navigation/tabs_test.go similarity index 91% rename from tabs_test.go rename to navigation/tabs_test.go index ff9b17f..18a2376 100644 --- a/tabs_test.go +++ b/navigation/tabs_test.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "image/color" @@ -8,8 +8,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func tabsSample() []Tab { @@ -24,7 +25,7 @@ func tabsSample() []Tab { func TestTabsClickAndPanel(t *testing.T) { sel, changes := 0, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ts := tabsSample() ts[0].Panel = func() { ui.Text(c, "General page") } ts[2].Panel = func() { ui.Text(c, "Colors page") } @@ -51,7 +52,7 @@ func TestTabsKeyboardSkipsDisabled(t *testing.T) { for _, vertical := range []bool{false, true} { sel := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Tabs(c, &sel, tabsSample(), TabsOptions{Vertical: vertical}) }, 500, 300) tt.Key(0, ui.KeyTab) @@ -85,7 +86,7 @@ func TestTabsOverflowScrolls(t *testing.T) { } var w float32 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Width(240).Children(func() { w = Tabs(c, &sel, tabs, TabsOptions{}).Element.Bounds().W }) @@ -119,7 +120,7 @@ func TestTabsInvalid(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Tabs(c, &tc.sel, tc.tabs, TabsOptions{}) }, 300, 100, 1) }) @@ -149,7 +150,7 @@ func TestTabsSnapshot(t *testing.T) { snapshot(t, "tabs", 520, 300, tabsView) for _, d := range []theme.Density{theme.Compact, theme.Comfortable} { snapshot(t, "tabs-"+strings.ToLower(d.String()), 520, 340, func(c *ui.Context) { - Use(c, Settings{Mode: tabsMode(c), Density: d}) + core.Use(c, core.Settings{Mode: tabsMode(c), Density: d}) tabsView(c) }) } @@ -175,7 +176,7 @@ func TestTabsStateSnapshots(t *testing.T) { func TestTabsStateColorsEase(t *testing.T) { sel := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) Tabs(c, &sel, []Tab{{Label: "One"}, {Label: "Two"}}, TabsOptions{}) }, 300, 120) r, _ := tt.Find("Two") @@ -195,11 +196,11 @@ func TestTabsStateColorsEase(t *testing.T) { func tabsRGBA(c ui.Color) color.RGBA { return color.RGBA{c.R, c.G, c.B, c.A} } // tabsMode recovers the mode snapshot applied, to add a density. -func tabsMode(c *ui.Context) Mode { - if Tokens(c).Dark { - return Dark +func tabsMode(c *ui.Context) core.Mode { + if core.Tokens(c).Dark { + return core.Dark } - return Light + return core.Light } func TestTabsAncestorDisabledNavigation(t *testing.T) { @@ -207,7 +208,7 @@ func TestTabsAncestorDisabledNavigation(t *testing.T) { selected, changes := 0, 0 disabled := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { if Tabs(c, &selected, tabsSample(), TabsOptions{}).Changed() { changes++ diff --git a/toolbar.go b/navigation/toolbar.go similarity index 75% rename from toolbar.go rename to navigation/toolbar.go index fff99f8..afb7508 100644 --- a/toolbar.go +++ b/navigation/toolbar.go @@ -1,10 +1,11 @@ -package mujica +package navigation import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // ToolbarOptions configure a Toolbar. @@ -15,9 +16,9 @@ type ToolbarOptions struct { // Toolbar is one Tab stop of actions; those that do not fit go to an overflow menu. func Toolbar(c *ui.Context, opts ToolbarOptions, fn func()) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) tb := ui.Toolbar(c, fn).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius). - MinHeight(Density(c).ControlHeight() + 8) + MinHeight(core.Density(c).ControlHeight() + 8) if opts.Label != "" { tb.Label(opts.Label) } @@ -37,8 +38,8 @@ func ToolbarButton(c *ui.Context, label string, opts ToolbarButtonOptions) *ui.E if label == "" { panic("mujica: ToolbarButton needs a label") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) u := d.Unit() text := opts.ShowLabel || opts.Icon == nil b := ui.ButtonBase(c).MinHeight(d.ControlHeight()).MinWidth(d.ControlHeight()).Gap(u * 2). @@ -56,7 +57,7 @@ func ToolbarButton(c *ui.Context, label string, opts ToolbarButtonOptions) *ui.E case b.Hovered(): b.Background(k.SurfaceHover) } - internal.Ease(b, Motion(c, theme.StateDuration), k.Text) + internal.Ease(b, core.Motion(c, theme.StateDuration), k.Text) b.Children(func() { if opts.Icon != nil { ui.Icon(c, opts.Icon).Size(16, 16) @@ -70,11 +71,11 @@ func ToolbarButton(c *ui.Context, label string, opts ToolbarButtonOptions) *ui.E // ToolbarGroup keeps related actions together; the overflow menu sets groups apart. func ToolbarGroup(c *ui.Context, fn func()) *ui.Element { - return ui.Row(c).AlignItems(ui.Center).Gap(Density(c).Unit() * 0.5).Shrink(0).Role(ui.RoleGroup).Children(fn) + return ui.Row(c).AlignItems(ui.Center).Gap(core.Density(c).Unit() * 0.5).Shrink(0).Role(ui.RoleGroup).Children(fn) } // ToolbarSeparator draws a thin rule between actions. func ToolbarSeparator(c *ui.Context) { - ui.Box(c).Size(1, Density(c).ControlHeight()-12).Margin(0, Density(c).Unit()).Shrink(0). - Background(Tokens(c).Ornament).Role(ui.RoleNone) + ui.Box(c).Size(1, core.Density(c).ControlHeight()-12).Margin(0, core.Density(c).Unit()).Shrink(0). + Background(core.Tokens(c).Ornament).Role(ui.RoleNone) } diff --git a/toolbar_test.go b/navigation/toolbar_test.go similarity index 91% rename from toolbar_test.go rename to navigation/toolbar_test.go index 125e65f..55e97ed 100644 --- a/toolbar_test.go +++ b/navigation/toolbar_test.go @@ -1,4 +1,4 @@ -package mujica +package navigation import ( "slices" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" ) type toolbarLog struct{ clicks []string } @@ -39,7 +40,7 @@ func (l *toolbarLog) view(width float32) func(c *ui.Context) { func TestToolbarClickAndArrows(t *testing.T) { l := &toolbarLog{} - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); l.view(600)(c) }, 700, 100) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); l.view(600)(c) }, 700, 100) if err := tt.Click("Italic"); err != nil { t.Fatal(err) } @@ -59,7 +60,7 @@ func TestToolbarOverflow(t *testing.T) { l := &toolbarLog{} // The snapshot's layout: MyGo caches the overflow chevrons by position. tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Gap(16).Children(func() { l.view(180)(c) }) }, 520, 300) tt.Frame() @@ -89,7 +90,7 @@ func TestToolbarButtonNeedsLabel(t *testing.T) { } }() ui.Render(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ToolbarButton(c, "", ToolbarButtonOptions{Icon: icons.Must("copy")}) }, 200, 100, 1) } diff --git a/alert_dialog.go b/overlay/alert_dialog.go similarity index 70% rename from alert_dialog.go rename to overlay/alert_dialog.go index 378ef2e..8a77a63 100644 --- a/alert_dialog.go +++ b/overlay/alert_dialog.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "fmt" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AlertDialogOptions configure an AlertDialog. Title is required. @@ -37,32 +39,32 @@ func AlertDialog(c *ui.Context, open *bool, opts AlertDialogOptions) AlertDialog panic("mujica: AlertDialog needs a Title") } var res AlertDialogResult - site, focus := overlaySite(c) + site, focus := core.OverlaySite(c) if !*open { - focus.closed(c, site) + focus.Closed(c, site) return res } - overlayLayer(c, open) - k := Tokens(c) - u := Density(c).Unit() + core.OverlayLayer(c, open) + k := core.Tokens(c) + u := core.Density(c).Unit() confirmLabel, cancelLabel := opts.ConfirmLabel, opts.CancelLabel if confirmLabel == "" { - confirmLabel = msg(c, "alert_dialog.confirm") + confirmLabel = core.Msg(c, "alert_dialog.confirm") } if cancelLabel == "" { - cancelLabel = msg(c, "alert_dialog.cancel") + cancelLabel = core.Msg(c, "alert_dialog.cancel") } shown, backClicked := true, false panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) { backClicked = back.Clicked() st := ui.Local(panel, "mujica.alert", func() alertDialogState { return alertDialogState{} }) - p := overlayEnter(c, panel) - back.Background(overlayScrim(k).Alpha(p)).Padding(16) - dialogPanel(c, panel, 420, false, p) + p := core.OverlayEnter(c, panel) + back.Background(OverlayScrim(k).Alpha(p)).Padding(16) + DialogPanel(c, panel, 420, false, p) panel.Role(ui.RoleAlertDialog).Label(opts.Title).Description(opts.Description) ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Shrink(0).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(22, 22).TextColor(k.Danger).Shrink(0).Margin(1, 0, 0, 0) - ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) }) if opts.Description != "" { // A long description scrolls so the field and the actions stay in the window. @@ -75,15 +77,15 @@ func AlertDialog(c *ui.Context, open *bool, opts AlertDialogOptions) AlertDialog } unlocked := opts.ConfirmText == "" || st.typed == opts.ConfirmText dialogActions(c, func() { - if Button(c, cancelLabel, ButtonOptions{Variant: Secondary}).AutoFocus().Clicked() { + if input.Button(c, cancelLabel, input.ButtonOptions{Variant: input.Secondary}).AutoFocus().Clicked() { res.Canceled = true } - if Button(c, confirmLabel, ButtonOptions{Variant: Danger, Disabled: !unlocked}).Clicked() { + if input.Button(c, confirmLabel, input.ButtonOptions{Variant: input.Danger, Disabled: !unlocked}).Clicked() { res.Confirmed = true } }) }) - focus.shown(panel) + focus.Shown(panel) if !shown && !backClicked { res.Canceled = true // Escape } @@ -99,11 +101,11 @@ func AlertDialog(c *ui.Context, open *bool, opts AlertDialogOptions) AlertDialog // alertDialogTyped builds the confirmation field; Enter in it never confirms, even mid-composition. func alertDialogTyped(c *ui.Context, st *alertDialogState, want string) { - k := Tokens(c) - d := Density(c) - prompt := fmt.Sprintf(msg(c, "alert_dialog.type_to_unlock"), want) + k := core.Tokens(c) + d := core.Density(c) + prompt := fmt.Sprintf(core.Msg(c, "alert_dialog.type_to_unlock"), want) ui.Column(c).Gap(d.Unit()).Shrink(0).Children(func() { - ui.Text(c, prompt).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, prompt).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) in := ui.TextInputBase(c, &st.typed).Label(prompt).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3). Radius(theme.ControlRadius).Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder) internal.Ring(in, k.Focus, theme.ControlRadius) diff --git a/alert_dialog_test.go b/overlay/alert_dialog_test.go similarity index 94% rename from alert_dialog_test.go rename to overlay/alert_dialog_test.go index 1653ffb..46bcaec 100644 --- a/alert_dialog_test.go +++ b/overlay/alert_dialog_test.go @@ -1,9 +1,12 @@ -package mujica +package overlay import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type alertApp struct { @@ -13,8 +16,8 @@ type alertApp struct { } func (a *alertApp) view(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Remove", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Remove", input.ButtonOptions{}).Clicked() { a.open = true } r := AlertDialog(c, &a.open, a.opts) diff --git a/context_menu.go b/overlay/context_menu.go similarity index 96% rename from context_menu.go rename to overlay/context_menu.go index 5644a06..d1d05bd 100644 --- a/context_menu.go +++ b/overlay/context_menu.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "log/slog" @@ -49,6 +49,6 @@ func ContextMenu(c *ui.Context, target *ui.Element, opts ContextMenuOptions, bui target.Children(func() { anchor = ui.Box(c).Key("mujica.contextmenu.anchor").Absolute().Left(st.x).Top(st.y).Size(1, 1).PassThrough() }) - popupMenuOpen(c, anchor, &st.open, build) + PopupMenuOpen(c, anchor, &st.open, build) return target } diff --git a/context_menu_test.go b/overlay/context_menu_test.go similarity index 90% rename from context_menu_test.go rename to overlay/context_menu_test.go index cd6c876..635f71a 100644 --- a/context_menu_test.go +++ b/overlay/context_menu_test.go @@ -1,18 +1,21 @@ -package mujica +package overlay import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func contextMenuTester(a *menuApp, disabled bool) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Gap(8).Children(func() { - row := ui.Row(c).Height(80).FillWidth().Padding(12).Background(Tokens(c).Surface) + row := ui.Row(c).Height(80).FillWidth().Padding(12).Background(core.Tokens(c).Surface) row.Children(func() { - Button(c, "Letter row", ButtonOptions{Variant: Ghost}) + input.Button(c, "Letter row", input.ButtonOptions{Variant: input.Ghost}) }) ContextMenu(c, row, ContextMenuOptions{Disabled: disabled}, a.build) }) @@ -95,7 +98,7 @@ func TestContextMenuSnapshot(t *testing.T) { a := &menuApp{} snapshotAfter(t, "context_menu", 460, 320, func(c *ui.Context) { ui.Column(c).Padding(20).Children(func() { - row := ui.Row(c).Height(36).FillWidth().Padding(0, 12).AlignItems(ui.Center).Background(Tokens(c).Selection) + row := ui.Row(c).Height(36).FillWidth().Padding(0, 12).AlignItems(ui.Center).Background(core.Tokens(c).Selection) row.Children(func() { ui.Text(c, "Letter to the Countess") }) ContextMenu(c, row, ContextMenuOptions{}, a.build) }) diff --git a/dialog.go b/overlay/dialog.go similarity index 74% rename from dialog.go rename to overlay/dialog.go index b368286..88903a2 100644 --- a/dialog.go +++ b/overlay/dialog.go @@ -1,13 +1,14 @@ -package mujica +package overlay import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DialogOptions configure a Dialog. Title is required. @@ -33,26 +34,26 @@ func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) *ui.E if opts.Title == "" { panic("mujica: Dialog needs a Title") } - site, focus := overlaySite(c) + site, focus := core.OverlaySite(c) if !*open { - focus.closed(c, site) + focus.Closed(c, site) return nil } - overlayLayer(c, open) - k := Tokens(c) + core.OverlayLayer(c, open) + k := core.Tokens(c) shown, backClicked, closed := true, false, false panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) { backClicked = back.Clicked() - p := overlayEnter(c, panel) - back.Background(overlayScrim(k).Alpha(p)).Padding(16) - dialogPanel(c, panel, opts.Width, opts.Decorated, p) + p := core.OverlayEnter(c, panel) + back.Background(OverlayScrim(k).Alpha(p)).Padding(16) + DialogPanel(c, panel, opts.Width, opts.Decorated, p) panel.Label(opts.Title) if opts.Description != "" { panel.Description(opts.Description) } closed = dialogHeader(c, opts.Title, opts.Decorated, !opts.HideClose, "dialog.close") // Description and content scroll together so the actions always keep their room. - ui.Scroll(c).Shrink(1).Gap(Density(c).Unit() * 3).Children(func() { + ui.Scroll(c).Shrink(1).Gap(core.Density(c).Unit() * 3).Children(func() { if opts.Description != "" { ui.Text(c, opts.Description).TextColor(k.TextMuted) } @@ -64,7 +65,7 @@ func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) *ui.E dialogActions(c, opts.Actions) } }) - focus.shown(panel) + focus.Shown(panel) switch { case closed: slog.Debug("mujica: dialog close", "by", "button") @@ -79,15 +80,15 @@ func Dialog(c *ui.Context, open *bool, opts DialogOptions, content func()) *ui.E return panel } -// dialogPanel styles a centered dialog panel at entrance progress p. -func dialogPanel(c *ui.Context, panel *ui.Element, width float32, decorated bool, p float32) { - k := Tokens(c) - u := Density(c).Unit() +// DialogPanel styles a centered dialog panel at entrance progress p. +func DialogPanel(c *ui.Context, panel *ui.Element, width float32, decorated bool, p float32) { + k := core.Tokens(c) + u := core.Density(c).Unit() if width == 0 { width = 440 } w, h := c.Size() - overlayStyle(c, panel, p) + core.OverlayStyle(c, panel, p) panel.Width(width).MaxWidth(w-32).MaxHeight(h-32).Padding(u*6).Gap(u*3).Margin(8*(1-p), 0, 0, 0) if decorated { panel.Border(theme.BorderWidth, k.Ornament).Padding(u*6 + 6).DrawOver(func(pt *ui.Painter, r ui.Rect) { @@ -98,14 +99,14 @@ func dialogPanel(c *ui.Context, panel *ui.Element, width float32, decorated bool // dialogHeader builds the title and close button; it reports a close click. func dialogHeader(c *ui.Context, title string, decorated, closable bool, closeKey string) bool { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() clicked := false ui.Column(c).Gap(u).Shrink(0).Children(func() { ui.Row(c).Gap(u * 3).AlignItems(ui.Start).Children(func() { - ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) + ui.Text(c, title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).FontWeight(600).Grow(1) if closable { - clicked = iconAction(c, icons.Must("x"), msg(c, closeKey)).Margin(-4, -4, 0, 0).Clicked() + clicked = core.IconAction(c, icons.Must("x"), core.Msg(c, closeKey)).Margin(-4, -4, 0, 0).Clicked() } }) if decorated { @@ -124,6 +125,6 @@ func dialogRule(c *ui.Context, ink ui.Color) { // dialogActions lays out a dialog footer. func dialogActions(c *ui.Context, actions func()) { - u := Density(c).Unit() + u := core.Density(c).Unit() ui.Row(c).Gap(u*2).Justify(ui.End).Wrap().Shrink(0).Margin(u*2, 0, 0, 0).Children(actions) } diff --git a/dialog_test.go b/overlay/dialog_test.go similarity index 82% rename from dialog_test.go rename to overlay/dialog_test.go index 5546998..a860ed7 100644 --- a/dialog_test.go +++ b/overlay/dialog_test.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "testing" @@ -6,7 +6,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type dialogApp struct { @@ -18,15 +20,15 @@ type dialogApp struct { } func (a *dialogApp) view(c *ui.Context) { - Use(c, Settings{Mode: Light, Density: a.densities}) - if Button(c, "Rename", ButtonOptions{}).Clicked() { + core.Use(c, core.Settings{Mode: core.Light, Density: a.densities}) + if input.Button(c, "Rename", input.ButtonOptions{}).Clicked() { a.open = true } a.opts.Actions = func() { - if Button(c, "Cancel", ButtonOptions{Variant: Secondary}).Clicked() { + if input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}).Clicked() { a.open = false } - if Button(c, "Save", ButtonOptions{}).Clicked() { + if input.Button(c, "Save", input.ButtonOptions{}).Clicked() { a.saved++ a.open = false } @@ -118,9 +120,9 @@ func TestDialogCloseRules(t *testing.T) { func TestDialogEscapeGoesToNestedPopoverFirst(t *testing.T) { open, inner := true, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Dialog(c, &open, DialogOptions{Title: "Outer"}, func() { - b := Button(c, "More", ButtonOptions{}) + b := input.Button(c, "More", input.ButtonOptions{}) if b.Clicked() { inner = true } @@ -149,26 +151,26 @@ func TestDialogNeedsTitle(t *testing.T) { }() open := true ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Dialog(c, &open, DialogOptions{}, nil) }, 300, 200) } func dialogSnapshotView(d theme.Density, decorated bool) func(c *ui.Context) { return func(c *ui.Context) { - mode := Light - if Tokens(c).Dark { - mode = Dark + mode := core.Light + if core.Tokens(c).Dark { + mode = core.Dark } - Use(c, Settings{Mode: mode, Density: d}) + core.Use(c, core.Settings{Mode: mode, Density: d}) open := true name := "Ophelia" ui.Column(c).Padding(24).Children(func() { ui.Text(c, "Behind the dialog") }) Dialog(c, &open, DialogOptions{ Title: "Rename the chamber", Description: "The new name shows in every hall.", Decorated: decorated, Actions: func() { - Button(c, "Cancel", ButtonOptions{Variant: Secondary}) - Button(c, "Rename", ButtonOptions{}) + input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}) + input.Button(c, "Rename", input.ButtonOptions{}) }, }, func() { ui.TextInput(c, &name) }) } @@ -177,9 +179,9 @@ func dialogSnapshotView(d theme.Density, decorated bool) func(c *ui.Context) { func TestDialogEscapeHidesTooltipBeforeClosing(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Dialog(c, &open, DialogOptions{Title: "Outer", HideClose: true}, func() { - b := Button(c, "Seal", ButtonOptions{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) Tooltip(c, b, "Seal with wax", TooltipOptions{Delay: time.Millisecond}) }) }, 600, 400) @@ -200,14 +202,14 @@ func TestDialogEscapeHidesTooltipBeforeClosing(t *testing.T) { func TestDialogFocusFallsBackWhenOpenerIsGone(t *testing.T) { open, gone := false, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { - Button(c, "First", ButtonOptions{}) - if !gone && Button(c, "Open", ButtonOptions{}).Clicked() { + input.Button(c, "First", input.ButtonOptions{}) + if !gone && input.Button(c, "Open", input.ButtonOptions{}).Clicked() { open, gone = true, true } Dialog(c, &open, DialogOptions{Title: "Gone"}, nil) - Button(c, "Next", ButtonOptions{}) + input.Button(c, "Next", input.ButtonOptions{}) }) }, 600, 400) _ = tt.Click("Open") diff --git a/overlay/doc.go b/overlay/doc.go new file mode 100644 index 0000000..f01864a --- /dev/null +++ b/overlay/doc.go @@ -0,0 +1,2 @@ +// Package overlay holds MujicaUI dialogs, drawers, popovers, tooltips and menus that float above the view. +package overlay diff --git a/drawer.go b/overlay/drawer.go similarity index 89% rename from drawer.go rename to overlay/drawer.go index 9c76ede..dc9e614 100644 --- a/drawer.go +++ b/overlay/drawer.go @@ -1,11 +1,12 @@ -package mujica +package overlay import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DrawerSide is the window edge a Drawer comes from. The zero value is DrawerRight. @@ -41,12 +42,12 @@ func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.E if opts.Size < 0 { panic("mujica: Drawer Size is negative") } - site, focus := overlaySite(c) + site, focus := core.OverlaySite(c) if !*open { - focus.closed(c, site) + focus.Closed(c, site) return nil } - k := Tokens(c) + k := core.Tokens(c) size := drawerSize(c, opts) closed := false body := func(panel *ui.Element, p float32) { @@ -65,23 +66,23 @@ func Drawer(c *ui.Context, open *bool, opts DrawerOptions, content func()) *ui.E if opts.NonModal { ui.Overlay(c, func() { panel = ui.Column(c).Absolute().Role(ui.RoleDialog) - p := overlayEnter(c, panel) + p := core.OverlayEnter(c, panel) drawerPlace(c, panel, opts.Side, size*p) panel.Children(func() { body(panel, p) }) }) - escaped = overlayEscape(c, open) + escaped = core.OverlayEscape(c, open) } else { - overlayLayer(c, open) + core.OverlayLayer(c, open) shown := true panel = ui.DialogBase(c, &shown, func(back, panel *ui.Element) { - p := overlayEnter(c, panel) - back.Background(overlayScrim(k).Alpha(p)) + p := core.OverlayEnter(c, panel) + back.Background(OverlayScrim(k).Alpha(p)) drawerBackdrop(back, opts.Side) body(panel, p) }) escaped = !shown } - focus.shown(panel) + focus.Shown(panel) if closed || escaped { slog.Debug("mujica: drawer close", "escape", escaped) *open = false @@ -133,8 +134,8 @@ func drawerPlace(c *ui.Context, panel *ui.Element, s DrawerSide, size float32) { // drawerPanel styles the panel at entrance progress p, sliding in from its edge. func drawerPanel(c *ui.Context, panel *ui.Element, s DrawerSide, size, p float32) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() off := -(1 - p) * size panel.Background(k.Surface).TextColor(k.Text).Opacity(p).Padding(u*6).Gap(u*3). Shadow(0, 0, 24, 0, ui.RGBA(0, 0, 0, 0.18*p)) diff --git a/drawer_test.go b/overlay/drawer_test.go similarity index 85% rename from drawer_test.go rename to overlay/drawer_test.go index fdfa533..047477a 100644 --- a/drawer_test.go +++ b/overlay/drawer_test.go @@ -1,9 +1,12 @@ -package mujica +package overlay import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type drawerApp struct { @@ -14,17 +17,17 @@ type drawerApp struct { } func (a *drawerApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Gap(8).Children(func() { - if Button(c, "Filters", ButtonOptions{}).Clicked() { + if input.Button(c, "Filters", input.ButtonOptions{}).Clicked() { a.open = true } - if Button(c, "Behind", ButtonOptions{}).Clicked() { + if input.Button(c, "Behind", input.ButtonOptions{}).Clicked() { a.clicked++ } }) if p := Drawer(c, &a.open, a.opts, func() { - Button(c, "Apply", ButtonOptions{}) + input.Button(c, "Apply", input.ButtonOptions{}) }); p != nil { a.panel = p.Bounds() } @@ -120,10 +123,10 @@ func TestDrawerSnapshot(t *testing.T) { open := true ui.Column(c).Padding(24).Children(func() { ui.Text(c, "The gallery behind") }) Drawer(c, &open, DrawerOptions{Title: "Seating chart", Size: 300, Actions: func() { - Button(c, "Reset", ButtonOptions{Variant: Secondary}) - Button(c, "Apply", ButtonOptions{}) + input.Button(c, "Reset", input.ButtonOptions{Variant: input.Secondary}) + input.Button(c, "Apply", input.ButtonOptions{}) }}, func() { - ui.Text(c, "Guests are placed by rank, then by arrival.").TextColor(Tokens(c).TextMuted) + ui.Text(c, "Guests are placed by rank, then by arrival.").TextColor(core.Tokens(c).TextMuted) }) }) } diff --git a/dropdown_menu.go b/overlay/dropdown_menu.go similarity index 95% rename from dropdown_menu.go rename to overlay/dropdown_menu.go index 87bc084..a27d00b 100644 --- a/dropdown_menu.go +++ b/overlay/dropdown_menu.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "log/slog" @@ -36,6 +36,6 @@ func DropdownMenu(c *ui.Context, label string, opts DropdownMenuOptions, build f slog.Debug("mujica: dropdown_menu open") } popupMenuTrigger(c, b, label, opts.Icon, *open) - popupMenuOpen(c, b, open, build) + PopupMenuOpen(c, b, open, build) return b } diff --git a/dropdown_menu_test.go b/overlay/dropdown_menu_test.go similarity index 95% rename from dropdown_menu_test.go rename to overlay/dropdown_menu_test.go index 9b4d078..db78b95 100644 --- a/dropdown_menu_test.go +++ b/overlay/dropdown_menu_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type menuApp struct { @@ -39,10 +42,10 @@ func (a *menuApp) build(m *PopupMenu) { } func (a *menuApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(20).Gap(8).AlignItems(ui.Start).Children(func() { DropdownMenu(c, "Actions", DropdownMenuOptions{}, a.build) - Button(c, "After", ButtonOptions{}) + input.Button(c, "After", input.ButtonOptions{}) }) } @@ -120,7 +123,7 @@ func TestDropdownMenuKeyboard(t *testing.T) { func TestDropdownMenuSubmenuFlips(t *testing.T) { a := &menuApp{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Fill().Justify(ui.End).Padding(20).Children(func() { DropdownMenu(c, "Actions", DropdownMenuOptions{}, a.build) }) @@ -154,7 +157,7 @@ func TestDropdownMenuWideSubmenuStaysInWindow(t *testing.T) { wide := strings.Repeat("Wide ", 30) var sub ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Fill().Padding(20, 0, 0, 200).Children(func() { DropdownMenu(c, "Actions", DropdownMenuOptions{}, func(m *PopupMenu) { m.Item(wide, PopupMenuItemOptions{}) @@ -181,7 +184,7 @@ func TestDropdownMenuWideSubmenuStaysInWindow(t *testing.T) { func TestDropdownMenuDisabled(t *testing.T) { a := &menuApp{} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) DropdownMenu(c, "Actions", DropdownMenuOptions{Disabled: true}, a.build) }, 300, 300) _ = tt.Click("Actions") diff --git a/overlay/example_essentials_test.go b/overlay/example_essentials_test.go new file mode 100644 index 0000000..f3f3cca --- /dev/null +++ b/overlay/example_essentials_test.go @@ -0,0 +1,23 @@ +package overlay_test + +import ( + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" +) + +func ExampleSplitButton() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := overlay.SplitButton(c, "Save", overlay.SplitButtonOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Save as…", overlay.PopupMenuItemOptions{}) { + // ask for a file name + } + }) + if r.Changed() { + // save + } + } + ui.Render(view, 400, 300, 1) +} diff --git a/overlay/example_overlay_test.go b/overlay/example_overlay_test.go new file mode 100644 index 0000000..db87074 --- /dev/null +++ b/overlay/example_overlay_test.go @@ -0,0 +1,169 @@ +package overlay_test + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/overlay" +) + +func ExampleDialog() { + open := true + name := "Ophelia" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "Rename…", input.ButtonOptions{}).Clicked() { + open = true + } + overlay.Dialog(c, &open, overlay.DialogOptions{ + Title: "Rename the chamber", + Description: "The new name shows in every hall.", + Decorated: true, + Actions: func() { + if input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}).Clicked() { + open = false + } + if input.Button(c, "Rename", input.ButtonOptions{}).Clicked() { + open = false + } + }, + }, func() { + ui.TextInput(c, &name) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleAlertDialog() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "Delete archive", input.ButtonOptions{Variant: input.Danger}).Clicked() { + open = true + } + r := overlay.AlertDialog(c, &open, overlay.AlertDialogOptions{ + Title: "Delete the archive?", + Description: "Twelve letters will be destroyed.", + ConfirmText: "archive", + }) + if r.Confirmed { + fmt.Println("deleted") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDrawer() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if input.Button(c, "Filters", input.ButtonOptions{}).Clicked() { + open = true + } + overlay.Drawer(c, &open, overlay.DrawerOptions{Title: "Filters", Side: overlay.DrawerRight, Size: 320}, func() { + ui.Text(c, "Guests are placed by rank.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePopover() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + b := input.Button(c, "Guests", input.ButtonOptions{Variant: input.Secondary}) + if b.Clicked() { + open = !open + } + overlay.Popover(c, b, &open, overlay.PopoverOptions{Title: "Guest list"}, func() { + ui.Text(c, "Twelve guests confirmed.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTooltip() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + b := input.Button(c, "Seal", input.ButtonOptions{}) + overlay.Tooltip(c, b, "Seal the letter with wax", overlay.TooltipOptions{}) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHoverCard() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + b := input.Button(c, "Lady Sakiko", input.ButtonOptions{Variant: input.Ghost}) + overlay.HoverCard(c, b, overlay.HoverCardOptions{}, func() { + ui.Text(c, "Keyboardist of the court.") + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDropdownMenu() { + pinned := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + overlay.DropdownMenu(c, "Actions", overlay.DropdownMenuOptions{}, func(m *overlay.PopupMenu) { + m.Group("Letter", func() { + if m.Item("Rename", overlay.PopupMenuItemOptions{}) { + fmt.Println("rename") + } + m.Check("Pinned", &pinned, overlay.PopupMenuItemOptions{KeepOpen: true}) + }) + m.Submenu("Move to", overlay.PopupMenuItemOptions{}, func(m *overlay.PopupMenu) { + m.Item("Archive", overlay.PopupMenuItemOptions{}) + }) + m.Separator() + m.Item("Burn", overlay.PopupMenuItemOptions{Disabled: true}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleContextMenu() { + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + row := ui.Row(c).Padding(12).Children(func() { + ui.Text(c, "Letter to the Countess") + }) + overlay.ContextMenu(c, row, overlay.ContextMenuOptions{}, func(m *overlay.PopupMenu) { + if m.Item("Reply", overlay.PopupMenuItemOptions{}) { + fmt.Println("reply") + } + m.Item("Delete", overlay.PopupMenuItemOptions{}) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExamplePopconfirm() { + open := false + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + b := input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) + if b.Clicked() { + open = true + } + if overlay.Popconfirm(c, b, &open, overlay.PopconfirmOptions{Title: "Archive this letter?"}).Confirmed { + fmt.Println("archived") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOverlay() { + open := true + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + overlay.Overlay(c, &open, overlay.OverlayOptions{Place: overlay.OverlayBottomRight, Label: "Now playing"}, func() { + ui.Text(c, "Symbol I: △") + }) + } + ui.Render(view, 400, 300, 1) +} diff --git a/overlay/helpers_test.go b/overlay/helpers_test.go new file mode 100644 index 0000000..0a6ac9f --- /dev/null +++ b/overlay/helpers_test.go @@ -0,0 +1,30 @@ +package overlay + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +// snapshotAfter is snapshot with drive sending input before the capture. +func snapshotAfter(t *testing.T, name string, w, h int, view func(c *ui.Context), drive func(tt *ui.Tester)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, drive) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/hover_card.go b/overlay/hover_card.go similarity index 89% rename from hover_card.go rename to overlay/hover_card.go index 030e5e1..7eac2d1 100644 --- a/hover_card.go +++ b/overlay/hover_card.go @@ -1,10 +1,12 @@ -package mujica +package overlay import ( "log/slog" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) // HoverCardOptions configure a HoverCard. @@ -90,13 +92,13 @@ func HoverCard(c *ui.Context, anchor *ui.Element, opts HoverCardOptions, content if width == 0 { width = 280 } - overlayLayer(c, st) + core.OverlayLayer(c, st) // A popover keeps the card in the anchor's focus order and modal scope, even inside a Dialog. shown := true panel := ui.PopoverBase(c, anchor, &shown, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(6, 0, 0, 0).Width(popoverWide(c, width)).Padding(Density(c).Unit() * 4).Gap(Density(c).Unit() * 2). - Label(opts.Label).MaxHeight(popoverRoom(c, b)) + core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b)) + panel.Margin(6, 0, 0, 0).Width(PopoverWide(c, width)).Padding(core.Density(c).Unit() * 4).Gap(core.Density(c).Unit() * 2). + Label(opts.Label).MaxHeight(PopoverRoom(c, b)) ui.Scroll(c).Shrink(1).Children(content) }) if !shown { diff --git a/hover_card_test.go b/overlay/hover_card_test.go similarity index 89% rename from hover_card_test.go rename to overlay/hover_card_test.go index 43e5991..7f7af8e 100644 --- a/hover_card_test.go +++ b/overlay/hover_card_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const hoverCardTestDelay = 150 * time.Millisecond @@ -15,20 +18,20 @@ type hoverCardApp struct { } func (a *hoverCardApp) card(c *ui.Context) { - b := Button(c, "Lady Sakiko", ButtonOptions{Variant: Ghost}) + b := input.Button(c, "Lady Sakiko", input.ButtonOptions{Variant: input.Ghost}) HoverCard(c, b, HoverCardOptions{OpenDelay: hoverCardTestDelay, CloseDelay: hoverCardTestDelay}, func() { ui.Text(c, "Keyboardist of the court") - if Button(c, "Follow", ButtonOptions{}).Clicked() { + if input.Button(c, "Follow", input.ButtonOptions{}).Clicked() { a.follows++ } }) - if Button(c, "Next", ButtonOptions{}).Clicked() { + if input.Button(c, "Next", input.ButtonOptions{}).Clicked() { a.next++ } } func (a *hoverCardApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if a.inDialog { open := true Dialog(c, &open, DialogOptions{Title: "Court", HideClose: true}, func() { @@ -156,12 +159,12 @@ func TestHoverCardInsideDialog(t *testing.T) { func TestHoverCardSnapshot(t *testing.T) { snapshotAfter(t, "hover_card", 420, 240, func(c *ui.Context) { ui.Column(c).Padding(24).AlignItems(ui.Start).Children(func() { - b := Button(c, "Lady Uika", ButtonOptions{Variant: Ghost}) + b := input.Button(c, "Lady Uika", input.ButtonOptions{Variant: input.Ghost}) HoverCard(c, b, HoverCardOptions{OpenDelay: time.Millisecond}, func() { ui.Column(c).Gap(8).AlignItems(ui.Start).Children(func() { ui.Text(c, "Lady Uika").FontWeight(600) - ui.Text(c, "Vocalist and guitarist; keeps the ledger of the masquerade.").TextColor(Tokens(c).TextMuted) - Button(c, "Follow", ButtonOptions{Variant: Outline}) + ui.Text(c, "Vocalist and guitarist; keeps the ledger of the masquerade.").TextColor(core.Tokens(c).TextMuted) + input.Button(c, "Follow", input.ButtonOptions{Variant: input.Outline}) }) }) }) diff --git a/overlay.go b/overlay/overlay.go similarity index 60% rename from overlay.go rename to overlay/overlay.go index c2448e2..03851bd 100644 --- a/overlay.go +++ b/overlay/overlay.go @@ -1,12 +1,12 @@ -package mujica +package overlay import ( "log/slog" - "time" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // OverlayPlace is where an Overlay sits in the window. The zero value is OverlayCenter. @@ -45,34 +45,34 @@ func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui if opts.Place < OverlayCenter || opts.Place > OverlayBottomRight { panic("mujica: Overlay Place out of range") } - site, focus := overlaySite(c) + site, focus := core.OverlaySite(c) if !*open { - focus.closed(c, site) + focus.Closed(c, site) return nil } - k := Tokens(c) + k := core.Tokens(c) inset := opts.Inset if inset == 0 { inset = 16 } fx, fy := overlayFractions(opts.Place) if opts.Modal { - overlayLayer(c, open) + core.OverlayLayer(c, open) var backClicked bool shown := true panel := ui.DialogBase(c, &shown, func(back, panel *ui.Element) { backClicked = back.Clicked() - p := overlayEnter(c, panel) + p := core.OverlayEnter(c, panel) back.Padding(inset).Justify(overlayAlign(fy)).AlignItems(overlayAlign(fx)) if opts.Dim { - back.Background(overlayScrim(k).Alpha(p)) + back.Background(OverlayScrim(k).Alpha(p)) } - overlayStyle(c, panel, p) + core.OverlayStyle(c, panel, p) w, h := c.Size() - panel.Label(opts.Label).Padding(Density(c).Unit() * 4).MaxWidth(w - 2*inset).MaxHeight(h - 2*inset) + panel.Label(opts.Label).Padding(core.Density(c).Unit() * 4).MaxWidth(w - 2*inset).MaxHeight(h - 2*inset) ui.Scroll(c).Shrink(1).Children(content) }) - focus.shown(panel) + focus.Shown(panel) if !shown { slog.Debug("mujica: overlay close", "backdrop", backClicked) *open = false @@ -82,20 +82,20 @@ func Overlay(c *ui.Context, open *bool, opts OverlayOptions, content func()) *ui var panel *ui.Element ui.Overlay(c, func() { w, h := c.Size() - panel = ui.Column(c).Absolute().Role(ui.RoleDialog).Label(opts.Label).Padding(Density(c).Unit() * 4). + panel = ui.Column(c).Absolute().Role(ui.RoleDialog).Label(opts.Label).Padding(core.Density(c).Unit() * 4). MaxWidth(w - 2*inset).MaxHeight(h - 2*inset) b := panel.Bounds() panel.Left(inset + (w-2*inset-b.W)*fx).Top(inset + (h-2*inset-b.H)*fy) - p := overlayEnter(c, panel) + p := core.OverlayEnter(c, panel) if b.W == 0 { p = 0 // not measured yet c.AnimationFrame() } - overlayStyle(c, panel, p) + core.OverlayStyle(c, panel, p) panel.Children(func() { ui.Scroll(c).Shrink(1).Children(content) }) }) - focus.shown(panel) - if overlayEscape(c, open) { + focus.Shown(panel) + if core.OverlayEscape(c, open) { slog.Debug("mujica: overlay close", "escape", true) *open = false } @@ -134,54 +134,19 @@ func overlayAlign(f float32) ui.Align { return ui.Center } -// overlayEnter returns how far, 0 to 1, the popup born with e has entered. -func overlayEnter(c *ui.Context, e *ui.Element) float32 { - born := ui.Local(e, "mujica.overlay.born", func() time.Time { return c.Now() }) - d := Motion(c, theme.PopupDuration) - if d == 0 { - return 1 - } - t := float32(c.Now().Sub(*born)) / float32(d) - if t >= 1 { - return 1 - } - c.AnimationFrame() - return ui.EaseOut(t) -} - -// overlayScrim is the color dimming the window behind a modal layer. -func overlayScrim(k theme.Tokens) ui.Color { +// OverlayScrim is the color dimming the window behind a modal layer. +func OverlayScrim(k theme.Tokens) ui.Color { if k.Dark { return ui.RGBA(0, 0, 0, 0.6) } return k.Text.Alpha(0.42) } -// overlayStyle gives a popup panel the surface, edge and shadow of the theme. -func overlayStyle(c *ui.Context, panel *ui.Element, p float32) { - k := Tokens(c) - panel.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.PopupRadius). - TextColor(k.Text).Opacity(p).Shadow(0, 6, 18, 0, ui.RGBA(0, 0, 0, 0.16*p)) -} - -// overlayPlace puts a w×h box below anchor a, above when there is no room, inside the window. -func overlayPlace(c *ui.Context, a ui.Rect, w, h, gap float32) (x, y float32) { - ww, wh := c.Size() - const edge = 4 - x, y = a.X, a.Y+a.H+gap - if y+h > wh-edge && a.Y-gap-h >= edge { - y = a.Y - gap - h - } - x = max(edge, min(x, ww-edge-w)) - y = max(edge, min(y, wh-edge-h)) - return x, y -} - -// overlayAnchored is overlayEnter, held at 0 until the anchor b has been laid out. -func overlayAnchored(c *ui.Context, panel *ui.Element, b ui.Rect) float32 { +// OverlayAnchored is overlayEnter, held at 0 until the anchor b has been laid out. +func OverlayAnchored(c *ui.Context, panel *ui.Element, b ui.Rect) float32 { if b.W == 0 && b.H == 0 { c.AnimationFrame() return 0 } - return overlayEnter(c, panel) + return core.OverlayEnter(c, panel) } diff --git a/overlay_fit_test.go b/overlay/overlay_fit_test.go similarity index 83% rename from overlay_fit_test.go rename to overlay/overlay_fit_test.go index a64ea50..e788afe 100644 --- a/overlay_fit_test.go +++ b/overlay/overlay_fit_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // overlayLongText is about 372 characters of description. @@ -24,10 +27,10 @@ func overlayInWindow(t *testing.T, tt *ui.Tester, w, h float32, labels ...string func TestDialogActionsStayInWindowAtLargeText(t *testing.T) { open, name := true, "Ophelia" tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Dialog(c, &open, DialogOptions{Title: "Rename", Description: overlayLongText, Actions: func() { - Button(c, "Cancel", ButtonOptions{Variant: Secondary}) - Button(c, "Save", ButtonOptions{}) + input.Button(c, "Cancel", input.ButtonOptions{Variant: input.Secondary}) + input.Button(c, "Save", input.ButtonOptions{}) }}, func() { ui.TextInput(c, &name) }) }, 800, 600) tt.SetPreferences(ui.Preferences{TextScale: 2}) @@ -38,7 +41,7 @@ func TestDialogActionsStayInWindowAtLargeText(t *testing.T) { func TestAlertDialogActionsStayInWindowAtLargeText(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) AlertDialog(c, &open, AlertDialogOptions{Title: "Burn?", Description: overlayLongText, ConfirmText: "letters"}) }, 800, 600) tt.SetPreferences(ui.Preferences{TextScale: 2}) @@ -51,10 +54,10 @@ func TestOverlayPanelFitsWindow(t *testing.T) { open := true var panel ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if p := Overlay(c, &open, OverlayOptions{Modal: modal}, func() { ui.Text(c, strings.Repeat(overlayLongText, 6)) - Button(c, "Done", ButtonOptions{}) + input.Button(c, "Done", input.ButtonOptions{}) }); p != nil { panel = p.Bounds() } diff --git a/overlay_snapshot_test.go b/overlay/overlay_snapshot_test.go similarity index 95% rename from overlay_snapshot_test.go rename to overlay/overlay_snapshot_test.go index edc40ef..9407a55 100644 --- a/overlay_snapshot_test.go +++ b/overlay/overlay_snapshot_test.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "time" diff --git a/overlay_test.go b/overlay/overlay_test.go similarity index 89% rename from overlay_test.go rename to overlay/overlay_test.go index 95394fb..5347021 100644 --- a/overlay_test.go +++ b/overlay/overlay_test.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "runtime" @@ -6,6 +6,9 @@ import ( "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type overlayApp struct { @@ -16,18 +19,18 @@ type overlayApp struct { } func (a *overlayApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).Gap(8).Children(func() { - if Button(c, "Show", ButtonOptions{}).Clicked() { + if input.Button(c, "Show", input.ButtonOptions{}).Clicked() { a.open = true } - if Button(c, "Behind", ButtonOptions{}).Clicked() { + if input.Button(c, "Behind", input.ButtonOptions{}).Clicked() { a.behind++ } }) if p := Overlay(c, &a.open, a.opts, func() { ui.Text(c, "Layer") - if Button(c, "Done", ButtonOptions{}).Clicked() { + if input.Button(c, "Done", input.ButtonOptions{}).Clicked() { a.open = false } }); p != nil { @@ -97,8 +100,8 @@ func TestOverlayNonModal(t *testing.T) { func TestOverlayEscapeClosesTopNonModalLayerFirst(t *testing.T) { bottom, top := false, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - if Button(c, "Show", ButtonOptions{}).Absolute().Right(10).Top(10).Clicked() { + core.Use(c, core.Settings{}) + if input.Button(c, "Show", input.ButtonOptions{}).Absolute().Right(10).Top(10).Clicked() { if bottom { top = true } @@ -126,11 +129,11 @@ func TestOverlayEscapeClosesTopNonModalLayerFirst(t *testing.T) { func TestOverlayNonModalRestoresFocusToItsPlace(t *testing.T) { open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { - Button(c, "First", ButtonOptions{}) - Overlay(c, &open, OverlayOptions{}, func() { Button(c, "Inside", ButtonOptions{}) }) - Button(c, "Next", ButtonOptions{}) + input.Button(c, "First", input.ButtonOptions{}) + Overlay(c, &open, OverlayOptions{}, func() { input.Button(c, "Inside", input.ButtonOptions{}) }) + input.Button(c, "Next", input.ButtonOptions{}) }) }, 600, 400) if err := tt.Click("Inside"); err != nil { @@ -154,7 +157,7 @@ func TestOverlayDimNeedsModal(t *testing.T) { }() open := true ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Overlay(c, &open, OverlayOptions{Dim: true}, func() {}) }, 200, 200) } @@ -176,7 +179,7 @@ func overlayCases() []overlayCase { } leave := func(tt *ui.Tester) { tt.Move(590, 390) } anchor := func(c *ui.Context, open *bool) *ui.Element { - b := Button(c, "Anchor", ButtonOptions{}) + b := input.Button(c, "Anchor", input.ButtonOptions{}) if b.Clicked() { *open = true } @@ -214,7 +217,7 @@ func overlayCases() []overlayCase { }, clickAnchor, escape, "Rename"}, {"ContextMenu", func(c *ui.Context, open *bool) { row := ui.Row(c) - row.Children(func() { Button(c, "Anchor", ButtonOptions{}) }) + row.Children(func() { input.Button(c, "Anchor", input.ButtonOptions{}) }) ContextMenu(c, row, ContextMenuOptions{}, (&menuApp{}).build) }, func(tt *ui.Tester) { clickAnchor(tt) @@ -235,7 +238,7 @@ func TestOverlayLifecycle(t *testing.T) { for _, oc := range overlayCases() { open := false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) oc.view(c, &open) }, 600, 400) tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1}) @@ -296,7 +299,7 @@ func TestOverlaySnapshot(t *testing.T) { ui.Column(c).Padding(24).Children(func() { ui.Text(c, "Score of the second act") }) Overlay(c, &open, OverlayOptions{Place: OverlayBottomRight, Label: "Now playing"}, func() { ui.Text(c, "Now playing").FontWeight(600) - ui.Text(c, "Symbol I: △").TextColor(Tokens(c).TextMuted) + ui.Text(c, "Symbol I: △").TextColor(core.Tokens(c).TextMuted) }) }) } diff --git a/popconfirm.go b/overlay/popconfirm.go similarity index 76% rename from popconfirm.go rename to overlay/popconfirm.go index 5799756..b52a0ab 100644 --- a/popconfirm.go +++ b/overlay/popconfirm.go @@ -1,11 +1,13 @@ -package mujica +package overlay import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) // PopconfirmOptions configure a Popconfirm. Title is required. @@ -37,37 +39,37 @@ func Popconfirm(c *ui.Context, anchor *ui.Element, open *bool, opts PopconfirmOp if !*open { return res } - overlayLayer(c, open) - u := Density(c).Unit() + core.OverlayLayer(c, open) + u := core.Density(c).Unit() confirm, cancel := opts.ConfirmLabel, opts.CancelLabel if confirm == "" { - confirm = msg(c, "popconfirm.confirm") + confirm = core.Msg(c, "popconfirm.confirm") } if cancel == "" { - cancel = msg(c, "popconfirm.cancel") + cancel = core.Msg(c, "popconfirm.cancel") } - variant := Primary + variant := input.Primary if opts.Danger { - variant = Danger + variant = input.Danger } b := anchor.Bounds() shown := true ui.PopoverBase(c, anchor, &shown, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 3).MaxWidth(popoverWide(c, 320)).MaxHeight(popoverRoom(c, b)).Role(ui.RoleAlertDialog). + core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b)) + panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 3).MaxWidth(PopoverWide(c, 320)).MaxHeight(PopoverRoom(c, b)).Role(ui.RoleAlertDialog). Label(opts.Title).Description(opts.Description) // The text scrolls so the actions stay in the window. ui.Scroll(c).Shrink(1).Children(func() { popconfirmText(c, opts) }) ui.Row(c).Gap(u * 2).Justify(ui.End).Shrink(0).Children(func() { // Danger focuses Cancel, so a second Enter never runs the destructive action. - cb := Button(c, cancel, ButtonOptions{Variant: Secondary}) + cb := input.Button(c, cancel, input.ButtonOptions{Variant: input.Secondary}) if opts.Danger { cb.AutoFocus() } if cb.Clicked() { res.Canceled = true } - ok := Button(c, confirm, ButtonOptions{Variant: variant}) + ok := input.Button(c, confirm, input.ButtonOptions{Variant: variant}) if !opts.Danger { ok.AutoFocus() } @@ -88,8 +90,8 @@ func Popconfirm(c *ui.Context, anchor *ui.Element, open *bool, opts PopconfirmOp // popconfirmText is the icon, title and description of a Popconfirm. func popconfirmText(c *ui.Context, opts PopconfirmOptions) { - k := Tokens(c) - u := Density(c).Unit() + k := core.Tokens(c) + u := core.Density(c).Unit() ui.Row(c).Gap(u * 2).AlignItems(ui.Start).Children(func() { color := k.Warning if opts.Danger { diff --git a/popconfirm_test.go b/overlay/popconfirm_test.go similarity index 85% rename from popconfirm_test.go rename to overlay/popconfirm_test.go index 0949480..9c1d70d 100644 --- a/popconfirm_test.go +++ b/overlay/popconfirm_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type popconfirmApp struct { @@ -13,9 +16,9 @@ type popconfirmApp struct { } func (a *popconfirmApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(30).AlignItems(ui.Start).Children(func() { - b := Button(c, "Archive", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Archive", input.ButtonOptions{Variant: input.Secondary}) if b.Clicked() { a.open = true } @@ -49,8 +52,8 @@ func TestPopconfirmConfirmByKeyboard(t *testing.T) { func TestPopconfirmDangerFocusesCancel(t *testing.T) { open, confirmed := false, 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - b := Button(c, "Dismiss", ButtonOptions{}) + core.Use(c, core.Settings{}) + b := input.Button(c, "Dismiss", input.ButtonOptions{}) if b.Clicked() { open = true } @@ -87,9 +90,9 @@ func TestPopconfirmKeepsActionsInWindow(t *testing.T) { long := strings.Repeat("The east gate stays unwatched tonight. ", 60) open := true tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(30).Children(func() { - b := Button(c, "Archive", ButtonOptions{}) + b := input.Button(c, "Archive", input.ButtonOptions{}) Popconfirm(c, b, &open, PopconfirmOptions{Title: "Archive?", Description: long}) }) }, 400, 300) @@ -106,7 +109,7 @@ func TestPopconfirmSnapshot(t *testing.T) { snapshot(t, "popconfirm", 420, 220, func(c *ui.Context) { open := true ui.Column(c).Padding(24).AlignItems(ui.Start).Children(func() { - b := Button(c, "Dismiss the guard", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Dismiss the guard", input.ButtonOptions{Variant: input.Secondary}) Popconfirm(c, b, &open, PopconfirmOptions{Title: "Dismiss the guard?", Description: "The east gate stays unwatched tonight.", Danger: true}) }) }) diff --git a/popover.go b/overlay/popover.go similarity index 59% rename from popover.go rename to overlay/popover.go index 609a16d..5849a66 100644 --- a/popover.go +++ b/overlay/popover.go @@ -1,11 +1,12 @@ -package mujica +package overlay import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PopoverOptions configure a Popover. @@ -26,23 +27,23 @@ func Popover(c *ui.Context, anchor *ui.Element, open *bool, opts PopoverOptions, if !*open { return nil } - overlayLayer(c, open) - u := Density(c).Unit() + core.OverlayLayer(c, open) + u := core.Density(c).Unit() b := anchor.Bounds() label := opts.Label if opts.Title != "" { label = opts.Title } panel := ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 2).MinWidth(popoverWide(c, b.W)).MaxHeight(popoverRoom(c, b)).Label(label) + core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b)) + panel.Margin(6, 0, 0, 0).Padding(u * 3).Gap(u * 2).MinWidth(PopoverWide(c, b.W)).MaxHeight(PopoverRoom(c, b)).Label(label) if opts.Width > 0 { - panel.Width(popoverWide(c, opts.Width)) + panel.Width(PopoverWide(c, opts.Width)) } else { - panel.MaxWidth(popoverWide(c, 360)) + panel.MaxWidth(PopoverWide(c, 360)) } if opts.Title != "" { - ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(FontSize(c, theme.BodySize+2)).FontWeight(600).Shrink(0) + ui.Text(c, opts.Title).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.BodySize+2)).FontWeight(600).Shrink(0) } ui.Scroll(c).Shrink(1).Children(content) }) @@ -52,14 +53,14 @@ func Popover(c *ui.Context, anchor *ui.Element, open *bool, opts PopoverOptions, return panel } -// popoverRoom is the tallest a panel below or above anchor b may be. -func popoverRoom(c *ui.Context, b ui.Rect) float32 { +// PopoverRoom is the tallest a panel below or above anchor b may be. +func PopoverRoom(c *ui.Context, b ui.Rect) float32 { _, h := c.Size() return max(b.Y, h-b.Y-b.H) - 16 } -// popoverWide caps a panel width w to the window. -func popoverWide(c *ui.Context, w float32) float32 { +// PopoverWide caps a panel width w to the window. +func PopoverWide(c *ui.Context, w float32) float32 { ww, _ := c.Size() return min(w, ww-8) } diff --git a/popover_test.go b/overlay/popover_test.go similarity index 82% rename from popover_test.go rename to overlay/popover_test.go index db3887e..323199f 100644 --- a/popover_test.go +++ b/overlay/popover_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "strings" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) type popoverApp struct { @@ -15,18 +18,18 @@ type popoverApp struct { } func (a *popoverApp) view(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(a.top, 0, 0, 40).Gap(8).Children(func() { - b := Button(c, "Filter", ButtonOptions{}) + b := input.Button(c, "Filter", input.ButtonOptions{}) if b.Clicked() { a.open = !a.open } a.anchor = b.Bounds() - if Button(c, "Behind", ButtonOptions{}).Clicked() { + if input.Button(c, "Behind", input.ButtonOptions{}).Clicked() { a.behind++ } if p := Popover(c, b, &a.open, PopoverOptions{Title: "Filter rows"}, func() { - if Button(c, "Apply", ButtonOptions{}).Clicked() { + if input.Button(c, "Apply", input.ButtonOptions{}).Clicked() { a.applied++ a.open = false } @@ -81,12 +84,12 @@ func TestPopoverFitsWindow(t *testing.T) { open := true var panel ui.Rect tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(30).Children(func() { - b := Button(c, "Filter", ButtonOptions{}) + b := input.Button(c, "Filter", input.ButtonOptions{}) if p := Popover(c, b, &open, PopoverOptions{Width: 1000}, func() { ui.Text(c, strings.Repeat("Long row of guests. ", 200)) - Button(c, "Bottom", ButtonOptions{}) + input.Button(c, "Bottom", input.ButtonOptions{}) }); p != nil { panel = p.Bounds() } @@ -106,7 +109,7 @@ func TestPopoverNeedsAnchor(t *testing.T) { }() open := true ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) Popover(c, nil, &open, PopoverOptions{}, nil) }, 200, 200) } @@ -115,9 +118,9 @@ func TestPopoverSnapshot(t *testing.T) { snapshot(t, "popover", 420, 260, func(c *ui.Context) { open := true ui.Column(c).Padding(24).AlignItems(ui.Start).Children(func() { - b := Button(c, "Guests", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Guests", input.ButtonOptions{Variant: input.Secondary}) Popover(c, b, &open, PopoverOptions{Title: "Guest list", Width: 240}, func() { - ui.Text(c, "Twelve guests confirmed; three await a reply.").TextColor(Tokens(c).TextMuted) + ui.Text(c, "Twelve guests confirmed; three await a reply.").TextColor(core.Tokens(c).TextMuted) }) }) }) diff --git a/popup_menu.go b/overlay/popup_menu.go similarity index 86% rename from popup_menu.go rename to overlay/popup_menu.go index 24650cc..793c429 100644 --- a/popup_menu.go +++ b/overlay/popup_menu.go @@ -1,13 +1,15 @@ -package mujica +package overlay import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PopupMenuItemOptions configure one item of a PopupMenu. @@ -89,28 +91,28 @@ func (m *PopupMenu) Submenu(label string, opts PopupMenuItemOptions, build func( // Group adds a captioned group of items, separated from what comes before. func (m *PopupMenu) Group(label string, fn func()) { - k := Tokens(m.c) - u := Density(m.c).Unit() + k := core.Tokens(m.c) + u := core.Density(m.c).Unit() if m.rows > 0 { m.Separator() } if label != "" { - ui.Text(m.c, label).FontSize(FontSize(m.c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(u, u*3, u/2, u*3+16+u*2).Shrink(0) + ui.Text(m.c, label).FontSize(core.FontSize(m.c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(u, u*3, u/2, u*3+16+u*2).Shrink(0) } fn() } // Separator adds a line between items. func (m *PopupMenu) Separator() { - u := Density(m.c).Unit() - ui.Box(m.c).Height(theme.BorderWidth).Margin(u, u*2).Background(Tokens(m.c).Border).Shrink(0) + u := core.Density(m.c).Unit() + ui.Box(m.c).Height(theme.BorderWidth).Margin(u, u*2).Background(core.Tokens(m.c).Border).Shrink(0) } // row styles an item element and runs the hover, focus and highlight logic. func (m *PopupMenu) row(it *ui.Element, disabled, sub bool) *ui.Element { c := m.c - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) m.seq++ m.rows++ seq := m.seq @@ -146,7 +148,7 @@ func (m *PopupMenu) row(it *ui.Element, disabled, sub bool) *ui.Element { // content lays out the check column, label and submenu chevron of an item. func (m *PopupMenu) content(it *ui.Element, label string, checked, sub bool) { c := m.c - k := Tokens(c) + k := core.Tokens(c) it.Children(func() { mark := ui.Box(c).Size(16, 16).Shrink(0) if checked { @@ -159,17 +161,17 @@ func (m *PopupMenu) content(it *ui.Element, label string, checked, sub bool) { }) } -// popupMenuOpen shows the menu build fills below anchor while *open is true. -func popupMenuOpen(c *ui.Context, anchor *ui.Element, open *bool, build func(m *PopupMenu)) *ui.Element { +// PopupMenuOpen shows the menu build fills below anchor while *open is true. +func PopupMenuOpen(c *ui.Context, anchor *ui.Element, open *bool, build func(m *PopupMenu)) *ui.Element { if !*open { return nil } - overlayLayer(c, open) + core.OverlayLayer(c, open) b := anchor.Bounds() root := &popupMenuRoot{} panel := ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { - overlayStyle(c, panel, overlayAnchored(c, panel, b)) - panel.Margin(4, 0, 0, 0).MinWidth(popoverWide(c, max(b.W, 200))).MaxWidth(popoverWide(c, 360)).MaxHeight(popoverRoom(c, b)) + core.OverlayStyle(c, panel, OverlayAnchored(c, panel, b)) + panel.Margin(4, 0, 0, 0).MinWidth(PopoverWide(c, max(b.W, 200))).MaxWidth(PopoverWide(c, 360)).MaxHeight(PopoverRoom(c, b)) popupMenuLevelBuild(c, panel, root, build, true) // The panel holds every level, so Tab in a submenu closes the whole menu too. if panel.Shortcut(0, ui.KeyTab) || panel.Shortcut(ui.Shift, ui.KeyTab) { @@ -187,7 +189,7 @@ func popupMenuOpen(c *ui.Context, anchor *ui.Element, open *bool, build func(m * // popupMenuLevelBuild builds one level's items in outer, then its open submenu. func popupMenuLevelBuild(c *ui.Context, outer *ui.Element, root *popupMenuRoot, build func(m *PopupMenu), autofocus bool) { - u := Density(c).Unit() + u := core.Density(c).Unit() // The padding leaves room for the items' focus rings inside the clipping scroll. items := ui.Scroll(c).Shrink(1).Padding(u + theme.FocusGap + theme.FocusWidth).FocusGroup(ui.Vertical) m := &PopupMenu{c: c, root: root, autofocus: autofocus} @@ -203,7 +205,7 @@ func popupMenuLevelBuild(c *ui.Context, outer *ui.Element, root *popupMenuRoot, // popupMenuSubmenu builds a submenu beside its item, flipped to stay in the window. func popupMenuSubmenu(c *ui.Context, outer *ui.Element, root *popupMenuRoot, lv *popupMenuLevel, s popupMenuSub) { - u := Density(c).Unit() + u := core.Density(c).Unit() ob, ib := outer.Bounds(), s.item.Bounds() ww, wh := c.Size() box := ui.Column(c).Absolute().Key(s.seq).MinWidth(min(180, ww-8)).MaxWidth(min(360, ww-8)) @@ -216,12 +218,12 @@ func popupMenuSubmenu(c *ui.Context, outer *ui.Element, root *popupMenuRoot, lv left = max(4-ob.X, min(left, ww-4-sb.W-ob.X)) top = max(4-ob.Y, min(top, wh-4-sb.H-ob.Y)) box.Left(left).Top(top).MaxHeight(wh - 8) - p := overlayEnter(c, box) + p := core.OverlayEnter(c, box) if sb.W == 0 || ob.W == 0 { p = 0 // not measured yet c.AnimationFrame() } - overlayStyle(c, box, p) + core.OverlayStyle(c, box, p) box.Children(func() { popupMenuLevelBuild(c, box, root, s.build, lv.byKey) }) if box.Shortcut(0, ui.KeyLeft) || box.Shortcut(0, ui.KeyEscape) { lv.open = 0 @@ -231,12 +233,12 @@ func popupMenuSubmenu(c *ui.Context, outer *ui.Element, root *popupMenuRoot, lv // popupMenuTrigger styles a menu trigger button with a trailing chevron. func popupMenuTrigger(c *ui.Context, b *ui.Element, label string, icon *ui.SVG, open bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) b.MinHeight(d.ControlHeight()).MinWidth(theme.MinHitSize).Padding(0, d.Unit()*3, 0, d.Unit()*4). Gap(d.Unit() * 2).Radius(theme.ControlRadius) internal.Ring(b, k.Focus, theme.ControlRadius) - bg, fg, border := buttonColors(k, Secondary, b.Hovered() && !b.IsDisabled(), b.Pressed() || open) + bg, fg, border := input.ButtonColors(k, input.Secondary, b.Hovered() && !b.IsDisabled(), b.Pressed() || open) b.Background(bg).TextColor(fg).Border(theme.BorderWidth, border) b.Children(func() { if icon != nil { diff --git a/split_button.go b/overlay/split_button.go similarity index 73% rename from split_button.go rename to overlay/split_button.go index 200d60a..4a5411f 100644 --- a/split_button.go +++ b/overlay/split_button.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "fmt" @@ -6,15 +6,17 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SplitButtonOptions configure a SplitButton. type SplitButtonOptions struct { // Variant is Primary (the zero value) or Secondary. - Variant ButtonVariant + Variant input.ButtonVariant Icon *ui.SVG // MenuLabel names the menu half; empty uses "More options". MenuLabel string @@ -22,18 +24,18 @@ type SplitButtonOptions struct { } // SplitButton runs its main action from the left half and opens build's menu from the right; each half takes focus. -func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build func(m *PopupMenu)) ChoiceResult { +func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build func(m *PopupMenu)) core.ChoiceResult { if label == "" || build == nil { panic("mujica: SplitButton needs a label and a menu") } - if opts.Variant != Primary && opts.Variant != Secondary { + if opts.Variant != input.Primary && opts.Variant != input.Secondary { panic(fmt.Sprintf("mujica: SplitButton Variant %d must be Primary or Secondary", opts.Variant)) } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) menuName := opts.MenuLabel if menuName == "" { - menuName = msg(c, "split_button.more") + menuName = core.Msg(c, "split_button.more") } group := ui.Row(c).AlignItems(ui.Stretch).Disabled(opts.Disabled) disabled := group.IsDisabled() @@ -53,13 +55,13 @@ func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build fun ui.Text(c, label).SingleLine() }) clicked = main.Clicked() && !disabled - if opts.Variant == Primary { + if opts.Variant == input.Primary { ui.Box(c).Width(theme.BorderWidth).Background(k.OnAccent.Alpha(0.45)).Role(ui.RoleNone) } more := ui.ButtonBase(c).Role(ui.RoleMenuButton).Label(menuName).MinHeight(d.ControlHeight()).Width(theme.MinHitSize+d.Unit()). Radius(0, theme.ControlRadius, theme.ControlRadius, 0).Tooltip(menuName) splitButtonLook(c, k, more, opts.Variant, *open) - if opts.Variant == Secondary { + if opts.Variant == input.Secondary { more.BorderWidth(theme.BorderWidth, theme.BorderWidth, theme.BorderWidth, 0) } more.Children(func() { ui.Icon(c, icons.Must("chevron-down")).Size(14, 14) }) @@ -67,19 +69,19 @@ func SplitButton(c *ui.Context, label string, opts SplitButtonOptions, build fun *open = true slog.Debug("mujica: split_button open") } - popupMenuOpen(c, group, open, build) + PopupMenuOpen(c, group, open, build) }) if clicked { slog.Debug("mujica: split_button action", "label", label) } - return ChoiceResult{group, clicked} + return core.NewChoiceResult(group, clicked) } // splitButtonLook colors one half as a Button of variant v would be. -func splitButtonLook(c *ui.Context, k theme.Tokens, b *ui.Element, v ButtonVariant, open bool) { +func splitButtonLook(c *ui.Context, k theme.Tokens, b *ui.Element, v input.ButtonVariant, open bool) { internal.Ring(b, k.Focus, theme.ControlRadius) - bg, fg, border := buttonColors(k, v, b.Hovered() && !b.IsDisabled(), b.Pressed() || open) - b.Background(bg).TextColor(fg).Transition(colorFade) + bg, fg, border := input.ButtonColors(k, v, b.Hovered() && !b.IsDisabled(), b.Pressed() || open) + b.Background(bg).TextColor(fg).Transition(core.ColorFade) if border.A > 0 { b.Border(theme.BorderWidth, border) } diff --git a/split_button_test.go b/overlay/split_button_test.go similarity index 88% rename from split_button_test.go rename to overlay/split_button_test.go index 166d2c1..d9432dd 100644 --- a/split_button_test.go +++ b/overlay/split_button_test.go @@ -1,14 +1,17 @@ -package mujica +package overlay import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) func splitButtonTester(dis *bool, runs *int, chosen *string) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Padding(20).Children(func() { r := SplitButton(c, "Save", SplitButtonOptions{Disabled: *dis}, func(m *PopupMenu) { for _, s := range []string{"Save as…", "Save all"} { @@ -82,8 +85,8 @@ func TestSplitButtonDisabled(t *testing.T) { } }() ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) - SplitButton(c, "x", SplitButtonOptions{Variant: Danger}, func(*PopupMenu) {}) + core.Use(c, core.Settings{}) + SplitButton(c, "x", SplitButtonOptions{Variant: input.Danger}, func(*PopupMenu) {}) }, 200, 100) } @@ -92,7 +95,7 @@ func TestSplitButtonSnapshot(t *testing.T) { ui.Row(c).Padding(16).Gap(12).Children(func() { menu := func(m *PopupMenu) { m.Item("Other", PopupMenuItemOptions{}) } SplitButton(c, "Publish", SplitButtonOptions{}, menu) - SplitButton(c, "Export", SplitButtonOptions{Variant: Secondary}, menu) + SplitButton(c, "Export", SplitButtonOptions{Variant: input.Secondary}, menu) SplitButton(c, "Merge", SplitButtonOptions{Disabled: true}, menu) }) }) diff --git a/tooltip.go b/overlay/tooltip.go similarity index 69% rename from tooltip.go rename to overlay/tooltip.go index 95bbbd8..b75fbf3 100644 --- a/tooltip.go +++ b/overlay/tooltip.go @@ -1,4 +1,4 @@ -package mujica +package overlay import ( "log/slog" @@ -6,7 +6,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TooltipOptions configure a Tooltip. @@ -15,10 +16,10 @@ type TooltipOptions struct { Delay time.Duration } -// tooltipState tracks when the anchor became hot and whether Escape hid the tip. -type tooltipState struct { - since time.Time - dismissed bool +// TooltipState tracks when the anchor became hot and whether Escape hid the tip. +type TooltipState struct { + Since time.Time + Dismissed bool } const tooltipMaxWidth = 240 @@ -35,29 +36,29 @@ func Tooltip(c *ui.Context, anchor *ui.Element, text string, opts TooltipOptions return anchor } anchor.Description(text) - st := ui.Local(anchor, "mujica.tooltip", func() tooltipState { return tooltipState{} }) + st := ui.Local(anchor, "mujica.tooltip", func() TooltipState { return TooltipState{} }) hot := anchor.Hovered() || anchor.FocusVisible() if !hot { - st.since, st.dismissed = time.Time{}, false + st.Since, st.Dismissed = time.Time{}, false return anchor } - if st.dismissed { + if st.Dismissed { return anchor } - if st.since.IsZero() { - st.since = c.Now() + if st.Since.IsZero() { + st.Since = c.Now() } delay := opts.Delay if delay == 0 { delay = 500 * time.Millisecond } - if wait := delay - c.Now().Sub(st.since); wait > 0 { + if wait := delay - c.Now().Sub(st.Since); wait > 0 { c.After(wait) return anchor } - if overlayEscape(c, st) { + if core.OverlayEscape(c, st) { slog.Debug("mujica: tooltip dismiss") - st.dismissed = true + st.Dismissed = true return anchor } tooltipShow(c, anchor.Bounds(), text) @@ -66,13 +67,13 @@ func Tooltip(c *ui.Context, anchor *ui.Element, text string, opts TooltipOptions // tooltipShow draws the tip below a, or above it when there is no room. func tooltipShow(c *ui.Context, a ui.Rect, text string) { - k := Tokens(c) - u := Density(c).Unit() - size := FontSize(c, theme.CaptionSize+1) + k := core.Tokens(c) + u := core.Density(c).Unit() + size := core.FontSize(c, theme.CaptionSize+1) tw, th := c.MeasureText(tooltipMaxWidth, ui.Span{Text: text, Size: size, Font: c.Theme().Font}) padX, padY := u*2.5, u*1.5 w, h := tw+2*(padX+theme.BorderWidth)+1, th+2*(padY+theme.BorderWidth) - x, y := overlayPlace(c, ui.Rect{X: a.X + a.W/2 - w/2, Y: a.Y, W: w, H: a.H}, w, h, 6) + x, y := core.PlaceOverlay(c, ui.Rect{X: a.X + a.W/2 - w/2, Y: a.Y, W: w, H: a.H}, w, h, 6) ui.Overlay(c, func() { ui.Box(c).Absolute().Left(x).Top(y).Width(w).Padding(padY, padX).Radius(theme.ControlRadius). Background(k.Text).TextColor(k.Background).Border(theme.BorderWidth, k.Text). diff --git a/tooltip_test.go b/overlay/tooltip_test.go similarity index 91% rename from tooltip_test.go rename to overlay/tooltip_test.go index 3114530..9638586 100644 --- a/tooltip_test.go +++ b/overlay/tooltip_test.go @@ -1,10 +1,13 @@ -package mujica +package overlay import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" ) const tooltipTestDelay = 150 * time.Millisecond @@ -12,9 +15,9 @@ const tooltipTestDelay = 150 * time.Millisecond func tooltipTester(x, y float32) (*ui.Tester, *int) { clicks := 0 tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(y, 0, 0, x).Children(func() { - b := Button(c, "Share", ButtonOptions{}) + b := input.Button(c, "Share", input.ButtonOptions{}) if b.Clicked() { clicks++ } @@ -101,7 +104,7 @@ func TestTooltipScalesWithText(t *testing.T) { func TestTooltipSnapshot(t *testing.T) { snapshotAfter(t, "tooltip", 360, 140, func(c *ui.Context) { ui.Column(c).Padding(24).AlignItems(ui.Start).Children(func() { - b := Button(c, "Seal", ButtonOptions{Variant: Secondary}) + b := input.Button(c, "Seal", input.ButtonOptions{Variant: input.Secondary}) Tooltip(c, b, "Seal the letter with wax", TooltipOptions{Delay: time.Millisecond}) }) }, func(tt *ui.Tester) { diff --git a/price_text.go b/price_text.go deleted file mode 100644 index f415a16..0000000 --- a/price_text.go +++ /dev/null @@ -1,171 +0,0 @@ -package mujica - -import ( - "fmt" - "log/slog" - "math" - "time" - - "github.com/egoist/mygo/ui" - - "mujicaui/icons" - "mujicaui/theme" -) - -// Trend picks the rise color: green, or red (China). -type Trend int - -const ( - GreenUp Trend = iota - RedUp -) - -type trendKey struct{} - -// UseTrend sets the window's rise and fall colors. -func UseTrend(c *ui.Context, t Trend) { - if t != GreenUp && t != RedUp { - panic(fmt.Sprintf("mujica: unknown Trend %d", t)) - } - *ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) = t -} - -// trendColor colors a rise (sign > 0), fall or flat. -func trendColor(c *ui.Context, sign int) ui.Color { - k := Tokens(c) - up, down := k.Success, k.Danger - if *ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) == RedUp { - up, down = k.Danger, k.Success - } - switch { - case sign > 0: - return up - case sign < 0: - return down - } - return k.TextMuted -} - -// trendIcon is the arrow that says the direction without color. -func trendIcon(sign int) *ui.SVG { - switch { - case sign > 0: - return icons.Must("arrow-up") - case sign < 0: - return icons.Must("arrow-down") - } - return icons.Must("minus") -} - -// trendWord names the direction for assistive technology. -func trendWord(c *ui.Context, sign int) string { - switch { - case sign > 0: - return msg(c, "price_text.up") - case sign < 0: - return msg(c, "price_text.down") - } - return msg(c, "price_text.flat") -} - -func trendSign(v float64) int { - switch { - case v > 0: - return 1 - case v < 0: - return -1 - } - return 0 -} - -// priceDecimals resolves a Decimals option: zero means 2, negative panics. -func priceDecimals(name string, d int) int { - if d < 0 { - panic("mujica: " + name + " with negative Decimals") - } - if d == 0 { - return 2 - } - return d -} - -// priceTextFlash is the tint's fade, three state steps long. -const priceTextFlash = 3 * theme.StateDuration - -// PriceTextOptions configure a PriceText. -type PriceTextOptions struct { - // Decimals shown; zero means 2. - Decimals int - // NoGrouping drops the thousands separators. - NoGrouping bool - // Compare marks the price's move from Reference. - Compare bool - Reference float64 - // NoFlash keeps the price still when it changes. - NoFlash bool - // Size is the font size; zero is the body size. - Size float32 -} - -type priceTextState struct { - last float64 - seen bool - at time.Time - dir int -} - -// PriceText shows a price; a change tints it once. -func PriceText(c *ui.Context, value float64, opts PriceTextOptions) *ui.Element { - dec := priceDecimals("PriceText", opts.Decimals) - if opts.Size < 0 { - panic("mujica: PriceText with negative Size") - } - size := opts.Size - if size == 0 { - size = theme.BodySize - } - text := statisticFormat(value, dec) - if opts.NoGrouping { - text = priceUngrouped(value, dec) - } - e := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Padding(0, 2).Radius(theme.ControlRadius).Shrink(0) - st := ui.Local(e, "price", func() priceTextState { return priceTextState{} }) - if st.seen && value != st.last { - st.at, st.dir = c.Now(), trendSign(value-st.last) - slog.Debug("mujica: price_text changed", "dir", st.dir) - } - st.last, st.seen = value, true - sign := 0 - if opts.Compare { - sign = trendSign(value - opts.Reference) - } - col := Tokens(c).Text - spoken := text - if opts.Compare { - col = trendColor(c, sign) - spoken += ", " + trendWord(c, sign) - } - if d := Motion(c, priceTextFlash); d > 0 && !opts.NoFlash && !st.at.IsZero() { - if t := c.Now().Sub(st.at); t < d { - e.Background(trendColor(c, st.dir).Alpha(0.2 * float32(1-float64(t)/float64(d)))) - c.AnimationFrame() - } - } - e.Role(ui.RoleText).Label(spoken).TextColor(col) - e.Children(func() { - if opts.Compare { - ui.Icon(c, trendIcon(sign)).Size(size*0.85, size*0.85) - } - ui.Text(c, text).FontSize(FontSize(c, size)).FontFeatures("tnum").FontWeight(500).SingleLine() - }) - return e -} - -// priceUngrouped writes v with dec decimals and no separators. -func priceUngrouped(v float64, dec int) string { - s := fmt.Sprintf("%.*f", dec, math.Abs(v)) - if v < 0 && s != fmt.Sprintf("%.*f", dec, 0.0) { - return "−" + s - } - return s -} diff --git a/assignee_picker.go b/project/assignee_picker.go similarity index 75% rename from assignee_picker.go rename to project/assignee_picker.go index b42543b..741f7bb 100644 --- a/assignee_picker.go +++ b/project/assignee_picker.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,8 +7,12 @@ import ( "strings" "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // AssigneePickerOptions configure an AssigneePicker. @@ -26,7 +30,7 @@ type assigneeState struct { n, version int ready, open bool query, filtered string - options, shown []SelectOption[string] + options, shown []input.SelectOption[string] members map[string]ProjectMember selected map[string]bool selectedFirst *string @@ -36,14 +40,14 @@ type assigneeState struct { } // AssigneePicker searches and selects members, showing their avatars. -func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts AssigneePickerOptions) ChoiceResult { +func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts AssigneePickerOptions) core.ChoiceResult { if opts.Max < 0 { panic("mujica: AssigneePicker Max must be non-negative") } - d, k := Density(c), Tokens(c) + d, k := core.Density(c), core.Tokens(c) label := opts.Label if label == "" { - label = msg(c, "assignee_picker.label") + label = core.Msg(c, "assignee_picker.label") } row := ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Disabled(opts.Disabled) st := ui.Local(row, "assignees", func() assigneeState { return assigneeState{} }) @@ -59,13 +63,13 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts if dataChanged { st.first, st.n, st.version, st.ready = first, len(members), opts.Version, true st.members = make(map[string]ProjectMember, len(members)) - st.options = make([]SelectOption[string], len(members)) + st.options = make([]input.SelectOption[string], len(members)) for i, m := range members { if _, exists := st.members[m.ID]; exists || m.ID == "" { panic("mujica: AssigneePicker needs unique non-empty IDs") } st.members[m.ID] = m - st.options[i] = SelectOption[string]{Value: m.ID, Label: m.Name} + st.options[i] = input.SelectOption[string]{Value: m.ID, Label: m.Name} } st.shown = nil } @@ -73,11 +77,11 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts var trigger *ui.Element row.Children(func() { if len(*ids) > 0 { - items := make([]AvatarItem, min(3, len(*ids))) + items := make([]display.AvatarItem, min(3, len(*ids))) for i := range items { - items[i] = AvatarItem{Name: st.members[(*ids)[i]].Name} + items[i] = display.AvatarItem{Name: st.members[(*ids)[i]].Name} } - AvatarGroup(c, items, AvatarGroupOptions{Max: 3, Size: 28}) + display.AvatarGroup(c, items, display.AvatarGroupOptions{Max: 3, Size: 28}) } trigger = ui.ButtonBase(c).Disabled(row.IsDisabled()).Role(ui.RolePopUpButton).Label(label) if trigger.IsDisabled() { @@ -89,22 +93,22 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts if len(*ids) > 0 && !trigger.IsDisabled() && trigger.Shortcut(0, ui.KeyBackspace) { *ids = slices.Clone((*ids)[:len(*ids)-1]) } - selectField(c, trigger, st.open, func() { - options := make([]SelectOption[string], min(2, len(*ids))) + input.SelectField(c, trigger, st.open, func() { + options := make([]input.SelectOption[string], min(2, len(*ids))) for i := range options { - options[i] = SelectOption[string]{Value: (*ids)[i], Label: st.members[(*ids)[i]].Name} + options[i] = input.SelectOption[string]{Value: (*ids)[i], Label: st.members[(*ids)[i]].Name} } - multiSelectTags(c, ids, options, MultiSelectOptions{MaxTags: 2, Disabled: trigger.IsDisabled(), Placeholder: msg(c, "assignee_picker.placeholder")}) + input.MultiSelectTags(c, ids, options, input.MultiSelectOptions{MaxTags: 2, Disabled: trigger.IsDisabled(), Placeholder: core.Msg(c, "assignee_picker.placeholder")}) }) ui.PopoverBase(c, trigger, &st.open, func(panel *ui.Element) { - selectPanel(c, panel) + input.SelectPanel(c, panel) ww, wh := c.Size() panel.MinWidth(min(ww-8, trigger.Bounds().W)).MaxWidth(max(1, ww-8)).MaxHeight(max(1, wh-8)) ui.Scroll(c).MaxHeight(max(1, wh-24)).Children(func() { - search := selectSearch(c, &st.query, msg(c, "multi_select.search")) + search := input.SelectSearch(c, &st.query, core.Msg(c, "multi_select.search")) q := strings.ToLower(strings.TrimSpace(st.query)) if st.shown == nil || st.filtered != q { - st.shown = make([]SelectOption[string], 0) + st.shown = make([]input.SelectOption[string], 0) for _, o := range st.options { if strings.Contains(strings.ToLower(o.Label), q) { st.shown = append(st.shown, o) @@ -133,7 +137,7 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts } } if len(st.shown) == 0 { - selectNote(c, msg(c, "multi_select.no_results")) + input.SelectNote(c, core.Msg(c, "multi_select.no_results")) return } on, full := st.count == len(st.shown), opts.Max > 0 && len(*ids) >= opts.Max @@ -144,12 +148,14 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts mark = internal.Mixed } var all *ui.Element - ui.Row(c).MinHeight(d.ControlHeight()).Children(func() { all = multiSelectCheck(c, k, &on, mark, msg(c, "multi_select.all"), full && st.count == 0) }) + ui.Row(c).MinHeight(d.ControlHeight()).Children(func() { + all = input.MultiSelectCheck(c, k, &on, mark, core.Msg(c, "multi_select.all"), full && st.count == 0) + }) if search.Shortcut(0, ui.KeyDown) { all.Focus() } if all.Changed() && !all.IsDisabled() { - *ids = multiSelectAll(*ids, st.shown, on, opts.Max) + *ids = input.MultiSelectAll(*ids, st.shown, on, opts.Max) } if all.Shortcut(0, ui.KeyDown) && !all.IsDisabled() { for i, o := range st.shown { @@ -171,13 +177,13 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts state = internal.Checked } var cb *ui.Element - ui.Row(c).MinHeight(d.ControlHeight()).Children(func() { cb = multiSelectCheck(c, k, &selected, state, o.Label, full && !selected) }) + ui.Row(c).MinHeight(d.ControlHeight()).Children(func() { cb = input.MultiSelectCheck(c, k, &selected, state, o.Label, full && !selected) }) if st.focus == o.Value && !cb.IsDisabled() { cb.Focus() st.focus = "" } if cb.Changed() && !cb.IsDisabled() { - *ids = multiSelectSet(*ids, o.Value, selected) + *ids = input.MultiSelectSet(*ids, o.Value, selected) } if cb.IsDisabled() { return @@ -200,7 +206,7 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts } }).Height(min(240, min(float32(len(st.shown))*d.ControlHeight(), max(d.ControlHeight(), wh-140)))).Padding(4) if opts.Max > 0 { - ui.Text(c, fmt.Sprintf(msg(c, "multi_select.limit"), len(*ids), opts.Max)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "multi_select.limit"), len(*ids), opts.Max)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) } }) }) @@ -218,7 +224,7 @@ func AssigneePicker(c *ui.Context, ids *[]string, members []ProjectMember, opts slog.Debug("mujica: assignee_picker change", "count", len(*ids)) c.Invalidate() } - return ChoiceResult{Element: row, changed: changed} + return core.NewChoiceResult(row, changed) } // assigneeNext skips choices disabled by the selection limit. diff --git a/assignee_picker_test.go b/project/assignee_picker_test.go similarity index 91% rename from assignee_picker_test.go rename to project/assignee_picker_test.go index 6a0d39c..2f2978f 100644 --- a/assignee_picker_test.go +++ b/project/assignee_picker_test.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -6,12 +6,14 @@ import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func assigneeTester(ids *[]string, opts *AssigneePickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if AssigneePicker(c, ids, projMembers, *opts).Changed() { *n++ @@ -66,7 +68,7 @@ func TestAssigneePickerSnapshot(t *testing.T) { func TestAssigneePickerAncestorDisableCloses(t *testing.T) { ids, disabled := []string{}, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { AssigneePicker(c, &ids, projMembers, AssigneePickerOptions{}) }) }, 500, 520) if err := tt.Click("Assignees"); err != nil { @@ -88,7 +90,10 @@ func TestAssigneePickerLargeData(t *testing.T) { members[i] = ProjectMember{ID: fmt.Sprint(i), Name: fmt.Sprintf("Member %d", i)} } var ids []string - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); AssigneePicker(c, &ids, members, AssigneePickerOptions{}) }, 500, 520) + tt := ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + AssigneePicker(c, &ids, members, AssigneePickerOptions{}) + }, 500, 520) if err := tt.Click("Assignees"); err != nil { t.Fatal(err) } @@ -139,7 +144,7 @@ func TestAssigneePickerVersionRefreshAndBulkClear(t *testing.T) { func TestAssigneePickerCompactHitAreas(t *testing.T) { var ids []string tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: theme.Compact}) + core.Use(c, core.Settings{Density: theme.Compact}) ui.Column(c).Padding(16).Children(func() { AssigneePicker(c, &ids, []ProjectMember{{ID: "only", Name: "Only Member"}}, AssigneePickerOptions{}) }) diff --git a/burndown_chart.go b/project/burndown_chart.go similarity index 80% rename from burndown_chart.go rename to project/burndown_chart.go index 61caf65..63cee57 100644 --- a/burndown_chart.go +++ b/project/burndown_chart.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -9,15 +9,17 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // BurndownChartOptions configure a BurndownChart. type BurndownChartOptions struct { - Frame ChartFrame + Frame chart.ChartFrame // Start and End are the sprint's first and last days. - Start, End Date + Start, End core.Date // Total starts the ideal line, which reaches zero. Total float64 // Format writes amounts; nil uses up to two decimals. @@ -27,13 +29,13 @@ type BurndownChartOptions struct { } // BurndownChart compares remaining work with the ideal line. -func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions) ChartResult { - datetimeCheckDate("BurndownChart Start", opts.Start) - datetimeCheckDate("BurndownChart End", opts.End) +func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions) chart.ChartResult { + core.DatetimeCheckDate("BurndownChart Start", opts.Start) + core.DatetimeCheckDate("BurndownChart End", opts.End) if opts.Start.IsZero() || opts.End.Compare(opts.Start) <= 0 || !(opts.Total > 0) || math.IsInf(opts.Total, 0) { panic(fmt.Sprintf("mujica: BurndownChart needs Start < End and a positive Total, got %v..%v %v", opts.Start, opts.End, opts.Total)) } - days := int((opts.End.civil().Unix()-opts.Start.civil().Unix())/86400) + 1 + days := int((opts.End.Civil().Unix()-opts.Start.Civil().Unix())/86400) + 1 if len(remaining) > days { panic(fmt.Sprintf("mujica: BurndownChart has %d values for %d days", len(remaining), days)) } @@ -43,12 +45,12 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions return strings.TrimRight(strings.TrimRight(strconv.FormatFloat(v, 'f', 2, 64), "0"), ".") } } - ch := newChart(c, opts.Frame) - summary := chartMemo(ch, "burndown-domain", struct { + ch := chart.NewChart(c, opts.Frame) + summary := chart.ChartMemo(ch, "burndown-domain", struct { data string total float64 version int - }{chartID(remaining), opts.Total, opts.Version}, func() float64 { + }{chart.ChartID(remaining), opts.Total, opts.Version}, func() float64 { hi := opts.Total for _, v := range remaining { if v < 0 || math.IsInf(v, 0) { @@ -60,10 +62,10 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions } return hi }) - cache := ui.Local(ch.res.Element, "burndown-samples", func() burndownCache { return burndownCache{} }) - k := Tokens(c) + cache := ui.Local(ch.Res.Element, "burndown-samples", func() burndownCache { return burndownCache{} }) + k := core.Tokens(c) pal := internal.Palette(k) - size := FontSize(c, theme.CaptionSize) + size := core.FontSize(c, theme.CaptionSize) ideal := func(i int) float64 { return opts.Total * (1 - float64(i)/float64(days-1)) } actual := func(i int) float64 { if i < len(remaining) { @@ -73,9 +75,9 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions } lo, top, step := internal.Nice(0, summary, 4) ticks := internal.Ticks(lo, top, step) - keys := []ChartLegendItem{{Name: msg(c, "burndown_chart.actual"), Color: pal[0], Shape: 0}, {Name: msg(c, "burndown_chart.ideal"), Color: pal[5], Shape: 1}} + keys := []chart.ChartLegendItem{{Name: core.Msg(c, "burndown_chart.actual"), Color: pal[0], Shape: 0}, {Name: core.Msg(c, "burndown_chart.ideal"), Color: pal[5], Shape: 1}} geom := func(r ui.Rect) (ui.Rect, internal.Linear, internal.Linear) { - plot := chartInset(r, 48, 26) + plot := chart.ChartInset(r, 48, 26) return plot, internal.Linear{D0: 0, D1: float64(days - 1), R0: float64(plot.X), R1: float64(plot.X + plot.W)}, internal.Linear{D0: lo, D1: top, R0: float64(plot.Y + plot.H), R1: float64(plot.Y)} } @@ -83,7 +85,7 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions if len(remaining) == 0 { n = 0 } - spec := ChartSpec{Marks: n, Legend: keys, Hidden: ch.toggles(true)} + spec := chart.ChartSpec{Marks: n, Legend: keys, Hidden: ch.Toggles(true)} spec.Pick = func(r ui.Rect, x, y float32) int { plot, xs, _ := geom(r) if x < plot.X-8 || x > plot.X+plot.W+8 { @@ -93,11 +95,11 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions } spec.Readout = func(i int) string { day := opts.Start.AddDays(i) - parts := datetimeMonthName(c, day.Month) + " " + strconv.Itoa(day.Day) - if v := actual(i); !math.IsNaN(v) && !ch.hidden(keys[0].Name) { + parts := core.DatetimeMonthName(c, day.Month) + " " + strconv.Itoa(day.Day) + if v := actual(i); !math.IsNaN(v) && !ch.Hidden(keys[0].Name) { parts += " · " + keys[0].Name + " " + format(v) } - if !ch.hidden(keys[1].Name) { + if !ch.Hidden(keys[1].Name) { parts += " · " + keys[1].Name + " " + format(math.Round(ideal(i)*100)/100) } return parts @@ -116,16 +118,16 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions pt := func(i int, v float64) (float32, float32) { return internal.Snap(p, xs.Map(float64(i))), internal.Snap(p, ys.Map(v)) } - if !ch.hidden(keys[1].Name) { + if !ch.Hidden(keys[1].Name) { x0, y0 := pt(0, ideal(0)) x1, y1 := pt(days-1, 0) burndownDashed(p, x0, y0, x1, y1, keys[1].Color) } - if !ch.hidden(keys[0].Name) { + if !ch.Hidden(keys[0].Name) { var line ui.Path open := false width := max(1, int(plot.W*p.Scale())) - data := chartID(remaining) + data := chart.ChartID(remaining) if cache.data != data || cache.version != opts.Version || cache.days != days || cache.width != width { cache.data, cache.version, cache.days, cache.width = data, opts.Version, days, width cache.points = burndownSamples(remaining, days, width) @@ -149,7 +151,7 @@ func BurndownChart(c *ui.Context, remaining []float64, opts BurndownChartOptions p.Line(ax, plot.Y, ax, plot.Y+plot.H, 1, k.TextMuted) } } - res, _ := ch.build(spec) + res, _ := ch.Build(spec) return res } diff --git a/burndown_chart_test.go b/project/burndown_chart_test.go similarity index 79% rename from burndown_chart_test.go rename to project/burndown_chart_test.go index f9c00b8..782b510 100644 --- a/burndown_chart_test.go +++ b/project/burndown_chart_test.go @@ -1,10 +1,14 @@ -package mujica +package project import ( "math" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/data" ) var burndownRemaining = []float64{40, 38, 35, 35, 30, 26, math.NaN(), 20, 19} @@ -16,7 +20,7 @@ func burndownTester(remaining []float64, opts BurndownChartOptions, points *[]in opts.Total = 40 } return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Width(600).Children(func() { if r := BurndownChart(c, remaining, opts); r.Changed() { *points = append(*points, r.Point) @@ -57,7 +61,7 @@ func TestBurndownChartReadoutAndKeys(t *testing.T) { func TestBurndownChartEmptyAndInvalid(t *testing.T) { var points []int - tt := burndownTester(nil, BurndownChartOptions{Frame: ChartFrame{Status: DataLoading}}, &points) + tt := burndownTester(nil, BurndownChartOptions{Frame: chart.ChartFrame{Status: data.DataLoading}}, &points) if !tt.HasText("Loading") || tt.HasText("October") { t.Fatalf("loading state %v", tt.Texts()) } @@ -82,7 +86,7 @@ func TestBurndownChartEmptyAndInvalid(t *testing.T) { func TestBurndownChartSnapshot(t *testing.T) { snapshot(t, "burndown_chart", 620, 340, func(c *ui.Context) { ui.Column(c).Padding(16).Children(func() { - BurndownChart(c, burndownRemaining, BurndownChartOptions{Frame: ChartFrame{Label: "Burndown"}, Start: oct(1), End: oct(14), Total: 40}) + BurndownChart(c, burndownRemaining, BurndownChartOptions{Frame: chart.ChartFrame{Label: "Burndown"}, Start: oct(1), End: oct(14), Total: 40}) }) }) } @@ -107,14 +111,14 @@ func TestBurndownChartEmptyFailedDisabled(t *testing.T) { if !tt.HasText("No data") { t.Fatal("missing empty state") } - tt = burndownTester(nil, BurndownChartOptions{Frame: ChartFrame{Status: DataFailed, Error: "Unavailable"}}, &points) + tt = burndownTester(nil, BurndownChartOptions{Frame: chart.ChartFrame{Status: data.DataFailed, Error: "Unavailable"}}, &points) if !tt.HasText("Unavailable") { t.Fatal("missing error state") } tt = ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(true).Children(func() { - r := BurndownChart(c, burndownRemaining, BurndownChartOptions{Frame: ChartFrame{Label: "Burndown"}, Start: oct(1), End: oct(14), Total: 40}) + r := BurndownChart(c, burndownRemaining, BurndownChartOptions{Frame: chart.ChartFrame{Label: "Burndown"}, Start: oct(1), End: oct(14), Total: 40}) if _, ok := r.Toggled(); ok { t.Fatal("disabled legend toggled") } @@ -133,8 +137,8 @@ func TestBurndownChartLargeData(t *testing.T) { for i := range values { values[i] = float64(100000 - i) } - opts := BurndownChartOptions{Frame: ChartFrame{Label: "Large burndown"}, Start: Date{Year: 1900, Month: 1, Day: 1}, End: Date{Year: 1900, Month: 1, Day: 1}.AddDays(99999), Total: 100000} - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); BurndownChart(c, values, opts) }, 640, 360) + opts := BurndownChartOptions{Frame: chart.ChartFrame{Label: "Large burndown"}, Start: core.Date{Year: 1900, Month: 1, Day: 1}, End: core.Date{Year: 1900, Month: 1, Day: 1}.AddDays(99999), Total: 100000} + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); BurndownChart(c, values, opts) }, 640, 360) points := burndownSamples(values, 100000, 600) if len(points) > 2404 { t.Fatalf("built %d points", len(points)) diff --git a/project/doc.go b/project/doc.go new file mode 100644 index 0000000..23813f0 --- /dev/null +++ b/project/doc.go @@ -0,0 +1,2 @@ +// Package project holds MujicaUI project management: issues, boards and time tracking. +package project diff --git a/due_date_picker.go b/project/due_date_picker.go similarity index 56% rename from due_date_picker.go rename to project/due_date_picker.go index ed75472..4ee4ff6 100644 --- a/due_date_picker.go +++ b/project/due_date_picker.go @@ -1,53 +1,56 @@ -package mujica +package project import ( "log/slog" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // DueDatePickerOptions configure a DueDatePicker. type DueDatePickerOptions struct { // Done stops a past date from showing as overdue. Done bool - WeekStart WeekStart + WeekStart datetime.WeekStart // Today anchors shortcuts and overdue marks; zero uses today. - Today Date + Today core.Date Disabled bool Label string } // DueDatePicker combines date shortcuts, a calendar and overdue marks. -func DueDatePicker(c *ui.Context, value *Date, opts DueDatePickerOptions) DateTimeResult { - datetimeCheckDate("DueDatePicker value", *value) - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) - first := calendarFirstWeekday(c, opts.WeekStart) +func DueDatePicker(c *ui.Context, value *core.Date, opts DueDatePickerOptions) datetime.DateTimeResult { + core.DatetimeCheckDate("DueDatePicker value", *value) + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) + first := datetime.CalendarFirstWeekday(c, opts.WeekStart) nextWeek := today.AddDays(7 - (int(today.Weekday())-int(first)+7)%7) label := opts.Label if label == "" { - label = msg(c, "due_date_picker.label") + label = core.Msg(c, "due_date_picker.label") } old := *value col := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) - var r DateTimeResult + var r datetime.DateTimeResult col.Children(func() { ui.Row(c).Gap(d.Unit()).Wrap().Children(func() { for _, q := range []struct { key string - day Date + day core.Date }{{"due_date_picker.today", today}, {"due_date_picker.tomorrow", today.AddDays(1)}, {"due_date_picker.next_week", nextWeek}} { - variant := Secondary - text := msg(c, q.key) + variant := input.Secondary + text := core.Msg(c, q.key) if *value == q.day { - variant = Outline + variant = input.Outline text = "✓ " + text } - b := Button(c, text, ButtonOptions{Variant: variant}).Label(msg(c, q.key)) + b := input.Button(c, text, input.ButtonOptions{Variant: variant}).Label(core.Msg(c, q.key)) if *value == q.day { b.Background(k.Selection).TextColor(k.Text) } @@ -56,7 +59,7 @@ func DueDatePicker(c *ui.Context, value *Date, opts DueDatePickerOptions) DateTi } } }) - r = DatePicker(c, value, DatePickerOptions{Label: label, WeekStart: opts.WeekStart, Today: today, Clearable: true, Disabled: col.IsDisabled()}) + r = datetime.DatePicker(c, value, datetime.DatePickerOptions{Label: label, WeekStart: opts.WeekStart, Today: today, Clearable: true, Disabled: col.IsDisabled()}) if text, late := projectDueText(c, *value, today, opts.Done); text != "" { fg, icon := k.TextMuted, "calendar" if late { @@ -64,7 +67,7 @@ func DueDatePicker(c *ui.Context, value *Date, opts DueDatePickerOptions) DateTi } ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(fg).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)) }) } }) @@ -72,5 +75,5 @@ func DueDatePicker(c *ui.Context, value *Date, opts DueDatePickerOptions) DateTi if changed { slog.Debug("mujica: due_date_picker change", "from", old, "to", *value) } - return DateTimeResult{Element: col, Input: r.Input, changed: changed} + return datetime.NewDateTimeResult(col, r.Input, changed, false) } diff --git a/due_date_picker_test.go b/project/due_date_picker_test.go similarity index 90% rename from due_date_picker_test.go rename to project/due_date_picker_test.go index d2eb2a2..1a9fff9 100644 --- a/due_date_picker_test.go +++ b/project/due_date_picker_test.go @@ -1,14 +1,16 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) -func dueTester(v *Date, opts *DueDatePickerOptions, n *int) *ui.Tester { +func dueTester(v *core.Date, opts *DueDatePickerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := *opts o.Today = calendarToday ui.Column(c).Padding(16).Children(func() { @@ -20,7 +22,7 @@ func dueTester(v *Date, opts *DueDatePickerOptions, n *int) *ui.Tester { } func TestDueDatePickerShortcuts(t *testing.T) { - v, n := Date{}, 0 + v, n := core.Date{}, 0 tt := dueTester(&v, &DueDatePickerOptions{}, &n) _ = tt.Click("Tomorrow") if v != oct(6) || n != 1 || !tt.HasText("Due tomorrow") { diff --git a/project/example_projects_test.go b/project/example_projects_test.go new file mode 100644 index 0000000..d0c733c --- /dev/null +++ b/project/example_projects_test.go @@ -0,0 +1,247 @@ +package project_test + +import ( + "log" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/chart" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/project" +) + +func ExampleKanbanBoard() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.KanbanBoard(c, &tasks, project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}) + if move, ok := r.Moved(); ok { + log.Println("move", move.ID, move.To, move.Index) + } + if id, ok := r.Opened(); ok { + log.Println("open", id) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleTaskList() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} + filter := project.TaskShowAll + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.TaskList(c, tasks, &filter, project.TaskListOptions{GroupBy: project.TaskGroupStatus, Statuses: statuses}) + if id, ok := r.Changed(); ok { + log.Println("edit", id) + } + if id, ok := r.Opened(); ok { + log.Println("open", id) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleTaskItem() { + task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.TaskItem(c, &task, project.TaskItemOptions{}) + if id, ok := r.Changed(); ok { + log.Println("edit", id) + } + if id, ok := r.Opened(); ok { + log.Println("open", id) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleTaskDetailPanel() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.TaskDetailPanel(c, &task, project.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }}) + if r.Changed() { + log.Println("task edited", task.ID) + } + if r.Closed() { + log.Println("close", task.ID) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleSubtaskList() { + subtasks := []project.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.SubtaskList(c, &subtasks, project.SubtaskListOptions{}).Changed() { + log.Println("subtasks changed", len(subtasks)) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleSprintBoard() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + tasks := []project.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}} + sprint := project.Sprint{Name: "Release sprint", Goal: "Ship", Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.SprintBoard(c, sprint, &tasks, project.SprintBoardOptions{Board: project.KanbanBoardOptions{Columns: []project.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}}) + if move, ok := r.Moved(); ok { + log.Println("move", move.ID, move.To) + } + if id, ok := r.Opened(); ok { + log.Println("open", id) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleBurndownChart() { + remaining := []float64{40, 32, 25, 18} + opts := project.BurndownChartOptions{Frame: chart.ChartFrame{Label: "Release burndown"}, Start: core.Date{Year: 2026, Month: 10, Day: 1}, End: core.Date{Year: 2026, Month: 10, Day: 14}, Total: 40} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.BurndownChart(c, remaining, opts) + if r.Changed() { + log.Println("point", r.Point) + } + if name, ok := r.Toggled(); ok { + log.Println("series", name) + } + if r.Retried() { + log.Println("retry") + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExamplePriorityIndicator() { + priority := project.PriorityHigh + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + project.PriorityIndicator(c, priority, project.PriorityIndicatorOptions{}) + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleStatusSelect() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + value := "todo" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.StatusSelect(c, &value, statuses, project.StatusSelectOptions{}).Changed() { + log.Println("status", value) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleAssigneePicker() { + members := []project.ProjectMember{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}} + ids := []string{"ann"} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.AssigneePicker(c, &ids, members, project.AssigneePickerOptions{Max: 2}).Changed() { + log.Println("assignees", ids) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleDueDatePicker() { + var due core.Date + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.DueDatePicker(c, &due, project.DueDatePickerOptions{}).Changed() { + log.Println("due", due) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleLabelManager() { + labels := []project.TaskLabel{{ID: "ui", Name: "UI", Color: datetime.EventInfo}} + selected := []string{} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.LabelManager(c, &labels, &selected, project.LabelManagerOptions{}).Changed() { + log.Println("labels changed", len(labels), selected) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleIssueCard() { + statuses := []project.WorkflowStatus{{ID: "todo", Name: "To do", Group: project.StatusUnstarted}, {ID: "done", Name: "Done", Group: project.StatusCompleted}} + task := project.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: project.PriorityHigh} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.IssueCard(c, task, project.IssueCardOptions{Statuses: statuses}).Clicked() { + log.Println("open", task.ID) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleIssueIdBadge() { + id := "ENG-123" + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.IssueIdBadge(c, id, project.IssueIdBadgeOptions{Copyable: true}).Changed() { + log.Println("copied", id) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleMilestoneProgress() { + milestone := project.Milestone{Title: "Release", Due: core.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + project.MilestoneProgress(c, milestone, project.MilestoneProgressOptions{}) + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleWorkloadView() { + loads := []project.Workload{{Member: project.ProjectMember{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}} + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + project.WorkloadView(c, loads, project.WorkloadViewOptions{}) + } + ui.Render(view, 1000, 700, 1) +} + +func ExampleTimeTracker() { + var state project.TimeTrackerState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + if project.TimeTracker(c, &state, project.TimeTrackerOptions{}).Changed() { + log.Println("tracked", state.Total(c.Now())) + } + } + ui.Render(view, 1000, 700, 1) +} + +func ExamplePomodoroTimer() { + var state project.PomodoroState + view := func(c *ui.Context) { + core.Use(c, core.Settings{}) + r := project.PomodoroTimer(c, &state, project.PomodoroTimerOptions{Focus: 25 * time.Minute}) + if r.Changed() { + log.Println("timer", state.Phase(), state.Running()) + } + if phase, ok := r.Finished(); ok { + log.Println("finished", phase) + } + } + ui.Render(view, 1000, 700, 1) +} diff --git a/project/helpers_test.go b/project/helpers_test.go new file mode 100644 index 0000000..5fff264 --- /dev/null +++ b/project/helpers_test.go @@ -0,0 +1,55 @@ +package project + +import ( + "fmt" + "image/color" + "testing" + "time" + + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal/snaptest" +) + +// snapshot compares view in both themes with testdata/snapshots//-.png. +func snapshot(t *testing.T, name string, w, h int, view func(c *ui.Context)) { + t.Helper() + snaptest.Run(t, name, w, h, snapshotTheme, view, nil) +} + +func oct(day int) core.Date { return core.Date{Year: 2026, Month: time.October, Day: day} } + +var calendarToday = core.Date{Year: 2026, Month: time.October, Day: 5} + +// datetimeTestLabel is the en-US spoken name of a day cell. +func datetimeTestLabel(d core.Date) string { + return fmt.Sprintf("%v, %v %d, %d", d.Weekday(), d.Month, d.Day, d.Year) +} + +func datetimeNear(c color.RGBA, w ui.Color) bool { + d := func(a, b uint8) int { + if a > b { + return int(a - b) + } + return int(b - a) + } + return d(c.R, w.R)+d(c.G, w.G)+d(c.B, w.B) < 60 +} + +func calDrag(tt *ui.Tester, x0, y0, x1, y1 float32) { + tt.Press(x0, y0) + for i := 1; i <= 4; i++ { + f := float32(i) / 4 + tt.Move(x0+(x1-x0)*f, y0+(y1-y0)*f) + } + tt.Release(x1, y1) +} + +func snapshotTheme(c *ui.Context, mode string) { + m := core.Light + if mode == "dark" { + m = core.Dark + } + core.Use(c, core.Settings{Mode: m}) +} diff --git a/issue_card.go b/project/issue_card.go similarity index 67% rename from issue_card.go rename to project/issue_card.go index da121b4..93e5e16 100644 --- a/issue_card.go +++ b/project/issue_card.go @@ -1,12 +1,16 @@ -package mujica +package project import ( "fmt" - "github.com/egoist/mygo/ui" "log/slog" - "mujicaui/internal" - "mujicaui/theme" + "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // IssueCardOptions configure an IssueCard. @@ -22,12 +26,12 @@ type IssueCardOptions struct { func IssueCard(c *ui.Context, task Task, opts IssueCardOptions) *ui.Element { projectCheckPriority(task.Priority) st := projectStatus(opts.Statuses, task.Status) - k := Tokens(c) - d := Density(c) - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + d := core.Density(c) + small := core.FontSize(c, theme.CaptionSize) b := ui.ButtonBase(c).Column().AlignItems(ui.Stretch).Justify(ui.Start).Padding(d.Unit()*3).Gap(d.Unit()*2). Radius(theme.CardRadius).Border(theme.BorderWidth, k.Border).Background(k.Surface). - Label(task.ID + " " + task.Title).Description(fmt.Sprintf(msg(c, "issue_card.description"), st.Name, PriorityName(c, task.Priority))).Disabled(opts.Disabled) + Label(task.ID + " " + task.Title).Description(fmt.Sprintf(core.Msg(c, "issue_card.description"), st.Name, PriorityName(c, task.Priority))).Disabled(opts.Disabled) internal.Ring(b, k.Focus, theme.CardRadius) switch { case opts.Selected: @@ -36,9 +40,9 @@ func IssueCard(c *ui.Context, task Task, opts IssueCardOptions) *ui.Element { b.Background(k.SurfaceHover).Border(theme.BorderWidth, k.ControlBorder) } selected := opts.Selected - b.Transition(colorFade).Draw(func(p *ui.Painter, r ui.Rect) { + b.Transition(core.ColorFade).Draw(func(p *ui.Painter, r ui.Rect) { if selected { - datetimeSelectedMark(p, r, k.AccentText) + core.DatetimeSelectedMark(p, r, k.AccentText) } }) b.Children(func() { @@ -60,11 +64,11 @@ func IssueCard(c *ui.Context, task Task, opts IssueCardOptions) *ui.Element { } ui.Spacer(c) if len(task.Assignees) > 0 { - items := make([]AvatarItem, len(task.Assignees)) + items := make([]display.AvatarItem, len(task.Assignees)) for i, id := range task.Assignees { - items[i] = AvatarItem{Name: projectMember(opts.Members, id).Name} + items[i] = display.AvatarItem{Name: projectMember(opts.Members, id).Name} } - AvatarGroup(c, items, AvatarGroupOptions{Max: 3, Size: 22}) + display.AvatarGroup(c, items, display.AvatarGroupOptions{Max: 3, Size: 22}) } }) }) @@ -76,12 +80,12 @@ func IssueCard(c *ui.Context, task Task, opts IssueCardOptions) *ui.Element { // issueLabelChip displays a label with its color dot. func issueLabelChip(c *ui.Context, l TaskLabel) { - k := Tokens(c) - d := Density(c) - col := eventColor(k, l.Color) + k := core.Tokens(c) + d := core.Density(c) + col := datetime.EventTone(k, l.Color) ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).Padding(1, d.Unit()*1.5).Radius(theme.ControlRadius). Border(theme.BorderWidth, k.Border).Children(func() { ui.Box(c).Size(8, 8).Radius(4).Background(col) - ui.Text(c, l.Name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() + ui.Text(c, l.Name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Text).SingleLine() }) } diff --git a/issue_card_test.go b/project/issue_card_test.go similarity index 94% rename from issue_card_test.go rename to project/issue_card_test.go index 91e91ca..8de99e5 100644 --- a/issue_card_test.go +++ b/project/issue_card_test.go @@ -1,16 +1,18 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func issueCardTester(task Task, opts IssueCardOptions, n *int) *ui.Tester { opts.Statuses, opts.Members, opts.Labels = projStatuses, projMembers, projLabels return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Width(300).Children(func() { if IssueCard(c, task, opts).Clicked() { *n++ diff --git a/issue_id_badge.go b/project/issue_id_badge.go similarity index 74% rename from issue_id_badge.go rename to project/issue_id_badge.go index 9261c26..254d407 100644 --- a/issue_id_badge.go +++ b/project/issue_id_badge.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -8,9 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) var issueIDPattern = regexp.MustCompile(`^[A-Z][A-Z0-9]*-[1-9][0-9]*$`) @@ -26,18 +27,18 @@ type IssueIdBadgeOptions struct { const issueCopiedFor = 1500 * time.Millisecond // IssueIdBadge displays a monospace issue key with optional copying. -func IssueIdBadge(c *ui.Context, id string, opts IssueIdBadgeOptions) ChoiceResult { +func IssueIdBadge(c *ui.Context, id string, opts IssueIdBadgeOptions) core.ChoiceResult { if !issueIDPattern.MatchString(id) { panic(fmt.Sprintf("mujica: IssueIdBadge %q is not a key like ENG-123", id)) } - k := Tokens(c) - d := Density(c) - small := FontSize(c, theme.CaptionSize) + k := core.Tokens(c) + d := core.Density(c) + small := core.FontSize(c, theme.CaptionSize) if !opts.Copyable { e := ui.Row(c).Padding(d.Unit()/2, d.Unit()*1.5).Radius(theme.ControlRadius).Background(k.Background). Border(theme.BorderWidth, k.Border).Shrink(0).Label(id) e.Children(func() { ui.Text(c, id).Font(theme.MonoFont).FontSize(small).TextColor(k.TextMuted).SingleLine() }) - return ChoiceResult{Element: e} + return core.ChoiceResult{Element: e} } b := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Gap(d.Unit()).PaddingX(d.Unit()*1.5).Radius(theme.ControlRadius). Background(k.Background).Border(theme.BorderWidth, k.Border).Disabled(opts.Disabled) @@ -46,7 +47,7 @@ func IssueIdBadge(c *ui.Context, id string, opts IssueIdBadgeOptions) ChoiceResu changed := false if b.Clicked() && !b.IsDisabled() { c.WriteClipboard(id) - c.Announce(fmt.Sprintf(msg(c, "issue_id_badge.copied_label"), id)) + c.Announce(fmt.Sprintf(core.Msg(c, "issue_id_badge.copied_label"), id)) *copied, changed = c.Now(), true slog.Debug("mujica: issue_id_badge copy", "id", id) } @@ -54,11 +55,11 @@ func IssueIdBadge(c *ui.Context, id string, opts IssueIdBadgeOptions) ChoiceResu if showing { c.After(issueCopiedFor - c.Now().Sub(*copied)) } - b.Label(fmt.Sprintf(msg(c, "issue_id_badge.copy"), id)) + b.Label(fmt.Sprintf(core.Msg(c, "issue_id_badge.copy"), id)) if b.Hovered() { b.Background(k.SurfaceHover) } - b.Transition(colorFade).Children(func() { + b.Transition(core.ColorFade).Children(func() { ui.Text(c, id).Font(theme.MonoFont).FontSize(small).TextColor(k.Text).SingleLine() icon, col := "copy", k.TextMuted if showing { @@ -66,8 +67,8 @@ func IssueIdBadge(c *ui.Context, id string, opts IssueIdBadgeOptions) ChoiceResu } ui.Icon(c, icons.Must(icon)).Size(12, 12).TextColor(col) if showing { - ui.Text(c, msg(c, "issue_id_badge.copied")).FontSize(small).TextColor(k.Success) + ui.Text(c, core.Msg(c, "issue_id_badge.copied")).FontSize(small).TextColor(k.Success) } }) - return ChoiceResult{Element: b, changed: changed} + return core.NewChoiceResult(b, changed) } diff --git a/issue_id_badge_test.go b/project/issue_id_badge_test.go similarity index 89% rename from issue_id_badge_test.go rename to project/issue_id_badge_test.go index b63cf08..6e8ee90 100644 --- a/issue_id_badge_test.go +++ b/project/issue_id_badge_test.go @@ -1,16 +1,18 @@ -package mujica +package project import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestIssueIdBadgeCopy(t *testing.T) { n, disabled := 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Children(func() { if IssueIdBadge(c, "ENG-123", IssueIdBadgeOptions{Copyable: true, Disabled: disabled}).Changed() { n++ @@ -45,7 +47,7 @@ func TestIssueIdBadgeCopy(t *testing.T) { func TestIssueIdBadgeCopiedFades(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) IssueIdBadge(c, "ENG-1", IssueIdBadgeOptions{Copyable: true}) }, 300, 120) _ = tt.Click("Copy ENG-1") @@ -67,7 +69,7 @@ func TestIssueIdBadgeInvalid(t *testing.T) { t.Errorf("%q: expected panic", id) } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); IssueIdBadge(c, id, IssueIdBadgeOptions{}) }, 10, 10) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); IssueIdBadge(c, id, IssueIdBadgeOptions{}) }, 10, 10) }() } } diff --git a/kanban_board.go b/project/kanban_board.go similarity index 88% rename from kanban_board.go rename to project/kanban_board.go index 74b4303..4c6d742 100644 --- a/kanban_board.go +++ b/project/kanban_board.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // KanbanColumn is one column, holding the tasks in Status. @@ -82,10 +83,10 @@ func KanbanBoard(c *ui.Context, tasks *[]Task, opts KanbanBoardOptions) KanbanRe panic(fmt.Sprintf("mujica: KanbanBoard column %q repeats or has a negative WIP", col.Status)) } } - d := Density(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "kanban_board.label") + label = core.Msg(c, "kanban_board.label") } r := KanbanResult{} board := ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Stretch).Grow(1).MinHeight(0).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -100,7 +101,7 @@ func KanbanBoard(c *ui.Context, tasks *[]Task, opts KanbanBoardOptions) KanbanRe } t := (*tasks)[from] if wip := opts.Columns[ci].WIP; wip > 0 && t.Status != to && len(cols[ci]) >= wip { - st.problem, st.problemAt = fmt.Sprintf(msg(c, "kanban_board.full"), projectStatus(opts.Statuses, to).Name, wip), to + st.problem, st.problemAt = fmt.Sprintf(core.Msg(c, "kanban_board.full"), projectStatus(opts.Statuses, to).Name, wip), to slog.Debug("mujica: kanban_board wip refused", "id", id, "to", to) return } @@ -176,8 +177,8 @@ func kanbanColumns(st *kanbanState, tasks []Task, opts KanbanBoardOptions) [][]i // kanbanColumn draws a header, WIP count and virtual cards. func kanbanColumn(c *ui.Context, st *kanbanState, ci int, col KanbanColumn, colsOf func(int) []int, tasks []Task, opts KanbanBoardOptions, r *KanbanResult, move func(id, to, before, how string)) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) idx := colsOf(ci) status := projectStatus(opts.Statuses, col.Status) over := col.WIP > 0 && len(idx) > col.WIP @@ -203,15 +204,15 @@ func kanbanColumn(c *ui.Context, st *kanbanState, ci int, col KanbanColumn, cols fg = k.Danger ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14).TextColor(fg) } - wip := ui.Text(c, count).FontSize(FontSize(c, theme.CaptionSize)).TextColor(fg).FontFeatures("tnum") + wip := ui.Text(c, count).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(fg).FontFeatures("tnum") if col.WIP > 0 { - wip.Label(fmt.Sprintf(msg(c, "kanban_board.wip"), len(idx), col.WIP)) + wip.Label(fmt.Sprintf(core.Msg(c, "kanban_board.wip"), len(idx), col.WIP)) } }) if over { - ui.Text(c, fmt.Sprintf(msg(c, "kanban_board.over"), len(idx)-col.WIP)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) + ui.Text(c, fmt.Sprintf(core.Msg(c, "kanban_board.over"), len(idx)-col.WIP)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) } else if full && st.problem != "" && st.problemAt == col.Status { - ui.Text(c, st.problem).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) + ui.Text(c, st.problem).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Danger).Role(ui.RoleStatus) } ls := st.lists[ci] ls.Key = func(i int) any { return tasks[idx[i]].ID } @@ -256,7 +257,7 @@ func kanbanColumn(c *ui.Context, st *kanbanState, ci int, col KanbanColumn, cols }) }).Grow(1).MinHeight(d.ControlHeight() * 3).Children(func() { if len(idx) == 0 { - ui.Text(c, msg(c, "kanban_board.empty")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit() * 2) + ui.Text(c, core.Msg(c, "kanban_board.empty")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit() * 2) } }) }) diff --git a/kanban_board_test.go b/project/kanban_board_test.go similarity index 97% rename from kanban_board_test.go rename to project/kanban_board_test.go index f662109..41a960b 100644 --- a/kanban_board_test.go +++ b/project/kanban_board_test.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -6,6 +6,8 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var kanbanColumns3 = []KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: 3}, {Status: "review"}, {Status: "done"}} @@ -23,7 +25,7 @@ func (p *kanbanProbe) tester() *ui.Tester { p.cols = kanbanColumns3 } return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(560).Padding(8).Children(func() { r := KanbanBoard(c, &p.tasks, KanbanBoardOptions{Columns: p.cols, Statuses: projStatuses, Members: projMembers, Labels: projLabels, Disabled: p.disabled}) @@ -162,7 +164,7 @@ func TestKanbanBoardForeignDrop(t *testing.T) { right := []Task{{ID: "B-1", Title: "Second", Status: "todo"}} opts := KanbanBoardOptions{Columns: []KanbanColumn{{Status: "todo"}}, Statuses: projStatuses} tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Row(c).Height(400).Children(func() { ui.Box(c).Key("left").Width(300).Height(400).Children(func() { KanbanBoard(c, &left, opts) }) ui.Box(c).Key("right").Width(300).Height(400).Children(func() { KanbanBoard(c, &right, opts) }) diff --git a/label_manager.go b/project/label_manager.go similarity index 76% rename from label_manager.go rename to project/label_manager.go index 455a570..ac041cf 100644 --- a/label_manager.go +++ b/project/label_manager.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // LabelManagerOptions configure a LabelManager. @@ -30,12 +33,12 @@ type labelManagerState struct { } // LabelManager edits labels and stores chosen IDs in selected. -func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts LabelManagerOptions) ChoiceResult { - k := Tokens(c) - d := Density(c) +func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts LabelManagerOptions) core.ChoiceResult { + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "label_manager.label") + label = core.Msg(c, "label_manager.label") } changed := false col := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -54,7 +57,7 @@ func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts L *cache = labelDataCache{first: first, n: len(*labels), selectedFirst: selectedFirst, selectedN: len(*selected), version: opts.Version, ready: true, selected: map[string]bool{}} known := map[string]bool{} for _, l := range *labels { - eventColor(k, l.Color) + datetime.EventTone(k, l.Color) if l.ID == "" || known[l.ID] { panic("mujica: LabelManager needs unique non-empty IDs") } @@ -82,7 +85,7 @@ func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts L on := cache.selected[l.ID] box := ui.CheckboxBase(c, &on).Size(theme.MinHitSize, theme.MinHitSize).Center().Label(l.Name) internal.Ring(box, k.Focus, theme.ControlRadius) - if box.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, eventColor(k, l.Color), on, k) }).Changed() && live() { + if box.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, datetime.EventTone(k, l.Color), on, k) }).Changed() && live() { if on { *selected = append(*selected, l.ID) } else { @@ -96,10 +99,10 @@ func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts L return } ui.Text(c, l.Name).Grow(1).TextColor(k.Text).SingleLine().Ellipsis("…") - if b := iconAction(c, icons.Must("palette"), fmt.Sprintf(msg(c, "label_manager.edit"), l.Name)); b.Clicked() && live() { + if b := core.IconAction(c, icons.Must("palette"), fmt.Sprintf(core.Msg(c, "label_manager.edit"), l.Name)); b.Clicked() && live() { s.editing, s.name, s.problem = l.ID, l.Name, "" } - if b := iconAction(c, icons.Must("trash-2"), fmt.Sprintf(msg(c, "label_manager.delete"), l.Name)); b.Clicked() && live() { + if b := core.IconAction(c, icons.Must("trash-2"), fmt.Sprintf(core.Msg(c, "label_manager.delete"), l.Name)); b.Clicked() && live() { removed = i } }) @@ -112,18 +115,18 @@ func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts L changed = true } if len(*labels) == 0 { - ui.Text(c, msg(c, "label_manager.empty")).TextColor(k.TextMuted) + ui.Text(c, core.Msg(c, "label_manager.empty")).TextColor(k.TextMuted) } - in := TextInput(c, &s.draft, TextInputOptions{Placeholder: msg(c, "label_manager.new"), Label: msg(c, "label_manager.new"), + in := input.TextInput(c, &s.draft, input.TextInputOptions{Placeholder: core.Msg(c, "label_manager.new"), Label: core.Msg(c, "label_manager.new"), Disabled: opts.Disabled, Error: s.problem}) if in.Submitted() && live() { name := strings.TrimSpace(s.draft) switch { case name == "": case taken(name, ""): - s.problem = fmt.Sprintf(msg(c, "label_manager.taken"), name) + s.problem = fmt.Sprintf(core.Msg(c, "label_manager.taken"), name) default: - l := TaskLabel{ID: labelNewID(*labels), Name: name, Color: EventColor(len(*labels) % 6)} + l := TaskLabel{ID: labelNewID(*labels), Name: name, Color: datetime.EventColor(len(*labels) % 6)} *labels = append(*labels, l) s.draft, s.problem, changed = "", "", true slog.Debug("mujica: label_manager create", "id", l.ID) @@ -134,24 +137,24 @@ func LabelManager(c *ui.Context, labels *[]TaskLabel, selected *[]string, opts L cache.ready = false c.Invalidate() } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } // labelEditRow renames a label and picks its color. func labelEditRow(c *ui.Context, s *labelManagerState, l *TaskLabel, taken func(string, string) bool, changed *bool) { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) ui.Column(c).Grow(1).Gap(d.Unit()).Children(func() { - in := TextInput(c, &s.name, TextInputOptions{Label: msg(c, "label_manager.name"), Error: s.problem}) + in := input.TextInput(c, &s.name, input.TextInputOptions{Label: core.Msg(c, "label_manager.name"), Error: s.problem}) in.Input.AutoFocus() - ui.Row(c).Gap(d.Unit()).Role(ui.RoleRadioGroup).Label(msg(c, "label_manager.color")).Children(func() { + ui.Row(c).Gap(d.Unit()).Role(ui.RoleRadioGroup).Label(core.Msg(c, "label_manager.color")).Children(func() { for i := range 6 { - col := EventColor(i) + col := datetime.EventColor(i) sel := l.Color - b := ui.RadioBase(c, &sel, col).Size(theme.MinHitSize, theme.MinHitSize).Label(msg(c, labelColorKeys[i])) + b := ui.RadioBase(c, &sel, col).Size(theme.MinHitSize, theme.MinHitSize).Label(core.Msg(c, labelColorKeys[i])) internal.Ring(b, k.Focus, theme.ControlRadius) on := l.Color == col - b.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, eventColor(k, col), on, k) }) + b.Draw(func(p *ui.Painter, r ui.Rect) { labelSwatch(p, r, datetime.EventTone(k, col), on, k) }) if b.Changed() && !b.IsDisabled() { l.Color, *changed = col, true slog.Debug("mujica: label_manager color", "id", l.ID, "color", col) @@ -165,7 +168,7 @@ func labelEditRow(c *ui.Context, s *labelManagerState, l *TaskLabel, taken func( case name == "": done = false case taken(name, l.ID): - s.problem, done = fmt.Sprintf(msg(c, "label_manager.taken"), name), false + s.problem, done = fmt.Sprintf(core.Msg(c, "label_manager.taken"), name), false case name != l.Name: l.Name, *changed = name, true slog.Debug("mujica: label_manager rename", "id", l.ID) diff --git a/label_manager_test.go b/project/label_manager_test.go similarity index 91% rename from label_manager_test.go rename to project/label_manager_test.go index 8b1ff3c..37e93ac 100644 --- a/label_manager_test.go +++ b/project/label_manager_test.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -6,11 +6,14 @@ import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) func labelTester(labels *[]TaskLabel, sel *[]string, opts *LabelManagerOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if LabelManager(c, labels, sel, *opts).Changed() { *n++ @@ -53,7 +56,7 @@ func TestLabelManagerRenameAndColor(t *testing.T) { tt.Command("selectAll") tt.Type("UI") tt.Key(0, ui.KeyEnter) - if labels[0].Color != EventSuccess || labels[0].Name != "Bug" || !tt.HasText(`A label named "UI" exists.`) { + if labels[0].Color != datetime.EventSuccess || labels[0].Name != "Bug" || !tt.HasText(`A label named "UI" exists.`) { t.Fatalf("color/duplicate %+v", labels[0]) } tt.Command("selectAll") diff --git a/milestone_progress.go b/project/milestone_progress.go similarity index 74% rename from milestone_progress.go rename to project/milestone_progress.go index 42bc9f1..2d7cf6b 100644 --- a/milestone_progress.go +++ b/project/milestone_progress.go @@ -1,37 +1,38 @@ -package mujica +package project import ( "fmt" "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Milestone holds a goal, due date and completion counts. type Milestone struct { Title string - Due Date + Due core.Date Done, Total int } // MilestoneProgressOptions configure a MilestoneProgress. type MilestoneProgressOptions struct { // Today anchors the date note; zero uses the system clock. - Today Date + Today core.Date } // MilestoneProgress shows completion counts, progress and due dates. func MilestoneProgress(c *ui.Context, m Milestone, opts MilestoneProgressOptions) *ui.Element { - datetimeCheckDate("MilestoneProgress Due", m.Due) + core.DatetimeCheckDate("MilestoneProgress Due", m.Due) if m.Total < 0 || m.Done < 0 || m.Done > m.Total { panic(fmt.Sprintf("mujica: MilestoneProgress %d of %d is not valid", m.Done, m.Total)) } - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) frac := 0.0 if m.Total > 0 { frac = float64(m.Done) / float64(m.Total) @@ -50,20 +51,20 @@ func MilestoneProgress(c *ui.Context, m Milestone, opts MilestoneProgressOptions }) projectBar(c, frac, m.Title) ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "milestone_progress.count"), m.Done, m.Total)).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, fmt.Sprintf(core.Msg(c, "milestone_progress.count"), m.Done, m.Total)).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).Grow(1).FontFeatures("tnum") text, late := projectDueText(c, m.Due, today, reached) fg, icon := k.TextMuted, "calendar" switch { case reached: - text, fg, icon = msg(c, "milestone_progress.reached"), k.Success, "circle-check" + text, fg, icon = core.Msg(c, "milestone_progress.reached"), k.Success, "circle-check" case late: fg, icon = k.Danger, "triangle-alert" } if text != "" { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(fg).Role(ui.RoleStatus).Children(func() { ui.Icon(c, icons.Must(icon)).Size(14, 14) - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)) }) } }) diff --git a/milestone_progress_test.go b/project/milestone_progress_test.go similarity index 95% rename from milestone_progress_test.go rename to project/milestone_progress_test.go index 70466bd..d1e6b78 100644 --- a/milestone_progress_test.go +++ b/project/milestone_progress_test.go @@ -1,14 +1,16 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func milestoneTester(m Milestone) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Box(c).Width(360).Children(func() { MilestoneProgress(c, m, MilestoneProgressOptions{Today: calendarToday}) }) }, 400, 200) } diff --git a/pomodoro_timer.go b/project/pomodoro_timer.go similarity index 76% rename from pomodoro_timer.go rename to project/pomodoro_timer.go index 21b1731..8608f44 100644 --- a/pomodoro_timer.go +++ b/project/pomodoro_timer.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -8,9 +8,12 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PomodoroPhase is the part of the cycle running. @@ -96,11 +99,11 @@ func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) P if live { now = c.Now() } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "pomodoro_timer.label") + label = core.Msg(c, "pomodoro_timer.label") } r := PomodoroResult{} next := func() { @@ -117,7 +120,7 @@ func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) P } if s.running && s.elapsed(now) >= lengths[s.phase] { r.finished, r.done = s.phase, true - c.Announce(fmt.Sprintf(msg(c, "pomodoro_timer.done"), msg(c, pomodoroKeys[s.phase]))) + c.Announce(fmt.Sprintf(core.Msg(c, "pomodoro_timer.done"), core.Msg(c, pomodoroKeys[s.phase]))) slog.Debug("mujica: pomodoro_timer finished", "phase", s.phase) next() } @@ -135,9 +138,9 @@ func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) P secs := int(math.Ceil(left.Seconds())) frac := 1 - float32(secs)/float32(lengths[s.phase]/time.Second) col.Children(func() { - phase := msg(c, pomodoroKeys[s.phase]) + phase := core.Msg(c, pomodoroKeys[s.phase]) text := fmt.Sprintf("%02d:%02d", secs/60, secs%60) - ring := ui.Box(c).Size(160, 160).Center().Role(ui.RoleStatus).Label(fmt.Sprintf(msg(c, "pomodoro_timer.left"), phase, CountdownText(c, time.Duration(secs)*time.Second, CountdownWords))) + ring := ui.Box(c).Size(160, 160).Center().Role(ui.RoleStatus).Label(fmt.Sprintf(core.Msg(c, "pomodoro_timer.left"), phase, datetime.CountdownText(c, time.Duration(secs)*time.Second, datetime.CountdownWords))) tone := k.AccentText if s.phase != PomodoroFocus { tone = k.Success @@ -151,17 +154,17 @@ func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) P }) ring.Children(func() { ui.Column(c).AlignItems(ui.Center).Gap(2).Children(func() { - ui.Text(c, text).Font(theme.MonoFont).FontSize(FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum") - ui.Text(c, phase).FontSize(FontSize(c, theme.CaptionSize)).TextColor(tone) + ui.Text(c, text).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.H1Size)).TextColor(k.Text).FontFeatures("tnum") + ui.Text(c, phase).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(tone) }) }) - ui.Text(c, fmt.Sprintf(msg(c, "pomodoro_timer.sessions"), s.focused%every, every)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, fmt.Sprintf(core.Msg(c, "pomodoro_timer.sessions"), s.focused%every, every)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) ui.Row(c).Gap(d.Unit() * 2).Children(func() { - key, icon, variant := "pomodoro_timer.start", "play", Primary + key, icon, variant := "pomodoro_timer.start", "play", input.Primary if s.running { - key, icon, variant = "pomodoro_timer.pause", "pause", Secondary + key, icon, variant = "pomodoro_timer.pause", "pause", input.Secondary } - if Button(c, msg(c, key), ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() { + if input.Button(c, core.Msg(c, key), input.ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() { act("toggle", func() { if s.running { s.spent, s.running = s.elapsed(now), false @@ -170,10 +173,10 @@ func PomodoroTimer(c *ui.Context, s *PomodoroState, opts PomodoroTimerOptions) P } }) } - if Button(c, msg(c, "pomodoro_timer.skip"), ButtonOptions{Variant: Secondary}).Clicked() { + if input.Button(c, core.Msg(c, "pomodoro_timer.skip"), input.ButtonOptions{Variant: input.Secondary}).Clicked() { act("skip", next) } - if Button(c, msg(c, "pomodoro_timer.reset"), ButtonOptions{Variant: Ghost, Icon: icons.Must("rotate-ccw")}).Clicked() { + if input.Button(c, core.Msg(c, "pomodoro_timer.reset"), input.ButtonOptions{Variant: input.Ghost, Icon: icons.Must("rotate-ccw")}).Clicked() { act("reset", func() { *s = PomodoroState{} }) } }) diff --git a/pomodoro_timer_test.go b/project/pomodoro_timer_test.go similarity index 96% rename from pomodoro_timer_test.go rename to project/pomodoro_timer_test.go index 2c95216..5b8aba1 100644 --- a/pomodoro_timer_test.go +++ b/project/pomodoro_timer_test.go @@ -1,10 +1,12 @@ -package mujica +package project import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type pomodoroProbe struct { @@ -18,7 +20,7 @@ type pomodoroProbe struct { func (p *pomodoroProbe) tester() *ui.Tester { p.now = time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC) return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) o := p.opts o.Now = p.now r := PomodoroTimer(c, &p.s, o) @@ -106,7 +108,7 @@ func TestPomodoroTimerIdleAndHidden(t *testing.T) { s, shown, builds := PomodoroState{}, true, 0 tt := ui.NewTester(func(c *ui.Context) { builds++ - Use(c, Settings{}) + core.Use(c, core.Settings{}) if shown { PomodoroTimer(c, &s, PomodoroTimerOptions{}) } diff --git a/priority_indicator.go b/project/priority_indicator.go similarity index 86% rename from priority_indicator.go rename to project/priority_indicator.go index e0e3623..dd5eb7f 100644 --- a/priority_indicator.go +++ b/project/priority_indicator.go @@ -1,9 +1,10 @@ -package mujica +package project import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // PriorityIndicatorOptions configure a PriorityIndicator. @@ -18,14 +19,14 @@ var priorityKeys = [...]string{"priority_indicator.none", "priority_indicator.lo // PriorityName is a priority's localized name. func PriorityName(c *ui.Context, p Priority) string { projectCheckPriority(p) - return msg(c, priorityKeys[p]) + return core.Msg(c, priorityKeys[p]) } // PriorityIndicator displays urgency with shapes and localized text. func PriorityIndicator(c *ui.Context, p Priority, opts PriorityIndicatorOptions) *ui.Element { name := PriorityName(c, p) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) col := k.TextMuted switch p { case PriorityUrgent: @@ -37,7 +38,7 @@ func PriorityIndicator(c *ui.Context, p Priority, opts PriorityIndicatorOptions) row.Children(func() { ui.Box(c).Size(14, 14).Shrink(0).Draw(func(pp *ui.Painter, r ui.Rect) { priorityGlyph(pp, r, p, col, k.Surface, k.Border) }) if !opts.IconOnly { - ui.Text(c, name).FontSize(FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() + ui.Text(c, name).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(col).SingleLine() } }) return row diff --git a/priority_indicator_test.go b/project/priority_indicator_test.go similarity index 84% rename from priority_indicator_test.go rename to project/priority_indicator_test.go index ea7360a..6d35640 100644 --- a/priority_indicator_test.go +++ b/project/priority_indicator_test.go @@ -1,16 +1,17 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func TestPriorityIndicatorNamesAndShapes(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light, Locale: ZhCN}) + core.Use(c, core.Settings{Mode: core.Light, Locale: core.ZhCN}) ui.Column(c).Children(func() { for p := PriorityNone; p <= PriorityUrgent; p++ { PriorityIndicator(c, p, PriorityIndicatorOptions{}) @@ -28,7 +29,7 @@ func TestPriorityIndicatorNamesAndShapes(t *testing.T) { func TestPriorityIndicatorBarsShowLevel(t *testing.T) { tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Children(func() { for p := PriorityLow; p <= PriorityHigh; p++ { ui.Box(c).Height(20).Children(func() { PriorityIndicator(c, p, PriorityIndicatorOptions{IconOnly: true}) }) @@ -65,7 +66,7 @@ func TestPriorityIndicatorInvalid(t *testing.T) { t.Fatal("expected panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); PriorityIndicator(c, 9, PriorityIndicatorOptions{}) }, 10, 10) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); PriorityIndicator(c, 9, PriorityIndicatorOptions{}) }, 10, 10) } func TestPriorityIndicatorSnapshot(t *testing.T) { diff --git a/projects.go b/project/projects.go similarity index 83% rename from projects.go rename to project/projects.go index e86bea7..96955fd 100644 --- a/projects.go +++ b/project/projects.go @@ -1,10 +1,13 @@ -package mujica +package project import ( "fmt" "slices" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) // Priority describes urgency; zero means no priority. @@ -28,7 +31,7 @@ type ProjectMember struct { type TaskLabel struct { ID string Name string - Color EventColor + Color datetime.EventColor } // Subtask is one checklist step of a task. @@ -45,7 +48,7 @@ type Task struct { Title string Done bool Priority Priority - Due Date + Due core.Date Status string Assignees []string Labels []string @@ -118,26 +121,26 @@ func projectStatus(statuses []WorkflowStatus, id string) WorkflowStatus { } // projectDueText formats a date relative to today. -func projectDueText(c *ui.Context, due, today Date, done bool) (string, bool) { - datetimeCheckDate("project due", due) +func projectDueText(c *ui.Context, due, today core.Date, done bool) (string, bool) { + core.DatetimeCheckDate("project due", due) if due.IsZero() { return "", false } - days := int(due.civil().Sub(today.civil()).Hours() / 24) + days := int(due.Civil().Sub(today.Civil()).Hours() / 24) switch { case days < 0 && !done: - return calendarCount(c, "projects.overdue", -days), true + return core.CalendarCount(c, "projects.overdue", -days), true case days == 0: - return msg(c, "projects.due_today"), false + return core.Msg(c, "projects.due_today"), false case days == 1: - return msg(c, "projects.due_tomorrow"), false + return core.Msg(c, "projects.due_tomorrow"), false } - return fmt.Sprintf(msg(c, "projects.due"), datetimeMonthName(c, due.Month), due.Day), false + return fmt.Sprintf(core.Msg(c, "projects.due"), core.DatetimeMonthName(c, due.Month), due.Day), false } // projectBar draws a named completion bar. func projectBar(c *ui.Context, frac float64, label string) *ui.Element { - k := Tokens(c) + k := core.Tokens(c) return ui.Box(c).Height(6).FillWidth().Role(ui.RoleProgress).Label(label).Description(fmt.Sprintf("%d%%", int(frac*100))). Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(r, k.Border, 3) diff --git a/projects_test.go b/project/projects_test.go similarity index 85% rename from projects_test.go rename to project/projects_test.go index ef26682..c2d481d 100644 --- a/projects_test.go +++ b/project/projects_test.go @@ -1,9 +1,12 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" ) var projStatuses = []WorkflowStatus{ @@ -18,8 +21,8 @@ var projStatuses = []WorkflowStatus{ var projMembers = []ProjectMember{{ID: "saki", Name: "Sakiko Togawa"}, {ID: "mutsu", Name: "Mutsumi Wakaba"}, {ID: "uika", Name: "Uika Misumi"}, {ID: "nyamu", Name: "Nyamu Yuutenji"}, {ID: "umiri", Name: "Umiri Yahata"}} -var projLabels = []TaskLabel{{ID: "bug", Name: "Bug", Color: EventDanger}, {ID: "ui", Name: "UI", Color: EventInfo}, - {ID: "docs", Name: "Docs", Color: EventSuccess}} +var projLabels = []TaskLabel{{ID: "bug", Name: "Bug", Color: datetime.EventDanger}, {ID: "ui", Name: "UI", Color: datetime.EventInfo}, + {ID: "docs", Name: "Docs", Color: datetime.EventSuccess}} // projTasks returns fresh tasks for one test. func projTasks() []Task { @@ -36,9 +39,9 @@ func TestProjectDueText(t *testing.T) { var got []string var late []bool ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) for _, c2 := range []struct { - due Date + due core.Date done bool }{{oct(2), false}, {oct(4), false}, {oct(5), false}, {oct(6), false}, {oct(14), false}, {oct(1), true}} { s, l := projectDueText(c, c2.due, calendarToday, c2.done) diff --git a/sprint_board.go b/project/sprint_board.go similarity index 63% rename from sprint_board.go rename to project/sprint_board.go index 5bb76ed..fc3ba8e 100644 --- a/sprint_board.go +++ b/project/sprint_board.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -6,34 +6,37 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/feedback" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Sprint is one iteration: its name, goal and inclusive dates. type Sprint struct { Name string Goal string - Start, End Date + Start, End core.Date } // SprintBoardOptions configure a SprintBoard. type SprintBoardOptions struct { Board KanbanBoardOptions // Today counts the days left; zero uses the system clock. - Today Date + Today core.Date } // SprintBoard combines iteration information, completion and a task board. func SprintBoard(c *ui.Context, sprint Sprint, tasks *[]Task, opts SprintBoardOptions) KanbanResult { - datetimeCheckDate("SprintBoard Start", sprint.Start) - datetimeCheckDate("SprintBoard End", sprint.End) + core.DatetimeCheckDate("SprintBoard Start", sprint.Start) + core.DatetimeCheckDate("SprintBoard End", sprint.End) if sprint.Start.IsZero() || sprint.End.Compare(sprint.Start) < 0 { panic(fmt.Sprintf("mujica: SprintBoard dates %v..%v are not a span", sprint.Start, sprint.End)) } - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) var r KanbanResult col := ui.Column(c).Gap(d.Unit() * 4).Grow(1).MinHeight(0).Role(ui.RoleGroup).Label(sprint.Name) cache := ui.Local(col, "sprint-counts", func() sprintCache { return sprintCache{} }) @@ -56,20 +59,20 @@ func SprintBoard(c *ui.Context, sprint Sprint, tasks *[]Task, opts SprintBoardOp col.Children(func() { ui.Row(c).Gap(d.Unit() * 6).AlignItems(ui.Center).Wrap().Children(func() { ui.Column(c).Gap(d.Unit()).Grow(1).Children(func() { - ui.Text(c, sprint.Name).Font(theme.SerifFont).FontSize(FontSize(c, theme.H2Size)).TextColor(k.Text) + ui.Text(c, sprint.Name).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H2Size)).TextColor(k.Text) if sprint.Goal != "" { ui.Text(c, sprint.Goal).TextColor(k.TextMuted) } - ui.Text(c, weekViewTitle(c, sprint.Start, sprint.End)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, datetime.WeekViewTitle(c, sprint.Start, sprint.End)).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) - sprintStat(c, sprintDaysLeft(c, sprint, today), msg(c, "sprint_board.time")) + sprintStat(c, sprintDaysLeft(c, sprint, today), core.Msg(c, "sprint_board.time")) frac := 0.0 if total > 0 { frac = float64(done) / float64(total) } ui.Column(c).Width(d.ControlHeight() * 5).Gap(d.Unit()).Children(func() { - ui.Text(c, fmt.Sprintf(msg(c, "sprint_board.done"), done, total)).TextColor(k.Text).FontFeatures("tnum") - Progress(c, frac, ProgressOptions{Label: msg(c, "sprint_board.progress")}) + ui.Text(c, fmt.Sprintf(core.Msg(c, "sprint_board.done"), done, total)).TextColor(k.Text).FontFeatures("tnum") + feedback.Progress(c, frac, feedback.ProgressOptions{Label: core.Msg(c, "sprint_board.progress")}) }) }) ui.Box(c).Height(12).FillWidth().Draw(func(p *ui.Painter, rr ui.Rect) { internal.RuleDiamond(p, rr, 8, 6, k.Ornament) }) @@ -84,22 +87,22 @@ func SprintBoard(c *ui.Context, sprint Sprint, tasks *[]Task, opts SprintBoardOp } // sprintDaysLeft formats remaining time relative to the iteration. -func sprintDaysLeft(c *ui.Context, s Sprint, today Date) string { +func sprintDaysLeft(c *ui.Context, s Sprint, today core.Date) string { switch { case today.Compare(s.Start) < 0: - return calendarCount(c, "sprint_board.starts_in", int(s.Start.civil().Sub(today.civil()).Hours()/24)) + return core.CalendarCount(c, "sprint_board.starts_in", int(s.Start.Civil().Sub(today.Civil()).Hours()/24)) case today.Compare(s.End) > 0: - return msg(c, "sprint_board.ended") + return core.Msg(c, "sprint_board.ended") } - return calendarCount(c, "sprint_board.left", int(s.End.civil().Sub(today.civil()).Hours()/24)+1) + return core.CalendarCount(c, "sprint_board.left", int(s.End.Civil().Sub(today.Civil()).Hours()/24)+1) } // sprintStat is a figure above its caption. func sprintStat(c *ui.Context, value, caption string) { - k := Tokens(c) + k := core.Tokens(c) ui.Column(c).Gap(2).Role(ui.RoleStatus).Label(caption + ": " + value).Children(func() { - ui.Text(c, value).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.AccentText) - ui.Text(c, caption).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) + ui.Text(c, value).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(k.AccentText) + ui.Text(c, caption).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted) }) } diff --git a/sprint_board_test.go b/project/sprint_board_test.go similarity index 88% rename from sprint_board_test.go rename to project/sprint_board_test.go index e9c3c49..99558dc 100644 --- a/sprint_board_test.go +++ b/project/sprint_board_test.go @@ -1,17 +1,19 @@ -package mujica +package project import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) var projSprint = Sprint{Name: "Sprint 12", Goal: "Ready the masquerade", Start: oct(1), End: oct(14)} -func sprintTester(sprint Sprint, tasks *[]Task, today Date, disabled bool, moves *int) *ui.Tester { +func sprintTester(sprint Sprint, tasks *[]Task, today core.Date, disabled bool, moves *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(640).Padding(8).Children(func() { r := SprintBoard(c, sprint, tasks, SprintBoardOptions{Today: today, Board: KanbanBoardOptions{Columns: kanbanColumns3, Statuses: projStatuses, Members: projMembers, Labels: projLabels, Disabled: disabled}}) @@ -40,9 +42,9 @@ func TestSprintBoardHeaderAndMoves(t *testing.T) { func TestSprintBoardDaysAndDisabled(t *testing.T) { tasks, n := projTasks(), 0 for _, c := range []struct { - today Date + today core.Date want string - }{{Date{2026, 9, 30}, "Starts in 1 day"}, {oct(14), "1 day left"}, {oct(15), "Ended"}} { + }{{core.Date{Year: 2026, Month: 9, Day: 30}, "Starts in 1 day"}, {oct(14), "1 day left"}, {oct(15), "Ended"}} { if tt := sprintTester(projSprint, &tasks, c.today, false, &n); !tt.HasText(c.want) { t.Errorf("%v: want %q", c.today, c.want) } diff --git a/status_select.go b/project/status_select.go similarity index 82% rename from status_select.go rename to project/status_select.go index d4aa7fc..32bf2f3 100644 --- a/status_select.go +++ b/project/status_select.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // StatusSelectOptions configure a StatusSelect. @@ -21,7 +23,7 @@ var statusGroupKeys = [...]string{"status_select.backlog", "status_select.unstar "status_select.completed", "status_select.canceled"} // StatusSelect chooses a status grouped by workflow stage. -func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts StatusSelectOptions) ChoiceResult { +func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts StatusSelectOptions) core.ChoiceResult { if len(statuses) == 0 { panic("mujica: StatusSelect needs statuses") } @@ -36,7 +38,7 @@ func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts } } cur := projectStatus(statuses, *value) - k := Tokens(c) + k := core.Tokens(c) old := *value pick := old var s *ui.SelectParts[string] @@ -60,16 +62,16 @@ func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts tr := s.Trigger label := opts.Label if label == "" { - label = msg(c, "status_select.label") + label = core.Msg(c, "status_select.label") } tr.Label(label).Description(cur.Name) - selectField(c, tr, s.Open(), func() { + input.SelectField(c, tr, s.Open(), func() { statusGlyphBox(c, cur.Group) ui.Text(c, cur.Name).SingleLine().Grow(1).Shrink(1).TextColor(k.Text) }) if !tr.IsDisabled() { s.Popup(func(panel *ui.Element) { - selectPanel(c, panel) + input.SelectPanel(c, panel) _, wh := c.Size() ui.Scroll(c).MaxHeight(min(320, wh/2)).Children(func() { for g := StatusBacklog; g <= StatusCanceled; g++ { @@ -79,12 +81,12 @@ func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts continue } if first { - ui.Text(c, msg(c, statusGroupKeys[g])).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). - Padding(Density(c).Unit(), Density(c).Unit()*2).Role(ui.RoleColumnHeader) + ui.Text(c, core.Msg(c, statusGroupKeys[g])).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted). + Padding(core.Density(c).Unit(), core.Density(c).Unit()*2).Role(ui.RoleColumnHeader) first = false } it := s.Item(st.ID) - selectRow(c, it, it.Highlighted(), st.ID == *value, "") + input.SelectRow(c, it, it.Highlighted(), st.ID == *value, "") it.Label(st.Name).Children(func() { statusGlyphBox(c, st.Group) ui.Text(c, st.Name).SingleLine() @@ -101,12 +103,12 @@ func StatusSelect(c *ui.Context, value *string, statuses []WorkflowStatus, opts if changed { slog.Debug("mujica: status_select change", "from", old, "to", *value) } - return ChoiceResult{Element: tr, changed: changed} + return core.NewChoiceResult(tr, changed) } // statusGlyphBox reserves 14 DIPs for a status glyph. func statusGlyphBox(c *ui.Context, g StatusGroup) { - k := Tokens(c) + k := core.Tokens(c) ui.Box(c).Size(14, 14).Shrink(0).Draw(func(p *ui.Painter, r ui.Rect) { statusGlyph(p, r, g, k) }) } diff --git a/status_select_test.go b/project/status_select_test.go similarity index 94% rename from status_select_test.go rename to project/status_select_test.go index 90e273f..bfa2d34 100644 --- a/status_select_test.go +++ b/project/status_select_test.go @@ -1,14 +1,16 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func statusTester(v *string, opts *StatusSelectOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if StatusSelect(c, v, projStatuses, *opts).Changed() { *n++ @@ -74,7 +76,7 @@ func TestStatusSelectSnapshot(t *testing.T) { func TestStatusSelectAncestorDisableCloses(t *testing.T) { value, disabled := "todo", false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { StatusSelect(c, &value, projStatuses, StatusSelectOptions{}) }) }, 400, 520) if err := tt.Click("Status"); err != nil { diff --git a/subtask_list.go b/project/subtask_list.go similarity index 81% rename from subtask_list.go rename to project/subtask_list.go index 1095476..7d3cc95 100644 --- a/subtask_list.go +++ b/project/subtask_list.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -8,8 +8,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // SubtaskListOptions configure a SubtaskList. @@ -24,12 +26,12 @@ type SubtaskListOptions struct { type subtaskDrag string // SubtaskList edits and reorders subtasks with completion progress. -func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) ChoiceResult { - k := Tokens(c) - d := Density(c) +func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) core.ChoiceResult { + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "subtask_list.label") + label = core.Msg(c, "subtask_list.label") } changed := false col := ui.Column(c).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -80,7 +82,7 @@ func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) Choice frac = float64(done) / float64(len(*subs)) } ui.Box(c).Grow(1).Children(func() { projectBar(c, frac, label) }) - ui.Text(c, fmt.Sprintf(msg(c, "subtask_list.progress"), done, len(*subs))).FontSize(FontSize(c, theme.CaptionSize)). + ui.Text(c, fmt.Sprintf(core.Msg(c, "subtask_list.progress"), done, len(*subs))).FontSize(core.FontSize(c, theme.CaptionSize)). TextColor(k.TextMuted).FontFeatures("tnum") }) removed := -1 @@ -96,13 +98,13 @@ func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) Choice row.Background(k.Selection) } row.Children(func() { - grip := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Center().Cursor(ui.CursorGrab).Label(fmt.Sprintf(msg(c, "subtask_list.drag"), s.Title)) + grip := ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Center().Cursor(ui.CursorGrab).Label(fmt.Sprintf(core.Msg(c, "subtask_list.drag"), s.Title)) if !grip.IsDisabled() { grip.Drag(subtaskDrag(s.ID)) } grip.Children(func() { ui.Icon(c, icons.Must("grip-vertical")).Size(14, 14).TextColor(k.TextMuted) }) v := s.Done - cb := Checkbox(c, &v, s.Title, CheckboxOptions{Disabled: opts.Disabled}) + cb := input.Checkbox(c, &v, s.Title, input.CheckboxOptions{Disabled: opts.Disabled}) if cb.Changed() && !cb.IsDisabled() { s.Done, changed = v, true } @@ -115,7 +117,7 @@ func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) Choice } } ui.Spacer(c) - if b := iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "subtask_list.remove"), s.Title)); b.Clicked() && !b.IsDisabled() { + if b := core.IconAction(c, icons.Must("x"), fmt.Sprintf(core.Msg(c, "subtask_list.remove"), s.Title)); b.Clicked() && !b.IsDisabled() { removed = i } }) @@ -124,7 +126,7 @@ func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) Choice slog.Debug("mujica: subtask_list remove", "id", (*subs)[removed].ID) *subs, changed = slices.Delete(*subs, removed, removed+1), true } - in := TextInput(c, draft, TextInputOptions{Placeholder: msg(c, "subtask_list.add"), Label: msg(c, "subtask_list.add"), Disabled: opts.Disabled}) + in := input.TextInput(c, draft, input.TextInputOptions{Placeholder: core.Msg(c, "subtask_list.add"), Label: core.Msg(c, "subtask_list.add"), Disabled: opts.Disabled}) if in.Submitted() && strings.TrimSpace(*draft) != "" && !col.IsDisabled() { *subs = append(*subs, Subtask{ID: subtaskNewID(*subs), Title: strings.TrimSpace(*draft)}) *draft, changed = "", true @@ -138,7 +140,7 @@ func SubtaskList(c *ui.Context, subs *[]Subtask, opts SubtaskListOptions) Choice cache.ready = false c.Invalidate() } - return ChoiceResult{Element: col, changed: changed} + return core.NewChoiceResult(col, changed) } // subtaskNewID returns an ID no subtask uses. diff --git a/subtask_list_test.go b/project/subtask_list_test.go similarity index 96% rename from subtask_list_test.go rename to project/subtask_list_test.go index 8cf92ea..bc0384e 100644 --- a/subtask_list_test.go +++ b/project/subtask_list_test.go @@ -1,10 +1,12 @@ -package mujica +package project import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func subtaskSample() []Subtask { @@ -13,7 +15,7 @@ func subtaskSample() []Subtask { func subtaskTester(subs *[]Subtask, opts *SubtaskListOptions, n *int) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(16).Children(func() { if SubtaskList(c, subs, *opts).Changed() { *n++ diff --git a/task_detail_panel.go b/project/task_detail_panel.go similarity index 63% rename from task_detail_panel.go rename to project/task_detail_panel.go index 03c3dbb..d778c95 100644 --- a/task_detail_panel.go +++ b/project/task_detail_panel.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "log/slog" @@ -6,9 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TaskDetailPanelOptions configure a TaskDetailPanel. @@ -19,7 +21,7 @@ type TaskDetailPanelOptions struct { // Activity builds the caller's activity log below the fields. Activity func() // Today anchors the due date; zero uses the system clock. - Today Date + Today core.Date Disabled bool // Version changes when task or related data change in place. Version int @@ -41,8 +43,8 @@ func (r TaskDetailResult) Closed() bool { return r.closed } // TaskDetailPanel edits task fields and hosts an activity slot. func TaskDetailPanel(c *ui.Context, task *Task, opts TaskDetailPanelOptions) TaskDetailResult { projectStatus(opts.Statuses, task.Status) - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) r := TaskDetailResult{} panel := ui.Column(c).Gap(d.Unit()*4).Padding(d.Unit()*4).Background(k.Surface).Border(theme.BorderWidth, k.Border). Radius(theme.CardRadius).Role(ui.RoleGroup).Label(task.ID + " " + task.Title).Disabled(opts.Disabled) @@ -57,40 +59,40 @@ func TaskDetailPanel(c *ui.Context, task *Task, opts TaskDetailPanelOptions) Tas ui.Row(c).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { IssueIdBadge(c, task.ID, IssueIdBadgeOptions{Copyable: true, Disabled: opts.Disabled}) ui.Spacer(c) - if b := iconAction(c, icons.Must("x"), msg(c, "task_detail_panel.close")); b.Clicked() && !b.IsDisabled() { + if b := core.IconAction(c, icons.Must("x"), core.Msg(c, "task_detail_panel.close")); b.Clicked() && !b.IsDisabled() { r.closed = true } }) - title := TextInput(c, &task.Title, TextInputOptions{Label: msg(c, "task_detail_panel.title"), Disabled: opts.Disabled}) + title := input.TextInput(c, &task.Title, input.TextInputOptions{Label: core.Msg(c, "task_detail_panel.title"), Disabled: opts.Disabled}) title.Input.Font(theme.SerifFont) r.changed = title.Changed() - taskField(c, msg(c, "task_detail_panel.status"), func() { + taskField(c, core.Msg(c, "task_detail_panel.status"), func() { r.changed = StatusSelect(c, &task.Status, opts.Statuses, StatusSelectOptions{Disabled: opts.Disabled}).Changed() || r.changed }) - taskField(c, msg(c, "task_detail_panel.priority"), func() { - prios := make([]SelectOption[Priority], 0, 5) + taskField(c, core.Msg(c, "task_detail_panel.priority"), func() { + prios := make([]input.SelectOption[Priority], 0, 5) for p := PriorityUrgent; p >= PriorityNone; p-- { - prios = append(prios, SelectOption[Priority]{Value: p, Label: PriorityName(c, p)}) + prios = append(prios, input.SelectOption[Priority]{Value: p, Label: PriorityName(c, p)}) } - r.changed = Select(c, &task.Priority, prios, SelectOptions{Label: msg(c, "task_detail_panel.priority"), Disabled: opts.Disabled}).Changed() || r.changed + r.changed = input.Select(c, &task.Priority, prios, input.SelectOptions{Label: core.Msg(c, "task_detail_panel.priority"), Disabled: opts.Disabled}).Changed() || r.changed }) - taskField(c, msg(c, "assignee_picker.label"), func() { + taskField(c, core.Msg(c, "assignee_picker.label"), func() { r.changed = AssigneePicker(c, &task.Assignees, opts.Members, AssigneePickerOptions{Disabled: opts.Disabled, Version: opts.Version}).Changed() || r.changed }) - taskField(c, msg(c, "due_date_picker.label"), func() { + taskField(c, core.Msg(c, "due_date_picker.label"), func() { r.changed = DueDatePicker(c, &task.Due, DueDatePickerOptions{Done: task.Done, Today: opts.Today, Disabled: opts.Disabled}).Changed() || r.changed }) - taskField(c, msg(c, "label_manager.label"), func() { - options := make([]SelectOption[string], len(opts.Labels)) + taskField(c, core.Msg(c, "label_manager.label"), func() { + options := make([]input.SelectOption[string], len(opts.Labels)) for i, l := range opts.Labels { - options[i] = SelectOption[string]{Value: l.ID, Label: l.Name} + options[i] = input.SelectOption[string]{Value: l.ID, Label: l.Name} } - r.changed = MultiSelect(c, &task.Labels, options, MultiSelectOptions{Label: msg(c, "label_manager.label"), Disabled: opts.Disabled}).Changed() || r.changed + r.changed = input.MultiSelect(c, &task.Labels, options, input.MultiSelectOptions{Label: core.Msg(c, "label_manager.label"), Disabled: opts.Disabled}).Changed() || r.changed }) r.changed = SubtaskList(c, &task.Subtasks, SubtaskListOptions{Disabled: opts.Disabled, Version: opts.Version}).Changed() || r.changed if opts.Activity != nil { ui.Box(c).Height(16).FillWidth().Draw(func(p *ui.Painter, rr ui.Rect) { internal.RuleDiamond(p, rr, 8, 6, k.Ornament) }) - ui.Text(c, msg(c, "task_detail_panel.activity")).Font(theme.SerifFont).TextColor(k.Text) + ui.Text(c, core.Msg(c, "task_detail_panel.activity")).Font(theme.SerifFont).TextColor(k.Text) opts.Activity() } }) @@ -108,8 +110,8 @@ func TaskDetailPanel(c *ui.Context, task *Task, opts TaskDetailPanelOptions) Tas // taskField puts a caption above a control. func taskField(c *ui.Context, label string, control func()) { - ui.Column(c).Gap(Density(c).Unit()).Children(func() { - ui.Text(c, label).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted) + ui.Column(c).Gap(core.Density(c).Unit()).Children(func() { + ui.Text(c, label).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(core.Tokens(c).TextMuted) control() }) } diff --git a/task_detail_panel_test.go b/project/task_detail_panel_test.go similarity index 97% rename from task_detail_panel_test.go rename to project/task_detail_panel_test.go index 9ac7d55..52c4c07 100644 --- a/task_detail_panel_test.go +++ b/project/task_detail_panel_test.go @@ -1,10 +1,12 @@ -package mujica +package project import ( "fmt" "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type detailProbe struct { @@ -17,7 +19,7 @@ type detailProbe struct { func (p *detailProbe) tester() *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Scroll(c).Fill().Children(func() { ui.Box(c).Width(420).Children(func() { r := TaskDetailPanel(c, &p.task, TaskDetailPanelOptions{Statuses: projStatuses, Members: projMembers, Labels: projLabels, @@ -89,7 +91,7 @@ func TestTaskDetailPanelSnapshot(t *testing.T) { func TestTaskDetailPanelAncestorDisableClosesLabels(t *testing.T) { task, disabled := projTasks()[0], false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Disabled(disabled).Children(func() { TaskDetailPanel(c, &task, TaskDetailPanelOptions{Statuses: projStatuses, Members: projMembers, Labels: projLabels}) }) diff --git a/task_item.go b/project/task_item.go similarity index 72% rename from task_item.go rename to project/task_item.go index ad49947..2d74ac2 100644 --- a/task_item.go +++ b/project/task_item.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "log/slog" @@ -6,15 +6,18 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TaskItemOptions configure a TaskItem. type TaskItemOptions struct { Members []ProjectMember // Today anchors the overdue mark; zero uses the system clock. - Today Date + Today core.Date Disabled bool } @@ -30,9 +33,9 @@ func TaskItem(c *ui.Context, task *Task, opts TaskItemOptions) ProjectResult { for _, id := range task.Assignees { projectMember(opts.Members, id) } - k := Tokens(c) - d := Density(c) - today := datetimeToday(c, opts.Today) + k := core.Tokens(c) + d := core.Density(c) + today := core.DatetimeToday(c, opts.Today) r := ProjectResult{} row := ui.Row(c).MinHeight(d.ControlHeight()).Gap(d.Unit() * 3).AlignItems(ui.Center).PaddingX(d.Unit() * 2). Role(ui.RoleListItem).Label(task.Title).Disabled(opts.Disabled) @@ -43,12 +46,12 @@ func TaskItem(c *ui.Context, task *Task, opts TaskItemOptions) ProjectResult { } row.Children(func() { done := task.Done - if Checkbox(c, &done, "", CheckboxOptions{Disabled: opts.Disabled}).Label(task.Title).Changed() && !row.IsDisabled() { + if input.Checkbox(c, &done, "", input.CheckboxOptions{Disabled: opts.Disabled}).Label(task.Title).Changed() && !row.IsDisabled() { task.Done, r.changed = done, task.ID slog.Debug("mujica: task_item done", "id", task.ID, "done", done) } if edit.on && !row.IsDisabled() { - in := TextInput(c, &edit.draft, TextInputOptions{Label: msg(c, "task_item.rename")}) + in := input.TextInput(c, &edit.draft, input.TextInputOptions{Label: core.Msg(c, "task_item.rename")}) in.Element.Grow(1) in.Input.AutoFocus() switch { @@ -85,14 +88,14 @@ func TaskItem(c *ui.Context, task *Task, opts TaskItemOptions) ProjectResult { if late { fg = k.Danger } - ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(fg).SingleLine().Shrink(0) + ui.Text(c, text).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(fg).SingleLine().Shrink(0) } if len(task.Assignees) > 0 { - items := make([]AvatarItem, len(task.Assignees)) + items := make([]display.AvatarItem, len(task.Assignees)) for i, id := range task.Assignees { - items[i] = AvatarItem{Name: projectMember(opts.Members, id).Name} + items[i] = display.AvatarItem{Name: projectMember(opts.Members, id).Name} } - AvatarGroup(c, items, AvatarGroupOptions{Max: 2, Size: 24}) + display.AvatarGroup(c, items, display.AvatarGroupOptions{Max: 2, Size: 24}) } }) return r diff --git a/task_item_test.go b/project/task_item_test.go similarity index 96% rename from task_item_test.go rename to project/task_item_test.go index 3ac3266..65bf960 100644 --- a/task_item_test.go +++ b/project/task_item_test.go @@ -1,9 +1,11 @@ -package mujica +package project import ( "testing" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) type taskItemProbe struct { @@ -14,7 +16,7 @@ type taskItemProbe struct { func (p *taskItemProbe) tester() *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Padding(8).Children(func() { r := TaskItem(c, &p.task, TaskItemOptions{Members: projMembers, Today: calendarToday, Disabled: p.disabled}) if id, ok := r.Changed(); ok { diff --git a/task_list.go b/project/task_list.go similarity index 88% rename from task_list.go rename to project/task_list.go index 137b930..705a196 100644 --- a/task_list.go +++ b/project/task_list.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,8 +7,9 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/internal" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/internal" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TaskGrouping chooses the sections of a TaskList. @@ -38,7 +39,7 @@ type TaskListOptions struct { // Version must change when tasks change in place. Version int // Today anchors overdue marks; zero uses the system clock. - Today Date + Today core.Date Disabled bool Label string } @@ -58,7 +59,7 @@ type taskListCache struct { rows []taskRow counted [3]int statuses []WorkflowStatus - locale Locale + locale core.Locale } // TaskList groups and filters tasks, virtualizing visible rows. @@ -69,11 +70,11 @@ func TaskList(c *ui.Context, tasks []Task, filter *TaskFilter, opts TaskListOpti if opts.GroupBy == TaskGroupStatus && len(opts.Statuses) == 0 { panic("mujica: TaskList grouping by status needs Statuses") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "task_list.label") + label = core.Msg(c, "task_list.label") } r := ProjectResult{} root := ui.Column(c).Grow(1).MinHeight(0).Gap(d.Unit() * 2).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -83,8 +84,8 @@ func TaskList(c *ui.Context, tasks []Task, filter *TaskFilter, opts TaskListOpti if len(tasks) > 0 { first = &tasks[0] } - if x := *cache; x.rows == nil || x.first != first || x.n != len(tasks) || x.ver != opts.Version || x.group != opts.GroupBy || x.filter != *filter || !slices.Equal(x.statuses, opts.Statuses) || x.locale != current(c).locale { - *cache = &taskListCache{first: first, n: len(tasks), ver: opts.Version, group: opts.GroupBy, filter: *filter, statuses: slices.Clone(opts.Statuses), locale: current(c).locale} + if x := *cache; x.rows == nil || x.first != first || x.n != len(tasks) || x.ver != opts.Version || x.group != opts.GroupBy || x.filter != *filter || !slices.Equal(x.statuses, opts.Statuses) || x.locale != core.CurrentLocale(c) { + *cache = &taskListCache{first: first, n: len(tasks), ver: opts.Version, group: opts.GroupBy, filter: *filter, statuses: slices.Clone(opts.Statuses), locale: core.CurrentLocale(c)} (*cache).rows, (*cache).counted = taskListRows(c, tasks, opts, *filter) } rows := (*cache).rows @@ -101,9 +102,9 @@ func TaskList(c *ui.Context, tasks []Task, filter *TaskFilter, opts TaskListOpti counts := (*cache).counted sel := int(*filter) taskListFilter(c, &sel, []string{ - fmt.Sprintf(msg(c, "task_list.all"), counts[0]), - fmt.Sprintf(msg(c, "task_list.open"), counts[1]), - fmt.Sprintf(msg(c, "task_list.done"), counts[2]), + fmt.Sprintf(core.Msg(c, "task_list.all"), counts[0]), + fmt.Sprintf(core.Msg(c, "task_list.open"), counts[1]), + fmt.Sprintf(core.Msg(c, "task_list.done"), counts[2]), }) if sel != int(*filter) && !root.IsDisabled() { *filter = TaskFilter(sel) @@ -129,7 +130,7 @@ func TaskList(c *ui.Context, tasks []Task, filter *TaskFilter, opts TaskListOpti }).Grow(1).MinHeight(d.ControlHeight() * 4) list.Children(func() { if len(rows) == 0 { - ui.Text(c, msg(c, "task_list.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) + ui.Text(c, core.Msg(c, "task_list.empty")).TextColor(k.TextMuted).Padding(d.Unit() * 3) } }) }) @@ -188,10 +189,10 @@ func taskListSection(rows []taskRow, head string, keep []int, in func(int) bool) // taskListFilter marks the selected completion filter with an inset. func taskListFilter(c *ui.Context, value *int, labels []string) { - k, d := Tokens(c), Density(c) + k, d := core.Tokens(c), core.Density(c) parts := ui.SegmentedBase(c, value, len(labels)) parts.Track.MinHeight(max(d.ControlHeight(), theme.MinHitSize+4)).AlignItems(ui.Stretch).Padding(2).Gap(2).Radius(theme.ControlRadius+2). - Background(k.Background).Border(theme.BorderWidth, k.ControlBorder).Label(msg(c, "task_list.filter")) + Background(k.Background).Border(theme.BorderWidth, k.ControlBorder).Label(core.Msg(c, "task_list.filter")) parts.Track.Children(func() { for i, label := range labels { selected := i == *value @@ -207,7 +208,7 @@ func taskListFilter(c *ui.Context, value *int, labels []string) { fg = k.Text seg.Background(k.SurfaceHover) } - internal.Ease(seg, Motion(c, theme.StateDuration), fg) + internal.Ease(seg, core.Motion(c, theme.StateDuration), fg) internal.Ring(seg, k.Focus, theme.ControlRadius) seg.Children(func() { ui.Text(c, label).SingleLine() }) } diff --git a/task_list_test.go b/project/task_list_test.go similarity index 95% rename from task_list_test.go rename to project/task_list_test.go index fa72dd4..4ed2d17 100644 --- a/task_list_test.go +++ b/project/task_list_test.go @@ -1,11 +1,13 @@ -package mujica +package project import ( "fmt" "testing" "github.com/egoist/mygo/ui" - "mujicaui/theme" + + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) type taskListProbe struct { @@ -18,7 +20,7 @@ type taskListProbe struct { func (p *taskListProbe) tester() *ui.Tester { p.opts.Members, p.opts.Statuses, p.opts.Today = projMembers, projStatuses, calendarToday return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) ui.Column(c).Height(560).Padding(8).Children(func() { r := TaskList(c, p.tasks, &p.filter, p.opts) if id, ok := r.Changed(); ok { @@ -134,7 +136,7 @@ func TestTaskListFilterHitAreas(t *testing.T) { for _, density := range []theme.Density{theme.Compact, theme.Default, theme.Comfortable} { tasks, filter := projTasks(), TaskShowAll tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Density: density}) + core.Use(c, core.Settings{Density: density}) ui.Box(c).Height(300).Children(func() { TaskList(c, tasks, &filter, TaskListOptions{Members: projMembers}) }) }, 600, 320) for _, label := range []string{"All 5", "Open 4", "Done 1"} { diff --git a/time_tracker.go b/project/time_tracker.go similarity index 66% rename from time_tracker.go rename to project/time_tracker.go index b3baa35..122ab6d 100644 --- a/time_tracker.go +++ b/project/time_tracker.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "log/slog" @@ -6,8 +6,11 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/datetime" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/input" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // TimeEntry records a span; zero End means running. @@ -49,7 +52,7 @@ type TimeTrackerOptions struct { } // TimeTracker records spans and supports manual total corrections. -func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) ChoiceResult { +func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) core.ChoiceResult { now := opts.Now live := now.IsZero() if live { @@ -63,11 +66,11 @@ func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) Ch if s.Total(now) < 0 { panic("mujica: TimeTracker total must be non-negative") } - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) label := opts.Label if label == "" { - label = msg(c, "time_tracker.label") + label = core.Msg(c, "time_tracker.label") } changed := false row := ui.Row(c).Gap(d.Unit() * 3).AlignItems(ui.Center).Role(ui.RoleGroup).Label(label).Disabled(opts.Disabled) @@ -84,11 +87,11 @@ func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) Ch row.Children(func() { total := s.Total(now) running := s.Running() - icon, key, variant := "play", "time_tracker.start", Secondary + icon, key, variant := "play", "time_tracker.start", input.Secondary if running { - icon, key, variant = "pause", "time_tracker.stop", Primary + icon, key, variant = "pause", "time_tracker.stop", input.Primary } - if Button(c, msg(c, key), ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() { + if input.Button(c, core.Msg(c, key), input.ButtonOptions{Variant: variant, Icon: icons.Must(icon)}).Clicked() { act(key[len("time_tracker."):], func() { if running { s.Entries[len(s.Entries)-1].End = now @@ -98,12 +101,12 @@ func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) Ch }) } if *editing && !row.IsDisabled() { - DurationPicker(c, draft, DurationPickerOptions{Seconds: true, Label: msg(c, "time_tracker.correct")}) - if Button(c, msg(c, "time_tracker.apply"), ButtonOptions{}).Clicked() { + datetime.DurationPicker(c, draft, datetime.DurationPickerOptions{Seconds: true, Label: core.Msg(c, "time_tracker.correct")}) + if input.Button(c, core.Msg(c, "time_tracker.apply"), input.ButtonOptions{}).Clicked() { act("correct", func() { s.Adjust += *draft - total }) *editing = false } - if Button(c, msg(c, "calendar.cancel"), ButtonOptions{Variant: Ghost}).Clicked() { + if input.Button(c, core.Msg(c, "calendar.cancel"), input.ButtonOptions{Variant: input.Ghost}).Clicked() { *editing = false } return @@ -113,13 +116,13 @@ func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) Ch if running { fg = k.AccentText } - ui.Text(c, StopwatchText(total)).Font(theme.MonoFont).FontSize(FontSize(c, theme.H3Size)).TextColor(fg).FontFeatures("tnum"). - Label(label + ", " + DurationText(c, total.Truncate(time.Second))) + ui.Text(c, datetime.StopwatchText(total)).Font(theme.MonoFont).FontSize(core.FontSize(c, theme.H3Size)).TextColor(fg).FontFeatures("tnum"). + Label(label + ", " + datetime.DurationText(c, total.Truncate(time.Second))) if running { - ui.Text(c, msg(c, "time_tracker.running")).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) + ui.Text(c, core.Msg(c, "time_tracker.running")).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.AccentText) } ui.Box(c).Disabled(running).Children(func() { - if b := iconAction(c, icons.Must("pencil"), msg(c, "time_tracker.correct")); b.Clicked() && !b.IsDisabled() { + if b := core.IconAction(c, icons.Must("pencil"), core.Msg(c, "time_tracker.correct")); b.Clicked() && !b.IsDisabled() { *editing, *draft = true, total.Truncate(time.Second) } }) @@ -127,5 +130,5 @@ func TimeTracker(c *ui.Context, s *TimeTrackerState, opts TimeTrackerOptions) Ch if s.Running() && live { c.After(100*time.Millisecond - s.Total(now)%(100*time.Millisecond)) } - return ChoiceResult{Element: row, changed: changed} + return core.NewChoiceResult(row, changed) } diff --git a/time_tracker_test.go b/project/time_tracker_test.go similarity index 86% rename from time_tracker_test.go rename to project/time_tracker_test.go index 8da2e5d..45feeec 100644 --- a/time_tracker_test.go +++ b/project/time_tracker_test.go @@ -1,10 +1,12 @@ -package mujica +package project import ( "testing" "time" "github.com/egoist/mygo/ui" + + "github.com/ZacharyZhang-NY/MujicaUI/core" ) func TestTimeTrackerStartStopCorrect(t *testing.T) { @@ -12,7 +14,7 @@ func TestTimeTrackerStartStopCorrect(t *testing.T) { now := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC) n, disabled := 0, false tt := ui.NewTester(func(c *ui.Context) { - Use(c, Settings{}) + core.Use(c, core.Settings{}) if TimeTracker(c, &s, TimeTrackerOptions{Now: now, Disabled: disabled}).Changed() { n++ } @@ -60,7 +62,7 @@ func TestTimeTrackerInvalid(t *testing.T) { }() now := time.Now() s := TimeTrackerState{Entries: []TimeEntry{{Start: now}, {Start: now}}} - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); TimeTracker(c, &s, TimeTrackerOptions{Now: now}) }, 10, 10) + ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); TimeTracker(c, &s, TimeTrackerOptions{Now: now}) }, 10, 10) } func TestTimeTrackerSnapshot(t *testing.T) { @@ -84,7 +86,10 @@ func TestTimeTrackerRejectsInvalidSpans(t *testing.T) { t.Fatal("expected invalid span panic") } }() - ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); TimeTracker(c, &state, TimeTrackerOptions{Now: now}) }, 600, 200) + ui.NewTester(func(c *ui.Context) { + core.Use(c, core.Settings{}) + TimeTracker(c, &state, TimeTrackerOptions{Now: now}) + }, 600, 200) }) } } @@ -92,7 +97,7 @@ func TestTimeTrackerRejectsInvalidSpans(t *testing.T) { func TestTimeTrackerCorrectionRemovesFractionalRemainder(t *testing.T) { now := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.UTC) s := TimeTrackerState{Entries: []TimeEntry{{Start: now.Add(-30500 * time.Millisecond), End: now}}} - tt := ui.NewTester(func(c *ui.Context) { Use(c, Settings{}); TimeTracker(c, &s, TimeTrackerOptions{Now: now}) }, 900, 180) + tt := ui.NewTester(func(c *ui.Context) { core.Use(c, core.Settings{}); TimeTracker(c, &s, TimeTrackerOptions{Now: now}) }, 900, 180) if err := tt.Click("Correct the total"); err != nil { t.Fatal(err) } diff --git a/workload_view.go b/project/workload_view.go similarity index 84% rename from workload_view.go rename to project/workload_view.go index 3719bd3..9abd2c6 100644 --- a/workload_view.go +++ b/project/workload_view.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/icons" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/display" + "github.com/ZacharyZhang-NY/MujicaUI/icons" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) // Workload is one member's assigned and available hours. @@ -29,15 +31,15 @@ type WorkloadViewOptions struct { // WorkloadView virtualizes member loads with capacity and overload marks. func WorkloadView(c *ui.Context, loads []Workload, opts WorkloadViewOptions) *ui.Element { - k := Tokens(c) - d := Density(c) + k := core.Tokens(c) + d := core.Density(c) unit := opts.Unit if unit == "" { - unit = msg(c, "workload_view.hours") + unit = core.Msg(c, "workload_view.hours") } label := opts.Label if label == "" { - label = msg(c, "workload_view.label") + label = core.Msg(c, "workload_view.label") } root := ui.Column(c).Grow(1).MinHeight(0).Role(ui.RoleList).Label(label) cache := ui.Local(root, "workload-summary", func() workloadCache { return workloadCache{} }) @@ -59,7 +61,7 @@ func WorkloadView(c *ui.Context, loads []Workload, opts WorkloadViewOptions) *ui if over > 0 { ui.Row(c).Gap(d.Unit()).AlignItems(ui.Center).TextColor(k.Danger).Role(ui.RoleStatus).Padding(d.Unit()).Children(func() { ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14) - ui.Text(c, calendarCount(c, "workload_view.overloaded", over)).FontSize(FontSize(c, theme.CaptionSize)) + ui.Text(c, core.CalendarCount(c, "workload_view.overloaded", over)).FontSize(core.FontSize(c, theme.CaptionSize)) }) } ui.List(c, st, len(loads), func(i int) { @@ -68,11 +70,11 @@ func WorkloadView(c *ui.Context, loads []Workload, opts WorkloadViewOptions) *ui text := fmt.Sprintf("%s / %s %s", num(l.Assigned), num(l.Capacity), unit) note := "" if hot { - note = fmt.Sprintf(msg(c, "workload_view.over"), num(l.Assigned-l.Capacity), unit) + note = fmt.Sprintf(core.Msg(c, "workload_view.over"), num(l.Assigned-l.Capacity), unit) } ui.Row(c).MinHeight(d.ControlHeight()).Gap(d.Unit() * 3).AlignItems(ui.Center).PaddingX(d.Unit()).Role(ui.RoleListItem). Label(l.Member.Name + ", " + text + " " + note).Children(func() { - Avatar(c, l.Member.Name, AvatarOptions{Size: 24}) + display.Avatar(c, l.Member.Name, display.AvatarOptions{Size: 24}) ui.Text(c, l.Member.Name).Width(d.ControlHeight() * 4).Shrink(0).SingleLine().Ellipsis("…").TextColor(k.Text) ui.Box(c).Grow(1).Height(10).Draw(func(p *ui.Painter, r ui.Rect) { workloadBar(p, r, l, top, k.Border, k.AccentText, k.Danger, k.Text) }) fg := k.TextMuted @@ -80,7 +82,7 @@ func WorkloadView(c *ui.Context, loads []Workload, opts WorkloadViewOptions) *ui fg = k.Danger ui.Icon(c, icons.Must("triangle-alert")).Size(14, 14).TextColor(fg) } - ui.Text(c, text).Width(d.ControlHeight() * 3).Shrink(0).FontSize(FontSize(c, theme.CaptionSize)).TextColor(fg).FontFeatures("tnum").TextAlign(ui.End) + ui.Text(c, text).Width(d.ControlHeight() * 3).Shrink(0).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(fg).FontFeatures("tnum").TextAlign(ui.End) }) }).Grow(1).MinHeight(d.ControlHeight() * 3) }) diff --git a/workload_view_test.go b/project/workload_view_test.go similarity index 94% rename from workload_view_test.go rename to project/workload_view_test.go index 9f21e12..80d472a 100644 --- a/workload_view_test.go +++ b/project/workload_view_test.go @@ -1,4 +1,4 @@ -package mujica +package project import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/egoist/mygo/ui" - "mujicaui/theme" + "github.com/ZacharyZhang-NY/MujicaUI/core" + "github.com/ZacharyZhang-NY/MujicaUI/theme" ) func workloadSample() []Workload { @@ -17,7 +18,7 @@ func workloadSample() []Workload { func workloadTester(loads []Workload, opts WorkloadViewOptions) *ui.Tester { return ui.NewTester(func(c *ui.Context) { - Use(c, Settings{Mode: Light}) + core.Use(c, core.Settings{Mode: core.Light}) ui.Column(c).Height(300).Width(600).Children(func() { WorkloadView(c, loads, opts) }) }, 600, 300) } diff --git a/system_monitor.go b/system_monitor.go deleted file mode 100644 index 8b02c4c..0000000 --- a/system_monitor.go +++ /dev/null @@ -1,81 +0,0 @@ -package mujica - -import ( - "fmt" - "log/slog" - "math" - "time" - - "github.com/egoist/mygo/ui" - - "mujicaui/theme" -) - -// MonitorMetric holds samples aligned with the shared timestamps. -type MonitorMetric struct { - Name string - Unit string - Samples []float64 -} - -// SystemMonitorOptions configure a SystemMonitor. -type SystemMonitorOptions struct { - // Height of each chart; zero means 110. - Height float32 - Status DataStatus - Error string -} - -// SystemMonitor combines resource charts sampled by the caller. -func SystemMonitor(c *ui.Context, times []time.Time, metrics []MonitorMetric, opts SystemMonitorOptions) ChoiceResult { - if opts.Height < 0 || opts.Status != DataReady && opts.Status != DataLoading && opts.Status != DataFailed { - panic("mujica: SystemMonitor Height is negative") - } - for i, m := range metrics { - if m.Name == "" || len(m.Samples) != len(times) { - panic(fmt.Sprintf("mujica: SystemMonitor metric %d needs a Name and one sample per time", i)) - } - } - h := opts.Height - if h == 0 { - h = 110 - } - k := Tokens(c) - u := Density(c).Unit() - var r ChoiceResult - r.Element = ui.Column(c).Shrink(0).Gap(u * 3) - r.Element.Children(func() { - if len(metrics) == 0 { - ui.Column(c).Height(h).Children(func() { r.changed = dataPlaceholder(c, opts.Status, opts.Error, msg(c, "system_monitor.empty"), "") }) - return - } - for _, m := range metrics { - panel := ui.Column(c).Key(m.Name).Gap(u) - series := ui.Local(panel, "mujica.monitor.series", func() []ChartSeries { return []ChartSeries{{Name: m.Name, Values: m.Samples}} }) - (*series)[0].Name, (*series)[0].Values = m.Name, m.Samples - panel.Children(func() { - ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { - ui.Text(c, m.Name).FontWeight(600).Grow(1) - last := math.NaN() - if n := len(m.Samples); n > 0 { - last = m.Samples[n-1] - } - if !math.IsNaN(last) && opts.Status == DataReady { - ui.Text(c, fmt.Sprintf("%s %s", sandboxNum(last), m.Unit)).FontFeatures("tnum"). - FontSize(FontSize(c, theme.CaptionSize+1)).TextColor(k.Text).Role(ui.RoleStatus) - } - }) - unit := m.Unit - res := LineChart(c, *series, LineChartOptions{ - Frame: ChartFrame{Label: m.Name, Height: h, Status: opts.Status, Error: opts.Error}, - Times: times, Format: func(v float64) string { return sandboxNum(v) + " " + unit }, - }) - r.changed = r.changed || res.Retried() - }) - } - }) - if r.changed { - slog.Debug("mujica: system_monitor retry") - } - return r -} diff --git a/week_picker.go b/week_picker.go deleted file mode 100644 index d01665a..0000000 --- a/week_picker.go +++ /dev/null @@ -1,96 +0,0 @@ -package mujica - -import ( - "fmt" - "log/slog" - - "github.com/egoist/mygo/ui" - - "mujicaui/icons" -) - -// WeekPickerOptions configure a WeekPicker. -type WeekPickerOptions struct { - WeekStart WeekStart - // Min and Max bound the chosen week's days. - Min, Max Date - // Today is marked; zero uses the system clock. - Today Date - Clearable bool - Disabled bool - Label string -} - -// WeekPicker chooses a week; *value is its first day. -func WeekPicker(c *ui.Context, value *Date, opts WeekPickerOptions) ChoiceResult { - datetimeCheckDate("WeekPicker value", *value) - datetimeCheckBounds("WeekPicker", opts.Min, opts.Max) - first := calendarFirstWeekday(c, opts.WeekStart) - weekOf := func(d Date) Date { return d.AddDays(-((int(d.Weekday()) - int(first) + 7) % 7)) } - if !value.IsZero() && weekOf(*value) != *value { - panic(fmt.Sprintf("mujica: WeekPicker value %v does not start a week", *value)) - } - d := Density(c) - today := datetimeToday(c, opts.Today) - label := opts.Label - if label == "" { - label = msg(c, "week_picker.label") - } - enabled := func(x Date) bool { - w := weekOf(x) - return w.AddDays(6).within(opts.Min, Date{}) && w.within(Date{}, opts.Max) - } - col := ui.Column(c).Width(d.ControlHeight() * 8).Gap(d.Unit()).AlignItems(ui.Stretch).Disabled(opts.Disabled) - open := ui.Local(col, "week-picker-open", func() bool { return false }) - cursor := ui.Local(col, "week-picker-cursor", func() Date { return Date{} }) - hover := ui.Local(col, "week-picker-hover", func() Date { return Date{} }) - old := *value - text := "" - if !value.IsZero() { - text = weekViewTitle(c, *value, value.AddDays(6)) - } - col.Children(func() { - field := datetimeField(c, ui.Row(c), false, false) - field.Children(func() { - t := datetimeTrigger(c, text, msg(c, "week_picker.placeholder"), "calendar").Label(label).Description(text) - if !t.IsDisabled() && (t.Clicked() || !*open && t.Shortcut(0, ui.KeyDown)) { - *open, *hover = !*open, Date{} - *cursor = *value - if cursor.IsZero() { - *cursor = today - } - } - if opts.Clearable && !value.IsZero() { - if b := iconAction(c, icons.Must("x"), msg(c, "calendar.clear")); b.Clicked() && !b.IsDisabled() { - *value = Date{} - } - } - }) - datetimePopup(c, field, open, false, func() { - band := *hover - if band.IsZero() { - band = *cursor - } - picked, hovered := calendarMonth(c, cursor, calendarConfig{ - chosen: func(x Date) bool { return !value.IsZero() && weekOf(x) == *value }, - inRange: func(x Date) bool { return weekOf(x) == weekOf(band) }, - enabled: enabled, - today: today, - weekStart: opts.WeekStart, - autoFocus: true, - }) - if hovered != *hover { - *hover = hovered - c.Invalidate() - } - if !picked.IsZero() { - *value, *open = weekOf(picked), false - } - }) - }) - changed := *value != old - if changed { - slog.Debug("mujica: week_picker change", "from", old, "to", *value) - } - return ChoiceResult{Element: col, changed: changed} -}